Import Android SDK Platform PI [4335822]

/google/data/ro/projects/android/fetch_artifact \
    --bid 4335822 \
    --target sdk_phone_armv7-win_sdk \
    sdk-repo-linux-sources-4335822.zip

AndroidVersion.ApiLevel has been modified to appear as 28

Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
diff --git a/com/android/internal/alsa/AlsaCardsParser.java b/com/android/internal/alsa/AlsaCardsParser.java
new file mode 100644
index 0000000..5b92a17
--- /dev/null
+++ b/com/android/internal/alsa/AlsaCardsParser.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2014 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.internal.alsa;
+
+import android.util.Slog;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * @hide Retrieves information from an ALSA "cards" file.
+ */
+public class AlsaCardsParser {
+    private static final String TAG = "AlsaCardsParser";
+    protected static final boolean DEBUG = false;
+
+    private static final String kCardsFilePath = "/proc/asound/cards";
+
+    private static LineTokenizer mTokenizer = new LineTokenizer(" :[]");
+
+    private ArrayList<AlsaCardRecord> mCardRecords = new ArrayList<AlsaCardRecord>();
+
+    public class AlsaCardRecord {
+        private static final String TAG = "AlsaCardRecord";
+        private static final String kUsbCardKeyStr = "at usb-";
+
+        public int mCardNum = -1;
+        public String mField1 = "";
+        public String mCardName = "";
+        public String mCardDescription = "";
+        public boolean mIsUsb = false;
+
+        public AlsaCardRecord() {}
+
+        private boolean parse(String line, int lineIndex) {
+            int tokenIndex = 0;
+            int delimIndex = 0;
+
+            if (lineIndex == 0) {
+                // line # (skip)
+                tokenIndex = mTokenizer.nextToken(line, tokenIndex);
+                delimIndex = mTokenizer.nextDelimiter(line, tokenIndex);
+
+                try {
+                    // mCardNum
+                    mCardNum = Integer.parseInt(line.substring(tokenIndex, delimIndex));
+                } catch (NumberFormatException e) {
+                    Slog.e(TAG, "Failed to parse line " + lineIndex + " of " + kCardsFilePath
+                        + ": " + line.substring(tokenIndex, delimIndex));
+                    return false;
+                }
+
+                // mField1
+                tokenIndex = mTokenizer.nextToken(line, delimIndex);
+                delimIndex = mTokenizer.nextDelimiter(line, tokenIndex);
+                mField1 = line.substring(tokenIndex, delimIndex);
+
+                // mCardName
+                tokenIndex = mTokenizer.nextToken(line, delimIndex);
+                mCardName = line.substring(tokenIndex);
+
+                // done
+              } else if (lineIndex == 1) {
+                  tokenIndex = mTokenizer.nextToken(line, 0);
+                  if (tokenIndex != -1) {
+                      int keyIndex = line.indexOf(kUsbCardKeyStr);
+                      mIsUsb = keyIndex != -1;
+                      if (mIsUsb) {
+                          mCardDescription = line.substring(tokenIndex, keyIndex - 1);
+                      }
+                  }
+            }
+
+            return true;
+        }
+
+        public String textFormat() {
+          return mCardName + " : " + mCardDescription;
+        }
+
+        public void log(int listIndex) {
+            Slog.d(TAG, "" + listIndex +
+                " [" + mCardNum + " " + mCardName + " : " + mCardDescription +
+                " usb:" + mIsUsb);
+        }
+    }
+
+    public AlsaCardsParser() {}
+
+    public void scan() {
+        if (DEBUG) {
+            Slog.i(TAG, "AlsaCardsParser.scan()");
+        }
+        mCardRecords = new ArrayList<AlsaCardRecord>();
+
+        File cardsFile = new File(kCardsFilePath);
+        try {
+            FileReader reader = new FileReader(cardsFile);
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String line = "";
+            while ((line = bufferedReader.readLine()) != null) {
+                AlsaCardRecord cardRecord = new AlsaCardRecord();
+                if (DEBUG) {
+                    Slog.i(TAG, "  " + line);
+                }
+                cardRecord.parse(line, 0);
+
+                line = bufferedReader.readLine();
+                if (line == null) {
+                    break;
+                }
+                if (DEBUG) {
+                    Slog.i(TAG, "  " + line);
+                }
+                cardRecord.parse(line, 1);
+
+                mCardRecords.add(cardRecord);
+            }
+            reader.close();
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public ArrayList<AlsaCardRecord> getScanRecords() {
+        return mCardRecords;
+    }
+
+    public AlsaCardRecord getCardRecordAt(int index) {
+        return mCardRecords.get(index);
+    }
+
+    public AlsaCardRecord getCardRecordFor(int cardNum) {
+        for (AlsaCardRecord rec : mCardRecords) {
+            if (rec.mCardNum == cardNum) {
+                return rec;
+            }
+        }
+
+        return null;
+    }
+
+    public int getNumCardRecords() {
+        return mCardRecords.size();
+    }
+
+    public boolean isCardUsb(int cardNum) {
+        for (AlsaCardRecord rec : mCardRecords) {
+            if (rec.mCardNum == cardNum) {
+                return rec.mIsUsb;
+            }
+        }
+
+        return false;
+    }
+
+    // return -1 if none found
+    public int getDefaultUsbCard() {
+        // save the current list of devices
+        ArrayList<AlsaCardsParser.AlsaCardRecord> prevRecs = mCardRecords;
+        if (DEBUG) {
+            LogDevices("Previous Devices:", prevRecs);
+        }
+
+        // get the new list of devices
+        scan();
+        if (DEBUG) {
+            LogDevices("Current Devices:", mCardRecords);
+        }
+
+        // Calculate the difference between the old and new device list
+        ArrayList<AlsaCardRecord> newRecs = getNewCardRecords(prevRecs);
+        if (DEBUG) {
+            LogDevices("New Devices:", newRecs);
+        }
+
+        // Choose the most-recently added EXTERNAL card
+        // Check recently added devices
+        for (AlsaCardRecord rec : newRecs) {
+            if (DEBUG) {
+                Slog.d(TAG, rec.mCardName + " card:" + rec.mCardNum + " usb:" + rec.mIsUsb);
+            }
+            if (rec.mIsUsb) {
+                // Found it
+                return rec.mCardNum;
+            }
+        }
+
+        // or return the first added EXTERNAL card?
+        for (AlsaCardRecord rec : prevRecs) {
+            if (DEBUG) {
+                Slog.d(TAG, rec.mCardName + " card:" + rec.mCardNum + " usb:" + rec.mIsUsb);
+            }
+            if (rec.mIsUsb) {
+                return rec.mCardNum;
+            }
+        }
+
+        return -1;
+    }
+
+    public int getDefaultCard() {
+        // return an external card if possible
+        int card = getDefaultUsbCard();
+        if (DEBUG) {
+            Slog.d(TAG, "getDefaultCard() default usb card:" + card);
+        }
+
+        if (card < 0 && getNumCardRecords() > 0) {
+            // otherwise return the (internal) card with the highest number
+            card = getCardRecordAt(getNumCardRecords() - 1).mCardNum;
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "  returns card:" + card);
+        }
+        return card;
+    }
+
+    static public boolean hasCardNumber(ArrayList<AlsaCardRecord> recs, int cardNum) {
+        for (AlsaCardRecord cardRec : recs) {
+            if (cardRec.mCardNum == cardNum) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public ArrayList<AlsaCardRecord> getNewCardRecords(ArrayList<AlsaCardRecord> prevScanRecs) {
+        ArrayList<AlsaCardRecord> newRecs = new ArrayList<AlsaCardRecord>();
+        for (AlsaCardRecord rec : mCardRecords) {
+            // now scan to see if this card number is in the previous scan list
+            if (!hasCardNumber(prevScanRecs, rec.mCardNum)) {
+                newRecs.add(rec);
+            }
+        }
+        return newRecs;
+    }
+
+    //
+    // Logging
+    //
+    private void Log(String heading) {
+        if (DEBUG) {
+            Slog.i(TAG, heading);
+            for (AlsaCardRecord cardRec : mCardRecords) {
+                Slog.i(TAG, cardRec.textFormat());
+            }
+        }
+    }
+
+    static private void LogDevices(String caption, ArrayList<AlsaCardRecord> deviceList) {
+        Slog.d(TAG, caption + " ----------------");
+        int listIndex = 0;
+        for (AlsaCardRecord device : deviceList) {
+            device.log(listIndex++);
+        }
+        Slog.d(TAG, "----------------");
+    }
+}
diff --git a/com/android/internal/alsa/AlsaDevicesParser.java b/com/android/internal/alsa/AlsaDevicesParser.java
new file mode 100644
index 0000000..7cdd897
--- /dev/null
+++ b/com/android/internal/alsa/AlsaDevicesParser.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2014 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.internal.alsa;
+
+import android.util.Slog;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * @hide
+ * Retrieves information from an ALSA "devices" file.
+ */
+public class AlsaDevicesParser {
+    private static final String TAG = "AlsaDevicesParser";
+    protected static final boolean DEBUG = false;
+
+    private static final String kDevicesFilePath = "/proc/asound/devices";
+
+    private static final int kIndex_CardDeviceField = 5;
+    private static final int kStartIndex_CardNum = 6;
+    private static final int kEndIndex_CardNum = 8; // one past
+    private static final int kStartIndex_DeviceNum = 9;
+    private static final int kEndIndex_DeviceNum = 11; // one past
+    private static final int kStartIndex_Type = 14;
+
+    private static LineTokenizer mTokenizer = new LineTokenizer(" :[]-");
+
+    private boolean mHasCaptureDevices = false;
+    private boolean mHasPlaybackDevices = false;
+    private boolean mHasMIDIDevices = false;
+
+    public class AlsaDeviceRecord {
+        public static final int kDeviceType_Unknown = -1;
+        public static final int kDeviceType_Audio = 0;
+        public static final int kDeviceType_Control = 1;
+        public static final int kDeviceType_MIDI = 2;
+
+        public static final int kDeviceDir_Unknown = -1;
+        public static final int kDeviceDir_Capture = 0;
+        public static final int kDeviceDir_Playback = 1;
+
+        int mCardNum = -1;
+        int mDeviceNum = -1;
+        int mDeviceType = kDeviceType_Unknown;
+        int mDeviceDir = kDeviceDir_Unknown;
+
+        public AlsaDeviceRecord() {}
+
+        public boolean parse(String line) {
+            // "0123456789012345678901234567890"
+            // "  2: [ 0-31]: digital audio playback"
+            // "  3: [ 0-30]: digital audio capture"
+            // " 35: [ 1]   : control"
+            // " 36: [ 2- 0]: raw midi"
+
+            final int kToken_LineNum = 0;
+            final int kToken_CardNum = 1;
+            final int kToken_DeviceNum = 2;
+            final int kToken_Type0 = 3; // "digital", "control", "raw"
+            final int kToken_Type1 = 4; // "audio", "midi"
+            final int kToken_Type2 = 5; // "capture", "playback"
+
+            int tokenOffset = 0;
+            int delimOffset = 0;
+            int tokenIndex = kToken_LineNum;
+            while (true) {
+                tokenOffset = mTokenizer.nextToken(line, delimOffset);
+                if (tokenOffset == LineTokenizer.kTokenNotFound) {
+                    break; // bail
+                }
+                delimOffset = mTokenizer.nextDelimiter(line, tokenOffset);
+                if (delimOffset == LineTokenizer.kTokenNotFound) {
+                    delimOffset = line.length();
+                }
+                String token = line.substring(tokenOffset, delimOffset);
+
+                try {
+                    switch (tokenIndex) {
+                    case kToken_LineNum:
+                        // ignore
+                        break;
+
+                    case kToken_CardNum:
+                        mCardNum = Integer.parseInt(token);
+                        if (line.charAt(delimOffset) != '-') {
+                            tokenIndex++; // no device # in the token stream
+                        }
+                        break;
+
+                    case kToken_DeviceNum:
+                        mDeviceNum = Integer.parseInt(token);
+                        break;
+
+                    case kToken_Type0:
+                        if (token.equals("digital")) {
+                            // NOP
+                        } else if (token.equals("control")) {
+                            mDeviceType = kDeviceType_Control;
+                        } else if (token.equals("raw")) {
+                            // NOP
+                        }
+                        break;
+
+                    case kToken_Type1:
+                        if (token.equals("audio")) {
+                            mDeviceType = kDeviceType_Audio;
+                        } else if (token.equals("midi")) {
+                            mDeviceType = kDeviceType_MIDI;
+                            mHasMIDIDevices = true;
+                        }
+                        break;
+
+                    case kToken_Type2:
+                        if (token.equals("capture")) {
+                            mDeviceDir = kDeviceDir_Capture;
+                            mHasCaptureDevices = true;
+                        } else if (token.equals("playback")) {
+                            mDeviceDir = kDeviceDir_Playback;
+                            mHasPlaybackDevices = true;
+                        }
+                        break;
+                    } // switch (tokenIndex)
+                } catch (NumberFormatException e) {
+                    Slog.e(TAG, "Failed to parse token " + tokenIndex + " of " + kDevicesFilePath
+                        + " token: " + token);
+                    return false;
+                }
+
+                tokenIndex++;
+            } // while (true)
+
+            return true;
+        } // parse()
+
+        public String textFormat() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("[" + mCardNum + ":" + mDeviceNum + "]");
+
+            switch (mDeviceType) {
+            case kDeviceType_Unknown:
+            default:
+                sb.append(" N/A");
+                break;
+            case kDeviceType_Audio:
+                sb.append(" Audio");
+                break;
+            case kDeviceType_Control:
+                sb.append(" Control");
+                break;
+            case kDeviceType_MIDI:
+                sb.append(" MIDI");
+                break;
+            }
+
+            switch (mDeviceDir) {
+            case kDeviceDir_Unknown:
+            default:
+                sb.append(" N/A");
+                break;
+            case kDeviceDir_Capture:
+                sb.append(" Capture");
+                break;
+            case kDeviceDir_Playback:
+                sb.append(" Playback");
+                break;
+            }
+
+            return sb.toString();
+        }
+    }
+
+    private final ArrayList<AlsaDeviceRecord> mDeviceRecords = new ArrayList<AlsaDeviceRecord>();
+
+    public AlsaDevicesParser() {}
+
+    //
+    // Access
+    //
+    public int getDefaultDeviceNum(int card) {
+        // TODO - This (obviously) isn't sufficient. Revisit.
+        return 0;
+    }
+
+    //
+    // Predicates
+    //
+/*
+   public boolean hasPlaybackDevices() {
+        return mHasPlaybackDevices;
+    }
+*/
+
+    public boolean hasPlaybackDevices(int card) {
+        for (AlsaDeviceRecord deviceRecord : mDeviceRecords) {
+            if (deviceRecord.mCardNum == card &&
+                deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_Audio &&
+                deviceRecord.mDeviceDir == AlsaDeviceRecord.kDeviceDir_Playback) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+/*
+    public boolean hasCaptureDevices() {
+        return mHasCaptureDevices;
+    }
+*/
+
+    public boolean hasCaptureDevices(int card) {
+        for (AlsaDeviceRecord deviceRecord : mDeviceRecords) {
+            if (deviceRecord.mCardNum == card &&
+                deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_Audio &&
+                deviceRecord.mDeviceDir == AlsaDeviceRecord.kDeviceDir_Capture) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+/*
+    public boolean hasMIDIDevices() {
+        return mHasMIDIDevices;
+    }
+*/
+
+    public boolean hasMIDIDevices(int card) {
+        for (AlsaDeviceRecord deviceRecord : mDeviceRecords) {
+            if (deviceRecord.mCardNum == card &&
+                deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_MIDI) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    //
+    // Process
+    //
+    private boolean isLineDeviceRecord(String line) {
+        return line.charAt(kIndex_CardDeviceField) == '[';
+    }
+
+    public void scan() {
+        mDeviceRecords.clear();
+
+        File devicesFile = new File(kDevicesFilePath);
+        try {
+            FileReader reader = new FileReader(devicesFile);
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String line = "";
+            while ((line = bufferedReader.readLine()) != null) {
+                if (isLineDeviceRecord(line)) {
+                    AlsaDeviceRecord deviceRecord = new AlsaDeviceRecord();
+                    deviceRecord.parse(line);
+                    mDeviceRecords.add(deviceRecord);
+                }
+            }
+            reader.close();
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    //
+    // Loging
+    //
+    private void Log(String heading) {
+        if (DEBUG) {
+            Slog.i(TAG, heading);
+            for (AlsaDeviceRecord deviceRecord : mDeviceRecords) {
+                Slog.i(TAG, deviceRecord.textFormat());
+            }
+        }
+    }
+} // class AlsaDevicesParser
+
diff --git a/com/android/internal/alsa/LineTokenizer.java b/com/android/internal/alsa/LineTokenizer.java
new file mode 100644
index 0000000..b395da9
--- /dev/null
+++ b/com/android/internal/alsa/LineTokenizer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 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.internal.alsa;
+
+/**
+ * @hide
+ * Breaks lines in an ALSA "cards" or "devices" file into tokens.
+ * TODO(pmclean) Look into replacing this with String.split().
+ */
+public class LineTokenizer {
+    public static final int kTokenNotFound = -1;
+
+    private final String mDelimiters;
+
+    public LineTokenizer(String delimiters) {
+        mDelimiters = delimiters;
+    }
+
+    int nextToken(String line, int startIndex) {
+        int len = line.length();
+        int offset = startIndex;
+        for (; offset < len; offset++) {
+            if (mDelimiters.indexOf(line.charAt(offset)) == -1) {
+                // past a delimiter
+                break;
+            }
+      }
+
+      return offset < len ? offset : kTokenNotFound;
+    }
+
+    int nextDelimiter(String line, int startIndex) {
+        int len = line.length();
+        int offset = startIndex;
+        for (; offset < len; offset++) {
+            if (mDelimiters.indexOf(line.charAt(offset)) != -1) {
+                // past a delimiter
+                break;
+            }
+        }
+
+      return offset < len ? offset : kTokenNotFound;
+    }
+}
diff --git a/com/android/internal/annotations/GuardedBy.java b/com/android/internal/annotations/GuardedBy.java
new file mode 100644
index 0000000..fc61945
--- /dev/null
+++ b/com/android/internal/annotations/GuardedBy.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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.internal.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation type used to mark a method or field that can only be accessed when
+ * holding the referenced lock.
+ */
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.CLASS)
+public @interface GuardedBy {
+    String value();
+}
diff --git a/com/android/internal/annotations/Immutable.java b/com/android/internal/annotations/Immutable.java
new file mode 100644
index 0000000..b424275
--- /dev/null
+++ b/com/android/internal/annotations/Immutable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2012 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.internal.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation type used to mark a class which is immutable.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface Immutable {
+}
diff --git a/com/android/internal/annotations/VisibleForTesting.java b/com/android/internal/annotations/VisibleForTesting.java
new file mode 100644
index 0000000..99512ac
--- /dev/null
+++ b/com/android/internal/annotations/VisibleForTesting.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.internal.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Denotes that the class, method or field has its visibility relaxed so
+ * that unit tests can access it.
+ * <p/>
+ * The <code>visibility</code> argument can be used to specific what the original
+ * visibility should have been if it had not been made public or package-private for testing.
+ * The default is to consider the element private.
+ */
+@Retention(RetentionPolicy.CLASS)
+public @interface VisibleForTesting {
+    /**
+     * Intended visibility if the element had not been made public or package-private for
+     * testing.
+     */
+    enum Visibility {
+        /** The element should be considered protected. */
+        PROTECTED,
+        /** The element should be considered package-private. */
+        PACKAGE,
+        /** The element should be considered private. */
+        PRIVATE
+    }
+
+    /**
+     * Intended visibility if the element had not been made public or package-private for testing.
+     * If not specified, one should assume the element originally intended to be private.
+     */
+    Visibility visibility() default Visibility.PRIVATE;
+}
diff --git a/com/android/internal/app/AccessibilityButtonChooserActivity.java b/com/android/internal/app/AccessibilityButtonChooserActivity.java
new file mode 100644
index 0000000..b9ed963
--- /dev/null
+++ b/com/android/internal/app/AccessibilityButtonChooserActivity.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2017 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.internal.app;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.widget.ResolverDrawerLayout;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Activity used to display and persist a service or feature target for the Accessibility button.
+ */
+public class AccessibilityButtonChooserActivity extends Activity {
+
+    private static final String MAGNIFICATION_COMPONENT_ID =
+            "com.android.server.accessibility.MagnificationController";
+
+    private AccessibilityButtonTarget mMagnificationTarget = null;
+
+    private List<AccessibilityButtonTarget> mTargets = null;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.accessibility_button_chooser);
+
+        final ResolverDrawerLayout rdl = findViewById(R.id.contentPanel);
+        if (rdl != null) {
+            rdl.setOnDismissedListener(this::finish);
+        }
+
+        String component = Settings.Secure.getString(getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT);
+        if (TextUtils.isEmpty(component)) {
+            TextView prompt = findViewById(R.id.accessibility_button_prompt);
+            prompt.setVisibility(View.VISIBLE);
+        }
+
+        mMagnificationTarget = new AccessibilityButtonTarget(this, MAGNIFICATION_COMPONENT_ID,
+                R.string.accessibility_magnification_chooser_text,
+                R.drawable.ic_accessibility_magnification);
+
+        mTargets = getServiceAccessibilityButtonTargets(this);
+        if (Settings.Secure.getInt(getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1) {
+            mTargets.add(mMagnificationTarget);
+        }
+
+        if (mTargets.size() < 2) {
+            // Why are we here?
+            finish();
+        }
+
+        GridView gridview = findViewById(R.id.accessibility_button_chooser_grid);
+        gridview.setAdapter(new TargetAdapter());
+        gridview.setOnItemClickListener((parent, view, position, id) -> {
+            onTargetSelected(mTargets.get(position));
+        });
+    }
+
+    private static List<AccessibilityButtonTarget> getServiceAccessibilityButtonTargets(
+            @NonNull Context context) {
+        AccessibilityManager ams = (AccessibilityManager) context.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+        List<AccessibilityServiceInfo> services = ams.getEnabledAccessibilityServiceList(
+                AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+        if (services == null) {
+            return Collections.emptyList();
+        }
+
+        ArrayList<AccessibilityButtonTarget> targets = new ArrayList<>(services.size());
+        for (AccessibilityServiceInfo info : services) {
+            if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
+                targets.add(new AccessibilityButtonTarget(context, info));
+            }
+        }
+
+        return targets;
+    }
+
+    private void onTargetSelected(AccessibilityButtonTarget target) {
+        Settings.Secure.putString(getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, target.getId());
+        finish();
+    }
+
+    private class TargetAdapter extends BaseAdapter {
+        @Override
+        public int getCount() {
+            return mTargets.size();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return null;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            LayoutInflater inflater = AccessibilityButtonChooserActivity.this.getLayoutInflater();
+            View root = inflater.inflate(R.layout.accessibility_button_chooser_item, parent, false);
+            final AccessibilityButtonTarget target = mTargets.get(position);
+            ImageView iconView = root.findViewById(R.id.accessibility_button_target_icon);
+            TextView labelView = root.findViewById(R.id.accessibility_button_target_label);
+            iconView.setImageDrawable(target.getDrawable());
+            labelView.setText(target.getLabel());
+            return root;
+        }
+    }
+
+    private static class AccessibilityButtonTarget {
+        public String mId;
+        public CharSequence mLabel;
+        public Drawable mDrawable;
+
+        public AccessibilityButtonTarget(@NonNull Context context,
+                @NonNull AccessibilityServiceInfo serviceInfo) {
+            this.mId = serviceInfo.getComponentName().flattenToString();
+            this.mLabel = serviceInfo.getResolveInfo().loadLabel(context.getPackageManager());
+            this.mDrawable = serviceInfo.getResolveInfo().loadIcon(context.getPackageManager());
+        }
+
+        public AccessibilityButtonTarget(Context context, @NonNull String id, int labelResId,
+                int iconRes) {
+            this.mId = id;
+            this.mLabel = context.getText(labelResId);
+            this.mDrawable = context.getDrawable(iconRes);
+        }
+
+        public String getId() {
+            return mId;
+        }
+
+        public CharSequence getLabel() {
+            return mLabel;
+        }
+
+        public Drawable getDrawable() {
+            return mDrawable;
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/app/AlertActivity.java b/com/android/internal/app/AlertActivity.java
new file mode 100644
index 0000000..999a908
--- /dev/null
+++ b/com/android/internal/app/AlertActivity.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2007 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.internal.app;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * An activity that follows the visual style of an AlertDialog.
+ * 
+ * @see #mAlert
+ * @see #mAlertParams
+ * @see #setupAlert()
+ */
+public abstract class AlertActivity extends Activity implements DialogInterface {
+
+    /**
+     * The model for the alert.
+     * 
+     * @see #mAlertParams
+     */
+    protected AlertController mAlert;
+
+    /**
+     * The parameters for the alert.
+     */
+    protected AlertController.AlertParams mAlertParams;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mAlert = AlertController.create(this, this, getWindow());
+        mAlertParams = new AlertController.AlertParams(this);
+    }
+
+    public void cancel() {
+        finish();
+    }
+
+    public void dismiss() {
+        // This is called after the click, since we finish when handling the
+        // click, don't do that again here.
+        if (!isFinishing()) {
+            finish();
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        return dispatchPopulateAccessibilityEvent(this, event);
+    }
+
+    public static boolean dispatchPopulateAccessibilityEvent(Activity act,
+            AccessibilityEvent event) {
+        event.setClassName(Dialog.class.getName());
+        event.setPackageName(act.getPackageName());
+
+        ViewGroup.LayoutParams params = act.getWindow().getAttributes();
+        boolean isFullScreen = (params.width == ViewGroup.LayoutParams.MATCH_PARENT) &&
+                (params.height == ViewGroup.LayoutParams.MATCH_PARENT);
+        event.setFullScreen(isFullScreen);
+
+        return false;
+    }
+
+    /**
+     * Sets up the alert, including applying the parameters to the alert model,
+     * and installing the alert's content.
+     *
+     * @see #mAlert
+     * @see #mAlertParams
+     */
+    protected void setupAlert() {
+        mAlert.installContent(mAlertParams);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mAlert.onKeyDown(keyCode, event)) return true;
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (mAlert.onKeyUp(keyCode, event)) return true;
+        return super.onKeyUp(keyCode, event);
+    }
+}
diff --git a/com/android/internal/app/AlertController.java b/com/android/internal/app/AlertController.java
new file mode 100644
index 0000000..46cb546
--- /dev/null
+++ b/com/android/internal/app/AlertController.java
@@ -0,0 +1,1192 @@
+/*
+ * Copyright (C) 2008 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.internal.app;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import com.android.internal.R;
+
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewParent;
+import android.view.ViewStub;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckedTextView;
+import android.widget.CursorAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.ScrollView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+
+public class AlertController {
+    public static final int MICRO = 1;
+
+    private final Context mContext;
+    private final DialogInterface mDialogInterface;
+    protected final Window mWindow;
+
+    private CharSequence mTitle;
+    protected CharSequence mMessage;
+    protected ListView mListView;
+    private View mView;
+
+    private int mViewLayoutResId;
+
+    private int mViewSpacingLeft;
+    private int mViewSpacingTop;
+    private int mViewSpacingRight;
+    private int mViewSpacingBottom;
+    private boolean mViewSpacingSpecified = false;
+
+    private Button mButtonPositive;
+    private CharSequence mButtonPositiveText;
+    private Message mButtonPositiveMessage;
+
+    private Button mButtonNegative;
+    private CharSequence mButtonNegativeText;
+    private Message mButtonNegativeMessage;
+
+    private Button mButtonNeutral;
+    private CharSequence mButtonNeutralText;
+    private Message mButtonNeutralMessage;
+
+    protected ScrollView mScrollView;
+
+    private int mIconId = 0;
+    private Drawable mIcon;
+
+    private ImageView mIconView;
+    private TextView mTitleView;
+    protected TextView mMessageView;
+    private View mCustomTitleView;
+
+    private boolean mForceInverseBackground;
+
+    private ListAdapter mAdapter;
+
+    private int mCheckedItem = -1;
+
+    private int mAlertDialogLayout;
+    private int mButtonPanelSideLayout;
+    private int mListLayout;
+    private int mMultiChoiceItemLayout;
+    private int mSingleChoiceItemLayout;
+    private int mListItemLayout;
+
+    private boolean mShowTitle;
+
+    private int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE;
+
+    private Handler mHandler;
+
+    private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final Message m;
+            if (v == mButtonPositive && mButtonPositiveMessage != null) {
+                m = Message.obtain(mButtonPositiveMessage);
+            } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
+                m = Message.obtain(mButtonNegativeMessage);
+            } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
+                m = Message.obtain(mButtonNeutralMessage);
+            } else {
+                m = null;
+            }
+
+            if (m != null) {
+                m.sendToTarget();
+            }
+
+            // Post a message so we dismiss after the above handlers are executed
+            mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
+                    .sendToTarget();
+        }
+    };
+
+    private static final class ButtonHandler extends Handler {
+        // Button clicks have Message.what as the BUTTON{1,2,3} constant
+        private static final int MSG_DISMISS_DIALOG = 1;
+
+        private WeakReference<DialogInterface> mDialog;
+
+        public ButtonHandler(DialogInterface dialog) {
+            mDialog = new WeakReference<>(dialog);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+
+                case DialogInterface.BUTTON_POSITIVE:
+                case DialogInterface.BUTTON_NEGATIVE:
+                case DialogInterface.BUTTON_NEUTRAL:
+                    ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
+                    break;
+
+                case MSG_DISMISS_DIALOG:
+                    ((DialogInterface) msg.obj).dismiss();
+            }
+        }
+    }
+
+    private static boolean shouldCenterSingleButton(Context context) {
+        final TypedValue outValue = new TypedValue();
+        context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true);
+        return outValue.data != 0;
+    }
+
+    public static final AlertController create(Context context, DialogInterface di, Window window) {
+        final TypedArray a = context.obtainStyledAttributes(
+                null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
+        int controllerType = a.getInt(R.styleable.AlertDialog_controllerType, 0);
+        a.recycle();
+
+        switch (controllerType) {
+            case MICRO:
+                return new MicroAlertController(context, di, window);
+            default:
+                return new AlertController(context, di, window);
+        }
+    }
+
+    protected AlertController(Context context, DialogInterface di, Window window) {
+        mContext = context;
+        mDialogInterface = di;
+        mWindow = window;
+        mHandler = new ButtonHandler(di);
+
+        final TypedArray a = context.obtainStyledAttributes(null,
+                R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
+
+        mAlertDialogLayout = a.getResourceId(
+                R.styleable.AlertDialog_layout, R.layout.alert_dialog);
+        mButtonPanelSideLayout = a.getResourceId(
+                R.styleable.AlertDialog_buttonPanelSideLayout, 0);
+        mListLayout = a.getResourceId(
+                R.styleable.AlertDialog_listLayout, R.layout.select_dialog);
+
+        mMultiChoiceItemLayout = a.getResourceId(
+                R.styleable.AlertDialog_multiChoiceItemLayout,
+                R.layout.select_dialog_multichoice);
+        mSingleChoiceItemLayout = a.getResourceId(
+                R.styleable.AlertDialog_singleChoiceItemLayout,
+                R.layout.select_dialog_singlechoice);
+        mListItemLayout = a.getResourceId(
+                R.styleable.AlertDialog_listItemLayout,
+                R.layout.select_dialog_item);
+        mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);
+
+        a.recycle();
+
+        /* We use a custom title so never request a window title */
+        window.requestFeature(Window.FEATURE_NO_TITLE);
+    }
+
+    static boolean canTextInput(View v) {
+        if (v.onCheckIsTextEditor()) {
+            return true;
+        }
+
+        if (!(v instanceof ViewGroup)) {
+            return false;
+        }
+
+        ViewGroup vg = (ViewGroup)v;
+        int i = vg.getChildCount();
+        while (i > 0) {
+            i--;
+            v = vg.getChildAt(i);
+            if (canTextInput(v)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public void installContent(AlertParams params) {
+        params.apply(this);
+        installContent();
+    }
+
+    public void installContent() {
+        int contentView = selectContentView();
+        mWindow.setContentView(contentView);
+        setupView();
+    }
+
+    private int selectContentView() {
+        if (mButtonPanelSideLayout == 0) {
+            return mAlertDialogLayout;
+        }
+        if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
+            return mButtonPanelSideLayout;
+        }
+        // TODO: use layout hint side for long messages/lists
+        return mAlertDialogLayout;
+    }
+
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+        if (mTitleView != null) {
+            mTitleView.setText(title);
+        }
+    }
+
+    /**
+     * @see AlertDialog.Builder#setCustomTitle(View)
+     */
+    public void setCustomTitle(View customTitleView) {
+        mCustomTitleView = customTitleView;
+    }
+
+    public void setMessage(CharSequence message) {
+        mMessage = message;
+        if (mMessageView != null) {
+            mMessageView.setText(message);
+        }
+    }
+
+    /**
+     * Set the view resource to display in the dialog.
+     */
+    public void setView(int layoutResId) {
+        mView = null;
+        mViewLayoutResId = layoutResId;
+        mViewSpacingSpecified = false;
+    }
+
+    /**
+     * Set the view to display in the dialog.
+     */
+    public void setView(View view) {
+        mView = view;
+        mViewLayoutResId = 0;
+        mViewSpacingSpecified = false;
+    }
+
+    /**
+     * Set the view to display in the dialog along with the spacing around that view
+     */
+    public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight,
+            int viewSpacingBottom) {
+        mView = view;
+        mViewLayoutResId = 0;
+        mViewSpacingSpecified = true;
+        mViewSpacingLeft = viewSpacingLeft;
+        mViewSpacingTop = viewSpacingTop;
+        mViewSpacingRight = viewSpacingRight;
+        mViewSpacingBottom = viewSpacingBottom;
+    }
+
+    /**
+     * Sets a hint for the best button panel layout.
+     */
+    public void setButtonPanelLayoutHint(int layoutHint) {
+        mButtonPanelLayoutHint = layoutHint;
+    }
+
+    /**
+     * Sets a click listener or a message to be sent when the button is clicked.
+     * You only need to pass one of {@code listener} or {@code msg}.
+     *
+     * @param whichButton Which button, can be one of
+     *            {@link DialogInterface#BUTTON_POSITIVE},
+     *            {@link DialogInterface#BUTTON_NEGATIVE}, or
+     *            {@link DialogInterface#BUTTON_NEUTRAL}
+     * @param text The text to display in positive button.
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @param msg The {@link Message} to be sent when clicked.
+     */
+    public void setButton(int whichButton, CharSequence text,
+            DialogInterface.OnClickListener listener, Message msg) {
+
+        if (msg == null && listener != null) {
+            msg = mHandler.obtainMessage(whichButton, listener);
+        }
+
+        switch (whichButton) {
+
+            case DialogInterface.BUTTON_POSITIVE:
+                mButtonPositiveText = text;
+                mButtonPositiveMessage = msg;
+                break;
+
+            case DialogInterface.BUTTON_NEGATIVE:
+                mButtonNegativeText = text;
+                mButtonNegativeMessage = msg;
+                break;
+
+            case DialogInterface.BUTTON_NEUTRAL:
+                mButtonNeutralText = text;
+                mButtonNeutralMessage = msg;
+                break;
+
+            default:
+                throw new IllegalArgumentException("Button does not exist");
+        }
+    }
+
+    /**
+     * Specifies the icon to display next to the alert title.
+     *
+     * @param resId the resource identifier of the drawable to use as the icon,
+     *            or 0 for no icon
+     */
+    public void setIcon(int resId) {
+        mIcon = null;
+        mIconId = resId;
+
+        if (mIconView != null) {
+            if (resId != 0) {
+                mIconView.setVisibility(View.VISIBLE);
+                mIconView.setImageResource(mIconId);
+            } else {
+                mIconView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    /**
+     * Specifies the icon to display next to the alert title.
+     *
+     * @param icon the drawable to use as the icon or null for no icon
+     */
+    public void setIcon(Drawable icon) {
+        mIcon = icon;
+        mIconId = 0;
+
+        if (mIconView != null) {
+            if (icon != null) {
+                mIconView.setVisibility(View.VISIBLE);
+                mIconView.setImageDrawable(icon);
+            } else {
+                mIconView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    /**
+     * @param attrId the attributeId of the theme-specific drawable
+     * to resolve the resourceId for.
+     *
+     * @return resId the resourceId of the theme-specific drawable
+     */
+    public int getIconAttributeResId(int attrId) {
+        TypedValue out = new TypedValue();
+        mContext.getTheme().resolveAttribute(attrId, out, true);
+        return out.resourceId;
+    }
+
+    public void setInverseBackgroundForced(boolean forceInverseBackground) {
+        mForceInverseBackground = forceInverseBackground;
+    }
+
+    public ListView getListView() {
+        return mListView;
+    }
+
+    public Button getButton(int whichButton) {
+        switch (whichButton) {
+            case DialogInterface.BUTTON_POSITIVE:
+                return mButtonPositive;
+            case DialogInterface.BUTTON_NEGATIVE:
+                return mButtonNegative;
+            case DialogInterface.BUTTON_NEUTRAL:
+                return mButtonNeutral;
+            default:
+                return null;
+        }
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return mScrollView != null && mScrollView.executeKeyEvent(event);
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return mScrollView != null && mScrollView.executeKeyEvent(event);
+    }
+
+    /**
+     * Resolves whether a custom or default panel should be used. Removes the
+     * default panel if a custom panel should be used. If the resolved panel is
+     * a view stub, inflates before returning.
+     *
+     * @param customPanel the custom panel
+     * @param defaultPanel the default panel
+     * @return the panel to use
+     */
+    @Nullable
+    private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) {
+        if (customPanel == null) {
+            // Inflate the default panel, if needed.
+            if (defaultPanel instanceof ViewStub) {
+                defaultPanel = ((ViewStub) defaultPanel).inflate();
+            }
+
+            return (ViewGroup) defaultPanel;
+        }
+
+        // Remove the default panel entirely.
+        if (defaultPanel != null) {
+            final ViewParent parent = defaultPanel.getParent();
+            if (parent instanceof ViewGroup) {
+                ((ViewGroup) parent).removeView(defaultPanel);
+            }
+        }
+
+        // Inflate the custom panel, if needed.
+        if (customPanel instanceof ViewStub) {
+            customPanel = ((ViewStub) customPanel).inflate();
+        }
+
+        return (ViewGroup) customPanel;
+    }
+
+    private void setupView() {
+        final View parentPanel = mWindow.findViewById(R.id.parentPanel);
+        final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel);
+        final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel);
+        final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel);
+
+        // Install custom content before setting up the title or buttons so
+        // that we can handle panel overrides.
+        final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel);
+        setupCustomContent(customPanel);
+
+        final View customTopPanel = customPanel.findViewById(R.id.topPanel);
+        final View customContentPanel = customPanel.findViewById(R.id.contentPanel);
+        final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel);
+
+        // Resolve the correct panels and remove the defaults, if needed.
+        final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel);
+        final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel);
+        final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel);
+
+        setupContent(contentPanel);
+        setupButtons(buttonPanel);
+        setupTitle(topPanel);
+
+        final boolean hasCustomPanel = customPanel != null
+                && customPanel.getVisibility() != View.GONE;
+        final boolean hasTopPanel = topPanel != null
+                && topPanel.getVisibility() != View.GONE;
+        final boolean hasButtonPanel = buttonPanel != null
+                && buttonPanel.getVisibility() != View.GONE;
+
+        // Only display the text spacer if we don't have buttons.
+        if (!hasButtonPanel) {
+            if (contentPanel != null) {
+                final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons);
+                if (spacer != null) {
+                    spacer.setVisibility(View.VISIBLE);
+                }
+            }
+            mWindow.setCloseOnTouchOutsideIfNotSet(true);
+        }
+
+        if (hasTopPanel) {
+            // Only clip scrolling content to padding if we have a title.
+            if (mScrollView != null) {
+                mScrollView.setClipToPadding(true);
+            }
+
+            // Only show the divider if we have a title.
+            View divider = null;
+            if (mMessage != null || mListView != null || hasCustomPanel) {
+                if (!hasCustomPanel) {
+                    divider = topPanel.findViewById(R.id.titleDividerNoCustom);
+                }
+                if (divider == null) {
+                    divider = topPanel.findViewById(R.id.titleDivider);
+                }
+
+            } else {
+                divider = topPanel.findViewById(R.id.titleDividerTop);
+            }
+
+            if (divider != null) {
+                divider.setVisibility(View.VISIBLE);
+            }
+        } else {
+            if (contentPanel != null) {
+                final View spacer = contentPanel.findViewById(R.id.textSpacerNoTitle);
+                if (spacer != null) {
+                    spacer.setVisibility(View.VISIBLE);
+                }
+            }
+        }
+
+        if (mListView instanceof RecycleListView) {
+            ((RecycleListView) mListView).setHasDecor(hasTopPanel, hasButtonPanel);
+        }
+
+        // Update scroll indicators as needed.
+        if (!hasCustomPanel) {
+            final View content = mListView != null ? mListView : mScrollView;
+            if (content != null) {
+                final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0)
+                        | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0);
+                content.setScrollIndicators(indicators,
+                        View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
+            }
+        }
+
+        final TypedArray a = mContext.obtainStyledAttributes(
+                null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
+        setBackground(a, topPanel, contentPanel, customPanel, buttonPanel,
+                hasTopPanel, hasCustomPanel, hasButtonPanel);
+        a.recycle();
+    }
+
+    private void setupCustomContent(ViewGroup customPanel) {
+        final View customView;
+        if (mView != null) {
+            customView = mView;
+        } else if (mViewLayoutResId != 0) {
+            final LayoutInflater inflater = LayoutInflater.from(mContext);
+            customView = inflater.inflate(mViewLayoutResId, customPanel, false);
+        } else {
+            customView = null;
+        }
+
+        final boolean hasCustomView = customView != null;
+        if (!hasCustomView || !canTextInput(customView)) {
+            mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
+                    WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+        }
+
+        if (hasCustomView) {
+            final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom);
+            custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+            if (mViewSpacingSpecified) {
+                custom.setPadding(
+                        mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom);
+            }
+
+            if (mListView != null) {
+                ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0;
+            }
+        } else {
+            customPanel.setVisibility(View.GONE);
+        }
+    }
+
+    protected void setupTitle(ViewGroup topPanel) {
+        if (mCustomTitleView != null && mShowTitle) {
+            // Add the custom title view directly to the topPanel layout
+            final LayoutParams lp = new LayoutParams(
+                    LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+
+            topPanel.addView(mCustomTitleView, 0, lp);
+
+            // Hide the title template
+            final View titleTemplate = mWindow.findViewById(R.id.title_template);
+            titleTemplate.setVisibility(View.GONE);
+        } else {
+            mIconView = (ImageView) mWindow.findViewById(R.id.icon);
+
+            final boolean hasTextTitle = !TextUtils.isEmpty(mTitle);
+            if (hasTextTitle && mShowTitle) {
+                // Display the title if a title is supplied, else hide it.
+                mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle);
+                mTitleView.setText(mTitle);
+
+                // Do this last so that if the user has supplied any icons we
+                // use them instead of the default ones. If the user has
+                // specified 0 then make it disappear.
+                if (mIconId != 0) {
+                    mIconView.setImageResource(mIconId);
+                } else if (mIcon != null) {
+                    mIconView.setImageDrawable(mIcon);
+                } else {
+                    // Apply the padding from the icon to ensure the title is
+                    // aligned correctly.
+                    mTitleView.setPadding(mIconView.getPaddingLeft(),
+                            mIconView.getPaddingTop(),
+                            mIconView.getPaddingRight(),
+                            mIconView.getPaddingBottom());
+                    mIconView.setVisibility(View.GONE);
+                }
+            } else {
+                // Hide the title template
+                final View titleTemplate = mWindow.findViewById(R.id.title_template);
+                titleTemplate.setVisibility(View.GONE);
+                mIconView.setVisibility(View.GONE);
+                topPanel.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    protected void setupContent(ViewGroup contentPanel) {
+        mScrollView = (ScrollView) contentPanel.findViewById(R.id.scrollView);
+        mScrollView.setFocusable(false);
+
+        // Special case for users that only want to display a String
+        mMessageView = (TextView) contentPanel.findViewById(R.id.message);
+        if (mMessageView == null) {
+            return;
+        }
+
+        if (mMessage != null) {
+            mMessageView.setText(mMessage);
+        } else {
+            mMessageView.setVisibility(View.GONE);
+            mScrollView.removeView(mMessageView);
+
+            if (mListView != null) {
+                final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent();
+                final int childIndex = scrollParent.indexOfChild(mScrollView);
+                scrollParent.removeViewAt(childIndex);
+                scrollParent.addView(mListView, childIndex,
+                        new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+            } else {
+                contentPanel.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private static void manageScrollIndicators(View v, View upIndicator, View downIndicator) {
+        if (upIndicator != null) {
+            upIndicator.setVisibility(v.canScrollVertically(-1) ? View.VISIBLE : View.INVISIBLE);
+        }
+        if (downIndicator != null) {
+            downIndicator.setVisibility(v.canScrollVertically(1) ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    protected void setupButtons(ViewGroup buttonPanel) {
+        int BIT_BUTTON_POSITIVE = 1;
+        int BIT_BUTTON_NEGATIVE = 2;
+        int BIT_BUTTON_NEUTRAL = 4;
+        int whichButtons = 0;
+        mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1);
+        mButtonPositive.setOnClickListener(mButtonHandler);
+
+        if (TextUtils.isEmpty(mButtonPositiveText)) {
+            mButtonPositive.setVisibility(View.GONE);
+        } else {
+            mButtonPositive.setText(mButtonPositiveText);
+            mButtonPositive.setVisibility(View.VISIBLE);
+            whichButtons = whichButtons | BIT_BUTTON_POSITIVE;
+        }
+
+        mButtonNegative = (Button) buttonPanel.findViewById(R.id.button2);
+        mButtonNegative.setOnClickListener(mButtonHandler);
+
+        if (TextUtils.isEmpty(mButtonNegativeText)) {
+            mButtonNegative.setVisibility(View.GONE);
+        } else {
+            mButtonNegative.setText(mButtonNegativeText);
+            mButtonNegative.setVisibility(View.VISIBLE);
+
+            whichButtons = whichButtons | BIT_BUTTON_NEGATIVE;
+        }
+
+        mButtonNeutral = (Button) buttonPanel.findViewById(R.id.button3);
+        mButtonNeutral.setOnClickListener(mButtonHandler);
+
+        if (TextUtils.isEmpty(mButtonNeutralText)) {
+            mButtonNeutral.setVisibility(View.GONE);
+        } else {
+            mButtonNeutral.setText(mButtonNeutralText);
+            mButtonNeutral.setVisibility(View.VISIBLE);
+
+            whichButtons = whichButtons | BIT_BUTTON_NEUTRAL;
+        }
+
+        if (shouldCenterSingleButton(mContext)) {
+            /*
+             * If we only have 1 button it should be centered on the layout and
+             * expand to fill 50% of the available space.
+             */
+            if (whichButtons == BIT_BUTTON_POSITIVE) {
+                centerButton(mButtonPositive);
+            } else if (whichButtons == BIT_BUTTON_NEGATIVE) {
+                centerButton(mButtonNegative);
+            } else if (whichButtons == BIT_BUTTON_NEUTRAL) {
+                centerButton(mButtonNeutral);
+            }
+        }
+
+        final boolean hasButtons = whichButtons != 0;
+        if (!hasButtons) {
+            buttonPanel.setVisibility(View.GONE);
+        }
+    }
+
+    private void centerButton(Button button) {
+        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) button.getLayoutParams();
+        params.gravity = Gravity.CENTER_HORIZONTAL;
+        params.weight = 0.5f;
+        button.setLayoutParams(params);
+        View leftSpacer = mWindow.findViewById(R.id.leftSpacer);
+        if (leftSpacer != null) {
+            leftSpacer.setVisibility(View.VISIBLE);
+        }
+        View rightSpacer = mWindow.findViewById(R.id.rightSpacer);
+        if (rightSpacer != null) {
+            rightSpacer.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel,
+            View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) {
+        int fullDark = 0;
+        int topDark = 0;
+        int centerDark = 0;
+        int bottomDark = 0;
+        int fullBright = 0;
+        int topBright = 0;
+        int centerBright = 0;
+        int bottomBright = 0;
+        int bottomMedium = 0;
+
+        // If the needsDefaultBackgrounds attribute is set, we know we're
+        // inheriting from a framework style.
+        final boolean needsDefaultBackgrounds = a.getBoolean(
+                R.styleable.AlertDialog_needsDefaultBackgrounds, true);
+        if (needsDefaultBackgrounds) {
+            fullDark = R.drawable.popup_full_dark;
+            topDark = R.drawable.popup_top_dark;
+            centerDark = R.drawable.popup_center_dark;
+            bottomDark = R.drawable.popup_bottom_dark;
+            fullBright = R.drawable.popup_full_bright;
+            topBright = R.drawable.popup_top_bright;
+            centerBright = R.drawable.popup_center_bright;
+            bottomBright = R.drawable.popup_bottom_bright;
+            bottomMedium = R.drawable.popup_bottom_medium;
+        }
+
+        topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright);
+        topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark);
+        centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright);
+        centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark);
+
+        /* We now set the background of all of the sections of the alert.
+         * First collect together each section that is being displayed along
+         * with whether it is on a light or dark background, then run through
+         * them setting their backgrounds.  This is complicated because we need
+         * to correctly use the full, top, middle, and bottom graphics depending
+         * on how many views they are and where they appear.
+         */
+
+        final View[] views = new View[4];
+        final boolean[] light = new boolean[4];
+        View lastView = null;
+        boolean lastLight = false;
+
+        int pos = 0;
+        if (hasTitle) {
+            views[pos] = topPanel;
+            light[pos] = false;
+            pos++;
+        }
+
+        /* The contentPanel displays either a custom text message or
+         * a ListView. If it's text we should use the dark background
+         * for ListView we should use the light background. If neither
+         * are there the contentPanel will be hidden so set it as null.
+         */
+        views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel;
+        light[pos] = mListView != null;
+        pos++;
+
+        if (hasCustomView) {
+            views[pos] = customPanel;
+            light[pos] = mForceInverseBackground;
+            pos++;
+        }
+
+        if (hasButtons) {
+            views[pos] = buttonPanel;
+            light[pos] = true;
+        }
+
+        boolean setView = false;
+        for (pos = 0; pos < views.length; pos++) {
+            final View v = views[pos];
+            if (v == null) {
+                continue;
+            }
+
+            if (lastView != null) {
+                if (!setView) {
+                    lastView.setBackgroundResource(lastLight ? topBright : topDark);
+                } else {
+                    lastView.setBackgroundResource(lastLight ? centerBright : centerDark);
+                }
+                setView = true;
+            }
+
+            lastView = v;
+            lastLight = light[pos];
+        }
+
+        if (lastView != null) {
+            if (setView) {
+                bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright);
+                bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium);
+                bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark);
+
+                // ListViews will use the Bright background, but buttons use the
+                // Medium background.
+                lastView.setBackgroundResource(
+                        lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark);
+            } else {
+                fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright);
+                fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark);
+
+                lastView.setBackgroundResource(lastLight ? fullBright : fullDark);
+            }
+        }
+
+        final ListView listView = mListView;
+        if (listView != null && mAdapter != null) {
+            listView.setAdapter(mAdapter);
+            final int checkedItem = mCheckedItem;
+            if (checkedItem > -1) {
+                listView.setItemChecked(checkedItem, true);
+                listView.setSelectionFromTop(checkedItem,
+                        a.getDimensionPixelSize(R.styleable.AlertDialog_selectionScrollOffset, 0));
+            }
+        }
+    }
+
+    public static class RecycleListView extends ListView {
+        private final int mPaddingTopNoTitle;
+        private final int mPaddingBottomNoButtons;
+
+        boolean mRecycleOnMeasure = true;
+
+        public RecycleListView(Context context) {
+            this(context, null);
+        }
+
+        public RecycleListView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+
+            final TypedArray ta = context.obtainStyledAttributes(
+                    attrs, R.styleable.RecycleListView);
+            mPaddingBottomNoButtons = ta.getDimensionPixelOffset(
+                    R.styleable.RecycleListView_paddingBottomNoButtons, -1);
+            mPaddingTopNoTitle = ta.getDimensionPixelOffset(
+                    R.styleable.RecycleListView_paddingTopNoTitle, -1);
+        }
+
+        public void setHasDecor(boolean hasTitle, boolean hasButtons) {
+            if (!hasButtons || !hasTitle) {
+                final int paddingLeft = getPaddingLeft();
+                final int paddingTop = hasTitle ? getPaddingTop() : mPaddingTopNoTitle;
+                final int paddingRight = getPaddingRight();
+                final int paddingBottom = hasButtons ? getPaddingBottom() : mPaddingBottomNoButtons;
+                setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+            }
+        }
+
+        @Override
+        protected boolean recycleOnMeasure() {
+            return mRecycleOnMeasure;
+        }
+    }
+
+    public static class AlertParams {
+        public final Context mContext;
+        public final LayoutInflater mInflater;
+
+        public int mIconId = 0;
+        public Drawable mIcon;
+        public int mIconAttrId = 0;
+        public CharSequence mTitle;
+        public View mCustomTitleView;
+        public CharSequence mMessage;
+        public CharSequence mPositiveButtonText;
+        public DialogInterface.OnClickListener mPositiveButtonListener;
+        public CharSequence mNegativeButtonText;
+        public DialogInterface.OnClickListener mNegativeButtonListener;
+        public CharSequence mNeutralButtonText;
+        public DialogInterface.OnClickListener mNeutralButtonListener;
+        public boolean mCancelable;
+        public DialogInterface.OnCancelListener mOnCancelListener;
+        public DialogInterface.OnDismissListener mOnDismissListener;
+        public DialogInterface.OnKeyListener mOnKeyListener;
+        public CharSequence[] mItems;
+        public ListAdapter mAdapter;
+        public DialogInterface.OnClickListener mOnClickListener;
+        public int mViewLayoutResId;
+        public View mView;
+        public int mViewSpacingLeft;
+        public int mViewSpacingTop;
+        public int mViewSpacingRight;
+        public int mViewSpacingBottom;
+        public boolean mViewSpacingSpecified = false;
+        public boolean[] mCheckedItems;
+        public boolean mIsMultiChoice;
+        public boolean mIsSingleChoice;
+        public int mCheckedItem = -1;
+        public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener;
+        public Cursor mCursor;
+        public String mLabelColumn;
+        public String mIsCheckedColumn;
+        public boolean mForceInverseBackground;
+        public AdapterView.OnItemSelectedListener mOnItemSelectedListener;
+        public OnPrepareListViewListener mOnPrepareListViewListener;
+        public boolean mRecycleOnMeasure = true;
+
+        /**
+         * Interface definition for a callback to be invoked before the ListView
+         * will be bound to an adapter.
+         */
+        public interface OnPrepareListViewListener {
+
+            /**
+             * Called before the ListView is bound to an adapter.
+             * @param listView The ListView that will be shown in the dialog.
+             */
+            void onPrepareListView(ListView listView);
+        }
+
+        public AlertParams(Context context) {
+            mContext = context;
+            mCancelable = true;
+            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        }
+
+        public void apply(AlertController dialog) {
+            if (mCustomTitleView != null) {
+                dialog.setCustomTitle(mCustomTitleView);
+            } else {
+                if (mTitle != null) {
+                    dialog.setTitle(mTitle);
+                }
+                if (mIcon != null) {
+                    dialog.setIcon(mIcon);
+                }
+                if (mIconId != 0) {
+                    dialog.setIcon(mIconId);
+                }
+                if (mIconAttrId != 0) {
+                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
+                }
+            }
+            if (mMessage != null) {
+                dialog.setMessage(mMessage);
+            }
+            if (mPositiveButtonText != null) {
+                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
+                        mPositiveButtonListener, null);
+            }
+            if (mNegativeButtonText != null) {
+                dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
+                        mNegativeButtonListener, null);
+            }
+            if (mNeutralButtonText != null) {
+                dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
+                        mNeutralButtonListener, null);
+            }
+            if (mForceInverseBackground) {
+                dialog.setInverseBackgroundForced(true);
+            }
+            // For a list, the client can either supply an array of items or an
+            // adapter or a cursor
+            if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
+                createListView(dialog);
+            }
+            if (mView != null) {
+                if (mViewSpacingSpecified) {
+                    dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
+                            mViewSpacingBottom);
+                } else {
+                    dialog.setView(mView);
+                }
+            } else if (mViewLayoutResId != 0) {
+                dialog.setView(mViewLayoutResId);
+            }
+
+            /*
+            dialog.setCancelable(mCancelable);
+            dialog.setOnCancelListener(mOnCancelListener);
+            if (mOnKeyListener != null) {
+                dialog.setOnKeyListener(mOnKeyListener);
+            }
+            */
+        }
+
+        private void createListView(final AlertController dialog) {
+            final RecycleListView listView =
+                    (RecycleListView) mInflater.inflate(dialog.mListLayout, null);
+            final ListAdapter adapter;
+
+            if (mIsMultiChoice) {
+                if (mCursor == null) {
+                    adapter = new ArrayAdapter<CharSequence>(
+                            mContext, dialog.mMultiChoiceItemLayout, R.id.text1, mItems) {
+                        @Override
+                        public View getView(int position, View convertView, ViewGroup parent) {
+                            View view = super.getView(position, convertView, parent);
+                            if (mCheckedItems != null) {
+                                boolean isItemChecked = mCheckedItems[position];
+                                if (isItemChecked) {
+                                    listView.setItemChecked(position, true);
+                                }
+                            }
+                            return view;
+                        }
+                    };
+                } else {
+                    adapter = new CursorAdapter(mContext, mCursor, false) {
+                        private final int mLabelIndex;
+                        private final int mIsCheckedIndex;
+
+                        {
+                            final Cursor cursor = getCursor();
+                            mLabelIndex = cursor.getColumnIndexOrThrow(mLabelColumn);
+                            mIsCheckedIndex = cursor.getColumnIndexOrThrow(mIsCheckedColumn);
+                        }
+
+                        @Override
+                        public void bindView(View view, Context context, Cursor cursor) {
+                            CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1);
+                            text.setText(cursor.getString(mLabelIndex));
+                            listView.setItemChecked(
+                                    cursor.getPosition(),
+                                    cursor.getInt(mIsCheckedIndex) == 1);
+                        }
+
+                        @Override
+                        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+                            return mInflater.inflate(dialog.mMultiChoiceItemLayout,
+                                    parent, false);
+                        }
+
+                    };
+                }
+            } else {
+                final int layout;
+                if (mIsSingleChoice) {
+                    layout = dialog.mSingleChoiceItemLayout;
+                } else {
+                    layout = dialog.mListItemLayout;
+                }
+
+                if (mCursor != null) {
+                    adapter = new SimpleCursorAdapter(mContext, layout, mCursor,
+                            new String[] { mLabelColumn }, new int[] { R.id.text1 });
+                } else if (mAdapter != null) {
+                    adapter = mAdapter;
+                } else {
+                    adapter = new CheckedItemAdapter(mContext, layout, R.id.text1, mItems);
+                }
+            }
+
+            if (mOnPrepareListViewListener != null) {
+                mOnPrepareListViewListener.onPrepareListView(listView);
+            }
+
+            /* Don't directly set the adapter on the ListView as we might
+             * want to add a footer to the ListView later.
+             */
+            dialog.mAdapter = adapter;
+            dialog.mCheckedItem = mCheckedItem;
+
+            if (mOnClickListener != null) {
+                listView.setOnItemClickListener(new OnItemClickListener() {
+                    @Override
+                    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+                        mOnClickListener.onClick(dialog.mDialogInterface, position);
+                        if (!mIsSingleChoice) {
+                            dialog.mDialogInterface.dismiss();
+                        }
+                    }
+                });
+            } else if (mOnCheckboxClickListener != null) {
+                listView.setOnItemClickListener(new OnItemClickListener() {
+                    @Override
+                    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+                        if (mCheckedItems != null) {
+                            mCheckedItems[position] = listView.isItemChecked(position);
+                        }
+                        mOnCheckboxClickListener.onClick(
+                                dialog.mDialogInterface, position, listView.isItemChecked(position));
+                    }
+                });
+            }
+
+            // Attach a given OnItemSelectedListener to the ListView
+            if (mOnItemSelectedListener != null) {
+                listView.setOnItemSelectedListener(mOnItemSelectedListener);
+            }
+
+            if (mIsSingleChoice) {
+                listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+            } else if (mIsMultiChoice) {
+                listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+            }
+            listView.mRecycleOnMeasure = mRecycleOnMeasure;
+            dialog.mListView = listView;
+        }
+    }
+
+    private static class CheckedItemAdapter extends ArrayAdapter<CharSequence> {
+        public CheckedItemAdapter(Context context, int resource, int textViewResourceId,
+                CharSequence[] objects) {
+            super(context, resource, textViewResourceId, objects);
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+    }
+}
diff --git a/com/android/internal/app/AssistUtils.java b/com/android/internal/app/AssistUtils.java
new file mode 100644
index 0000000..2940079
--- /dev/null
+++ b/com/android/internal/app/AssistUtils.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2015 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.internal.app;
+
+import com.android.internal.R;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.Settings;
+import android.util.Log;
+
+/**
+ * Utility method for dealing with the assistant aspects of
+ * {@link com.android.internal.app.IVoiceInteractionManagerService IVoiceInteractionManagerService}.
+ */
+public class AssistUtils {
+
+    private static final String TAG = "AssistUtils";
+
+    private final Context mContext;
+    private final IVoiceInteractionManagerService mVoiceInteractionManagerService;
+
+    public AssistUtils(Context context) {
+        mContext = context;
+        mVoiceInteractionManagerService = IVoiceInteractionManagerService.Stub.asInterface(
+                ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+    }
+
+    public boolean showSessionForActiveService(Bundle args, int sourceFlags,
+            IVoiceInteractionSessionShowCallback showCallback, IBinder activityToken) {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                return mVoiceInteractionManagerService.showSessionForActiveService(args,
+                        sourceFlags, showCallback, activityToken);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call showSessionForActiveService", e);
+        }
+        return false;
+    }
+
+    public void launchVoiceAssistFromKeyguard() {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                mVoiceInteractionManagerService.launchVoiceAssistFromKeyguard();
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call launchVoiceAssistFromKeyguard", e);
+        }
+    }
+
+    public boolean activeServiceSupportsAssistGesture() {
+        try {
+            return mVoiceInteractionManagerService != null
+                    && mVoiceInteractionManagerService.activeServiceSupportsAssist();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call activeServiceSupportsAssistGesture", e);
+            return false;
+        }
+    }
+
+    public boolean activeServiceSupportsLaunchFromKeyguard() {
+        try {
+            return mVoiceInteractionManagerService != null
+                    && mVoiceInteractionManagerService.activeServiceSupportsLaunchFromKeyguard();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call activeServiceSupportsLaunchFromKeyguard", e);
+            return false;
+        }
+    }
+
+    public ComponentName getActiveServiceComponentName() {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                return mVoiceInteractionManagerService.getActiveServiceComponentName();
+            } else {
+                return null;
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call getActiveServiceComponentName", e);
+            return null;
+        }
+    }
+
+    public boolean isSessionRunning() {
+        try {
+            return mVoiceInteractionManagerService != null
+                    && mVoiceInteractionManagerService.isSessionRunning();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call isSessionRunning", e);
+            return false;
+        }
+    }
+
+    public void hideCurrentSession() {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                mVoiceInteractionManagerService.hideCurrentSession();
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call hideCurrentSession", e);
+        }
+    }
+
+    public void onLockscreenShown() {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                mVoiceInteractionManagerService.onLockscreenShown();
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call onLockscreenShown", e);
+        }
+    }
+
+    public void registerVoiceInteractionSessionListener(IVoiceInteractionSessionListener listener) {
+        try {
+            if (mVoiceInteractionManagerService != null) {
+                mVoiceInteractionManagerService.registerVoiceInteractionSessionListener(listener);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to register voice interaction listener", e);
+        }
+    }
+
+    public ComponentName getAssistComponentForUser(int userId) {
+        final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.ASSISTANT, userId);
+        if (setting != null) {
+            return ComponentName.unflattenFromString(setting);
+        }
+
+        // Fallback to keep backward compatible behavior when there is no user setting.
+        if (activeServiceSupportsAssistGesture()) {
+            return getActiveServiceComponentName();
+        }
+
+        Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
+                .getAssistIntent(false);
+        PackageManager pm = mContext.getPackageManager();
+        ResolveInfo info = pm.resolveActivityAsUser(intent, PackageManager.MATCH_DEFAULT_ONLY,
+                userId);
+        if (info != null) {
+            return new ComponentName(info.activityInfo.applicationInfo.packageName,
+                    info.activityInfo.name);
+        }
+        return null;
+    }
+
+    public static boolean isPreinstalledAssistant(Context context, ComponentName assistant) {
+        if (assistant == null) {
+            return false;
+        }
+        ApplicationInfo applicationInfo;
+        try {
+            applicationInfo = context.getPackageManager().getApplicationInfo(
+                    assistant.getPackageName(), 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+        return applicationInfo.isSystemApp() || applicationInfo.isUpdatedSystemApp();
+    }
+
+    private static boolean isDisclosureEnabled(Context context) {
+        return Settings.Secure.getInt(context.getContentResolver(),
+                Settings.Secure.ASSIST_DISCLOSURE_ENABLED, 0) != 0;
+    }
+
+    /**
+     * @return if the disclosure animation should trigger for the given assistant.
+     *
+     * Third-party assistants will always need to disclose, while the user can configure this for
+     * pre-installed assistants.
+     */
+    public static boolean shouldDisclose(Context context, ComponentName assistant) {
+        if (!allowDisablingAssistDisclosure(context)) {
+            return true;
+        }
+
+        return isDisclosureEnabled(context) || !isPreinstalledAssistant(context, assistant);
+    }
+
+    public static boolean allowDisablingAssistDisclosure(Context context) {
+        return context.getResources().getBoolean(
+                com.android.internal.R.bool.config_allowDisablingAssistDisclosure);
+    }
+}
diff --git a/com/android/internal/app/ChooserActivity.java b/com/android/internal/app/ChooserActivity.java
new file mode 100644
index 0000000..2cab009
--- /dev/null
+++ b/com/android/internal/app/ChooserActivity.java
@@ -0,0 +1,1729 @@
+/*
+ * Copyright (C) 2008 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.internal.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.usage.UsageStatsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LabeledIntent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.database.DataSetObserver;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.service.chooser.ChooserTarget;
+import android.service.chooser.ChooserTargetService;
+import android.service.chooser.IChooserTargetResult;
+import android.service.chooser.IChooserTargetService;
+import android.text.TextUtils;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Space;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ResolverActivity.TargetInfo;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.google.android.collect.Lists;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class ChooserActivity extends ResolverActivity {
+    private static final String TAG = "ChooserActivity";
+
+    /**
+     * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
+     * in onStop when launched in a new task. If this extra is set to true, we do not finish
+     * ourselves when onStop gets called.
+     */
+    public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
+            = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
+
+    private static final boolean DEBUG = false;
+
+    private static final int QUERY_TARGET_SERVICE_LIMIT = 5;
+    private static final int WATCHDOG_TIMEOUT_MILLIS = 5000;
+
+    private Bundle mReplacementExtras;
+    private IntentSender mChosenComponentSender;
+    private IntentSender mRefinementIntentSender;
+    private RefinementResultReceiver mRefinementResultReceiver;
+    private ChooserTarget[] mCallerChooserTargets;
+    private ComponentName[] mFilteredComponentNames;
+
+    private Intent mReferrerFillInIntent;
+
+    private long mChooserShownTime;
+    protected boolean mIsSuccessfullySelected;
+
+    private ChooserListAdapter mChooserListAdapter;
+    private ChooserRowAdapter mChooserRowAdapter;
+
+    private SharedPreferences mPinnedSharedPrefs;
+    private static final float PINNED_TARGET_SCORE_BOOST = 1000.f;
+    private static final float CALLER_TARGET_SCORE_BOOST = 900.f;
+    private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
+    private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
+
+    private final List<ChooserTargetServiceConnection> mServiceConnections = new ArrayList<>();
+
+    private static final int CHOOSER_TARGET_SERVICE_RESULT = 1;
+    private static final int CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT = 2;
+
+    private final Handler mChooserHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case CHOOSER_TARGET_SERVICE_RESULT:
+                    if (DEBUG) Log.d(TAG, "CHOOSER_TARGET_SERVICE_RESULT");
+                    if (isDestroyed()) break;
+                    final ServiceResultInfo sri = (ServiceResultInfo) msg.obj;
+                    if (!mServiceConnections.contains(sri.connection)) {
+                        Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection
+                                + " returned after being removed from active connections."
+                                + " Have you considered returning results faster?");
+                        break;
+                    }
+                    if (sri.resultTargets != null) {
+                        mChooserListAdapter.addServiceResults(sri.originalTarget,
+                                sri.resultTargets);
+                    }
+                    unbindService(sri.connection);
+                    sri.connection.destroy();
+                    mServiceConnections.remove(sri.connection);
+                    if (mServiceConnections.isEmpty()) {
+                        mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
+                        sendVoiceChoicesIfNeeded();
+                        mChooserListAdapter.setShowServiceTargets(true);
+                    }
+                    break;
+
+                case CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT:
+                    if (DEBUG) {
+                        Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services");
+                    }
+                    unbindRemainingServices();
+                    sendVoiceChoicesIfNeeded();
+                    mChooserListAdapter.setShowServiceTargets(true);
+                    break;
+
+                default:
+                    super.handleMessage(msg);
+            }
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        final long intentReceivedTime = System.currentTimeMillis();
+        mIsSuccessfullySelected = false;
+        Intent intent = getIntent();
+        Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+        if (!(targetParcelable instanceof Intent)) {
+            Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable);
+            finish();
+            super.onCreate(null);
+            return;
+        }
+        Intent target = (Intent) targetParcelable;
+        if (target != null) {
+            modifyTargetIntent(target);
+        }
+        Parcelable[] targetsParcelable
+                = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS);
+        if (targetsParcelable != null) {
+            final boolean offset = target == null;
+            Intent[] additionalTargets =
+                    new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length];
+            for (int i = 0; i < targetsParcelable.length; i++) {
+                if (!(targetsParcelable[i] instanceof Intent)) {
+                    Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: "
+                            + targetsParcelable[i]);
+                    finish();
+                    super.onCreate(null);
+                    return;
+                }
+                final Intent additionalTarget = (Intent) targetsParcelable[i];
+                if (i == 0 && target == null) {
+                    target = additionalTarget;
+                    modifyTargetIntent(target);
+                } else {
+                    additionalTargets[offset ? i - 1 : i] = additionalTarget;
+                    modifyTargetIntent(additionalTarget);
+                }
+            }
+            setAdditionalTargets(additionalTargets);
+        }
+
+        mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
+        CharSequence title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE);
+        int defaultTitleRes = 0;
+        if (title == null) {
+            defaultTitleRes = com.android.internal.R.string.chooseActivity;
+        }
+        Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS);
+        Intent[] initialIntents = null;
+        if (pa != null) {
+            initialIntents = new Intent[pa.length];
+            for (int i=0; i<pa.length; i++) {
+                if (!(pa[i] instanceof Intent)) {
+                    Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]);
+                    finish();
+                    super.onCreate(null);
+                    return;
+                }
+                final Intent in = (Intent) pa[i];
+                modifyTargetIntent(in);
+                initialIntents[i] = in;
+            }
+        }
+
+        mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, getReferrer());
+
+        mChosenComponentSender = intent.getParcelableExtra(
+                Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
+        mRefinementIntentSender = intent.getParcelableExtra(
+                Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
+        setSafeForwardingMode(true);
+
+        pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS);
+        if (pa != null) {
+            ComponentName[] names = new ComponentName[pa.length];
+            for (int i = 0; i < pa.length; i++) {
+                if (!(pa[i] instanceof ComponentName)) {
+                    Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]);
+                    names = null;
+                    break;
+                }
+                names[i] = (ComponentName) pa[i];
+            }
+            mFilteredComponentNames = names;
+        }
+
+        pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
+        if (pa != null) {
+            ChooserTarget[] targets = new ChooserTarget[pa.length];
+            for (int i = 0; i < pa.length; i++) {
+                if (!(pa[i] instanceof ChooserTarget)) {
+                    Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]);
+                    targets = null;
+                    break;
+                }
+                targets[i] = (ChooserTarget) pa[i];
+            }
+            mCallerChooserTargets = targets;
+        }
+
+        mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+        setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false));
+        super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
+                null, false);
+
+        MetricsLogger.action(this, MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN);
+
+        mChooserShownTime = System.currentTimeMillis();
+        final long systemCost = mChooserShownTime - intentReceivedTime;
+        MetricsLogger.histogram(null, "system_cost_for_smart_sharing", (int) systemCost);
+        if (DEBUG) {
+            Log.d(TAG, "System Time Cost is " + systemCost);
+        }
+    }
+
+    static SharedPreferences getPinnedSharedPrefs(Context context) {
+        // The code below is because in the android:ui process, no one can hear you scream.
+        // The package info in the context isn't initialized in the way it is for normal apps,
+        // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
+        // build the path manually below using the same policy that appears in ContextImpl.
+        // This fails silently under the hood if there's a problem, so if we find ourselves in
+        // the case where we don't have access to credential encrypted storage we just won't
+        // have our pinned target info.
+        final File prefsFile = new File(new File(
+                Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
+                        context.getUserId(), context.getPackageName()),
+                "shared_prefs"),
+                PINNED_SHARED_PREFS_NAME + ".xml");
+        return context.getSharedPreferences(prefsFile, MODE_PRIVATE);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mRefinementResultReceiver != null) {
+            mRefinementResultReceiver.destroy();
+            mRefinementResultReceiver = null;
+        }
+        unbindRemainingServices();
+        mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_RESULT);
+    }
+
+    @Override
+    public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+        Intent result = defIntent;
+        if (mReplacementExtras != null) {
+            final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName);
+            if (replExtras != null) {
+                result = new Intent(defIntent);
+                result.putExtras(replExtras);
+            }
+        }
+        if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
+                || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+            result = Intent.createChooser(result,
+                    getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
+
+            // Don't auto-launch single intents if the intent is being forwarded. This is done
+            // because automatically launching a resolving application as a response to the user
+            // action of switching accounts is pretty unexpected.
+            result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
+        }
+        return result;
+    }
+
+    @Override
+    public void onActivityStarted(TargetInfo cti) {
+        if (mChosenComponentSender != null) {
+            final ComponentName target = cti.getResolvedComponentName();
+            if (target != null) {
+                final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
+                try {
+                    mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null);
+                } catch (IntentSender.SendIntentException e) {
+                    Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+                            + "the chosen component: " + e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) {
+        final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null;
+        mChooserListAdapter = (ChooserListAdapter) adapter;
+        if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) {
+            mChooserListAdapter.addServiceResults(null, Lists.newArrayList(mCallerChooserTargets));
+        }
+        mChooserRowAdapter = new ChooserRowAdapter(mChooserListAdapter);
+        mChooserRowAdapter.registerDataSetObserver(new OffsetDataSetObserver(adapterView));
+        adapterView.setAdapter(mChooserRowAdapter);
+        if (listView != null) {
+            listView.setItemsCanFocus(true);
+        }
+    }
+
+    @Override
+    public int getLayoutResource() {
+        return R.layout.chooser_grid;
+    }
+
+    @Override
+    public boolean shouldGetActivityMetadata() {
+        return true;
+    }
+
+    @Override
+    public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+        // Note that this is only safe because the Intent handled by the ChooserActivity is
+        // guaranteed to contain no extras unknown to the local ClassLoader. That is why this
+        // method can not be replaced in the ResolverActivity whole hog.
+        return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE,
+                super.shouldAutoLaunchSingleChoice(target));
+    }
+
+    @Override
+    public void showTargetDetails(ResolveInfo ri) {
+        ComponentName name = ri.activityInfo.getComponentName();
+        boolean pinned = mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+        ResolverTargetActionsDialogFragment f =
+                new ResolverTargetActionsDialogFragment(ri.loadLabel(getPackageManager()),
+                        name, pinned);
+        f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
+    }
+
+    private void modifyTargetIntent(Intent in) {
+        final String action = in.getAction();
+        if (Intent.ACTION_SEND.equals(action) ||
+                Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+            in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
+                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+    }
+
+    @Override
+    protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+        if (mRefinementIntentSender != null) {
+            final Intent fillIn = new Intent();
+            final List<Intent> sourceIntents = target.getAllSourceIntents();
+            if (!sourceIntents.isEmpty()) {
+                fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
+                if (sourceIntents.size() > 1) {
+                    final Intent[] alts = new Intent[sourceIntents.size() - 1];
+                    for (int i = 1, N = sourceIntents.size(); i < N; i++) {
+                        alts[i - 1] = sourceIntents.get(i);
+                    }
+                    fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts);
+                }
+                if (mRefinementResultReceiver != null) {
+                    mRefinementResultReceiver.destroy();
+                }
+                mRefinementResultReceiver = new RefinementResultReceiver(this, target, null);
+                fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER,
+                        mRefinementResultReceiver);
+                try {
+                    mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null);
+                    return false;
+                } catch (SendIntentException e) {
+                    Log.e(TAG, "Refinement IntentSender failed to send", e);
+                }
+            }
+        }
+        updateModelAndChooserCounts(target);
+        return super.onTargetSelected(target, alwaysCheck);
+    }
+
+    @Override
+    public void startSelected(int which, boolean always, boolean filtered) {
+        final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
+        super.startSelected(which, always, filtered);
+
+        if (mChooserListAdapter != null) {
+            // Log the index of which type of target the user picked.
+            // Lower values mean the ranking was better.
+            int cat = 0;
+            int value = which;
+            switch (mChooserListAdapter.getPositionTargetType(which)) {
+                case ChooserListAdapter.TARGET_CALLER:
+                    cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
+                    break;
+                case ChooserListAdapter.TARGET_SERVICE:
+                    cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
+                    value -= mChooserListAdapter.getCallerTargetCount();
+                    break;
+                case ChooserListAdapter.TARGET_STANDARD:
+                    cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
+                    value -= mChooserListAdapter.getCallerTargetCount()
+                            + mChooserListAdapter.getServiceTargetCount();
+                    break;
+            }
+
+            if (cat != 0) {
+                MetricsLogger.action(this, cat, value);
+            }
+
+            if (mIsSuccessfullySelected) {
+                if (DEBUG) {
+                    Log.d(TAG, "User Selection Time Cost is " + selectionCost);
+                    Log.d(TAG, "position of selected app/service/caller is " +
+                            Integer.toString(value));
+                }
+                MetricsLogger.histogram(null, "user_selection_cost_for_smart_sharing",
+                        (int) selectionCost);
+                MetricsLogger.histogram(null, "app_position_for_smart_sharing", value);
+            }
+        }
+    }
+
+    void queryTargetServices(ChooserListAdapter adapter) {
+        final PackageManager pm = getPackageManager();
+        int targetsToQuery = 0;
+        for (int i = 0, N = adapter.getDisplayResolveInfoCount(); i < N; i++) {
+            final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i);
+            if (adapter.getScore(dri) == 0) {
+                // A score of 0 means the app hasn't been used in some time;
+                // don't query it as it's not likely to be relevant.
+                continue;
+            }
+            final ActivityInfo ai = dri.getResolveInfo().activityInfo;
+            final Bundle md = ai.metaData;
+            final String serviceName = md != null ? convertServiceName(ai.packageName,
+                    md.getString(ChooserTargetService.META_DATA_NAME)) : null;
+            if (serviceName != null) {
+                final ComponentName serviceComponent = new ComponentName(
+                        ai.packageName, serviceName);
+                final Intent serviceIntent = new Intent(ChooserTargetService.SERVICE_INTERFACE)
+                        .setComponent(serviceComponent);
+
+                if (DEBUG) {
+                    Log.d(TAG, "queryTargets found target with service " + serviceComponent);
+                }
+
+                try {
+                    final String perm = pm.getServiceInfo(serviceComponent, 0).permission;
+                    if (!ChooserTargetService.BIND_PERMISSION.equals(perm)) {
+                        Log.w(TAG, "ChooserTargetService " + serviceComponent + " does not require"
+                                + " permission " + ChooserTargetService.BIND_PERMISSION
+                                + " - this service will not be queried for ChooserTargets."
+                                + " add android:permission=\""
+                                + ChooserTargetService.BIND_PERMISSION + "\""
+                                + " to the <service> tag for " + serviceComponent
+                                + " in the manifest.");
+                        continue;
+                    }
+                } catch (NameNotFoundException e) {
+                    Log.e(TAG, "Could not look up service " + serviceComponent
+                            + "; component name not found");
+                    continue;
+                }
+
+                final ChooserTargetServiceConnection conn =
+                        new ChooserTargetServiceConnection(this, dri);
+
+                // Explicitly specify Process.myUserHandle instead of calling bindService
+                // to avoid the warning from calling from the system process without an explicit
+                // user handle
+                if (bindServiceAsUser(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND,
+                        Process.myUserHandle())) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Binding service connection for target " + dri
+                                + " intent " + serviceIntent);
+                    }
+                    mServiceConnections.add(conn);
+                    targetsToQuery++;
+                }
+            }
+            if (targetsToQuery >= QUERY_TARGET_SERVICE_LIMIT) {
+                if (DEBUG) Log.d(TAG, "queryTargets hit query target limit "
+                        + QUERY_TARGET_SERVICE_LIMIT);
+                break;
+            }
+        }
+
+        if (!mServiceConnections.isEmpty()) {
+            if (DEBUG) Log.d(TAG, "queryTargets setting watchdog timer for "
+                    + WATCHDOG_TIMEOUT_MILLIS + "ms");
+            mChooserHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT,
+                    WATCHDOG_TIMEOUT_MILLIS);
+        } else {
+            sendVoiceChoicesIfNeeded();
+        }
+    }
+
+    private String convertServiceName(String packageName, String serviceName) {
+        if (TextUtils.isEmpty(serviceName)) {
+            return null;
+        }
+
+        final String fullName;
+        if (serviceName.startsWith(".")) {
+            // Relative to the app package. Prepend the app package name.
+            fullName = packageName + serviceName;
+        } else if (serviceName.indexOf('.') >= 0) {
+            // Fully qualified package name.
+            fullName = serviceName;
+        } else {
+            fullName = null;
+        }
+        return fullName;
+    }
+
+    void unbindRemainingServices() {
+        if (DEBUG) {
+            Log.d(TAG, "unbindRemainingServices, " + mServiceConnections.size() + " left");
+        }
+        for (int i = 0, N = mServiceConnections.size(); i < N; i++) {
+            final ChooserTargetServiceConnection conn = mServiceConnections.get(i);
+            if (DEBUG) Log.d(TAG, "unbinding " + conn);
+            unbindService(conn);
+            conn.destroy();
+        }
+        mServiceConnections.clear();
+        mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
+    }
+
+    public void onSetupVoiceInteraction() {
+        // Do nothing. We'll send the voice stuff ourselves.
+    }
+
+    void updateModelAndChooserCounts(TargetInfo info) {
+        if (info != null) {
+            final ResolveInfo ri = info.getResolveInfo();
+            Intent targetIntent = getTargetIntent();
+            if (ri != null && ri.activityInfo != null && targetIntent != null) {
+                if (mAdapter != null) {
+                    mAdapter.updateModel(info.getResolvedComponentName());
+                    mAdapter.updateChooserCounts(ri.activityInfo.packageName, getUserId(),
+                            targetIntent.getAction());
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
+                    Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
+                }
+            } else if(DEBUG) {
+                Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo");
+            }
+        }
+        mIsSuccessfullySelected = true;
+    }
+
+    void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) {
+        if (mRefinementResultReceiver != null) {
+            mRefinementResultReceiver.destroy();
+            mRefinementResultReceiver = null;
+        }
+        if (selectedTarget == null) {
+            Log.e(TAG, "Refinement result intent did not match any known targets; canceling");
+        } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) {
+            Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget
+                    + " cannot match refined source intent " + matchingIntent);
+        } else {
+            TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0);
+            if (super.onTargetSelected(clonedTarget, false)) {
+                updateModelAndChooserCounts(clonedTarget);
+                finish();
+                return;
+            }
+        }
+        onRefinementCanceled();
+    }
+
+    void onRefinementCanceled() {
+        if (mRefinementResultReceiver != null) {
+            mRefinementResultReceiver.destroy();
+            mRefinementResultReceiver = null;
+        }
+        finish();
+    }
+
+    boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) {
+        final List<Intent> targetIntents = target.getAllSourceIntents();
+        for (int i = 0, N = targetIntents.size(); i < N; i++) {
+            final Intent targetIntent = targetIntents.get(i);
+            if (targetIntent.filterEquals(matchingIntent)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void filterServiceTargets(String packageName, List<ChooserTarget> targets) {
+        if (targets == null) {
+            return;
+        }
+
+        final PackageManager pm = getPackageManager();
+        for (int i = targets.size() - 1; i >= 0; i--) {
+            final ChooserTarget target = targets.get(i);
+            final ComponentName targetName = target.getComponentName();
+            if (packageName != null && packageName.equals(targetName.getPackageName())) {
+                // Anything from the original target's package is fine.
+                continue;
+            }
+
+            boolean remove;
+            try {
+                final ActivityInfo ai = pm.getActivityInfo(targetName, 0);
+                remove = !ai.exported || ai.permission != null;
+            } catch (NameNotFoundException e) {
+                Log.e(TAG, "Target " + target + " returned by " + packageName
+                        + " component not found");
+                remove = true;
+            }
+
+            if (remove) {
+                targets.remove(i);
+            }
+        }
+    }
+
+    public class ChooserListController extends ResolverListController {
+        public ChooserListController(Context context,
+                PackageManager pm,
+                Intent targetIntent,
+                String referrerPackageName,
+                int launchedFromUid) {
+            super(context, pm, targetIntent, referrerPackageName, launchedFromUid);
+        }
+
+        @Override
+        boolean isComponentPinned(ComponentName name) {
+            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+        }
+
+        @Override
+        boolean isComponentFiltered(ComponentName name) {
+            if (mFilteredComponentNames == null) {
+                return false;
+            }
+            for (ComponentName filteredComponentName : mFilteredComponentNames) {
+                if (name.equals(filteredComponentName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public float getScore(DisplayResolveInfo target) {
+            if (target == null) {
+                return CALLER_TARGET_SCORE_BOOST;
+            }
+            float score = super.getScore(target);
+            if (target.isPinned()) {
+                score += PINNED_TARGET_SCORE_BOOST;
+            }
+            return score;
+        }
+    }
+
+    @Override
+    public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents,
+            Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
+            boolean filterLastUsed) {
+        final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents,
+                initialIntents, rList, launchedFromUid, filterLastUsed, createListController());
+        return adapter;
+    }
+
+    @VisibleForTesting
+    protected ResolverListController createListController() {
+        return new ChooserListController(
+                this,
+                mPm,
+                getTargetIntent(),
+                getReferrerPackageName(),
+                mLaunchedFromUid);
+    }
+
+    final class ChooserTargetInfo implements TargetInfo {
+        private final DisplayResolveInfo mSourceInfo;
+        private final ResolveInfo mBackupResolveInfo;
+        private final ChooserTarget mChooserTarget;
+        private Drawable mBadgeIcon = null;
+        private CharSequence mBadgeContentDescription;
+        private Drawable mDisplayIcon;
+        private final Intent mFillInIntent;
+        private final int mFillInFlags;
+        private final float mModifiedScore;
+
+        public ChooserTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget,
+                float modifiedScore) {
+            mSourceInfo = sourceInfo;
+            mChooserTarget = chooserTarget;
+            mModifiedScore = modifiedScore;
+            if (sourceInfo != null) {
+                final ResolveInfo ri = sourceInfo.getResolveInfo();
+                if (ri != null) {
+                    final ActivityInfo ai = ri.activityInfo;
+                    if (ai != null && ai.applicationInfo != null) {
+                        final PackageManager pm = getPackageManager();
+                        mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
+                        mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
+                    }
+                }
+            }
+            final Icon icon = chooserTarget.getIcon();
+            // TODO do this in the background
+            mDisplayIcon = icon != null ? icon.loadDrawable(ChooserActivity.this) : null;
+
+            if (sourceInfo != null) {
+                mBackupResolveInfo = null;
+            } else {
+                mBackupResolveInfo = getPackageManager().resolveActivity(getResolvedIntent(), 0);
+            }
+
+            mFillInIntent = null;
+            mFillInFlags = 0;
+        }
+
+        private ChooserTargetInfo(ChooserTargetInfo other, Intent fillInIntent, int flags) {
+            mSourceInfo = other.mSourceInfo;
+            mBackupResolveInfo = other.mBackupResolveInfo;
+            mChooserTarget = other.mChooserTarget;
+            mBadgeIcon = other.mBadgeIcon;
+            mBadgeContentDescription = other.mBadgeContentDescription;
+            mDisplayIcon = other.mDisplayIcon;
+            mFillInIntent = fillInIntent;
+            mFillInFlags = flags;
+            mModifiedScore = other.mModifiedScore;
+        }
+
+        public float getModifiedScore() {
+            return mModifiedScore;
+        }
+
+        @Override
+        public Intent getResolvedIntent() {
+            if (mSourceInfo != null) {
+                return mSourceInfo.getResolvedIntent();
+            }
+
+            final Intent targetIntent = new Intent(getTargetIntent());
+            targetIntent.setComponent(mChooserTarget.getComponentName());
+            targetIntent.putExtras(mChooserTarget.getIntentExtras());
+            return targetIntent;
+        }
+
+        @Override
+        public ComponentName getResolvedComponentName() {
+            if (mSourceInfo != null) {
+                return mSourceInfo.getResolvedComponentName();
+            } else if (mBackupResolveInfo != null) {
+                return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
+                        mBackupResolveInfo.activityInfo.name);
+            }
+            return null;
+        }
+
+        private Intent getBaseIntentToSend() {
+            Intent result = getResolvedIntent();
+            if (result == null) {
+                Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
+            } else {
+                result = new Intent(result);
+                if (mFillInIntent != null) {
+                    result.fillIn(mFillInIntent, mFillInFlags);
+                }
+                result.fillIn(mReferrerFillInIntent, 0);
+            }
+            return result;
+        }
+
+        @Override
+        public boolean start(Activity activity, Bundle options) {
+            throw new RuntimeException("ChooserTargets should be started as caller.");
+        }
+
+        @Override
+        public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+            final Intent intent = getBaseIntentToSend();
+            if (intent == null) {
+                return false;
+            }
+            intent.setComponent(mChooserTarget.getComponentName());
+            intent.putExtras(mChooserTarget.getIntentExtras());
+
+            // Important: we will ignore the target security checks in ActivityManager
+            // if and only if the ChooserTarget's target package is the same package
+            // where we got the ChooserTargetService that provided it. This lets a
+            // ChooserTargetService provide a non-exported or permission-guarded target
+            // to the chooser for the user to pick.
+            //
+            // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
+            // so we'll obey the caller's normal security checks.
+            final boolean ignoreTargetSecurity = mSourceInfo != null
+                    && mSourceInfo.getResolvedComponentName().getPackageName()
+                    .equals(mChooserTarget.getComponentName().getPackageName());
+            activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
+            return true;
+        }
+
+        @Override
+        public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+            throw new RuntimeException("ChooserTargets should be started as caller.");
+        }
+
+        @Override
+        public ResolveInfo getResolveInfo() {
+            return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+        }
+
+        @Override
+        public CharSequence getDisplayLabel() {
+            return mChooserTarget.getTitle();
+        }
+
+        @Override
+        public CharSequence getExtendedInfo() {
+            // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
+            return null;
+        }
+
+        @Override
+        public Drawable getDisplayIcon() {
+            return mDisplayIcon;
+        }
+
+        @Override
+        public Drawable getBadgeIcon() {
+            return mBadgeIcon;
+        }
+
+        @Override
+        public CharSequence getBadgeContentDescription() {
+            return mBadgeContentDescription;
+        }
+
+        @Override
+        public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+            return new ChooserTargetInfo(this, fillInIntent, flags);
+        }
+
+        @Override
+        public List<Intent> getAllSourceIntents() {
+            final List<Intent> results = new ArrayList<>();
+            if (mSourceInfo != null) {
+                // We only queried the service for the first one in our sourceinfo.
+                results.add(mSourceInfo.getAllSourceIntents().get(0));
+            }
+            return results;
+        }
+
+        @Override
+        public boolean isPinned() {
+            return mSourceInfo != null ? mSourceInfo.isPinned() : false;
+        }
+    }
+
+    public class ChooserListAdapter extends ResolveListAdapter {
+        public static final int TARGET_BAD = -1;
+        public static final int TARGET_CALLER = 0;
+        public static final int TARGET_SERVICE = 1;
+        public static final int TARGET_STANDARD = 2;
+
+        private static final int MAX_SERVICE_TARGETS = 8;
+        private static final int MAX_TARGETS_PER_SERVICE = 4;
+
+        private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
+        private final List<TargetInfo> mCallerTargets = new ArrayList<>();
+        private boolean mShowServiceTargets;
+
+        private float mLateFee = 1.f;
+
+        private final BaseChooserTargetComparator mBaseTargetComparator
+                = new BaseChooserTargetComparator();
+
+        public ChooserListAdapter(Context context, List<Intent> payloadIntents,
+                Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
+                boolean filterLastUsed, ResolverListController resolverListController) {
+            // Don't send the initial intents through the shared ResolverActivity path,
+            // we want to separate them into a different section.
+            super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed,
+                    resolverListController);
+
+            if (initialIntents != null) {
+                final PackageManager pm = getPackageManager();
+                for (int i = 0; i < initialIntents.length; i++) {
+                    final Intent ii = initialIntents[i];
+                    if (ii == null) {
+                        continue;
+                    }
+
+                    // We reimplement Intent#resolveActivityInfo here because if we have an
+                    // implicit intent, we want the ResolveInfo returned by PackageManager
+                    // instead of one we reconstruct ourselves. The ResolveInfo returned might
+                    // have extra metadata and resolvePackageName set and we want to respect that.
+                    ResolveInfo ri = null;
+                    ActivityInfo ai = null;
+                    final ComponentName cn = ii.getComponent();
+                    if (cn != null) {
+                        try {
+                            ai = pm.getActivityInfo(ii.getComponent(), 0);
+                            ri = new ResolveInfo();
+                            ri.activityInfo = ai;
+                        } catch (PackageManager.NameNotFoundException ignored) {
+                            // ai will == null below
+                        }
+                    }
+                    if (ai == null) {
+                        ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
+                        ai = ri != null ? ri.activityInfo : null;
+                    }
+                    if (ai == null) {
+                        Log.w(TAG, "No activity found for " + ii);
+                        continue;
+                    }
+                    UserManager userManager =
+                            (UserManager) getSystemService(Context.USER_SERVICE);
+                    if (ii instanceof LabeledIntent) {
+                        LabeledIntent li = (LabeledIntent)ii;
+                        ri.resolvePackageName = li.getSourcePackage();
+                        ri.labelRes = li.getLabelResource();
+                        ri.nonLocalizedLabel = li.getNonLocalizedLabel();
+                        ri.icon = li.getIconResource();
+                        ri.iconResourceId = ri.icon;
+                    }
+                    if (userManager.isManagedProfile()) {
+                        ri.noResourceId = true;
+                        ri.icon = 0;
+                    }
+                    mCallerTargets.add(new DisplayResolveInfo(ii, ri,
+                            ri.loadLabel(pm), null, ii));
+                }
+            }
+        }
+
+        @Override
+        public boolean showsExtendedInfo(TargetInfo info) {
+            // We have badges so we don't need this text shown.
+            return false;
+        }
+
+        @Override
+        public boolean isComponentPinned(ComponentName name) {
+            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+        }
+
+        @Override
+        public View onCreateView(ViewGroup parent) {
+            return mInflater.inflate(
+                    com.android.internal.R.layout.resolve_grid_item, parent, false);
+        }
+
+        @Override
+        public void onListRebuilt() {
+            if (mServiceTargets != null) {
+                pruneServiceTargets();
+            }
+            if (DEBUG) Log.d(TAG, "List built querying services");
+            queryTargetServices(this);
+        }
+
+        @Override
+        public boolean shouldGetResolvedFilter() {
+            return true;
+        }
+
+        @Override
+        public int getCount() {
+            return super.getCount() + getServiceTargetCount() + getCallerTargetCount();
+        }
+
+        @Override
+        public int getUnfilteredCount() {
+            return super.getUnfilteredCount() + getServiceTargetCount() + getCallerTargetCount();
+        }
+
+        public int getCallerTargetCount() {
+            return mCallerTargets.size();
+        }
+
+        public int getServiceTargetCount() {
+            if (!mShowServiceTargets) {
+                return 0;
+            }
+            return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
+        }
+
+        public int getStandardTargetCount() {
+            return super.getCount();
+        }
+
+        public int getPositionTargetType(int position) {
+            int offset = 0;
+
+            final int callerTargetCount = getCallerTargetCount();
+            if (position < callerTargetCount) {
+                return TARGET_CALLER;
+            }
+            offset += callerTargetCount;
+
+            final int serviceTargetCount = getServiceTargetCount();
+            if (position - offset < serviceTargetCount) {
+                return TARGET_SERVICE;
+            }
+            offset += serviceTargetCount;
+
+            final int standardTargetCount = super.getCount();
+            if (position - offset < standardTargetCount) {
+                return TARGET_STANDARD;
+            }
+
+            return TARGET_BAD;
+        }
+
+        @Override
+        public TargetInfo getItem(int position) {
+            return targetInfoForPosition(position, true);
+        }
+
+        @Override
+        public TargetInfo targetInfoForPosition(int position, boolean filtered) {
+            int offset = 0;
+
+            final int callerTargetCount = getCallerTargetCount();
+            if (position < callerTargetCount) {
+                return mCallerTargets.get(position);
+            }
+            offset += callerTargetCount;
+
+            final int serviceTargetCount = getServiceTargetCount();
+            if (position - offset < serviceTargetCount) {
+                return mServiceTargets.get(position - offset);
+            }
+            offset += serviceTargetCount;
+
+            return filtered ? super.getItem(position - offset)
+                    : getDisplayInfoAt(position - offset);
+        }
+
+        public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets) {
+            if (DEBUG) Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
+                    + " targets");
+            final float parentScore = getScore(origTarget);
+            Collections.sort(targets, mBaseTargetComparator);
+            float lastScore = 0;
+            for (int i = 0, N = Math.min(targets.size(), MAX_TARGETS_PER_SERVICE); i < N; i++) {
+                final ChooserTarget target = targets.get(i);
+                float targetScore = target.getScore();
+                targetScore *= parentScore;
+                targetScore *= mLateFee;
+                if (i > 0 && targetScore >= lastScore) {
+                    // Apply a decay so that the top app can't crowd out everything else.
+                    // This incents ChooserTargetServices to define what's truly better.
+                    targetScore = lastScore * 0.95f;
+                }
+                insertServiceTarget(new ChooserTargetInfo(origTarget, target, targetScore));
+
+                if (DEBUG) {
+                    Log.d(TAG, " => " + target.toString() + " score=" + targetScore
+                            + " base=" + target.getScore()
+                            + " lastScore=" + lastScore
+                            + " parentScore=" + parentScore
+                            + " lateFee=" + mLateFee);
+                }
+
+                lastScore = targetScore;
+            }
+
+            mLateFee *= 0.95f;
+
+            notifyDataSetChanged();
+        }
+
+        /**
+         * Set to true to reveal all service targets at once.
+         */
+        public void setShowServiceTargets(boolean show) {
+            if (show != mShowServiceTargets) {
+                mShowServiceTargets = show;
+                notifyDataSetChanged();
+            }
+        }
+
+        private void insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
+            final float newScore = chooserTargetInfo.getModifiedScore();
+            for (int i = 0, N = mServiceTargets.size(); i < N; i++) {
+                final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
+                if (newScore > serviceTarget.getModifiedScore()) {
+                    mServiceTargets.add(i, chooserTargetInfo);
+                    return;
+                }
+            }
+            mServiceTargets.add(chooserTargetInfo);
+        }
+
+        private void pruneServiceTargets() {
+            if (DEBUG) Log.d(TAG, "pruneServiceTargets");
+            for (int i = mServiceTargets.size() - 1; i >= 0; i--) {
+                final ChooserTargetInfo cti = mServiceTargets.get(i);
+                if (!hasResolvedTarget(cti.getResolveInfo())) {
+                    if (DEBUG) Log.d(TAG, " => " + i + " " + cti);
+                    mServiceTargets.remove(i);
+                }
+            }
+        }
+    }
+
+    static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
+        @Override
+        public int compare(ChooserTarget lhs, ChooserTarget rhs) {
+            // Descending order
+            return (int) Math.signum(rhs.getScore() - lhs.getScore());
+        }
+    }
+
+    static class RowScale {
+        private static final int DURATION = 400;
+
+        float mScale;
+        ChooserRowAdapter mAdapter;
+        private final ObjectAnimator mAnimator;
+
+        public static final FloatProperty<RowScale> PROPERTY =
+                new FloatProperty<RowScale>("scale") {
+            @Override
+            public void setValue(RowScale object, float value) {
+                object.mScale = value;
+                object.mAdapter.notifyDataSetChanged();
+            }
+
+            @Override
+            public Float get(RowScale object) {
+                return object.mScale;
+            }
+        };
+
+        public RowScale(@NonNull ChooserRowAdapter adapter, float from, float to) {
+            mAdapter = adapter;
+            mScale = from;
+            if (from == to) {
+                mAnimator = null;
+                return;
+            }
+
+            mAnimator = ObjectAnimator.ofFloat(this, PROPERTY, from, to)
+                .setDuration(DURATION);
+            mAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    mAdapter.onAnimationStart();
+                }
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAdapter.onAnimationEnd();
+                }
+            });
+        }
+
+        public RowScale setInterpolator(Interpolator interpolator) {
+            if (mAnimator != null) {
+                mAnimator.setInterpolator(interpolator);
+            }
+            return this;
+        }
+
+        public float get() {
+            return mScale;
+        }
+
+        public void startAnimation() {
+            if (mAnimator != null) {
+                mAnimator.start();
+            }
+        }
+
+        public void cancelAnimation() {
+            if (mAnimator != null) {
+                mAnimator.cancel();
+            }
+        }
+    }
+
+    class ChooserRowAdapter extends BaseAdapter {
+        private ChooserListAdapter mChooserListAdapter;
+        private final LayoutInflater mLayoutInflater;
+        private final int mColumnCount = 4;
+        private RowScale[] mServiceTargetScale;
+        private final Interpolator mInterpolator;
+        private int mAnimationCount = 0;
+
+        public ChooserRowAdapter(ChooserListAdapter wrappedAdapter) {
+            mChooserListAdapter = wrappedAdapter;
+            mLayoutInflater = LayoutInflater.from(ChooserActivity.this);
+
+            mInterpolator = AnimationUtils.loadInterpolator(ChooserActivity.this,
+                    android.R.interpolator.decelerate_quint);
+
+            wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
+                @Override
+                public void onChanged() {
+                    super.onChanged();
+                    final int rcount = getServiceTargetRowCount();
+                    if (mServiceTargetScale == null
+                            || mServiceTargetScale.length != rcount) {
+                        RowScale[] old = mServiceTargetScale;
+                        int oldRCount = old != null ? old.length : 0;
+                        mServiceTargetScale = new RowScale[rcount];
+                        if (old != null && rcount > 0) {
+                            System.arraycopy(old, 0, mServiceTargetScale, 0,
+                                    Math.min(old.length, rcount));
+                        }
+
+                        for (int i = rcount; i < oldRCount; i++) {
+                            old[i].cancelAnimation();
+                        }
+
+                        for (int i = oldRCount; i < rcount; i++) {
+                            final RowScale rs = new RowScale(ChooserRowAdapter.this, 0.f, 1.f)
+                                    .setInterpolator(mInterpolator);
+                            mServiceTargetScale[i] = rs;
+                        }
+
+                        // Start the animations in a separate loop.
+                        // The process of starting animations will result in
+                        // binding views to set up initial values, and we must
+                        // have ALL of the new RowScale objects created above before
+                        // we get started.
+                        for (int i = oldRCount; i < rcount; i++) {
+                            mServiceTargetScale[i].startAnimation();
+                        }
+                    }
+
+                    notifyDataSetChanged();
+                }
+
+                @Override
+                public void onInvalidated() {
+                    super.onInvalidated();
+                    notifyDataSetInvalidated();
+                    if (mServiceTargetScale != null) {
+                        for (RowScale rs : mServiceTargetScale) {
+                            rs.cancelAnimation();
+                        }
+                    }
+                }
+            });
+        }
+
+        private float getRowScale(int rowPosition) {
+            final int start = getCallerTargetRowCount();
+            final int end = start + getServiceTargetRowCount();
+            if (rowPosition >= start && rowPosition < end) {
+                return mServiceTargetScale[rowPosition - start].get();
+            }
+            return 1.f;
+        }
+
+        public void onAnimationStart() {
+            final boolean lock = mAnimationCount == 0;
+            mAnimationCount++;
+            if (lock) {
+                mResolverDrawerLayout.setDismissLocked(true);
+            }
+        }
+
+        public void onAnimationEnd() {
+            mAnimationCount--;
+            if (mAnimationCount == 0) {
+                mResolverDrawerLayout.setDismissLocked(false);
+            }
+        }
+
+        @Override
+        public int getCount() {
+            return (int) (
+                    getCallerTargetRowCount()
+                    + getServiceTargetRowCount()
+                    + Math.ceil((float) mChooserListAdapter.getStandardTargetCount() / mColumnCount)
+            );
+        }
+
+        public int getCallerTargetRowCount() {
+            return (int) Math.ceil(
+                    (float) mChooserListAdapter.getCallerTargetCount() / mColumnCount);
+        }
+
+        public int getServiceTargetRowCount() {
+            return (int) Math.ceil(
+                    (float) mChooserListAdapter.getServiceTargetCount() / mColumnCount);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            // We have nothing useful to return here.
+            return position;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final RowViewHolder holder;
+            if (convertView == null) {
+                holder = createViewHolder(parent);
+            } else {
+                holder = (RowViewHolder) convertView.getTag();
+            }
+            bindViewHolder(position, holder);
+
+            return holder.row;
+        }
+
+        RowViewHolder createViewHolder(ViewGroup parent) {
+            final ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
+                    parent, false);
+            final RowViewHolder holder = new RowViewHolder(row, mColumnCount);
+            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+
+            for (int i = 0; i < mColumnCount; i++) {
+                final View v = mChooserListAdapter.createView(row);
+                final int column = i;
+                v.setOnClickListener(new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        startSelected(holder.itemIndices[column], false, true);
+                    }
+                });
+                v.setOnLongClickListener(new OnLongClickListener() {
+                    @Override
+                    public boolean onLongClick(View v) {
+                        showTargetDetails(
+                                mChooserListAdapter.resolveInfoForPosition(
+                                        holder.itemIndices[column], true));
+                        return true;
+                    }
+                });
+                row.addView(v);
+                holder.cells[i] = v;
+
+                // Force height to be a given so we don't have visual disruption during scaling.
+                LayoutParams lp = v.getLayoutParams();
+                v.measure(spec, spec);
+                if (lp == null) {
+                    lp = new LayoutParams(LayoutParams.MATCH_PARENT, v.getMeasuredHeight());
+                    row.setLayoutParams(lp);
+                } else {
+                    lp.height = v.getMeasuredHeight();
+                }
+                if (i != (mColumnCount - 1)) {
+                    row.addView(new Space(ChooserActivity.this),
+                            new LinearLayout.LayoutParams(0, 0, 1));
+                }
+            }
+
+            // Pre-measure so we can scale later.
+            holder.measure();
+            LayoutParams lp = row.getLayoutParams();
+            if (lp == null) {
+                lp = new LayoutParams(LayoutParams.MATCH_PARENT, holder.measuredRowHeight);
+                row.setLayoutParams(lp);
+            } else {
+                lp.height = holder.measuredRowHeight;
+            }
+            row.setTag(holder);
+            return holder;
+        }
+
+        void bindViewHolder(int rowPosition, RowViewHolder holder) {
+            final int start = getFirstRowPosition(rowPosition);
+            final int startType = mChooserListAdapter.getPositionTargetType(start);
+
+            int end = start + mColumnCount - 1;
+            while (mChooserListAdapter.getPositionTargetType(end) != startType && end >= start) {
+                end--;
+            }
+
+            if (startType == ChooserListAdapter.TARGET_SERVICE) {
+                holder.row.setBackgroundColor(
+                        getColor(R.color.chooser_service_row_background_color));
+                int nextStartType = mChooserListAdapter.getPositionTargetType(
+                        getFirstRowPosition(rowPosition + 1));
+                int serviceSpacing = holder.row.getContext().getResources()
+                        .getDimensionPixelSize(R.dimen.chooser_service_spacing);
+                int top = rowPosition == 0 ? serviceSpacing : 0;
+                if (nextStartType != ChooserListAdapter.TARGET_SERVICE) {
+                    setVertPadding(holder, top, serviceSpacing);
+                } else {
+                    setVertPadding(holder, top, 0);
+                }
+            } else {
+                holder.row.setBackgroundColor(Color.TRANSPARENT);
+                int lastStartType = mChooserListAdapter.getPositionTargetType(
+                        getFirstRowPosition(rowPosition - 1));
+                if (lastStartType == ChooserListAdapter.TARGET_SERVICE || rowPosition == 0) {
+                    int serviceSpacing = holder.row.getContext().getResources()
+                            .getDimensionPixelSize(R.dimen.chooser_service_spacing);
+                    setVertPadding(holder, serviceSpacing, 0);
+                } else {
+                    setVertPadding(holder, 0, 0);
+                }
+            }
+
+            final int oldHeight = holder.row.getLayoutParams().height;
+            holder.row.getLayoutParams().height = Math.max(1,
+                    (int) (holder.measuredRowHeight * getRowScale(rowPosition)));
+            if (holder.row.getLayoutParams().height != oldHeight) {
+                holder.row.requestLayout();
+            }
+
+            for (int i = 0; i < mColumnCount; i++) {
+                final View v = holder.cells[i];
+                if (start + i <= end) {
+                    v.setVisibility(View.VISIBLE);
+                    holder.itemIndices[i] = start + i;
+                    mChooserListAdapter.bindView(holder.itemIndices[i], v);
+                } else {
+                    v.setVisibility(View.INVISIBLE);
+                }
+            }
+        }
+
+        private void setVertPadding(RowViewHolder holder, int top, int bottom) {
+            holder.row.setPadding(holder.row.getPaddingLeft(), top,
+                    holder.row.getPaddingRight(), bottom);
+        }
+
+        int getFirstRowPosition(int row) {
+            final int callerCount = mChooserListAdapter.getCallerTargetCount();
+            final int callerRows = (int) Math.ceil((float) callerCount / mColumnCount);
+
+            if (row < callerRows) {
+                return row * mColumnCount;
+            }
+
+            final int serviceCount = mChooserListAdapter.getServiceTargetCount();
+            final int serviceRows = (int) Math.ceil((float) serviceCount / mColumnCount);
+
+            if (row < callerRows + serviceRows) {
+                return callerCount + (row - callerRows) * mColumnCount;
+            }
+
+            return callerCount + serviceCount
+                    + (row - callerRows - serviceRows) * mColumnCount;
+        }
+    }
+
+    static class RowViewHolder {
+        final View[] cells;
+        final ViewGroup row;
+        int measuredRowHeight;
+        int[] itemIndices;
+
+        public RowViewHolder(ViewGroup row, int cellCount) {
+            this.row = row;
+            this.cells = new View[cellCount];
+            this.itemIndices = new int[cellCount];
+        }
+
+        public void measure() {
+            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+            row.measure(spec, spec);
+            measuredRowHeight = row.getMeasuredHeight();
+        }
+    }
+
+    static class ChooserTargetServiceConnection implements ServiceConnection {
+        private DisplayResolveInfo mOriginalTarget;
+        private ComponentName mConnectedComponent;
+        private ChooserActivity mChooserActivity;
+        private final Object mLock = new Object();
+
+        private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() {
+            @Override
+            public void sendResult(List<ChooserTarget> targets) throws RemoteException {
+                synchronized (mLock) {
+                    if (mChooserActivity == null) {
+                        Log.e(TAG, "destroyed ChooserTargetServiceConnection received result from "
+                                + mConnectedComponent + "; ignoring...");
+                        return;
+                    }
+                    mChooserActivity.filterServiceTargets(
+                            mOriginalTarget.getResolveInfo().activityInfo.packageName, targets);
+                    final Message msg = Message.obtain();
+                    msg.what = CHOOSER_TARGET_SERVICE_RESULT;
+                    msg.obj = new ServiceResultInfo(mOriginalTarget, targets,
+                            ChooserTargetServiceConnection.this);
+                    mChooserActivity.mChooserHandler.sendMessage(msg);
+                }
+            }
+        };
+
+        public ChooserTargetServiceConnection(ChooserActivity chooserActivity,
+                DisplayResolveInfo dri) {
+            mChooserActivity = chooserActivity;
+            mOriginalTarget = dri;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (DEBUG) Log.d(TAG, "onServiceConnected: " + name);
+            synchronized (mLock) {
+                if (mChooserActivity == null) {
+                    Log.e(TAG, "destroyed ChooserTargetServiceConnection got onServiceConnected");
+                    return;
+                }
+
+                final IChooserTargetService icts = IChooserTargetService.Stub.asInterface(service);
+                try {
+                    icts.getChooserTargets(mOriginalTarget.getResolvedComponentName(),
+                            mOriginalTarget.getResolveInfo().filter, mChooserTargetResult);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Querying ChooserTargetService " + name + " failed.", e);
+                    mChooserActivity.unbindService(this);
+                    destroy();
+                    mChooserActivity.mServiceConnections.remove(this);
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name);
+            synchronized (mLock) {
+                if (mChooserActivity == null) {
+                    Log.e(TAG,
+                            "destroyed ChooserTargetServiceConnection got onServiceDisconnected");
+                    return;
+                }
+
+                mChooserActivity.unbindService(this);
+                destroy();
+                mChooserActivity.mServiceConnections.remove(this);
+                if (mChooserActivity.mServiceConnections.isEmpty()) {
+                    mChooserActivity.mChooserHandler.removeMessages(
+                            CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
+                    mChooserActivity.sendVoiceChoicesIfNeeded();
+                }
+                mConnectedComponent = null;
+            }
+        }
+
+        public void destroy() {
+            synchronized (mLock) {
+                mChooserActivity = null;
+                mOriginalTarget = null;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "ChooserTargetServiceConnection{service="
+                    + mConnectedComponent + ", activity="
+                    + (mOriginalTarget != null
+                    ? mOriginalTarget.getResolveInfo().activityInfo.toString()
+                    : "<connection destroyed>") + "}";
+        }
+    }
+
+    static class ServiceResultInfo {
+        public final DisplayResolveInfo originalTarget;
+        public final List<ChooserTarget> resultTargets;
+        public final ChooserTargetServiceConnection connection;
+
+        public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt,
+                ChooserTargetServiceConnection c) {
+            originalTarget = ot;
+            resultTargets = rt;
+            connection = c;
+        }
+    }
+
+    static class RefinementResultReceiver extends ResultReceiver {
+        private ChooserActivity mChooserActivity;
+        private TargetInfo mSelectedTarget;
+
+        public RefinementResultReceiver(ChooserActivity host, TargetInfo target,
+                Handler handler) {
+            super(handler);
+            mChooserActivity = host;
+            mSelectedTarget = target;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            if (mChooserActivity == null) {
+                Log.e(TAG, "Destroyed RefinementResultReceiver received a result");
+                return;
+            }
+            if (resultData == null) {
+                Log.e(TAG, "RefinementResultReceiver received null resultData");
+                return;
+            }
+
+            switch (resultCode) {
+                case RESULT_CANCELED:
+                    mChooserActivity.onRefinementCanceled();
+                    break;
+                case RESULT_OK:
+                    Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT);
+                    if (intentParcelable instanceof Intent) {
+                        mChooserActivity.onRefinementResult(mSelectedTarget,
+                                (Intent) intentParcelable);
+                    } else {
+                        Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent"
+                                + " in resultData with key Intent.EXTRA_INTENT");
+                    }
+                    break;
+                default:
+                    Log.w(TAG, "Unknown result code " + resultCode
+                            + " sent to RefinementResultReceiver");
+                    break;
+            }
+        }
+
+        public void destroy() {
+            mChooserActivity = null;
+            mSelectedTarget = null;
+        }
+    }
+
+    class OffsetDataSetObserver extends DataSetObserver {
+        private final AbsListView mListView;
+        private int mCachedViewType = -1;
+        private View mCachedView;
+
+        public OffsetDataSetObserver(AbsListView listView) {
+            mListView = listView;
+        }
+
+        @Override
+        public void onChanged() {
+            if (mResolverDrawerLayout == null) {
+                return;
+            }
+
+            final int chooserTargetRows = mChooserRowAdapter.getServiceTargetRowCount();
+            int offset = 0;
+            for (int i = 0; i < chooserTargetRows; i++)  {
+                final int pos = mChooserRowAdapter.getCallerTargetRowCount() + i;
+                final int vt = mChooserRowAdapter.getItemViewType(pos);
+                if (vt != mCachedViewType) {
+                    mCachedView = null;
+                }
+                final View v = mChooserRowAdapter.getView(pos, mCachedView, mListView);
+                int height = ((RowViewHolder) (v.getTag())).measuredRowHeight;
+
+                offset += (int) (height * mChooserRowAdapter.getRowScale(pos));
+
+                if (vt >= 0) {
+                    mCachedViewType = vt;
+                    mCachedView = v;
+                } else {
+                    mCachedViewType = -1;
+                }
+            }
+
+            mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
+        }
+    }
+}
diff --git a/com/android/internal/app/ConfirmUserCreationActivity.java b/com/android/internal/app/ConfirmUserCreationActivity.java
new file mode 100644
index 0000000..03da9bc
--- /dev/null
+++ b/com/android/internal/app/ConfirmUserCreationActivity.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.internal.R;
+
+/**
+ * Activity to confirm with the user that it is ok to create a new user, as requested by
+ * an app. It has to do some checks to decide what kind of prompt the user should be shown.
+ * Particularly, it needs to check if the account requested already exists on another user.
+ */
+public class ConfirmUserCreationActivity extends AlertActivity
+        implements DialogInterface.OnClickListener {
+
+    private static final String TAG = "CreateUser";
+
+    private String mUserName;
+    private String mAccountName;
+    private String mAccountType;
+    private PersistableBundle mAccountOptions;
+    private boolean mCanProceed;
+    private UserManager mUserManager;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        Intent intent = getIntent();
+        mUserName = intent.getStringExtra(UserManager.EXTRA_USER_NAME);
+        mAccountName = intent.getStringExtra(UserManager.EXTRA_USER_ACCOUNT_NAME);
+        mAccountType = intent.getStringExtra(UserManager.EXTRA_USER_ACCOUNT_TYPE);
+        mAccountOptions = (PersistableBundle)
+                intent.getParcelableExtra(UserManager.EXTRA_USER_ACCOUNT_OPTIONS);
+
+        mUserManager = getSystemService(UserManager.class);
+
+        String message = checkUserCreationRequirements();
+
+        if (message == null) {
+            finish();
+            return;
+        }
+        final AlertController.AlertParams ap = mAlertParams;
+        ap.mMessage = message;
+        ap.mPositiveButtonText = getString(android.R.string.ok);
+        ap.mPositiveButtonListener = this;
+
+        // Show the negative button if the user actually has a choice
+        if (mCanProceed) {
+            ap.mNegativeButtonText = getString(android.R.string.cancel);
+            ap.mNegativeButtonListener = this;
+        }
+        setupAlert();
+    }
+
+    private String checkUserCreationRequirements() {
+        final String callingPackage = getCallingPackage();
+        if (callingPackage == null) {
+            throw new SecurityException(
+                    "User Creation intent must be launched with startActivityForResult");
+        }
+        final ApplicationInfo appInfo;
+        try {
+            appInfo = getPackageManager().getApplicationInfo(callingPackage, 0);
+        } catch (NameNotFoundException nnfe) {
+            throw new SecurityException(
+                    "Cannot find the calling package");
+        }
+        final String message;
+        // Check the user restrictions
+        boolean cantCreateUser = mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER)
+                || !mUserManager.isAdminUser();
+        // Check the system state and user count
+        boolean cantCreateAnyMoreUsers = !mUserManager.canAddMoreUsers();
+        // Check the account existence
+        final Account account = new Account(mAccountName, mAccountType);
+        boolean accountExists = mAccountName != null && mAccountType != null
+                && (AccountManager.get(this).someUserHasAccount(account)
+                    | mUserManager.someUserHasSeedAccount(mAccountName, mAccountType));
+        mCanProceed = true;
+        final String appName = appInfo.loadLabel(getPackageManager()).toString();
+        if (cantCreateUser) {
+            setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED);
+            return null;
+        } else if (cantCreateAnyMoreUsers) {
+            setResult(UserManager.USER_CREATION_FAILED_NO_MORE_USERS);
+            return null;
+        } else if (accountExists) {
+            message = getString(R.string.user_creation_account_exists, appName, mAccountName);
+        } else {
+            message = getString(R.string.user_creation_adding, appName, mAccountName);
+        }
+        return message;
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        setResult(RESULT_CANCELED);
+        if (which == BUTTON_POSITIVE && mCanProceed) {
+            Log.i(TAG, "Ok, creating user");
+            UserInfo user = mUserManager.createUser(mUserName, 0);
+            if (user == null) {
+                Log.e(TAG, "Couldn't create user");
+                finish();
+                return;
+            }
+            mUserManager.setSeedAccountData(user.id, mAccountName, mAccountType, mAccountOptions);
+            setResult(RESULT_OK);
+        }
+        finish();
+    }
+}
diff --git a/com/android/internal/app/DisableCarModeActivity.java b/com/android/internal/app/DisableCarModeActivity.java
new file mode 100644
index 0000000..7943c61
--- /dev/null
+++ b/com/android/internal/app/DisableCarModeActivity.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010 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.internal.app;
+
+import android.app.Activity;
+import android.app.IUiModeManager;
+import android.app.UiModeManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+public class DisableCarModeActivity extends Activity {
+    private static final String TAG = "DisableCarModeActivity";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        try {
+            IUiModeManager uiModeManager = IUiModeManager.Stub.asInterface(
+                    ServiceManager.getService("uimode"));
+            uiModeManager.disableCarMode(UiModeManager.DISABLE_CAR_MODE_GO_HOME);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to disable car mode", e);
+        }
+        finish();
+    }
+
+}
diff --git a/com/android/internal/app/DumpHeapActivity.java b/com/android/internal/app/DumpHeapActivity.java
new file mode 100644
index 0000000..0ce501e
--- /dev/null
+++ b/com/android/internal/app/DumpHeapActivity.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2015 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.internal.app;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.DebugUtils;
+import android.util.Slog;
+
+/**
+ * This activity is displayed when the system has collected a heap dump from
+ * a large process and the user has selected to share it.
+ */
+public class DumpHeapActivity extends Activity {
+    /** The process we are reporting */
+    public static final String KEY_PROCESS = "process";
+    /** The size limit the process reached */
+    public static final String KEY_SIZE = "size";
+    /** Optional name of package to directly launch */
+    public static final String KEY_DIRECT_LAUNCH = "direct_launch";
+
+    // Broadcast action to determine when to delete the current dump heap data.
+    public static final String ACTION_DELETE_DUMPHEAP = "com.android.server.am.DELETE_DUMPHEAP";
+
+    // Extra for above: delay delete of data, since the user is in the process of sharing it.
+    public static final String EXTRA_DELAY_DELETE = "delay_delete";
+
+    static final public Uri JAVA_URI = Uri.parse("content://com.android.server.heapdump/java");
+
+    String mProcess;
+    long mSize;
+    AlertDialog mDialog;
+    boolean mHandled = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mProcess = getIntent().getStringExtra(KEY_PROCESS);
+        mSize = getIntent().getLongExtra(KEY_SIZE, 0);
+
+        String directLaunch = getIntent().getStringExtra(KEY_DIRECT_LAUNCH);
+        if (directLaunch != null) {
+            Intent intent = new Intent(ActivityManager.ACTION_REPORT_HEAP_LIMIT);
+            intent.setPackage(directLaunch);
+            ClipData clip = ClipData.newUri(getContentResolver(), "Heap Dump", JAVA_URI);
+            intent.setClipData(clip);
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.setType(clip.getDescription().getMimeType(0));
+            intent.putExtra(Intent.EXTRA_STREAM, JAVA_URI);
+            try {
+                startActivity(intent);
+                scheduleDelete();
+                mHandled = true;
+                finish();
+                return;
+            } catch (ActivityNotFoundException e) {
+                Slog.i("DumpHeapActivity", "Unable to direct launch to " + directLaunch
+                        + ": " + e.getMessage());
+            }
+        }
+
+        AlertDialog.Builder b = new AlertDialog.Builder(this,
+                android.R.style.Theme_Material_Light_Dialog_Alert);
+        b.setTitle(com.android.internal.R.string.dump_heap_title);
+        b.setMessage(getString(com.android.internal.R.string.dump_heap_text,
+                mProcess, DebugUtils.sizeValueToString(mSize, null)));
+        b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                mHandled = true;
+                sendBroadcast(new Intent(ACTION_DELETE_DUMPHEAP));
+                finish();
+            }
+        });
+        b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                mHandled = true;
+                scheduleDelete();
+                Intent intent = new Intent(Intent.ACTION_SEND);
+                ClipData clip = ClipData.newUri(getContentResolver(), "Heap Dump", JAVA_URI);
+                intent.setClipData(clip);
+                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                intent.setType(clip.getDescription().getMimeType(0));
+                intent.putExtra(Intent.EXTRA_STREAM, JAVA_URI);
+                startActivity(Intent.createChooser(intent,
+                        getText(com.android.internal.R.string.dump_heap_title)));
+                finish();
+        }
+        });
+        mDialog = b.show();
+    }
+
+    void scheduleDelete() {
+        Intent broadcast = new Intent(ACTION_DELETE_DUMPHEAP);
+        broadcast.putExtra(EXTRA_DELAY_DELETE, true);
+        sendBroadcast(broadcast);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (!isChangingConfigurations()) {
+            if (!mHandled) {
+                sendBroadcast(new Intent(ACTION_DELETE_DUMPHEAP));
+            }
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mDialog.dismiss();
+    }
+}
diff --git a/com/android/internal/app/HeavyWeightSwitcherActivity.java b/com/android/internal/app/HeavyWeightSwitcherActivity.java
new file mode 100644
index 0000000..459071b
--- /dev/null
+++ b/com/android/internal/app/HeavyWeightSwitcherActivity.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2010 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.internal.app;
+
+import com.android.internal.R;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.Window;
+import android.view.View.OnClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * This activity is displayed when the system attempts to start an Intent for
+ * which there is more than one matching activity, allowing the user to decide
+ * which to go to.  It is not normally used directly by application developers.
+ */
+public class HeavyWeightSwitcherActivity extends Activity {
+    /** The PendingIntent of the new activity being launched. */
+    public static final String KEY_INTENT = "intent";
+    /** Set if the caller is requesting a result. */
+    public static final String KEY_HAS_RESULT = "has_result";
+    /** Package of current heavy-weight app. */
+    public static final String KEY_CUR_APP = "cur_app";
+    /** Task that current heavy-weight activity is running in. */
+    public static final String KEY_CUR_TASK = "cur_task";
+    /** Package of newly requested heavy-weight app. */
+    public static final String KEY_NEW_APP = "new_app";
+    
+    IntentSender mStartIntent;
+    boolean mHasResult;
+    String mCurApp;
+    int mCurTask;
+    String mNewApp;
+    
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        
+        requestWindowFeature(Window.FEATURE_LEFT_ICON);
+        
+        mStartIntent = (IntentSender)getIntent().getParcelableExtra(KEY_INTENT);
+        mHasResult = getIntent().getBooleanExtra(KEY_HAS_RESULT, false);
+        mCurApp = getIntent().getStringExtra(KEY_CUR_APP);
+        mCurTask = getIntent().getIntExtra(KEY_CUR_TASK, 0);
+        mNewApp = getIntent().getStringExtra(KEY_NEW_APP);
+        
+        setContentView(com.android.internal.R.layout.heavy_weight_switcher);
+        
+        setIconAndText(R.id.old_app_icon, R.id.old_app_action, R.id.old_app_description,
+                mCurApp, R.string.old_app_action, R.string.old_app_description);
+        setIconAndText(R.id.new_app_icon, R.id.new_app_action, R.id.new_app_description,
+                mNewApp, R.string.new_app_action, R.string.new_app_description);
+            
+        View button = findViewById((R.id.switch_old));
+        button.setOnClickListener(mSwitchOldListener);
+        button = findViewById((R.id.switch_new));
+        button.setOnClickListener(mSwitchNewListener);
+        button = findViewById((R.id.cancel));
+        button.setOnClickListener(mCancelListener);
+        
+        TypedValue out = new TypedValue();
+        getTheme().resolveAttribute(android.R.attr.alertDialogIcon, out, true);
+        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, 
+                out.resourceId);
+    }
+
+    void setText(int id, CharSequence text) {
+        ((TextView)findViewById(id)).setText(text);
+    }
+    
+    void setDrawable(int id, Drawable dr) {
+        if (dr != null) {
+            ((ImageView)findViewById(id)).setImageDrawable(dr);
+        }
+    }
+    
+    void setIconAndText(int iconId, int actionId, int descriptionId,
+            String packageName, int actionStr, int descriptionStr) {
+        CharSequence appName = "";
+        Drawable appIcon = null;
+        if (mCurApp != null) {
+            try {
+                ApplicationInfo info = getPackageManager().getApplicationInfo(
+                        packageName, 0);
+                appName = info.loadLabel(getPackageManager());
+                appIcon = info.loadIcon(getPackageManager());
+            } catch (PackageManager.NameNotFoundException e) {
+            }
+        }
+        
+        setDrawable(iconId, appIcon);
+        setText(actionId, getString(actionStr, appName));
+        setText(descriptionId, getText(descriptionStr));
+    }
+    
+    private OnClickListener mSwitchOldListener = new OnClickListener() {
+        public void onClick(View v) {
+            try {
+                ActivityManager.getService().moveTaskToFront(mCurTask, 0, null);
+            } catch (RemoteException e) {
+            }
+            finish();
+        }
+    };
+    
+    private OnClickListener mSwitchNewListener = new OnClickListener() {
+        public void onClick(View v) {
+            try {
+                ActivityManager.getService().finishHeavyWeightApp();
+            } catch (RemoteException e) {
+            }
+            try {
+                if (mHasResult) {
+                    startIntentSenderForResult(mStartIntent, -1, null,
+                            Intent.FLAG_ACTIVITY_FORWARD_RESULT,
+                            Intent.FLAG_ACTIVITY_FORWARD_RESULT, 0);
+                } else {
+                    startIntentSenderForResult(mStartIntent, -1, null, 0, 0, 0);
+                }
+            } catch (IntentSender.SendIntentException ex) {
+                Log.w("HeavyWeightSwitcherActivity", "Failure starting", ex);
+            }
+            finish();
+        }
+    };
+    
+    private OnClickListener mCancelListener = new OnClickListener() {
+        public void onClick(View v) {
+            finish();
+        }
+    };
+}
diff --git a/com/android/internal/app/IntentForwarderActivity.java b/com/android/internal/app/IntentForwarderActivity.java
new file mode 100644
index 0000000..398d087
--- /dev/null
+++ b/com/android/internal/app/IntentForwarderActivity.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2014 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.internal.app;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.AppGlobals;
+import android.app.admin.DevicePolicyManager;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+import android.widget.Toast;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
+
+/**
+ * This is used in conjunction with
+ * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to
+ * be passed in and out of a managed profile.
+ */
+public class IntentForwarderActivity extends Activity  {
+
+    public static String TAG = "IntentForwarderActivity";
+
+    public static String FORWARD_INTENT_TO_PARENT
+            = "com.android.internal.app.ForwardIntentToParent";
+
+    public static String FORWARD_INTENT_TO_MANAGED_PROFILE
+            = "com.android.internal.app.ForwardIntentToManagedProfile";
+
+    private Injector mInjector;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mInjector = createInjector();
+
+        Intent intentReceived = getIntent();
+        String className = intentReceived.getComponent().getClassName();
+        final int targetUserId;
+        final int userMessageId;
+        if (className.equals(FORWARD_INTENT_TO_PARENT)) {
+            userMessageId = com.android.internal.R.string.forward_intent_to_owner;
+            targetUserId = getProfileParent();
+        } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+            userMessageId = com.android.internal.R.string.forward_intent_to_work;
+            targetUserId = getManagedProfile();
+        } else {
+            Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly");
+            userMessageId = -1;
+            targetUserId = UserHandle.USER_NULL;
+        }
+        if (targetUserId == UserHandle.USER_NULL) {
+            // This covers the case where there is no parent / managed profile.
+            finish();
+            return;
+        }
+
+        final int callingUserId = getUserId();
+        final Intent newIntent = canForward(intentReceived, targetUserId);
+        if (newIntent != null) {
+            if (Intent.ACTION_CHOOSER.equals(newIntent.getAction())) {
+                Intent innerIntent = newIntent.getParcelableExtra(Intent.EXTRA_INTENT);
+                // At this point, innerIntent is not null. Otherwise, canForward would have returned
+                // false.
+                innerIntent.prepareToLeaveUser(callingUserId);
+            } else {
+                newIntent.prepareToLeaveUser(callingUserId);
+            }
+
+            final android.content.pm.ResolveInfo ri =
+                    mInjector.getPackageManager().resolveActivityAsUser(
+                            newIntent,
+                            MATCH_DEFAULT_ONLY,
+                            targetUserId);
+
+            // Don't show the disclosure if next activity is ResolverActivity or ChooserActivity
+            // as those will already have shown work / personal as neccesary etc.
+            final boolean shouldShowDisclosure = ri == null || ri.activityInfo == null ||
+                    !"android".equals(ri.activityInfo.packageName) ||
+                    !(ResolverActivity.class.getName().equals(ri.activityInfo.name)
+                            || ChooserActivity.class.getName().equals(ri.activityInfo.name));
+
+            try {
+                startActivityAsCaller(newIntent, null, false, targetUserId);
+            } catch (RuntimeException e) {
+                int launchedFromUid = -1;
+                String launchedFromPackage = "?";
+                try {
+                    launchedFromUid = ActivityManager.getService().getLaunchedFromUid(
+                            getActivityToken());
+                    launchedFromPackage = ActivityManager.getService().getLaunchedFromPackage(
+                            getActivityToken());
+                } catch (RemoteException ignored) {
+                }
+
+                Slog.wtf(TAG, "Unable to launch as UID " + launchedFromUid + " package "
+                        + launchedFromPackage + ", while running in "
+                        + ActivityThread.currentProcessName(), e);
+            }
+
+            if (shouldShowDisclosure) {
+                Toast.makeText(this, getString(userMessageId), Toast.LENGTH_LONG).show();
+            }
+        } else {
+            Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user "
+                    + callingUserId + " to user " + targetUserId);
+        }
+        finish();
+    }
+
+    /**
+     * Check whether the intent can be forwarded to target user. Return the intent used for
+     * forwarding if it can be forwarded, {@code null} otherwise.
+     */
+    Intent canForward(Intent incomingIntent, int targetUserId)  {
+        Intent forwardIntent = new Intent(incomingIntent);
+        forwardIntent.addFlags(
+                Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+        sanitizeIntent(forwardIntent);
+
+        Intent intentToCheck = forwardIntent;
+        if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) {
+            // The EXTRA_INITIAL_INTENTS may not be allowed to be forwarded.
+            if (forwardIntent.hasExtra(Intent.EXTRA_INITIAL_INTENTS)) {
+                Slog.wtf(TAG, "An chooser intent with extra initial intents cannot be forwarded to"
+                        + " a different user");
+                return null;
+            }
+            if (forwardIntent.hasExtra(Intent.EXTRA_REPLACEMENT_EXTRAS)) {
+                Slog.wtf(TAG, "A chooser intent with replacement extras cannot be forwarded to a"
+                        + " different user");
+                return null;
+            }
+            intentToCheck = forwardIntent.getParcelableExtra(Intent.EXTRA_INTENT);
+            if (intentToCheck == null) {
+                Slog.wtf(TAG, "Cannot forward a chooser intent with no extra "
+                        + Intent.EXTRA_INTENT);
+                return null;
+            }
+        }
+        if (forwardIntent.getSelector() != null) {
+            intentToCheck = forwardIntent.getSelector();
+        }
+        String resolvedType = intentToCheck.resolveTypeIfNeeded(getContentResolver());
+        sanitizeIntent(intentToCheck);
+        try {
+            if (mInjector.getIPackageManager().
+                    canForwardTo(intentToCheck, resolvedType, getUserId(), targetUserId)) {
+                return forwardIntent;
+            }
+        } catch (RemoteException e) {
+            Slog.e(TAG, "PackageManagerService is dead?");
+        }
+        return null;
+    }
+
+    /**
+     * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is
+     * no managed profile.
+     *
+     * TODO: Remove the assumption that there is only one managed profile
+     * on the device.
+     */
+    private int getManagedProfile() {
+        List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
+        for (UserInfo userInfo : relatedUsers) {
+            if (userInfo.isManagedProfile()) return userInfo.id;
+        }
+        Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE
+                + " has been called, but there is no managed profile");
+        return UserHandle.USER_NULL;
+    }
+
+    /**
+     * Returns the userId of the profile parent or UserHandle.USER_NULL if there is
+     * no parent.
+     */
+    private int getProfileParent() {
+        UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId());
+        if (parent == null) {
+            Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT
+                    + " has been called, but there is no parent");
+            return UserHandle.USER_NULL;
+        }
+        return parent.id;
+    }
+
+    /**
+     * Sanitize the intent in place.
+     */
+    private void sanitizeIntent(Intent intent) {
+        // Apps should not be allowed to target a specific package/ component in the target user.
+        intent.setPackage(null);
+        intent.setComponent(null);
+    }
+
+    @VisibleForTesting
+    protected Injector createInjector() {
+        return new InjectorImpl();
+    }
+
+    private class InjectorImpl implements Injector {
+
+        @Override
+        public IPackageManager getIPackageManager() {
+            return AppGlobals.getPackageManager();
+        }
+
+        @Override
+        public UserManager getUserManager() {
+            return getSystemService(UserManager.class);
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return IntentForwarderActivity.this.getPackageManager();
+        }
+    }
+
+    public interface Injector {
+        IPackageManager getIPackageManager();
+
+        UserManager getUserManager();
+
+        PackageManager getPackageManager();
+    }
+}
diff --git a/com/android/internal/app/LocaleHelper.java b/com/android/internal/app/LocaleHelper.java
new file mode 100644
index 0000000..386aa84
--- /dev/null
+++ b/com/android/internal/app/LocaleHelper.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.annotation.IntRange;
+import android.icu.text.ListFormatter;
+import android.icu.util.ULocale;
+import android.os.LocaleList;
+import android.text.TextUtils;
+
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.Locale;
+
+/**
+ * This class implements some handy methods to process with locales.
+ */
+public class LocaleHelper {
+
+    /**
+     * Sentence-case (first character uppercased).
+     *
+     * <p>There is no good API available for this, not even in ICU.
+     * We can revisit this if we get some ICU support later.</p>
+     *
+     * <p>There are currently several tickets requesting this feature:</p>
+     * <ul>
+     * <li>ICU needs to provide an easy way to titlecase only one first letter
+     *   http://bugs.icu-project.org/trac/ticket/11729</li>
+     * <li>Add "initial case"
+     *    http://bugs.icu-project.org/trac/ticket/8394</li>
+     * <li>Add code for initialCase, toTitlecase don't modify after Lt,
+     *   avoid 49Ers, low-level language-specific casing
+     *   http://bugs.icu-project.org/trac/ticket/10410</li>
+     * <li>BreakIterator.getFirstInstance: Often you need to titlecase just the first
+     *   word, and leave the rest of the string alone.  (closed as duplicate)
+     *   http://bugs.icu-project.org/trac/ticket/8946</li>
+     * </ul>
+     *
+     * <p>A (clunky) option with the current ICU API is:</p>
+     * {{
+     *   BreakIterator breakIterator = BreakIterator.getSentenceInstance(locale);
+     *   String result = UCharacter.toTitleCase(locale,
+     *       source, breakIterator, UCharacter.TITLECASE_NO_LOWERCASE);
+     * }}
+     *
+     * <p>That also means creating a BreakIterator for each locale. Expensive...</p>
+     *
+     * @param str the string to sentence-case.
+     * @param locale the locale used for the case conversion.
+     * @return the string converted to sentence-case.
+     */
+    public static String toSentenceCase(String str, Locale locale) {
+        if (str.isEmpty()) {
+            return str;
+        }
+        final int firstCodePointLen = str.offsetByCodePoints(0, 1);
+        return str.substring(0, firstCodePointLen).toUpperCase(locale)
+                + str.substring(firstCodePointLen);
+    }
+
+    /**
+     * Normalizes a string for locale name search. Does case conversion for now,
+     * but might do more in the future.
+     *
+     * <p>Warning: it is only intended to be used in searches by the locale picker.
+     * Don't use it for other things, it is very limited.</p>
+     *
+     * @param str the string to normalize
+     * @param locale the locale that might be used for certain operations (i.e. case conversion)
+     * @return the string normalized for search
+     */
+    public static String normalizeForSearch(String str, Locale locale) {
+        // TODO: tbd if it needs to be smarter (real normalization, remove accents, etc.)
+        // If needed we might use case folding and ICU/CLDR's collation-based loose searching.
+        // TODO: decide what should the locale be, the default locale, or the locale of the string.
+        // Uppercase is better than lowercase because of things like sharp S, Greek sigma, ...
+        return str.toUpperCase();
+    }
+
+    // For some locales we want to use a "dialect" form, for instance
+    // "Dari" instead of "Persian (Afghanistan)", or "Moldavian" instead of "Romanian (Moldova)"
+    private static boolean shouldUseDialectName(Locale locale) {
+        final String lang = locale.getLanguage();
+        return "fa".equals(lang) // Persian
+                || "ro".equals(lang) // Romanian
+                || "zh".equals(lang); // Chinese
+    }
+
+    /**
+     * Returns the locale localized for display in the provided locale.
+     *
+     * @param locale the locale whose name is to be displayed.
+     * @param displayLocale the locale in which to display the name.
+     * @param sentenceCase true if the result should be sentence-cased
+     * @return the localized name of the locale.
+     */
+    public static String getDisplayName(Locale locale, Locale displayLocale, boolean sentenceCase) {
+        final ULocale displayULocale = ULocale.forLocale(displayLocale);
+        String result = shouldUseDialectName(locale)
+                ? ULocale.getDisplayNameWithDialect(locale.toLanguageTag(), displayULocale)
+                : ULocale.getDisplayName(locale.toLanguageTag(), displayULocale);
+        return sentenceCase ? toSentenceCase(result, displayLocale) : result;
+    }
+
+    /**
+     * Returns the locale localized for display in the default locale.
+     *
+     * @param locale the locale whose name is to be displayed.
+     * @param sentenceCase true if the result should be sentence-cased
+     * @return the localized name of the locale.
+     */
+    public static String getDisplayName(Locale locale, boolean sentenceCase) {
+        return getDisplayName(locale, Locale.getDefault(), sentenceCase);
+    }
+
+    /**
+     * Returns a locale's country localized for display in the provided locale.
+     *
+     * @param locale the locale whose country will be displayed.
+     * @param displayLocale the locale in which to display the name.
+     * @return the localized country name.
+     */
+    public static String getDisplayCountry(Locale locale, Locale displayLocale) {
+        return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.forLocale(displayLocale));
+    }
+
+    /**
+     * Returns a locale's country localized for display in the default locale.
+     *
+     * @param locale the locale whose country will be displayed.
+     * @return the localized country name.
+     */
+    public static String getDisplayCountry(Locale locale) {
+        return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.getDefault());
+    }
+
+    /**
+     * Returns the locale list localized for display in the provided locale.
+     *
+     * @param locales the list of locales whose names is to be displayed.
+     * @param displayLocale the locale in which to display the names.
+     *                      If this is null, it will use the default locale.
+     * @param maxLocales maximum number of locales to display. Generates ellipsis after that.
+     * @return the locale aware list of locale names
+     */
+    public static String getDisplayLocaleList(
+            LocaleList locales, Locale displayLocale, @IntRange(from=1) int maxLocales) {
+
+        final Locale dispLocale = displayLocale == null ? Locale.getDefault() : displayLocale;
+
+        final boolean ellipsisNeeded = locales.size() > maxLocales;
+        final int localeCount, listCount;
+        if (ellipsisNeeded) {
+            localeCount = maxLocales;
+            listCount = maxLocales + 1;  // One extra slot for the ellipsis
+        } else {
+            listCount = localeCount = locales.size();
+        }
+        final String[] localeNames = new String[listCount];
+        for (int i = 0; i < localeCount; i++) {
+            localeNames[i] = LocaleHelper.getDisplayName(locales.get(i), dispLocale, false);
+        }
+        if (ellipsisNeeded) {
+            // Theoretically, we want to extract this from ICU's Resource Bundle for
+            // "Ellipsis/final", which seems to have different strings than the normal ellipsis for
+            // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two
+            // problems: it's expensive to extract it, and in case the output string becomes
+            // automatically ellipsized, it can result in weird output.
+            localeNames[maxLocales] = TextUtils.getEllipsisString(TextUtils.TruncateAt.END);
+        }
+
+        ListFormatter lfn = ListFormatter.getInstance(dispLocale);
+        return lfn.format((Object[]) localeNames);
+    }
+
+    /**
+     * Adds the likely subtags for a provided locale ID.
+     *
+     * @param locale the locale to maximize.
+     * @return the maximized Locale instance.
+     */
+    public static Locale addLikelySubtags(Locale locale) {
+        return libcore.icu.ICU.addLikelySubtags(locale);
+    }
+
+    /**
+     * Locale-sensitive comparison for LocaleInfo.
+     *
+     * <p>It uses the label, leaving the decision on what to put there to the LocaleInfo.
+     * For instance fr-CA can be shown as "français" as a generic label in the language selection,
+     * or "français (Canada)" if it is a suggestion, or "Canada" in the country selection.</p>
+     *
+     * <p>Gives priority to suggested locales (to sort them at the top).</p>
+     */
+    public static final class LocaleInfoComparator implements Comparator<LocaleStore.LocaleInfo> {
+        private final Collator mCollator;
+        private final boolean mCountryMode;
+        private static final String PREFIX_ARABIC = "\u0627\u0644"; // ALEF-LAM, ال
+
+        /**
+         * Constructor.
+         *
+         * @param sortLocale the locale to be used for sorting.
+         */
+        public LocaleInfoComparator(Locale sortLocale, boolean countryMode) {
+            mCollator = Collator.getInstance(sortLocale);
+            mCountryMode = countryMode;
+        }
+
+        /*
+         * The Arabic collation should ignore Alef-Lam at the beginning (b/26277596)
+         *
+         * We look at the label's locale, not the current system locale.
+         * This is because the name of the Arabic language itself is in Arabic,
+         * and starts with Alef-Lam, no matter what the system locale is.
+         */
+        private String removePrefixForCompare(Locale locale, String str) {
+            if ("ar".equals(locale.getLanguage()) && str.startsWith(PREFIX_ARABIC)) {
+                return str.substring(PREFIX_ARABIC.length());
+            }
+            return str;
+        }
+
+        /**
+         * Compares its two arguments for order.
+         *
+         * @param lhs   the first object to be compared
+         * @param rhs   the second object to be compared
+         * @return  a negative integer, zero, or a positive integer as the first
+         *          argument is less than, equal to, or greater than the second.
+         */
+        @Override
+        public int compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs) {
+            // We don't care about the various suggestion types, just "suggested" (!= 0)
+            // and "all others" (== 0)
+            if (lhs.isSuggested() == rhs.isSuggested()) {
+                // They are in the same "bucket" (suggested / others), so we compare the text
+                return mCollator.compare(
+                        removePrefixForCompare(lhs.getLocale(), lhs.getLabel(mCountryMode)),
+                        removePrefixForCompare(rhs.getLocale(), rhs.getLabel(mCountryMode)));
+            } else {
+                // One locale is suggested and one is not, so we put them in different "buckets"
+                return lhs.isSuggested() ? -1 : 1;
+            }
+        }
+    }
+}
diff --git a/com/android/internal/app/LocalePicker.java b/com/android/internal/app/LocalePicker.java
new file mode 100644
index 0000000..9936ed5
--- /dev/null
+++ b/com/android/internal/app/LocalePicker.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2010 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.internal.app;
+
+import com.android.internal.R;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.app.ListFragment;
+import android.app.backup.BackupManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.text.Collator;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.ArrayList;
+
+public class LocalePicker extends ListFragment {
+    private static final String TAG = "LocalePicker";
+    private static final boolean DEBUG = false;
+    private static final String[] pseudoLocales = { "en-XA", "ar-XB" };
+
+    public static interface LocaleSelectionListener {
+        // You can add any argument if you really need it...
+        public void onLocaleSelected(Locale locale);
+    }
+
+    LocaleSelectionListener mListener;  // default to null
+
+    public static class LocaleInfo implements Comparable<LocaleInfo> {
+        static final Collator sCollator = Collator.getInstance();
+
+        String label;
+        final Locale locale;
+
+        public LocaleInfo(String label, Locale locale) {
+            this.label = label;
+            this.locale = locale;
+        }
+
+        public String getLabel() {
+            return label;
+        }
+
+        public Locale getLocale() {
+            return locale;
+        }
+
+        @Override
+        public String toString() {
+            return this.label;
+        }
+
+        @Override
+        public int compareTo(LocaleInfo another) {
+            return sCollator.compare(this.label, another.label);
+        }
+    }
+
+    public static String[] getSystemAssetLocales() {
+        return Resources.getSystem().getAssets().getLocales();
+    }
+
+    public static String[] getSupportedLocales(Context context) {
+        return context.getResources().getStringArray(R.array.supported_locales);
+    }
+
+    public static String[] getPseudoLocales() {
+        return pseudoLocales;
+    }
+
+    public static List<LocaleInfo> getAllAssetLocales(Context context, boolean isInDeveloperMode) {
+        final Resources resources = context.getResources();
+
+        final String[] locales = getSystemAssetLocales();
+        List<String> localeList = new ArrayList<String>(locales.length);
+        Collections.addAll(localeList, locales);
+
+        // Don't show the pseudolocales unless we're in developer mode. http://b/17190407.
+        if (!isInDeveloperMode) {
+            for (String locale : pseudoLocales) {
+                localeList.remove(locale);
+            }
+        }
+
+        Collections.sort(localeList);
+        final String[] specialLocaleCodes = resources.getStringArray(R.array.special_locale_codes);
+        final String[] specialLocaleNames = resources.getStringArray(R.array.special_locale_names);
+
+        final ArrayList<LocaleInfo> localeInfos = new ArrayList<LocaleInfo>(localeList.size());
+        for (String locale : localeList) {
+            final Locale l = Locale.forLanguageTag(locale.replace('_', '-'));
+            if (l == null || "und".equals(l.getLanguage())
+                    || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) {
+                continue;
+            }
+
+            if (localeInfos.isEmpty()) {
+                if (DEBUG) {
+                    Log.v(TAG, "adding initial "+ toTitleCase(l.getDisplayLanguage(l)));
+                }
+                localeInfos.add(new LocaleInfo(toTitleCase(l.getDisplayLanguage(l)), l));
+            } else {
+                // check previous entry:
+                //  same lang and a country -> upgrade to full name and
+                //    insert ours with full name
+                //  diff lang -> insert ours with lang-only name
+                final LocaleInfo previous = localeInfos.get(localeInfos.size() - 1);
+                if (previous.locale.getLanguage().equals(l.getLanguage()) &&
+                        !previous.locale.getLanguage().equals("zz")) {
+                    if (DEBUG) {
+                        Log.v(TAG, "backing up and fixing " + previous.label + " to " +
+                                getDisplayName(previous.locale, specialLocaleCodes, specialLocaleNames));
+                    }
+                    previous.label = toTitleCase(getDisplayName(
+                            previous.locale, specialLocaleCodes, specialLocaleNames));
+                    if (DEBUG) {
+                        Log.v(TAG, "  and adding "+ toTitleCase(
+                                getDisplayName(l, specialLocaleCodes, specialLocaleNames)));
+                    }
+                    localeInfos.add(new LocaleInfo(toTitleCase(
+                            getDisplayName(l, specialLocaleCodes, specialLocaleNames)), l));
+                } else {
+                    String displayName = toTitleCase(l.getDisplayLanguage(l));
+                    if (DEBUG) {
+                        Log.v(TAG, "adding "+displayName);
+                    }
+                    localeInfos.add(new LocaleInfo(displayName, l));
+                }
+            }
+        }
+
+        Collections.sort(localeInfos);
+        return localeInfos;
+    }
+
+    /**
+     * Constructs an Adapter object containing Locale information. Content is sorted by
+     * {@link LocaleInfo#label}.
+     */
+    public static ArrayAdapter<LocaleInfo> constructAdapter(Context context) {
+        return constructAdapter(context, R.layout.locale_picker_item, R.id.locale);
+    }
+
+    public static ArrayAdapter<LocaleInfo> constructAdapter(Context context,
+            final int layoutId, final int fieldId) {
+        boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
+                Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
+        final List<LocaleInfo> localeInfos = getAllAssetLocales(context, isInDeveloperMode);
+
+        final LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        return new ArrayAdapter<LocaleInfo>(context, layoutId, fieldId, localeInfos) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                View view;
+                TextView text;
+                if (convertView == null) {
+                    view = inflater.inflate(layoutId, parent, false);
+                    text = (TextView) view.findViewById(fieldId);
+                    view.setTag(text);
+                } else {
+                    view = convertView;
+                    text = (TextView) view.getTag();
+                }
+                LocaleInfo item = getItem(position);
+                text.setText(item.toString());
+                text.setTextLocale(item.getLocale());
+
+                return view;
+            }
+        };
+    }
+
+    private static String toTitleCase(String s) {
+        if (s.length() == 0) {
+            return s;
+        }
+
+        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
+    }
+
+    private static String getDisplayName(
+            Locale l, String[] specialLocaleCodes, String[] specialLocaleNames) {
+        String code = l.toString();
+
+        for (int i = 0; i < specialLocaleCodes.length; i++) {
+            if (specialLocaleCodes[i].equals(code)) {
+                return specialLocaleNames[i];
+            }
+        }
+
+        return l.getDisplayName(l);
+    }
+
+    @Override
+    public void onActivityCreated(final Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        final ArrayAdapter<LocaleInfo> adapter = constructAdapter(getActivity());
+        setListAdapter(adapter);
+    }
+
+    public void setLocaleSelectionListener(LocaleSelectionListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        getListView().requestFocus();
+    }
+
+    /**
+     * Each listener needs to call {@link #updateLocale(Locale)} to actually change the locale.
+     *
+     * We don't call {@link #updateLocale(Locale)} automatically, as it halt the system for
+     * a moment and some callers won't want it.
+     */
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        if (mListener != null) {
+            final Locale locale = ((LocaleInfo)getListAdapter().getItem(position)).locale;
+            mListener.onLocaleSelected(locale);
+        }
+    }
+
+    /**
+     * Requests the system to update the system locale. Note that the system looks halted
+     * for a while during the Locale migration, so the caller need to take care of it.
+     *
+     * @see #updateLocales(LocaleList)
+     */
+    public static void updateLocale(Locale locale) {
+        updateLocales(new LocaleList(locale));
+    }
+
+    /**
+     * Requests the system to update the list of system locales.
+     * Note that the system looks halted for a while during the Locale migration,
+     * so the caller need to take care of it.
+     */
+    public static void updateLocales(LocaleList locales) {
+        try {
+            final IActivityManager am = ActivityManager.getService();
+            final Configuration config = am.getConfiguration();
+
+            config.setLocales(locales);
+            config.userSetLocale = true;
+
+            am.updatePersistentConfiguration(config);
+            // Trigger the dirty bit for the Settings Provider.
+            BackupManager.dataChanged("com.android.providers.settings");
+        } catch (RemoteException e) {
+            // Intentionally left blank
+        }
+    }
+
+    /**
+     * Get the locale list.
+     *
+     * @return The locale list.
+     */
+    public static LocaleList getLocales() {
+        try {
+            return ActivityManager.getService()
+                    .getConfiguration().getLocales();
+        } catch (RemoteException e) {
+            // If something went wrong
+            return LocaleList.getDefault();
+        }
+    }
+}
diff --git a/com/android/internal/app/LocalePickerWithRegion.java b/com/android/internal/app/LocalePickerWithRegion.java
new file mode 100644
index 0000000..3d5cd0f
--- /dev/null
+++ b/com/android/internal/app/LocalePickerWithRegion.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.ListFragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.SearchView;
+
+import com.android.internal.R;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * A two-step locale picker. It shows a language, then a country.
+ *
+ * <p>It shows suggestions at the top, then the rest of the locales.
+ * Allows the user to search for locales using both their native name and their name in the
+ * default locale.</p>
+ */
+public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener {
+    private static final String PARENT_FRAGMENT_NAME = "localeListEditor";
+
+    private SuggestedLocaleAdapter mAdapter;
+    private LocaleSelectedListener mListener;
+    private Set<LocaleStore.LocaleInfo> mLocaleList;
+    private LocaleStore.LocaleInfo mParentLocale;
+    private boolean mTranslatedOnly = false;
+    private SearchView mSearchView = null;
+    private CharSequence mPreviousSearch = null;
+    private boolean mPreviousSearchHadFocus = false;
+    private int mFirstVisiblePosition = 0;
+    private int mTopDistance = 0;
+
+    /**
+     * Other classes can register to be notified when a locale was selected.
+     *
+     * <p>This is the mechanism to "return" the result of the selection.</p>
+     */
+    public interface LocaleSelectedListener {
+        /**
+         * The classes that want to retrieve the locale picked should implement this method.
+         * @param locale    the locale picked.
+         */
+        void onLocaleSelected(LocaleStore.LocaleInfo locale);
+    }
+
+    private static LocalePickerWithRegion createCountryPicker(Context context,
+            LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
+            boolean translatedOnly) {
+        LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+        boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
+                translatedOnly);
+        return shouldShowTheList ? localePicker : null;
+    }
+
+    public static LocalePickerWithRegion createLanguagePicker(Context context,
+            LocaleSelectedListener listener, boolean translatedOnly) {
+        LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+        localePicker.setListener(context, listener, /* parent */ null, translatedOnly);
+        return localePicker;
+    }
+
+    /**
+     * Sets the listener and initializes the locale list.
+     *
+     * <p>Returns true if we need to show the list, false if not.</p>
+     *
+     * <p>Can return false because of an error, trying to show a list of countries,
+     * but no parent locale was provided.</p>
+     *
+     * <p>It can also return false if the caller tries to show the list in country mode and
+     * there is only one country available (i.e. Japanese => Japan).
+     * In this case we don't even show the list, we call the listener with that locale,
+     * "pretending" it was selected, and return false.</p>
+     */
+    private boolean setListener(Context context, LocaleSelectedListener listener,
+            LocaleStore.LocaleInfo parent, boolean translatedOnly) {
+        this.mParentLocale = parent;
+        this.mListener = listener;
+        this.mTranslatedOnly = translatedOnly;
+        setRetainInstance(true);
+
+        final HashSet<String> langTagsToIgnore = new HashSet<>();
+        if (!translatedOnly) {
+            final LocaleList userLocales = LocalePicker.getLocales();
+            final String[] langTags = userLocales.toLanguageTags().split(",");
+            Collections.addAll(langTagsToIgnore, langTags);
+        }
+
+        if (parent != null) {
+            mLocaleList = LocaleStore.getLevelLocales(context,
+                    langTagsToIgnore, parent, translatedOnly);
+            if (mLocaleList.size() <= 1) {
+                if (listener != null && (mLocaleList.size() == 1)) {
+                    listener.onLocaleSelected(mLocaleList.iterator().next());
+                }
+                return false;
+            }
+        } else {
+            mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
+                    null /* no parent */, translatedOnly);
+        }
+
+        return true;
+    }
+
+    private void returnToParentFrame() {
+        getFragmentManager().popBackStack(PARENT_FRAGMENT_NAME,
+                FragmentManager.POP_BACK_STACK_INCLUSIVE);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+
+        if (mLocaleList == null) {
+            // The fragment was killed and restored by the FragmentManager.
+            // At this point we have no data, no listener. Just return, to prevend a NPE.
+            // Fixes b/28748150. Created b/29400003 for a cleaner solution.
+            returnToParentFrame();
+            return;
+        }
+
+        final boolean countryMode = mParentLocale != null;
+        final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault();
+        mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode);
+        final LocaleHelper.LocaleInfoComparator comp =
+                new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode);
+        mAdapter.sort(comp);
+        setListAdapter(mAdapter);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem menuItem) {
+        int id = menuItem.getItemId();
+        switch (id) {
+            case android.R.id.home:
+                getFragmentManager().popBackStack();
+                return true;
+        }
+        return super.onOptionsItemSelected(menuItem);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        if (mParentLocale != null) {
+            getActivity().setTitle(mParentLocale.getFullNameNative());
+        } else {
+            getActivity().setTitle(R.string.language_selection_title);
+        }
+
+        getListView().requestFocus();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+
+        // Save search status
+        if (mSearchView != null) {
+            mPreviousSearchHadFocus = mSearchView.hasFocus();
+            mPreviousSearch = mSearchView.getQuery();
+        } else {
+            mPreviousSearchHadFocus = false;
+            mPreviousSearch = null;
+        }
+
+        // Save scroll position
+        final ListView list = getListView();
+        final View firstChild = list.getChildAt(0);
+        mFirstVisiblePosition = list.getFirstVisiblePosition();
+        mTopDistance = (firstChild == null) ? 0 : (firstChild.getTop() - list.getPaddingTop());
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        final LocaleStore.LocaleInfo locale =
+                (LocaleStore.LocaleInfo) getListAdapter().getItem(position);
+
+        if (locale.getParent() != null) {
+            if (mListener != null) {
+                mListener.onLocaleSelected(locale);
+            }
+            returnToParentFrame();
+        } else {
+            LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
+                    getContext(), mListener, locale, mTranslatedOnly /* translate only */);
+            if (selector != null) {
+                getFragmentManager().beginTransaction()
+                        .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+                        .replace(getId(), selector).addToBackStack(null)
+                        .commit();
+            } else {
+                returnToParentFrame();
+            }
+        }
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (mParentLocale == null) {
+            inflater.inflate(R.menu.language_selection_list, menu);
+
+            final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu);
+            mSearchView = (SearchView) searchMenuItem.getActionView();
+
+            mSearchView.setQueryHint(getText(R.string.search_language_hint));
+            mSearchView.setOnQueryTextListener(this);
+
+            // Restore previous search status
+            if (mPreviousSearch != null) {
+                searchMenuItem.expandActionView();
+                mSearchView.setIconified(false);
+                mSearchView.setActivated(true);
+                if (mPreviousSearchHadFocus) {
+                    mSearchView.requestFocus();
+                }
+                mSearchView.setQuery(mPreviousSearch, true /* submit */);
+            } else {
+                mSearchView.setQuery(null, false /* submit */);
+            }
+
+            // Restore previous scroll position
+            getListView().setSelectionFromTop(mFirstVisiblePosition, mTopDistance);
+        }
+    }
+
+    @Override
+    public boolean onQueryTextSubmit(String query) {
+        return false;
+    }
+
+    @Override
+    public boolean onQueryTextChange(String newText) {
+        if (mAdapter != null) {
+            mAdapter.getFilter().filter(newText);
+        }
+        return false;
+    }
+}
diff --git a/com/android/internal/app/LocaleStore.java b/com/android/internal/app/LocaleStore.java
new file mode 100644
index 0000000..e3fce51
--- /dev/null
+++ b/com/android/internal/app/LocaleStore.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IllformedLocaleException;
+import java.util.Locale;
+import java.util.Set;
+
+public class LocaleStore {
+    private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
+    private static boolean sFullyInitialized = false;
+
+    public static class LocaleInfo {
+        private static final int SUGGESTION_TYPE_NONE = 0;
+        private static final int SUGGESTION_TYPE_SIM = 1 << 0;
+        private static final int SUGGESTION_TYPE_CFG = 1 << 1;
+
+        private final Locale mLocale;
+        private final Locale mParent;
+        private final String mId;
+        private boolean mIsTranslated;
+        private boolean mIsPseudo;
+        private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion
+        // Combination of flags for various reasons to show a locale as a suggestion.
+        // Can be SIM, location, etc.
+        private int mSuggestionFlags;
+
+        private String mFullNameNative;
+        private String mFullCountryNameNative;
+        private String mLangScriptKey;
+
+        private LocaleInfo(Locale locale) {
+            this.mLocale = locale;
+            this.mId = locale.toLanguageTag();
+            this.mParent = getParent(locale);
+            this.mIsChecked = false;
+            this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
+            this.mIsTranslated = false;
+            this.mIsPseudo = false;
+        }
+
+        private LocaleInfo(String localeId) {
+            this(Locale.forLanguageTag(localeId));
+        }
+
+        private static Locale getParent(Locale locale) {
+            if (locale.getCountry().isEmpty()) {
+                return null;
+            }
+            return new Locale.Builder()
+                    .setLocale(locale).setRegion("")
+                    .build();
+        }
+
+        @Override
+        public String toString() {
+            return mId;
+        }
+
+        public Locale getLocale() {
+            return mLocale;
+        }
+
+        public Locale getParent() {
+            return mParent;
+        }
+
+        public String getId() {
+            return mId;
+        }
+
+        public boolean isTranslated() {
+            return mIsTranslated;
+        }
+
+        public void setTranslated(boolean isTranslated) {
+            mIsTranslated = isTranslated;
+        }
+
+        /* package */ boolean isSuggested() {
+            if (!mIsTranslated) { // Never suggest an untranslated locale
+                return false;
+            }
+            return mSuggestionFlags != SUGGESTION_TYPE_NONE;
+        }
+
+        private boolean isSuggestionOfType(int suggestionMask) {
+            if (!mIsTranslated) { // Never suggest an untranslated locale
+                return false;
+            }
+            return (mSuggestionFlags & suggestionMask) == suggestionMask;
+        }
+
+        public String getFullNameNative() {
+            if (mFullNameNative == null) {
+                mFullNameNative =
+                        LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
+            }
+            return mFullNameNative;
+        }
+
+        String getFullCountryNameNative() {
+            if (mFullCountryNameNative == null) {
+                mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
+            }
+            return mFullCountryNameNative;
+        }
+
+        String getFullCountryNameInUiLanguage() {
+            // We don't cache the UI name because the default locale keeps changing
+            return LocaleHelper.getDisplayCountry(mLocale);
+        }
+
+        /** Returns the name of the locale in the language of the UI.
+         * It is used for search, but never shown.
+         * For instance German will show as "Deutsch" in the list, but we will also search for
+         * "allemand" if the system UI is in French.
+         */
+        public String getFullNameInUiLanguage() {
+            // We don't cache the UI name because the default locale keeps changing
+            return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
+        }
+
+        private String getLangScriptKey() {
+            if (mLangScriptKey == null) {
+                Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(mLocale));
+                mLangScriptKey =
+                        (parentWithScript == null)
+                        ? mLocale.toLanguageTag()
+                        : parentWithScript.toLanguageTag();
+            }
+            return mLangScriptKey;
+        }
+
+        String getLabel(boolean countryMode) {
+            if (countryMode) {
+                return getFullCountryNameNative();
+            } else {
+                return getFullNameNative();
+            }
+        }
+
+        String getContentDescription(boolean countryMode) {
+            if (countryMode) {
+                return getFullCountryNameInUiLanguage();
+            } else {
+                return getFullNameInUiLanguage();
+            }
+        }
+
+        public boolean getChecked() {
+            return mIsChecked;
+        }
+
+        public void setChecked(boolean checked) {
+            mIsChecked = checked;
+        }
+    }
+
+    private static Set<String> getSimCountries(Context context) {
+        Set<String> result = new HashSet<>();
+
+        TelephonyManager tm = TelephonyManager.from(context);
+
+        if (tm != null) {
+            String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
+            if (!iso.isEmpty()) {
+                result.add(iso);
+            }
+
+            iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
+            if (!iso.isEmpty()) {
+                result.add(iso);
+            }
+        }
+
+        return result;
+    }
+
+    /*
+     * This method is added for SetupWizard, to force an update of the suggested locales
+     * when the SIM is initialized.
+     *
+     * <p>When the device is freshly started, it sometimes gets to the language selection
+     * before the SIM is properly initialized.
+     * So at the time the cache is filled, the info from the SIM might not be available.
+     * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
+     * SetupWizard will call this function when that happens.</p>
+     *
+     * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
+     * The user might change the SIM or might cross border and connect to a network
+     * in a different country, without restarting the Settings application or the phone.</p>
+     */
+    public static void updateSimCountries(Context context) {
+        Set<String> simCountries = getSimCountries(context);
+
+        for (LocaleInfo li : sLocaleCache.values()) {
+            // This method sets the suggestion flags for the (new) SIM locales, but it does not
+            // try to clean up the old flags. After all, if the user replaces a German SIM
+            // with a French one, it is still possible that they are speaking German.
+            // So both French and German are reasonable suggestions.
+            if (simCountries.contains(li.getLocale().getCountry())) {
+                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
+            }
+        }
+    }
+
+    /*
+     * Show all the languages supported for a country in the suggested list.
+     * This is also handy for devices without SIM (tablets).
+     */
+    private static void addSuggestedLocalesForRegion(Locale locale) {
+        if (locale == null) {
+            return;
+        }
+        final String country = locale.getCountry();
+        if (country.isEmpty()) {
+            return;
+        }
+
+        for (LocaleInfo li : sLocaleCache.values()) {
+            if (country.equals(li.getLocale().getCountry())) {
+                // We don't need to differentiate between manual and SIM suggestions
+                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
+            }
+        }
+    }
+
+    public static void fillCache(Context context) {
+        if (sFullyInitialized) {
+            return;
+        }
+
+        Set<String> simCountries = getSimCountries(context);
+
+        for (String localeId : LocalePicker.getSupportedLocales(context)) {
+            if (localeId.isEmpty()) {
+                throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
+            }
+            LocaleInfo li = new LocaleInfo(localeId);
+            if (simCountries.contains(li.getLocale().getCountry())) {
+                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
+            }
+            sLocaleCache.put(li.getId(), li);
+            final Locale parent = li.getParent();
+            if (parent != null) {
+                String parentId = parent.toLanguageTag();
+                if (!sLocaleCache.containsKey(parentId)) {
+                    sLocaleCache.put(parentId, new LocaleInfo(parent));
+                }
+            }
+        }
+
+        boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
+                Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
+        for (String localeId : LocalePicker.getPseudoLocales()) {
+            LocaleInfo li = getLocaleInfo(Locale.forLanguageTag(localeId));
+            if (isInDeveloperMode) {
+                li.setTranslated(true);
+                li.mIsPseudo = true;
+                li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
+            } else {
+                sLocaleCache.remove(li.getId());
+            }
+        }
+
+        // TODO: See if we can reuse what LocaleList.matchScore does
+        final HashSet<String> localizedLocales = new HashSet<>();
+        for (String localeId : LocalePicker.getSystemAssetLocales()) {
+            LocaleInfo li = new LocaleInfo(localeId);
+            final String country = li.getLocale().getCountry();
+            // All this is to figure out if we should suggest a country
+            if (!country.isEmpty()) {
+                LocaleInfo cachedLocale = null;
+                if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
+                    cachedLocale = sLocaleCache.get(li.getId());
+                } else { // e.g. zh-TW localized, zh-Hant-TW in cache
+                    final String langScriptCtry = li.getLangScriptKey() + "-" + country;
+                    if (sLocaleCache.containsKey(langScriptCtry)) {
+                        cachedLocale = sLocaleCache.get(langScriptCtry);
+                    }
+                }
+                if (cachedLocale != null) {
+                    cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
+                }
+            }
+            localizedLocales.add(li.getLangScriptKey());
+        }
+
+        for (LocaleInfo li : sLocaleCache.values()) {
+            li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
+        }
+
+        addSuggestedLocalesForRegion(Locale.getDefault());
+
+        sFullyInitialized = true;
+    }
+
+    private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
+        if (ignorables.contains(li.getId())) return 0;
+        if (li.mIsPseudo) return 2;
+        if (translatedOnly && !li.isTranslated()) return 0;
+        if (li.getParent() != null) return 2;
+        return 0;
+    }
+
+    /**
+     * Returns a list of locales for language or region selection.
+     * If the parent is null, then it is the language list.
+     * If it is not null, then the list will contain all the locales that belong to that parent.
+     * Example: if the parent is "ar", then the region list will contain all Arabic locales.
+     * (this is not language based, but language-script, so that it works for zh-Hant and so on.
+     */
+    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
+            LocaleInfo parent, boolean translatedOnly) {
+        fillCache(context);
+        String parentId = parent == null ? null : parent.getId();
+
+        HashSet<LocaleInfo> result = new HashSet<>();
+        for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
+            int level = getLevel(ignorables, li, translatedOnly);
+            if (level == 2) {
+                if (parent != null) { // region selection
+                    if (parentId.equals(li.getParent().toLanguageTag())) {
+                        result.add(li);
+                    }
+                } else { // language selection
+                    if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
+                        result.add(li);
+                    } else {
+                        result.add(getLocaleInfo(li.getParent()));
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    public static LocaleInfo getLocaleInfo(Locale locale) {
+        String id = locale.toLanguageTag();
+        LocaleInfo result;
+        if (!sLocaleCache.containsKey(id)) {
+            result = new LocaleInfo(locale);
+            sLocaleCache.put(id, result);
+        } else {
+            result = sLocaleCache.get(id);
+        }
+        return result;
+    }
+}
diff --git a/com/android/internal/app/MediaRouteChooserDialog.java b/com/android/internal/app/MediaRouteChooserDialog.java
new file mode 100644
index 0000000..7108d14
--- /dev/null
+++ b/com/android/internal/app/MediaRouteChooserDialog.java
@@ -0,0 +1,281 @@
+/*
+ * 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.internal.app;
+
+import com.android.internal.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.media.MediaRouter;
+import android.media.MediaRouter.RouteInfo;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.Comparator;
+
+/**
+ * This class implements the route chooser dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to choose a route that matches a given selector.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ *
+ * TODO: Move this back into the API, as in the support library media router.
+ */
+public class MediaRouteChooserDialog extends Dialog {
+    private final MediaRouter mRouter;
+    private final MediaRouterCallback mCallback;
+
+    private int mRouteTypes;
+    private View.OnClickListener mExtendedSettingsClickListener;
+    private RouteAdapter mAdapter;
+    private ListView mListView;
+    private Button mExtendedSettingsButton;
+    private boolean mAttachedToWindow;
+
+    public MediaRouteChooserDialog(Context context, int theme) {
+        super(context, theme);
+
+        mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+        mCallback = new MediaRouterCallback();
+    }
+
+    /**
+     * Gets the media route types for filtering the routes that the user can
+     * select using the media route chooser dialog.
+     *
+     * @return The route types.
+     */
+    public int getRouteTypes() {
+        return mRouteTypes;
+    }
+
+    /**
+     * Sets the types of routes that will be shown in the media route chooser dialog
+     * launched by this button.
+     *
+     * @param types The route types to match.
+     */
+    public void setRouteTypes(int types) {
+        if (mRouteTypes != types) {
+            mRouteTypes = types;
+
+            if (mAttachedToWindow) {
+                mRouter.removeCallback(mCallback);
+                mRouter.addCallback(types, mCallback,
+                        MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+            }
+
+            refreshRoutes();
+        }
+    }
+
+    public void setExtendedSettingsClickListener(View.OnClickListener listener) {
+        if (listener != mExtendedSettingsClickListener) {
+            mExtendedSettingsClickListener = listener;
+            updateExtendedSettingsButton();
+        }
+    }
+
+    /**
+     * Returns true if the route should be included in the list.
+     * <p>
+     * The default implementation returns true for enabled non-default routes that
+     * match the route types.  Subclasses can override this method to filter routes
+     * differently.
+     * </p>
+     *
+     * @param route The route to consider, never null.
+     * @return True if the route should be included in the chooser dialog.
+     */
+    public boolean onFilterRoute(MediaRouter.RouteInfo route) {
+        return !route.isDefault() && route.isEnabled() && route.matchesTypes(mRouteTypes);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+        setContentView(R.layout.media_route_chooser_dialog);
+        setTitle(mRouteTypes == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY
+                ? R.string.media_route_chooser_title_for_remote_display
+                : R.string.media_route_chooser_title);
+
+        // Must be called after setContentView.
+        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
+                isLightTheme(getContext()) ? R.drawable.ic_media_route_off_holo_light
+                    : R.drawable.ic_media_route_off_holo_dark);
+
+        mAdapter = new RouteAdapter(getContext());
+        mListView = (ListView)findViewById(R.id.media_route_list);
+        mListView.setAdapter(mAdapter);
+        mListView.setOnItemClickListener(mAdapter);
+        mListView.setEmptyView(findViewById(android.R.id.empty));
+
+        mExtendedSettingsButton = (Button)findViewById(R.id.media_route_extended_settings_button);
+        updateExtendedSettingsButton();
+    }
+
+    private void updateExtendedSettingsButton() {
+        if (mExtendedSettingsButton != null) {
+            mExtendedSettingsButton.setOnClickListener(mExtendedSettingsClickListener);
+            mExtendedSettingsButton.setVisibility(
+                    mExtendedSettingsClickListener != null ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mAttachedToWindow = true;
+        mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+        refreshRoutes();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        mAttachedToWindow = false;
+        mRouter.removeCallback(mCallback);
+
+        super.onDetachedFromWindow();
+    }
+
+    /**
+     * Refreshes the list of routes that are shown in the chooser dialog.
+     */
+    public void refreshRoutes() {
+        if (mAttachedToWindow) {
+            mAdapter.update();
+        }
+    }
+
+    static boolean isLightTheme(Context context) {
+        TypedValue value = new TypedValue();
+        return context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true)
+                && value.data != 0;
+    }
+
+    private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
+            implements ListView.OnItemClickListener {
+        private final LayoutInflater mInflater;
+
+        public RouteAdapter(Context context) {
+            super(context, 0);
+            mInflater = LayoutInflater.from(context);
+        }
+
+        public void update() {
+            clear();
+            final int count = mRouter.getRouteCount();
+            for (int i = 0; i < count; i++) {
+                MediaRouter.RouteInfo route = mRouter.getRouteAt(i);
+                if (onFilterRoute(route)) {
+                    add(route);
+                }
+            }
+            sort(RouteComparator.sInstance);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return getItem(position).isEnabled();
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view = convertView;
+            if (view == null) {
+                view = mInflater.inflate(R.layout.media_route_list_item, parent, false);
+            }
+            MediaRouter.RouteInfo route = getItem(position);
+            TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+            TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+            text1.setText(route.getName());
+            CharSequence description = route.getDescription();
+            if (TextUtils.isEmpty(description)) {
+                text2.setVisibility(View.GONE);
+                text2.setText("");
+            } else {
+                text2.setVisibility(View.VISIBLE);
+                text2.setText(description);
+            }
+            view.setEnabled(route.isEnabled());
+            return view;
+        }
+
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            MediaRouter.RouteInfo route = getItem(position);
+            if (route.isEnabled()) {
+                route.select();
+                dismiss();
+            }
+        }
+    }
+
+    private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
+        @Override
+        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoutes();
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoutes();
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoutes();
+        }
+
+        @Override
+        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
+            dismiss();
+        }
+    }
+
+    private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
+        public static final RouteComparator sInstance = new RouteComparator();
+
+        @Override
+        public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
+            return lhs.getName().toString().compareTo(rhs.getName().toString());
+        }
+    }
+}
diff --git a/com/android/internal/app/MediaRouteChooserDialogFragment.java b/com/android/internal/app/MediaRouteChooserDialogFragment.java
new file mode 100644
index 0000000..3cbc9ea
--- /dev/null
+++ b/com/android/internal/app/MediaRouteChooserDialogFragment.java
@@ -0,0 +1,104 @@
+/*
+ * 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.internal.app;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * Media route chooser dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteChooserDialog}.  The application may subclass
+ * this dialog fragment to customize the media route chooser dialog.
+ * </p>
+ *
+ * TODO: Move this back into the API, as in the support library media router.
+ */
+public class MediaRouteChooserDialogFragment extends DialogFragment {
+    private final String ARGUMENT_ROUTE_TYPES = "routeTypes";
+
+    private View.OnClickListener mExtendedSettingsClickListener;
+
+    /**
+     * Creates a media route chooser dialog fragment.
+     * <p>
+     * All subclasses of this class must also possess a default constructor.
+     * </p>
+     */
+    public MediaRouteChooserDialogFragment() {
+        int theme = MediaRouteChooserDialog.isLightTheme(getContext())
+                ? android.R.style.Theme_DeviceDefault_Light_Dialog
+                : android.R.style.Theme_DeviceDefault_Dialog;
+
+        setCancelable(true);
+        setStyle(STYLE_NORMAL, theme);
+    }
+
+    public int getRouteTypes() {
+        Bundle args = getArguments();
+        return args != null ? args.getInt(ARGUMENT_ROUTE_TYPES) : 0;
+    }
+
+    public void setRouteTypes(int types) {
+        if (types != getRouteTypes()) {
+            Bundle args = getArguments();
+            if (args == null) {
+                args = new Bundle();
+            }
+            args.putInt(ARGUMENT_ROUTE_TYPES, types);
+            setArguments(args);
+
+            MediaRouteChooserDialog dialog = (MediaRouteChooserDialog)getDialog();
+            if (dialog != null) {
+                dialog.setRouteTypes(types);
+            }
+        }
+    }
+
+    public void setExtendedSettingsClickListener(View.OnClickListener listener) {
+        if (listener != mExtendedSettingsClickListener) {
+            mExtendedSettingsClickListener = listener;
+
+            MediaRouteChooserDialog dialog = (MediaRouteChooserDialog)getDialog();
+            if (dialog != null) {
+                dialog.setExtendedSettingsClickListener(listener);
+            }
+        }
+    }
+
+    /**
+     * Called when the chooser dialog is being created.
+     * <p>
+     * Subclasses may override this method to customize the dialog.
+     * </p>
+     */
+    public MediaRouteChooserDialog onCreateChooserDialog(
+            Context context, Bundle savedInstanceState) {
+        return new MediaRouteChooserDialog(context, getTheme());
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        MediaRouteChooserDialog dialog = onCreateChooserDialog(getActivity(), savedInstanceState);
+        dialog.setRouteTypes(getRouteTypes());
+        dialog.setExtendedSettingsClickListener(mExtendedSettingsClickListener);
+        return dialog;
+    }
+}
diff --git a/com/android/internal/app/MediaRouteControllerDialog.java b/com/android/internal/app/MediaRouteControllerDialog.java
new file mode 100644
index 0000000..61e63d1
--- /dev/null
+++ b/com/android/internal/app/MediaRouteControllerDialog.java
@@ -0,0 +1,355 @@
+/*
+ * 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.internal.app;
+
+import com.android.internal.R;
+
+import android.app.AlertDialog;
+import android.app.MediaRouteActionProvider;
+import android.app.MediaRouteButton;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.media.MediaRouter;
+import android.media.MediaRouter.RouteGroup;
+import android.media.MediaRouter.RouteInfo;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+/**
+ * This class implements the route controller dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to control or disconnect from the currently selected route.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ *
+ * TODO: Move this back into the API, as in the support library media router.
+ */
+public class MediaRouteControllerDialog extends AlertDialog {
+    // Time to wait before updating the volume when the user lets go of the seek bar
+    // to allow the route provider time to propagate the change and publish a new
+    // route descriptor.
+    private static final int VOLUME_UPDATE_DELAY_MILLIS = 250;
+
+    private final MediaRouter mRouter;
+    private final MediaRouterCallback mCallback;
+    private final MediaRouter.RouteInfo mRoute;
+
+    private boolean mCreated;
+    private Drawable mMediaRouteButtonDrawable;
+    private int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled };
+    private int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled };
+    private Drawable mCurrentIconDrawable;
+
+    private boolean mVolumeControlEnabled = true;
+    private LinearLayout mVolumeLayout;
+    private SeekBar mVolumeSlider;
+    private boolean mVolumeSliderTouched;
+
+    private View mControlView;
+    private boolean mAttachedToWindow;
+
+    public MediaRouteControllerDialog(Context context, int theme) {
+        super(context, theme);
+
+        mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+        mCallback = new MediaRouterCallback();
+        mRoute = mRouter.getSelectedRoute();
+    }
+
+    /**
+     * Gets the route that this dialog is controlling.
+     */
+    public MediaRouter.RouteInfo getRoute() {
+        return mRoute;
+    }
+
+    /**
+     * Provides the subclass an opportunity to create a view that will
+     * be included within the body of the dialog to offer additional media controls
+     * for the currently playing content.
+     *
+     * @param savedInstanceState The dialog's saved instance state.
+     * @return The media control view, or null if none.
+     */
+    public View onCreateMediaControlView(Bundle savedInstanceState) {
+        return null;
+    }
+
+    /**
+     * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
+     *
+     * @return The media control view, or null if none.
+     */
+    public View getMediaControlView() {
+        return mControlView;
+    }
+
+    /**
+     * Sets whether to enable the volume slider and volume control using the volume keys
+     * when the route supports it.
+     * <p>
+     * The default value is true.
+     * </p>
+     */
+    public void setVolumeControlEnabled(boolean enable) {
+        if (mVolumeControlEnabled != enable) {
+            mVolumeControlEnabled = enable;
+            if (mCreated) {
+                updateVolume();
+            }
+        }
+    }
+
+    /**
+     * Returns whether to enable the volume slider and volume control using the volume keys
+     * when the route supports it.
+     */
+    public boolean isVolumeControlEnabled() {
+        return mVolumeControlEnabled;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        setTitle(mRoute.getName());
+        Resources res = getContext().getResources();
+        setButton(BUTTON_NEGATIVE, res.getString(R.string.media_route_controller_disconnect),
+                new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialogInterface, int id) {
+                        if (mRoute.isSelected()) {
+                            if (mRoute.isBluetooth()) {
+                                mRouter.getDefaultRoute().select();
+                            } else {
+                                mRouter.getFallbackRoute().select();
+                            }
+                        }
+                        dismiss();
+                    }
+                });
+        View customView = getLayoutInflater().inflate(R.layout.media_route_controller_dialog, null);
+        setView(customView, 0, 0, 0, 0);
+        super.onCreate(savedInstanceState);
+
+        View customPanelView = getWindow().findViewById(R.id.customPanel);
+        if (customPanelView != null) {
+            customPanelView.setMinimumHeight(0);
+        }
+        mVolumeLayout = (LinearLayout) customView.findViewById(R.id.media_route_volume_layout);
+        mVolumeSlider = (SeekBar) customView.findViewById(R.id.media_route_volume_slider);
+        mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            private final Runnable mStopTrackingTouch = new Runnable() {
+                @Override
+                public void run() {
+                    if (mVolumeSliderTouched) {
+                        mVolumeSliderTouched = false;
+                        updateVolume();
+                    }
+                }
+            };
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+                if (mVolumeSliderTouched) {
+                    mVolumeSlider.removeCallbacks(mStopTrackingTouch);
+                } else {
+                    mVolumeSliderTouched = true;
+                }
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                // Defer resetting mVolumeSliderTouched to allow the media route provider
+                // a little time to settle into its new state and publish the final
+                // volume update.
+                mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS);
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (fromUser) {
+                    mRoute.requestSetVolume(progress);
+                }
+            }
+        });
+
+        mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable();
+        mCreated = true;
+        if (update()) {
+            mControlView = onCreateMediaControlView(savedInstanceState);
+            FrameLayout controlFrame =
+                    (FrameLayout) customView.findViewById(R.id.media_route_control_frame);
+            if (mControlView != null) {
+                controlFrame.addView(mControlView);
+                controlFrame.setVisibility(View.VISIBLE);
+            } else {
+                controlFrame.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAttachedToWindow = true;
+
+        mRouter.addCallback(0, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
+        update();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        mRouter.removeCallback(mCallback);
+        mAttachedToWindow = false;
+
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+            mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    private boolean update() {
+        if (!mRoute.isSelected() || mRoute.isDefault()) {
+            dismiss();
+            return false;
+        }
+
+        setTitle(mRoute.getName());
+        updateVolume();
+
+        Drawable icon = getIconDrawable();
+        if (icon != mCurrentIconDrawable) {
+            mCurrentIconDrawable = icon;
+            if (icon instanceof AnimationDrawable) {
+                AnimationDrawable animDrawable = (AnimationDrawable) icon;
+                if (!mAttachedToWindow && !mRoute.isConnecting()) {
+                    // When the route is already connected before the view is attached, show the
+                    // last frame of the connected animation immediately.
+                    if (animDrawable.isRunning()) {
+                        animDrawable.stop();
+                    }
+                    icon = animDrawable.getFrame(animDrawable.getNumberOfFrames() - 1);
+                } else if (!animDrawable.isRunning()) {
+                    animDrawable.start();
+                }
+            }
+            setIcon(icon);
+        }
+        return true;
+    }
+
+    private Drawable obtainMediaRouteButtonDrawable() {
+        Context context = getContext();
+        TypedValue value = new TypedValue();
+        if (!context.getTheme().resolveAttribute(R.attr.mediaRouteButtonStyle, value, true)) {
+            return null;
+        }
+        int[] drawableAttrs = new int[] { R.attr.externalRouteEnabledDrawable };
+        TypedArray a = context.obtainStyledAttributes(value.data, drawableAttrs);
+        Drawable drawable = a.getDrawable(0);
+        a.recycle();
+        return drawable;
+    }
+
+    private Drawable getIconDrawable() {
+        if (!(mMediaRouteButtonDrawable instanceof StateListDrawable)) {
+            return mMediaRouteButtonDrawable;
+        } else if (mRoute.isConnecting()) {
+            StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable;
+            stateListDrawable.setState(mMediaRouteConnectingState);
+            return stateListDrawable.getCurrent();
+        } else {
+            StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable;
+            stateListDrawable.setState(mMediaRouteOnState);
+            return stateListDrawable.getCurrent();
+        }
+    }
+
+    private void updateVolume() {
+        if (!mVolumeSliderTouched) {
+            if (isVolumeControlAvailable()) {
+                mVolumeLayout.setVisibility(View.VISIBLE);
+                mVolumeSlider.setMax(mRoute.getVolumeMax());
+                mVolumeSlider.setProgress(mRoute.getVolume());
+            } else {
+                mVolumeLayout.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private boolean isVolumeControlAvailable() {
+        return mVolumeControlEnabled && mRoute.getVolumeHandling() ==
+                MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
+    }
+
+    private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
+        @Override
+        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
+            update();
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+            update();
+        }
+
+        @Override
+        public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+            if (route == mRoute) {
+                updateVolume();
+            }
+        }
+
+        @Override
+        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
+                int index) {
+            update();
+        }
+
+        @Override
+        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
+            update();
+        }
+    }
+}
diff --git a/com/android/internal/app/MediaRouteControllerDialogFragment.java b/com/android/internal/app/MediaRouteControllerDialogFragment.java
new file mode 100644
index 0000000..4c30501
--- /dev/null
+++ b/com/android/internal/app/MediaRouteControllerDialogFragment.java
@@ -0,0 +1,59 @@
+/*
+ * 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.internal.app;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.os.Bundle;
+
+/**
+ * Media route controller dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteControllerDialog}.  The application may subclass
+ * this dialog fragment to customize the media route controller dialog.
+ * </p>
+ *
+ * TODO: Move this back into the API, as in the support library media router.
+ */
+public class MediaRouteControllerDialogFragment extends DialogFragment {
+    /**
+     * Creates a media route controller dialog fragment.
+     * <p>
+     * All subclasses of this class must also possess a default constructor.
+     * </p>
+     */
+    public MediaRouteControllerDialogFragment() {
+        setCancelable(true);
+    }
+
+    /**
+     * Called when the controller dialog is being created.
+     * <p>
+     * Subclasses may override this method to customize the dialog.
+     * </p>
+     */
+    public MediaRouteControllerDialog onCreateControllerDialog(
+            Context context, Bundle savedInstanceState) {
+        return new MediaRouteControllerDialog(context, getTheme());
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return onCreateControllerDialog(getContext(), savedInstanceState);
+    }
+}
diff --git a/com/android/internal/app/MediaRouteDialogPresenter.java b/com/android/internal/app/MediaRouteDialogPresenter.java
new file mode 100644
index 0000000..bb2d7fa
--- /dev/null
+++ b/com/android/internal/app/MediaRouteDialogPresenter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.internal.app;
+
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.media.MediaRouter;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * Shows media route dialog as appropriate.
+ * @hide
+ */
+public abstract class MediaRouteDialogPresenter {
+    private static final String TAG = "MediaRouter";
+
+    private static final String CHOOSER_FRAGMENT_TAG =
+            "android.app.MediaRouteButton:MediaRouteChooserDialogFragment";
+    private static final String CONTROLLER_FRAGMENT_TAG =
+            "android.app.MediaRouteButton:MediaRouteControllerDialogFragment";
+
+    public static DialogFragment showDialogFragment(Activity activity,
+            int routeTypes, View.OnClickListener extendedSettingsClickListener) {
+        final MediaRouter router = (MediaRouter)activity.getSystemService(
+                Context.MEDIA_ROUTER_SERVICE);
+        final FragmentManager fm = activity.getFragmentManager();
+
+        MediaRouter.RouteInfo route = router.getSelectedRoute();
+        if (route.isDefault() || !route.matchesTypes(routeTypes)) {
+            if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
+                Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
+                return null;
+            }
+            MediaRouteChooserDialogFragment f = new MediaRouteChooserDialogFragment();
+            f.setRouteTypes(routeTypes);
+            f.setExtendedSettingsClickListener(extendedSettingsClickListener);
+            f.show(fm, CHOOSER_FRAGMENT_TAG);
+            return f;
+        } else {
+            if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
+                Log.w(TAG, "showDialog(): Route controller dialog already showing!");
+                return null;
+            }
+            MediaRouteControllerDialogFragment f = new MediaRouteControllerDialogFragment();
+            f.show(fm, CONTROLLER_FRAGMENT_TAG);
+            return f;
+        }
+    }
+
+    public static Dialog createDialog(Context context,
+            int routeTypes, View.OnClickListener extendedSettingsClickListener) {
+        final MediaRouter router = (MediaRouter)context.getSystemService(
+                Context.MEDIA_ROUTER_SERVICE);
+
+        int theme = MediaRouteChooserDialog.isLightTheme(context)
+                ? android.R.style.Theme_DeviceDefault_Light_Dialog
+                : android.R.style.Theme_DeviceDefault_Dialog;
+
+        MediaRouter.RouteInfo route = router.getSelectedRoute();
+        if (route.isDefault() || !route.matchesTypes(routeTypes)) {
+            final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme);
+            d.setRouteTypes(routeTypes);
+            d.setExtendedSettingsClickListener(extendedSettingsClickListener);
+            return d;
+        } else {
+            MediaRouteControllerDialog d = new MediaRouteControllerDialog(context, theme);
+            return d;
+        }
+    }
+}
diff --git a/com/android/internal/app/MicroAlertController.java b/com/android/internal/app/MicroAlertController.java
new file mode 100644
index 0000000..4431f3c
--- /dev/null
+++ b/com/android/internal/app/MicroAlertController.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.internal.app.AlertController;
+import com.android.internal.R;
+
+public class MicroAlertController extends AlertController {
+    public MicroAlertController(Context context, DialogInterface di, Window window) {
+        super(context, di, window);
+    }
+
+    @Override
+    protected void setupContent(ViewGroup contentPanel) {
+        // Special case for small screen - the scroll view is higher in hierarchy
+        mScrollView = (ScrollView) mWindow.findViewById(R.id.scrollView);
+
+        // Special case for users that only want to display a String
+        mMessageView = (TextView) contentPanel.findViewById(R.id.message);
+        if (mMessageView == null) {
+            return;
+        }
+
+        if (mMessage != null) {
+            mMessageView.setText(mMessage);
+        } else {
+            // no message, remove associated views
+            mMessageView.setVisibility(View.GONE);
+            contentPanel.removeView(mMessageView);
+
+            if (mListView != null) {
+                // has ListView, swap scrollView with ListView
+
+                // move topPanel into top of scrollParent
+                View topPanel = mScrollView.findViewById(R.id.topPanel);
+                ((ViewGroup) topPanel.getParent()).removeView(topPanel);
+                FrameLayout.LayoutParams topParams =
+                        new FrameLayout.LayoutParams(topPanel.getLayoutParams());
+                topParams.gravity = Gravity.TOP;
+                topPanel.setLayoutParams(topParams);
+
+                // move buttonPanel into bottom of scrollParent
+                View buttonPanel = mScrollView.findViewById(R.id.buttonPanel);
+                ((ViewGroup) buttonPanel.getParent()).removeView(buttonPanel);
+                FrameLayout.LayoutParams buttonParams =
+                        new FrameLayout.LayoutParams(buttonPanel.getLayoutParams());
+                buttonParams.gravity = Gravity.BOTTOM;
+                buttonPanel.setLayoutParams(buttonParams);
+
+                // remove scrollview
+                final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent();
+                final int childIndex = scrollParent.indexOfChild(mScrollView);
+                scrollParent.removeViewAt(childIndex);
+
+                // add list view
+                scrollParent.addView(mListView,
+                        new ViewGroup.LayoutParams(
+                                ViewGroup.LayoutParams.MATCH_PARENT,
+                                ViewGroup.LayoutParams.MATCH_PARENT));
+
+                // add top and button panel
+                scrollParent.addView(topPanel);
+                scrollParent.addView(buttonPanel);
+            } else {
+                // no content, just hide everything
+                contentPanel.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    protected void setupTitle(ViewGroup topPanel) {
+        super.setupTitle(topPanel);
+        if (topPanel.getVisibility() == View.GONE) {
+            topPanel.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    @Override
+    protected void setupButtons(ViewGroup buttonPanel) {
+        super.setupButtons(buttonPanel);
+        if (buttonPanel.getVisibility() == View.GONE) {
+            buttonPanel.setVisibility(View.INVISIBLE);
+        }
+    }
+}
diff --git a/com/android/internal/app/NavItemSelectedListener.java b/com/android/internal/app/NavItemSelectedListener.java
new file mode 100644
index 0000000..545f44b
--- /dev/null
+++ b/com/android/internal/app/NavItemSelectedListener.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.internal.app;
+
+import android.app.ActionBar;
+import android.view.View;
+import android.widget.AdapterView;
+
+/**
+ * Wrapper to adapt the ActionBar.OnNavigationListener in an AdapterView.OnItemSelectedListener
+ * for use in Spinner widgets. Used by action bar implementations.
+ */
+class NavItemSelectedListener implements AdapterView.OnItemSelectedListener {
+    private final ActionBar.OnNavigationListener mListener;
+
+    public NavItemSelectedListener(ActionBar.OnNavigationListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        if (mListener != null) {
+            mListener.onNavigationItemSelected(position, id);
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        // Do nothing
+    }
+}
diff --git a/com/android/internal/app/NetInitiatedActivity.java b/com/android/internal/app/NetInitiatedActivity.java
new file mode 100644
index 0000000..d3bae16
--- /dev/null
+++ b/com/android/internal/app/NetInitiatedActivity.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2007 Google Inc.
+ *
+ * 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.internal.app;
+
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.widget.Toast;
+import android.util.Log;
+import android.location.LocationManager;
+
+import com.android.internal.R;
+import com.android.internal.location.GpsNetInitiatedHandler;
+
+/**
+ * This activity is shown to the user for him/her to accept or deny network-initiated
+ * requests. It uses the alert dialog style. It will be launched from a notification.
+ */
+public class NetInitiatedActivity extends AlertActivity implements DialogInterface.OnClickListener {
+
+    private static final String TAG = "NetInitiatedActivity";
+
+    private static final boolean DEBUG = true;
+    private static final boolean VERBOSE = false;
+
+    private static final int POSITIVE_BUTTON = AlertDialog.BUTTON_POSITIVE;
+    private static final int NEGATIVE_BUTTON = AlertDialog.BUTTON_NEGATIVE;
+
+    private static final int GPS_NO_RESPONSE_TIME_OUT = 1;
+    // Received ID from intent, -1 when no notification is in progress
+    private int notificationId = -1;
+    private int timeout = -1;
+    private int default_response = -1;
+    private int default_response_timeout = 6;
+
+    /** Used to detect when NI request is received */
+    private BroadcastReceiver mNetInitiatedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) Log.d(TAG, "NetInitiatedReceiver onReceive: " + intent.getAction());
+            if (intent.getAction() == GpsNetInitiatedHandler.ACTION_NI_VERIFY) {
+                handleNIVerify(intent);
+            }
+        }
+    };
+
+    private final Handler mHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+            case GPS_NO_RESPONSE_TIME_OUT: {
+                if (notificationId != -1) {
+                    sendUserResponse(default_response);
+                }
+                finish();
+            }
+            break;
+            default:
+            }
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Set up the "dialog"
+        final Intent intent = getIntent();
+        final AlertController.AlertParams p = mAlertParams;
+        Context context = getApplicationContext();
+        p.mTitle = intent.getStringExtra(GpsNetInitiatedHandler.NI_INTENT_KEY_TITLE);
+        p.mMessage = intent.getStringExtra(GpsNetInitiatedHandler.NI_INTENT_KEY_MESSAGE);
+        p.mPositiveButtonText = String.format(context.getString(R.string.gpsVerifYes));
+        p.mPositiveButtonListener = this;
+        p.mNegativeButtonText = String.format(context.getString(R.string.gpsVerifNo));
+        p.mNegativeButtonListener = this;
+
+        notificationId = intent.getIntExtra(GpsNetInitiatedHandler.NI_INTENT_KEY_NOTIF_ID, -1);
+        timeout = intent.getIntExtra(GpsNetInitiatedHandler.NI_INTENT_KEY_TIMEOUT, default_response_timeout);
+        default_response = intent.getIntExtra(GpsNetInitiatedHandler.NI_INTENT_KEY_DEFAULT_RESPONSE, GpsNetInitiatedHandler.GPS_NI_RESPONSE_ACCEPT);
+        if (DEBUG) Log.d(TAG, "onCreate() : notificationId: " + notificationId + " timeout: " + timeout + " default_response:" + default_response);
+
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(GPS_NO_RESPONSE_TIME_OUT), (timeout * 1000));
+        setupAlert();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (DEBUG) Log.d(TAG, "onResume");
+        registerReceiver(mNetInitiatedReceiver, new IntentFilter(GpsNetInitiatedHandler.ACTION_NI_VERIFY));
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if (DEBUG) Log.d(TAG, "onPause");
+        unregisterReceiver(mNetInitiatedReceiver);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void onClick(DialogInterface dialog, int which) {
+        if (which == POSITIVE_BUTTON) {
+            sendUserResponse(GpsNetInitiatedHandler.GPS_NI_RESPONSE_ACCEPT);
+        }
+        if (which == NEGATIVE_BUTTON) {
+            sendUserResponse(GpsNetInitiatedHandler.GPS_NI_RESPONSE_DENY);
+        }
+
+        // No matter what, finish the activity
+        finish();
+        notificationId = -1;
+    }
+
+    // Respond to NI Handler under GnssLocationProvider, 1 = accept, 2 = deny
+    private void sendUserResponse(int response) {
+        if (DEBUG) Log.d(TAG, "sendUserResponse, response: " + response);
+        LocationManager locationManager = (LocationManager)
+            this.getSystemService(Context.LOCATION_SERVICE);
+        locationManager.sendNiResponse(notificationId, response);
+    }
+
+    private void handleNIVerify(Intent intent) {
+        int notifId = intent.getIntExtra(GpsNetInitiatedHandler.NI_INTENT_KEY_NOTIF_ID, -1);
+        notificationId = notifId;
+
+        if (DEBUG) Log.d(TAG, "handleNIVerify action: " + intent.getAction());
+    }
+
+    private void showNIError() {
+        Toast.makeText(this, "NI error" /* com.android.internal.R.string.usb_storage_error_message */,
+                Toast.LENGTH_LONG).show();
+    }
+}
diff --git a/com/android/internal/app/NightDisplayController.java b/com/android/internal/app/NightDisplayController.java
new file mode 100644
index 0000000..860c5c4
--- /dev/null
+++ b/com/android/internal/app/NightDisplayController.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings.Secure;
+import android.util.Slog;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Controller for managing Night display settings.
+ * <p/>
+ * Night display tints your screen red at night. This makes it easier to look at your screen in
+ * dim light and may help you fall asleep more easily.
+ */
+public final class NightDisplayController {
+
+    private static final String TAG = "NightDisplayController";
+    private static final boolean DEBUG = false;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ AUTO_MODE_DISABLED, AUTO_MODE_CUSTOM, AUTO_MODE_TWILIGHT })
+    public @interface AutoMode {}
+
+    /**
+     * Auto mode value to prevent Night display from being automatically activated. It can still
+     * be activated manually via {@link #setActivated(boolean)}.
+     *
+     * @see #setAutoMode(int)
+     */
+    public static final int AUTO_MODE_DISABLED = 0;
+    /**
+     * Auto mode value to automatically activate Night display at a specific start and end time.
+     *
+     * @see #setAutoMode(int)
+     * @see #setCustomStartTime(LocalTime)
+     * @see #setCustomEndTime(LocalTime)
+     */
+    public static final int AUTO_MODE_CUSTOM = 1;
+    /**
+     * Auto mode value to automatically activate Night display from sunset to sunrise.
+     *
+     * @see #setAutoMode(int)
+     */
+    public static final int AUTO_MODE_TWILIGHT = 2;
+
+    private final Context mContext;
+    private final int mUserId;
+
+    private final ContentObserver mContentObserver;
+
+    private Callback mCallback;
+
+    public NightDisplayController(@NonNull Context context) {
+        this(context, ActivityManager.getCurrentUser());
+    }
+
+    public NightDisplayController(@NonNull Context context, int userId) {
+        mContext = context.getApplicationContext();
+        mUserId = userId;
+
+        mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                super.onChange(selfChange, uri);
+
+                final String setting = uri == null ? null : uri.getLastPathSegment();
+                if (setting != null) {
+                    onSettingChanged(setting);
+                }
+            }
+        };
+    }
+
+    /**
+     * Returns {@code true} when Night display is activated (the display is tinted red).
+     */
+    public boolean isActivated() {
+        return Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_ACTIVATED, 0, mUserId) == 1;
+    }
+
+    /**
+     * Sets whether Night display should be activated. This also sets the last activated time.
+     *
+     * @param activated {@code true} if Night display should be activated
+     * @return {@code true} if the activated value was set successfully
+     */
+    public boolean setActivated(boolean activated) {
+        if (isActivated() != activated) {
+            Secure.putLongForUser(mContext.getContentResolver(),
+                    Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME, System.currentTimeMillis(),
+                    mUserId);
+        }
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_ACTIVATED, activated ? 1 : 0, mUserId);
+    }
+
+    /**
+     * Returns the time when Night display's activation state last changed, or {@code null} if it
+     * has never been changed.
+     */
+    public Calendar getLastActivatedTime() {
+        final ContentResolver cr = mContext.getContentResolver();
+        final long lastActivatedTimeMillis = Secure.getLongForUser(
+                cr, Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME, -1, mUserId);
+        if (lastActivatedTimeMillis < 0) {
+            return null;
+        }
+
+        final Calendar lastActivatedTime = Calendar.getInstance();
+        lastActivatedTime.setTimeInMillis(lastActivatedTimeMillis);
+        return lastActivatedTime;
+    }
+
+    /**
+     * Returns the current auto mode value controlling when Night display will be automatically
+     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
+     * {@link #AUTO_MODE_TWILIGHT}.
+     */
+    public @AutoMode int getAutoMode() {
+        int autoMode = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_AUTO_MODE, -1, mUserId);
+        if (autoMode == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: " + Secure.NIGHT_DISPLAY_AUTO_MODE);
+            }
+            autoMode = mContext.getResources().getInteger(
+                    R.integer.config_defaultNightDisplayAutoMode);
+        }
+
+        if (autoMode != AUTO_MODE_DISABLED
+                && autoMode != AUTO_MODE_CUSTOM
+                && autoMode != AUTO_MODE_TWILIGHT) {
+            Slog.e(TAG, "Invalid autoMode: " + autoMode);
+            autoMode = AUTO_MODE_DISABLED;
+        }
+
+        return autoMode;
+    }
+
+    /**
+     * Sets the current auto mode value controlling when Night display will be automatically
+     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
+     * {@link #AUTO_MODE_TWILIGHT}.
+     *
+     * @param autoMode the new auto mode to use
+     * @return {@code true} if new auto mode was set successfully
+     */
+    public boolean setAutoMode(@AutoMode int autoMode) {
+        if (autoMode != AUTO_MODE_DISABLED
+                && autoMode != AUTO_MODE_CUSTOM
+                && autoMode != AUTO_MODE_TWILIGHT) {
+            throw new IllegalArgumentException("Invalid autoMode: " + autoMode);
+        }
+
+        if (getAutoMode() != autoMode) {
+            Secure.putLongForUser(mContext.getContentResolver(),
+                    Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME, -1L, mUserId);
+        }
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_AUTO_MODE, autoMode, mUserId);
+    }
+
+    /**
+     * Returns the local time when Night display will be automatically activated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     */
+    public @NonNull LocalTime getCustomStartTime() {
+        int startTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, -1, mUserId);
+        if (startTimeValue == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: "
+                        + Secure.NIGHT_DISPLAY_CUSTOM_START_TIME);
+            }
+            startTimeValue = mContext.getResources().getInteger(
+                    R.integer.config_defaultNightDisplayCustomStartTime);
+        }
+
+        return LocalTime.valueOf(startTimeValue);
+    }
+
+    /**
+     * Sets the local time when Night display will be automatically activated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     *
+     * @param startTime the local time to automatically activate Night display
+     * @return {@code true} if the new custom start time was set successfully
+     */
+    public boolean setCustomStartTime(@NonNull LocalTime startTime) {
+        if (startTime == null) {
+            throw new IllegalArgumentException("startTime cannot be null");
+        }
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, startTime.toMillis(), mUserId);
+    }
+
+    /**
+     * Returns the local time when Night display will be automatically deactivated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     */
+    public @NonNull LocalTime getCustomEndTime() {
+        int endTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, -1, mUserId);
+        if (endTimeValue == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: "
+                        + Secure.NIGHT_DISPLAY_CUSTOM_END_TIME);
+            }
+            endTimeValue = mContext.getResources().getInteger(
+                    R.integer.config_defaultNightDisplayCustomEndTime);
+        }
+
+        return LocalTime.valueOf(endTimeValue);
+    }
+
+    /**
+     * Sets the local time when Night display will be automatically deactivated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     *
+     * @param endTime the local time to automatically deactivate Night display
+     * @return {@code true} if the new custom end time was set successfully
+     */
+    public boolean setCustomEndTime(@NonNull LocalTime endTime) {
+        if (endTime == null) {
+            throw new IllegalArgumentException("endTime cannot be null");
+        }
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, endTime.toMillis(), mUserId);
+    }
+
+    /**
+     * Returns the color temperature (in Kelvin) to tint the display when activated.
+     */
+    public int getColorTemperature() {
+        int colorTemperature = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, -1, mUserId);
+        if (colorTemperature == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: "
+                    + Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE);
+            }
+            colorTemperature = getDefaultColorTemperature();
+        }
+        final int minimumTemperature = getMinimumColorTemperature();
+        final int maximumTemperature = getMaximumColorTemperature();
+        if (colorTemperature < minimumTemperature) {
+            colorTemperature = minimumTemperature;
+        } else if (colorTemperature > maximumTemperature) {
+            colorTemperature = maximumTemperature;
+        }
+
+        return colorTemperature;
+    }
+
+    /**
+     * Sets the current temperature.
+     *
+     * @param colorTemperature the temperature, in Kelvin.
+     * @return {@code true} if new temperature was set successfully.
+     */
+    public boolean setColorTemperature(int colorTemperature) {
+        return Secure.putIntForUser(mContext.getContentResolver(),
+            Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, colorTemperature, mUserId);
+    }
+
+    /**
+     * Returns the minimum allowed color temperature (in Kelvin) to tint the display when activated.
+     */
+    public int getMinimumColorTemperature() {
+        return mContext.getResources().getInteger(
+                R.integer.config_nightDisplayColorTemperatureMin);
+    }
+
+    /**
+     * Returns the maximum allowed color temperature (in Kelvin) to tint the display when activated.
+     */
+    public int getMaximumColorTemperature() {
+        return mContext.getResources().getInteger(
+                R.integer.config_nightDisplayColorTemperatureMax);
+    }
+
+    /**
+     * Returns the default color temperature (in Kelvin) to tint the display when activated.
+     */
+    public int getDefaultColorTemperature() {
+        return mContext.getResources().getInteger(
+                R.integer.config_nightDisplayColorTemperatureDefault);
+    }
+
+    private void onSettingChanged(@NonNull String setting) {
+        if (DEBUG) {
+            Slog.d(TAG, "onSettingChanged: " + setting);
+        }
+
+        if (mCallback != null) {
+            switch (setting) {
+                case Secure.NIGHT_DISPLAY_ACTIVATED:
+                    mCallback.onActivated(isActivated());
+                    break;
+                case Secure.NIGHT_DISPLAY_AUTO_MODE:
+                    mCallback.onAutoModeChanged(getAutoMode());
+                    break;
+                case Secure.NIGHT_DISPLAY_CUSTOM_START_TIME:
+                    mCallback.onCustomStartTimeChanged(getCustomStartTime());
+                    break;
+                case Secure.NIGHT_DISPLAY_CUSTOM_END_TIME:
+                    mCallback.onCustomEndTimeChanged(getCustomEndTime());
+                    break;
+                case Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE:
+                    mCallback.onColorTemperatureChanged(getColorTemperature());
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Register a callback to be invoked whenever the Night display settings are changed.
+     */
+    public void setListener(Callback callback) {
+        final Callback oldCallback = mCallback;
+        if (oldCallback != callback) {
+            mCallback = callback;
+
+            if (callback == null) {
+                // Stop listening for changes now that there IS NOT a listener.
+                mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+            } else if (oldCallback == null) {
+                // Start listening for changes now that there IS a listener.
+                final ContentResolver cr = mContext.getContentResolver();
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_ACTIVATED),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_AUTO_MODE),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_START_TIME),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_END_TIME),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if Night display is supported by the device.
+     */
+    public static boolean isAvailable(Context context) {
+        return context.getResources().getBoolean(R.bool.config_nightDisplayAvailable);
+    }
+
+    /**
+     * A time without a time-zone or date.
+     */
+    public static class LocalTime {
+
+        /**
+         * The hour of the day from 0 - 23.
+         */
+        public final int hourOfDay;
+        /**
+         * The minute within the hour from 0 - 59.
+         */
+        public final int minute;
+
+        public LocalTime(int hourOfDay, int minute) {
+            if (hourOfDay < 0 || hourOfDay > 23) {
+                throw new IllegalArgumentException("Invalid hourOfDay: " + hourOfDay);
+            } else if (minute < 0 || minute > 59) {
+                throw new IllegalArgumentException("Invalid minute: " + minute);
+            }
+
+            this.hourOfDay = hourOfDay;
+            this.minute = minute;
+        }
+
+        /**
+         * Returns the first date time corresponding to this local time that occurs before the
+         * provided date time.
+         *
+         * @param time the date time to compare against
+         * @return the prior date time corresponding to this local time
+         */
+        public Calendar getDateTimeBefore(Calendar time) {
+            final Calendar c = Calendar.getInstance();
+            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
+            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
+
+            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
+            c.set(Calendar.MINUTE, minute);
+            c.set(Calendar.SECOND, 0);
+            c.set(Calendar.MILLISECOND, 0);
+
+            // Check if the local time has past, if so return the same time tomorrow.
+            if (c.after(time)) {
+                c.add(Calendar.DATE, -1);
+            }
+
+            return c;
+        }
+
+        /**
+         * Returns the first date time corresponding to this local time that occurs after the
+         * provided date time.
+         *
+         * @param time the date time to compare against
+         * @return the next date time corresponding to this local time
+         */
+        public Calendar getDateTimeAfter(Calendar time) {
+            final Calendar c = Calendar.getInstance();
+            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
+            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
+
+            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
+            c.set(Calendar.MINUTE, minute);
+            c.set(Calendar.SECOND, 0);
+            c.set(Calendar.MILLISECOND, 0);
+
+            // Check if the local time has past, if so return the same time tomorrow.
+            if (c.before(time)) {
+                c.add(Calendar.DATE, 1);
+            }
+
+            return c;
+        }
+
+        /**
+         * Returns a local time corresponding the given number of milliseconds from midnight.
+         *
+         * @param millis the number of milliseconds from midnight
+         * @return the corresponding local time
+         */
+        private static LocalTime valueOf(int millis) {
+            final int hourOfDay = (millis / 3600000) % 24;
+            final int minutes = (millis / 60000) % 60;
+            return new LocalTime(hourOfDay, minutes);
+        }
+
+        /**
+         * Returns the local time represented as milliseconds from midnight.
+         */
+        private int toMillis() {
+            return hourOfDay * 3600000 + minute * 60000;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.US, "%02d:%02d", hourOfDay, minute);
+        }
+    }
+
+    /**
+     * Callback invoked whenever the Night display settings are changed.
+     */
+    public interface Callback {
+        /**
+         * Callback invoked when the activated state changes.
+         *
+         * @param activated {@code true} if Night display is activated
+         */
+        default void onActivated(boolean activated) {}
+        /**
+         * Callback invoked when the auto mode changes.
+         *
+         * @param autoMode the auto mode to use
+         */
+        default void onAutoModeChanged(int autoMode) {}
+        /**
+         * Callback invoked when the time to automatically activate Night display changes.
+         *
+         * @param startTime the local time to automatically activate Night display
+         */
+        default void onCustomStartTimeChanged(LocalTime startTime) {}
+        /**
+         * Callback invoked when the time to automatically deactivate Night display changes.
+         *
+         * @param endTime the local time to automatically deactivate Night display
+         */
+        default void onCustomEndTimeChanged(LocalTime endTime) {}
+
+        /**
+         * Callback invoked when the color temperature changes.
+         *
+         * @param colorTemperature the color temperature to tint the screen
+         */
+        default void onColorTemperatureChanged(int colorTemperature) {}
+    }
+}
diff --git a/com/android/internal/app/PlatLogoActivity.java b/com/android/internal/app/PlatLogoActivity.java
new file mode 100644
index 0000000..b22ce5e
--- /dev/null
+++ b/com/android/internal/app/PlatLogoActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2010 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.internal.app;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.OvalShape;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+public class PlatLogoActivity extends Activity {
+    public static final boolean FINISH = true;
+
+    FrameLayout mLayout;
+    int mTapCount;
+    int mKeyCount;
+    PathInterpolator mInterpolator = new PathInterpolator(0f, 0f, 0.5f, 1f);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mLayout = new FrameLayout(this);
+        setContentView(mLayout);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        final DisplayMetrics dm = getResources().getDisplayMetrics();
+        final float dp = dm.density;
+        final int size = (int)
+                (Math.min(Math.min(dm.widthPixels, dm.heightPixels), 600*dp) - 100*dp);
+
+        final ImageView im = new ImageView(this);
+        final int pad = (int)(40*dp);
+        im.setPadding(pad, pad, pad, pad);
+        im.setTranslationZ(20);
+        im.setScaleX(0.5f);
+        im.setScaleY(0.5f);
+        im.setAlpha(0f);
+
+        im.setBackground(new RippleDrawable(
+                ColorStateList.valueOf(0xFF776677),
+                getDrawable(com.android.internal.R.drawable.platlogo),
+                null));
+        im.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                final int w = view.getWidth();
+                final int h = view.getHeight();
+                outline.setOval((int)(w*.125), (int)(h*.125), (int)(w*.96), (int)(h*.96));
+            }
+        });
+        im.setElevation(12f*dp);
+        im.setClickable(true);
+        im.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                im.setOnLongClickListener(new View.OnLongClickListener() {
+                    @Override
+                    public boolean onLongClick(View v) {
+                        if (mTapCount < 5) return false;
+
+                        final ContentResolver cr = getContentResolver();
+                        if (Settings.System.getLong(cr, Settings.System.EGG_MODE, 0)
+                                == 0) {
+                            // For posterity: the moment this user unlocked the easter egg
+                            try {
+                                Settings.System.putLong(cr,
+                                        Settings.System.EGG_MODE,
+                                        System.currentTimeMillis());
+                            } catch (RuntimeException e) {
+                                Log.e("PlatLogoActivity", "Can't write settings", e);
+                            }
+                        }
+                        im.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                try {
+                                    startActivity(new Intent(Intent.ACTION_MAIN)
+                                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                                                    | Intent.FLAG_ACTIVITY_CLEAR_TASK
+                                                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                                            .addCategory("com.android.internal.category.PLATLOGO"));
+                                } catch (ActivityNotFoundException ex) {
+                                    Log.e("PlatLogoActivity", "No more eggs.");
+                                }
+                                if (FINISH) finish();
+                            }
+                        });
+                        return true;
+                    }
+                });
+                mTapCount++;
+            }
+        });
+
+        // Enable hardware keyboard input for TV compatibility.
+        im.setFocusable(true);
+        im.requestFocus();
+        im.setOnKeyListener(new View.OnKeyListener() {
+            @Override
+            public boolean onKey(View v, int keyCode, KeyEvent event) {
+                if (keyCode != KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+                    ++mKeyCount;
+                    if (mKeyCount > 2) {
+                        if (mTapCount > 5) {
+                            im.performLongClick();
+                        } else {
+                            im.performClick();
+                        }
+                    }
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        });
+
+        mLayout.addView(im, new FrameLayout.LayoutParams(size, size, Gravity.CENTER));
+
+        im.animate().scaleX(1f).scaleY(1f).alpha(1f)
+                .setInterpolator(mInterpolator)
+                .setDuration(500)
+                .setStartDelay(800)
+                .start();
+    }
+}
diff --git a/com/android/internal/app/ProcessMap.java b/com/android/internal/app/ProcessMap.java
new file mode 100644
index 0000000..81036f7
--- /dev/null
+++ b/com/android/internal/app/ProcessMap.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2006 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.internal.app;
+
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+public class ProcessMap<E> {
+    final ArrayMap<String, SparseArray<E>> mMap
+            = new ArrayMap<String, SparseArray<E>>();
+    
+    public E get(String name, int uid) {
+        SparseArray<E> uids = mMap.get(name);
+        if (uids == null) return null;
+        return uids.get(uid);
+    }
+    
+    public E put(String name, int uid, E value) {
+        SparseArray<E> uids = mMap.get(name);
+        if (uids == null) {
+            uids = new SparseArray<E>(2);
+            mMap.put(name, uids);
+        }
+        uids.put(uid, value);
+        return value;
+    }
+    
+    public E remove(String name, int uid) {
+        SparseArray<E> uids = mMap.get(name);
+        if (uids != null) {
+            final E old = uids.removeReturnOld(uid);
+            if (uids.size() == 0) {
+                mMap.remove(name);
+            }
+            return old;
+        }
+        return null;
+    }
+    
+    public ArrayMap<String, SparseArray<E>> getMap() {
+        return mMap;
+    }
+
+    public int size() {
+        return mMap.size();
+    }
+}
diff --git a/com/android/internal/app/ResolverActivity.java b/com/android/internal/app/ResolverActivity.java
new file mode 100644
index 0000000..ceb06f5
--- /dev/null
+++ b/com/android/internal/app/ResolverActivity.java
@@ -0,0 +1,2065 @@
+/*
+ * Copyright (C) 2008 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.internal.app;
+
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.UiThread;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.app.VoiceInteractor.Prompt;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LabeledIntent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PatternMatcher;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.IconDrawableFactory;
+import android.util.Log;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.widget.ResolverDrawerLayout;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+/**
+ * This activity is displayed when the system attempts to start an Intent for
+ * which there is more than one matching activity, allowing the user to decide
+ * which to go to.  It is not normally used directly by application developers.
+ */
+@UiThread
+public class ResolverActivity extends Activity {
+
+    protected ResolveListAdapter mAdapter;
+    private boolean mSafeForwardingMode;
+    private AbsListView mAdapterView;
+    private Button mAlwaysButton;
+    private Button mOnceButton;
+    private View mProfileView;
+    private int mIconDpi;
+    private int mLastSelected = AbsListView.INVALID_POSITION;
+    private boolean mResolvingHome = false;
+    private int mProfileSwitchMessageId = -1;
+    private int mLayoutId;
+    private final ArrayList<Intent> mIntents = new ArrayList<>();
+    private PickTargetOptionRequest mPickOptionRequest;
+    private String mReferrerPackage;
+    private CharSequence mTitle;
+    private int mDefaultTitleResId;
+
+    // Whether or not this activity supports choosing a default handler for the intent.
+    private boolean mSupportsAlwaysUseOption;
+    protected ResolverDrawerLayout mResolverDrawerLayout;
+    protected PackageManager mPm;
+    protected int mLaunchedFromUid;
+
+    private static final String TAG = "ResolverActivity";
+    private static final boolean DEBUG = false;
+    private Runnable mPostListReadyRunnable;
+
+    private boolean mRegistered;
+
+    /** See {@link #setRetainInOnStop}. */
+    private boolean mRetainInOnStop;
+
+    IconDrawableFactory mIconFactory;
+
+    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
+        @Override public void onSomePackagesChanged() {
+            mAdapter.handlePackagesChanged();
+            if (mProfileView != null) {
+                bindProfileView();
+            }
+        }
+
+        @Override
+        public boolean onPackageChanged(String packageName, int uid, String[] components) {
+            // We care about all package changes, not just the whole package itself which is
+            // default behavior.
+            return true;
+        }
+    };
+
+    /**
+     * Get the string resource to be used as a label for the link to the resolver activity for an
+     * action.
+     *
+     * @param action The action to resolve
+     *
+     * @return The string resource to be used as a label
+     */
+    public static @StringRes int getLabelRes(String action) {
+        return ActionTitle.forAction(action).labelRes;
+    }
+
+    private enum ActionTitle {
+        VIEW(Intent.ACTION_VIEW,
+                com.android.internal.R.string.whichViewApplication,
+                com.android.internal.R.string.whichViewApplicationNamed,
+                com.android.internal.R.string.whichViewApplicationLabel),
+        EDIT(Intent.ACTION_EDIT,
+                com.android.internal.R.string.whichEditApplication,
+                com.android.internal.R.string.whichEditApplicationNamed,
+                com.android.internal.R.string.whichEditApplicationLabel),
+        SEND(Intent.ACTION_SEND,
+                com.android.internal.R.string.whichSendApplication,
+                com.android.internal.R.string.whichSendApplicationNamed,
+                com.android.internal.R.string.whichSendApplicationLabel),
+        SENDTO(Intent.ACTION_SENDTO,
+                com.android.internal.R.string.whichSendToApplication,
+                com.android.internal.R.string.whichSendToApplicationNamed,
+                com.android.internal.R.string.whichSendToApplicationLabel),
+        SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
+                com.android.internal.R.string.whichSendApplication,
+                com.android.internal.R.string.whichSendApplicationNamed,
+                com.android.internal.R.string.whichSendApplicationLabel),
+        CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
+                com.android.internal.R.string.whichImageCaptureApplication,
+                com.android.internal.R.string.whichImageCaptureApplicationNamed,
+                com.android.internal.R.string.whichImageCaptureApplicationLabel),
+        DEFAULT(null,
+                com.android.internal.R.string.whichApplication,
+                com.android.internal.R.string.whichApplicationNamed,
+                com.android.internal.R.string.whichApplicationLabel),
+        HOME(Intent.ACTION_MAIN,
+                com.android.internal.R.string.whichHomeApplication,
+                com.android.internal.R.string.whichHomeApplicationNamed,
+                com.android.internal.R.string.whichHomeApplicationLabel);
+
+        public final String action;
+        public final int titleRes;
+        public final int namedTitleRes;
+        public final @StringRes int labelRes;
+
+        ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
+            this.action = action;
+            this.titleRes = titleRes;
+            this.namedTitleRes = namedTitleRes;
+            this.labelRes = labelRes;
+        }
+
+        public static ActionTitle forAction(String action) {
+            for (ActionTitle title : values()) {
+                if (title != HOME && action != null && action.equals(title.action)) {
+                    return title;
+                }
+            }
+            return DEFAULT;
+        }
+    }
+
+    private Intent makeMyIntent() {
+        Intent intent = new Intent(getIntent());
+        intent.setComponent(null);
+        // The resolver activity is set to be hidden from recent tasks.
+        // we don't want this attribute to be propagated to the next activity
+        // being launched.  Note that if the original Intent also had this
+        // flag set, we are now losing it.  That should be a very rare case
+        // and we can live with this.
+        intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        return intent;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // Use a specialized prompt when we're handling the 'Home' app startActivity()
+        final Intent intent = makeMyIntent();
+        final Set<String> categories = intent.getCategories();
+        if (Intent.ACTION_MAIN.equals(intent.getAction())
+                && categories != null
+                && categories.size() == 1
+                && categories.contains(Intent.CATEGORY_HOME)) {
+            // Note: this field is not set to true in the compatibility version.
+            mResolvingHome = true;
+        }
+
+        setSafeForwardingMode(true);
+
+        onCreate(savedInstanceState, intent, null, 0, null, null, true);
+    }
+
+    /**
+     * Compatibility version for other bundled services that use this overload without
+     * a default title resource
+     */
+    protected void onCreate(Bundle savedInstanceState, Intent intent,
+            CharSequence title, Intent[] initialIntents,
+            List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
+        onCreate(savedInstanceState, intent, title, 0, initialIntents, rList,
+                supportsAlwaysUseOption);
+    }
+
+    protected void onCreate(Bundle savedInstanceState, Intent intent,
+            CharSequence title, int defaultTitleRes, Intent[] initialIntents,
+            List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
+        setTheme(R.style.Theme_DeviceDefault_Resolver);
+        super.onCreate(savedInstanceState);
+
+        // Determine whether we should show that intent is forwarded
+        // from managed profile to owner or other way around.
+        setProfileSwitchMessageId(intent.getContentUserHint());
+
+        try {
+            mLaunchedFromUid = ActivityManager.getService().getLaunchedFromUid(
+                    getActivityToken());
+        } catch (RemoteException e) {
+            mLaunchedFromUid = -1;
+        }
+
+        if (mLaunchedFromUid < 0 || UserHandle.isIsolated(mLaunchedFromUid)) {
+            // Gulp!
+            finish();
+            return;
+        }
+
+        mPm = getPackageManager();
+
+        mPackageMonitor.register(this, getMainLooper(), false);
+        mRegistered = true;
+        mReferrerPackage = getReferrerPackageName();
+        mSupportsAlwaysUseOption = supportsAlwaysUseOption;
+
+        final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
+        mIconDpi = am.getLauncherLargeIconDensity();
+
+        // Add our initial intent as the first item, regardless of what else has already been added.
+        mIntents.add(0, new Intent(intent));
+        mTitle = title;
+        mDefaultTitleResId = defaultTitleRes;
+
+        if (configureContentView(mIntents, initialIntents, rList)) {
+            return;
+        }
+
+        final ResolverDrawerLayout rdl = findViewById(R.id.contentPanel);
+        if (rdl != null) {
+            rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() {
+                @Override
+                public void onDismissed() {
+                    finish();
+                }
+            });
+            if (isVoiceInteraction()) {
+                rdl.setCollapsed(false);
+            }
+            mResolverDrawerLayout = rdl;
+        }
+
+        mProfileView = findViewById(R.id.profile_button);
+        if (mProfileView != null) {
+            mProfileView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    final DisplayResolveInfo dri = mAdapter.getOtherProfile();
+                    if (dri == null) {
+                        return;
+                    }
+
+                    // Do not show the profile switch message anymore.
+                    mProfileSwitchMessageId = -1;
+
+                    onTargetSelected(dri, false);
+                    finish();
+                }
+            });
+            bindProfileView();
+        }
+
+        if (isVoiceInteraction()) {
+            onSetupVoiceInteraction();
+        }
+        final Set<String> categories = intent.getCategories();
+        MetricsLogger.action(this, mAdapter.hasFilteredItem()
+                ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+                : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+                intent.getAction() + ":" + intent.getType() + ":"
+                        + (categories != null ? Arrays.toString(categories.toArray()) : ""));
+        mIconFactory = IconDrawableFactory.newInstance(this, true);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mAdapter.handlePackagesChanged();
+    }
+
+    /**
+     * Perform any initialization needed for voice interaction.
+     */
+    public void onSetupVoiceInteraction() {
+        // Do it right now. Subclasses may delay this and send it later.
+        sendVoiceChoicesIfNeeded();
+    }
+
+    public void sendVoiceChoicesIfNeeded() {
+        if (!isVoiceInteraction()) {
+            // Clearly not needed.
+            return;
+        }
+
+
+        final Option[] options = new Option[mAdapter.getCount()];
+        for (int i = 0, N = options.length; i < N; i++) {
+            options[i] = optionForChooserTarget(mAdapter.getItem(i), i);
+        }
+
+        mPickOptionRequest = new PickTargetOptionRequest(
+                new Prompt(getTitle()), options, null);
+        getVoiceInteractor().submitRequest(mPickOptionRequest);
+    }
+
+    Option optionForChooserTarget(TargetInfo target, int index) {
+        return new Option(target.getDisplayLabel(), index);
+    }
+
+    protected final void setAdditionalTargets(Intent[] intents) {
+        if (intents != null) {
+            for (Intent intent : intents) {
+                mIntents.add(intent);
+            }
+        }
+    }
+
+    public Intent getTargetIntent() {
+        return mIntents.isEmpty() ? null : mIntents.get(0);
+    }
+
+    protected String getReferrerPackageName() {
+        final Uri referrer = getReferrer();
+        if (referrer != null && "android-app".equals(referrer.getScheme())) {
+            return referrer.getHost();
+        }
+        return null;
+    }
+
+    public int getLayoutResource() {
+        return R.layout.resolver_list;
+    }
+
+    void bindProfileView() {
+        final DisplayResolveInfo dri = mAdapter.getOtherProfile();
+        if (dri != null) {
+            mProfileView.setVisibility(View.VISIBLE);
+            View text = mProfileView.findViewById(R.id.profile_button);
+            if (!(text instanceof TextView)) {
+                text = mProfileView.findViewById(R.id.text1);
+            }
+            ((TextView) text).setText(dri.getDisplayLabel());
+        } else {
+            mProfileView.setVisibility(View.GONE);
+        }
+    }
+
+    private void setProfileSwitchMessageId(int contentUserHint) {
+        if (contentUserHint != UserHandle.USER_CURRENT &&
+                contentUserHint != UserHandle.myUserId()) {
+            UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+            UserInfo originUserInfo = userManager.getUserInfo(contentUserHint);
+            boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile()
+                    : false;
+            boolean targetIsManaged = userManager.isManagedProfile();
+            if (originIsManaged && !targetIsManaged) {
+                mProfileSwitchMessageId = com.android.internal.R.string.forward_intent_to_owner;
+            } else if (!originIsManaged && targetIsManaged) {
+                mProfileSwitchMessageId = com.android.internal.R.string.forward_intent_to_work;
+            }
+        }
+    }
+
+    /**
+     * Turn on launch mode that is safe to use when forwarding intents received from
+     * applications and running in system processes.  This mode uses Activity.startActivityAsCaller
+     * instead of the normal Activity.startActivity for launching the activity selected
+     * by the user.
+     *
+     * <p>This mode is set to true by default if the activity is initialized through
+     * {@link #onCreate(android.os.Bundle)}.  If a subclass calls one of the other onCreate
+     * methods, it is set to false by default.  You must set it before calling one of the
+     * more detailed onCreate methods, so that it will be set correctly in the case where
+     * there is only one intent to resolve and it is thus started immediately.</p>
+     */
+    public void setSafeForwardingMode(boolean safeForwarding) {
+        mSafeForwardingMode = safeForwarding;
+    }
+
+    protected CharSequence getTitleForAction(String action, int defaultTitleRes) {
+        final ActionTitle title = mResolvingHome ? ActionTitle.HOME : ActionTitle.forAction(action);
+        // While there may already be a filtered item, we can only use it in the title if the list
+        // is already sorted and all information relevant to it is already in the list.
+        final boolean named = mAdapter.getFilteredPosition() >= 0;
+        if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) {
+            return getString(defaultTitleRes);
+        } else {
+            return named
+                    ? getString(title.namedTitleRes, mAdapter.getFilteredItem().getDisplayLabel())
+                    : getString(title.titleRes);
+        }
+    }
+
+    void dismiss() {
+        if (!isFinishing()) {
+            finish();
+        }
+    }
+
+    Drawable getIcon(Resources res, int resId) {
+        Drawable result;
+        try {
+            result = res.getDrawableForDensity(resId, mIconDpi);
+        } catch (Resources.NotFoundException e) {
+            result = null;
+        }
+
+        return result;
+    }
+
+    Drawable loadIconForResolveInfo(ResolveInfo ri) {
+        Drawable dr;
+        try {
+            if (ri.resolvePackageName != null && ri.icon != 0) {
+                dr = getIcon(mPm.getResourcesForApplication(ri.resolvePackageName), ri.icon);
+                if (dr != null) {
+                    return mIconFactory.getShadowedIcon(dr);
+                }
+            }
+            final int iconRes = ri.getIconResource();
+            if (iconRes != 0) {
+                dr = getIcon(mPm.getResourcesForApplication(ri.activityInfo.packageName), iconRes);
+                if (dr != null) {
+                    return mIconFactory.getShadowedIcon(dr);
+                }
+            }
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Couldn't find resources for package", e);
+        }
+        return mIconFactory.getBadgedIcon(ri.activityInfo.applicationInfo);
+    }
+
+    @Override
+    protected void onRestart() {
+        super.onRestart();
+        if (!mRegistered) {
+            mPackageMonitor.register(this, getMainLooper(), false);
+            mRegistered = true;
+        }
+        mAdapter.handlePackagesChanged();
+        if (mProfileView != null) {
+            bindProfileView();
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mRegistered) {
+            mPackageMonitor.unregister();
+            mRegistered = false;
+        }
+        final Intent intent = getIntent();
+        if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
+                && !mResolvingHome && !mRetainInOnStop) {
+            // This resolver is in the unusual situation where it has been
+            // launched at the top of a new task.  We don't let it be added
+            // to the recent tasks shown to the user, and we need to make sure
+            // that each time we are launched we get the correct launching
+            // uid (not re-using the same resolver from an old launching uid),
+            // so we will now finish ourself since being no longer visible,
+            // the user probably can't get back to us.
+            if (!isChangingConfigurations()) {
+                finish();
+            }
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (!isChangingConfigurations() && mPickOptionRequest != null) {
+            mPickOptionRequest.cancel();
+        }
+        if (mPostListReadyRunnable != null) {
+            getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
+            mPostListReadyRunnable = null;
+        }
+        if (mAdapter != null && mAdapter.mResolverListController != null) {
+            mAdapter.mResolverListController.destroy();
+        }
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+        resetAlwaysOrOnceButtonBar();
+    }
+
+    private boolean hasManagedProfile() {
+        UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+        if (userManager == null) {
+            return false;
+        }
+
+        try {
+            List<UserInfo> profiles = userManager.getProfiles(getUserId());
+            for (UserInfo userInfo : profiles) {
+                if (userInfo != null && userInfo.isManagedProfile()) {
+                    return true;
+                }
+            }
+        } catch (SecurityException e) {
+            return false;
+        }
+        return false;
+    }
+
+    private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {
+        try {
+            ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
+                    resolveInfo.activityInfo.packageName, 0 /* default flags */);
+            return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
+            boolean filtered) {
+        boolean enabled = false;
+        if (hasValidSelection) {
+            ResolveInfo ri = mAdapter.resolveInfoForPosition(checkedPos, filtered);
+            if (ri == null) {
+                Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled");
+                return;
+            } else if (ri.targetUserId != UserHandle.USER_CURRENT) {
+                Log.e(TAG, "Attempted to set selection to resolve info for another user");
+                return;
+            } else {
+                enabled = true;
+            }
+        }
+        mAlwaysButton.setEnabled(enabled);
+    }
+
+    public void onButtonClick(View v) {
+        final int id = v.getId();
+        startSelected(mAdapter.hasFilteredItem() ?
+                        mAdapter.getFilteredPosition():
+                        mAdapterView.getCheckedItemPosition(),
+                id == R.id.button_always,
+                !mAdapter.hasFilteredItem());
+    }
+
+    public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) {
+        if (isFinishing()) {
+            return;
+        }
+        ResolveInfo ri = mAdapter.resolveInfoForPosition(which, hasIndexBeenFiltered);
+        if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+            Toast.makeText(this, String.format(getResources().getString(
+                    com.android.internal.R.string.activity_resolver_work_profiles_support),
+                    ri.activityInfo.loadLabel(getPackageManager()).toString()),
+                    Toast.LENGTH_LONG).show();
+            return;
+        }
+
+        TargetInfo target = mAdapter.targetInfoForPosition(which, hasIndexBeenFiltered);
+        if (target == null) {
+            return;
+        }
+        if (onTargetSelected(target, always)) {
+            if (always && mSupportsAlwaysUseOption) {
+                MetricsLogger.action(
+                        this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
+            } else if (mSupportsAlwaysUseOption) {
+                MetricsLogger.action(
+                        this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
+            } else {
+                MetricsLogger.action(
+                        this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+            }
+            MetricsLogger.action(this, mAdapter.hasFilteredItem()
+                            ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+                            : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
+            finish();
+        }
+    }
+
+    /**
+     * Replace me in subclasses!
+     */
+    public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+        return defIntent;
+    }
+
+    protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+        final ResolveInfo ri = target.getResolveInfo();
+        final Intent intent = target != null ? target.getResolvedIntent() : null;
+
+        if (intent != null && (mSupportsAlwaysUseOption || mAdapter.hasFilteredItem())
+                && mAdapter.mUnfilteredResolveList != null) {
+            // Build a reasonable intent filter, based on what matched.
+            IntentFilter filter = new IntentFilter();
+            Intent filterIntent;
+
+            if (intent.getSelector() != null) {
+                filterIntent = intent.getSelector();
+            } else {
+                filterIntent = intent;
+            }
+
+            String action = filterIntent.getAction();
+            if (action != null) {
+                filter.addAction(action);
+            }
+            Set<String> categories = filterIntent.getCategories();
+            if (categories != null) {
+                for (String cat : categories) {
+                    filter.addCategory(cat);
+                }
+            }
+            filter.addCategory(Intent.CATEGORY_DEFAULT);
+
+            int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK;
+            Uri data = filterIntent.getData();
+            if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
+                String mimeType = filterIntent.resolveType(this);
+                if (mimeType != null) {
+                    try {
+                        filter.addDataType(mimeType);
+                    } catch (IntentFilter.MalformedMimeTypeException e) {
+                        Log.w("ResolverActivity", e);
+                        filter = null;
+                    }
+                }
+            }
+            if (data != null && data.getScheme() != null) {
+                // We need the data specification if there was no type,
+                // OR if the scheme is not one of our magical "file:"
+                // or "content:" schemes (see IntentFilter for the reason).
+                if (cat != IntentFilter.MATCH_CATEGORY_TYPE
+                        || (!"file".equals(data.getScheme())
+                                && !"content".equals(data.getScheme()))) {
+                    filter.addDataScheme(data.getScheme());
+
+                    // Look through the resolved filter to determine which part
+                    // of it matched the original Intent.
+                    Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator();
+                    if (pIt != null) {
+                        String ssp = data.getSchemeSpecificPart();
+                        while (ssp != null && pIt.hasNext()) {
+                            PatternMatcher p = pIt.next();
+                            if (p.match(ssp)) {
+                                filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
+                                break;
+                            }
+                        }
+                    }
+                    Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
+                    if (aIt != null) {
+                        while (aIt.hasNext()) {
+                            IntentFilter.AuthorityEntry a = aIt.next();
+                            if (a.match(data) >= 0) {
+                                int port = a.getPort();
+                                filter.addDataAuthority(a.getHost(),
+                                        port >= 0 ? Integer.toString(port) : null);
+                                break;
+                            }
+                        }
+                    }
+                    pIt = ri.filter.pathsIterator();
+                    if (pIt != null) {
+                        String path = data.getPath();
+                        while (path != null && pIt.hasNext()) {
+                            PatternMatcher p = pIt.next();
+                            if (p.match(path)) {
+                                filter.addDataPath(p.getPath(), p.getType());
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (filter != null) {
+                final int N = mAdapter.mUnfilteredResolveList.size();
+                ComponentName[] set;
+                // If we don't add back in the component for forwarding the intent to a managed
+                // profile, the preferred activity may not be updated correctly (as the set of
+                // components we tell it we knew about will have changed).
+                final boolean needToAddBackProfileForwardingComponent
+                        = mAdapter.mOtherProfile != null;
+                if (!needToAddBackProfileForwardingComponent) {
+                    set = new ComponentName[N];
+                } else {
+                    set = new ComponentName[N + 1];
+                }
+
+                int bestMatch = 0;
+                for (int i=0; i<N; i++) {
+                    ResolveInfo r = mAdapter.mUnfilteredResolveList.get(i).getResolveInfoAt(0);
+                    set[i] = new ComponentName(r.activityInfo.packageName,
+                            r.activityInfo.name);
+                    if (r.match > bestMatch) bestMatch = r.match;
+                }
+
+                if (needToAddBackProfileForwardingComponent) {
+                    set[N] = mAdapter.mOtherProfile.getResolvedComponentName();
+                    final int otherProfileMatch = mAdapter.mOtherProfile.getResolveInfo().match;
+                    if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
+                }
+
+                if (alwaysCheck) {
+                    final int userId = getUserId();
+                    final PackageManager pm = getPackageManager();
+
+                    // Set the preferred Activity
+                    pm.addPreferredActivity(filter, bestMatch, set, intent.getComponent());
+
+                    if (ri.handleAllWebDataURI) {
+                        // Set default Browser if needed
+                        final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
+                        if (TextUtils.isEmpty(packageName)) {
+                            pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
+                        }
+                    } else {
+                        // Update Domain Verification status
+                        ComponentName cn = intent.getComponent();
+                        String packageName = cn.getPackageName();
+                        String dataScheme = (data != null) ? data.getScheme() : null;
+
+                        boolean isHttpOrHttps = (dataScheme != null) &&
+                                (dataScheme.equals(IntentFilter.SCHEME_HTTP) ||
+                                        dataScheme.equals(IntentFilter.SCHEME_HTTPS));
+
+                        boolean isViewAction = (action != null) && action.equals(Intent.ACTION_VIEW);
+                        boolean hasCategoryBrowsable = (categories != null) &&
+                                categories.contains(Intent.CATEGORY_BROWSABLE);
+
+                        if (isHttpOrHttps && isViewAction && hasCategoryBrowsable) {
+                            pm.updateIntentVerificationStatusAsUser(packageName,
+                                    PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS,
+                                    userId);
+                        }
+                    }
+                } else {
+                    try {
+                        mAdapter.mResolverListController.setLastChosen(intent, filter, bestMatch);
+                    } catch (RemoteException re) {
+                        Log.d(TAG, "Error calling setLastChosenActivity\n" + re);
+                    }
+                }
+            }
+        }
+
+        if (target != null) {
+            safelyStartActivity(target);
+        }
+        return true;
+    }
+
+    public void safelyStartActivity(TargetInfo cti) {
+        // We're dispatching intents that might be coming from legacy apps, so
+        // don't kill ourselves.
+        StrictMode.disableDeathOnFileUriExposure();
+        try {
+            safelyStartActivityInternal(cti);
+        } finally {
+            StrictMode.enableDeathOnFileUriExposure();
+        }
+    }
+
+    private void safelyStartActivityInternal(TargetInfo cti) {
+        // If needed, show that intent is forwarded
+        // from managed profile to owner or other way around.
+        if (mProfileSwitchMessageId != -1) {
+            Toast.makeText(this, getString(mProfileSwitchMessageId), Toast.LENGTH_LONG).show();
+        }
+        if (!mSafeForwardingMode) {
+            if (cti.start(this, null)) {
+                onActivityStarted(cti);
+            }
+            return;
+        }
+        try {
+            if (cti.startAsCaller(this, null, UserHandle.USER_NULL)) {
+                onActivityStarted(cti);
+            }
+        } catch (RuntimeException e) {
+            String launchedFromPackage;
+            try {
+                launchedFromPackage = ActivityManager.getService().getLaunchedFromPackage(
+                        getActivityToken());
+            } catch (RemoteException e2) {
+                launchedFromPackage = "??";
+            }
+            Slog.wtf(TAG, "Unable to launch as uid " + mLaunchedFromUid
+                    + " package " + launchedFromPackage + ", while running in "
+                    + ActivityThread.currentProcessName(), e);
+        }
+    }
+
+    public void onActivityStarted(TargetInfo cti) {
+        // Do nothing
+    }
+
+    public boolean shouldGetActivityMetadata() {
+        return false;
+    }
+
+    public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+        return true;
+    }
+
+    public void showTargetDetails(ResolveInfo ri) {
+        Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                .setData(Uri.fromParts("package", ri.activityInfo.packageName, null))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        startActivity(in);
+    }
+
+    public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents,
+            Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
+            boolean filterLastUsed) {
+        return new ResolveListAdapter(context, payloadIntents, initialIntents, rList,
+                launchedFromUid, filterLastUsed, createListController());
+    }
+
+    @VisibleForTesting
+    protected ResolverListController createListController() {
+        return new ResolverListController(
+                this,
+                mPm,
+                getTargetIntent(),
+                getReferrerPackageName(),
+                mLaunchedFromUid);
+    }
+
+    /**
+     * Returns true if the activity is finishing and creation should halt
+     */
+    public boolean configureContentView(List<Intent> payloadIntents, Intent[] initialIntents,
+            List<ResolveInfo> rList) {
+        // The last argument of createAdapter is whether to do special handling
+        // of the last used choice to highlight it in the list.  We need to always
+        // turn this off when running under voice interaction, since it results in
+        // a more complicated UI that the current voice interaction flow is not able
+        // to handle.
+        mAdapter = createAdapter(this, payloadIntents, initialIntents, rList,
+                mLaunchedFromUid, mSupportsAlwaysUseOption && !isVoiceInteraction());
+        boolean rebuildCompleted = mAdapter.rebuildList();
+
+        if (useLayoutWithDefault()) {
+            mLayoutId = R.layout.resolver_list_with_default;
+        } else {
+            mLayoutId = getLayoutResource();
+        }
+        setContentView(mLayoutId);
+
+        int count = mAdapter.getUnfilteredCount();
+
+        // We only rebuild asynchronously when we have multiple elements to sort. In the case where
+        // we're already done, we can check if we should auto-launch immediately.
+        if (rebuildCompleted) {
+            if (count == 1 && mAdapter.getOtherProfile() == null) {
+                // Only one target, so we're a candidate to auto-launch!
+                final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
+                if (shouldAutoLaunchSingleChoice(target)) {
+                    safelyStartActivity(target);
+                    mPackageMonitor.unregister();
+                    mRegistered = false;
+                    finish();
+                    return true;
+                }
+            }
+        }
+
+
+        mAdapterView = findViewById(R.id.resolver_list);
+
+        if (count == 0 && mAdapter.mPlaceholderCount == 0) {
+            final TextView emptyView = findViewById(R.id.empty);
+            emptyView.setVisibility(View.VISIBLE);
+            mAdapterView.setVisibility(View.GONE);
+        } else {
+            mAdapterView.setVisibility(View.VISIBLE);
+            onPrepareAdapterView(mAdapterView, mAdapter);
+        }
+        return false;
+    }
+
+    public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) {
+        final boolean useHeader = adapter.hasFilteredItem();
+        final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null;
+
+        adapterView.setAdapter(mAdapter);
+
+        final ItemClickListener listener = new ItemClickListener();
+        adapterView.setOnItemClickListener(listener);
+        adapterView.setOnItemLongClickListener(listener);
+
+        if (mSupportsAlwaysUseOption) {
+            listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+        }
+
+        // In case this method is called again (due to activity recreation), avoid adding a new
+        // header if one is already present.
+        if (useHeader && listView != null && listView.getHeaderViewsCount() == 0) {
+            listView.addHeaderView(LayoutInflater.from(this).inflate(
+                    R.layout.resolver_different_item_header, listView, false));
+        }
+    }
+
+    public void setTitleAndIcon() {
+        if (mAdapter.getCount() == 0 && mAdapter.mPlaceholderCount == 0) {
+            final TextView titleView = findViewById(R.id.title);
+            if (titleView != null) {
+                titleView.setVisibility(View.GONE);
+            }
+        }
+
+        CharSequence title = mTitle != null
+                ? mTitle
+                : getTitleForAction(getTargetIntent().getAction(), mDefaultTitleResId);
+
+        if (!TextUtils.isEmpty(title)) {
+            final TextView titleView = findViewById(R.id.title);
+            if (titleView != null) {
+                titleView.setText(title);
+            }
+            setTitle(title);
+
+            // Try to initialize the title icon if we have a view for it and a title to match
+            final ImageView titleIcon = findViewById(R.id.title_icon);
+            if (titleIcon != null) {
+                ApplicationInfo ai = null;
+                try {
+                    if (!TextUtils.isEmpty(mReferrerPackage)) {
+                        ai = mPm.getApplicationInfo(mReferrerPackage, 0);
+                    }
+                } catch (NameNotFoundException e) {
+                    Log.e(TAG, "Could not find referrer package " + mReferrerPackage);
+                }
+
+                if (ai != null) {
+                    titleIcon.setImageDrawable(ai.loadIcon(mPm));
+                }
+            }
+        }
+
+        final ImageView iconView = findViewById(R.id.icon);
+        final DisplayResolveInfo iconInfo = mAdapter.getFilteredItem();
+        if (iconView != null && iconInfo != null) {
+            new LoadIconIntoViewTask(iconInfo, iconView).execute();
+        }
+    }
+
+    public void resetAlwaysOrOnceButtonBar() {
+        if (mSupportsAlwaysUseOption) {
+            final ViewGroup buttonLayout = findViewById(R.id.button_bar);
+            if (buttonLayout != null) {
+                buttonLayout.setVisibility(View.VISIBLE);
+                mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always);
+                mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once);
+            } else {
+                Log.e(TAG, "Layout unexpectedly does not have a button bar");
+            }
+        }
+
+        if (useLayoutWithDefault()
+                && mAdapter.getFilteredPosition() != ListView.INVALID_POSITION) {
+            setAlwaysButtonEnabled(true, mAdapter.getFilteredPosition(), false);
+            mOnceButton.setEnabled(true);
+            return;
+        }
+
+        // When the items load in, if an item was already selected, enable the buttons
+        if (mAdapterView != null
+                && mAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) {
+            setAlwaysButtonEnabled(true, mAdapterView.getCheckedItemPosition(), true);
+            mOnceButton.setEnabled(true);
+        }
+    }
+
+    private boolean useLayoutWithDefault() {
+        return mSupportsAlwaysUseOption && mAdapter.hasFilteredItem();
+    }
+
+    /**
+     * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets
+     * called and we are launched in a new task.
+     */
+    protected void setRetainInOnStop(boolean retainInOnStop) {
+        mRetainInOnStop = retainInOnStop;
+    }
+
+    /**
+     * Check a simple match for the component of two ResolveInfos.
+     */
+    static boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
+        return lhs == null ? rhs == null
+                : lhs.activityInfo == null ? rhs.activityInfo == null
+                : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name)
+                && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName);
+    }
+
+    public final class DisplayResolveInfo implements TargetInfo {
+        private final ResolveInfo mResolveInfo;
+        private final CharSequence mDisplayLabel;
+        private Drawable mDisplayIcon;
+        private Drawable mBadge;
+        private final CharSequence mExtendedInfo;
+        private final Intent mResolvedIntent;
+        private final List<Intent> mSourceIntents = new ArrayList<>();
+        private boolean mPinned;
+
+        public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
+                CharSequence pInfo, Intent pOrigIntent) {
+            mSourceIntents.add(originalIntent);
+            mResolveInfo = pri;
+            mDisplayLabel = pLabel;
+            mExtendedInfo = pInfo;
+
+            final Intent intent = new Intent(pOrigIntent != null ? pOrigIntent :
+                    getReplacementIntent(pri.activityInfo, getTargetIntent()));
+            intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
+                    | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+            final ActivityInfo ai = mResolveInfo.activityInfo;
+            intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
+
+            mResolvedIntent = intent;
+        }
+
+        private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags) {
+            mSourceIntents.addAll(other.getAllSourceIntents());
+            mResolveInfo = other.mResolveInfo;
+            mDisplayLabel = other.mDisplayLabel;
+            mDisplayIcon = other.mDisplayIcon;
+            mExtendedInfo = other.mExtendedInfo;
+            mResolvedIntent = new Intent(other.mResolvedIntent);
+            mResolvedIntent.fillIn(fillInIntent, flags);
+            mPinned = other.mPinned;
+        }
+
+        public ResolveInfo getResolveInfo() {
+            return mResolveInfo;
+        }
+
+        public CharSequence getDisplayLabel() {
+            return mDisplayLabel;
+        }
+
+        public Drawable getDisplayIcon() {
+            return mDisplayIcon;
+        }
+
+        public Drawable getBadgeIcon() {
+            // We only expose a badge if we have extended info.
+            // The badge is a higher-priority disambiguation signal
+            // but we don't need one if we wouldn't show extended info at all.
+            if (TextUtils.isEmpty(getExtendedInfo())) {
+                return null;
+            }
+
+            if (mBadge == null && mResolveInfo != null && mResolveInfo.activityInfo != null
+                    && mResolveInfo.activityInfo.applicationInfo != null) {
+                if (mResolveInfo.activityInfo.icon == 0 || mResolveInfo.activityInfo.icon
+                        == mResolveInfo.activityInfo.applicationInfo.icon) {
+                    // Badging an icon with exactly the same icon is silly.
+                    // If the activityInfo icon resid is 0 it will fall back
+                    // to the application's icon, making it a match.
+                    return null;
+                }
+                mBadge = mResolveInfo.activityInfo.applicationInfo.loadIcon(mPm);
+            }
+            return mBadge;
+        }
+
+        @Override
+        public CharSequence getBadgeContentDescription() {
+            return null;
+        }
+
+        @Override
+        public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+            return new DisplayResolveInfo(this, fillInIntent, flags);
+        }
+
+        @Override
+        public List<Intent> getAllSourceIntents() {
+            return mSourceIntents;
+        }
+
+        public void addAlternateSourceIntent(Intent alt) {
+            mSourceIntents.add(alt);
+        }
+
+        public void setDisplayIcon(Drawable icon) {
+            mDisplayIcon = icon;
+        }
+
+        public boolean hasDisplayIcon() {
+            return mDisplayIcon != null;
+        }
+
+        public CharSequence getExtendedInfo() {
+            return mExtendedInfo;
+        }
+
+        public Intent getResolvedIntent() {
+            return mResolvedIntent;
+        }
+
+        @Override
+        public ComponentName getResolvedComponentName() {
+            return new ComponentName(mResolveInfo.activityInfo.packageName,
+                    mResolveInfo.activityInfo.name);
+        }
+
+        @Override
+        public boolean start(Activity activity, Bundle options) {
+            activity.startActivity(mResolvedIntent, options);
+            return true;
+        }
+
+        @Override
+        public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+            activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
+            return true;
+        }
+
+        @Override
+        public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+            activity.startActivityAsUser(mResolvedIntent, options, user);
+            return false;
+        }
+
+        @Override
+        public boolean isPinned() {
+            return mPinned;
+        }
+
+        public void setPinned(boolean pinned) {
+            mPinned = pinned;
+        }
+    }
+
+    /**
+     * A single target as represented in the chooser.
+     */
+    public interface TargetInfo {
+        /**
+         * Get the resolved intent that represents this target. Note that this may not be the
+         * intent that will be launched by calling one of the <code>start</code> methods provided;
+         * this is the intent that will be credited with the launch.
+         *
+         * @return the resolved intent for this target
+         */
+        Intent getResolvedIntent();
+
+        /**
+         * Get the resolved component name that represents this target. Note that this may not
+         * be the component that will be directly launched by calling one of the <code>start</code>
+         * methods provided; this is the component that will be credited with the launch.
+         *
+         * @return the resolved ComponentName for this target
+         */
+        ComponentName getResolvedComponentName();
+
+        /**
+         * Start the activity referenced by this target.
+         *
+         * @param activity calling Activity performing the launch
+         * @param options ActivityOptions bundle
+         * @return true if the start completed successfully
+         */
+        boolean start(Activity activity, Bundle options);
+
+        /**
+         * Start the activity referenced by this target as if the ResolverActivity's caller
+         * was performing the start operation.
+         *
+         * @param activity calling Activity (actually) performing the launch
+         * @param options ActivityOptions bundle
+         * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller
+         * @return true if the start completed successfully
+         */
+        boolean startAsCaller(Activity activity, Bundle options, int userId);
+
+        /**
+         * Start the activity referenced by this target as a given user.
+         *
+         * @param activity calling activity performing the launch
+         * @param options ActivityOptions bundle
+         * @param user handle for the user to start the activity as
+         * @return true if the start completed successfully
+         */
+        boolean startAsUser(Activity activity, Bundle options, UserHandle user);
+
+        /**
+         * Return the ResolveInfo about how and why this target matched the original query
+         * for available targets.
+         *
+         * @return ResolveInfo representing this target's match
+         */
+        ResolveInfo getResolveInfo();
+
+        /**
+         * Return the human-readable text label for this target.
+         *
+         * @return user-visible target label
+         */
+        CharSequence getDisplayLabel();
+
+        /**
+         * Return any extended info for this target. This may be used to disambiguate
+         * otherwise identical targets.
+         *
+         * @return human-readable disambig string or null if none present
+         */
+        CharSequence getExtendedInfo();
+
+        /**
+         * @return The drawable that should be used to represent this target
+         */
+        Drawable getDisplayIcon();
+
+        /**
+         * @return The (small) icon to badge the target with
+         */
+        Drawable getBadgeIcon();
+
+        /**
+         * @return The content description for the badge icon
+         */
+        CharSequence getBadgeContentDescription();
+
+        /**
+         * Clone this target with the given fill-in information.
+         */
+        TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
+
+        /**
+         * @return the list of supported source intents deduped against this single target
+         */
+        List<Intent> getAllSourceIntents();
+
+        /**
+         * @return true if this target should be pinned to the front by the request of the user
+         */
+        boolean isPinned();
+    }
+
+    public class ResolveListAdapter extends BaseAdapter {
+        private final List<Intent> mIntents;
+        private final Intent[] mInitialIntents;
+        private final List<ResolveInfo> mBaseResolveList;
+        protected ResolveInfo mLastChosen;
+        private DisplayResolveInfo mOtherProfile;
+        private boolean mHasExtendedInfo;
+        private ResolverListController mResolverListController;
+        private int mPlaceholderCount;
+
+        protected final LayoutInflater mInflater;
+
+        List<DisplayResolveInfo> mDisplayList;
+        List<ResolvedComponentInfo> mUnfilteredResolveList;
+
+        private int mLastChosenPosition = -1;
+        private boolean mFilterLastUsed;
+
+        public ResolveListAdapter(Context context, List<Intent> payloadIntents,
+                Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
+                boolean filterLastUsed,
+                ResolverListController resolverListController) {
+            mIntents = payloadIntents;
+            mInitialIntents = initialIntents;
+            mBaseResolveList = rList;
+            mLaunchedFromUid = launchedFromUid;
+            mInflater = LayoutInflater.from(context);
+            mDisplayList = new ArrayList<>();
+            mFilterLastUsed = filterLastUsed;
+            mResolverListController = resolverListController;
+        }
+
+        public void handlePackagesChanged() {
+            rebuildList();
+            if (getCount() == 0) {
+                // We no longer have any items...  just finish the activity.
+                finish();
+            }
+        }
+
+        public void setPlaceholderCount(int count) {
+            mPlaceholderCount = count;
+        }
+
+        public int getPlaceholderCount() { return mPlaceholderCount; }
+
+        @Nullable
+        public DisplayResolveInfo getFilteredItem() {
+            if (mFilterLastUsed && mLastChosenPosition >= 0) {
+                // Not using getItem since it offsets to dodge this position for the list
+                return mDisplayList.get(mLastChosenPosition);
+            }
+            return null;
+        }
+
+        public DisplayResolveInfo getOtherProfile() {
+            return mOtherProfile;
+        }
+
+        public int getFilteredPosition() {
+            if (mFilterLastUsed && mLastChosenPosition >= 0) {
+                return mLastChosenPosition;
+            }
+            return AbsListView.INVALID_POSITION;
+        }
+
+        public boolean hasFilteredItem() {
+            return mFilterLastUsed && mLastChosen != null;
+        }
+
+        public float getScore(DisplayResolveInfo target) {
+            return mResolverListController.getScore(target);
+        }
+
+        public void updateModel(ComponentName componentName) {
+            mResolverListController.updateModel(componentName);
+        }
+
+        public void updateChooserCounts(String packageName, int userId, String action) {
+            mResolverListController.updateChooserCounts(packageName, userId, action);
+        }
+
+        /**
+         * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work
+         * to complete.
+         *
+         * @return Whether or not the list building is completed.
+         */
+        protected boolean rebuildList() {
+            List<ResolvedComponentInfo> currentResolveList = null;
+            // Clear the value of mOtherProfile from previous call.
+            mOtherProfile = null;
+            mLastChosen = null;
+            mLastChosenPosition = -1;
+            mDisplayList.clear();
+            if (mBaseResolveList != null) {
+                currentResolveList = mUnfilteredResolveList = new ArrayList<>();
+                mResolverListController.addResolveListDedupe(currentResolveList,
+                        getTargetIntent(),
+                        mBaseResolveList);
+            } else {
+                currentResolveList = mUnfilteredResolveList =
+                        mResolverListController.getResolversForIntent(shouldGetResolvedFilter(),
+                                shouldGetActivityMetadata(),
+                                mIntents);
+                if (currentResolveList == null) {
+                    processSortedList(currentResolveList);
+                    return true;
+                }
+                List<ResolvedComponentInfo> originalList =
+                        mResolverListController.filterIneligibleActivities(currentResolveList,
+                                true);
+                if (originalList != null) {
+                    mUnfilteredResolveList = originalList;
+                }
+            }
+
+            // So far we only support a single other profile at a time.
+            // The first one we see gets special treatment.
+            for (ResolvedComponentInfo info : currentResolveList) {
+                if (info.getResolveInfoAt(0).targetUserId != UserHandle.USER_CURRENT) {
+                    mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0),
+                            info.getResolveInfoAt(0),
+                            info.getResolveInfoAt(0).loadLabel(mPm),
+                            info.getResolveInfoAt(0).loadLabel(mPm),
+                            getReplacementIntent(info.getResolveInfoAt(0).activityInfo,
+                                    info.getIntentAt(0)));
+                    currentResolveList.remove(info);
+                    break;
+                }
+            }
+
+            if (mOtherProfile == null) {
+                try {
+                    mLastChosen = mResolverListController.getLastChosen();
+                } catch (RemoteException re) {
+                    Log.d(TAG, "Error calling getLastChosenActivity\n" + re);
+                }
+            }
+
+            int N;
+            if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) {
+                // We only care about fixing the unfilteredList if the current resolve list and
+                // current resolve list are currently the same.
+                List<ResolvedComponentInfo> originalList =
+                        mResolverListController.filterLowPriority(currentResolveList,
+                                mUnfilteredResolveList == currentResolveList);
+                if (originalList != null) {
+                    mUnfilteredResolveList = originalList;
+                }
+
+                if (currentResolveList.size() > 1) {
+                    int placeholderCount = currentResolveList.size();
+                    if (useLayoutWithDefault()) {
+                        --placeholderCount;
+                    }
+                    setPlaceholderCount(placeholderCount);
+                    AsyncTask<List<ResolvedComponentInfo>,
+                            Void,
+                            List<ResolvedComponentInfo>> sortingTask =
+                            new AsyncTask<List<ResolvedComponentInfo>,
+                                    Void,
+                                    List<ResolvedComponentInfo>>() {
+                        @Override
+                        protected List<ResolvedComponentInfo> doInBackground(
+                                List<ResolvedComponentInfo>... params) {
+                            mResolverListController.sort(params[0]);
+                            return params[0];
+                        }
+
+                        @Override
+                        protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
+                            processSortedList(sortedComponents);
+                            if (mProfileView != null) {
+                                bindProfileView();
+                            }
+                            notifyDataSetChanged();
+                        }
+                    };
+                    sortingTask.execute(currentResolveList);
+                    postListReadyRunnable();
+                    return false;
+                } else {
+                    processSortedList(currentResolveList);
+                    return true;
+                }
+            } else {
+                processSortedList(currentResolveList);
+                return true;
+            }
+        }
+
+        private void processSortedList(List<ResolvedComponentInfo> sortedComponents) {
+            int N;
+            if (sortedComponents != null && (N = sortedComponents.size()) != 0) {
+                // First put the initial items at the top.
+                if (mInitialIntents != null) {
+                    for (int i = 0; i < mInitialIntents.length; i++) {
+                        Intent ii = mInitialIntents[i];
+                        if (ii == null) {
+                            continue;
+                        }
+                        ActivityInfo ai = ii.resolveActivityInfo(
+                                getPackageManager(), 0);
+                        if (ai == null) {
+                            Log.w(TAG, "No activity found for " + ii);
+                            continue;
+                        }
+                        ResolveInfo ri = new ResolveInfo();
+                        ri.activityInfo = ai;
+                        UserManager userManager =
+                                (UserManager) getSystemService(Context.USER_SERVICE);
+                        if (ii instanceof LabeledIntent) {
+                            LabeledIntent li = (LabeledIntent) ii;
+                            ri.resolvePackageName = li.getSourcePackage();
+                            ri.labelRes = li.getLabelResource();
+                            ri.nonLocalizedLabel = li.getNonLocalizedLabel();
+                            ri.icon = li.getIconResource();
+                            ri.iconResourceId = ri.icon;
+                        }
+                        if (userManager.isManagedProfile()) {
+                            ri.noResourceId = true;
+                            ri.icon = 0;
+                        }
+                        addResolveInfo(new DisplayResolveInfo(ii, ri,
+                                ri.loadLabel(getPackageManager()), null, ii));
+                    }
+                }
+
+                // Check for applications with same name and use application name or
+                // package name if necessary
+                ResolvedComponentInfo rci0 = sortedComponents.get(0);
+                ResolveInfo r0 = rci0.getResolveInfoAt(0);
+                int start = 0;
+                CharSequence r0Label = r0.loadLabel(mPm);
+                mHasExtendedInfo = false;
+                for (int i = 1; i < N; i++) {
+                    if (r0Label == null) {
+                        r0Label = r0.activityInfo.packageName;
+                    }
+                    ResolvedComponentInfo rci = sortedComponents.get(i);
+                    ResolveInfo ri = rci.getResolveInfoAt(0);
+                    CharSequence riLabel = ri.loadLabel(mPm);
+                    if (riLabel == null) {
+                        riLabel = ri.activityInfo.packageName;
+                    }
+                    if (riLabel.equals(r0Label)) {
+                        continue;
+                    }
+                    processGroup(sortedComponents, start, (i - 1), rci0, r0Label);
+                    rci0 = rci;
+                    r0 = ri;
+                    r0Label = riLabel;
+                    start = i;
+                }
+                // Process last group
+                processGroup(sortedComponents, start, (N - 1), rci0, r0Label);
+            }
+
+            postListReadyRunnable();
+        }
+
+        /**
+         * Some necessary methods for creating the list are initiated in onCreate and will also
+         * determine the layout known. We therefore can't update the UI inline and post to the
+         * handler thread to update after the current task is finished.
+         */
+        private void postListReadyRunnable() {
+            if (mPostListReadyRunnable == null) {
+                mPostListReadyRunnable = new Runnable() {
+                    @Override
+                    public void run() {
+                        setTitleAndIcon();
+                        resetAlwaysOrOnceButtonBar();
+                        onListRebuilt();
+                        mPostListReadyRunnable = null;
+                    }
+                };
+                getMainThreadHandler().post(mPostListReadyRunnable);
+            }
+        }
+
+        public void onListRebuilt() {
+            int count = getUnfilteredCount();
+            if (count == 1 && getOtherProfile() == null) {
+                // Only one target, so we're a candidate to auto-launch!
+                final TargetInfo target = targetInfoForPosition(0, false);
+                if (shouldAutoLaunchSingleChoice(target)) {
+                    safelyStartActivity(target);
+                    finish();
+                }
+            }
+        }
+
+        public boolean shouldGetResolvedFilter() {
+            return mFilterLastUsed;
+        }
+
+        private void processGroup(List<ResolvedComponentInfo> rList, int start, int end,
+                ResolvedComponentInfo ro, CharSequence roLabel) {
+            // Process labels from start to i
+            int num = end - start+1;
+            if (num == 1) {
+                // No duplicate labels. Use label for entry at start
+                addResolveInfoWithAlternates(ro, null, roLabel);
+            } else {
+                mHasExtendedInfo = true;
+                boolean usePkg = false;
+                final ApplicationInfo ai = ro.getResolveInfoAt(0).activityInfo.applicationInfo;
+                final CharSequence startApp = ai.loadLabel(mPm);
+                if (startApp == null) {
+                    usePkg = true;
+                }
+                if (!usePkg) {
+                    // Use HashSet to track duplicates
+                    HashSet<CharSequence> duplicates =
+                        new HashSet<CharSequence>();
+                    duplicates.add(startApp);
+                    for (int j = start+1; j <= end ; j++) {
+                        ResolveInfo jRi = rList.get(j).getResolveInfoAt(0);
+                        CharSequence jApp = jRi.activityInfo.applicationInfo.loadLabel(mPm);
+                        if ( (jApp == null) || (duplicates.contains(jApp))) {
+                            usePkg = true;
+                            break;
+                        } else {
+                            duplicates.add(jApp);
+                        }
+                    }
+                    // Clear HashSet for later use
+                    duplicates.clear();
+                }
+                for (int k = start; k <= end; k++) {
+                    final ResolvedComponentInfo rci = rList.get(k);
+                    final ResolveInfo add = rci.getResolveInfoAt(0);
+                    final CharSequence extraInfo;
+                    if (usePkg) {
+                        // Use package name for all entries from start to end-1
+                        extraInfo = add.activityInfo.packageName;
+                    } else {
+                        // Use application name for all entries from start to end-1
+                        extraInfo = add.activityInfo.applicationInfo.loadLabel(mPm);
+                    }
+                    addResolveInfoWithAlternates(rci, extraInfo, roLabel);
+                }
+            }
+        }
+
+        private void addResolveInfoWithAlternates(ResolvedComponentInfo rci,
+                CharSequence extraInfo, CharSequence roLabel) {
+            final int count = rci.getCount();
+            final Intent intent = rci.getIntentAt(0);
+            final ResolveInfo add = rci.getResolveInfoAt(0);
+            final Intent replaceIntent = getReplacementIntent(add.activityInfo, intent);
+            final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, roLabel,
+                    extraInfo, replaceIntent);
+            dri.setPinned(rci.isPinned());
+            addResolveInfo(dri);
+            if (replaceIntent == intent) {
+                // Only add alternates if we didn't get a specific replacement from
+                // the caller. If we have one it trumps potential alternates.
+                for (int i = 1, N = count; i < N; i++) {
+                    final Intent altIntent = rci.getIntentAt(i);
+                    dri.addAlternateSourceIntent(altIntent);
+                }
+            }
+            updateLastChosenPosition(add);
+        }
+
+        private void updateLastChosenPosition(ResolveInfo info) {
+            // If another profile is present, ignore the last chosen entry.
+            if (mOtherProfile != null) {
+                mLastChosenPosition = -1;
+                return;
+            }
+            if (mLastChosen != null
+                    && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName)
+                    && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) {
+                mLastChosenPosition = mDisplayList.size() - 1;
+            }
+        }
+
+        // We assume that at this point we've already filtered out the only intent for a different
+        // targetUserId which we're going to use.
+        private void addResolveInfo(DisplayResolveInfo dri) {
+            if (dri != null && dri.mResolveInfo != null
+                    && dri.mResolveInfo.targetUserId == UserHandle.USER_CURRENT) {
+                // Checks if this info is already listed in display.
+                for (DisplayResolveInfo existingInfo : mDisplayList) {
+                    if (resolveInfoMatch(dri.mResolveInfo, existingInfo.mResolveInfo)) {
+                        return;
+                    }
+                }
+                mDisplayList.add(dri);
+            }
+        }
+
+        @Nullable
+        public ResolveInfo resolveInfoForPosition(int position, boolean filtered) {
+            TargetInfo target = targetInfoForPosition(position, filtered);
+            if (target != null) {
+                return target.getResolveInfo();
+             }
+             return null;
+        }
+
+        @Nullable
+        public TargetInfo targetInfoForPosition(int position, boolean filtered) {
+            if (filtered) {
+                return getItem(position);
+            }
+            if (mDisplayList.size() > position) {
+                return mDisplayList.get(position);
+            }
+            return null;
+        }
+
+        public int getCount() {
+            int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
+                    mDisplayList.size();
+            if (mFilterLastUsed && mLastChosenPosition >= 0) {
+                totalSize--;
+            }
+            return totalSize;
+        }
+
+        public int getUnfilteredCount() {
+            return mDisplayList.size();
+        }
+
+        public int getDisplayInfoCount() {
+            return mDisplayList.size();
+        }
+
+        public DisplayResolveInfo getDisplayInfoAt(int index) {
+            return mDisplayList.get(index);
+        }
+
+        @Nullable
+        public TargetInfo getItem(int position) {
+            if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
+                position++;
+            }
+            if (mDisplayList.size() > position) {
+                return mDisplayList.get(position);
+            } else {
+                return null;
+            }
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        public boolean hasExtendedInfo() {
+            return mHasExtendedInfo;
+        }
+
+        public boolean hasResolvedTarget(ResolveInfo info) {
+            for (int i = 0, N = mDisplayList.size(); i < N; i++) {
+                if (resolveInfoMatch(info, mDisplayList.get(i).getResolveInfo())) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public int getDisplayResolveInfoCount() {
+            return mDisplayList.size();
+        }
+
+        public DisplayResolveInfo getDisplayResolveInfo(int index) {
+            // Used to query services. We only query services for primary targets, not alternates.
+            return mDisplayList.get(index);
+        }
+
+        public final View getView(int position, View convertView, ViewGroup parent) {
+            View view = convertView;
+            if (view == null) {
+                view = createView(parent);
+            }
+            onBindView(view, getItem(position));
+            return view;
+        }
+
+        public final View createView(ViewGroup parent) {
+            final View view = onCreateView(parent);
+            final ViewHolder holder = new ViewHolder(view);
+            view.setTag(holder);
+            return view;
+        }
+
+        public View onCreateView(ViewGroup parent) {
+            return mInflater.inflate(
+                    com.android.internal.R.layout.resolve_list_item, parent, false);
+        }
+
+        public boolean showsExtendedInfo(TargetInfo info) {
+            return !TextUtils.isEmpty(info.getExtendedInfo());
+        }
+
+        public boolean isComponentPinned(ComponentName name) {
+            return false;
+        }
+
+        public final void bindView(int position, View view) {
+            onBindView(view, getItem(position));
+        }
+
+        private void onBindView(View view, TargetInfo info) {
+            final ViewHolder holder = (ViewHolder) view.getTag();
+            if (info == null) {
+                holder.icon.setImageDrawable(
+                        getDrawable(R.drawable.resolver_icon_placeholder));
+                return;
+            }
+            final CharSequence label = info.getDisplayLabel();
+            if (!TextUtils.equals(holder.text.getText(), label)) {
+                holder.text.setText(info.getDisplayLabel());
+            }
+            if (showsExtendedInfo(info)) {
+                holder.text2.setVisibility(View.VISIBLE);
+                holder.text2.setText(info.getExtendedInfo());
+            } else {
+                holder.text2.setVisibility(View.GONE);
+            }
+            if (info instanceof DisplayResolveInfo
+                    && !((DisplayResolveInfo) info).hasDisplayIcon()) {
+                new LoadAdapterIconTask((DisplayResolveInfo) info).execute();
+            }
+            holder.icon.setImageDrawable(info.getDisplayIcon());
+            if (holder.badge != null) {
+                final Drawable badge = info.getBadgeIcon();
+                if (badge != null) {
+                    holder.badge.setImageDrawable(badge);
+                    holder.badge.setContentDescription(info.getBadgeContentDescription());
+                    holder.badge.setVisibility(View.VISIBLE);
+                } else {
+                    holder.badge.setVisibility(View.GONE);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public static final class ResolvedComponentInfo {
+        public final ComponentName name;
+        private boolean mPinned;
+        private final List<Intent> mIntents = new ArrayList<>();
+        private final List<ResolveInfo> mResolveInfos = new ArrayList<>();
+
+        public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) {
+            this.name = name;
+            add(intent, info);
+        }
+
+        public void add(Intent intent, ResolveInfo info) {
+            mIntents.add(intent);
+            mResolveInfos.add(info);
+        }
+
+        public int getCount() {
+            return mIntents.size();
+        }
+
+        public Intent getIntentAt(int index) {
+            return index >= 0 ? mIntents.get(index) : null;
+        }
+
+        public ResolveInfo getResolveInfoAt(int index) {
+            return index >= 0 ? mResolveInfos.get(index) : null;
+        }
+
+        public int findIntent(Intent intent) {
+            for (int i = 0, N = mIntents.size(); i < N; i++) {
+                if (intent.equals(mIntents.get(i))) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public int findResolveInfo(ResolveInfo info) {
+            for (int i = 0, N = mResolveInfos.size(); i < N; i++) {
+                if (info.equals(mResolveInfos.get(i))) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public boolean isPinned() {
+            return mPinned;
+        }
+
+        public void setPinned(boolean pinned) {
+            mPinned = pinned;
+        }
+    }
+
+    static class ViewHolder {
+        public TextView text;
+        public TextView text2;
+        public ImageView icon;
+        public ImageView badge;
+
+        public ViewHolder(View view) {
+            text = (TextView) view.findViewById(com.android.internal.R.id.text1);
+            text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
+            icon = (ImageView) view.findViewById(R.id.icon);
+            badge = (ImageView) view.findViewById(R.id.target_badge);
+        }
+    }
+
+    class ItemClickListener implements AdapterView.OnItemClickListener,
+            AdapterView.OnItemLongClickListener {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+            if (listView != null) {
+                position -= listView.getHeaderViewsCount();
+            }
+            if (position < 0) {
+                // Header views don't count.
+                return;
+            }
+            // If we're still loading, we can't yet enable the buttons.
+            if (mAdapter.resolveInfoForPosition(position, true) == null) {
+                return;
+            }
+
+            final int checkedPos = mAdapterView.getCheckedItemPosition();
+            final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION;
+            if (!useLayoutWithDefault()
+                    && (!hasValidSelection || mLastSelected != checkedPos)
+                    && mAlwaysButton != null) {
+                setAlwaysButtonEnabled(hasValidSelection, checkedPos, true);
+                mOnceButton.setEnabled(hasValidSelection);
+                if (hasValidSelection) {
+                    mAdapterView.smoothScrollToPosition(checkedPos);
+                }
+                mLastSelected = checkedPos;
+            } else {
+                startSelected(position, false, true);
+            }
+        }
+
+        @Override
+        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+            final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+            if (listView != null) {
+                position -= listView.getHeaderViewsCount();
+            }
+            if (position < 0) {
+                // Header views don't count.
+                return false;
+            }
+            ResolveInfo ri = mAdapter.resolveInfoForPosition(position, true);
+            showTargetDetails(ri);
+            return true;
+        }
+
+    }
+
+    abstract class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
+        protected final DisplayResolveInfo mDisplayResolveInfo;
+        private final ResolveInfo mResolveInfo;
+
+        public LoadIconTask(DisplayResolveInfo dri) {
+            mDisplayResolveInfo = dri;
+            mResolveInfo = dri.getResolveInfo();
+        }
+
+        @Override
+        protected Drawable doInBackground(Void... params) {
+            return loadIconForResolveInfo(mResolveInfo);
+        }
+
+        @Override
+        protected void onPostExecute(Drawable d) {
+            mDisplayResolveInfo.setDisplayIcon(d);
+        }
+    }
+
+    class LoadAdapterIconTask extends LoadIconTask {
+        public LoadAdapterIconTask(DisplayResolveInfo dri) {
+            super(dri);
+        }
+
+        @Override
+        protected void onPostExecute(Drawable d) {
+            super.onPostExecute(d);
+            if (mProfileView != null && mAdapter.getOtherProfile() == mDisplayResolveInfo) {
+                bindProfileView();
+            }
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    class LoadIconIntoViewTask extends LoadIconTask {
+        private final ImageView mTargetView;
+
+        public LoadIconIntoViewTask(DisplayResolveInfo dri, ImageView target) {
+            super(dri);
+            mTargetView = target;
+        }
+
+        @Override
+        protected void onPostExecute(Drawable d) {
+            super.onPostExecute(d);
+            mTargetView.setImageDrawable(d);
+        }
+    }
+
+    static final boolean isSpecificUriMatch(int match) {
+        match = match&IntentFilter.MATCH_CATEGORY_MASK;
+        return match >= IntentFilter.MATCH_CATEGORY_HOST
+                && match <= IntentFilter.MATCH_CATEGORY_PATH;
+    }
+
+    static class PickTargetOptionRequest extends PickOptionRequest {
+        public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
+                @Nullable Bundle extras) {
+            super(prompt, options, extras);
+        }
+
+        @Override
+        public void onCancel() {
+            super.onCancel();
+            final ResolverActivity ra = (ResolverActivity) getActivity();
+            if (ra != null) {
+                ra.mPickOptionRequest = null;
+                ra.finish();
+            }
+        }
+
+        @Override
+        public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+            super.onPickOptionResult(finished, selections, result);
+            if (selections.length != 1) {
+                // TODO In a better world we would filter the UI presented here and let the
+                // user refine. Maybe later.
+                return;
+            }
+
+            final ResolverActivity ra = (ResolverActivity) getActivity();
+            if (ra != null) {
+                final TargetInfo ti = ra.mAdapter.getItem(selections[0].getIndex());
+                if (ra.onTargetSelected(ti, false)) {
+                    ra.mPickOptionRequest = null;
+                    ra.finish();
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/app/ResolverComparator.java b/com/android/internal/app/ResolverComparator.java
new file mode 100644
index 0000000..77cfc2f
--- /dev/null
+++ b/com/android/internal/app/ResolverComparator.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2015 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.internal.app;
+
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.SharedPreferences;
+import android.content.ServiceConnection;
+import android.metrics.LogMaker;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.storage.StorageManager;
+import android.os.UserHandle;
+import android.service.resolver.IResolverRankerService;
+import android.service.resolver.IResolverRankerResult;
+import android.service.resolver.ResolverRankerService;
+import android.service.resolver.ResolverTarget;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.io.File;
+import java.lang.InterruptedException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Ranks and compares packages based on usage stats.
+ */
+class ResolverComparator implements Comparator<ResolvedComponentInfo> {
+    private static final String TAG = "ResolverComparator";
+
+    private static final boolean DEBUG = false;
+
+    private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3;
+
+    // One week
+    private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7;
+
+    private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12;
+
+    private static final float RECENCY_MULTIPLIER = 2.f;
+
+    // message types
+    private static final int RESOLVER_RANKER_SERVICE_RESULT = 0;
+    private static final int RESOLVER_RANKER_RESULT_TIMEOUT = 1;
+
+    // timeout for establishing connections with a ResolverRankerService.
+    private static final int CONNECTION_COST_TIMEOUT_MILLIS = 200;
+    // timeout for establishing connections with a ResolverRankerService, collecting features and
+    // predicting ranking scores.
+    private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
+
+    private final Collator mCollator;
+    private final boolean mHttp;
+    private final PackageManager mPm;
+    private final UsageStatsManager mUsm;
+    private final Map<String, UsageStats> mStats;
+    private final long mCurrentTime;
+    private final long mSinceTime;
+    private final LinkedHashMap<ComponentName, ResolverTarget> mTargetsDict = new LinkedHashMap<>();
+    private final String mReferrerPackage;
+    private final Object mLock = new Object();
+    private ArrayList<ResolverTarget> mTargets;
+    private String mContentType;
+    private String[] mAnnotations;
+    private String mAction;
+    private ComponentName mResolvedRankerName;
+    private ComponentName mRankerServiceName;
+    private IResolverRankerService mRanker;
+    private ResolverRankerServiceConnection mConnection;
+    private AfterCompute mAfterCompute;
+    private Context mContext;
+    private CountDownLatch mConnectSignal;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case RESOLVER_RANKER_SERVICE_RESULT:
+                    if (DEBUG) {
+                        Log.d(TAG, "RESOLVER_RANKER_SERVICE_RESULT");
+                    }
+                    if (mHandler.hasMessages(RESOLVER_RANKER_RESULT_TIMEOUT)) {
+                        if (msg.obj != null) {
+                            final List<ResolverTarget> receivedTargets =
+                                    (List<ResolverTarget>) msg.obj;
+                            if (receivedTargets != null && mTargets != null
+                                    && receivedTargets.size() == mTargets.size()) {
+                                final int size = mTargets.size();
+                                boolean isUpdated = false;
+                                for (int i = 0; i < size; ++i) {
+                                    final float predictedProb =
+                                            receivedTargets.get(i).getSelectProbability();
+                                    if (predictedProb != mTargets.get(i).getSelectProbability()) {
+                                        mTargets.get(i).setSelectProbability(predictedProb);
+                                        isUpdated = true;
+                                    }
+                                }
+                                if (isUpdated) {
+                                    mRankerServiceName = mResolvedRankerName;
+                                }
+                            } else {
+                                Log.e(TAG, "Sizes of sent and received ResolverTargets diff.");
+                            }
+                        } else {
+                            Log.e(TAG, "Receiving null prediction results.");
+                        }
+                        mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT);
+                        mAfterCompute.afterCompute();
+                    }
+                    break;
+
+                case RESOLVER_RANKER_RESULT_TIMEOUT:
+                    if (DEBUG) {
+                        Log.d(TAG, "RESOLVER_RANKER_RESULT_TIMEOUT; unbinding services");
+                    }
+                    mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT);
+                    mAfterCompute.afterCompute();
+                    break;
+
+                default:
+                    super.handleMessage(msg);
+            }
+        }
+    };
+
+    public interface AfterCompute {
+        public void afterCompute ();
+    }
+
+    public ResolverComparator(Context context, Intent intent, String referrerPackage,
+                              AfterCompute afterCompute) {
+        mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
+        String scheme = intent.getScheme();
+        mHttp = "http".equals(scheme) || "https".equals(scheme);
+        mReferrerPackage = referrerPackage;
+        mAfterCompute = afterCompute;
+        mContext = context;
+
+        mPm = context.getPackageManager();
+        mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+
+        mCurrentTime = System.currentTimeMillis();
+        mSinceTime = mCurrentTime - USAGE_STATS_PERIOD;
+        mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime);
+        mContentType = intent.getType();
+        getContentAnnotations(intent);
+        mAction = intent.getAction();
+        mRankerServiceName = new ComponentName(mContext, this.getClass());
+    }
+
+    // get annotations of content from intent.
+    public void getContentAnnotations(Intent intent) {
+        ArrayList<String> annotations = intent.getStringArrayListExtra(
+                Intent.EXTRA_CONTENT_ANNOTATIONS);
+        if (annotations != null) {
+            int size = annotations.size();
+            if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) {
+                size = NUM_OF_TOP_ANNOTATIONS_TO_USE;
+            }
+            mAnnotations = new String[size];
+            for (int i = 0; i < size; i++) {
+                mAnnotations[i] = annotations.get(i);
+            }
+        }
+    }
+
+    public void setCallBack(AfterCompute afterCompute) {
+        mAfterCompute = afterCompute;
+    }
+
+    // compute features for each target according to usage stats of targets.
+    public void compute(List<ResolvedComponentInfo> targets) {
+        reset();
+
+        final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD;
+
+        float mostRecencyScore = 1.0f;
+        float mostTimeSpentScore = 1.0f;
+        float mostLaunchScore = 1.0f;
+        float mostChooserScore = 1.0f;
+
+        for (ResolvedComponentInfo target : targets) {
+            final ResolverTarget resolverTarget = new ResolverTarget();
+            mTargetsDict.put(target.name, resolverTarget);
+            final UsageStats pkStats = mStats.get(target.name.getPackageName());
+            if (pkStats != null) {
+                // Only count recency for apps that weren't the caller
+                // since the caller is always the most recent.
+                // Persistent processes muck this up, so omit them too.
+                if (!target.name.getPackageName().equals(mReferrerPackage)
+                        && !isPersistentProcess(target)) {
+                    final float recencyScore =
+                            (float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0);
+                    resolverTarget.setRecencyScore(recencyScore);
+                    if (recencyScore > mostRecencyScore) {
+                        mostRecencyScore = recencyScore;
+                    }
+                }
+                final float timeSpentScore = (float) pkStats.getTotalTimeInForeground();
+                resolverTarget.setTimeSpentScore(timeSpentScore);
+                if (timeSpentScore > mostTimeSpentScore) {
+                    mostTimeSpentScore = timeSpentScore;
+                }
+                final float launchScore = (float) pkStats.mLaunchCount;
+                resolverTarget.setLaunchScore(launchScore);
+                if (launchScore > mostLaunchScore) {
+                    mostLaunchScore = launchScore;
+                }
+
+                float chooserScore = 0.0f;
+                if (pkStats.mChooserCounts != null && mAction != null
+                        && pkStats.mChooserCounts.get(mAction) != null) {
+                    chooserScore = (float) pkStats.mChooserCounts.get(mAction)
+                            .getOrDefault(mContentType, 0);
+                    if (mAnnotations != null) {
+                        final int size = mAnnotations.length;
+                        for (int i = 0; i < size; i++) {
+                            chooserScore += (float) pkStats.mChooserCounts.get(mAction)
+                                    .getOrDefault(mAnnotations[i], 0);
+                        }
+                    }
+                }
+                if (DEBUG) {
+                    if (mAction == null) {
+                        Log.d(TAG, "Action type is null");
+                    } else {
+                        Log.d(TAG, "Chooser Count of " + mAction + ":" +
+                                target.name.getPackageName() + " is " +
+                                Float.toString(chooserScore));
+                    }
+                }
+                resolverTarget.setChooserScore(chooserScore);
+                if (chooserScore > mostChooserScore) {
+                    mostChooserScore = chooserScore;
+                }
+            }
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "compute - mostRecencyScore: " + mostRecencyScore
+                    + " mostTimeSpentScore: " + mostTimeSpentScore
+                    + " mostLaunchScore: " + mostLaunchScore
+                    + " mostChooserScore: " + mostChooserScore);
+        }
+
+        mTargets = new ArrayList<>(mTargetsDict.values());
+        for (ResolverTarget target : mTargets) {
+            final float recency = target.getRecencyScore() / mostRecencyScore;
+            setFeatures(target, recency * recency * RECENCY_MULTIPLIER,
+                    target.getLaunchScore() / mostLaunchScore,
+                    target.getTimeSpentScore() / mostTimeSpentScore,
+                    target.getChooserScore() / mostChooserScore);
+            addDefaultSelectProbability(target);
+            if (DEBUG) {
+                Log.d(TAG, "Scores: " + target);
+            }
+        }
+        predictSelectProbabilities(mTargets);
+    }
+
+    @Override
+    public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) {
+        final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
+        final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
+
+        // We want to put the one targeted to another user at the end of the dialog.
+        if (lhs.targetUserId != UserHandle.USER_CURRENT) {
+            return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1;
+        }
+        if (rhs.targetUserId != UserHandle.USER_CURRENT) {
+            return -1;
+        }
+
+        if (mHttp) {
+            // Special case: we want filters that match URI paths/schemes to be
+            // ordered before others.  This is for the case when opening URIs,
+            // to make native apps go above browsers.
+            final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match);
+            final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match);
+            if (lhsSpecific != rhsSpecific) {
+                return lhsSpecific ? -1 : 1;
+            }
+        }
+
+        final boolean lPinned = lhsp.isPinned();
+        final boolean rPinned = rhsp.isPinned();
+
+        if (lPinned && !rPinned) {
+            return -1;
+        } else if (!lPinned && rPinned) {
+            return 1;
+        }
+
+        // Pinned items stay stable within a normal lexical sort and ignore scoring.
+        if (!lPinned && !rPinned) {
+            if (mStats != null) {
+                final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName(
+                        lhs.activityInfo.packageName, lhs.activityInfo.name));
+                final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName(
+                        rhs.activityInfo.packageName, rhs.activityInfo.name));
+
+                if (lhsTarget != null && rhsTarget != null) {
+                    final int selectProbabilityDiff = Float.compare(
+                        rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability());
+
+                    if (selectProbabilityDiff != 0) {
+                        return selectProbabilityDiff > 0 ? 1 : -1;
+                    }
+                }
+            }
+        }
+
+        CharSequence  sa = lhs.loadLabel(mPm);
+        if (sa == null) sa = lhs.activityInfo.name;
+        CharSequence  sb = rhs.loadLabel(mPm);
+        if (sb == null) sb = rhs.activityInfo.name;
+
+        return mCollator.compare(sa.toString().trim(), sb.toString().trim());
+    }
+
+    public float getScore(ComponentName name) {
+        final ResolverTarget target = mTargetsDict.get(name);
+        if (target != null) {
+            return target.getSelectProbability();
+        }
+        return 0;
+    }
+
+    public void updateChooserCounts(String packageName, int userId, String action) {
+        if (mUsm != null) {
+            mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
+        }
+    }
+
+    // update ranking model when the connection to it is valid.
+    public void updateModel(ComponentName componentName) {
+        synchronized (mLock) {
+            if (mRanker != null) {
+                try {
+                    int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet())
+                            .indexOf(componentName);
+                    if (selectedPos >= 0 && mTargets != null) {
+                        final float selectedProbability = getScore(componentName);
+                        int order = 0;
+                        for (ResolverTarget target : mTargets) {
+                            if (target.getSelectProbability() > selectedProbability) {
+                                order++;
+                            }
+                        }
+                        logMetrics(order);
+                        mRanker.train(mTargets, selectedPos);
+                    } else {
+                        if (DEBUG) {
+                            Log.d(TAG, "Selected a unknown component: " + componentName);
+                        }
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error in Train: " + e);
+                }
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "Ranker is null; skip updateModel.");
+                }
+            }
+        }
+    }
+
+    // unbind the service and clear unhandled messges.
+    public void destroy() {
+        mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT);
+        mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT);
+        if (mConnection != null) {
+            mContext.unbindService(mConnection);
+            mConnection.destroy();
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Unbinded Resolver Ranker.");
+        }
+    }
+
+    // records metrics for evaluation.
+    private void logMetrics(int selectedPos) {
+        if (mRankerServiceName != null) {
+            MetricsLogger metricsLogger = new MetricsLogger();
+            LogMaker log = new LogMaker(MetricsEvent.ACTION_TARGET_SELECTED);
+            log.setComponentName(mRankerServiceName);
+            int isCategoryUsed = (mAnnotations == null) ? 0 : 1;
+            log.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, isCategoryUsed);
+            log.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, selectedPos);
+            metricsLogger.write(log);
+        }
+    }
+
+    // connect to a ranking service.
+    private void initRanker(Context context) {
+        synchronized (mLock) {
+            if (mConnection != null && mRanker != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "Ranker still exists; reusing the existing one.");
+                }
+                return;
+            }
+        }
+        Intent intent = resolveRankerService();
+        if (intent == null) {
+            return;
+        }
+        mConnectSignal = new CountDownLatch(1);
+        mConnection = new ResolverRankerServiceConnection(mConnectSignal);
+        context.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
+    }
+
+    // resolve the service for ranking.
+    private Intent resolveRankerService() {
+        Intent intent = new Intent(ResolverRankerService.SERVICE_INTERFACE);
+        final List<ResolveInfo> resolveInfos = mPm.queryIntentServices(intent, 0);
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            if (resolveInfo == null || resolveInfo.serviceInfo == null
+                    || resolveInfo.serviceInfo.applicationInfo == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "Failed to retrieve a ranker: " + resolveInfo);
+                }
+                continue;
+            }
+            ComponentName componentName = new ComponentName(
+                    resolveInfo.serviceInfo.applicationInfo.packageName,
+                    resolveInfo.serviceInfo.name);
+            try {
+                final String perm = mPm.getServiceInfo(componentName, 0).permission;
+                if (!ResolverRankerService.BIND_PERMISSION.equals(perm)) {
+                    Log.w(TAG, "ResolverRankerService " + componentName + " does not require"
+                            + " permission " + ResolverRankerService.BIND_PERMISSION
+                            + " - this service will not be queried for ResolverComparator."
+                            + " add android:permission=\""
+                            + ResolverRankerService.BIND_PERMISSION + "\""
+                            + " to the <service> tag for " + componentName
+                            + " in the manifest.");
+                    continue;
+                }
+                if (PackageManager.PERMISSION_GRANTED != mPm.checkPermission(
+                        ResolverRankerService.HOLD_PERMISSION,
+                        resolveInfo.serviceInfo.packageName)) {
+                    Log.w(TAG, "ResolverRankerService " + componentName + " does not hold"
+                            + " permission " + ResolverRankerService.HOLD_PERMISSION
+                            + " - this service will not be queried for ResolverComparator.");
+                    continue;
+                }
+            } catch (NameNotFoundException e) {
+                Log.e(TAG, "Could not look up service " + componentName
+                        + "; component name not found");
+                continue;
+            }
+            if (DEBUG) {
+                Log.d(TAG, "Succeeded to retrieve a ranker: " + componentName);
+            }
+            mResolvedRankerName = componentName;
+            intent.setComponent(componentName);
+            return intent;
+        }
+        return null;
+    }
+
+    // set a watchdog, to avoid waiting for ranking service for too long.
+    private void startWatchDog(int timeOutLimit) {
+        if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + timeOutLimit + "ms");
+        if (mHandler == null) {
+            Log.d(TAG, "Error: Handler is Null; Needs to be initialized.");
+        }
+        mHandler.sendEmptyMessageDelayed(RESOLVER_RANKER_RESULT_TIMEOUT, timeOutLimit);
+    }
+
+    private class ResolverRankerServiceConnection implements ServiceConnection {
+        private final CountDownLatch mConnectSignal;
+
+        public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
+            mConnectSignal = connectSignal;
+        }
+
+        public final IResolverRankerResult resolverRankerResult =
+                new IResolverRankerResult.Stub() {
+            @Override
+            public void sendResult(List<ResolverTarget> targets) throws RemoteException {
+                if (DEBUG) {
+                    Log.d(TAG, "Sending Result back to Resolver: " + targets);
+                }
+                synchronized (mLock) {
+                    final Message msg = Message.obtain();
+                    msg.what = RESOLVER_RANKER_SERVICE_RESULT;
+                    msg.obj = targets;
+                    mHandler.sendMessage(msg);
+                }
+            }
+        };
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (DEBUG) {
+                Log.d(TAG, "onServiceConnected: " + name);
+            }
+            synchronized (mLock) {
+                mRanker = IResolverRankerService.Stub.asInterface(service);
+                mConnectSignal.countDown();
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DEBUG) {
+                Log.d(TAG, "onServiceDisconnected: " + name);
+            }
+            synchronized (mLock) {
+                destroy();
+            }
+        }
+
+        public void destroy() {
+            synchronized (mLock) {
+                mRanker = null;
+            }
+        }
+    }
+
+    private void reset() {
+        mTargetsDict.clear();
+        mTargets = null;
+        mRankerServiceName = new ComponentName(mContext, this.getClass());
+        mResolvedRankerName = null;
+        startWatchDog(WATCHDOG_TIMEOUT_MILLIS);
+        initRanker(mContext);
+    }
+
+    // predict select probabilities if ranking service is valid.
+    private void predictSelectProbabilities(List<ResolverTarget> targets) {
+        if (mConnection == null) {
+            if (DEBUG) {
+                Log.d(TAG, "Has not found valid ResolverRankerService; Skip Prediction");
+            }
+            return;
+        } else {
+            try {
+                mConnectSignal.await(CONNECTION_COST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+                synchronized (mLock) {
+                    if (mRanker != null) {
+                        mRanker.predict(targets, mConnection.resolverRankerResult);
+                        return;
+                    } else {
+                        if (DEBUG) {
+                            Log.d(TAG, "Ranker has not been initialized; skip predict.");
+                        }
+                    }
+                }
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Error in Wait for Service Connection.");
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in Predict: " + e);
+            }
+        }
+        if (mAfterCompute != null) {
+            mAfterCompute.afterCompute();
+        }
+    }
+
+    // adds select prob as the default values, according to a pre-trained Logistic Regression model.
+    private void addDefaultSelectProbability(ResolverTarget target) {
+        float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() +
+                0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore();
+        target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum))));
+    }
+
+    // sets features for each target
+    private void setFeatures(ResolverTarget target, float recencyScore, float launchScore,
+                             float timeSpentScore, float chooserScore) {
+        target.setRecencyScore(recencyScore);
+        target.setLaunchScore(launchScore);
+        target.setTimeSpentScore(timeSpentScore);
+        target.setChooserScore(chooserScore);
+    }
+
+    static boolean isPersistentProcess(ResolvedComponentInfo rci) {
+        if (rci != null && rci.getCount() > 0) {
+            return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
+                    ApplicationInfo.FLAG_PERSISTENT) != 0;
+        }
+        return false;
+    }
+}
diff --git a/com/android/internal/app/ResolverListController.java b/com/android/internal/app/ResolverListController.java
new file mode 100644
index 0000000..2ab2d20
--- /dev/null
+++ b/com/android/internal/app/ResolverListController.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.annotation.WorkerThread;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.RemoteException;
+import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.InterruptedException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.List;
+
+/**
+ * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of
+ * resolvers.
+ */
+public class ResolverListController {
+
+    private final Context mContext;
+    private final PackageManager mpm;
+    private final int mLaunchedFromUid;
+
+    // Needed for sorting resolvers.
+    private final Intent mTargetIntent;
+    private final String mReferrerPackage;
+
+    private static final String TAG = "ResolverListController";
+    private static final boolean DEBUG = false;
+
+    Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private ResolverComparator mResolverComparator;
+    private boolean isComputed = false;
+
+    public ResolverListController(
+            Context context,
+            PackageManager pm,
+            Intent targetIntent,
+            String referrerPackage,
+            int launchedFromUid) {
+        mContext = context;
+        mpm = pm;
+        mLaunchedFromUid = launchedFromUid;
+        mTargetIntent = targetIntent;
+        mReferrerPackage = referrerPackage;
+        synchronized (mLock) {
+            mResolverComparator =
+                    new ResolverComparator(mContext, mTargetIntent, mReferrerPackage, null);
+        }
+    }
+
+    @VisibleForTesting
+    public ResolveInfo getLastChosen() throws RemoteException {
+        return AppGlobals.getPackageManager().getLastChosenActivity(
+                mTargetIntent, mTargetIntent.resolveTypeIfNeeded(mContext.getContentResolver()),
+                PackageManager.MATCH_DEFAULT_ONLY);
+    }
+
+    @VisibleForTesting
+    public void setLastChosen(Intent intent, IntentFilter filter, int match)
+            throws RemoteException {
+        AppGlobals.getPackageManager().setLastChosenActivity(intent,
+                intent.resolveType(mContext.getContentResolver()),
+                PackageManager.MATCH_DEFAULT_ONLY,
+                filter, match, intent.getComponent());
+    }
+
+    @VisibleForTesting
+    public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntent(
+            boolean shouldGetResolvedFilter,
+            boolean shouldGetActivityMetadata,
+            List<Intent> intents) {
+        List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
+        for (int i = 0, N = intents.size(); i < N; i++) {
+            final Intent intent = intents.get(i);
+            final List<ResolveInfo> infos = mpm.queryIntentActivities(intent,
+                    PackageManager.MATCH_DEFAULT_ONLY
+                            | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
+                            | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0)
+                            | PackageManager.MATCH_INSTANT);
+            // Remove any activities that are not exported.
+            int totalSize = infos.size();
+            for (int j = totalSize - 1; j >= 0 ; j--) {
+                ResolveInfo info = infos.get(j);
+                if (info.activityInfo != null && !info.activityInfo.exported) {
+                    infos.remove(j);
+                }
+            }
+            if (infos != null) {
+                if (resolvedComponents == null) {
+                    resolvedComponents = new ArrayList<>();
+                }
+                addResolveListDedupe(resolvedComponents, intent, infos);
+            }
+        }
+        return resolvedComponents;
+    }
+
+    @VisibleForTesting
+    public void addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into,
+            Intent intent,
+            List<ResolveInfo> from) {
+        final int fromCount = from.size();
+        final int intoCount = into.size();
+        for (int i = 0; i < fromCount; i++) {
+            final ResolveInfo newInfo = from.get(i);
+            boolean found = false;
+            // Only loop to the end of into as it was before we started; no dupes in from.
+            for (int j = 0; j < intoCount; j++) {
+                final ResolverActivity.ResolvedComponentInfo rci = into.get(j);
+                if (isSameResolvedComponent(newInfo, rci)) {
+                    found = true;
+                    rci.add(intent, newInfo);
+                    break;
+                }
+            }
+            if (!found) {
+                final ComponentName name = new ComponentName(
+                        newInfo.activityInfo.packageName, newInfo.activityInfo.name);
+                final ResolverActivity.ResolvedComponentInfo rci =
+                        new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
+                rci.setPinned(isComponentPinned(name));
+                into.add(rci);
+            }
+        }
+    }
+
+    // Filter out any activities that the launched uid does not have permission for.
+    //
+    // Also filter out those that are suspended because they couldn't be started. We don't do this
+    // when we have an explicit list of resolved activities, because that only happens when
+    // we are being subclassed, so we can safely launch whatever they gave us.
+    //
+    // To preserve the inputList, optionally will return the original list if any modification has
+    // been made.
+    @VisibleForTesting
+    public ArrayList<ResolverActivity.ResolvedComponentInfo> filterIneligibleActivities(
+            List<ResolverActivity.ResolvedComponentInfo> inputList,
+            boolean returnCopyOfOriginalListIfModified) {
+        ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+        for (int i = inputList.size()-1; i >= 0; i--) {
+            ActivityInfo ai = inputList.get(i)
+                    .getResolveInfoAt(0).activityInfo;
+            int granted = ActivityManager.checkComponentPermission(
+                    ai.permission, mLaunchedFromUid,
+                    ai.applicationInfo.uid, ai.exported);
+            boolean suspended = (ai.applicationInfo.flags
+                    & ApplicationInfo.FLAG_SUSPENDED) != 0;
+            if (granted != PackageManager.PERMISSION_GRANTED || suspended
+                    || isComponentFiltered(ai.getComponentName())) {
+                // Access not allowed! We're about to filter an item,
+                // so modify the unfiltered version if it hasn't already been modified.
+                if (returnCopyOfOriginalListIfModified && listToReturn == null) {
+                    listToReturn = new ArrayList<>(inputList);
+                }
+                inputList.remove(i);
+            }
+        }
+        return listToReturn;
+    }
+
+    // Filter out any low priority items.
+    //
+    // To preserve the inputList, optionally will return the original list if any modification has
+    // been made.
+    @VisibleForTesting
+    public ArrayList<ResolverActivity.ResolvedComponentInfo> filterLowPriority(
+            List<ResolverActivity.ResolvedComponentInfo> inputList,
+            boolean returnCopyOfOriginalListIfModified) {
+        ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+        // Only display the first matches that are either of equal
+        // priority or have asked to be default options.
+        ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0);
+        ResolveInfo r0 = rci0.getResolveInfoAt(0);
+        int N = inputList.size();
+        for (int i = 1; i < N; i++) {
+            ResolveInfo ri = inputList.get(i).getResolveInfoAt(0);
+            if (DEBUG) Log.v(
+                    TAG,
+                    r0.activityInfo.name + "=" +
+                            r0.priority + "/" + r0.isDefault + " vs " +
+                            ri.activityInfo.name + "=" +
+                            ri.priority + "/" + ri.isDefault);
+            if (r0.priority != ri.priority ||
+                    r0.isDefault != ri.isDefault) {
+                while (i < N) {
+                    if (returnCopyOfOriginalListIfModified && listToReturn == null) {
+                        listToReturn = new ArrayList<>(inputList);
+                    }
+                    inputList.remove(i);
+                    N--;
+                }
+            }
+        }
+        return listToReturn;
+    }
+
+    private class ComputeCallback implements ResolverComparator.AfterCompute {
+
+        private CountDownLatch mFinishComputeSignal;
+
+        public ComputeCallback(CountDownLatch finishComputeSignal) {
+            mFinishComputeSignal = finishComputeSignal;
+        }
+
+        public void afterCompute () {
+            mFinishComputeSignal.countDown();
+        }
+    }
+
+    @VisibleForTesting
+    @WorkerThread
+    public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
+        synchronized (mLock) {
+            if (mResolverComparator == null) {
+                Log.d(TAG, "Comparator has already been destroyed; skipped.");
+                return;
+            }
+            final CountDownLatch finishComputeSignal = new CountDownLatch(1);
+            ComputeCallback callback = new ComputeCallback(finishComputeSignal);
+            mResolverComparator.setCallBack(callback);
+            try {
+                long beforeRank = System.currentTimeMillis();
+                if (!isComputed) {
+                    mResolverComparator.compute(inputList);
+                    finishComputeSignal.await();
+                    isComputed = true;
+                }
+                Collections.sort(inputList, mResolverComparator);
+                long afterRank = System.currentTimeMillis();
+                if (DEBUG) {
+                    Log.d(TAG, "Time Cost: " + Long.toString(afterRank - beforeRank));
+                }
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Compute & Sort was interrupted: " + e);
+            }
+        }
+    }
+
+    private static boolean isSameResolvedComponent(ResolveInfo a,
+            ResolverActivity.ResolvedComponentInfo b) {
+        final ActivityInfo ai = a.activityInfo;
+        return ai.packageName.equals(b.name.getPackageName())
+                && ai.name.equals(b.name.getClassName());
+    }
+
+    boolean isComponentPinned(ComponentName name) {
+        return false;
+    }
+
+    boolean isComponentFiltered(ComponentName componentName) {
+        return false;
+    }
+
+    @VisibleForTesting
+    public float getScore(ResolverActivity.DisplayResolveInfo target) {
+        synchronized (mLock) {
+            if (mResolverComparator == null) {
+                return 0.0f;
+            }
+            return mResolverComparator.getScore(target.getResolvedComponentName());
+        }
+    }
+
+    public void updateModel(ComponentName componentName) {
+        synchronized (mLock) {
+            if (mResolverComparator != null) {
+                mResolverComparator.updateModel(componentName);
+            }
+        }
+    }
+
+    public void updateChooserCounts(String packageName, int userId, String action) {
+        synchronized (mLock) {
+            if (mResolverComparator != null) {
+                mResolverComparator.updateChooserCounts(packageName, userId, action);
+            }
+        }
+    }
+
+    public void destroy() {
+        synchronized (mLock) {
+            if (mResolverComparator != null) {
+                mResolverComparator.destroy();
+            }
+            mResolverComparator = null;
+        }
+    }
+}
diff --git a/com/android/internal/app/ResolverTargetActionsDialogFragment.java b/com/android/internal/app/ResolverTargetActionsDialogFragment.java
new file mode 100644
index 0000000..8156f79
--- /dev/null
+++ b/com/android/internal/app/ResolverTargetActionsDialogFragment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+
+import com.android.internal.R;
+
+/**
+ * Shows a dialog with actions to take on a chooser target
+ */
+public class ResolverTargetActionsDialogFragment extends DialogFragment
+        implements DialogInterface.OnClickListener {
+    private static final String NAME_KEY = "componentName";
+    private static final String PINNED_KEY = "pinned";
+    private static final String TITLE_KEY = "title";
+
+    // Sync with R.array.resolver_target_actions_* resources
+    private static final int TOGGLE_PIN_INDEX = 0;
+    private static final int APP_INFO_INDEX = 1;
+
+    public ResolverTargetActionsDialogFragment() {
+    }
+
+    public ResolverTargetActionsDialogFragment(CharSequence title, ComponentName name,
+            boolean pinned) {
+        Bundle args = new Bundle();
+        args.putCharSequence(TITLE_KEY, title);
+        args.putParcelable(NAME_KEY, name);
+        args.putBoolean(PINNED_KEY, pinned);
+        setArguments(args);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Bundle args = getArguments();
+        final int itemRes = args.getBoolean(PINNED_KEY, false)
+                ? R.array.resolver_target_actions_unpin
+                : R.array.resolver_target_actions_pin;
+        return new Builder(getContext())
+                .setCancelable(true)
+                .setItems(itemRes, this)
+                .setTitle(args.getCharSequence(TITLE_KEY))
+                .create();
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        final Bundle args = getArguments();
+        ComponentName name = args.getParcelable(NAME_KEY);
+        switch (which) {
+            case TOGGLE_PIN_INDEX:
+                SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext());
+                final String key = name.flattenToString();
+                boolean currentVal = sp.getBoolean(name.flattenToString(), false);
+                if (currentVal) {
+                    sp.edit().remove(key).apply();
+                } else {
+                    sp.edit().putBoolean(key, true).apply();
+                }
+
+                // Force the chooser to requery and resort things
+                getActivity().recreate();
+                break;
+            case APP_INFO_INDEX:
+                Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                        .setData(Uri.fromParts("package", name.getPackageName(), null))
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+                startActivity(in);
+                break;
+        }
+        dismiss();
+    }
+}
diff --git a/com/android/internal/app/ShutdownActivity.java b/com/android/internal/app/ShutdownActivity.java
new file mode 100644
index 0000000..745d28f
--- /dev/null
+++ b/com/android/internal/app/ShutdownActivity.java
@@ -0,0 +1,71 @@
+/*
+ * 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.internal.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IPowerManager;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Slog;
+
+public class ShutdownActivity extends Activity {
+
+    private static final String TAG = "ShutdownActivity";
+    private boolean mReboot;
+    private boolean mConfirm;
+    private boolean mUserRequested;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent intent = getIntent();
+        mReboot = Intent.ACTION_REBOOT.equals(intent.getAction());
+        mConfirm = intent.getBooleanExtra(Intent.EXTRA_KEY_CONFIRM, false);
+        mUserRequested = intent.getBooleanExtra(Intent.EXTRA_USER_REQUESTED_SHUTDOWN, false);
+        Slog.i(TAG, "onCreate(): confirm=" + mConfirm);
+
+        Thread thr = new Thread("ShutdownActivity") {
+            @Override
+            public void run() {
+                IPowerManager pm = IPowerManager.Stub.asInterface(
+                        ServiceManager.getService(Context.POWER_SERVICE));
+                try {
+                    if (mReboot) {
+                        pm.reboot(mConfirm, null, false);
+                    } else {
+                        pm.shutdown(mConfirm,
+                                    mUserRequested ? PowerManager.SHUTDOWN_USER_REQUESTED : null,
+                                    false);
+                    }
+                } catch (RemoteException e) {
+                }
+            }
+        };
+        thr.start();
+        finish();
+        // Wait for us to tell the power manager to shutdown.
+        try {
+            thr.join();
+        } catch (InterruptedException e) {
+        }
+    }
+}
diff --git a/com/android/internal/app/SuggestedLocaleAdapter.java b/com/android/internal/app/SuggestedLocaleAdapter.java
new file mode 100644
index 0000000..46f47a3
--- /dev/null
+++ b/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Set;
+
+
+/**
+ * This adapter wraps around a regular ListAdapter for LocaleInfo, and creates 2 sections.
+ *
+ * <p>The first section contains "suggested" languages (usually including a region),
+ * the second section contains all the languages within the original adapter.
+ * The "others" might still include languages that appear in the "suggested" section.</p>
+ *
+ * <p>Example: if we show "German Switzerland" as "suggested" (based on SIM, let's say),
+ * then "German" will still show in the "others" section, clicking on it will only show the
+ * countries for all the other German locales, but not Switzerland
+ * (Austria, Belgium, Germany, Liechtenstein, Luxembourg)</p>
+ */
+public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
+    private static final int TYPE_HEADER_SUGGESTED = 0;
+    private static final int TYPE_HEADER_ALL_OTHERS = 1;
+    private static final int TYPE_LOCALE = 2;
+    private static final int MIN_REGIONS_FOR_SUGGESTIONS = 6;
+
+    private ArrayList<LocaleStore.LocaleInfo> mLocaleOptions;
+    private ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
+    private int mSuggestionCount;
+    private final boolean mCountryMode;
+    private LayoutInflater mInflater;
+
+    private Locale mDisplayLocale = null;
+    // used to potentially cache a modified Context that uses mDisplayLocale
+    private Context mContextOverride = null;
+
+    public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) {
+        mCountryMode = countryMode;
+        mLocaleOptions = new ArrayList<>(localeOptions.size());
+        for (LocaleStore.LocaleInfo li : localeOptions) {
+            if (li.isSuggested()) {
+                mSuggestionCount++;
+            }
+            mLocaleOptions.add(li);
+        }
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return getItemViewType(position) == TYPE_LOCALE;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (!showHeaders()) {
+            return TYPE_LOCALE;
+        } else {
+            if (position == 0) {
+                return TYPE_HEADER_SUGGESTED;
+            }
+            if (position == mSuggestionCount + 1) {
+                return TYPE_HEADER_ALL_OTHERS;
+            }
+            return TYPE_LOCALE;
+        }
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        if (showHeaders()) {
+            return 3; // Two headers in addition to the locales
+        } else {
+            return 1; // Locales items only
+        }
+    }
+
+    @Override
+    public int getCount() {
+        if (showHeaders()) {
+            return mLocaleOptions.size() + 2; // 2 extra for the headers
+        } else {
+            return mLocaleOptions.size();
+        }
+    }
+
+    @Override
+    public Object getItem(int position) {
+        int offset = 0;
+        if (showHeaders()) {
+            offset = position > mSuggestionCount ? -2 : -1;
+        }
+
+        return mLocaleOptions.get(position + offset);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    /**
+     * Overrides the locale used to display localized labels. Setting the locale to null will reset
+     * the Adapter to use the default locale for the labels.
+     */
+    public void setDisplayLocale(@NonNull Context context, @Nullable Locale locale) {
+        if (locale == null) {
+            mDisplayLocale = null;
+            mContextOverride = null;
+        } else if (!locale.equals(mDisplayLocale)) {
+            mDisplayLocale = locale;
+            final Configuration configOverride = new Configuration();
+            configOverride.setLocale(locale);
+            mContextOverride = context.createConfigurationContext(configOverride);
+        }
+    }
+
+    private void setTextTo(@NonNull TextView textView, int resId) {
+        if (mContextOverride == null) {
+            textView.setText(resId);
+        } else {
+            textView.setText(mContextOverride.getText(resId));
+            // If mContextOverride is not null, mDisplayLocale can't be null either.
+        }
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null && mInflater == null) {
+            mInflater = LayoutInflater.from(parent.getContext());
+        }
+
+        int itemType = getItemViewType(position);
+        switch (itemType) {
+            case TYPE_HEADER_SUGGESTED: // intentional fallthrough
+            case TYPE_HEADER_ALL_OTHERS:
+                // Covers both null, and "reusing" a wrong kind of view
+                if (!(convertView instanceof TextView)) {
+                    convertView = mInflater.inflate(R.layout.language_picker_section_header,
+                            parent, false);
+                }
+                TextView textView = (TextView) convertView;
+                if (itemType == TYPE_HEADER_SUGGESTED) {
+                    setTextTo(textView, R.string.language_picker_section_suggested);
+                } else {
+                    if (mCountryMode) {
+                        setTextTo(textView, R.string.region_picker_section_all);
+                    } else {
+                        setTextTo(textView, R.string.language_picker_section_all);
+                    }
+                }
+                textView.setTextLocale(
+                        mDisplayLocale != null ? mDisplayLocale : Locale.getDefault());
+                break;
+            default:
+                // Covers both null, and "reusing" a wrong kind of view
+                if (!(convertView instanceof ViewGroup)) {
+                    convertView = mInflater.inflate(R.layout.language_picker_item, parent, false);
+                }
+
+                TextView text = (TextView) convertView.findViewById(R.id.locale);
+                LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
+                text.setText(item.getLabel(mCountryMode));
+                text.setTextLocale(item.getLocale());
+                text.setContentDescription(item.getContentDescription(mCountryMode));
+                if (mCountryMode) {
+                    int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
+                    //noinspection ResourceType
+                    convertView.setLayoutDirection(layoutDir);
+                    text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL
+                            ? View.TEXT_DIRECTION_RTL
+                            : View.TEXT_DIRECTION_LTR);
+                }
+        }
+        return convertView;
+    }
+
+    private boolean showHeaders() {
+        // We don't want to show suggestions for locales with very few regions
+        // (e.g. Romanian, with 2 regions)
+        // So we put a (somewhat) arbitrary limit.
+        //
+        // The initial idea was to make that limit dependent on the screen height.
+        // But that would mean rotating the screen could make the suggestions disappear,
+        // as the number of countries that fits on the screen would be different in portrait
+        // and landscape mode.
+        if (mCountryMode && mLocaleOptions.size() < MIN_REGIONS_FOR_SUGGESTIONS) {
+            return false;
+        }
+        return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size();
+    }
+
+    /**
+     * Sorts the items in the adapter using a locale-aware comparator.
+     * @param comp The locale-aware comparator to use.
+     */
+    public void sort(LocaleHelper.LocaleInfoComparator comp) {
+        Collections.sort(mLocaleOptions, comp);
+    }
+
+    class FilterByNativeAndUiNames extends Filter {
+
+        @Override
+        protected FilterResults performFiltering(CharSequence prefix) {
+            FilterResults results = new FilterResults();
+
+            if (mOriginalLocaleOptions == null) {
+                mOriginalLocaleOptions = new ArrayList<>(mLocaleOptions);
+            }
+
+            ArrayList<LocaleStore.LocaleInfo> values;
+            values = new ArrayList<>(mOriginalLocaleOptions);
+            if (prefix == null || prefix.length() == 0) {
+                results.values = values;
+                results.count = values.size();
+            } else {
+                // TODO: decide if we should use the string's locale
+                Locale locale = Locale.getDefault();
+                String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale);
+
+                final int count = values.size();
+                final ArrayList<LocaleStore.LocaleInfo> newValues = new ArrayList<>();
+
+                for (int i = 0; i < count; i++) {
+                    final LocaleStore.LocaleInfo value = values.get(i);
+                    final String nameToCheck = LocaleHelper.normalizeForSearch(
+                            value.getFullNameInUiLanguage(), locale);
+                    final String nativeNameToCheck = LocaleHelper.normalizeForSearch(
+                            value.getFullNameNative(), locale);
+                    if (wordMatches(nativeNameToCheck, prefixString)
+                            || wordMatches(nameToCheck, prefixString)) {
+                        newValues.add(value);
+                    }
+                }
+
+                results.values = newValues;
+                results.count = newValues.size();
+            }
+
+            return results;
+        }
+
+        // TODO: decide if this is enough, or we want to use a BreakIterator...
+        boolean wordMatches(String valueText, String prefixString) {
+            // First match against the whole, non-split value
+            if (valueText.startsWith(prefixString)) {
+                return true;
+            }
+
+            final String[] words = valueText.split(" ");
+            // Start at index 0, in case valueText starts with space(s)
+            for (String word : words) {
+                if (word.startsWith(prefixString)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults results) {
+            mLocaleOptions = (ArrayList<LocaleStore.LocaleInfo>) results.values;
+
+            mSuggestionCount = 0;
+            for (LocaleStore.LocaleInfo li : mLocaleOptions) {
+                if (li.isSuggested()) {
+                    mSuggestionCount++;
+                }
+            }
+
+            if (results.count > 0) {
+                notifyDataSetChanged();
+            } else {
+                notifyDataSetInvalidated();
+            }
+        }
+    }
+
+    @Override
+    public Filter getFilter() {
+        return new FilterByNativeAndUiNames();
+    }
+}
diff --git a/com/android/internal/app/SystemUserHomeActivity.java b/com/android/internal/app/SystemUserHomeActivity.java
new file mode 100644
index 0000000..26fbf6f
--- /dev/null
+++ b/com/android/internal/app/SystemUserHomeActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 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.internal.app;
+
+import android.app.Activity;
+
+/**
+ * Placeholder home activity, which is always installed on the system user. At least one home
+ * activity must be present and enabled in order for the system to boot.
+ */
+public class SystemUserHomeActivity extends Activity {
+}
diff --git a/com/android/internal/app/ToolbarActionBar.java b/com/android/internal/app/ToolbarActionBar.java
new file mode 100644
index 0000000..b3904f4
--- /dev/null
+++ b/com/android/internal/app/ToolbarActionBar.java
@@ -0,0 +1,584 @@
+/*
+ * Copyright (C) 2014 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.internal.app;
+
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.widget.DecorToolbar;
+import com.android.internal.widget.ToolbarWidgetWrapper;
+
+import android.annotation.Nullable;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.view.ActionMode;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowCallbackWrapper;
+import android.widget.SpinnerAdapter;
+import android.widget.Toolbar;
+
+import java.util.ArrayList;
+
+public class ToolbarActionBar extends ActionBar {
+    private DecorToolbar mDecorToolbar;
+    private boolean mToolbarMenuPrepared;
+    private Window.Callback mWindowCallback;
+    private boolean mMenuCallbackSet;
+
+    private boolean mLastMenuVisibility;
+    private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners =
+            new ArrayList<OnMenuVisibilityListener>();
+
+    private final Runnable mMenuInvalidator = new Runnable() {
+        @Override
+        public void run() {
+            populateOptionsMenu();
+        }
+    };
+
+    private final Toolbar.OnMenuItemClickListener mMenuClicker =
+            new Toolbar.OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            return mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, item);
+        }
+    };
+
+    public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback windowCallback) {
+        mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false);
+        mWindowCallback = new ToolbarCallbackWrapper(windowCallback);
+        mDecorToolbar.setWindowCallback(mWindowCallback);
+        toolbar.setOnMenuItemClickListener(mMenuClicker);
+        mDecorToolbar.setWindowTitle(title);
+    }
+
+    public Window.Callback getWrappedWindowCallback() {
+        return mWindowCallback;
+    }
+
+    @Override
+    public void setCustomView(View view) {
+        setCustomView(view, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+    }
+
+    @Override
+    public void setCustomView(View view, LayoutParams layoutParams) {
+        if (view != null) {
+            view.setLayoutParams(layoutParams);
+        }
+        mDecorToolbar.setCustomView(view);
+    }
+
+    @Override
+    public void setCustomView(int resId) {
+        final LayoutInflater inflater = LayoutInflater.from(mDecorToolbar.getContext());
+        setCustomView(inflater.inflate(resId, mDecorToolbar.getViewGroup(), false));
+    }
+
+    @Override
+    public void setIcon(int resId) {
+        mDecorToolbar.setIcon(resId);
+    }
+
+    @Override
+    public void setIcon(Drawable icon) {
+        mDecorToolbar.setIcon(icon);
+    }
+
+    @Override
+    public void setLogo(int resId) {
+        mDecorToolbar.setLogo(resId);
+    }
+
+    @Override
+    public void setLogo(Drawable logo) {
+        mDecorToolbar.setLogo(logo);
+    }
+
+    @Override
+    public void setStackedBackgroundDrawable(Drawable d) {
+        // This space for rent (do nothing)
+    }
+
+    @Override
+    public void setSplitBackgroundDrawable(Drawable d) {
+        // This space for rent (do nothing)
+    }
+
+    @Override
+    public void setHomeButtonEnabled(boolean enabled) {
+        // If the nav button on a Toolbar is present, it's enabled. No-op.
+    }
+
+    @Override
+    public void setElevation(float elevation) {
+        mDecorToolbar.getViewGroup().setElevation(elevation);
+    }
+
+    @Override
+    public float getElevation() {
+        return mDecorToolbar.getViewGroup().getElevation();
+    }
+
+    @Override
+    public Context getThemedContext() {
+        return mDecorToolbar.getContext();
+    }
+
+    @Override
+    public boolean isTitleTruncated() {
+        return super.isTitleTruncated();
+    }
+
+    @Override
+    public void setHomeAsUpIndicator(Drawable indicator) {
+        mDecorToolbar.setNavigationIcon(indicator);
+    }
+
+    @Override
+    public void setHomeAsUpIndicator(int resId) {
+        mDecorToolbar.setNavigationIcon(resId);
+    }
+
+    @Override
+    public void setHomeActionContentDescription(CharSequence description) {
+        mDecorToolbar.setNavigationContentDescription(description);
+    }
+
+    @Override
+    public void setDefaultDisplayHomeAsUpEnabled(boolean enabled) {
+        // Do nothing
+    }
+
+    @Override
+    public void setHomeActionContentDescription(int resId) {
+        mDecorToolbar.setNavigationContentDescription(resId);
+    }
+
+    @Override
+    public void setShowHideAnimationEnabled(boolean enabled) {
+        // This space for rent; no-op.
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+    }
+
+    @Override
+    public ActionMode startActionMode(ActionMode.Callback callback) {
+        return null;
+    }
+
+    @Override
+    public void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback) {
+        mDecorToolbar.setDropdownParams(adapter, new NavItemSelectedListener(callback));
+    }
+
+    @Override
+    public void setSelectedNavigationItem(int position) {
+        switch (mDecorToolbar.getNavigationMode()) {
+            case NAVIGATION_MODE_LIST:
+                mDecorToolbar.setDropdownSelectedPosition(position);
+                break;
+            default:
+                throw new IllegalStateException(
+                        "setSelectedNavigationIndex not valid for current navigation mode");
+        }
+    }
+
+    @Override
+    public int getSelectedNavigationIndex() {
+        return -1;
+    }
+
+    @Override
+    public int getNavigationItemCount() {
+        return 0;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mDecorToolbar.setTitle(title);
+    }
+
+    @Override
+    public void setTitle(int resId) {
+        mDecorToolbar.setTitle(resId != 0 ? mDecorToolbar.getContext().getText(resId) : null);
+    }
+
+    @Override
+    public void setWindowTitle(CharSequence title) {
+        mDecorToolbar.setWindowTitle(title);
+    }
+
+    @Override
+    public void setSubtitle(CharSequence subtitle) {
+        mDecorToolbar.setSubtitle(subtitle);
+    }
+
+    @Override
+    public void setSubtitle(int resId) {
+        mDecorToolbar.setSubtitle(resId != 0 ? mDecorToolbar.getContext().getText(resId) : null);
+    }
+
+    @Override
+    public void setDisplayOptions(@DisplayOptions int options) {
+        setDisplayOptions(options, 0xffffffff);
+    }
+
+    @Override
+    public void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask) {
+        final int currentOptions = mDecorToolbar.getDisplayOptions();
+        mDecorToolbar.setDisplayOptions(options & mask | currentOptions & ~mask);
+    }
+
+    @Override
+    public void setDisplayUseLogoEnabled(boolean useLogo) {
+        setDisplayOptions(useLogo ? DISPLAY_USE_LOGO : 0, DISPLAY_USE_LOGO);
+    }
+
+    @Override
+    public void setDisplayShowHomeEnabled(boolean showHome) {
+        setDisplayOptions(showHome ? DISPLAY_SHOW_HOME : 0, DISPLAY_SHOW_HOME);
+    }
+
+    @Override
+    public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
+        setDisplayOptions(showHomeAsUp ? DISPLAY_HOME_AS_UP : 0, DISPLAY_HOME_AS_UP);
+    }
+
+    @Override
+    public void setDisplayShowTitleEnabled(boolean showTitle) {
+        setDisplayOptions(showTitle ? DISPLAY_SHOW_TITLE : 0, DISPLAY_SHOW_TITLE);
+    }
+
+    @Override
+    public void setDisplayShowCustomEnabled(boolean showCustom) {
+        setDisplayOptions(showCustom ? DISPLAY_SHOW_CUSTOM : 0, DISPLAY_SHOW_CUSTOM);
+    }
+
+    @Override
+    public void setBackgroundDrawable(@Nullable Drawable d) {
+        mDecorToolbar.setBackgroundDrawable(d);
+    }
+
+    @Override
+    public View getCustomView() {
+        return mDecorToolbar.getCustomView();
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        return mDecorToolbar.getTitle();
+    }
+
+    @Override
+    public CharSequence getSubtitle() {
+        return mDecorToolbar.getSubtitle();
+    }
+
+    @Override
+    public int getNavigationMode() {
+        return NAVIGATION_MODE_STANDARD;
+    }
+
+    @Override
+    public void setNavigationMode(@NavigationMode int mode) {
+        if (mode == ActionBar.NAVIGATION_MODE_TABS) {
+            throw new IllegalArgumentException("Tabs not supported in this configuration");
+        }
+        mDecorToolbar.setNavigationMode(mode);
+    }
+
+    @Override
+    public int getDisplayOptions() {
+        return mDecorToolbar.getDisplayOptions();
+    }
+
+    @Override
+    public Tab newTab() {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void addTab(Tab tab) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void addTab(Tab tab, boolean setSelected) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void addTab(Tab tab, int position) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void addTab(Tab tab, int position, boolean setSelected) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void removeTab(Tab tab) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void removeTabAt(int position) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void removeAllTabs() {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public void selectTab(Tab tab) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public Tab getSelectedTab() {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public Tab getTabAt(int index) {
+        throw new UnsupportedOperationException(
+                "Tabs are not supported in toolbar action bars");
+    }
+
+    @Override
+    public int getTabCount() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return mDecorToolbar.getHeight();
+    }
+
+    @Override
+    public void show() {
+        // TODO: Consider a better transition for this.
+        // Right now use no automatic transition so that the app can supply one if desired.
+        mDecorToolbar.setVisibility(View.VISIBLE);
+    }
+
+    @Override
+    public void hide() {
+        // TODO: Consider a better transition for this.
+        // Right now use no automatic transition so that the app can supply one if desired.
+        mDecorToolbar.setVisibility(View.GONE);
+    }
+
+    @Override
+    public boolean isShowing() {
+        return mDecorToolbar.getVisibility() == View.VISIBLE;
+    }
+
+    @Override
+    public boolean openOptionsMenu() {
+        return mDecorToolbar.showOverflowMenu();
+    }
+
+    @Override
+    public boolean closeOptionsMenu() {
+        return mDecorToolbar.hideOverflowMenu();
+    }
+
+    @Override
+    public boolean invalidateOptionsMenu() {
+        mDecorToolbar.getViewGroup().removeCallbacks(mMenuInvalidator);
+        mDecorToolbar.getViewGroup().postOnAnimation(mMenuInvalidator);
+        return true;
+    }
+
+    @Override
+    public boolean collapseActionView() {
+        if (mDecorToolbar.hasExpandedActionView()) {
+            mDecorToolbar.collapseActionView();
+            return true;
+        }
+        return false;
+    }
+
+    void populateOptionsMenu() {
+        if (!mMenuCallbackSet) {
+            mDecorToolbar.setMenuCallbacks(new ActionMenuPresenterCallback(), new MenuBuilderCallback());
+            mMenuCallbackSet = true;
+        }
+        final Menu menu = mDecorToolbar.getMenu();
+        final MenuBuilder mb = menu instanceof MenuBuilder ? (MenuBuilder) menu : null;
+        if (mb != null) {
+            mb.stopDispatchingItemsChanged();
+        }
+        try {
+            menu.clear();
+            if (!mWindowCallback.onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, menu) ||
+                    !mWindowCallback.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, null, menu)) {
+                menu.clear();
+            }
+        } finally {
+            if (mb != null) {
+                mb.startDispatchingItemsChanged();
+            }
+        }
+    }
+
+    @Override
+    public boolean onMenuKeyEvent(KeyEvent event) {
+        if (event.getAction() == KeyEvent.ACTION_UP) {
+            openOptionsMenu();
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+        Menu menu = mDecorToolbar.getMenu();
+        if (menu != null) {
+            final KeyCharacterMap kmap = KeyCharacterMap.load(
+                    event != null ? event.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD);
+            menu.setQwertyMode(kmap.getKeyboardType() != KeyCharacterMap.NUMERIC);
+            return menu.performShortcut(keyCode, event, 0);
+        }
+        return false;
+    }
+
+    @Override
+    public void onDestroy() {
+        // Remove any invalidation callbacks
+        mDecorToolbar.getViewGroup().removeCallbacks(mMenuInvalidator);
+    }
+
+    public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        mMenuVisibilityListeners.add(listener);
+    }
+
+    public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        mMenuVisibilityListeners.remove(listener);
+    }
+
+    public void dispatchMenuVisibilityChanged(boolean isVisible) {
+        if (isVisible == mLastMenuVisibility) {
+            return;
+        }
+        mLastMenuVisibility = isVisible;
+
+        final int count = mMenuVisibilityListeners.size();
+        for (int i = 0; i < count; i++) {
+            mMenuVisibilityListeners.get(i).onMenuVisibilityChanged(isVisible);
+        }
+    }
+
+    private class ToolbarCallbackWrapper extends WindowCallbackWrapper {
+        public ToolbarCallbackWrapper(Window.Callback wrapped) {
+            super(wrapped);
+        }
+
+        @Override
+        public boolean onPreparePanel(int featureId, View view, Menu menu) {
+            final boolean result = super.onPreparePanel(featureId, view, menu);
+            if (result && !mToolbarMenuPrepared) {
+                mDecorToolbar.setMenuPrepared();
+                mToolbarMenuPrepared = true;
+            }
+            return result;
+        }
+
+        @Override
+        public View onCreatePanelView(int featureId) {
+            if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+                // This gets called by PhoneWindow.preparePanel. Since this already manages
+                // its own panel, we return a dummy view here to prevent PhoneWindow from
+                // preparing a default one.
+                return new View(mDecorToolbar.getContext());
+            }
+            return super.onCreatePanelView(featureId);
+        }
+    }
+
+    private final class ActionMenuPresenterCallback implements MenuPresenter.Callback {
+        private boolean mClosingActionMenu;
+
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            if (mWindowCallback != null) {
+                mWindowCallback.onMenuOpened(Window.FEATURE_ACTION_BAR, subMenu);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+            if (mClosingActionMenu) {
+                return;
+            }
+
+            mClosingActionMenu = true;
+            mDecorToolbar.dismissPopupMenus();
+            if (mWindowCallback != null) {
+                mWindowCallback.onPanelClosed(Window.FEATURE_ACTION_BAR, menu);
+            }
+            mClosingActionMenu = false;
+        }
+    }
+
+    private final class MenuBuilderCallback implements MenuBuilder.Callback {
+
+        @Override
+        public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+            return false;
+        }
+
+        @Override
+        public void onMenuModeChange(MenuBuilder menu) {
+            if (mWindowCallback != null) {
+                if (mDecorToolbar.isOverflowMenuShowing()) {
+                    mWindowCallback.onPanelClosed(Window.FEATURE_ACTION_BAR, menu);
+                } else if (mWindowCallback.onPreparePanel(Window.FEATURE_OPTIONS_PANEL,
+                        null, menu)) {
+                    mWindowCallback.onMenuOpened(Window.FEATURE_ACTION_BAR, menu);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/app/UnlaunchableAppActivity.java b/com/android/internal/app/UnlaunchableAppActivity.java
new file mode 100644
index 0000000..0a539f1
--- /dev/null
+++ b/com/android/internal/app/UnlaunchableAppActivity.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+/**
+ * A dialog shown to the user when they try to launch an app from a quiet profile
+ * ({@link UserManager#isQuietModeEnabled(UserHandle)}.
+ */
+public class UnlaunchableAppActivity extends Activity
+        implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener {
+    private static final String TAG = "UnlaunchableAppActivity";
+
+    private static final int UNLAUNCHABLE_REASON_QUIET_MODE = 1;
+    private static final String EXTRA_UNLAUNCHABLE_REASON = "unlaunchable_reason";
+
+    private int mUserId;
+    private int mReason;
+    private IntentSender mTarget;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // As this activity has nothing to show, we should hide the title bar also
+        // TODO: Use AlertActivity so we don't need to hide title bar and create a dialog
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        Intent intent = getIntent();
+        mReason = intent.getIntExtra(EXTRA_UNLAUNCHABLE_REASON, -1);
+        mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+        mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+
+        if (mUserId == UserHandle.USER_NULL) {
+            Log.wtf(TAG, "Invalid user id: " + mUserId + ". Stopping.");
+            finish();
+            return;
+        }
+
+        String dialogTitle;
+        String dialogMessage = null;
+        if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE) {
+            dialogTitle = getResources().getString(R.string.work_mode_off_title);
+            dialogMessage = getResources().getString(R.string.work_mode_off_message);
+        } else {
+            Log.wtf(TAG, "Invalid unlaunchable type: " + mReason);
+            finish();
+            return;
+        }
+
+        View rootView = LayoutInflater.from(this).inflate(R.layout.unlaunchable_app_activity, null);
+        TextView titleView = (TextView)rootView.findViewById(R.id.unlaunchable_app_title);
+        TextView messageView = (TextView)rootView.findViewById(R.id.unlaunchable_app_message);
+        titleView.setText(dialogTitle);
+        messageView.setText(dialogMessage);
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                .setView(rootView)
+                .setOnDismissListener(this);
+        if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE) {
+            builder.setPositiveButton(R.string.work_mode_turn_on, this)
+                    .setNegativeButton(R.string.cancel, null);
+        } else {
+            builder.setPositiveButton(R.string.ok, null);
+        }
+        builder.show();
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        finish();
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE && which == DialogInterface.BUTTON_POSITIVE) {
+            if (UserManager.get(this).trySetQuietModeDisabled(mUserId, mTarget)
+                    && mTarget != null) {
+                try {
+                    startIntentSenderForResult(mTarget, -1, null, 0, 0, 0);
+                } catch (IntentSender.SendIntentException e) {
+                    /* ignore */
+                }
+            }
+        }
+    }
+
+    private static final Intent createBaseIntent() {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName("android", UnlaunchableAppActivity.class.getName()));
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        return intent;
+    }
+
+    public static Intent createInQuietModeDialogIntent(int userId) {
+        Intent intent = createBaseIntent();
+        intent.putExtra(EXTRA_UNLAUNCHABLE_REASON, UNLAUNCHABLE_REASON_QUIET_MODE);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+        return intent;
+    }
+
+    public static Intent createInQuietModeDialogIntent(int userId, IntentSender target) {
+        Intent intent = createInQuietModeDialogIntent(userId);
+        intent.putExtra(Intent.EXTRA_INTENT, target);
+        return intent;
+    }
+}
diff --git a/com/android/internal/app/WindowDecorActionBar.java b/com/android/internal/app/WindowDecorActionBar.java
new file mode 100644
index 0000000..1b3faf5
--- /dev/null
+++ b/com/android/internal/app/WindowDecorActionBar.java
@@ -0,0 +1,1384 @@
+/*
+ * Copyright (C) 2010 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.internal.app;
+
+import com.android.internal.R;
+import com.android.internal.view.ActionBarPolicy;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.SubMenuBuilder;
+import com.android.internal.widget.ActionBarContainer;
+import com.android.internal.widget.ActionBarContextView;
+import com.android.internal.widget.ActionBarOverlayLayout;
+import com.android.internal.widget.DecorToolbar;
+import com.android.internal.widget.ScrollingTabContainerView;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.Window;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AnimationUtils;
+import android.widget.SpinnerAdapter;
+import android.widget.Toolbar;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * WindowDecorActionBar is the ActionBar implementation used
+ * by devices of all screen sizes as part of the window decor layout.
+ * If it detects a compatible decor, it will split contextual modes
+ * across both the ActionBarView at the top of the screen and
+ * a horizontal LinearLayout at the bottom which is normally hidden.
+ */
+public class WindowDecorActionBar extends ActionBar implements
+        ActionBarOverlayLayout.ActionBarVisibilityCallback {
+    private static final String TAG = "WindowDecorActionBar";
+
+    private Context mContext;
+    private Context mThemedContext;
+    private Activity mActivity;
+    private Dialog mDialog;
+
+    private ActionBarOverlayLayout mOverlayLayout;
+    private ActionBarContainer mContainerView;
+    private DecorToolbar mDecorToolbar;
+    private ActionBarContextView mContextView;
+    private ActionBarContainer mSplitView;
+    private View mContentView;
+    private ScrollingTabContainerView mTabScrollView;
+
+    private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>();
+
+    private TabImpl mSelectedTab;
+    private int mSavedTabPosition = INVALID_POSITION;
+
+    private boolean mDisplayHomeAsUpSet;
+
+    ActionMode mActionMode;
+    ActionMode mDeferredDestroyActionMode;
+    ActionMode.Callback mDeferredModeDestroyCallback;
+
+    private boolean mLastMenuVisibility;
+    private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners =
+            new ArrayList<OnMenuVisibilityListener>();
+
+    private static final int CONTEXT_DISPLAY_NORMAL = 0;
+    private static final int CONTEXT_DISPLAY_SPLIT = 1;
+
+    private static final int INVALID_POSITION = -1;
+
+    // The fade duration for toolbar and action bar when entering/exiting action mode.
+    private static final long FADE_OUT_DURATION_MS = 100;
+    private static final long FADE_IN_DURATION_MS = 200;
+
+    private int mContextDisplayMode;
+    private boolean mHasEmbeddedTabs;
+
+    private int mCurWindowVisibility = View.VISIBLE;
+
+    private boolean mContentAnimations = true;
+    private boolean mHiddenByApp;
+    private boolean mHiddenBySystem;
+    private boolean mShowingForMode;
+
+    private boolean mNowShowing = true;
+
+    private Animator mCurrentShowAnim;
+    private boolean mShowHideAnimationEnabled;
+    boolean mHideOnContentScroll;
+
+    final AnimatorListener mHideListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (mContentAnimations && mContentView != null) {
+                mContentView.setTranslationY(0);
+                mContainerView.setTranslationY(0);
+            }
+            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
+                mSplitView.setVisibility(View.GONE);
+            }
+            mContainerView.setVisibility(View.GONE);
+            mContainerView.setTransitioning(false);
+            mCurrentShowAnim = null;
+            completeDeferredDestroyActionMode();
+            if (mOverlayLayout != null) {
+                mOverlayLayout.requestApplyInsets();
+            }
+        }
+    };
+
+    final AnimatorListener mShowListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mCurrentShowAnim = null;
+            mContainerView.requestLayout();
+        }
+    };
+
+    final ValueAnimator.AnimatorUpdateListener mUpdateListener =
+            new ValueAnimator.AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            final ViewParent parent = mContainerView.getParent();
+            ((View) parent).invalidate();
+        }
+    };
+
+    public WindowDecorActionBar(Activity activity) {
+        mActivity = activity;
+        Window window = activity.getWindow();
+        View decor = window.getDecorView();
+        boolean overlayMode = mActivity.getWindow().hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+        init(decor);
+        if (!overlayMode) {
+            mContentView = decor.findViewById(android.R.id.content);
+        }
+    }
+
+    public WindowDecorActionBar(Dialog dialog) {
+        mDialog = dialog;
+        init(dialog.getWindow().getDecorView());
+    }
+
+    /**
+     * Only for edit mode.
+     * @hide
+     */
+    public WindowDecorActionBar(View layout) {
+        assert layout.isInEditMode();
+        init(layout);
+    }
+
+    private void init(View decor) {
+        mOverlayLayout = (ActionBarOverlayLayout) decor.findViewById(
+                com.android.internal.R.id.decor_content_parent);
+        if (mOverlayLayout != null) {
+            mOverlayLayout.setActionBarVisibilityCallback(this);
+        }
+        mDecorToolbar = getDecorToolbar(decor.findViewById(com.android.internal.R.id.action_bar));
+        mContextView = (ActionBarContextView) decor.findViewById(
+                com.android.internal.R.id.action_context_bar);
+        mContainerView = (ActionBarContainer) decor.findViewById(
+                com.android.internal.R.id.action_bar_container);
+        mSplitView = (ActionBarContainer) decor.findViewById(
+                com.android.internal.R.id.split_action_bar);
+
+        if (mDecorToolbar == null || mContextView == null || mContainerView == null) {
+            throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+                    "with a compatible window decor layout");
+        }
+
+        mContext = mDecorToolbar.getContext();
+        mContextDisplayMode = mDecorToolbar.isSplit() ?
+                CONTEXT_DISPLAY_SPLIT : CONTEXT_DISPLAY_NORMAL;
+
+        // This was initially read from the action bar style
+        final int current = mDecorToolbar.getDisplayOptions();
+        final boolean homeAsUp = (current & DISPLAY_HOME_AS_UP) != 0;
+        if (homeAsUp) {
+            mDisplayHomeAsUpSet = true;
+        }
+
+        ActionBarPolicy abp = ActionBarPolicy.get(mContext);
+        setHomeButtonEnabled(abp.enableHomeButtonByDefault() || homeAsUp);
+        setHasEmbeddedTabs(abp.hasEmbeddedTabs());
+
+        final TypedArray a = mContext.obtainStyledAttributes(null,
+                com.android.internal.R.styleable.ActionBar,
+                com.android.internal.R.attr.actionBarStyle, 0);
+        if (a.getBoolean(R.styleable.ActionBar_hideOnContentScroll, false)) {
+            setHideOnContentScrollEnabled(true);
+        }
+        final int elevation = a.getDimensionPixelSize(R.styleable.ActionBar_elevation, 0);
+        if (elevation != 0) {
+            setElevation(elevation);
+        }
+        a.recycle();
+    }
+
+    private DecorToolbar getDecorToolbar(View view) {
+        if (view instanceof DecorToolbar) {
+            return (DecorToolbar) view;
+        } else if (view instanceof Toolbar) {
+            return ((Toolbar) view).getWrapper();
+        } else {
+            throw new IllegalStateException("Can't make a decor toolbar out of " +
+                    view.getClass().getSimpleName());
+        }
+    }
+
+    @Override
+    public void setElevation(float elevation) {
+        mContainerView.setElevation(elevation);
+        if (mSplitView != null) {
+            mSplitView.setElevation(elevation);
+        }
+    }
+
+    @Override
+    public float getElevation() {
+        return mContainerView.getElevation();
+    }
+
+    public void onConfigurationChanged(Configuration newConfig) {
+        setHasEmbeddedTabs(ActionBarPolicy.get(mContext).hasEmbeddedTabs());
+    }
+
+    private void setHasEmbeddedTabs(boolean hasEmbeddedTabs) {
+        mHasEmbeddedTabs = hasEmbeddedTabs;
+        // Switch tab layout configuration if needed
+        if (!mHasEmbeddedTabs) {
+            mDecorToolbar.setEmbeddedTabView(null);
+            mContainerView.setTabContainer(mTabScrollView);
+        } else {
+            mContainerView.setTabContainer(null);
+            mDecorToolbar.setEmbeddedTabView(mTabScrollView);
+        }
+        final boolean isInTabMode = getNavigationMode() == NAVIGATION_MODE_TABS;
+        if (mTabScrollView != null) {
+            if (isInTabMode) {
+                mTabScrollView.setVisibility(View.VISIBLE);
+                if (mOverlayLayout != null) {
+                    mOverlayLayout.requestApplyInsets();
+                }
+            } else {
+                mTabScrollView.setVisibility(View.GONE);
+            }
+        }
+        mDecorToolbar.setCollapsible(!mHasEmbeddedTabs && isInTabMode);
+        mOverlayLayout.setHasNonEmbeddedTabs(!mHasEmbeddedTabs && isInTabMode);
+    }
+
+    private void ensureTabsExist() {
+        if (mTabScrollView != null) {
+            return;
+        }
+
+        ScrollingTabContainerView tabScroller = new ScrollingTabContainerView(mContext);
+
+        if (mHasEmbeddedTabs) {
+            tabScroller.setVisibility(View.VISIBLE);
+            mDecorToolbar.setEmbeddedTabView(tabScroller);
+        } else {
+            if (getNavigationMode() == NAVIGATION_MODE_TABS) {
+                tabScroller.setVisibility(View.VISIBLE);
+                if (mOverlayLayout != null) {
+                    mOverlayLayout.requestApplyInsets();
+                }
+            } else {
+                tabScroller.setVisibility(View.GONE);
+            }
+            mContainerView.setTabContainer(tabScroller);
+        }
+        mTabScrollView = tabScroller;
+    }
+
+    void completeDeferredDestroyActionMode() {
+        if (mDeferredModeDestroyCallback != null) {
+            mDeferredModeDestroyCallback.onDestroyActionMode(mDeferredDestroyActionMode);
+            mDeferredDestroyActionMode = null;
+            mDeferredModeDestroyCallback = null;
+        }
+    }
+
+    public void onWindowVisibilityChanged(int visibility) {
+        mCurWindowVisibility = visibility;
+    }
+
+    /**
+     * Enables or disables animation between show/hide states.
+     * If animation is disabled using this method, animations in progress
+     * will be finished.
+     *
+     * @param enabled true to animate, false to not animate.
+     */
+    public void setShowHideAnimationEnabled(boolean enabled) {
+        mShowHideAnimationEnabled = enabled;
+        if (!enabled && mCurrentShowAnim != null) {
+            mCurrentShowAnim.end();
+        }
+    }
+
+    public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        mMenuVisibilityListeners.add(listener);
+    }
+
+    public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        mMenuVisibilityListeners.remove(listener);
+    }
+
+    public void dispatchMenuVisibilityChanged(boolean isVisible) {
+        if (isVisible == mLastMenuVisibility) {
+            return;
+        }
+        mLastMenuVisibility = isVisible;
+
+        final int count = mMenuVisibilityListeners.size();
+        for (int i = 0; i < count; i++) {
+            mMenuVisibilityListeners.get(i).onMenuVisibilityChanged(isVisible);
+        }
+    }
+
+    @Override
+    public void setCustomView(int resId) {
+        setCustomView(LayoutInflater.from(getThemedContext()).inflate(resId,
+                mDecorToolbar.getViewGroup(), false));
+    }
+
+    @Override
+    public void setDisplayUseLogoEnabled(boolean useLogo) {
+        setDisplayOptions(useLogo ? DISPLAY_USE_LOGO : 0, DISPLAY_USE_LOGO);
+    }
+
+    @Override
+    public void setDisplayShowHomeEnabled(boolean showHome) {
+        setDisplayOptions(showHome ? DISPLAY_SHOW_HOME : 0, DISPLAY_SHOW_HOME);
+    }
+
+    @Override
+    public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
+        setDisplayOptions(showHomeAsUp ? DISPLAY_HOME_AS_UP : 0, DISPLAY_HOME_AS_UP);
+    }
+
+    @Override
+    public void setDisplayShowTitleEnabled(boolean showTitle) {
+        setDisplayOptions(showTitle ? DISPLAY_SHOW_TITLE : 0, DISPLAY_SHOW_TITLE);
+    }
+
+    @Override
+    public void setDisplayShowCustomEnabled(boolean showCustom) {
+        setDisplayOptions(showCustom ? DISPLAY_SHOW_CUSTOM : 0, DISPLAY_SHOW_CUSTOM);
+    }
+
+    @Override
+    public void setHomeButtonEnabled(boolean enable) {
+        mDecorToolbar.setHomeButtonEnabled(enable);
+    }
+
+    @Override
+    public void setTitle(int resId) {
+        setTitle(mContext.getString(resId));
+    }
+
+    @Override
+    public void setSubtitle(int resId) {
+        setSubtitle(mContext.getString(resId));
+    }
+
+    public void setSelectedNavigationItem(int position) {
+        switch (mDecorToolbar.getNavigationMode()) {
+        case NAVIGATION_MODE_TABS:
+            selectTab(mTabs.get(position));
+            break;
+        case NAVIGATION_MODE_LIST:
+            mDecorToolbar.setDropdownSelectedPosition(position);
+            break;
+        default:
+            throw new IllegalStateException(
+                    "setSelectedNavigationIndex not valid for current navigation mode");
+        }
+    }
+
+    public void removeAllTabs() {
+        cleanupTabs();
+    }
+
+    private void cleanupTabs() {
+        if (mSelectedTab != null) {
+            selectTab(null);
+        }
+        mTabs.clear();
+        if (mTabScrollView != null) {
+            mTabScrollView.removeAllTabs();
+        }
+        mSavedTabPosition = INVALID_POSITION;
+    }
+
+    public void setTitle(CharSequence title) {
+        mDecorToolbar.setTitle(title);
+    }
+
+    @Override
+    public void setWindowTitle(CharSequence title) {
+        mDecorToolbar.setWindowTitle(title);
+    }
+
+    public void setSubtitle(CharSequence subtitle) {
+        mDecorToolbar.setSubtitle(subtitle);
+    }
+
+    public void setDisplayOptions(int options) {
+        if ((options & DISPLAY_HOME_AS_UP) != 0) {
+            mDisplayHomeAsUpSet = true;
+        }
+        mDecorToolbar.setDisplayOptions(options);
+    }
+
+    public void setDisplayOptions(int options, int mask) {
+        final int current = mDecorToolbar.getDisplayOptions();
+        if ((mask & DISPLAY_HOME_AS_UP) != 0) {
+            mDisplayHomeAsUpSet = true;
+        }
+        mDecorToolbar.setDisplayOptions((options & mask) | (current & ~mask));
+    }
+
+    public void setBackgroundDrawable(Drawable d) {
+        mContainerView.setPrimaryBackground(d);
+    }
+
+    public void setStackedBackgroundDrawable(Drawable d) {
+        mContainerView.setStackedBackground(d);
+    }
+
+    public void setSplitBackgroundDrawable(Drawable d) {
+        if (mSplitView != null) {
+            mSplitView.setSplitBackground(d);
+        }
+    }
+
+    public View getCustomView() {
+        return mDecorToolbar.getCustomView();
+    }
+
+    public CharSequence getTitle() {
+        return mDecorToolbar.getTitle();
+    }
+
+    public CharSequence getSubtitle() {
+        return mDecorToolbar.getSubtitle();
+    }
+
+    public int getNavigationMode() {
+        return mDecorToolbar.getNavigationMode();
+    }
+
+    public int getDisplayOptions() {
+        return mDecorToolbar.getDisplayOptions();
+    }
+
+    public ActionMode startActionMode(ActionMode.Callback callback) {
+        if (mActionMode != null) {
+            mActionMode.finish();
+        }
+
+        mOverlayLayout.setHideOnContentScrollEnabled(false);
+        mContextView.killMode();
+        ActionModeImpl mode = new ActionModeImpl(mContextView.getContext(), callback);
+        if (mode.dispatchOnCreate()) {
+            // This needs to be set before invalidate() so that it calls
+            // onPrepareActionMode()
+            mActionMode = mode;
+            mode.invalidate();
+            mContextView.initForMode(mode);
+            animateToMode(true);
+            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
+                // TODO animate this
+                if (mSplitView.getVisibility() != View.VISIBLE) {
+                    mSplitView.setVisibility(View.VISIBLE);
+                    if (mOverlayLayout != null) {
+                        mOverlayLayout.requestApplyInsets();
+                    }
+                }
+            }
+            mContextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+            return mode;
+        }
+        return null;
+    }
+
+    private void configureTab(Tab tab, int position) {
+        final TabImpl tabi = (TabImpl) tab;
+        final ActionBar.TabListener callback = tabi.getCallback();
+
+        if (callback == null) {
+            throw new IllegalStateException("Action Bar Tab must have a Callback");
+        }
+
+        tabi.setPosition(position);
+        mTabs.add(position, tabi);
+
+        final int count = mTabs.size();
+        for (int i = position + 1; i < count; i++) {
+            mTabs.get(i).setPosition(i);
+        }
+    }
+
+    @Override
+    public void addTab(Tab tab) {
+        addTab(tab, mTabs.isEmpty());
+    }
+
+    @Override
+    public void addTab(Tab tab, int position) {
+        addTab(tab, position, mTabs.isEmpty());
+    }
+
+    @Override
+    public void addTab(Tab tab, boolean setSelected) {
+        ensureTabsExist();
+        mTabScrollView.addTab(tab, setSelected);
+        configureTab(tab, mTabs.size());
+        if (setSelected) {
+            selectTab(tab);
+        }
+    }
+
+    @Override
+    public void addTab(Tab tab, int position, boolean setSelected) {
+        ensureTabsExist();
+        mTabScrollView.addTab(tab, position, setSelected);
+        configureTab(tab, position);
+        if (setSelected) {
+            selectTab(tab);
+        }
+    }
+
+    @Override
+    public Tab newTab() {
+        return new TabImpl();
+    }
+
+    @Override
+    public void removeTab(Tab tab) {
+        removeTabAt(tab.getPosition());
+    }
+
+    @Override
+    public void removeTabAt(int position) {
+        if (mTabScrollView == null) {
+            // No tabs around to remove
+            return;
+        }
+
+        int selectedTabPosition = mSelectedTab != null
+                ? mSelectedTab.getPosition() : mSavedTabPosition;
+        mTabScrollView.removeTabAt(position);
+        TabImpl removedTab = mTabs.remove(position);
+        if (removedTab != null) {
+            removedTab.setPosition(-1);
+        }
+
+        final int newTabCount = mTabs.size();
+        for (int i = position; i < newTabCount; i++) {
+            mTabs.get(i).setPosition(i);
+        }
+
+        if (selectedTabPosition == position) {
+            selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
+        }
+    }
+
+    @Override
+    public void selectTab(Tab tab) {
+        if (getNavigationMode() != NAVIGATION_MODE_TABS) {
+            mSavedTabPosition = tab != null ? tab.getPosition() : INVALID_POSITION;
+            return;
+        }
+
+        final FragmentTransaction trans = mDecorToolbar.getViewGroup().isInEditMode() ? null :
+                mActivity.getFragmentManager().beginTransaction().disallowAddToBackStack();
+
+        if (mSelectedTab == tab) {
+            if (mSelectedTab != null) {
+                mSelectedTab.getCallback().onTabReselected(mSelectedTab, trans);
+                mTabScrollView.animateToTab(tab.getPosition());
+            }
+        } else {
+            mTabScrollView.setTabSelected(tab != null ? tab.getPosition() : Tab.INVALID_POSITION);
+            if (mSelectedTab != null) {
+                mSelectedTab.getCallback().onTabUnselected(mSelectedTab, trans);
+            }
+            mSelectedTab = (TabImpl) tab;
+            if (mSelectedTab != null) {
+                mSelectedTab.getCallback().onTabSelected(mSelectedTab, trans);
+            }
+        }
+
+        if (trans != null && !trans.isEmpty()) {
+            trans.commit();
+        }
+    }
+
+    @Override
+    public Tab getSelectedTab() {
+        return mSelectedTab;
+    }
+
+    @Override
+    public int getHeight() {
+        return mContainerView.getHeight();
+    }
+
+    public void enableContentAnimations(boolean enabled) {
+        mContentAnimations = enabled;
+    }
+
+    @Override
+    public void show() {
+        if (mHiddenByApp) {
+            mHiddenByApp = false;
+            updateVisibility(false);
+        }
+    }
+
+    private void showForActionMode() {
+        if (!mShowingForMode) {
+            mShowingForMode = true;
+            if (mOverlayLayout != null) {
+                mOverlayLayout.setShowingForActionMode(true);
+            }
+            updateVisibility(false);
+        }
+    }
+
+    public void showForSystem() {
+        if (mHiddenBySystem) {
+            mHiddenBySystem = false;
+            updateVisibility(true);
+        }
+    }
+
+    @Override
+    public void hide() {
+        if (!mHiddenByApp) {
+            mHiddenByApp = true;
+            updateVisibility(false);
+        }
+    }
+
+    private void hideForActionMode() {
+        if (mShowingForMode) {
+            mShowingForMode = false;
+            if (mOverlayLayout != null) {
+                mOverlayLayout.setShowingForActionMode(false);
+            }
+            updateVisibility(false);
+        }
+    }
+
+    public void hideForSystem() {
+        if (!mHiddenBySystem) {
+            mHiddenBySystem = true;
+            updateVisibility(true);
+        }
+    }
+
+    @Override
+    public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) {
+        if (hideOnContentScroll && !mOverlayLayout.isInOverlayMode()) {
+            throw new IllegalStateException("Action bar must be in overlay mode " +
+                    "(Window.FEATURE_OVERLAY_ACTION_BAR) to enable hide on content scroll");
+        }
+        mHideOnContentScroll = hideOnContentScroll;
+        mOverlayLayout.setHideOnContentScrollEnabled(hideOnContentScroll);
+    }
+
+    @Override
+    public boolean isHideOnContentScrollEnabled() {
+        return mOverlayLayout.isHideOnContentScrollEnabled();
+    }
+
+    @Override
+    public int getHideOffset() {
+        return mOverlayLayout.getActionBarHideOffset();
+    }
+
+    @Override
+    public void setHideOffset(int offset) {
+        if (offset != 0 && !mOverlayLayout.isInOverlayMode()) {
+            throw new IllegalStateException("Action bar must be in overlay mode " +
+                    "(Window.FEATURE_OVERLAY_ACTION_BAR) to set a non-zero hide offset");
+        }
+        mOverlayLayout.setActionBarHideOffset(offset);
+    }
+
+    private static boolean checkShowingFlags(boolean hiddenByApp, boolean hiddenBySystem,
+            boolean showingForMode) {
+        if (showingForMode) {
+            return true;
+        } else if (hiddenByApp || hiddenBySystem) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void updateVisibility(boolean fromSystem) {
+        // Based on the current state, should we be hidden or shown?
+        final boolean shown = checkShowingFlags(mHiddenByApp, mHiddenBySystem,
+                mShowingForMode);
+
+        if (shown) {
+            if (!mNowShowing) {
+                mNowShowing = true;
+                doShow(fromSystem);
+            }
+        } else {
+            if (mNowShowing) {
+                mNowShowing = false;
+                doHide(fromSystem);
+            }
+        }
+    }
+
+    public void doShow(boolean fromSystem) {
+        if (mCurrentShowAnim != null) {
+            mCurrentShowAnim.end();
+        }
+        mContainerView.setVisibility(View.VISIBLE);
+
+        if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled
+                || fromSystem)) {
+            mContainerView.setTranslationY(0); // because we're about to ask its window loc
+            float startingY = -mContainerView.getHeight();
+            if (fromSystem) {
+                int topLeft[] = {0, 0};
+                mContainerView.getLocationInWindow(topLeft);
+                startingY -= topLeft[1];
+            }
+            mContainerView.setTranslationY(startingY);
+            AnimatorSet anim = new AnimatorSet();
+            ObjectAnimator a = ObjectAnimator.ofFloat(mContainerView, View.TRANSLATION_Y, 0);
+            a.addUpdateListener(mUpdateListener);
+            AnimatorSet.Builder b = anim.play(a);
+            if (mContentAnimations && mContentView != null) {
+                b.with(ObjectAnimator.ofFloat(mContentView, View.TRANSLATION_Y,
+                        startingY, 0));
+            }
+            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
+                mSplitView.setTranslationY(mSplitView.getHeight());
+                mSplitView.setVisibility(View.VISIBLE);
+                b.with(ObjectAnimator.ofFloat(mSplitView, View.TRANSLATION_Y, 0));
+            }
+            anim.setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                    com.android.internal.R.interpolator.decelerate_cubic));
+            anim.setDuration(250);
+            // If this is being shown from the system, add a small delay.
+            // This is because we will also be animating in the status bar,
+            // and these two elements can't be done in lock-step.  So we give
+            // a little time for the status bar to start its animation before
+            // the action bar animates.  (This corresponds to the corresponding
+            // case when hiding, where the status bar has a small delay before
+            // starting.)
+            anim.addListener(mShowListener);
+            mCurrentShowAnim = anim;
+            anim.start();
+        } else {
+            mContainerView.setAlpha(1);
+            mContainerView.setTranslationY(0);
+            if (mContentAnimations && mContentView != null) {
+                mContentView.setTranslationY(0);
+            }
+            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
+                mSplitView.setAlpha(1);
+                mSplitView.setTranslationY(0);
+                mSplitView.setVisibility(View.VISIBLE);
+            }
+            mShowListener.onAnimationEnd(null);
+        }
+        if (mOverlayLayout != null) {
+            mOverlayLayout.requestApplyInsets();
+        }
+    }
+
+    public void doHide(boolean fromSystem) {
+        if (mCurrentShowAnim != null) {
+            mCurrentShowAnim.end();
+        }
+
+        if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled
+                || fromSystem)) {
+            mContainerView.setAlpha(1);
+            mContainerView.setTransitioning(true);
+            AnimatorSet anim = new AnimatorSet();
+            float endingY = -mContainerView.getHeight();
+            if (fromSystem) {
+                int topLeft[] = {0, 0};
+                mContainerView.getLocationInWindow(topLeft);
+                endingY -= topLeft[1];
+            }
+            ObjectAnimator a = ObjectAnimator.ofFloat(mContainerView, View.TRANSLATION_Y, endingY);
+            a.addUpdateListener(mUpdateListener);
+            AnimatorSet.Builder b = anim.play(a);
+            if (mContentAnimations && mContentView != null) {
+                b.with(ObjectAnimator.ofFloat(mContentView, View.TRANSLATION_Y,
+                        0, endingY));
+            }
+            if (mSplitView != null && mSplitView.getVisibility() == View.VISIBLE) {
+                mSplitView.setAlpha(1);
+                b.with(ObjectAnimator.ofFloat(mSplitView, View.TRANSLATION_Y,
+                        mSplitView.getHeight()));
+            }
+            anim.setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                    com.android.internal.R.interpolator.accelerate_cubic));
+            anim.setDuration(250);
+            anim.addListener(mHideListener);
+            mCurrentShowAnim = anim;
+            anim.start();
+        } else {
+            mHideListener.onAnimationEnd(null);
+        }
+    }
+
+    public boolean isShowing() {
+        final int height = getHeight();
+        // Take into account the case where the bar has a 0 height due to not being measured yet.
+        return mNowShowing && (height == 0 || getHideOffset() < height);
+    }
+
+    void animateToMode(boolean toActionMode) {
+        if (toActionMode) {
+            showForActionMode();
+        } else {
+            hideForActionMode();
+        }
+
+        if (shouldAnimateContextView()) {
+            Animator fadeIn, fadeOut;
+            if (toActionMode) {
+                fadeOut = mDecorToolbar.setupAnimatorToVisibility(View.GONE,
+                        FADE_OUT_DURATION_MS);
+                fadeIn = mContextView.setupAnimatorToVisibility(View.VISIBLE,
+                        FADE_IN_DURATION_MS);
+            } else {
+                fadeIn = mDecorToolbar.setupAnimatorToVisibility(View.VISIBLE,
+                        FADE_IN_DURATION_MS);
+                fadeOut = mContextView.setupAnimatorToVisibility(View.GONE,
+                        FADE_OUT_DURATION_MS);
+            }
+            AnimatorSet set = new AnimatorSet();
+            set.playSequentially(fadeOut, fadeIn);
+            set.start();
+        } else {
+            if (toActionMode) {
+                mDecorToolbar.setVisibility(View.GONE);
+                mContextView.setVisibility(View.VISIBLE);
+            } else {
+                mDecorToolbar.setVisibility(View.VISIBLE);
+                mContextView.setVisibility(View.GONE);
+            }
+        }
+        // mTabScrollView's visibility is not affected by action mode.
+    }
+
+    private boolean shouldAnimateContextView() {
+        // We only to animate the action mode in if the container view has already been laid out.
+        // If it hasn't been laid out, it hasn't been drawn to screen yet.
+        return mContainerView.isLaidOut();
+    }
+
+    public Context getThemedContext() {
+        if (mThemedContext == null) {
+            TypedValue outValue = new TypedValue();
+            Resources.Theme currentTheme = mContext.getTheme();
+            currentTheme.resolveAttribute(com.android.internal.R.attr.actionBarWidgetTheme,
+                    outValue, true);
+            final int targetThemeRes = outValue.resourceId;
+
+            if (targetThemeRes != 0 && mContext.getThemeResId() != targetThemeRes) {
+                mThemedContext = new ContextThemeWrapper(mContext, targetThemeRes);
+            } else {
+                mThemedContext = mContext;
+            }
+        }
+        return mThemedContext;
+    }
+
+    @Override
+    public boolean isTitleTruncated() {
+        return mDecorToolbar != null && mDecorToolbar.isTitleTruncated();
+    }
+
+    @Override
+    public void setHomeAsUpIndicator(Drawable indicator) {
+        mDecorToolbar.setNavigationIcon(indicator);
+    }
+
+    @Override
+    public void setHomeAsUpIndicator(int resId) {
+        mDecorToolbar.setNavigationIcon(resId);
+    }
+
+    @Override
+    public void setHomeActionContentDescription(CharSequence description) {
+        mDecorToolbar.setNavigationContentDescription(description);
+    }
+
+    @Override
+    public void setHomeActionContentDescription(int resId) {
+        mDecorToolbar.setNavigationContentDescription(resId);
+    }
+
+    @Override
+    public void onContentScrollStarted() {
+        if (mCurrentShowAnim != null) {
+            mCurrentShowAnim.cancel();
+            mCurrentShowAnim = null;
+        }
+    }
+
+    @Override
+    public void onContentScrollStopped() {
+    }
+
+    @Override
+    public boolean collapseActionView() {
+        if (mDecorToolbar != null && mDecorToolbar.hasExpandedActionView()) {
+            mDecorToolbar.collapseActionView();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    public class ActionModeImpl extends ActionMode implements MenuBuilder.Callback {
+        private final Context mActionModeContext;
+        private final MenuBuilder mMenu;
+
+        private ActionMode.Callback mCallback;
+        private WeakReference<View> mCustomView;
+
+        public ActionModeImpl(
+                Context context, ActionMode.Callback callback) {
+            mActionModeContext = context;
+            mCallback = callback;
+            mMenu = new MenuBuilder(context)
+                        .setDefaultShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            mMenu.setCallback(this);
+        }
+
+        @Override
+        public MenuInflater getMenuInflater() {
+            return new MenuInflater(mActionModeContext);
+        }
+
+        @Override
+        public Menu getMenu() {
+            return mMenu;
+        }
+
+        @Override
+        public void finish() {
+            if (mActionMode != this) {
+                // Not the active action mode - no-op
+                return;
+            }
+
+            // If this change in state is going to cause the action bar
+            // to be hidden, defer the onDestroy callback until the animation
+            // is finished and associated relayout is about to happen. This lets
+            // apps better anticipate visibility and layout behavior.
+            if (!checkShowingFlags(mHiddenByApp, mHiddenBySystem, false)) {
+                // With the current state but the action bar hidden, our
+                // overall showing state is going to be false.
+                mDeferredDestroyActionMode = this;
+                mDeferredModeDestroyCallback = mCallback;
+            } else {
+                mCallback.onDestroyActionMode(this);
+            }
+            mCallback = null;
+            animateToMode(false);
+
+            // Clear out the context mode views after the animation finishes
+            mContextView.closeMode();
+            mDecorToolbar.getViewGroup().sendAccessibilityEvent(
+                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+            mOverlayLayout.setHideOnContentScrollEnabled(mHideOnContentScroll);
+
+            mActionMode = null;
+        }
+
+        @Override
+        public void invalidate() {
+            if (mActionMode != this) {
+                // Not the active action mode - no-op. It's possible we are
+                // currently deferring onDestroy, so the app doesn't yet know we
+                // are going away and is trying to use us. That's also a no-op.
+                return;
+            }
+
+            mMenu.stopDispatchingItemsChanged();
+            try {
+                mCallback.onPrepareActionMode(this, mMenu);
+            } finally {
+                mMenu.startDispatchingItemsChanged();
+            }
+        }
+
+        public boolean dispatchOnCreate() {
+            mMenu.stopDispatchingItemsChanged();
+            try {
+                return mCallback.onCreateActionMode(this, mMenu);
+            } finally {
+                mMenu.startDispatchingItemsChanged();
+            }
+        }
+
+        @Override
+        public void setCustomView(View view) {
+            mContextView.setCustomView(view);
+            mCustomView = new WeakReference<View>(view);
+        }
+
+        @Override
+        public void setSubtitle(CharSequence subtitle) {
+            mContextView.setSubtitle(subtitle);
+        }
+
+        @Override
+        public void setTitle(CharSequence title) {
+            mContextView.setTitle(title);
+        }
+
+        @Override
+        public void setTitle(int resId) {
+            setTitle(mContext.getResources().getString(resId));
+        }
+
+        @Override
+        public void setSubtitle(int resId) {
+            setSubtitle(mContext.getResources().getString(resId));
+        }
+
+        @Override
+        public CharSequence getTitle() {
+            return mContextView.getTitle();
+        }
+
+        @Override
+        public CharSequence getSubtitle() {
+            return mContextView.getSubtitle();
+        }
+
+        @Override
+        public void setTitleOptionalHint(boolean titleOptional) {
+            super.setTitleOptionalHint(titleOptional);
+            mContextView.setTitleOptional(titleOptional);
+        }
+
+        @Override
+        public boolean isTitleOptional() {
+            return mContextView.isTitleOptional();
+        }
+
+        @Override
+        public View getCustomView() {
+            return mCustomView != null ? mCustomView.get() : null;
+        }
+
+        public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+            if (mCallback != null) {
+                return mCallback.onActionItemClicked(this, item);
+            } else {
+                return false;
+            }
+        }
+
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        }
+
+        public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+            if (mCallback == null) {
+                return false;
+            }
+
+            if (!subMenu.hasVisibleItems()) {
+                return true;
+            }
+
+            new MenuPopupHelper(getThemedContext(), subMenu).show();
+            return true;
+        }
+
+        public void onCloseSubMenu(SubMenuBuilder menu) {
+        }
+
+        public void onMenuModeChange(MenuBuilder menu) {
+            if (mCallback == null) {
+                return;
+            }
+            invalidate();
+            mContextView.showOverflowMenu();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public class TabImpl extends ActionBar.Tab {
+        private ActionBar.TabListener mCallback;
+        private Object mTag;
+        private Drawable mIcon;
+        private CharSequence mText;
+        private CharSequence mContentDesc;
+        private int mPosition = -1;
+        private View mCustomView;
+
+        @Override
+        public Object getTag() {
+            return mTag;
+        }
+
+        @Override
+        public Tab setTag(Object tag) {
+            mTag = tag;
+            return this;
+        }
+
+        public ActionBar.TabListener getCallback() {
+            return mCallback;
+        }
+
+        @Override
+        public Tab setTabListener(ActionBar.TabListener callback) {
+            mCallback = callback;
+            return this;
+        }
+
+        @Override
+        public View getCustomView() {
+            return mCustomView;
+        }
+
+        @Override
+        public Tab setCustomView(View view) {
+            mCustomView = view;
+            if (mPosition >= 0) {
+                mTabScrollView.updateTab(mPosition);
+            }
+            return this;
+        }
+
+        @Override
+        public Tab setCustomView(int layoutResId) {
+            return setCustomView(LayoutInflater.from(getThemedContext())
+                    .inflate(layoutResId, null));
+        }
+
+        @Override
+        public Drawable getIcon() {
+            return mIcon;
+        }
+
+        @Override
+        public int getPosition() {
+            return mPosition;
+        }
+
+        public void setPosition(int position) {
+            mPosition = position;
+        }
+
+        @Override
+        public CharSequence getText() {
+            return mText;
+        }
+
+        @Override
+        public Tab setIcon(Drawable icon) {
+            mIcon = icon;
+            if (mPosition >= 0) {
+                mTabScrollView.updateTab(mPosition);
+            }
+            return this;
+        }
+
+        @Override
+        public Tab setIcon(int resId) {
+            return setIcon(mContext.getDrawable(resId));
+        }
+
+        @Override
+        public Tab setText(CharSequence text) {
+            mText = text;
+            if (mPosition >= 0) {
+                mTabScrollView.updateTab(mPosition);
+            }
+            return this;
+        }
+
+        @Override
+        public Tab setText(int resId) {
+            return setText(mContext.getResources().getText(resId));
+        }
+
+        @Override
+        public void select() {
+            selectTab(this);
+        }
+
+        @Override
+        public Tab setContentDescription(int resId) {
+            return setContentDescription(mContext.getResources().getText(resId));
+        }
+
+        @Override
+        public Tab setContentDescription(CharSequence contentDesc) {
+            mContentDesc = contentDesc;
+            if (mPosition >= 0) {
+                mTabScrollView.updateTab(mPosition);
+            }
+            return this;
+        }
+
+        @Override
+        public CharSequence getContentDescription() {
+            return mContentDesc;
+        }
+    }
+
+    @Override
+    public void setCustomView(View view) {
+        mDecorToolbar.setCustomView(view);
+    }
+
+    @Override
+    public void setCustomView(View view, LayoutParams layoutParams) {
+        view.setLayoutParams(layoutParams);
+        mDecorToolbar.setCustomView(view);
+    }
+
+    @Override
+    public void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback) {
+        mDecorToolbar.setDropdownParams(adapter, new NavItemSelectedListener(callback));
+    }
+
+    @Override
+    public int getSelectedNavigationIndex() {
+        switch (mDecorToolbar.getNavigationMode()) {
+            case NAVIGATION_MODE_TABS:
+                return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
+            case NAVIGATION_MODE_LIST:
+                return mDecorToolbar.getDropdownSelectedPosition();
+            default:
+                return -1;
+        }
+    }
+
+    @Override
+    public int getNavigationItemCount() {
+        switch (mDecorToolbar.getNavigationMode()) {
+            case NAVIGATION_MODE_TABS:
+                return mTabs.size();
+            case NAVIGATION_MODE_LIST:
+                return mDecorToolbar.getDropdownItemCount();
+            default:
+                return 0;
+        }
+    }
+
+    @Override
+    public int getTabCount() {
+        return mTabs.size();
+    }
+
+    @Override
+    public void setNavigationMode(int mode) {
+        final int oldMode = mDecorToolbar.getNavigationMode();
+        switch (oldMode) {
+            case NAVIGATION_MODE_TABS:
+                mSavedTabPosition = getSelectedNavigationIndex();
+                selectTab(null);
+                mTabScrollView.setVisibility(View.GONE);
+                break;
+        }
+        if (oldMode != mode && !mHasEmbeddedTabs) {
+            if (mOverlayLayout != null) {
+                mOverlayLayout.requestFitSystemWindows();
+            }
+        }
+        mDecorToolbar.setNavigationMode(mode);
+        switch (mode) {
+            case NAVIGATION_MODE_TABS:
+                ensureTabsExist();
+                mTabScrollView.setVisibility(View.VISIBLE);
+                if (mSavedTabPosition != INVALID_POSITION) {
+                    setSelectedNavigationItem(mSavedTabPosition);
+                    mSavedTabPosition = INVALID_POSITION;
+                }
+                break;
+        }
+        mDecorToolbar.setCollapsible(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs);
+        mOverlayLayout.setHasNonEmbeddedTabs(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs);
+    }
+
+    @Override
+    public Tab getTabAt(int index) {
+        return mTabs.get(index);
+    }
+
+
+    @Override
+    public void setIcon(int resId) {
+        mDecorToolbar.setIcon(resId);
+    }
+
+    @Override
+    public void setIcon(Drawable icon) {
+        mDecorToolbar.setIcon(icon);
+    }
+
+    public boolean hasIcon() {
+        return mDecorToolbar.hasIcon();
+    }
+
+    @Override
+    public void setLogo(int resId) {
+        mDecorToolbar.setLogo(resId);
+    }
+
+    @Override
+    public void setLogo(Drawable logo) {
+        mDecorToolbar.setLogo(logo);
+    }
+
+    public boolean hasLogo() {
+        return mDecorToolbar.hasLogo();
+    }
+
+    public void setDefaultDisplayHomeAsUpEnabled(boolean enable) {
+        if (!mDisplayHomeAsUpSet) {
+            setDisplayHomeAsUpEnabled(enable);
+        }
+    }
+
+}
diff --git a/com/android/internal/app/procstats/DumpUtils.java b/com/android/internal/app/procstats/DumpUtils.java
new file mode 100644
index 0000000..ebedc89
--- /dev/null
+++ b/com/android/internal/app/procstats/DumpUtils.java
@@ -0,0 +1,369 @@
+/*
+ * 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.internal.app.procstats;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import static com.android.internal.app.procstats.ProcessStats.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * Utilities for dumping.
+ */
+public final class DumpUtils {
+    public static final String[] STATE_NAMES = new String[] {
+            "Persist", "Top    ", "ImpFg  ", "ImpBg  ",
+            "Backup ", "HeavyWt", "Service", "ServRst",
+            "Receivr", "Home   ",
+            "LastAct", "CchAct ", "CchCAct", "CchEmty"
+    };
+
+    public static final String[] ADJ_SCREEN_NAMES_CSV = new String[] {
+            "off", "on"
+    };
+
+    public static final String[] ADJ_MEM_NAMES_CSV = new String[] {
+            "norm", "mod",  "low", "crit"
+    };
+
+    public static final String[] STATE_NAMES_CSV = new String[] {
+            "pers", "top", "impfg", "impbg", "backup", "heavy",
+            "service", "service-rs", "receiver", "home", "lastact",
+            "cch-activity", "cch-aclient", "cch-empty"
+    };
+
+    static final String[] ADJ_SCREEN_TAGS = new String[] {
+            "0", "1"
+    };
+
+    static final String[] ADJ_MEM_TAGS = new String[] {
+            "n", "m",  "l", "c"
+    };
+
+    static final String[] STATE_TAGS = new String[] {
+            "p", "t", "f", "b", "u", "w",
+            "s", "x", "r", "h", "l", "a", "c", "e"
+    };
+
+    static final String CSV_SEP = "\t";
+
+    /**
+     * No instantiate
+     */
+    private DumpUtils() {
+    }
+
+    public static void printScreenLabel(PrintWriter pw, int offset) {
+        switch (offset) {
+            case ADJ_NOTHING:
+                pw.print("     ");
+                break;
+            case ADJ_SCREEN_OFF:
+                pw.print("SOff/");
+                break;
+            case ADJ_SCREEN_ON:
+                pw.print("SOn /");
+                break;
+            default:
+                pw.print("????/");
+                break;
+        }
+    }
+
+    public static void printScreenLabelCsv(PrintWriter pw, int offset) {
+        switch (offset) {
+            case ADJ_NOTHING:
+                break;
+            case ADJ_SCREEN_OFF:
+                pw.print(ADJ_SCREEN_NAMES_CSV[0]);
+                break;
+            case ADJ_SCREEN_ON:
+                pw.print(ADJ_SCREEN_NAMES_CSV[1]);
+                break;
+            default:
+                pw.print("???");
+                break;
+        }
+    }
+
+    public static void printMemLabel(PrintWriter pw, int offset, char sep) {
+        switch (offset) {
+            case ADJ_NOTHING:
+                pw.print("    ");
+                if (sep != 0) pw.print(' ');
+                break;
+            case ADJ_MEM_FACTOR_NORMAL:
+                pw.print("Norm");
+                if (sep != 0) pw.print(sep);
+                break;
+            case ADJ_MEM_FACTOR_MODERATE:
+                pw.print("Mod ");
+                if (sep != 0) pw.print(sep);
+                break;
+            case ADJ_MEM_FACTOR_LOW:
+                pw.print("Low ");
+                if (sep != 0) pw.print(sep);
+                break;
+            case ADJ_MEM_FACTOR_CRITICAL:
+                pw.print("Crit");
+                if (sep != 0) pw.print(sep);
+                break;
+            default:
+                pw.print("????");
+                if (sep != 0) pw.print(sep);
+                break;
+        }
+    }
+
+    public static void printMemLabelCsv(PrintWriter pw, int offset) {
+        if (offset >= ADJ_MEM_FACTOR_NORMAL) {
+            if (offset <= ADJ_MEM_FACTOR_CRITICAL) {
+                pw.print(ADJ_MEM_NAMES_CSV[offset]);
+            } else {
+                pw.print("???");
+            }
+        }
+    }
+
+    public static void printPercent(PrintWriter pw, double fraction) {
+        fraction *= 100;
+        if (fraction < 1) {
+            pw.print(String.format("%.2f", fraction));
+        } else if (fraction < 10) {
+            pw.print(String.format("%.1f", fraction));
+        } else {
+            pw.print(String.format("%.0f", fraction));
+        }
+        pw.print("%");
+    }
+
+    public static void printProcStateTag(PrintWriter pw, int state) {
+        state = printArrayEntry(pw, ADJ_SCREEN_TAGS,  state, ADJ_SCREEN_MOD*STATE_COUNT);
+        state = printArrayEntry(pw, ADJ_MEM_TAGS,  state, STATE_COUNT);
+        printArrayEntry(pw, STATE_TAGS,  state, 1);
+    }
+
+    public static void printAdjTag(PrintWriter pw, int state) {
+        state = printArrayEntry(pw, ADJ_SCREEN_TAGS,  state, ADJ_SCREEN_MOD);
+        printArrayEntry(pw, ADJ_MEM_TAGS, state, 1);
+    }
+
+    public static void printProcStateTagAndValue(PrintWriter pw, int state, long value) {
+        pw.print(',');
+        printProcStateTag(pw, state);
+        pw.print(':');
+        pw.print(value);
+    }
+
+    public static void printAdjTagAndValue(PrintWriter pw, int state, long value) {
+        pw.print(',');
+        printAdjTag(pw, state);
+        pw.print(':');
+        pw.print(value);
+    }
+
+    public static long dumpSingleTime(PrintWriter pw, String prefix, long[] durations,
+            int curState, long curStartTime, long now) {
+        long totalTime = 0;
+        int printedScreen = -1;
+        for (int iscreen=0; iscreen<ADJ_COUNT; iscreen+=ADJ_SCREEN_MOD) {
+            int printedMem = -1;
+            for (int imem=0; imem<ADJ_MEM_FACTOR_COUNT; imem++) {
+                int state = imem+iscreen;
+                long time = durations[state];
+                String running = "";
+                if (curState == state) {
+                    time += now - curStartTime;
+                    if (pw != null) {
+                        running = " (running)";
+                    }
+                }
+                if (time != 0) {
+                    if (pw != null) {
+                        pw.print(prefix);
+                        printScreenLabel(pw, printedScreen != iscreen
+                                ? iscreen : STATE_NOTHING);
+                        printedScreen = iscreen;
+                        printMemLabel(pw, printedMem != imem ? imem : STATE_NOTHING, (char)0);
+                        printedMem = imem;
+                        pw.print(": ");
+                        TimeUtils.formatDuration(time, pw); pw.println(running);
+                    }
+                    totalTime += time;
+                }
+            }
+        }
+        if (totalTime != 0 && pw != null) {
+            pw.print(prefix);
+            pw.print("    TOTAL: ");
+            TimeUtils.formatDuration(totalTime, pw);
+            pw.println();
+        }
+        return totalTime;
+    }
+
+    public static void dumpAdjTimesCheckin(PrintWriter pw, String sep, long[] durations,
+            int curState, long curStartTime, long now) {
+        for (int iscreen=0; iscreen<ADJ_COUNT; iscreen+=ADJ_SCREEN_MOD) {
+            for (int imem=0; imem<ADJ_MEM_FACTOR_COUNT; imem++) {
+                int state = imem+iscreen;
+                long time = durations[state];
+                if (curState == state) {
+                    time += now - curStartTime;
+                }
+                if (time != 0) {
+                    printAdjTagAndValue(pw, state, time);
+                }
+            }
+        }
+    }
+
+    private static void dumpStateHeadersCsv(PrintWriter pw, String sep, int[] screenStates,
+            int[] memStates, int[] procStates) {
+        final int NS = screenStates != null ? screenStates.length : 1;
+        final int NM = memStates != null ? memStates.length : 1;
+        final int NP = procStates != null ? procStates.length : 1;
+        for (int is=0; is<NS; is++) {
+            for (int im=0; im<NM; im++) {
+                for (int ip=0; ip<NP; ip++) {
+                    pw.print(sep);
+                    boolean printed = false;
+                    if (screenStates != null && screenStates.length > 1) {
+                        printScreenLabelCsv(pw, screenStates[is]);
+                        printed = true;
+                    }
+                    if (memStates != null && memStates.length > 1) {
+                        if (printed) {
+                            pw.print("-");
+                        }
+                        printMemLabelCsv(pw, memStates[im]);
+                        printed = true;
+                    }
+                    if (procStates != null && procStates.length > 1) {
+                        if (printed) {
+                            pw.print("-");
+                        }
+                        pw.print(STATE_NAMES_CSV[procStates[ip]]);
+                    }
+                }
+            }
+        }
+    }
+
+    /*
+     * Doesn't seem to be used.
+     *
+    public static void dumpProcessList(PrintWriter pw, String prefix, ArrayList<ProcessState> procs,
+            int[] screenStates, int[] memStates, int[] procStates, long now) {
+        String innerPrefix = prefix + "  ";
+        for (int i=procs.size()-1; i>=0; i--) {
+            ProcessState proc = procs.get(i);
+            pw.print(prefix);
+            pw.print(proc.mName);
+            pw.print(" / ");
+            UserHandle.formatUid(pw, proc.mUid);
+            pw.print(" (");
+            pw.print(proc.durations.getKeyCount());
+            pw.print(" entries)");
+            pw.println(":");
+            proc.dumpProcessState(pw, innerPrefix, screenStates, memStates, procStates, now);
+            if (proc.pssTable.getKeyCount() > 0) {
+                proc.dumpPss(pw, innerPrefix, screenStates, memStates, procStates);
+            }
+        }
+    }
+    */
+
+    public static void dumpProcessSummaryLocked(PrintWriter pw, String prefix,
+            ArrayList<ProcessState> procs, int[] screenStates, int[] memStates, int[] procStates,
+            long now, long totalTime) {
+        for (int i=procs.size()-1; i>=0; i--) {
+            final ProcessState proc = procs.get(i);
+            proc.dumpSummary(pw, prefix, screenStates, memStates, procStates, now, totalTime);
+        }
+    }
+
+    public static void dumpProcessListCsv(PrintWriter pw, ArrayList<ProcessState> procs,
+            boolean sepScreenStates, int[] screenStates, boolean sepMemStates, int[] memStates,
+            boolean sepProcStates, int[] procStates, long now) {
+        pw.print("process");
+        pw.print(CSV_SEP);
+        pw.print("uid");
+        pw.print(CSV_SEP);
+        pw.print("vers");
+        dumpStateHeadersCsv(pw, CSV_SEP, sepScreenStates ? screenStates : null,
+                sepMemStates ? memStates : null,
+                sepProcStates ? procStates : null);
+        pw.println();
+        for (int i=procs.size()-1; i>=0; i--) {
+            ProcessState proc = procs.get(i);
+            pw.print(proc.getName());
+            pw.print(CSV_SEP);
+            UserHandle.formatUid(pw, proc.getUid());
+            pw.print(CSV_SEP);
+            pw.print(proc.getVersion());
+            proc.dumpCsv(pw, sepScreenStates, screenStates, sepMemStates,
+                    memStates, sepProcStates, procStates, now);
+            pw.println();
+        }
+    }
+
+    public static int printArrayEntry(PrintWriter pw, String[] array, int value, int mod) {
+        int index = value/mod;
+        if (index >= 0 && index < array.length) {
+            pw.print(array[index]);
+        } else {
+            pw.print('?');
+        }
+        return value - index*mod;
+    }
+
+    public static String collapseString(String pkgName, String itemName) {
+        if (itemName.startsWith(pkgName)) {
+            final int ITEMLEN = itemName.length();
+            final int PKGLEN = pkgName.length();
+            if (ITEMLEN == PKGLEN) {
+                return "";
+            } else if (ITEMLEN >= PKGLEN) {
+                if (itemName.charAt(PKGLEN) == '.') {
+                    return itemName.substring(PKGLEN);
+                }
+            }
+        }
+        return itemName;
+    }
+}
diff --git a/com/android/internal/app/procstats/DurationsTable.java b/com/android/internal/app/procstats/DurationsTable.java
new file mode 100644
index 0000000..b711ca1
--- /dev/null
+++ b/com/android/internal/app/procstats/DurationsTable.java
@@ -0,0 +1,64 @@
+/*
+ * 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.internal.app.procstats;
+
+/**
+ * Sparse mapping table to store durations of processes, etc running in different
+ * states.
+ */
+public class DurationsTable extends SparseMappingTable.Table {
+    public DurationsTable(SparseMappingTable tableData) {
+        super(tableData);
+    }
+
+    /**
+     * Add all of the durations from the other table into this one.
+     * Resultant durations will be the sum of what is currently in the table
+     * and the new value.
+     */
+    public void addDurations(DurationsTable from) {
+        final int N = from.getKeyCount();
+        for (int i=0; i<N; i++) {
+            final int key = from.getKeyAt(i);
+            this.addDuration(SparseMappingTable.getIdFromKey(key), from.getValue(key));
+        }
+    }
+
+    /**
+     * Add the value into the value stored for the state.
+     *
+     * Resultant duration will be the sum of what is currently in the table
+     * and the new value.
+     */
+    public void addDuration(int state, long value) {
+        final int key = getOrAddKey((byte)state, 1);
+        setValue(key, getValue(key) + value);
+    }
+
+    /*
+    public long getDuration(int state, long now) {
+        final int key = getKey((byte)state);
+        if (key != SparseMappingTable.INVALID_KEY) {
+            return getValue(key);
+        } else {
+            return 0;
+        }
+    }
+    */
+}
+
+
diff --git a/com/android/internal/app/procstats/ProcessState.java b/com/android/internal/app/procstats/ProcessState.java
new file mode 100644
index 0000000..e0a4053
--- /dev/null
+++ b/com/android/internal/app/procstats/ProcessState.java
@@ -0,0 +1,1170 @@
+/*
+ * 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.internal.app.procstats;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.app.procstats.ProcessStats;
+import com.android.internal.app.procstats.ProcessStats.PackageState;
+import com.android.internal.app.procstats.ProcessStats.ProcessStateHolder;
+import com.android.internal.app.procstats.ProcessStats.TotalMemoryUseCollection;
+import static com.android.internal.app.procstats.ProcessStats.PSS_SAMPLE_COUNT;
+import static com.android.internal.app.procstats.ProcessStats.PSS_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.PSS_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_USS_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_USS_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.PSS_USS_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_COUNT;
+import static com.android.internal.app.procstats.ProcessStats.STATE_NOTHING;
+import static com.android.internal.app.procstats.ProcessStats.STATE_PERSISTENT;
+import static com.android.internal.app.procstats.ProcessStats.STATE_TOP;
+import static com.android.internal.app.procstats.ProcessStats.STATE_IMPORTANT_FOREGROUND;
+import static com.android.internal.app.procstats.ProcessStats.STATE_IMPORTANT_BACKGROUND;
+import static com.android.internal.app.procstats.ProcessStats.STATE_BACKUP;
+import static com.android.internal.app.procstats.ProcessStats.STATE_HEAVY_WEIGHT;
+import static com.android.internal.app.procstats.ProcessStats.STATE_SERVICE;
+import static com.android.internal.app.procstats.ProcessStats.STATE_SERVICE_RESTARTING;
+import static com.android.internal.app.procstats.ProcessStats.STATE_RECEIVER;
+import static com.android.internal.app.procstats.ProcessStats.STATE_HOME;
+import static com.android.internal.app.procstats.ProcessStats.STATE_LAST_ACTIVITY;
+import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_ACTIVITY;
+import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_ACTIVITY_CLIENT;
+import static com.android.internal.app.procstats.ProcessStats.STATE_CACHED_EMPTY;
+import static com.android.internal.app.procstats.ProcessStats.STATE_COUNT;
+
+import dalvik.system.VMRuntime;
+import libcore.util.EmptyArray;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Objects;
+
+public final class ProcessState {
+    private static final String TAG = "ProcessStats";
+    private static final boolean DEBUG = false;
+    private static final boolean DEBUG_PARCEL = false;
+
+    // Map from process states to the states we track.
+    private static final int[] PROCESS_STATE_TO_STATE = new int[] {
+        STATE_PERSISTENT,               // ActivityManager.PROCESS_STATE_PERSISTENT
+        STATE_PERSISTENT,               // ActivityManager.PROCESS_STATE_PERSISTENT_UI
+        STATE_TOP,                      // ActivityManager.PROCESS_STATE_TOP
+        STATE_IMPORTANT_FOREGROUND,     // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+        STATE_IMPORTANT_FOREGROUND,     // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+        STATE_TOP,                      // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+        STATE_IMPORTANT_FOREGROUND,     // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
+        STATE_IMPORTANT_BACKGROUND,     // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
+        STATE_IMPORTANT_BACKGROUND,     // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
+        STATE_BACKUP,                   // ActivityManager.PROCESS_STATE_BACKUP
+        STATE_HEAVY_WEIGHT,             // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
+        STATE_SERVICE,                  // ActivityManager.PROCESS_STATE_SERVICE
+        STATE_RECEIVER,                 // ActivityManager.PROCESS_STATE_RECEIVER
+        STATE_HOME,                     // ActivityManager.PROCESS_STATE_HOME
+        STATE_LAST_ACTIVITY,            // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
+        STATE_CACHED_ACTIVITY,          // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
+        STATE_CACHED_ACTIVITY_CLIENT,   // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT
+        STATE_CACHED_EMPTY,             // ActivityManager.PROCESS_STATE_CACHED_EMPTY
+    };
+
+    public static final Comparator<ProcessState> COMPARATOR = new Comparator<ProcessState>() {
+            @Override
+            public int compare(ProcessState lhs, ProcessState rhs) {
+                if (lhs.mTmpTotalTime < rhs.mTmpTotalTime) {
+                    return -1;
+                } else if (lhs.mTmpTotalTime > rhs.mTmpTotalTime) {
+                    return 1;
+                }
+                return 0;
+            }
+        };
+
+    static class PssAggr {
+        long pss = 0;
+        long samples = 0;
+
+        void add(long newPss, long newSamples) {
+            pss = (long)( (pss*(double)samples) + (newPss*(double)newSamples) )
+                    / (samples+newSamples);
+            samples += newSamples;
+        }
+    }
+
+    // Used by reset to count rather than storing extra maps. Be careful.
+    public int tmpNumInUse;
+    public ProcessState tmpFoundSubProc;
+
+    private final ProcessStats mStats;
+    private final String mName;
+    private final String mPackage;
+    private final int mUid;
+    private final int mVersion;
+    private final DurationsTable mDurations;
+    private final PssTable mPssTable;
+
+    private ProcessState mCommonProcess;
+    private int mCurState = STATE_NOTHING;
+    private long mStartTime;
+
+    private int mLastPssState = STATE_NOTHING;
+    private long mLastPssTime;
+
+    private boolean mActive;
+    private int mNumActiveServices;
+    private int mNumStartedServices;
+
+    private int mNumExcessiveWake;
+    private int mNumExcessiveCpu;
+
+    private int mNumCachedKill;
+    private long mMinCachedKillPss;
+    private long mAvgCachedKillPss;
+    private long mMaxCachedKillPss;
+
+    private boolean mMultiPackage;
+    private boolean mDead;
+
+    // Set in computeProcessTimeLocked and used by COMPARATOR to sort. Be careful.
+    private long mTmpTotalTime;
+
+    /**
+     * Create a new top-level process state, for the initial case where there is only
+     * a single package running in a process.  The initial state is not running.
+     */
+    public ProcessState(ProcessStats processStats, String pkg, int uid, int vers, String name) {
+        mStats = processStats;
+        mName = name;
+        mCommonProcess = this;
+        mPackage = pkg;
+        mUid = uid;
+        mVersion = vers;
+        mDurations = new DurationsTable(processStats.mTableData);
+        mPssTable = new PssTable(processStats.mTableData);
+    }
+
+    /**
+     * Create a new per-package process state for an existing top-level process
+     * state.  The current running state of the top-level process is also copied,
+     * marked as started running at 'now'.
+     */
+    public ProcessState(ProcessState commonProcess, String pkg, int uid, int vers, String name,
+            long now) {
+        mStats = commonProcess.mStats;
+        mName = name;
+        mCommonProcess = commonProcess;
+        mPackage = pkg;
+        mUid = uid;
+        mVersion = vers;
+        mCurState = commonProcess.mCurState;
+        mStartTime = now;
+        mDurations = new DurationsTable(commonProcess.mStats.mTableData);
+        mPssTable = new PssTable(commonProcess.mStats.mTableData);
+    }
+
+    public ProcessState clone(long now) {
+        ProcessState pnew = new ProcessState(this, mPackage, mUid, mVersion, mName, now);
+        pnew.mDurations.addDurations(mDurations);
+        pnew.mPssTable.copyFrom(mPssTable, PSS_COUNT);
+        pnew.mNumExcessiveCpu = mNumExcessiveCpu;
+        pnew.mNumCachedKill = mNumCachedKill;
+        pnew.mMinCachedKillPss = mMinCachedKillPss;
+        pnew.mAvgCachedKillPss = mAvgCachedKillPss;
+        pnew.mMaxCachedKillPss = mMaxCachedKillPss;
+        pnew.mActive = mActive;
+        pnew.mNumActiveServices = mNumActiveServices;
+        pnew.mNumStartedServices = mNumStartedServices;
+        return pnew;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public ProcessState getCommonProcess() {
+        return mCommonProcess;
+    }
+
+    /**
+     * Say that we are not part of a shared process, so mCommonProcess = this.
+     */
+    public void makeStandalone() {
+        mCommonProcess = this;
+    }
+
+    public String getPackage() {
+        return mPackage;
+    }
+
+    public int getUid() {
+        return mUid;
+    }
+
+    public int getVersion() {
+        return mVersion;
+    }
+
+    public boolean isMultiPackage() {
+        return mMultiPackage;
+    }
+
+    public void setMultiPackage(boolean val) {
+        mMultiPackage = val;
+    }
+    
+    public int getDurationsBucketCount() {
+        return mDurations.getKeyCount();
+    }
+
+    public void add(ProcessState other) {
+        mDurations.addDurations(other.mDurations);
+        mPssTable.mergeStats(other.mPssTable);
+        mNumExcessiveCpu += other.mNumExcessiveCpu;
+        if (other.mNumCachedKill > 0) {
+            addCachedKill(other.mNumCachedKill, other.mMinCachedKillPss,
+                    other.mAvgCachedKillPss, other.mMaxCachedKillPss);
+        }
+    }
+
+    public void resetSafely(long now) {
+        mDurations.resetTable();
+        mPssTable.resetTable();
+        mStartTime = now;
+        mLastPssState = STATE_NOTHING;
+        mLastPssTime = 0;
+        mNumExcessiveCpu = 0;
+        mNumCachedKill = 0;
+        mMinCachedKillPss = mAvgCachedKillPss = mMaxCachedKillPss = 0;
+    }
+
+    public void makeDead() {
+        mDead = true;
+    }
+
+    private void ensureNotDead() {
+        if (!mDead) {
+            return;
+        }
+        Slog.w(TAG, "ProcessState dead: name=" + mName
+                + " pkg=" + mPackage + " uid=" + mUid + " common.name=" + mCommonProcess.mName);
+    }
+
+    public void writeToParcel(Parcel out, long now) {
+        out.writeInt(mMultiPackage ? 1 : 0);
+        mDurations.writeToParcel(out);
+        mPssTable.writeToParcel(out);
+        out.writeInt(0);  // was mNumExcessiveWake
+        out.writeInt(mNumExcessiveCpu);
+        out.writeInt(mNumCachedKill);
+        if (mNumCachedKill > 0) {
+            out.writeLong(mMinCachedKillPss);
+            out.writeLong(mAvgCachedKillPss);
+            out.writeLong(mMaxCachedKillPss);
+        }
+    }
+
+    public boolean readFromParcel(Parcel in, boolean fully) {
+        boolean multiPackage = in.readInt() != 0;
+        if (fully) {
+            mMultiPackage = multiPackage;
+        }
+        if (DEBUG_PARCEL) Slog.d(TAG, "Reading durations table...");
+        if (!mDurations.readFromParcel(in)) {
+            return false;
+        }
+        if (DEBUG_PARCEL) Slog.d(TAG, "Reading pss table...");
+        if (!mPssTable.readFromParcel(in)) {
+            return false;
+        }
+        in.readInt(); // was mNumExcessiveWake
+        mNumExcessiveCpu = in.readInt();
+        mNumCachedKill = in.readInt();
+        if (mNumCachedKill > 0) {
+            mMinCachedKillPss = in.readLong();
+            mAvgCachedKillPss = in.readLong();
+            mMaxCachedKillPss = in.readLong();
+        } else {
+            mMinCachedKillPss = mAvgCachedKillPss = mMaxCachedKillPss = 0;
+        }
+        return true;
+    }
+
+    public void makeActive() {
+        ensureNotDead();
+        mActive = true;
+    }
+
+    public void makeInactive() {
+        mActive = false;
+    }
+
+    public boolean isInUse() {
+        return mActive || mNumActiveServices > 0 || mNumStartedServices > 0
+                || mCurState != STATE_NOTHING;
+    }
+
+    public boolean isActive() {
+        return mActive;
+    }
+
+    public boolean hasAnyData() {
+        return !(mDurations.getKeyCount() == 0
+                && mCurState == STATE_NOTHING
+                && mPssTable.getKeyCount() == 0);
+    }
+
+    /**
+     * Update the current state of the given list of processes.
+     *
+     * @param state Current ActivityManager.PROCESS_STATE_*
+     * @param memFactor Current mem factor constant.
+     * @param now Current time.
+     * @param pkgList Processes to update.
+     */
+    public void setState(int state, int memFactor, long now,
+            ArrayMap<String, ProcessStateHolder> pkgList) {
+        if (state < 0) {
+            state = mNumStartedServices > 0
+                    ? (STATE_SERVICE_RESTARTING+(memFactor*STATE_COUNT)) : STATE_NOTHING;
+        } else {
+            state = PROCESS_STATE_TO_STATE[state] + (memFactor*STATE_COUNT);
+        }
+
+        // First update the common process.
+        mCommonProcess.setState(state, now);
+
+        // If the common process is not multi-package, there is nothing else to do.
+        if (!mCommonProcess.mMultiPackage) {
+            return;
+        }
+
+        if (pkgList != null) {
+            for (int ip=pkgList.size()-1; ip>=0; ip--) {
+                pullFixedProc(pkgList, ip).setState(state, now);
+            }
+        }
+    }
+
+    public void setState(int state, long now) {
+        ensureNotDead();
+        if (!mDead && (mCurState != state)) {
+            //Slog.i(TAG, "Setting state in " + mName + "/" + mPackage + ": " + state);
+            commitStateTime(now);
+            mCurState = state;
+        }
+    }
+
+    public void commitStateTime(long now) {
+        if (mCurState != STATE_NOTHING) {
+            long dur = now - mStartTime;
+            if (dur > 0) {
+                mDurations.addDuration(mCurState, dur);
+            }
+        }
+        mStartTime = now;
+    }
+
+    public void incActiveServices(String serviceName) {
+        if (DEBUG && "".equals(mName)) {
+            RuntimeException here = new RuntimeException("here");
+            here.fillInStackTrace();
+            Slog.d(TAG, "incActiveServices: " + this + " service=" + serviceName
+                    + " to " + (mNumActiveServices+1), here);
+        }
+        if (mCommonProcess != this) {
+            mCommonProcess.incActiveServices(serviceName);
+        }
+        mNumActiveServices++;
+    }
+
+    public void decActiveServices(String serviceName) {
+        if (DEBUG && "".equals(mName)) {
+            RuntimeException here = new RuntimeException("here");
+            here.fillInStackTrace();
+            Slog.d(TAG, "decActiveServices: " + this + " service=" + serviceName
+                    + " to " + (mNumActiveServices-1), here);
+        }
+        if (mCommonProcess != this) {
+            mCommonProcess.decActiveServices(serviceName);
+        }
+        mNumActiveServices--;
+        if (mNumActiveServices < 0) {
+            Slog.wtfStack(TAG, "Proc active services underrun: pkg=" + mPackage
+                    + " uid=" + mUid + " proc=" + mName + " service=" + serviceName);
+            mNumActiveServices = 0;
+        }
+    }
+
+    public void incStartedServices(int memFactor, long now, String serviceName) {
+        if (false) {
+            RuntimeException here = new RuntimeException("here");
+            here.fillInStackTrace();
+            Slog.d(TAG, "incStartedServices: " + this + " service=" + serviceName
+                    + " to " + (mNumStartedServices+1), here);
+        }
+        if (mCommonProcess != this) {
+            mCommonProcess.incStartedServices(memFactor, now, serviceName);
+        }
+        mNumStartedServices++;
+        if (mNumStartedServices == 1 && mCurState == STATE_NOTHING) {
+            setState(STATE_SERVICE_RESTARTING + (memFactor*STATE_COUNT), now);
+        }
+    }
+
+    public void decStartedServices(int memFactor, long now, String serviceName) {
+        if (false) {
+            RuntimeException here = new RuntimeException("here");
+            here.fillInStackTrace();
+            Slog.d(TAG, "decActiveServices: " + this + " service=" + serviceName
+                    + " to " + (mNumStartedServices-1), here);
+        }
+        if (mCommonProcess != this) {
+            mCommonProcess.decStartedServices(memFactor, now, serviceName);
+        }
+        mNumStartedServices--;
+        if (mNumStartedServices == 0 && (mCurState%STATE_COUNT) == STATE_SERVICE_RESTARTING) {
+            setState(STATE_NOTHING, now);
+        } else if (mNumStartedServices < 0) {
+            Slog.wtfStack(TAG, "Proc started services underrun: pkg="
+                    + mPackage + " uid=" + mUid + " name=" + mName);
+            mNumStartedServices = 0;
+        }
+    }
+
+    public void addPss(long pss, long uss, boolean always,
+            ArrayMap<String, ProcessStateHolder> pkgList) {
+        ensureNotDead();
+        if (!always) {
+            if (mLastPssState == mCurState && SystemClock.uptimeMillis()
+                    < (mLastPssTime+(30*1000))) {
+                return;
+            }
+        }
+        mLastPssState = mCurState;
+        mLastPssTime = SystemClock.uptimeMillis();
+        if (mCurState != STATE_NOTHING) {
+            // First update the common process.
+            mCommonProcess.mPssTable.mergeStats(mCurState, 1, pss, pss, pss, uss, uss, uss);
+
+            // If the common process is not multi-package, there is nothing else to do.
+            if (!mCommonProcess.mMultiPackage) {
+                return;
+            }
+
+            if (pkgList != null) {
+                for (int ip=pkgList.size()-1; ip>=0; ip--) {
+                    pullFixedProc(pkgList, ip).mPssTable.mergeStats(mCurState, 1,
+                            pss, pss, pss, uss, uss, uss);
+                }
+            }
+        }
+    }
+
+    public void reportExcessiveCpu(ArrayMap<String, ProcessStateHolder> pkgList) {
+        ensureNotDead();
+        mCommonProcess.mNumExcessiveCpu++;
+        if (!mCommonProcess.mMultiPackage) {
+            return;
+        }
+
+        for (int ip=pkgList.size()-1; ip>=0; ip--) {
+            pullFixedProc(pkgList, ip).mNumExcessiveCpu++;
+        }
+    }
+
+    private void addCachedKill(int num, long minPss, long avgPss, long maxPss) {
+        if (mNumCachedKill <= 0) {
+            mNumCachedKill = num;
+            mMinCachedKillPss = minPss;
+            mAvgCachedKillPss = avgPss;
+            mMaxCachedKillPss = maxPss;
+        } else {
+            if (minPss < mMinCachedKillPss) {
+                mMinCachedKillPss = minPss;
+            }
+            if (maxPss > mMaxCachedKillPss) {
+                mMaxCachedKillPss = maxPss;
+            }
+            mAvgCachedKillPss = (long)( ((mAvgCachedKillPss*(double)mNumCachedKill) + avgPss)
+                    / (mNumCachedKill+num) );
+            mNumCachedKill += num;
+        }
+    }
+
+    public void reportCachedKill(ArrayMap<String, ProcessStateHolder> pkgList, long pss) {
+        ensureNotDead();
+        mCommonProcess.addCachedKill(1, pss, pss, pss);
+        if (!mCommonProcess.mMultiPackage) {
+            return;
+        }
+
+        for (int ip=pkgList.size()-1; ip>=0; ip--) {
+            pullFixedProc(pkgList, ip).addCachedKill(1, pss, pss, pss);
+        }
+    }
+
+    public ProcessState pullFixedProc(String pkgName) {
+        if (mMultiPackage) {
+            // The array map is still pointing to a common process state
+            // that is now shared across packages.  Update it to point to
+            // the new per-package state.
+            SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgName, mUid);
+            if (vpkg == null) {
+                throw new IllegalStateException("Didn't find package " + pkgName
+                        + " / " + mUid);
+            }
+            PackageState pkg = vpkg.get(mVersion);
+            if (pkg == null) {
+                throw new IllegalStateException("Didn't find package " + pkgName
+                        + " / " + mUid + " vers " + mVersion);
+            }
+            ProcessState proc = pkg.mProcesses.get(mName);
+            if (proc == null) {
+                throw new IllegalStateException("Didn't create per-package process "
+                        + mName + " in pkg " + pkgName + " / " + mUid + " vers " + mVersion);
+            }
+            return proc;
+        }
+        return this;
+    }
+
+    private ProcessState pullFixedProc(ArrayMap<String, ProcessStateHolder> pkgList,
+            int index) {
+        ProcessStateHolder holder = pkgList.valueAt(index);
+        ProcessState proc = holder.state;
+        if (mDead && proc.mCommonProcess != proc) {
+            // Somehow we are contining to use a process state that is dead, because
+            // it was not being told it was active during the last commit.  We can recover
+            // from this by generating a fresh new state, but this is bad because we
+            // are losing whatever data we had in the old process state.
+            Log.wtf(TAG, "Pulling dead proc: name=" + mName + " pkg=" + mPackage
+                    + " uid=" + mUid + " common.name=" + mCommonProcess.mName);
+            proc = mStats.getProcessStateLocked(proc.mPackage, proc.mUid, proc.mVersion,
+                    proc.mName);
+        }
+        if (proc.mMultiPackage) {
+            // The array map is still pointing to a common process state
+            // that is now shared across packages.  Update it to point to
+            // the new per-package state.
+            SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgList.keyAt(index),
+                    proc.mUid);
+            if (vpkg == null) {
+                throw new IllegalStateException("No existing package "
+                        + pkgList.keyAt(index) + "/" + proc.mUid
+                        + " for multi-proc " + proc.mName);
+            }
+            PackageState pkg = vpkg.get(proc.mVersion);
+            if (pkg == null) {
+                throw new IllegalStateException("No existing package "
+                        + pkgList.keyAt(index) + "/" + proc.mUid
+                        + " for multi-proc " + proc.mName + " version " + proc.mVersion);
+            }
+            String savedName = proc.mName;
+            proc = pkg.mProcesses.get(proc.mName);
+            if (proc == null) {
+                throw new IllegalStateException("Didn't create per-package process "
+                        + savedName + " in pkg " + pkg.mPackageName + "/" + pkg.mUid);
+            }
+            holder.state = proc;
+        }
+        return proc;
+    }
+
+    public long getDuration(int state, long now) {
+        long time = mDurations.getValueForId((byte)state);
+        if (mCurState == state) {
+            time += now - mStartTime;
+        }
+        return time;
+    }
+
+    public long getPssSampleCount(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_SAMPLE_COUNT);
+    }
+
+    public long getPssMinimum(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_MINIMUM);
+    }
+
+    public long getPssAverage(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_AVERAGE);
+    }
+
+    public long getPssMaximum(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_MAXIMUM);
+    }
+
+    public long getPssUssMinimum(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_USS_MINIMUM);
+    }
+
+    public long getPssUssAverage(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_USS_AVERAGE);
+    }
+
+    public long getPssUssMaximum(int state) {
+        return mPssTable.getValueForId((byte)state, PSS_USS_MAXIMUM);
+    }
+
+    /**
+     * Sums up the PSS data and adds it to 'data'.
+     * 
+     * @param data The aggregate data is added here.
+     * @param now SystemClock.uptimeMillis()
+     */
+    public void aggregatePss(TotalMemoryUseCollection data, long now) {
+        final PssAggr fgPss = new PssAggr();
+        final PssAggr bgPss = new PssAggr();
+        final PssAggr cachedPss = new PssAggr();
+        boolean havePss = false;
+        for (int i=0; i<mDurations.getKeyCount(); i++) {
+            final int key = mDurations.getKeyAt(i);
+            int type = SparseMappingTable.getIdFromKey(key);
+            int procState = type % STATE_COUNT;
+            long samples = getPssSampleCount(type);
+            if (samples > 0) {
+                long avg = getPssAverage(type);
+                havePss = true;
+                if (procState <= STATE_IMPORTANT_FOREGROUND) {
+                    fgPss.add(avg, samples);
+                } else if (procState <= STATE_RECEIVER) {
+                    bgPss.add(avg, samples);
+                } else {
+                    cachedPss.add(avg, samples);
+                }
+            }
+        }
+        if (!havePss) {
+            return;
+        }
+        boolean fgHasBg = false;
+        boolean fgHasCached = false;
+        boolean bgHasCached = false;
+        if (fgPss.samples < 3 && bgPss.samples > 0) {
+            fgHasBg = true;
+            fgPss.add(bgPss.pss, bgPss.samples);
+        }
+        if (fgPss.samples < 3 && cachedPss.samples > 0) {
+            fgHasCached = true;
+            fgPss.add(cachedPss.pss, cachedPss.samples);
+        }
+        if (bgPss.samples < 3 && cachedPss.samples > 0) {
+            bgHasCached = true;
+            bgPss.add(cachedPss.pss, cachedPss.samples);
+        }
+        if (bgPss.samples < 3 && !fgHasBg && fgPss.samples > 0) {
+            bgPss.add(fgPss.pss, fgPss.samples);
+        }
+        if (cachedPss.samples < 3 && !bgHasCached && bgPss.samples > 0) {
+            cachedPss.add(bgPss.pss, bgPss.samples);
+        }
+        if (cachedPss.samples < 3 && !fgHasCached && fgPss.samples > 0) {
+            cachedPss.add(fgPss.pss, fgPss.samples);
+        }
+        for (int i=0; i<mDurations.getKeyCount(); i++) {
+            final int key = mDurations.getKeyAt(i);
+            final int type = SparseMappingTable.getIdFromKey(key);
+            long time = mDurations.getValue(key);
+            if (mCurState == type) {
+                time += now - mStartTime;
+            }
+            final int procState = type % STATE_COUNT;
+            data.processStateTime[procState] += time;
+            long samples = getPssSampleCount(type);
+            long avg;
+            if (samples > 0) {
+                avg = getPssAverage(type);
+            } else if (procState <= STATE_IMPORTANT_FOREGROUND) {
+                samples = fgPss.samples;
+                avg = fgPss.pss;
+            } else if (procState <= STATE_RECEIVER) {
+                samples = bgPss.samples;
+                avg = bgPss.pss;
+            } else {
+                samples = cachedPss.samples;
+                avg = cachedPss.pss;
+            }
+            double newAvg = ( (data.processStatePss[procState]
+                    * (double)data.processStateSamples[procState])
+                        + (avg*(double)samples)
+                    ) / (data.processStateSamples[procState]+samples);
+            data.processStatePss[procState] = (long)newAvg;
+            data.processStateSamples[procState] += samples;
+            data.processStateWeight[procState] += avg * (double)time;
+        }
+    }
+
+    public long computeProcessTimeLocked(int[] screenStates, int[] memStates,
+                int[] procStates, long now) {
+        long totalTime = 0;
+        for (int is=0; is<screenStates.length; is++) {
+            for (int im=0; im<memStates.length; im++) {
+                for (int ip=0; ip<procStates.length; ip++) {
+                    int bucket = ((screenStates[is] + memStates[im]) * STATE_COUNT)
+                            + procStates[ip];
+                    totalTime += getDuration(bucket, now);
+                }
+            }
+        }
+        mTmpTotalTime = totalTime;
+        return totalTime;
+    }
+
+    public void dumpSummary(PrintWriter pw, String prefix,
+            int[] screenStates, int[] memStates, int[] procStates,
+            long now, long totalTime) {
+        pw.print(prefix);
+        pw.print("* ");
+        pw.print(mName);
+        pw.print(" / ");
+        UserHandle.formatUid(pw, mUid);
+        pw.print(" / v");
+        pw.print(mVersion);
+        pw.println(":");
+        dumpProcessSummaryDetails(pw, prefix, "         TOTAL: ", screenStates, memStates,
+                procStates, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "    Persistent: ", screenStates, memStates,
+                new int[] { STATE_PERSISTENT }, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "           Top: ", screenStates, memStates,
+                new int[] {STATE_TOP}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "        Imp Fg: ", screenStates, memStates,
+                new int[] { STATE_IMPORTANT_FOREGROUND }, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "        Imp Bg: ", screenStates, memStates,
+                new int[] {STATE_IMPORTANT_BACKGROUND}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "        Backup: ", screenStates, memStates,
+                new int[] {STATE_BACKUP}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "     Heavy Wgt: ", screenStates, memStates,
+                new int[] {STATE_HEAVY_WEIGHT}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "       Service: ", screenStates, memStates,
+                new int[] {STATE_SERVICE}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "    Service Rs: ", screenStates, memStates,
+                new int[] {STATE_SERVICE_RESTARTING}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "      Receiver: ", screenStates, memStates,
+                new int[] {STATE_RECEIVER}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "        (Home): ", screenStates, memStates,
+                new int[] {STATE_HOME}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "    (Last Act): ", screenStates, memStates,
+                new int[] {STATE_LAST_ACTIVITY}, now, totalTime, true);
+        dumpProcessSummaryDetails(pw, prefix, "      (Cached): ", screenStates, memStates,
+                new int[] {STATE_CACHED_ACTIVITY, STATE_CACHED_ACTIVITY_CLIENT,
+                        STATE_CACHED_EMPTY}, now, totalTime, true);
+    }
+
+    public void dumpProcessState(PrintWriter pw, String prefix,
+            int[] screenStates, int[] memStates, int[] procStates, long now) {
+        long totalTime = 0;
+        int printedScreen = -1;
+        for (int is=0; is<screenStates.length; is++) {
+            int printedMem = -1;
+            for (int im=0; im<memStates.length; im++) {
+                for (int ip=0; ip<procStates.length; ip++) {
+                    final int iscreen = screenStates[is];
+                    final int imem = memStates[im];
+                    final int bucket = ((iscreen + imem) * STATE_COUNT) + procStates[ip];
+                    long time = mDurations.getValueForId((byte)bucket);
+                    String running = "";
+                    if (mCurState == bucket) {
+                        running = " (running)";
+                    }
+                    if (time != 0) {
+                        pw.print(prefix);
+                        if (screenStates.length > 1) {
+                            DumpUtils.printScreenLabel(pw, printedScreen != iscreen
+                                    ? iscreen : STATE_NOTHING);
+                            printedScreen = iscreen;
+                        }
+                        if (memStates.length > 1) {
+                            DumpUtils.printMemLabel(pw,
+                                    printedMem != imem ? imem : STATE_NOTHING, '/');
+                            printedMem = imem;
+                        }
+                        pw.print(DumpUtils.STATE_NAMES[procStates[ip]]); pw.print(": ");
+                        TimeUtils.formatDuration(time, pw); pw.println(running);
+                        totalTime += time;
+                    }
+                }
+            }
+        }
+        if (totalTime != 0) {
+            pw.print(prefix);
+            if (screenStates.length > 1) {
+                DumpUtils.printScreenLabel(pw, STATE_NOTHING);
+            }
+            if (memStates.length > 1) {
+                DumpUtils.printMemLabel(pw, STATE_NOTHING, '/');
+            }
+            pw.print("TOTAL  : ");
+            TimeUtils.formatDuration(totalTime, pw);
+            pw.println();
+        }
+    }
+
+    public void dumpPss(PrintWriter pw, String prefix,
+            int[] screenStates, int[] memStates, int[] procStates) {
+        boolean printedHeader = false;
+        int printedScreen = -1;
+        for (int is=0; is<screenStates.length; is++) {
+            int printedMem = -1;
+            for (int im=0; im<memStates.length; im++) {
+                for (int ip=0; ip<procStates.length; ip++) {
+                    final int iscreen = screenStates[is];
+                    final int imem = memStates[im];
+                    final int bucket = ((iscreen + imem) * STATE_COUNT) + procStates[ip];
+                    long count = getPssSampleCount(bucket);
+                    if (count > 0) {
+                        if (!printedHeader) {
+                            pw.print(prefix);
+                            pw.print("PSS/USS (");
+                            pw.print(mPssTable.getKeyCount());
+                            pw.println(" entries):");
+                            printedHeader = true;
+                        }
+                        pw.print(prefix);
+                        pw.print("  ");
+                        if (screenStates.length > 1) {
+                            DumpUtils.printScreenLabel(pw,
+                                    printedScreen != iscreen ? iscreen : STATE_NOTHING);
+                            printedScreen = iscreen;
+                        }
+                        if (memStates.length > 1) {
+                            DumpUtils.printMemLabel(pw,
+                                    printedMem != imem ? imem : STATE_NOTHING, '/');
+                            printedMem = imem;
+                        }
+                        pw.print(DumpUtils.STATE_NAMES[procStates[ip]]); pw.print(": ");
+                        pw.print(count);
+                        pw.print(" samples ");
+                        DebugUtils.printSizeValue(pw, getPssMinimum(bucket) * 1024);
+                        pw.print(" ");
+                        DebugUtils.printSizeValue(pw, getPssAverage(bucket) * 1024);
+                        pw.print(" ");
+                        DebugUtils.printSizeValue(pw, getPssMaximum(bucket) * 1024);
+                        pw.print(" / ");
+                        DebugUtils.printSizeValue(pw, getPssUssMinimum(bucket) * 1024);
+                        pw.print(" ");
+                        DebugUtils.printSizeValue(pw, getPssUssAverage(bucket) * 1024);
+                        pw.print(" ");
+                        DebugUtils.printSizeValue(pw, getPssUssMaximum(bucket) * 1024);
+                        pw.println();
+                    }
+                }
+            }
+        }
+        if (mNumExcessiveCpu != 0) {
+            pw.print(prefix); pw.print("Killed for excessive CPU use: ");
+                    pw.print(mNumExcessiveCpu); pw.println(" times");
+        }
+        if (mNumCachedKill != 0) {
+            pw.print(prefix); pw.print("Killed from cached state: ");
+                    pw.print(mNumCachedKill); pw.print(" times from pss ");
+                    DebugUtils.printSizeValue(pw, mMinCachedKillPss * 1024); pw.print("-");
+                    DebugUtils.printSizeValue(pw, mAvgCachedKillPss * 1024); pw.print("-");
+                    DebugUtils.printSizeValue(pw, mMaxCachedKillPss * 1024); pw.println();
+        }
+    }
+
+    private void dumpProcessSummaryDetails(PrintWriter pw, String prefix,
+            String label, int[] screenStates, int[] memStates, int[] procStates,
+            long now, long totalTime, boolean full) {
+        ProcessStats.ProcessDataCollection totals = new ProcessStats.ProcessDataCollection(
+                screenStates, memStates, procStates);
+        computeProcessData(totals, now);
+        final double percentage = (double) totals.totalTime / (double) totalTime * 100;
+        // We don't print percentages < .01, so just drop those.
+        if (percentage >= 0.005 || totals.numPss != 0) {
+            if (prefix != null) {
+                pw.print(prefix);
+            }
+            if (label != null) {
+                pw.print(label);
+            }
+            totals.print(pw, totalTime, full);
+            if (prefix != null) {
+                pw.println();
+            }
+        }
+    }
+
+    public void dumpInternalLocked(PrintWriter pw, String prefix, boolean dumpAll) {
+        if (dumpAll) {
+            pw.print(prefix); pw.print("myID=");
+                    pw.print(Integer.toHexString(System.identityHashCode(this)));
+                    pw.print(" mCommonProcess=");
+                    pw.print(Integer.toHexString(System.identityHashCode(mCommonProcess)));
+                    pw.print(" mPackage="); pw.println(mPackage);
+            if (mMultiPackage) {
+                pw.print(prefix); pw.print("mMultiPackage="); pw.println(mMultiPackage);
+            }
+            if (this != mCommonProcess) {
+                pw.print(prefix); pw.print("Common Proc: "); pw.print(mCommonProcess.mName);
+                        pw.print("/"); pw.print(mCommonProcess.mUid);
+                        pw.print(" pkg="); pw.println(mCommonProcess.mPackage);
+            }
+        }
+        if (mActive) {
+            pw.print(prefix); pw.print("mActive="); pw.println(mActive);
+        }
+        if (mDead) {
+            pw.print(prefix); pw.print("mDead="); pw.println(mDead);
+        }
+        if (mNumActiveServices != 0 || mNumStartedServices != 0) {
+            pw.print(prefix); pw.print("mNumActiveServices="); pw.print(mNumActiveServices);
+                    pw.print(" mNumStartedServices=");
+                    pw.println(mNumStartedServices);
+        }
+    }
+
+    public void computeProcessData(ProcessStats.ProcessDataCollection data, long now) {
+        data.totalTime = 0;
+        data.numPss = data.minPss = data.avgPss = data.maxPss =
+                data.minUss = data.avgUss = data.maxUss = 0;
+        for (int is=0; is<data.screenStates.length; is++) {
+            for (int im=0; im<data.memStates.length; im++) {
+                for (int ip=0; ip<data.procStates.length; ip++) {
+                    int bucket = ((data.screenStates[is] + data.memStates[im]) * STATE_COUNT)
+                            + data.procStates[ip];
+                    data.totalTime += getDuration(bucket, now);
+                    long samples = getPssSampleCount(bucket);
+                    if (samples > 0) {
+                        long minPss = getPssMinimum(bucket);
+                        long avgPss = getPssAverage(bucket);
+                        long maxPss = getPssMaximum(bucket);
+                        long minUss = getPssUssMinimum(bucket);
+                        long avgUss = getPssUssAverage(bucket);
+                        long maxUss = getPssUssMaximum(bucket);
+                        if (data.numPss == 0) {
+                            data.minPss = minPss;
+                            data.avgPss = avgPss;
+                            data.maxPss = maxPss;
+                            data.minUss = minUss;
+                            data.avgUss = avgUss;
+                            data.maxUss = maxUss;
+                        } else {
+                            if (minPss < data.minPss) {
+                                data.minPss = minPss;
+                            }
+                            data.avgPss = (long)( ((data.avgPss*(double)data.numPss)
+                                    + (avgPss*(double)samples)) / (data.numPss+samples) );
+                            if (maxPss > data.maxPss) {
+                                data.maxPss = maxPss;
+                            }
+                            if (minUss < data.minUss) {
+                                data.minUss = minUss;
+                            }
+                            data.avgUss = (long)( ((data.avgUss*(double)data.numPss)
+                                    + (avgUss*(double)samples)) / (data.numPss+samples) );
+                            if (maxUss > data.maxUss) {
+                                data.maxUss = maxUss;
+                            }
+                        }
+                        data.numPss += samples;
+                    }
+                }
+            }
+        }
+    }
+
+    public void dumpCsv(PrintWriter pw,
+            boolean sepScreenStates, int[] screenStates, boolean sepMemStates,
+            int[] memStates, boolean sepProcStates, int[] procStates, long now) {
+        final int NSS = sepScreenStates ? screenStates.length : 1;
+        final int NMS = sepMemStates ? memStates.length : 1;
+        final int NPS = sepProcStates ? procStates.length : 1;
+        for (int iss=0; iss<NSS; iss++) {
+            for (int ims=0; ims<NMS; ims++) {
+                for (int ips=0; ips<NPS; ips++) {
+                    final int vsscreen = sepScreenStates ? screenStates[iss] : 0;
+                    final int vsmem = sepMemStates ? memStates[ims] : 0;
+                    final int vsproc = sepProcStates ? procStates[ips] : 0;
+                    final int NSA = sepScreenStates ? 1 : screenStates.length;
+                    final int NMA = sepMemStates ? 1 : memStates.length;
+                    final int NPA = sepProcStates ? 1 : procStates.length;
+                    long totalTime = 0;
+                    for (int isa=0; isa<NSA; isa++) {
+                        for (int ima=0; ima<NMA; ima++) {
+                            for (int ipa=0; ipa<NPA; ipa++) {
+                                final int vascreen = sepScreenStates ? 0 : screenStates[isa];
+                                final int vamem = sepMemStates ? 0 : memStates[ima];
+                                final int vaproc = sepProcStates ? 0 : procStates[ipa];
+                                final int bucket = ((vsscreen + vascreen + vsmem + vamem)
+                                        * STATE_COUNT) + vsproc + vaproc;
+                                totalTime += getDuration(bucket, now);
+                            }
+                        }
+                    }
+                    pw.print(DumpUtils.CSV_SEP);
+                    pw.print(totalTime);
+                }
+            }
+        }
+    }
+
+    public void dumpPackageProcCheckin(PrintWriter pw, String pkgName, int uid, int vers,
+            String itemName, long now) {
+        pw.print("pkgproc,");
+        pw.print(pkgName);
+        pw.print(",");
+        pw.print(uid);
+        pw.print(",");
+        pw.print(vers);
+        pw.print(",");
+        pw.print(DumpUtils.collapseString(pkgName, itemName));
+        dumpAllStateCheckin(pw, now);
+        pw.println();
+        if (mPssTable.getKeyCount() > 0) {
+            pw.print("pkgpss,");
+            pw.print(pkgName);
+            pw.print(",");
+            pw.print(uid);
+            pw.print(",");
+            pw.print(vers);
+            pw.print(",");
+            pw.print(DumpUtils.collapseString(pkgName, itemName));
+            dumpAllPssCheckin(pw);
+            pw.println();
+        }
+        if (mNumExcessiveCpu > 0 || mNumCachedKill > 0) {
+            pw.print("pkgkills,");
+            pw.print(pkgName);
+            pw.print(",");
+            pw.print(uid);
+            pw.print(",");
+            pw.print(vers);
+            pw.print(",");
+            pw.print(DumpUtils.collapseString(pkgName, itemName));
+            pw.print(",");
+            pw.print("0"); // was mNumExcessiveWake
+            pw.print(",");
+            pw.print(mNumExcessiveCpu);
+            pw.print(",");
+            pw.print(mNumCachedKill);
+            pw.print(",");
+            pw.print(mMinCachedKillPss);
+            pw.print(":");
+            pw.print(mAvgCachedKillPss);
+            pw.print(":");
+            pw.print(mMaxCachedKillPss);
+            pw.println();
+        }
+    }
+
+    public void dumpProcCheckin(PrintWriter pw, String procName, int uid, long now) {
+        if (mDurations.getKeyCount() > 0) {
+            pw.print("proc,");
+            pw.print(procName);
+            pw.print(",");
+            pw.print(uid);
+            dumpAllStateCheckin(pw, now);
+            pw.println();
+        }
+        if (mPssTable.getKeyCount() > 0) {
+            pw.print("pss,");
+            pw.print(procName);
+            pw.print(",");
+            pw.print(uid);
+            dumpAllPssCheckin(pw);
+            pw.println();
+        }
+        if (mNumExcessiveCpu > 0 || mNumCachedKill > 0) {
+            pw.print("kills,");
+            pw.print(procName);
+            pw.print(",");
+            pw.print(uid);
+            pw.print(",");
+            pw.print("0"); // was mNumExcessiveWake
+            pw.print(",");
+            pw.print(mNumExcessiveCpu);
+            pw.print(",");
+            pw.print(mNumCachedKill);
+            pw.print(",");
+            pw.print(mMinCachedKillPss);
+            pw.print(":");
+            pw.print(mAvgCachedKillPss);
+            pw.print(":");
+            pw.print(mMaxCachedKillPss);
+            pw.println();
+        }
+    }
+
+    public void dumpAllStateCheckin(PrintWriter pw, long now) {
+        boolean didCurState = false;
+        for (int i=0; i<mDurations.getKeyCount(); i++) {
+            final int key = mDurations.getKeyAt(i);
+            final int type = SparseMappingTable.getIdFromKey(key);
+            long time = mDurations.getValue(key);
+            if (mCurState == type) {
+                didCurState = true;
+                time += now - mStartTime;
+            }
+            DumpUtils.printProcStateTagAndValue(pw, type, time);
+        }
+        if (!didCurState && mCurState != STATE_NOTHING) {
+            DumpUtils.printProcStateTagAndValue(pw, mCurState, now - mStartTime);
+        }
+    }
+
+    public void dumpAllPssCheckin(PrintWriter pw) {
+        final int N = mPssTable.getKeyCount();
+        for (int i=0; i<N; i++) {
+            final int key = mPssTable.getKeyAt(i);
+            final int type = SparseMappingTable.getIdFromKey(key);
+            pw.print(',');
+            DumpUtils.printProcStateTag(pw, type);
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_SAMPLE_COUNT));
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_MINIMUM));
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_AVERAGE));
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_MAXIMUM));
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_USS_MINIMUM));
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_USS_AVERAGE));
+            pw.print(':');
+            pw.print(mPssTable.getValue(key, PSS_USS_MAXIMUM));
+        }
+    }
+
+    public String toString() {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("ProcessState{").append(Integer.toHexString(System.identityHashCode(this)))
+                .append(" ").append(mName).append("/").append(mUid)
+                .append(" pkg=").append(mPackage);
+        if (mMultiPackage) sb.append(" (multi)");
+        if (mCommonProcess != this) sb.append(" (sub)");
+        sb.append("}");
+        return sb.toString();
+    }
+}
diff --git a/com/android/internal/app/procstats/ProcessStats.java b/com/android/internal/app/procstats/ProcessStats.java
new file mode 100644
index 0000000..35b53c2
--- /dev/null
+++ b/com/android/internal/app/procstats/ProcessStats.java
@@ -0,0 +1,1804 @@
+/*
+ * 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.internal.app.procstats;
+
+import android.os.Debug;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.app.ProcessMap;
+import com.android.internal.app.procstats.DurationsTable;
+import com.android.internal.app.procstats.ProcessState;
+import com.android.internal.app.procstats.PssTable;
+import com.android.internal.app.procstats.ServiceState;
+import com.android.internal.app.procstats.SparseMappingTable;
+import com.android.internal.app.procstats.SysMemUsageTable;
+import com.android.internal.app.procstats.DumpUtils.*;
+
+import dalvik.system.VMRuntime;
+import libcore.util.EmptyArray;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+public final class ProcessStats implements Parcelable {
+    public static final String TAG = "ProcessStats";
+    static final boolean DEBUG = false;
+    static final boolean DEBUG_PARCEL = false;
+
+    public static final String SERVICE_NAME = "procstats";
+
+    // How often the service commits its data, giving the minimum batching
+    // that is done.
+    public static long COMMIT_PERIOD = 3*60*60*1000;  // Commit current stats every 3 hours
+
+    // Minimum uptime period before committing.  If the COMMIT_PERIOD has elapsed but
+    // the total uptime has not exceeded this amount, then the commit will be held until
+    // it is reached.
+    public static long COMMIT_UPTIME_PERIOD = 60*60*1000;  // Must have at least 1 hour elapsed
+
+    public static final int STATE_NOTHING = -1;
+    public static final int STATE_PERSISTENT = 0;
+    public static final int STATE_TOP = 1;
+    public static final int STATE_IMPORTANT_FOREGROUND = 2;
+    public static final int STATE_IMPORTANT_BACKGROUND = 3;
+    public static final int STATE_BACKUP = 4;
+    public static final int STATE_HEAVY_WEIGHT = 5;
+    public static final int STATE_SERVICE = 6;
+    public static final int STATE_SERVICE_RESTARTING = 7;
+    public static final int STATE_RECEIVER = 8;
+    public static final int STATE_HOME = 9;
+    public static final int STATE_LAST_ACTIVITY = 10;
+    public static final int STATE_CACHED_ACTIVITY = 11;
+    public static final int STATE_CACHED_ACTIVITY_CLIENT = 12;
+    public static final int STATE_CACHED_EMPTY = 13;
+    public static final int STATE_COUNT = STATE_CACHED_EMPTY+1;
+
+    public static final int PSS_SAMPLE_COUNT = 0;
+    public static final int PSS_MINIMUM = 1;
+    public static final int PSS_AVERAGE = 2;
+    public static final int PSS_MAXIMUM = 3;
+    public static final int PSS_USS_MINIMUM = 4;
+    public static final int PSS_USS_AVERAGE = 5;
+    public static final int PSS_USS_MAXIMUM = 6;
+    public static final int PSS_COUNT = PSS_USS_MAXIMUM+1;
+
+    public static final int SYS_MEM_USAGE_SAMPLE_COUNT = 0;
+    public static final int SYS_MEM_USAGE_CACHED_MINIMUM = 1;
+    public static final int SYS_MEM_USAGE_CACHED_AVERAGE = 2;
+    public static final int SYS_MEM_USAGE_CACHED_MAXIMUM = 3;
+    public static final int SYS_MEM_USAGE_FREE_MINIMUM = 4;
+    public static final int SYS_MEM_USAGE_FREE_AVERAGE = 5;
+    public static final int SYS_MEM_USAGE_FREE_MAXIMUM = 6;
+    public static final int SYS_MEM_USAGE_ZRAM_MINIMUM = 7;
+    public static final int SYS_MEM_USAGE_ZRAM_AVERAGE = 8;
+    public static final int SYS_MEM_USAGE_ZRAM_MAXIMUM = 9;
+    public static final int SYS_MEM_USAGE_KERNEL_MINIMUM = 10;
+    public static final int SYS_MEM_USAGE_KERNEL_AVERAGE = 11;
+    public static final int SYS_MEM_USAGE_KERNEL_MAXIMUM = 12;
+    public static final int SYS_MEM_USAGE_NATIVE_MINIMUM = 13;
+    public static final int SYS_MEM_USAGE_NATIVE_AVERAGE = 14;
+    public static final int SYS_MEM_USAGE_NATIVE_MAXIMUM = 15;
+    public static final int SYS_MEM_USAGE_COUNT = SYS_MEM_USAGE_NATIVE_MAXIMUM+1;
+
+    public static final int ADJ_NOTHING = -1;
+    public static final int ADJ_MEM_FACTOR_NORMAL = 0;
+    public static final int ADJ_MEM_FACTOR_MODERATE = 1;
+    public static final int ADJ_MEM_FACTOR_LOW = 2;
+    public static final int ADJ_MEM_FACTOR_CRITICAL = 3;
+    public static final int ADJ_MEM_FACTOR_COUNT = ADJ_MEM_FACTOR_CRITICAL+1;
+    public static final int ADJ_SCREEN_MOD = ADJ_MEM_FACTOR_COUNT;
+    public static final int ADJ_SCREEN_OFF = 0;
+    public static final int ADJ_SCREEN_ON = ADJ_SCREEN_MOD;
+    public static final int ADJ_COUNT = ADJ_SCREEN_ON*2;
+
+    public static final int FLAG_COMPLETE = 1<<0;
+    public static final int FLAG_SHUTDOWN = 1<<1;
+    public static final int FLAG_SYSPROPS = 1<<2;
+
+    public static final int[] ALL_MEM_ADJ = new int[] { ADJ_MEM_FACTOR_NORMAL,
+            ADJ_MEM_FACTOR_MODERATE, ADJ_MEM_FACTOR_LOW, ADJ_MEM_FACTOR_CRITICAL };
+
+    public static final int[] ALL_SCREEN_ADJ = new int[] { ADJ_SCREEN_OFF, ADJ_SCREEN_ON };
+
+    public static final int[] NON_CACHED_PROC_STATES = new int[] {
+            STATE_PERSISTENT, STATE_TOP, STATE_IMPORTANT_FOREGROUND,
+            STATE_IMPORTANT_BACKGROUND, STATE_BACKUP, STATE_HEAVY_WEIGHT,
+            STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER
+    };
+
+    public static final int[] BACKGROUND_PROC_STATES = new int[] {
+            STATE_IMPORTANT_FOREGROUND, STATE_IMPORTANT_BACKGROUND, STATE_BACKUP,
+            STATE_HEAVY_WEIGHT, STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER
+    };
+
+    public static final int[] ALL_PROC_STATES = new int[] { STATE_PERSISTENT,
+            STATE_TOP, STATE_IMPORTANT_FOREGROUND, STATE_IMPORTANT_BACKGROUND, STATE_BACKUP,
+            STATE_HEAVY_WEIGHT, STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER,
+            STATE_HOME, STATE_LAST_ACTIVITY, STATE_CACHED_ACTIVITY,
+            STATE_CACHED_ACTIVITY_CLIENT, STATE_CACHED_EMPTY
+    };
+
+    // Current version of the parcel format.
+    private static final int PARCEL_VERSION = 21;
+    // In-memory Parcel magic number, used to detect attempts to unmarshall bad data
+    private static final int MAGIC = 0x50535454;
+
+    public String mReadError;
+    public String mTimePeriodStartClockStr;
+    public int mFlags;
+
+    public final ProcessMap<SparseArray<PackageState>> mPackages
+            = new ProcessMap<SparseArray<PackageState>>();
+    public final ProcessMap<ProcessState> mProcesses = new ProcessMap<ProcessState>();
+
+    public final long[] mMemFactorDurations = new long[ADJ_COUNT];
+    public int mMemFactor = STATE_NOTHING;
+    public long mStartTime;
+
+    public long mTimePeriodStartClock;
+    public long mTimePeriodStartRealtime;
+    public long mTimePeriodEndRealtime;
+    public long mTimePeriodStartUptime;
+    public long mTimePeriodEndUptime;
+    String mRuntime;
+    boolean mRunning;
+
+    boolean mHasSwappedOutPss;
+
+    public final SparseMappingTable mTableData = new SparseMappingTable();
+
+    public final long[] mSysMemUsageArgs = new long[SYS_MEM_USAGE_COUNT];
+    public final SysMemUsageTable mSysMemUsage = new SysMemUsageTable(mTableData);
+
+    // For writing parcels.
+    ArrayMap<String, Integer> mCommonStringToIndex;
+
+    // For reading parcels.
+    ArrayList<String> mIndexToCommonString;
+
+    private static final Pattern sPageTypeRegex = Pattern.compile(
+            "^Node\\s+(\\d+),.*. type\\s+(\\w+)\\s+([\\s\\d]+?)\\s*$");
+    private final ArrayList<Integer> mPageTypeZones = new ArrayList<Integer>();
+    private final ArrayList<String> mPageTypeLabels = new ArrayList<String>();
+    private final ArrayList<int[]> mPageTypeSizes = new ArrayList<int[]>();
+
+    public ProcessStats(boolean running) {
+        mRunning = running;
+        reset();
+        if (running) {
+            // If we are actively running, we need to determine whether the system is
+            // collecting swap pss data.
+            Debug.MemoryInfo info = new Debug.MemoryInfo();
+            Debug.getMemoryInfo(android.os.Process.myPid(), info);
+            mHasSwappedOutPss = info.hasSwappedOutPss();
+        }
+    }
+
+    public ProcessStats(Parcel in) {
+        reset();
+        readFromParcel(in);
+    }
+
+    public void add(ProcessStats other) {
+        ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = other.mPackages.getMap();
+        for (int ip=0; ip<pkgMap.size(); ip++) {
+            final String pkgName = pkgMap.keyAt(ip);
+            final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+            for (int iu=0; iu<uids.size(); iu++) {
+                final int uid = uids.keyAt(iu);
+                final SparseArray<PackageState> versions = uids.valueAt(iu);
+                for (int iv=0; iv<versions.size(); iv++) {
+                    final int vers = versions.keyAt(iv);
+                    final PackageState otherState = versions.valueAt(iv);
+                    final int NPROCS = otherState.mProcesses.size();
+                    final int NSRVS = otherState.mServices.size();
+                    for (int iproc=0; iproc<NPROCS; iproc++) {
+                        ProcessState otherProc = otherState.mProcesses.valueAt(iproc);
+                        if (otherProc.getCommonProcess() != otherProc) {
+                            if (DEBUG) Slog.d(TAG, "Adding pkg " + pkgName + " uid " + uid
+                                    + " vers " + vers + " proc " + otherProc.getName());
+                            ProcessState thisProc = getProcessStateLocked(pkgName, uid, vers,
+                                    otherProc.getName());
+                            if (thisProc.getCommonProcess() == thisProc) {
+                                if (DEBUG) Slog.d(TAG, "Existing process is single-package, splitting");
+                                thisProc.setMultiPackage(true);
+                                long now = SystemClock.uptimeMillis();
+                                final PackageState pkgState = getPackageStateLocked(pkgName, uid,
+                                        vers);
+                                thisProc = thisProc.clone(now);
+                                pkgState.mProcesses.put(thisProc.getName(), thisProc);
+                            }
+                            thisProc.add(otherProc);
+                        }
+                    }
+                    for (int isvc=0; isvc<NSRVS; isvc++) {
+                        ServiceState otherSvc = otherState.mServices.valueAt(isvc);
+                        if (DEBUG) Slog.d(TAG, "Adding pkg " + pkgName + " uid " + uid
+                                + " service " + otherSvc.getName());
+                        ServiceState thisSvc = getServiceStateLocked(pkgName, uid, vers,
+                                otherSvc.getProcessName(), otherSvc.getName());
+                        thisSvc.add(otherSvc);
+                    }
+                }
+            }
+        }
+
+        ArrayMap<String, SparseArray<ProcessState>> procMap = other.mProcesses.getMap();
+        for (int ip=0; ip<procMap.size(); ip++) {
+            SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            for (int iu=0; iu<uids.size(); iu++) {
+                int uid = uids.keyAt(iu);
+                ProcessState otherProc = uids.valueAt(iu);
+                final String name = otherProc.getName();
+                final String pkg = otherProc.getPackage();
+                final int vers = otherProc.getVersion();
+                ProcessState thisProc = mProcesses.get(name, uid);
+                if (DEBUG) Slog.d(TAG, "Adding uid " + uid + " proc " + name);
+                if (thisProc == null) {
+                    if (DEBUG) Slog.d(TAG, "Creating new process!");
+                    thisProc = new ProcessState(this, pkg, uid, vers, name);
+                    mProcesses.put(name, uid, thisProc);
+                    PackageState thisState = getPackageStateLocked(pkg, uid, vers);
+                    if (!thisState.mProcesses.containsKey(name)) {
+                        thisState.mProcesses.put(name, thisProc);
+                    }
+                }
+                thisProc.add(otherProc);
+            }
+        }
+
+        for (int i=0; i<ADJ_COUNT; i++) {
+            if (DEBUG) Slog.d(TAG, "Total duration #" + i + " inc by "
+                    + other.mMemFactorDurations[i] + " from "
+                    + mMemFactorDurations[i]);
+            mMemFactorDurations[i] += other.mMemFactorDurations[i];
+        }
+
+        mSysMemUsage.mergeStats(other.mSysMemUsage);
+
+        if (other.mTimePeriodStartClock < mTimePeriodStartClock) {
+            mTimePeriodStartClock = other.mTimePeriodStartClock;
+            mTimePeriodStartClockStr = other.mTimePeriodStartClockStr;
+        }
+        mTimePeriodEndRealtime += other.mTimePeriodEndRealtime - other.mTimePeriodStartRealtime;
+        mTimePeriodEndUptime += other.mTimePeriodEndUptime - other.mTimePeriodStartUptime;
+
+        mHasSwappedOutPss |= other.mHasSwappedOutPss;
+    }
+
+    public void addSysMemUsage(long cachedMem, long freeMem, long zramMem, long kernelMem,
+            long nativeMem) {
+        if (mMemFactor != STATE_NOTHING) {
+            int state = mMemFactor * STATE_COUNT;
+            mSysMemUsageArgs[SYS_MEM_USAGE_SAMPLE_COUNT] = 1;
+            for (int i=0; i<3; i++) {
+                mSysMemUsageArgs[SYS_MEM_USAGE_CACHED_MINIMUM + i] = cachedMem;
+                mSysMemUsageArgs[SYS_MEM_USAGE_FREE_MINIMUM + i] = freeMem;
+                mSysMemUsageArgs[SYS_MEM_USAGE_ZRAM_MINIMUM + i] = zramMem;
+                mSysMemUsageArgs[SYS_MEM_USAGE_KERNEL_MINIMUM + i] = kernelMem;
+                mSysMemUsageArgs[SYS_MEM_USAGE_NATIVE_MINIMUM + i] = nativeMem;
+            }
+            mSysMemUsage.mergeStats(state, mSysMemUsageArgs, 0);
+        }
+    }
+
+    public static final Parcelable.Creator<ProcessStats> CREATOR
+            = new Parcelable.Creator<ProcessStats>() {
+        public ProcessStats createFromParcel(Parcel in) {
+            return new ProcessStats(in);
+        }
+
+        public ProcessStats[] newArray(int size) {
+            return new ProcessStats[size];
+        }
+    };
+
+    public void computeTotalMemoryUse(TotalMemoryUseCollection data, long now) {
+        data.totalTime = 0;
+        for (int i=0; i<STATE_COUNT; i++) {
+            data.processStateWeight[i] = 0;
+            data.processStatePss[i] = 0;
+            data.processStateTime[i] = 0;
+            data.processStateSamples[i] = 0;
+        }
+        for (int i=0; i<SYS_MEM_USAGE_COUNT; i++) {
+            data.sysMemUsage[i] = 0;
+        }
+        data.sysMemCachedWeight = 0;
+        data.sysMemFreeWeight = 0;
+        data.sysMemZRamWeight = 0;
+        data.sysMemKernelWeight = 0;
+        data.sysMemNativeWeight = 0;
+        data.sysMemSamples = 0;
+        final long[] totalMemUsage = mSysMemUsage.getTotalMemUsage();
+        for (int is=0; is<data.screenStates.length; is++) {
+            for (int im=0; im<data.memStates.length; im++) {
+                int memBucket = data.screenStates[is] + data.memStates[im];
+                int stateBucket = memBucket * STATE_COUNT;
+                long memTime = mMemFactorDurations[memBucket];
+                if (mMemFactor == memBucket) {
+                    memTime += now - mStartTime;
+                }
+                data.totalTime += memTime;
+                final int sysKey = mSysMemUsage.getKey((byte)stateBucket);
+                long[] longs = totalMemUsage;
+                int idx = 0;
+                if (sysKey != SparseMappingTable.INVALID_KEY) {
+                    final long[] tmpLongs = mSysMemUsage.getArrayForKey(sysKey);
+                    final int tmpIndex = SparseMappingTable.getIndexFromKey(sysKey);
+                    if (tmpLongs[tmpIndex+SYS_MEM_USAGE_SAMPLE_COUNT] >= 3) {
+                        SysMemUsageTable.mergeSysMemUsage(data.sysMemUsage, 0, longs, idx);
+                        longs = tmpLongs;
+                        idx = tmpIndex;
+                    }
+                }
+                data.sysMemCachedWeight += longs[idx+SYS_MEM_USAGE_CACHED_AVERAGE]
+                        * (double)memTime;
+                data.sysMemFreeWeight += longs[idx+SYS_MEM_USAGE_FREE_AVERAGE]
+                        * (double)memTime;
+                data.sysMemZRamWeight += longs[idx + SYS_MEM_USAGE_ZRAM_AVERAGE]
+                        * (double) memTime;
+                data.sysMemKernelWeight += longs[idx+SYS_MEM_USAGE_KERNEL_AVERAGE]
+                        * (double)memTime;
+                data.sysMemNativeWeight += longs[idx+SYS_MEM_USAGE_NATIVE_AVERAGE]
+                        * (double)memTime;
+                data.sysMemSamples += longs[idx+SYS_MEM_USAGE_SAMPLE_COUNT];
+             }
+        }
+        data.hasSwappedOutPss = mHasSwappedOutPss;
+        ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap();
+        for (int iproc=0; iproc<procMap.size(); iproc++) {
+            SparseArray<ProcessState> uids = procMap.valueAt(iproc);
+            for (int iu=0; iu<uids.size(); iu++) {
+                final ProcessState proc = uids.valueAt(iu);
+                proc.aggregatePss(data, now);
+            }
+        }
+    }
+
+    public void reset() {
+        if (DEBUG) Slog.d(TAG, "Resetting state of " + mTimePeriodStartClockStr);
+        resetCommon();
+        mPackages.getMap().clear();
+        mProcesses.getMap().clear();
+        mMemFactor = STATE_NOTHING;
+        mStartTime = 0;
+        if (DEBUG) Slog.d(TAG, "State reset; now " + mTimePeriodStartClockStr);
+    }
+
+    public void resetSafely() {
+        if (DEBUG) Slog.d(TAG, "Safely resetting state of " + mTimePeriodStartClockStr);
+        resetCommon();
+
+        // First initialize use count of all common processes.
+        final long now = SystemClock.uptimeMillis();
+        final ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap();
+        for (int ip=procMap.size()-1; ip>=0; ip--) {
+            final SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            for (int iu=uids.size()-1; iu>=0; iu--) {
+                uids.valueAt(iu).tmpNumInUse = 0;
+           }
+        }
+
+        // Next reset or prune all per-package processes, and for the ones that are reset
+        // track this back to the common processes.
+        final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+        for (int ip=pkgMap.size()-1; ip>=0; ip--) {
+            final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+            for (int iu=uids.size()-1; iu>=0; iu--) {
+                final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+                for (int iv=vpkgs.size()-1; iv>=0; iv--) {
+                    final PackageState pkgState = vpkgs.valueAt(iv);
+                    for (int iproc=pkgState.mProcesses.size()-1; iproc>=0; iproc--) {
+                        final ProcessState ps = pkgState.mProcesses.valueAt(iproc);
+                        if (ps.isInUse()) {
+                            ps.resetSafely(now);
+                            ps.getCommonProcess().tmpNumInUse++;
+                            ps.getCommonProcess().tmpFoundSubProc = ps;
+                        } else {
+                            pkgState.mProcesses.valueAt(iproc).makeDead();
+                            pkgState.mProcesses.removeAt(iproc);
+                        }
+                    }
+                    for (int isvc=pkgState.mServices.size()-1; isvc>=0; isvc--) {
+                        final ServiceState ss = pkgState.mServices.valueAt(isvc);
+                        if (ss.isInUse()) {
+                            ss.resetSafely(now);
+                        } else {
+                            pkgState.mServices.removeAt(isvc);
+                        }
+                    }
+                    if (pkgState.mProcesses.size() <= 0 && pkgState.mServices.size() <= 0) {
+                        vpkgs.removeAt(iv);
+                    }
+                }
+                if (vpkgs.size() <= 0) {
+                    uids.removeAt(iu);
+                }
+            }
+            if (uids.size() <= 0) {
+                pkgMap.removeAt(ip);
+            }
+        }
+
+        // Finally prune out any common processes that are no longer in use.
+        for (int ip=procMap.size()-1; ip>=0; ip--) {
+            final SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            for (int iu=uids.size()-1; iu>=0; iu--) {
+                ProcessState ps = uids.valueAt(iu);
+                if (ps.isInUse() || ps.tmpNumInUse > 0) {
+                    // If this is a process for multiple packages, we could at this point
+                    // be back down to one package.  In that case, we want to revert back
+                    // to a single shared ProcessState.  We can do this by converting the
+                    // current package-specific ProcessState up to the shared ProcessState,
+                    // throwing away the current one we have here (because nobody else is
+                    // using it).
+                    if (!ps.isActive() && ps.isMultiPackage() && ps.tmpNumInUse == 1) {
+                        // Here we go...
+                        ps = ps.tmpFoundSubProc;
+                        ps.makeStandalone();
+                        uids.setValueAt(iu, ps);
+                    } else {
+                        ps.resetSafely(now);
+                    }
+                } else {
+                    ps.makeDead();
+                    uids.removeAt(iu);
+                }
+            }
+            if (uids.size() <= 0) {
+                procMap.removeAt(ip);
+            }
+        }
+
+        mStartTime = now;
+        if (DEBUG) Slog.d(TAG, "State reset; now " + mTimePeriodStartClockStr);
+    }
+
+    private void resetCommon() {
+        mTimePeriodStartClock = System.currentTimeMillis();
+        buildTimePeriodStartClockStr();
+        mTimePeriodStartRealtime = mTimePeriodEndRealtime = SystemClock.elapsedRealtime();
+        mTimePeriodStartUptime = mTimePeriodEndUptime = SystemClock.uptimeMillis();
+        mTableData.reset();
+        Arrays.fill(mMemFactorDurations, 0);
+        mSysMemUsage.resetTable();
+        mStartTime = 0;
+        mReadError = null;
+        mFlags = 0;
+        evaluateSystemProperties(true);
+        updateFragmentation();
+    }
+
+    public boolean evaluateSystemProperties(boolean update) {
+        boolean changed = false;
+        String runtime = SystemProperties.get("persist.sys.dalvik.vm.lib.2",
+                VMRuntime.getRuntime().vmLibrary());
+        if (!Objects.equals(runtime, mRuntime)) {
+            changed = true;
+            if (update) {
+                mRuntime = runtime;
+            }
+        }
+        return changed;
+    }
+
+    private void buildTimePeriodStartClockStr() {
+        mTimePeriodStartClockStr = DateFormat.format("yyyy-MM-dd-HH-mm-ss",
+                mTimePeriodStartClock).toString();
+    }
+
+    static final int[] BAD_TABLE = new int[0];
+
+
+    /**
+     * Load the system's memory fragmentation info.
+     */
+    public void updateFragmentation() {
+        // Parse /proc/pagetypeinfo and store the values.
+        BufferedReader reader = null;
+        try {
+            reader = new BufferedReader(new FileReader("/proc/pagetypeinfo"));
+            final Matcher matcher = sPageTypeRegex.matcher("");
+            mPageTypeZones.clear();
+            mPageTypeLabels.clear();
+            mPageTypeSizes.clear();
+            while (true) {
+                final String line = reader.readLine();
+                if (line == null) {
+                    break;
+                }
+                matcher.reset(line);
+                if (matcher.matches()) {
+                    final Integer zone = Integer.valueOf(matcher.group(1), 10);
+                    if (zone == null) {
+                        continue;
+                    }
+                    mPageTypeZones.add(zone);
+                    mPageTypeLabels.add(matcher.group(2));
+                    mPageTypeSizes.add(splitAndParseNumbers(matcher.group(3)));
+                }
+            }
+        } catch (IOException ex) {
+            mPageTypeZones.clear();
+            mPageTypeLabels.clear();
+            mPageTypeSizes.clear();
+            return;
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (IOException allHopeIsLost) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Split the string of digits separaed by spaces.  There must be no
+     * leading or trailing spaces.  The format is ensured by the regex
+     * above.
+     */
+    private static int[] splitAndParseNumbers(String s) {
+        // These are always positive and the numbers can't be so big that we'll overflow
+        // so just do the parsing inline.
+        boolean digit = false;
+        int count = 0;
+        final int N = s.length();
+        // Count the numbers
+        for (int i=0; i<N; i++) {
+            final char c = s.charAt(i);
+            if (c >= '0' && c <= '9') {
+                if (!digit) {
+                    digit = true;
+                    count++;
+                }
+            } else {
+                digit = false;
+            }
+        }
+        // Parse the numbers
+        final int[] result = new int[count];
+        int p = 0;
+        int val = 0;
+        for (int i=0; i<N; i++) {
+            final char c = s.charAt(i);
+            if (c >= '0' && c <= '9') {
+                if (!digit) {
+                    digit = true;
+                    val = c - '0';
+                } else {
+                    val *= 10;
+                    val += c - '0';
+                }
+            } else {
+                if (digit) {
+                    digit = false;
+                    result[p++] = val;
+                }
+            }
+        }
+        if (count > 0) {
+            result[count-1] = val;
+        }
+        return result;
+    }
+
+
+    private void writeCompactedLongArray(Parcel out, long[] array, int num) {
+        for (int i=0; i<num; i++) {
+            long val = array[i];
+            if (val < 0) {
+                Slog.w(TAG, "Time val negative: " + val);
+                val = 0;
+            }
+            if (val <= Integer.MAX_VALUE) {
+                out.writeInt((int)val);
+            } else {
+                int top = ~((int)((val>>32)&0x7fffffff));
+                int bottom = (int)(val&0x0ffffffffL);
+                out.writeInt(top);
+                out.writeInt(bottom);
+            }
+        }
+    }
+
+    private void readCompactedLongArray(Parcel in, int version, long[] array, int num) {
+        if (version <= 10) {
+            in.readLongArray(array);
+            return;
+        }
+        final int alen = array.length;
+        if (num > alen) {
+            throw new RuntimeException("bad array lengths: got " + num + " array is " + alen);
+        }
+        int i;
+        for (i=0; i<num; i++) {
+            int val = in.readInt();
+            if (val >= 0) {
+                array[i] = val;
+            } else {
+                int bottom = in.readInt();
+                array[i] = (((long)~val)<<32) | bottom;
+            }
+        }
+        while (i < alen) {
+            array[i] = 0;
+            i++;
+        }
+    }
+
+    private void writeCommonString(Parcel out, String name) {
+        Integer index = mCommonStringToIndex.get(name);
+        if (index != null) {
+            out.writeInt(index);
+            return;
+        }
+        index = mCommonStringToIndex.size();
+        mCommonStringToIndex.put(name, index);
+        out.writeInt(~index);
+        out.writeString(name);
+    }
+
+    private String readCommonString(Parcel in, int version) {
+        if (version <= 9) {
+            return in.readString();
+        }
+        int index = in.readInt();
+        if (index >= 0) {
+            return mIndexToCommonString.get(index);
+        }
+        index = ~index;
+        String name = in.readString();
+        while (mIndexToCommonString.size() <= index) {
+            mIndexToCommonString.add(null);
+        }
+        mIndexToCommonString.set(index, name);
+        return name;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        writeToParcel(out, SystemClock.uptimeMillis(), flags);
+    }
+
+    /** @hide */
+    public void writeToParcel(Parcel out, long now, int flags) {
+        out.writeInt(MAGIC);
+        out.writeInt(PARCEL_VERSION);
+        out.writeInt(STATE_COUNT);
+        out.writeInt(ADJ_COUNT);
+        out.writeInt(PSS_COUNT);
+        out.writeInt(SYS_MEM_USAGE_COUNT);
+        out.writeInt(SparseMappingTable.ARRAY_SIZE);
+
+        mCommonStringToIndex = new ArrayMap<String, Integer>(mProcesses.size());
+
+        // First commit all running times.
+        ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap();
+        final int NPROC = procMap.size();
+        for (int ip=0; ip<NPROC; ip++) {
+            SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            final int NUID = uids.size();
+            for (int iu=0; iu<NUID; iu++) {
+                uids.valueAt(iu).commitStateTime(now);
+            }
+        }
+        final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+        final int NPKG = pkgMap.size();
+        for (int ip=0; ip<NPKG; ip++) {
+            final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+            final int NUID = uids.size();
+            for (int iu=0; iu<NUID; iu++) {
+                final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+                final int NVERS = vpkgs.size();
+                for (int iv=0; iv<NVERS; iv++) {
+                    PackageState pkgState = vpkgs.valueAt(iv);
+                    final int NPROCS = pkgState.mProcesses.size();
+                    for (int iproc=0; iproc<NPROCS; iproc++) {
+                        ProcessState proc = pkgState.mProcesses.valueAt(iproc);
+                        if (proc.getCommonProcess() != proc) {
+                            proc.commitStateTime(now);
+                        }
+                    }
+                    final int NSRVS = pkgState.mServices.size();
+                    for (int isvc=0; isvc<NSRVS; isvc++) {
+                        pkgState.mServices.valueAt(isvc).commitStateTime(now);
+                    }
+                }
+            }
+        }
+
+        out.writeLong(mTimePeriodStartClock);
+        out.writeLong(mTimePeriodStartRealtime);
+        out.writeLong(mTimePeriodEndRealtime);
+        out.writeLong(mTimePeriodStartUptime);
+        out.writeLong(mTimePeriodEndUptime);
+        out.writeString(mRuntime);
+        out.writeInt(mHasSwappedOutPss ? 1 : 0);
+        out.writeInt(mFlags);
+
+        mTableData.writeToParcel(out);
+
+        if (mMemFactor != STATE_NOTHING) {
+            mMemFactorDurations[mMemFactor] += now - mStartTime;
+            mStartTime = now;
+        }
+        writeCompactedLongArray(out, mMemFactorDurations, mMemFactorDurations.length);
+
+        mSysMemUsage.writeToParcel(out);
+
+        out.writeInt(NPROC);
+        for (int ip=0; ip<NPROC; ip++) {
+            writeCommonString(out, procMap.keyAt(ip));
+            final SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            final int NUID = uids.size();
+            out.writeInt(NUID);
+            for (int iu=0; iu<NUID; iu++) {
+                out.writeInt(uids.keyAt(iu));
+                final ProcessState proc = uids.valueAt(iu);
+                writeCommonString(out, proc.getPackage());
+                out.writeInt(proc.getVersion());
+                proc.writeToParcel(out, now);
+            }
+        }
+        out.writeInt(NPKG);
+        for (int ip=0; ip<NPKG; ip++) {
+            writeCommonString(out, pkgMap.keyAt(ip));
+            final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+            final int NUID = uids.size();
+            out.writeInt(NUID);
+            for (int iu=0; iu<NUID; iu++) {
+                out.writeInt(uids.keyAt(iu));
+                final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+                final int NVERS = vpkgs.size();
+                out.writeInt(NVERS);
+                for (int iv=0; iv<NVERS; iv++) {
+                    out.writeInt(vpkgs.keyAt(iv));
+                    final PackageState pkgState = vpkgs.valueAt(iv);
+                    final int NPROCS = pkgState.mProcesses.size();
+                    out.writeInt(NPROCS);
+                    for (int iproc=0; iproc<NPROCS; iproc++) {
+                        writeCommonString(out, pkgState.mProcesses.keyAt(iproc));
+                        final ProcessState proc = pkgState.mProcesses.valueAt(iproc);
+                        if (proc.getCommonProcess() == proc) {
+                            // This is the same as the common process we wrote above.
+                            out.writeInt(0);
+                        } else {
+                            // There is separate data for this package's process.
+                            out.writeInt(1);
+                            proc.writeToParcel(out, now);
+                        }
+                    }
+                    final int NSRVS = pkgState.mServices.size();
+                    out.writeInt(NSRVS);
+                    for (int isvc=0; isvc<NSRVS; isvc++) {
+                        out.writeString(pkgState.mServices.keyAt(isvc));
+                        final ServiceState svc = pkgState.mServices.valueAt(isvc);
+                        writeCommonString(out, svc.getProcessName());
+                        svc.writeToParcel(out, now);
+                    }
+                }
+            }
+        }
+
+        // Fragmentation info (/proc/pagetypeinfo)
+        final int NPAGETYPES = mPageTypeLabels.size();
+        out.writeInt(NPAGETYPES);
+        for (int i=0; i<NPAGETYPES; i++) {
+            out.writeInt(mPageTypeZones.get(i));
+            out.writeString(mPageTypeLabels.get(i));
+            out.writeIntArray(mPageTypeSizes.get(i));
+        }
+
+        mCommonStringToIndex = null;
+    }
+
+    private boolean readCheckedInt(Parcel in, int val, String what) {
+        int got;
+        if ((got=in.readInt()) != val) {
+            mReadError = "bad " + what + ": " + got;
+            return false;
+        }
+        return true;
+    }
+
+    static byte[] readFully(InputStream stream, int[] outLen) throws IOException {
+        int pos = 0;
+        final int initialAvail = stream.available();
+        byte[] data = new byte[initialAvail > 0 ? (initialAvail+1) : 16384];
+        while (true) {
+            int amt = stream.read(data, pos, data.length-pos);
+            if (DEBUG_PARCEL) Slog.i("foo", "Read " + amt + " bytes at " + pos
+                    + " of avail " + data.length);
+            if (amt < 0) {
+                if (DEBUG_PARCEL) Slog.i("foo", "**** FINISHED READING: pos=" + pos
+                        + " len=" + data.length);
+                outLen[0] = pos;
+                return data;
+            }
+            pos += amt;
+            if (pos >= data.length) {
+                byte[] newData = new byte[pos+16384];
+                if (DEBUG_PARCEL) Slog.i(TAG, "Copying " + pos + " bytes to new array len "
+                        + newData.length);
+                System.arraycopy(data, 0, newData, 0, pos);
+                data = newData;
+            }
+        }
+    }
+
+    public void read(InputStream stream) {
+        try {
+            int[] len = new int[1];
+            byte[] raw = readFully(stream, len);
+            Parcel in = Parcel.obtain();
+            in.unmarshall(raw, 0, len[0]);
+            in.setDataPosition(0);
+            stream.close();
+
+            readFromParcel(in);
+        } catch (IOException e) {
+            mReadError = "caught exception: " + e;
+        }
+    }
+
+    public void readFromParcel(Parcel in) {
+        final boolean hadData = mPackages.getMap().size() > 0
+                || mProcesses.getMap().size() > 0;
+        if (hadData) {
+            resetSafely();
+        }
+
+        if (!readCheckedInt(in, MAGIC, "magic number")) {
+            return;
+        }
+        int version = in.readInt();
+        if (version != PARCEL_VERSION) {
+            mReadError = "bad version: " + version;
+            return;
+        }
+        if (!readCheckedInt(in, STATE_COUNT, "state count")) {
+            return;
+        }
+        if (!readCheckedInt(in, ADJ_COUNT, "adj count")) {
+            return;
+        }
+        if (!readCheckedInt(in, PSS_COUNT, "pss count")) {
+            return;
+        }
+        if (!readCheckedInt(in, SYS_MEM_USAGE_COUNT, "sys mem usage count")) {
+            return;
+        }
+        if (!readCheckedInt(in, SparseMappingTable.ARRAY_SIZE, "longs size")) {
+            return;
+        }
+
+        mIndexToCommonString = new ArrayList<String>();
+
+        mTimePeriodStartClock = in.readLong();
+        buildTimePeriodStartClockStr();
+        mTimePeriodStartRealtime = in.readLong();
+        mTimePeriodEndRealtime = in.readLong();
+        mTimePeriodStartUptime = in.readLong();
+        mTimePeriodEndUptime = in.readLong();
+        mRuntime = in.readString();
+        mHasSwappedOutPss = in.readInt() != 0;
+        mFlags = in.readInt();
+        mTableData.readFromParcel(in);
+        readCompactedLongArray(in, version, mMemFactorDurations, mMemFactorDurations.length);
+        if (!mSysMemUsage.readFromParcel(in)) {
+            return;
+        }
+
+        int NPROC = in.readInt();
+        if (NPROC < 0) {
+            mReadError = "bad process count: " + NPROC;
+            return;
+        }
+        while (NPROC > 0) {
+            NPROC--;
+            final String procName = readCommonString(in, version);
+            if (procName == null) {
+                mReadError = "bad process name";
+                return;
+            }
+            int NUID = in.readInt();
+            if (NUID < 0) {
+                mReadError = "bad uid count: " + NUID;
+                return;
+            }
+            while (NUID > 0) {
+                NUID--;
+                final int uid = in.readInt();
+                if (uid < 0) {
+                    mReadError = "bad uid: " + uid;
+                    return;
+                }
+                final String pkgName = readCommonString(in, version);
+                if (pkgName == null) {
+                    mReadError = "bad process package name";
+                    return;
+                }
+                final int vers = in.readInt();
+                ProcessState proc = hadData ? mProcesses.get(procName, uid) : null;
+                if (proc != null) {
+                    if (!proc.readFromParcel(in, false)) {
+                        return;
+                    }
+                } else {
+                    proc = new ProcessState(this, pkgName, uid, vers, procName);
+                    if (!proc.readFromParcel(in, true)) {
+                        return;
+                    }
+                }
+                if (DEBUG_PARCEL) Slog.d(TAG, "Adding process: " + procName + " " + uid
+                        + " " + proc);
+                mProcesses.put(procName, uid, proc);
+            }
+        }
+
+        if (DEBUG_PARCEL) Slog.d(TAG, "Read " + mProcesses.getMap().size() + " processes");
+
+        int NPKG = in.readInt();
+        if (NPKG < 0) {
+            mReadError = "bad package count: " + NPKG;
+            return;
+        }
+        while (NPKG > 0) {
+            NPKG--;
+            final String pkgName = readCommonString(in, version);
+            if (pkgName == null) {
+                mReadError = "bad package name";
+                return;
+            }
+            int NUID = in.readInt();
+            if (NUID < 0) {
+                mReadError = "bad uid count: " + NUID;
+                return;
+            }
+            while (NUID > 0) {
+                NUID--;
+                final int uid = in.readInt();
+                if (uid < 0) {
+                    mReadError = "bad uid: " + uid;
+                    return;
+                }
+                int NVERS = in.readInt();
+                if (NVERS < 0) {
+                    mReadError = "bad versions count: " + NVERS;
+                    return;
+                }
+                while (NVERS > 0) {
+                    NVERS--;
+                    final int vers = in.readInt();
+                    PackageState pkgState = new PackageState(pkgName, uid);
+                    SparseArray<PackageState> vpkg = mPackages.get(pkgName, uid);
+                    if (vpkg == null) {
+                        vpkg = new SparseArray<PackageState>();
+                        mPackages.put(pkgName, uid, vpkg);
+                    }
+                    vpkg.put(vers, pkgState);
+                    int NPROCS = in.readInt();
+                    if (NPROCS < 0) {
+                        mReadError = "bad package process count: " + NPROCS;
+                        return;
+                    }
+                    while (NPROCS > 0) {
+                        NPROCS--;
+                        String procName = readCommonString(in, version);
+                        if (procName == null) {
+                            mReadError = "bad package process name";
+                            return;
+                        }
+                        int hasProc = in.readInt();
+                        if (DEBUG_PARCEL) Slog.d(TAG, "Reading package " + pkgName + " " + uid
+                                + " process " + procName + " hasProc=" + hasProc);
+                        ProcessState commonProc = mProcesses.get(procName, uid);
+                        if (DEBUG_PARCEL) Slog.d(TAG, "Got common proc " + procName + " " + uid
+                                + ": " + commonProc);
+                        if (commonProc == null) {
+                            mReadError = "no common proc: " + procName;
+                            return;
+                        }
+                        if (hasProc != 0) {
+                            // The process for this package is unique to the package; we
+                            // need to load it.  We don't need to do anything about it if
+                            // it is not unique because if someone later looks for it
+                            // they will find and use it from the global procs.
+                            ProcessState proc = hadData ? pkgState.mProcesses.get(procName) : null;
+                            if (proc != null) {
+                                if (!proc.readFromParcel(in, false)) {
+                                    return;
+                                }
+                            } else {
+                                proc = new ProcessState(commonProc, pkgName, uid, vers, procName,
+                                        0);
+                                if (!proc.readFromParcel(in, true)) {
+                                    return;
+                                }
+                            }
+                            if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " process: "
+                                    + procName + " " + uid + " " + proc);
+                            pkgState.mProcesses.put(procName, proc);
+                        } else {
+                            if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " process: "
+                                    + procName + " " + uid + " " + commonProc);
+                            pkgState.mProcesses.put(procName, commonProc);
+                        }
+                    }
+                    int NSRVS = in.readInt();
+                    if (NSRVS < 0) {
+                        mReadError = "bad package service count: " + NSRVS;
+                        return;
+                    }
+                    while (NSRVS > 0) {
+                        NSRVS--;
+                        String serviceName = in.readString();
+                        if (serviceName == null) {
+                            mReadError = "bad package service name";
+                            return;
+                        }
+                        String processName = version > 9 ? readCommonString(in, version) : null;
+                        ServiceState serv = hadData ? pkgState.mServices.get(serviceName) : null;
+                        if (serv == null) {
+                            serv = new ServiceState(this, pkgName, serviceName, processName, null);
+                        }
+                        if (!serv.readFromParcel(in)) {
+                            return;
+                        }
+                        if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " service: "
+                                + serviceName + " " + uid + " " + serv);
+                        pkgState.mServices.put(serviceName, serv);
+                    }
+                }
+            }
+        }
+
+        // Fragmentation info
+        final int NPAGETYPES = in.readInt();
+        mPageTypeZones.clear();
+        mPageTypeZones.ensureCapacity(NPAGETYPES);
+        mPageTypeLabels.clear();
+        mPageTypeLabels.ensureCapacity(NPAGETYPES);
+        mPageTypeSizes.clear();
+        mPageTypeSizes.ensureCapacity(NPAGETYPES);
+        for (int i=0; i<NPAGETYPES; i++) {
+            mPageTypeZones.add(in.readInt());
+            mPageTypeLabels.add(in.readString());
+            mPageTypeSizes.add(in.createIntArray());
+        }
+
+        mIndexToCommonString = null;
+
+        if (DEBUG_PARCEL) Slog.d(TAG, "Successfully read procstats!");
+    }
+
+    public PackageState getPackageStateLocked(String packageName, int uid, int vers) {
+        SparseArray<PackageState> vpkg = mPackages.get(packageName, uid);
+        if (vpkg == null) {
+            vpkg = new SparseArray<PackageState>();
+            mPackages.put(packageName, uid, vpkg);
+        }
+        PackageState as = vpkg.get(vers);
+        if (as != null) {
+            return as;
+        }
+        as = new PackageState(packageName, uid);
+        vpkg.put(vers, as);
+        return as;
+    }
+
+    public ProcessState getProcessStateLocked(String packageName, int uid, int vers,
+            String processName) {
+        final PackageState pkgState = getPackageStateLocked(packageName, uid, vers);
+        ProcessState ps = pkgState.mProcesses.get(processName);
+        if (ps != null) {
+            return ps;
+        }
+        ProcessState commonProc = mProcesses.get(processName, uid);
+        if (commonProc == null) {
+            commonProc = new ProcessState(this, packageName, uid, vers, processName);
+            mProcesses.put(processName, uid, commonProc);
+            if (DEBUG) Slog.d(TAG, "GETPROC created new common " + commonProc);
+        }
+        if (!commonProc.isMultiPackage()) {
+            if (packageName.equals(commonProc.getPackage()) && vers == commonProc.getVersion()) {
+                // This common process is not in use by multiple packages, and
+                // is for the calling package, so we can just use it directly.
+                ps = commonProc;
+                if (DEBUG) Slog.d(TAG, "GETPROC also using for pkg " + commonProc);
+            } else {
+                if (DEBUG) Slog.d(TAG, "GETPROC need to split common proc!");
+                // This common process has not been in use by multiple packages,
+                // but it was created for a different package than the caller.
+                // We need to convert it to a multi-package process.
+                commonProc.setMultiPackage(true);
+                // To do this, we need to make two new process states, one a copy
+                // of the current state for the process under the original package
+                // name, and the second a free new process state for it as the
+                // new package name.
+                long now = SystemClock.uptimeMillis();
+                // First let's make a copy of the current process state and put
+                // that under the now unique state for its original package name.
+                final PackageState commonPkgState = getPackageStateLocked(commonProc.getPackage(),
+                        uid, commonProc.getVersion());
+                if (commonPkgState != null) {
+                    ProcessState cloned = commonProc.clone(now);
+                    if (DEBUG) Slog.d(TAG, "GETPROC setting clone to pkg " + commonProc.getPackage()
+                            + ": " + cloned);
+                    commonPkgState.mProcesses.put(commonProc.getName(), cloned);
+                    // If this has active services, we need to update their process pointer
+                    // to point to the new package-specific process state.
+                    for (int i=commonPkgState.mServices.size()-1; i>=0; i--) {
+                        ServiceState ss = commonPkgState.mServices.valueAt(i);
+                        if (ss.getProcess() == commonProc) {
+                            if (DEBUG) Slog.d(TAG, "GETPROC switching service to cloned: " + ss);
+                            ss.setProcess(cloned);
+                        } else if (DEBUG) {
+                            Slog.d(TAG, "GETPROC leaving proc of " + ss);
+                        }
+                    }
+                } else {
+                    Slog.w(TAG, "Cloning proc state: no package state " + commonProc.getPackage()
+                            + "/" + uid + " for proc " + commonProc.getName());
+                }
+                // And now make a fresh new process state for the new package name.
+                ps = new ProcessState(commonProc, packageName, uid, vers, processName, now);
+                if (DEBUG) Slog.d(TAG, "GETPROC created new pkg " + ps);
+            }
+        } else {
+            // The common process is for multiple packages, we need to create a
+            // separate object for the per-package data.
+            ps = new ProcessState(commonProc, packageName, uid, vers, processName,
+                    SystemClock.uptimeMillis());
+            if (DEBUG) Slog.d(TAG, "GETPROC created new pkg " + ps);
+        }
+        pkgState.mProcesses.put(processName, ps);
+        if (DEBUG) Slog.d(TAG, "GETPROC adding new pkg " + ps);
+        return ps;
+    }
+
+    public ServiceState getServiceStateLocked(String packageName, int uid, int vers,
+            String processName, String className) {
+        final ProcessStats.PackageState as = getPackageStateLocked(packageName, uid, vers);
+        ServiceState ss = as.mServices.get(className);
+        if (ss != null) {
+            if (DEBUG) Slog.d(TAG, "GETSVC: returning existing " + ss);
+            return ss;
+        }
+        final ProcessState ps = processName != null
+                ? getProcessStateLocked(packageName, uid, vers, processName) : null;
+        ss = new ServiceState(this, packageName, className, processName, ps);
+        as.mServices.put(className, ss);
+        if (DEBUG) Slog.d(TAG, "GETSVC: creating " + ss + " in " + ps);
+        return ss;
+    }
+
+    public void dumpLocked(PrintWriter pw, String reqPackage, long now, boolean dumpSummary,
+            boolean dumpAll, boolean activeOnly) {
+        long totalTime = DumpUtils.dumpSingleTime(null, null, mMemFactorDurations, mMemFactor,
+                mStartTime, now);
+        boolean sepNeeded = false;
+        if (mSysMemUsage.getKeyCount() > 0) {
+            pw.println("System memory usage:");
+            mSysMemUsage.dump(pw, "  ", ALL_SCREEN_ADJ, ALL_MEM_ADJ);
+            sepNeeded = true;
+        }
+        ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+        boolean printedHeader = false;
+        for (int ip=0; ip<pkgMap.size(); ip++) {
+            final String pkgName = pkgMap.keyAt(ip);
+            final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+            for (int iu=0; iu<uids.size(); iu++) {
+                final int uid = uids.keyAt(iu);
+                final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+                for (int iv=0; iv<vpkgs.size(); iv++) {
+                    final int vers = vpkgs.keyAt(iv);
+                    final PackageState pkgState = vpkgs.valueAt(iv);
+                    final int NPROCS = pkgState.mProcesses.size();
+                    final int NSRVS = pkgState.mServices.size();
+                    final boolean pkgMatch = reqPackage == null || reqPackage.equals(pkgName);
+                    if (!pkgMatch) {
+                        boolean procMatch = false;
+                        for (int iproc=0; iproc<NPROCS; iproc++) {
+                            ProcessState proc = pkgState.mProcesses.valueAt(iproc);
+                            if (reqPackage.equals(proc.getName())) {
+                                procMatch = true;
+                                break;
+                            }
+                        }
+                        if (!procMatch) {
+                            continue;
+                        }
+                    }
+                    if (NPROCS > 0 || NSRVS > 0) {
+                        if (!printedHeader) {
+                            if (sepNeeded) pw.println();
+                            pw.println("Per-Package Stats:");
+                            printedHeader = true;
+                            sepNeeded = true;
+                        }
+                        pw.print("  * "); pw.print(pkgName); pw.print(" / ");
+                                UserHandle.formatUid(pw, uid); pw.print(" / v");
+                                pw.print(vers); pw.println(":");
+                    }
+                    if (!dumpSummary || dumpAll) {
+                        for (int iproc=0; iproc<NPROCS; iproc++) {
+                            ProcessState proc = pkgState.mProcesses.valueAt(iproc);
+                            if (!pkgMatch && !reqPackage.equals(proc.getName())) {
+                                continue;
+                            }
+                            if (activeOnly && !proc.isInUse()) {
+                                pw.print("      (Not active: ");
+                                        pw.print(pkgState.mProcesses.keyAt(iproc)); pw.println(")");
+                                continue;
+                            }
+                            pw.print("      Process ");
+                            pw.print(pkgState.mProcesses.keyAt(iproc));
+                            if (proc.getCommonProcess().isMultiPackage()) {
+                                pw.print(" (multi, ");
+                            } else {
+                                pw.print(" (unique, ");
+                            }
+                            pw.print(proc.getDurationsBucketCount());
+                            pw.print(" entries)");
+                            pw.println(":");
+                            proc.dumpProcessState(pw, "        ", ALL_SCREEN_ADJ, ALL_MEM_ADJ,
+                                    ALL_PROC_STATES, now);
+                            proc.dumpPss(pw, "        ", ALL_SCREEN_ADJ, ALL_MEM_ADJ,
+                                    ALL_PROC_STATES);
+                            proc.dumpInternalLocked(pw, "        ", dumpAll);
+                        }
+                    } else {
+                        ArrayList<ProcessState> procs = new ArrayList<ProcessState>();
+                        for (int iproc=0; iproc<NPROCS; iproc++) {
+                            ProcessState proc = pkgState.mProcesses.valueAt(iproc);
+                            if (!pkgMatch && !reqPackage.equals(proc.getName())) {
+                                continue;
+                            }
+                            if (activeOnly && !proc.isInUse()) {
+                                continue;
+                            }
+                            procs.add(proc);
+                        }
+                        DumpUtils.dumpProcessSummaryLocked(pw, "      ", procs,
+                                ALL_SCREEN_ADJ, ALL_MEM_ADJ, NON_CACHED_PROC_STATES,
+                                now, totalTime);
+                    }
+                    for (int isvc=0; isvc<NSRVS; isvc++) {
+                        ServiceState svc = pkgState.mServices.valueAt(isvc);
+                        if (!pkgMatch && !reqPackage.equals(svc.getProcessName())) {
+                            continue;
+                        }
+                        if (activeOnly && !svc.isInUse()) {
+                            pw.print("      (Not active: ");
+                                    pw.print(pkgState.mServices.keyAt(isvc)); pw.println(")");
+                            continue;
+                        }
+                        if (dumpAll) {
+                            pw.print("      Service ");
+                        } else {
+                            pw.print("      * ");
+                        }
+                        pw.print(pkgState.mServices.keyAt(isvc));
+                        pw.println(":");
+                        pw.print("        Process: "); pw.println(svc.getProcessName());
+                        svc.dumpStats(pw, "        ", "          ", "    ",
+                                now, totalTime, dumpSummary, dumpAll);
+                    }
+                }
+            }
+        }
+
+        ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap();
+        printedHeader = false;
+        int numShownProcs = 0, numTotalProcs = 0;
+        for (int ip=0; ip<procMap.size(); ip++) {
+            String procName = procMap.keyAt(ip);
+            SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            for (int iu=0; iu<uids.size(); iu++) {
+                int uid = uids.keyAt(iu);
+                numTotalProcs++;
+                final ProcessState proc = uids.valueAt(iu);
+                if (proc.hasAnyData()) {
+                    continue;
+                }
+                if (!proc.isMultiPackage()) {
+                    continue;
+                }
+                if (reqPackage != null && !reqPackage.equals(procName)
+                        && !reqPackage.equals(proc.getPackage())) {
+                    continue;
+                }
+                numShownProcs++;
+                if (sepNeeded) {
+                    pw.println();
+                }
+                sepNeeded = true;
+                if (!printedHeader) {
+                    pw.println("Multi-Package Common Processes:");
+                    printedHeader = true;
+                }
+                if (activeOnly && !proc.isInUse()) {
+                    pw.print("      (Not active: "); pw.print(procName); pw.println(")");
+                    continue;
+                }
+                pw.print("  * "); pw.print(procName); pw.print(" / ");
+                        UserHandle.formatUid(pw, uid);
+                        pw.print(" ("); pw.print(proc.getDurationsBucketCount());
+                        pw.print(" entries)"); pw.println(":");
+                proc.dumpProcessState(pw, "        ", ALL_SCREEN_ADJ, ALL_MEM_ADJ,
+                        ALL_PROC_STATES, now);
+                proc.dumpPss(pw, "        ", ALL_SCREEN_ADJ, ALL_MEM_ADJ, ALL_PROC_STATES);
+                proc.dumpInternalLocked(pw, "        ", dumpAll);
+            }
+        }
+        if (dumpAll) {
+            pw.println();
+            pw.print("  Total procs: "); pw.print(numShownProcs);
+                    pw.print(" shown of "); pw.print(numTotalProcs); pw.println(" total");
+        }
+
+        if (sepNeeded) {
+            pw.println();
+        }
+        if (dumpSummary) {
+            pw.println("Summary:");
+            dumpSummaryLocked(pw, reqPackage, now, activeOnly);
+        } else {
+            dumpTotalsLocked(pw, now);
+        }
+
+        if (dumpAll) {
+            pw.println();
+            pw.println("Internal state:");
+            /*
+            pw.print("  Num long arrays: "); pw.println(mLongs.size());
+            pw.print("  Next long entry: "); pw.println(mNextLong);
+            */
+            pw.print("  mRunning="); pw.println(mRunning);
+        }
+
+        dumpFragmentationLocked(pw);
+    }
+
+    public void dumpSummaryLocked(PrintWriter pw, String reqPackage, long now, boolean activeOnly) {
+        long totalTime = DumpUtils.dumpSingleTime(null, null, mMemFactorDurations, mMemFactor,
+                mStartTime, now);
+        dumpFilteredSummaryLocked(pw, null, "  ", ALL_SCREEN_ADJ, ALL_MEM_ADJ,
+                ALL_PROC_STATES, NON_CACHED_PROC_STATES, now, totalTime, reqPackage, activeOnly);
+        pw.println();
+        dumpTotalsLocked(pw, now);
+    }
+
+    private void dumpFragmentationLocked(PrintWriter pw) {
+        pw.println();
+        pw.println("Available pages by page size:");
+        final int NPAGETYPES = mPageTypeLabels.size();
+        for (int i=0; i<NPAGETYPES; i++) {
+            pw.format("Zone %3d  %14s ", mPageTypeZones.get(i), mPageTypeLabels.get(i));
+            final int[] sizes = mPageTypeSizes.get(i);
+            final int N = sizes == null ? 0 : sizes.length;
+            for (int j=0; j<N; j++) {
+                pw.format("%6d", sizes[j]);
+            }
+            pw.println();
+        }
+    }
+
+    long printMemoryCategory(PrintWriter pw, String prefix, String label, double memWeight,
+            long totalTime, long curTotalMem, int samples) {
+        if (memWeight != 0) {
+            long mem = (long)(memWeight * 1024 / totalTime);
+            pw.print(prefix);
+            pw.print(label);
+            pw.print(": ");
+            DebugUtils.printSizeValue(pw, mem);
+            pw.print(" (");
+            pw.print(samples);
+            pw.print(" samples)");
+            pw.println();
+            return curTotalMem + mem;
+        }
+        return curTotalMem;
+    }
+
+    void dumpTotalsLocked(PrintWriter pw, long now) {
+        pw.println("Run time Stats:");
+        DumpUtils.dumpSingleTime(pw, "  ", mMemFactorDurations, mMemFactor, mStartTime, now);
+        pw.println();
+        pw.println("Memory usage:");
+        TotalMemoryUseCollection totalMem = new TotalMemoryUseCollection(ALL_SCREEN_ADJ,
+                ALL_MEM_ADJ);
+        computeTotalMemoryUse(totalMem, now);
+        long totalPss = 0;
+        totalPss = printMemoryCategory(pw, "  ", "Kernel ", totalMem.sysMemKernelWeight,
+                totalMem.totalTime, totalPss, totalMem.sysMemSamples);
+        totalPss = printMemoryCategory(pw, "  ", "Native ", totalMem.sysMemNativeWeight,
+                totalMem.totalTime, totalPss, totalMem.sysMemSamples);
+        for (int i=0; i<STATE_COUNT; i++) {
+            // Skip restarting service state -- that is not actually a running process.
+            if (i != STATE_SERVICE_RESTARTING) {
+                totalPss = printMemoryCategory(pw, "  ", DumpUtils.STATE_NAMES[i],
+                        totalMem.processStateWeight[i], totalMem.totalTime, totalPss,
+                        totalMem.processStateSamples[i]);
+            }
+        }
+        totalPss = printMemoryCategory(pw, "  ", "Cached ", totalMem.sysMemCachedWeight,
+                totalMem.totalTime, totalPss, totalMem.sysMemSamples);
+        totalPss = printMemoryCategory(pw, "  ", "Free   ", totalMem.sysMemFreeWeight,
+                totalMem.totalTime, totalPss, totalMem.sysMemSamples);
+        totalPss = printMemoryCategory(pw, "  ", "Z-Ram  ", totalMem.sysMemZRamWeight,
+                totalMem.totalTime, totalPss, totalMem.sysMemSamples);
+        pw.print("  TOTAL  : ");
+        DebugUtils.printSizeValue(pw, totalPss);
+        pw.println();
+        printMemoryCategory(pw, "  ", DumpUtils.STATE_NAMES[STATE_SERVICE_RESTARTING],
+                totalMem.processStateWeight[STATE_SERVICE_RESTARTING], totalMem.totalTime, totalPss,
+                totalMem.processStateSamples[STATE_SERVICE_RESTARTING]);
+        pw.println();
+        pw.print("          Start time: ");
+        pw.print(DateFormat.format("yyyy-MM-dd HH:mm:ss", mTimePeriodStartClock));
+        pw.println();
+        pw.print("  Total elapsed time: ");
+        TimeUtils.formatDuration(
+                (mRunning ? SystemClock.elapsedRealtime() : mTimePeriodEndRealtime)
+                        - mTimePeriodStartRealtime, pw);
+        boolean partial = true;
+        if ((mFlags&FLAG_SHUTDOWN) != 0) {
+            pw.print(" (shutdown)");
+            partial = false;
+        }
+        if ((mFlags&FLAG_SYSPROPS) != 0) {
+            pw.print(" (sysprops)");
+            partial = false;
+        }
+        if ((mFlags&FLAG_COMPLETE) != 0) {
+            pw.print(" (complete)");
+            partial = false;
+        }
+        if (partial) {
+            pw.print(" (partial)");
+        }
+        if (mHasSwappedOutPss) {
+            pw.print(" (swapped-out-pss)");
+        }
+        pw.print(' ');
+        pw.print(mRuntime);
+        pw.println();
+    }
+
+    void dumpFilteredSummaryLocked(PrintWriter pw, String header, String prefix,
+            int[] screenStates, int[] memStates, int[] procStates,
+            int[] sortProcStates, long now, long totalTime, String reqPackage, boolean activeOnly) {
+        ArrayList<ProcessState> procs = collectProcessesLocked(screenStates, memStates,
+                procStates, sortProcStates, now, reqPackage, activeOnly);
+        if (procs.size() > 0) {
+            if (header != null) {
+                pw.println();
+                pw.println(header);
+            }
+            DumpUtils.dumpProcessSummaryLocked(pw, prefix, procs, screenStates, memStates,
+                    sortProcStates, now, totalTime);
+        }
+    }
+
+    public ArrayList<ProcessState> collectProcessesLocked(int[] screenStates, int[] memStates,
+            int[] procStates, int sortProcStates[], long now, String reqPackage,
+            boolean activeOnly) {
+        final ArraySet<ProcessState> foundProcs = new ArraySet<ProcessState>();
+        final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+        for (int ip=0; ip<pkgMap.size(); ip++) {
+            final String pkgName = pkgMap.keyAt(ip);
+            final SparseArray<SparseArray<PackageState>> procs = pkgMap.valueAt(ip);
+            for (int iu=0; iu<procs.size(); iu++) {
+                final SparseArray<PackageState> vpkgs = procs.valueAt(iu);
+                final int NVERS = vpkgs.size();
+                for (int iv=0; iv<NVERS; iv++) {
+                    final PackageState state = vpkgs.valueAt(iv);
+                    final int NPROCS = state.mProcesses.size();
+                    final boolean pkgMatch = reqPackage == null || reqPackage.equals(pkgName);
+                    for (int iproc=0; iproc<NPROCS; iproc++) {
+                        final ProcessState proc = state.mProcesses.valueAt(iproc);
+                        if (!pkgMatch && !reqPackage.equals(proc.getName())) {
+                            continue;
+                        }
+                        if (activeOnly && !proc.isInUse()) {
+                            continue;
+                        }
+                        foundProcs.add(proc.getCommonProcess());
+                    }
+                }
+            }
+        }
+        ArrayList<ProcessState> outProcs = new ArrayList<ProcessState>(foundProcs.size());
+        for (int i=0; i<foundProcs.size(); i++) {
+            ProcessState proc = foundProcs.valueAt(i);
+            if (proc.computeProcessTimeLocked(screenStates, memStates, procStates, now) > 0) {
+                outProcs.add(proc);
+                if (procStates != sortProcStates) {
+                    proc.computeProcessTimeLocked(screenStates, memStates, sortProcStates, now);
+                }
+            }
+        }
+        Collections.sort(outProcs, ProcessState.COMPARATOR);
+        return outProcs;
+    }
+
+    public void dumpCheckinLocked(PrintWriter pw, String reqPackage) {
+        final long now = SystemClock.uptimeMillis();
+        final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+        pw.println("vers,5");
+        pw.print("period,"); pw.print(mTimePeriodStartClockStr);
+        pw.print(","); pw.print(mTimePeriodStartRealtime); pw.print(",");
+        pw.print(mRunning ? SystemClock.elapsedRealtime() : mTimePeriodEndRealtime);
+        boolean partial = true;
+        if ((mFlags&FLAG_SHUTDOWN) != 0) {
+            pw.print(",shutdown");
+            partial = false;
+        }
+        if ((mFlags&FLAG_SYSPROPS) != 0) {
+            pw.print(",sysprops");
+            partial = false;
+        }
+        if ((mFlags&FLAG_COMPLETE) != 0) {
+            pw.print(",complete");
+            partial = false;
+        }
+        if (partial) {
+            pw.print(",partial");
+        }
+        if (mHasSwappedOutPss) {
+            pw.print(",swapped-out-pss");
+        }
+        pw.println();
+        pw.print("config,"); pw.println(mRuntime);
+        for (int ip=0; ip<pkgMap.size(); ip++) {
+            final String pkgName = pkgMap.keyAt(ip);
+            if (reqPackage != null && !reqPackage.equals(pkgName)) {
+                continue;
+            }
+            final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+            for (int iu=0; iu<uids.size(); iu++) {
+                final int uid = uids.keyAt(iu);
+                final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+                for (int iv=0; iv<vpkgs.size(); iv++) {
+                    final int vers = vpkgs.keyAt(iv);
+                    final PackageState pkgState = vpkgs.valueAt(iv);
+                    final int NPROCS = pkgState.mProcesses.size();
+                    final int NSRVS = pkgState.mServices.size();
+                    for (int iproc=0; iproc<NPROCS; iproc++) {
+                        ProcessState proc = pkgState.mProcesses.valueAt(iproc);
+                        proc.dumpPackageProcCheckin(pw, pkgName, uid, vers,
+                                pkgState.mProcesses.keyAt(iproc), now);
+                    }
+                    for (int isvc=0; isvc<NSRVS; isvc++) {
+                        final String serviceName = DumpUtils.collapseString(pkgName,
+                                pkgState.mServices.keyAt(isvc));
+                        final ServiceState svc = pkgState.mServices.valueAt(isvc);
+                        svc.dumpTimesCheckin(pw, pkgName, uid, vers, serviceName, now);
+                    }
+                }
+            }
+        }
+
+        ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap();
+        for (int ip=0; ip<procMap.size(); ip++) {
+            String procName = procMap.keyAt(ip);
+            SparseArray<ProcessState> uids = procMap.valueAt(ip);
+            for (int iu=0; iu<uids.size(); iu++) {
+                final int uid = uids.keyAt(iu);
+                final ProcessState procState = uids.valueAt(iu);
+                procState.dumpProcCheckin(pw, procName, uid, now);
+            }
+        }
+        pw.print("total");
+        DumpUtils.dumpAdjTimesCheckin(pw, ",", mMemFactorDurations, mMemFactor, mStartTime, now);
+        pw.println();
+        final int sysMemUsageCount = mSysMemUsage.getKeyCount();
+        if (sysMemUsageCount > 0) {
+            pw.print("sysmemusage");
+            for (int i=0; i<sysMemUsageCount; i++) {
+                final int key = mSysMemUsage.getKeyAt(i);
+                final int type = SparseMappingTable.getIdFromKey(key);
+                pw.print(",");
+                DumpUtils.printProcStateTag(pw, type);
+                for (int j=SYS_MEM_USAGE_SAMPLE_COUNT; j<SYS_MEM_USAGE_COUNT; j++) {
+                    if (j > SYS_MEM_USAGE_CACHED_MINIMUM) {
+                        pw.print(":");
+                    }
+                    pw.print(mSysMemUsage.getValue(key, j));
+                }
+            }
+        }
+        pw.println();
+        TotalMemoryUseCollection totalMem = new TotalMemoryUseCollection(ALL_SCREEN_ADJ,
+                ALL_MEM_ADJ);
+        computeTotalMemoryUse(totalMem, now);
+        pw.print("weights,");
+        pw.print(totalMem.totalTime);
+        pw.print(",");
+        pw.print(totalMem.sysMemCachedWeight);
+        pw.print(":");
+        pw.print(totalMem.sysMemSamples);
+        pw.print(",");
+        pw.print(totalMem.sysMemFreeWeight);
+        pw.print(":");
+        pw.print(totalMem.sysMemSamples);
+        pw.print(",");
+        pw.print(totalMem.sysMemZRamWeight);
+        pw.print(":");
+        pw.print(totalMem.sysMemSamples);
+        pw.print(",");
+        pw.print(totalMem.sysMemKernelWeight);
+        pw.print(":");
+        pw.print(totalMem.sysMemSamples);
+        pw.print(",");
+        pw.print(totalMem.sysMemNativeWeight);
+        pw.print(":");
+        pw.print(totalMem.sysMemSamples);
+        for (int i=0; i<STATE_COUNT; i++) {
+            pw.print(",");
+            pw.print(totalMem.processStateWeight[i]);
+            pw.print(":");
+            pw.print(totalMem.processStateSamples[i]);
+        }
+        pw.println();
+
+        final int NPAGETYPES = mPageTypeLabels.size();
+        for (int i=0; i<NPAGETYPES; i++) {
+            pw.print("availablepages,");
+            pw.print(mPageTypeLabels.get(i));
+            pw.print(",");
+            pw.print(mPageTypeZones.get(i));
+            pw.print(",");
+            final int[] sizes = mPageTypeSizes.get(i);
+            final int N = sizes == null ? 0 : sizes.length;
+            for (int j=0; j<N; j++) {
+                if (j != 0) {
+                    pw.print(",");
+                }
+                pw.print(sizes[j]);
+            }
+            pw.println();
+        }
+    }
+
+
+    final public static class ProcessStateHolder {
+        public final int appVersion;
+        public ProcessState state;
+
+        public ProcessStateHolder(int _appVersion) {
+            appVersion = _appVersion;
+        }
+    }
+
+    public static final class PackageState {
+        public final ArrayMap<String, ProcessState> mProcesses
+                = new ArrayMap<String, ProcessState>();
+        public final ArrayMap<String, ServiceState> mServices
+                = new ArrayMap<String, ServiceState>();
+        public final String mPackageName;
+        public final int mUid;
+
+        public PackageState(String packageName, int uid) {
+            mUid = uid;
+            mPackageName = packageName;
+        }
+    }
+
+    public static final class ProcessDataCollection {
+        final int[] screenStates;
+        final int[] memStates;
+        final int[] procStates;
+
+        public long totalTime;
+        public long numPss;
+        public long minPss;
+        public long avgPss;
+        public long maxPss;
+        public long minUss;
+        public long avgUss;
+        public long maxUss;
+
+        public ProcessDataCollection(int[] _screenStates, int[] _memStates, int[] _procStates) {
+            screenStates = _screenStates;
+            memStates = _memStates;
+            procStates = _procStates;
+        }
+
+        void print(PrintWriter pw, long overallTime, boolean full) {
+            if (totalTime > overallTime) {
+                pw.print("*");
+            }
+            DumpUtils.printPercent(pw, (double) totalTime / (double) overallTime);
+            if (numPss > 0) {
+                pw.print(" (");
+                DebugUtils.printSizeValue(pw, minPss * 1024);
+                pw.print("-");
+                DebugUtils.printSizeValue(pw, avgPss * 1024);
+                pw.print("-");
+                DebugUtils.printSizeValue(pw, maxPss * 1024);
+                pw.print("/");
+                DebugUtils.printSizeValue(pw, minUss * 1024);
+                pw.print("-");
+                DebugUtils.printSizeValue(pw, avgUss * 1024);
+                pw.print("-");
+                DebugUtils.printSizeValue(pw, maxUss * 1024);
+                if (full) {
+                    pw.print(" over ");
+                    pw.print(numPss);
+                }
+                pw.print(")");
+            }
+        }
+    }
+
+    public static class TotalMemoryUseCollection {
+        final int[] screenStates;
+        final int[] memStates;
+
+        public TotalMemoryUseCollection(int[] _screenStates, int[] _memStates) {
+            screenStates = _screenStates;
+            memStates = _memStates;
+        }
+
+        public long totalTime;
+        public long[] processStatePss = new long[STATE_COUNT];
+        public double[] processStateWeight = new double[STATE_COUNT];
+        public long[] processStateTime = new long[STATE_COUNT];
+        public int[] processStateSamples = new int[STATE_COUNT];
+        public long[] sysMemUsage = new long[SYS_MEM_USAGE_COUNT];
+        public double sysMemCachedWeight;
+        public double sysMemFreeWeight;
+        public double sysMemZRamWeight;
+        public double sysMemKernelWeight;
+        public double sysMemNativeWeight;
+        public int sysMemSamples;
+        public boolean hasSwappedOutPss;
+    }
+
+}
diff --git a/com/android/internal/app/procstats/PssTable.java b/com/android/internal/app/procstats/PssTable.java
new file mode 100644
index 0000000..de5f673
--- /dev/null
+++ b/com/android/internal/app/procstats/PssTable.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal.app.procstats;
+
+import static com.android.internal.app.procstats.ProcessStats.PSS_SAMPLE_COUNT;
+import static com.android.internal.app.procstats.ProcessStats.PSS_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.PSS_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_USS_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_USS_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.PSS_USS_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.PSS_COUNT;
+
+/**
+ * Class to accumulate PSS data.
+ */
+public class PssTable extends SparseMappingTable.Table {
+    /**
+     * Construct the PssTable with 'tableData' as backing store
+     * for the longs data.
+     */
+    public PssTable(SparseMappingTable tableData) {
+        super(tableData);
+    }
+
+    /**
+     * Merge the the values from the other table into this one.
+     */
+    public void mergeStats(PssTable that) {
+        final int N = that.getKeyCount();
+        for (int i=0; i<N; i++) {
+            final int key = that.getKeyAt(i);
+            final int state = SparseMappingTable.getIdFromKey(key);
+            mergeStats(state, (int)that.getValue(key, PSS_SAMPLE_COUNT),
+                    that.getValue(key, PSS_MINIMUM),
+                    that.getValue(key, PSS_AVERAGE),
+                    that.getValue(key, PSS_MAXIMUM),
+                    that.getValue(key, PSS_USS_MINIMUM),
+                    that.getValue(key, PSS_USS_AVERAGE),
+                    that.getValue(key, PSS_USS_MAXIMUM));
+        }
+    }
+
+    /**
+     * Merge the supplied PSS data in.  The new min pss will be the minimum of the existing
+     * one and the new one, the average will now incorporate the new average, etc.
+     */
+    public void mergeStats(int state, int inCount, long minPss, long avgPss, long maxPss,
+            long minUss, long avgUss, long maxUss) {
+        final int key = getOrAddKey((byte)state, PSS_COUNT);
+        final long count = getValue(key, PSS_SAMPLE_COUNT);
+        if (count == 0) {
+            setValue(key, PSS_SAMPLE_COUNT, inCount);
+            setValue(key, PSS_MINIMUM, minPss);
+            setValue(key, PSS_AVERAGE, avgPss);
+            setValue(key, PSS_MAXIMUM, maxPss);
+            setValue(key, PSS_USS_MINIMUM, minUss);
+            setValue(key, PSS_USS_AVERAGE, avgUss);
+            setValue(key, PSS_USS_MAXIMUM, maxUss);
+        } else {
+            setValue(key, PSS_SAMPLE_COUNT, count + inCount);
+
+            long val;
+
+            val = getValue(key, PSS_MINIMUM);
+            if (val > minPss) {
+                setValue(key, PSS_MINIMUM, minPss);
+            }
+
+            val = getValue(key, PSS_AVERAGE);
+            setValue(key, PSS_AVERAGE,
+                    (long)(((val*(double)count)+(avgPss*(double)inCount)) / (count+inCount)));
+
+            val = getValue(key, PSS_MAXIMUM);
+            if (val < maxPss) {
+                setValue(key, PSS_MAXIMUM, maxPss);
+            }
+
+            val = getValue(key, PSS_USS_MINIMUM);
+            if (val > minUss) {
+                setValue(key, PSS_USS_MINIMUM, minUss);
+            }
+
+            val = getValue(key, PSS_USS_AVERAGE);
+            setValue(key, PSS_USS_AVERAGE,
+                    (long)(((val*(double)count)+(avgUss*(double)inCount)) / (count+inCount)));
+
+            val = getValue(key, PSS_USS_MAXIMUM);
+            if (val < maxUss) {
+                setValue(key, PSS_USS_MAXIMUM, maxUss);
+            }
+        }
+    }
+}
diff --git a/com/android/internal/app/procstats/ServiceState.java b/com/android/internal/app/procstats/ServiceState.java
new file mode 100644
index 0000000..2e11c43
--- /dev/null
+++ b/com/android/internal/app/procstats/ServiceState.java
@@ -0,0 +1,502 @@
+/*
+ * 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.internal.app.procstats;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.app.procstats.ProcessStats;
+import static com.android.internal.app.procstats.ProcessStats.STATE_NOTHING;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Objects;
+
+public final class ServiceState {
+    private static final String TAG = "ProcessStats";
+    private static final boolean DEBUG = false;
+
+    public static final int SERVICE_RUN = 0;
+    public static final int SERVICE_STARTED = 1;
+    public static final int SERVICE_BOUND = 2;
+    public static final int SERVICE_EXEC = 3;
+    public static final int SERVICE_COUNT = 4;
+
+    private final String mPackage;
+    private final String mProcessName;
+    private final String mName;
+    private final DurationsTable mDurations;
+
+    private ProcessState mProc;
+    private Object mOwner;
+
+    private int mRunCount;
+    private int mRunState = STATE_NOTHING;
+    private long mRunStartTime;
+
+    private boolean mStarted;
+    private boolean mRestarting;
+    private int mStartedCount;
+    private int mStartedState = STATE_NOTHING;
+    private long mStartedStartTime;
+
+    private int mBoundCount;
+    private int mBoundState = STATE_NOTHING;
+    private long mBoundStartTime;
+
+    private int mExecCount;
+    private int mExecState = STATE_NOTHING;
+    private long mExecStartTime;
+
+    public ServiceState(ProcessStats processStats, String pkg, String name,
+            String processName, ProcessState proc) {
+        mPackage = pkg;
+        mName = name;
+        mProcessName = processName;
+        mProc = proc;
+        mDurations = new DurationsTable(processStats.mTableData);
+    }
+
+    public String getPackage() {
+        return mPackage;
+    }
+    
+    public String getProcessName() {
+        return mProcessName;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public ProcessState getProcess() {
+        return mProc;
+    }
+
+    public void setProcess(ProcessState proc) {
+        mProc = proc;
+    }
+
+    public void setMemFactor(int memFactor, long now) {
+        if (isRestarting()) {
+            setRestarting(true, memFactor, now);
+        } else if (isInUse()) {
+            if (mStartedState != ProcessStats.STATE_NOTHING) {
+                setStarted(true, memFactor, now);
+            }
+            if (mBoundState != ProcessStats.STATE_NOTHING) {
+                setBound(true, memFactor, now);
+            }
+            if (mExecState != ProcessStats.STATE_NOTHING) {
+                setExecuting(true, memFactor, now);
+            }
+        }
+    }
+
+    public void applyNewOwner(Object newOwner) {
+        if (mOwner != newOwner) {
+            if (mOwner == null) {
+                mOwner = newOwner;
+                mProc.incActiveServices(mName);
+            } else {
+                // There was already an old owner, reset this object for its
+                // new owner.
+                mOwner = newOwner;
+                if (mStarted || mBoundState != STATE_NOTHING || mExecState != STATE_NOTHING) {
+                    long now = SystemClock.uptimeMillis();
+                    if (mStarted) {
+                        if (DEBUG) Slog.d(TAG, "Service has new owner " + newOwner
+                                + " from " + mOwner + " while started: pkg="
+                                + mPackage + " service=" + mName + " proc=" + mProc);
+                        setStarted(false, 0, now);
+                    }
+                    if (mBoundState != STATE_NOTHING) {
+                        if (DEBUG) Slog.d(TAG, "Service has new owner " + newOwner
+                                + " from " + mOwner + " while bound: pkg="
+                                + mPackage + " service=" + mName + " proc=" + mProc);
+                        setBound(false, 0, now);
+                    }
+                    if (mExecState != STATE_NOTHING) {
+                        if (DEBUG) Slog.d(TAG, "Service has new owner " + newOwner
+                                + " from " + mOwner + " while executing: pkg="
+                                + mPackage + " service=" + mName + " proc=" + mProc);
+                        setExecuting(false, 0, now);
+                    }
+                }
+            }
+        }
+    }
+
+    public void clearCurrentOwner(Object owner, boolean silently) {
+        if (mOwner == owner) {
+            mProc.decActiveServices(mName);
+            if (mStarted || mBoundState != STATE_NOTHING || mExecState != STATE_NOTHING) {
+                long now = SystemClock.uptimeMillis();
+                if (mStarted) {
+                    if (!silently) {
+                        Slog.wtfStack(TAG, "Service owner " + owner
+                                + " cleared while started: pkg=" + mPackage + " service="
+                                + mName + " proc=" + mProc);
+                    }
+                    setStarted(false, 0, now);
+                }
+                if (mBoundState != STATE_NOTHING) {
+                    if (!silently) {
+                        Slog.wtfStack(TAG, "Service owner " + owner
+                                + " cleared while bound: pkg=" + mPackage + " service="
+                                + mName + " proc=" + mProc);
+                    }
+                    setBound(false, 0, now);
+                }
+                if (mExecState != STATE_NOTHING) {
+                    if (!silently) {
+                        Slog.wtfStack(TAG, "Service owner " + owner
+                                + " cleared while exec: pkg=" + mPackage + " service="
+                                + mName + " proc=" + mProc);
+                    }
+                    setExecuting(false, 0, now);
+                }
+            }
+            mOwner = null;
+        }
+    }
+
+    public boolean isInUse() {
+        return mOwner != null || mRestarting;
+    }
+
+    public boolean isRestarting() {
+        return mRestarting;
+    }
+
+    public void add(ServiceState other) {
+        mDurations.addDurations(other.mDurations);
+        mRunCount += other.mRunCount;
+        mStartedCount += other.mStartedCount;
+        mBoundCount += other.mBoundCount;
+        mExecCount += other.mExecCount;
+    }
+
+    public void resetSafely(long now) {
+        mDurations.resetTable();
+        mRunCount = mRunState != STATE_NOTHING ? 1 : 0;
+        mStartedCount = mStartedState != STATE_NOTHING ? 1 : 0;
+        mBoundCount = mBoundState != STATE_NOTHING ? 1 : 0;
+        mExecCount = mExecState != STATE_NOTHING ? 1 : 0;
+        mRunStartTime = mStartedStartTime = mBoundStartTime = mExecStartTime = now;
+    }
+
+    public void writeToParcel(Parcel out, long now) {
+        mDurations.writeToParcel(out);
+        out.writeInt(mRunCount);
+        out.writeInt(mStartedCount);
+        out.writeInt(mBoundCount);
+        out.writeInt(mExecCount);
+    }
+
+    public boolean readFromParcel(Parcel in) {
+        if (!mDurations.readFromParcel(in)) {
+            return false;
+        }
+        mRunCount = in.readInt();
+        mStartedCount = in.readInt();
+        mBoundCount = in.readInt();
+        mExecCount = in.readInt();
+        return true;
+    }
+
+    public void commitStateTime(long now) {
+        if (mRunState != STATE_NOTHING) {
+            mDurations.addDuration(SERVICE_RUN + (mRunState*SERVICE_COUNT),
+                    now - mRunStartTime);
+            mRunStartTime = now;
+        }
+        if (mStartedState != STATE_NOTHING) {
+            mDurations.addDuration(SERVICE_STARTED + (mStartedState*SERVICE_COUNT),
+                    now - mStartedStartTime);
+            mStartedStartTime = now;
+        }
+        if (mBoundState != STATE_NOTHING) {
+            mDurations.addDuration(SERVICE_BOUND + (mBoundState*SERVICE_COUNT),
+                    now - mBoundStartTime);
+            mBoundStartTime = now;
+        }
+        if (mExecState != STATE_NOTHING) {
+            mDurations.addDuration(SERVICE_EXEC + (mExecState*SERVICE_COUNT),
+                    now - mExecStartTime);
+            mExecStartTime = now;
+        }
+    }
+
+    private void updateRunning(int memFactor, long now) {
+        final int state = (mStartedState != STATE_NOTHING || mBoundState != STATE_NOTHING
+                || mExecState != STATE_NOTHING) ? memFactor : STATE_NOTHING;
+        if (mRunState != state) {
+            if (mRunState != STATE_NOTHING) {
+                mDurations.addDuration(SERVICE_RUN + (mRunState*SERVICE_COUNT),
+                        now - mRunStartTime);
+            } else if (state != STATE_NOTHING) {
+                mRunCount++;
+            }
+            mRunState = state;
+            mRunStartTime = now;
+        }
+    }
+
+    public void setStarted(boolean started, int memFactor, long now) {
+        if (mOwner == null) {
+            Slog.wtf(TAG, "Starting service " + this + " without owner");
+        }
+        mStarted = started;
+        updateStartedState(memFactor, now);
+    }
+
+    public void setRestarting(boolean restarting, int memFactor, long now) {
+        mRestarting = restarting;
+        updateStartedState(memFactor, now);
+    }
+
+    public void updateStartedState(int memFactor, long now) {
+        final boolean wasStarted = mStartedState != STATE_NOTHING;
+        final boolean started = mStarted || mRestarting;
+        final int state = started ? memFactor : STATE_NOTHING;
+        if (mStartedState != state) {
+            if (mStartedState != STATE_NOTHING) {
+                mDurations.addDuration(SERVICE_STARTED + (mStartedState*SERVICE_COUNT),
+                        now - mStartedStartTime);
+            } else if (started) {
+                mStartedCount++;
+            }
+            mStartedState = state;
+            mStartedStartTime = now;
+            mProc = mProc.pullFixedProc(mPackage);
+            if (wasStarted != started) {
+                if (started) {
+                    mProc.incStartedServices(memFactor, now, mName);
+                } else {
+                    mProc.decStartedServices(memFactor, now, mName);
+                }
+            }
+            updateRunning(memFactor, now);
+        }
+    }
+
+    public void setBound(boolean bound, int memFactor, long now) {
+        if (mOwner == null) {
+            Slog.wtf(TAG, "Binding service " + this + " without owner");
+        }
+        final int state = bound ? memFactor : STATE_NOTHING;
+        if (mBoundState != state) {
+            if (mBoundState != STATE_NOTHING) {
+                mDurations.addDuration(SERVICE_BOUND + (mBoundState*SERVICE_COUNT),
+                        now - mBoundStartTime);
+            } else if (bound) {
+                mBoundCount++;
+            }
+            mBoundState = state;
+            mBoundStartTime = now;
+            updateRunning(memFactor, now);
+        }
+    }
+
+    public void setExecuting(boolean executing, int memFactor, long now) {
+        if (mOwner == null) {
+            Slog.wtf(TAG, "Executing service " + this + " without owner");
+        }
+        final int state = executing ? memFactor : STATE_NOTHING;
+        if (mExecState != state) {
+            if (mExecState != STATE_NOTHING) {
+                mDurations.addDuration(SERVICE_EXEC + (mExecState*SERVICE_COUNT),
+                        now - mExecStartTime);
+            } else if (executing) {
+                mExecCount++;
+            }
+            mExecState = state;
+            mExecStartTime = now;
+            updateRunning(memFactor, now);
+        }
+    }
+
+    public long getDuration(int opType, int curState, long startTime, int memFactor,
+            long now) {
+        int state = opType + (memFactor*SERVICE_COUNT);
+        long time = mDurations.getValueForId((byte)state);
+        if (curState == memFactor) {
+            time += now - startTime;
+        }
+        return time;
+    }
+
+    public void dumpStats(PrintWriter pw, String prefix, String prefixInner, String headerPrefix,
+            long now, long totalTime, boolean dumpSummary, boolean dumpAll) {
+        dumpStats(pw, prefix, prefixInner, headerPrefix, "Running",
+                mRunCount, ServiceState.SERVICE_RUN, mRunState,
+                mRunStartTime, now, totalTime, !dumpSummary || dumpAll);
+        dumpStats(pw, prefix, prefixInner, headerPrefix, "Started",
+                mStartedCount, ServiceState.SERVICE_STARTED, mStartedState,
+                mStartedStartTime, now, totalTime, !dumpSummary || dumpAll);
+        dumpStats(pw, prefix, prefixInner, headerPrefix, "Bound",
+                mBoundCount, ServiceState.SERVICE_BOUND, mBoundState,
+                mBoundStartTime, now, totalTime, !dumpSummary || dumpAll);
+        dumpStats(pw, prefix, prefixInner, headerPrefix, "Executing",
+                mExecCount, ServiceState.SERVICE_EXEC, mExecState,
+                mExecStartTime, now, totalTime, !dumpSummary || dumpAll);
+        if (dumpAll) {
+            if (mOwner != null) {
+                pw.print("        mOwner="); pw.println(mOwner);
+            }
+            if (mStarted || mRestarting) {
+                pw.print("        mStarted="); pw.print(mStarted);
+                pw.print(" mRestarting="); pw.println(mRestarting);
+            }
+        }
+    }
+
+    private void dumpStats(PrintWriter pw, String prefix, String prefixInner,
+            String headerPrefix, String header,
+            int count, int serviceType, int state, long startTime, long now, long totalTime,
+            boolean dumpAll) {
+        if (count != 0) {
+            if (dumpAll) {
+                pw.print(prefix); pw.print(header);
+                pw.print(" op count "); pw.print(count); pw.println(":");
+                dumpTime(pw, prefixInner, serviceType, state, startTime, now);
+            } else {
+                long myTime = dumpTime(null, null, serviceType, state, startTime, now);
+                pw.print(prefix); pw.print(headerPrefix); pw.print(header);
+                pw.print(" count "); pw.print(count);
+                pw.print(" / time ");
+                DumpUtils.printPercent(pw, (double)myTime/(double)totalTime);
+                pw.println();
+            }
+        }
+    }
+
+    public long dumpTime(PrintWriter pw, String prefix,
+            int serviceType, int curState, long curStartTime, long now) {
+        long totalTime = 0;
+        int printedScreen = -1;
+        for (int iscreen=0; iscreen<ProcessStats.ADJ_COUNT; iscreen+=ProcessStats.ADJ_SCREEN_MOD) {
+            int printedMem = -1;
+            for (int imem=0; imem<ProcessStats.ADJ_MEM_FACTOR_COUNT; imem++) {
+                int state = imem+iscreen;
+                long time = getDuration(serviceType, curState, curStartTime, state, now);
+                String running = "";
+                if (curState == state && pw != null) {
+                    running = " (running)";
+                }
+                if (time != 0) {
+                    if (pw != null) {
+                        pw.print(prefix);
+                        DumpUtils.printScreenLabel(pw, printedScreen != iscreen
+                                ? iscreen : STATE_NOTHING);
+                        printedScreen = iscreen;
+                        DumpUtils.printMemLabel(pw, printedMem != imem ? imem : STATE_NOTHING,
+                                (char)0);
+                        printedMem = imem;
+                        pw.print(": ");
+                        TimeUtils.formatDuration(time, pw); pw.println(running);
+                    }
+                    totalTime += time;
+                }
+            }
+        }
+        if (totalTime != 0 && pw != null) {
+            pw.print(prefix);
+            pw.print("    TOTAL: ");
+            TimeUtils.formatDuration(totalTime, pw);
+            pw.println();
+        }
+        return totalTime;
+    }
+
+    public void dumpTimesCheckin(PrintWriter pw, String pkgName, int uid, int vers,
+            String serviceName, long now) {
+        dumpTimeCheckin(pw, "pkgsvc-run", pkgName, uid, vers, serviceName,
+                ServiceState.SERVICE_RUN, mRunCount, mRunState, mRunStartTime, now);
+        dumpTimeCheckin(pw, "pkgsvc-start", pkgName, uid, vers, serviceName,
+                ServiceState.SERVICE_STARTED, mStartedCount, mStartedState, mStartedStartTime, now);
+        dumpTimeCheckin(pw, "pkgsvc-bound", pkgName, uid, vers, serviceName,
+                ServiceState.SERVICE_BOUND, mBoundCount, mBoundState, mBoundStartTime, now);
+        dumpTimeCheckin(pw, "pkgsvc-exec", pkgName, uid, vers, serviceName,
+                ServiceState.SERVICE_EXEC, mExecCount, mExecState, mExecStartTime, now);
+    }
+
+    private void dumpTimeCheckin(PrintWriter pw, String label, String packageName,
+            int uid, int vers, String serviceName, int serviceType, int opCount,
+            int curState, long curStartTime, long now) {
+        if (opCount <= 0) {
+            return;
+        }
+        pw.print(label);
+        pw.print(",");
+        pw.print(packageName);
+        pw.print(",");
+        pw.print(uid);
+        pw.print(",");
+        pw.print(vers);
+        pw.print(",");
+        pw.print(serviceName);
+        pw.print(",");
+        pw.print(opCount);
+        boolean didCurState = false;
+        final int N = mDurations.getKeyCount();
+        for (int i=0; i<N; i++) {
+            final int key = mDurations.getKeyAt(i);
+            long time = mDurations.getValue(key);
+            int type = SparseMappingTable.getIdFromKey(key);
+            int memFactor = type / ServiceState.SERVICE_COUNT;
+            type %= ServiceState.SERVICE_COUNT;
+            if (type != serviceType) {
+                continue;
+            }
+            if (curState == memFactor) {
+                didCurState = true;
+                time += now - curStartTime;
+            }
+            DumpUtils.printAdjTagAndValue(pw, memFactor, time);
+        }
+        if (!didCurState && curState != STATE_NOTHING) {
+            DumpUtils.printAdjTagAndValue(pw, curState, now - curStartTime);
+        }
+        pw.println();
+    }
+
+
+    public String toString() {
+        return "ServiceState{" + Integer.toHexString(System.identityHashCode(this))
+                + " " + mName + " pkg=" + mPackage + " proc="
+                + Integer.toHexString(System.identityHashCode(this)) + "}";
+    }
+}
diff --git a/com/android/internal/app/procstats/SparseMappingTable.java b/com/android/internal/app/procstats/SparseMappingTable.java
new file mode 100644
index 0000000..956ce99
--- /dev/null
+++ b/com/android/internal/app/procstats/SparseMappingTable.java
@@ -0,0 +1,653 @@
+/*
+ * 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.internal.app.procstats;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.util.Slog;
+import libcore.util.EmptyArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import com.android.internal.util.GrowingArrayUtils;
+
+/**
+ * Class that contains a set of tables mapping byte ids to long values.
+ *
+ * This class is used to store the ProcessStats data.  This data happens to be
+ * a set of very sparse tables, that is mostly append or overwrite, with infrequent
+ * resets of the data.
+ *
+ * Data is stored as a list of large long[] arrays containing the actual values.  There are a
+ * set of Table objects that each contain a small array mapping the byte IDs to a position
+ * in the larger arrays.
+ *
+ * The data itself is either a single long value or a range of long values which are always
+ * stored continguously in one of the long arrays. When the caller allocates a slot with
+ * getOrAddKey, an int key is returned.  That key can be re-retreived with getKey without
+ * allocating the value.  The data can then be set or retrieved with that key.
+ */
+public class SparseMappingTable {
+    private static final String TAG = "SparseMappingTable";
+
+    // How big each array is.
+    public static final int ARRAY_SIZE = 4096;
+
+    public static final int INVALID_KEY = 0xffffffff;
+
+    // Where the "type"/"state" part of the data appears in an offset integer.
+    private static final int ID_SHIFT = 0;
+    private static final int ID_MASK = 0xff;
+    // Where the "which array" part of the data appears in an offset integer.
+    private static final int ARRAY_SHIFT = 8;
+    private static final int ARRAY_MASK = 0xff;
+    // Where the "index into array" part of the data appears in an offset integer.
+    private static final int INDEX_SHIFT = 16;
+    private static final int INDEX_MASK = 0xffff;
+
+    private int mSequence;
+    private int mNextIndex;
+    private final ArrayList<long[]> mLongs = new ArrayList<long[]>();
+
+    /**
+     * A table of data as stored in a SparseMappingTable.
+     */
+    public static class Table {
+        private SparseMappingTable mParent;
+        private int mSequence = 1;
+        private int[] mTable;
+        private int mSize;
+
+        public Table(SparseMappingTable parent) {
+            mParent = parent;
+            mSequence = parent.mSequence;
+        }
+
+        /**
+         * Pulls the data from 'copyFrom' and stores it in our own longs table.
+         *
+         * @param copyFrom   The Table to copy from
+         * @param valueCount The number of values to copy for each key
+         */
+        public void copyFrom(Table copyFrom, int valueCount) {
+            mTable = null;
+            mSize = 0;
+
+            final int N = copyFrom.getKeyCount();
+            for (int i=0; i<N; i++) {
+                final int theirKey = copyFrom.getKeyAt(i);
+                final long[] theirLongs = copyFrom.mParent.mLongs.get(getArrayFromKey(theirKey));
+
+                final byte id = SparseMappingTable.getIdFromKey(theirKey);
+
+                final int myKey = this.getOrAddKey((byte)id, valueCount);
+                final long[] myLongs = mParent.mLongs.get(getArrayFromKey(myKey));
+
+                System.arraycopy(theirLongs, getIndexFromKey(theirKey),
+                        myLongs, getIndexFromKey(myKey), valueCount);
+            }
+        }
+
+        /**
+         * Allocates data in the buffer, and stores that key in the mapping for this
+         * table.
+         *
+         * @param id    The id of the item (will be used in making the key)
+         * @param count The number of bytes to allocate.  Must be less than
+         *              SparseMappingTable.ARRAY_SIZE.
+         *
+         * @return The 'key' for this data value, which contains both the id itself
+         *         and the location in the long arrays that the data is actually stored
+         *         but should be considered opaque to the caller.
+         */
+        public int getOrAddKey(byte id, int count) {
+            assertConsistency();
+
+            final int idx = binarySearch(id);
+            if (idx >= 0) {
+                // Found
+                return mTable[idx];
+            } else {
+                // Not found. Need to allocate it.
+
+                // Get an array with enough space to store 'count' values.
+                final ArrayList<long[]> list = mParent.mLongs;
+                int whichArray = list.size()-1;
+                long[] array = list.get(whichArray);
+                if (mParent.mNextIndex + count > array.length) {
+                    // if it won't fit then make a new array.
+                    array = new long[ARRAY_SIZE];
+                    list.add(array);
+                    whichArray++;
+                    mParent.mNextIndex = 0;
+                }
+
+                // The key is a combination of whichArray, which index in that array, and
+                // the table value itself, which will be used for lookup
+                final int key = (whichArray << ARRAY_SHIFT)
+                        | (mParent.mNextIndex << INDEX_SHIFT)
+                        | (((int)id) << ID_SHIFT);
+
+                mParent.mNextIndex += count;
+
+                // Store the key in the sparse lookup table for this Table object.
+                mTable = GrowingArrayUtils.insert(mTable != null ? mTable : EmptyArray.INT,
+                        mSize, ~idx, key);
+                mSize++;
+
+                return key;
+            }
+        }
+
+        /**
+         * Looks up a key in the table.
+         *
+         * @return The key from this table or INVALID_KEY if the id is not found.
+         */
+        public int getKey(byte id) {
+            assertConsistency();
+
+            final int idx = binarySearch(id);
+            if (idx >= 0) {
+                return mTable[idx];
+            } else {
+                return INVALID_KEY;
+            }
+        }
+
+        /**
+         * Get the value for the given key and offset from that key.
+         *
+         * @param key   A key as obtained from getKey or getOrAddKey.
+         * @param value The value to set.
+         */
+        public long getValue(int key) {
+            return getValue(key, 0);
+        }
+
+        /**
+         * Get the value for the given key and offset from that key.
+         *
+         * @param key   A key as obtained from getKey or getOrAddKey.
+         * @param index The offset from that key.  Must be less than the count
+         *              provided to getOrAddKey when the space was allocated.
+         * @param value The value to set.
+         *
+         * @return the value, or 0 in case of an error
+         */
+        public long getValue(int key, int index) {
+            assertConsistency();
+
+            try {
+                final long[] array = mParent.mLongs.get(getArrayFromKey(key));
+                return array[getIndexFromKey(key) + index];
+            } catch (IndexOutOfBoundsException ex) {
+                logOrThrow("key=0x" + Integer.toHexString(key)
+                        + " index=" + index + " -- " + dumpInternalState(), ex);
+                return 0;
+            }
+        }
+
+        /**
+         * Set the value for the given id at offset 0 from that id.
+         * If the id is not found, return 0 instead.
+         *
+         * @param id    The id of the item.
+         */
+        public long getValueForId(byte id) {
+            return getValueForId(id, 0);
+        }
+
+        /**
+         * Set the value for the given id and index offset from that id.
+         * If the id is not found, return 0 instead.
+         *
+         * @param id    The id of the item.
+         * @param index The offset from that key.  Must be less than the count
+         *              provided to getOrAddKey when the space was allocated.
+         */
+        public long getValueForId(byte id, int index) {
+            assertConsistency();
+
+            final int idx = binarySearch(id);
+            if (idx >= 0) {
+                final int key = mTable[idx];
+                try {
+                    final long[] array = mParent.mLongs.get(getArrayFromKey(key));
+                    return array[getIndexFromKey(key) + index];
+                } catch (IndexOutOfBoundsException ex) {
+                    logOrThrow("id=0x" + Integer.toHexString(id) + " idx=" + idx
+                            + " key=0x" + Integer.toHexString(key) + " index=" + index
+                            + " -- " + dumpInternalState(), ex);
+                    return 0;
+                }
+            } else {
+                return 0;
+            }
+        }
+
+        /**
+         * Return the raw storage long[] for the given key.
+         */
+        public long[] getArrayForKey(int key) {
+            assertConsistency();
+
+            return mParent.mLongs.get(getArrayFromKey(key));
+        }
+
+        /**
+         * Set the value for the given key and offset from that key.
+         *
+         * @param key   A key as obtained from getKey or getOrAddKey.
+         * @param value The value to set.
+         */
+        public void setValue(int key, long value) {
+            setValue(key, 0, value);
+        }
+
+        /**
+         * Set the value for the given key and offset from that key.
+         *
+         * @param key   A key as obtained from getKey or getOrAddKey.
+         * @param index The offset from that key.  Must be less than the count
+         *              provided to getOrAddKey when the space was allocated.
+         * @param value The value to set.
+         */
+        public void setValue(int key, int index, long value) {
+            assertConsistency();
+
+            if (value < 0) {
+                logOrThrow("can't store negative values"
+                        + " key=0x" + Integer.toHexString(key)
+                        + " index=" + index + " value=" + value
+                        + " -- " + dumpInternalState());
+                return;
+            }
+
+            try {
+                final long[] array = mParent.mLongs.get(getArrayFromKey(key));
+                array[getIndexFromKey(key) + index] = value;
+            } catch (IndexOutOfBoundsException ex) {
+                logOrThrow("key=0x" + Integer.toHexString(key)
+                        + " index=" + index + " value=" + value
+                        + " -- " + dumpInternalState(), ex);
+                return;
+            }
+        }
+
+        /**
+         * Clear out the table, and reset the sequence numbers so future writes
+         * without allocations will assert.
+         */
+        public void resetTable() {
+            // Clear out our table.
+            mTable = null;
+            mSize = 0;
+
+            // Reset our sequence number.  This will make all read/write calls
+            // start to fail, and then when we re-allocate it will be re-synced
+            // to that of mParent.
+            mSequence = mParent.mSequence;
+        }
+
+        /**
+         * Write the keys stored in the table to the parcel. The parent must
+         * be separately written. Does not save the actual data.
+         */
+        public void writeToParcel(Parcel out) {
+            out.writeInt(mSequence);
+            out.writeInt(mSize);
+            for (int i=0; i<mSize; i++) {
+                out.writeInt(mTable[i]);
+            }
+        }
+
+        /**
+         * Read the keys from the parcel. The parent (with its long array) must
+         * have been previously initialized.
+         */
+        public boolean readFromParcel(Parcel in) {
+            // Read the state
+            mSequence = in.readInt();
+            mSize = in.readInt();
+            if (mSize != 0) {
+                mTable = new int[mSize];
+                for (int i=0; i<mSize; i++) {
+                    mTable[i] = in.readInt();
+                }
+            } else {
+                mTable = null;
+            }
+
+            // Make sure we're all healthy
+            if (validateKeys(true)) {
+                return true;
+            } else {
+                // Clear it out
+                mSize = 0;
+                mTable = null;
+                return false;
+            }
+        }
+
+        /**
+         * Return the number of keys that have been added to this Table.
+         */
+        public int getKeyCount() {
+            return mSize;
+        }
+
+        /**
+         * Get the key at the given index in our table.
+         */
+        public int getKeyAt(int i) {
+            return mTable[i];
+        }
+
+        /**
+         * Throw an exception if one of a variety of internal consistency checks fails.
+         */
+        private void assertConsistency() {
+            // Something with this checking isn't working and is triggering
+            // more problems than it's helping to debug.
+            //   Original bug: b/27045736
+            //   New bug: b/27960286
+            if (false) {
+                // Assert that our sequence number matches mParent's.  If it isn't that means
+                // we have been reset and our.  If our sequence is UNITIALIZED_SEQUENCE, then 
+                // it's possible that everything is working fine and we just haven't been
+                // written to since the last resetTable().
+                if (mSequence != mParent.mSequence) {
+                    if (mSequence < mParent.mSequence) {
+                        logOrThrow("Sequence mismatch. SparseMappingTable.reset()"
+                                + " called but not Table.resetTable() -- "
+                                + dumpInternalState());
+                        return;
+                    } else if (mSequence > mParent.mSequence) {
+                        logOrThrow("Sequence mismatch. Table.resetTable()"
+                                + " called but not SparseMappingTable.reset() -- "
+                                + dumpInternalState());
+                        return;
+                    }
+                }
+            }
+        }
+
+        /**
+         * Finds the 'id' inside the array of length size (physical size of the array
+         * is not used).
+         *
+         * @return The index of the array, or the bitwise not (~index) of where it
+         * would go if you wanted to insert 'id' into the array.
+         */
+        private int binarySearch(byte id) {
+            int lo = 0;
+            int hi = mSize - 1;
+
+            while (lo <= hi) {
+                int mid = (lo + hi) >>> 1;
+                byte midId = (byte)((mTable[mid] >> ID_SHIFT) & ID_MASK);
+
+                if (midId < id) {
+                    lo = mid + 1;
+                } else if (midId > id) {
+                    hi = mid - 1;
+                } else {
+                    return mid;  // id found
+                }
+            }
+            return ~lo;  // id not present
+        }
+
+        /**
+         * Check that all the keys are valid locations in the long arrays.
+         *
+         * If any aren't, log it and return false. Else return true.
+         */
+        private boolean validateKeys(boolean log) {
+            ArrayList<long[]> longs = mParent.mLongs;
+            final int longsSize = longs.size();
+
+            final int N = mSize;
+            for (int i=0; i<N; i++) {
+                final int key = mTable[i];
+                final int arrayIndex = getArrayFromKey(key);
+                final int index = getIndexFromKey(key);
+                if (arrayIndex >= longsSize || index >= longs.get(arrayIndex).length) {
+                    if (log) {
+                        Slog.w(TAG, "Invalid stats at index " + i + " -- " + dumpInternalState());
+                    }
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        public String dumpInternalState() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("SparseMappingTable.Table{mSequence=");
+            sb.append(mSequence);
+            sb.append(" mParent.mSequence=");
+            sb.append(mParent.mSequence);
+            sb.append(" mParent.mLongs.size()=");
+            sb.append(mParent.mLongs.size());
+            sb.append(" mSize=");
+            sb.append(mSize);
+            sb.append(" mTable=");
+            if (mTable == null) {
+                sb.append("null");
+            } else {
+                final int N = mTable.length;
+                sb.append('[');
+                for (int i=0; i<N; i++) {
+                    final int key = mTable[i];
+                    sb.append("0x");
+                    sb.append(Integer.toHexString((key >> ID_SHIFT) & ID_MASK));
+                    sb.append("/0x");
+                    sb.append(Integer.toHexString((key >> ARRAY_SHIFT) & ARRAY_MASK));
+                    sb.append("/0x");
+                    sb.append(Integer.toHexString((key >> INDEX_SHIFT) & INDEX_MASK));
+                    if (i != N-1) {
+                        sb.append(", ");
+                    }
+                }
+                sb.append(']');
+            }
+            sb.append(" clazz=");
+            sb.append(getClass().getName());
+            sb.append('}');
+
+            return sb.toString();
+        }
+    }
+
+    public SparseMappingTable() {
+        mLongs.add(new long[ARRAY_SIZE]);
+    }
+
+    /**
+     * Wipe out all the data.
+     */
+    public void reset() {
+        // Clear out mLongs, and prime it with a new array of data
+        mLongs.clear();
+        mLongs.add(new long[ARRAY_SIZE]);
+        mNextIndex = 0;
+
+        // Increment out sequence counter, because all of the tables will
+        // now be out of sync with the data.
+        mSequence++;
+    }
+
+    /**
+     * Write the data arrays to the parcel.
+     */
+    public void writeToParcel(Parcel out) {
+        out.writeInt(mSequence);
+        out.writeInt(mNextIndex);
+        final int N = mLongs.size();
+        out.writeInt(N);
+        for (int i=0; i<N-1; i++) {
+            final long[] array = mLongs.get(i);
+            out.writeInt(array.length);
+            writeCompactedLongArray(out, array, array.length);
+        }
+        // save less for the last one. upon re-loading they'll just start a new array.
+        final long[] lastLongs = mLongs.get(N-1);
+        out.writeInt(mNextIndex);
+        writeCompactedLongArray(out, lastLongs, mNextIndex);
+    }
+
+    /**
+     * Read the data arrays from the parcel.
+     */
+    public void readFromParcel(Parcel in) {
+        mSequence = in.readInt();
+        mNextIndex = in.readInt();
+
+        mLongs.clear();
+        final int N = in.readInt();
+        for (int i=0; i<N; i++) {
+            final int size = in.readInt();
+            final long[] array = new long[size];
+            readCompactedLongArray(in, array, size);
+            mLongs.add(array);
+        }
+    }
+
+    /**
+     * Return a string for debugging.
+     */
+    public String dumpInternalState(boolean includeData) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("SparseMappingTable{");
+        sb.append("mSequence=");
+        sb.append(mSequence);
+        sb.append(" mNextIndex=");
+        sb.append(mNextIndex);
+        sb.append(" mLongs.size=");
+        final int N = mLongs.size();
+        sb.append(N);
+        sb.append("\n");
+        if (includeData) {
+            for (int i=0; i<N; i++) {
+                final long[] array = mLongs.get(i);
+                for (int j=0; j<array.length; j++) {
+                    if (i == N-1 && j == mNextIndex) {
+                        break;
+                    }
+                    sb.append(String.format(" %4d %d 0x%016x %-19d\n", i, j, array[j], array[j]));
+                }
+            }
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Write the long array to the parcel in a compacted form.  Does not allow negative
+     * values in the array.
+     */
+    private static void writeCompactedLongArray(Parcel out, long[] array, int num) {
+        for (int i=0; i<num; i++) {
+            long val = array[i];
+            if (val < 0) {
+                Slog.w(TAG, "Time val negative: " + val);
+                val = 0;
+            }
+            if (val <= Integer.MAX_VALUE) {
+                out.writeInt((int)val);
+            } else {
+                int top = ~((int)((val>>32)&0x7fffffff));
+                int bottom = (int)(val&0x0ffffffffL);
+                out.writeInt(top);
+                out.writeInt(bottom);
+            }
+        }
+    }
+
+    /**
+     * Read the compacted array into the long[].
+     */
+    private static void readCompactedLongArray(Parcel in, long[] array, int num) {
+        final int alen = array.length;
+        if (num > alen) {
+            logOrThrow("bad array lengths: got " + num + " array is " + alen);
+            return;
+        }
+        int i;
+        for (i=0; i<num; i++) {
+            int val = in.readInt();
+            if (val >= 0) {
+                array[i] = val;
+            } else {
+                int bottom = in.readInt();
+                array[i] = (((long)~val)<<32) | bottom;
+            }
+        }
+        while (i < alen) {
+            array[i] = 0;
+            i++;
+        }
+    }
+
+    /**
+     * Extract the id from a key.
+     */
+    public static byte getIdFromKey(int key) {
+        return (byte)((key >> ID_SHIFT) & ID_MASK);
+    }
+
+    /**
+     * Gets the index of the array in the list of arrays.
+     *
+     * Not to be confused with getIndexFromKey.
+     */
+    public static int getArrayFromKey(int key) {
+        return (key >> ARRAY_SHIFT) & ARRAY_MASK;
+    }
+
+    /**
+     * Gets the index of a value in a long[].
+     *
+     * Not to be confused with getArrayFromKey.
+     */
+    public static int getIndexFromKey(int key) {
+        return (key >> INDEX_SHIFT) & INDEX_MASK;
+    }
+
+    /**
+     * Do a Slog.wtf or throw an exception (thereby crashing the system process if
+     * this is a debug build.)
+     */
+    private static void logOrThrow(String message) {
+        logOrThrow(message, new RuntimeException("Stack trace"));
+    }
+
+    /**
+     * Do a Slog.wtf or throw an exception (thereby crashing the system process if
+     * this is an eng build.)
+     */
+    private static void logOrThrow(String message, Throwable th) {
+        Slog.e(TAG, message, th);
+        if (Build.IS_ENG) {
+            throw new RuntimeException(message, th);
+        }
+    }
+}
diff --git a/com/android/internal/app/procstats/SysMemUsageTable.java b/com/android/internal/app/procstats/SysMemUsageTable.java
new file mode 100644
index 0000000..e71bc55
--- /dev/null
+++ b/com/android/internal/app/procstats/SysMemUsageTable.java
@@ -0,0 +1,189 @@
+/*
+ * 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.internal.app.procstats;
+
+import android.util.DebugUtils;
+
+import static com.android.internal.app.procstats.ProcessStats.STATE_COUNT;
+import static com.android.internal.app.procstats.ProcessStats.STATE_NOTHING;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_SAMPLE_COUNT;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_CACHED_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_CACHED_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_CACHED_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_FREE_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_FREE_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_FREE_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_ZRAM_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_ZRAM_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_ZRAM_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_KERNEL_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_KERNEL_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_KERNEL_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_NATIVE_MINIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_NATIVE_AVERAGE;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_NATIVE_MAXIMUM;
+import static com.android.internal.app.procstats.ProcessStats.SYS_MEM_USAGE_COUNT;
+
+import java.io.PrintWriter;
+
+
+/**
+ * Class to accumulate system mem usage data.
+ */
+public class SysMemUsageTable extends SparseMappingTable.Table {
+    /**
+     * Construct the SysMemUsageTable with 'tableData' as backing store
+     * for the longs data.
+     */
+    public SysMemUsageTable(SparseMappingTable tableData) {
+        super(tableData);
+    }
+
+    /**
+     * Merge the stats given into our own values.
+     *
+     * @param that  SysMemUsageTable to copy from.
+     */
+    public void mergeStats(SysMemUsageTable that) {
+        final int N = that.getKeyCount();
+        for (int i=0; i<N; i++) {
+            final int key = that.getKeyAt(i);
+
+            final int state = SparseMappingTable.getIdFromKey(key);
+            final long[] addData = that.getArrayForKey(key);
+            final int addOff = SparseMappingTable.getIndexFromKey(key);
+
+            mergeStats(state, addData, addOff);
+        }
+    }
+
+    /**
+     * Merge the stats given into our own values.
+     *
+     * @param state     The state
+     * @param addData   The data array to copy
+     * @param addOff    The index in addOff to start copying from
+     */
+    public void mergeStats(int state, long[] addData, int addOff) {
+        final int key = getOrAddKey((byte)state, SYS_MEM_USAGE_COUNT);
+        
+        final long[] dstData = getArrayForKey(key);
+        final int dstOff = SparseMappingTable.getIndexFromKey(key);
+
+        SysMemUsageTable.mergeSysMemUsage(dstData, dstOff, addData, addOff);
+    }
+
+    /**
+     * Return a long[] containing the merge of all of the usage in this table.
+     */
+    public long[] getTotalMemUsage() {
+        long[] total = new long[SYS_MEM_USAGE_COUNT];
+        final int N = getKeyCount();
+        for (int i=0; i<N; i++) {
+            final int key = getKeyAt(i);
+
+            final long[] addData = getArrayForKey(key);
+            final int addOff = SparseMappingTable.getIndexFromKey(key);
+
+            SysMemUsageTable.mergeSysMemUsage(total, 0, addData, addOff);
+        }
+        return total;
+    }
+
+    /**
+     * Merge the stats from one raw long[] into another.
+     *
+     * @param dstData The destination array
+     * @param dstOff  The index in the destination array to start from
+     * @param addData The source array
+     * @param addOff  The index in the source array to start from
+     */
+    public static void mergeSysMemUsage(long[] dstData, int dstOff,
+            long[] addData, int addOff) {
+        final long dstCount = dstData[dstOff+SYS_MEM_USAGE_SAMPLE_COUNT];
+        final long addCount = addData[addOff+SYS_MEM_USAGE_SAMPLE_COUNT];
+        if (dstCount == 0) {
+            dstData[dstOff+SYS_MEM_USAGE_SAMPLE_COUNT] = addCount;
+            for (int i=SYS_MEM_USAGE_CACHED_MINIMUM; i<SYS_MEM_USAGE_COUNT; i++) {
+                dstData[dstOff+i] = addData[addOff+i];
+            }
+        } else if (addCount > 0) {
+            dstData[dstOff+SYS_MEM_USAGE_SAMPLE_COUNT] = dstCount + addCount;
+            for (int i=SYS_MEM_USAGE_CACHED_MINIMUM; i<SYS_MEM_USAGE_COUNT; i+=3) {
+                if (dstData[dstOff+i] > addData[addOff+i]) {
+                    dstData[dstOff+i] = addData[addOff+i];
+                }
+                dstData[dstOff+i+1] = (long)(
+                        ((dstData[dstOff+i+1]*(double)dstCount)
+                                + (addData[addOff+i+1]*(double)addCount))
+                                / (dstCount+addCount) );
+                if (dstData[dstOff+i+2] < addData[addOff+i+2]) {
+                    dstData[dstOff+i+2] = addData[addOff+i+2];
+                }
+            }
+        }
+    }
+
+
+    public void dump(PrintWriter pw, String prefix, int[] screenStates, int[] memStates) {
+        int printedScreen = -1;
+        for (int is=0; is<screenStates.length; is++) {
+            int printedMem = -1;
+            for (int im=0; im<memStates.length; im++) {
+                final int iscreen = screenStates[is];
+                final int imem = memStates[im];
+                final int bucket = ((iscreen + imem) * STATE_COUNT);
+                long count = getValueForId((byte)bucket, SYS_MEM_USAGE_SAMPLE_COUNT);
+                if (count > 0) {
+                    pw.print(prefix);
+                    if (screenStates.length > 1) {
+                        DumpUtils.printScreenLabel(pw, printedScreen != iscreen
+                                ? iscreen : STATE_NOTHING);
+                        printedScreen = iscreen;
+                    }
+                    if (memStates.length > 1) {
+                        DumpUtils.printMemLabel(pw,
+                                printedMem != imem ? imem : STATE_NOTHING, '\0');
+                        printedMem = imem;
+                    }
+                    pw.print(": ");
+                    pw.print(count);
+                    pw.println(" samples:");
+                    dumpCategory(pw, prefix, "  Cached", bucket, SYS_MEM_USAGE_CACHED_MINIMUM);
+                    dumpCategory(pw, prefix, "  Free", bucket, SYS_MEM_USAGE_FREE_MINIMUM);
+                    dumpCategory(pw, prefix, "  ZRam", bucket, SYS_MEM_USAGE_ZRAM_MINIMUM);
+                    dumpCategory(pw, prefix, "  Kernel", bucket, SYS_MEM_USAGE_KERNEL_MINIMUM);
+                    dumpCategory(pw, prefix, "  Native", bucket, SYS_MEM_USAGE_NATIVE_MINIMUM);
+                }
+            }
+        }
+    }
+
+    private void dumpCategory(PrintWriter pw, String prefix, String label, int bucket, int index) {
+        pw.print(prefix); pw.print(label);
+        pw.print(": ");
+        DebugUtils.printSizeValue(pw, getValueForId((byte)bucket, index) * 1024);
+        pw.print(" min, ");
+        DebugUtils.printSizeValue(pw, getValueForId((byte)bucket, index + 1) * 1024);
+        pw.print(" avg, ");
+        DebugUtils.printSizeValue(pw, getValueForId((byte)bucket, index+2) * 1024);
+        pw.println(" max");
+    }
+    
+}
+
+
diff --git a/com/android/internal/backup/LocalTransport.java b/com/android/internal/backup/LocalTransport.java
new file mode 100644
index 0000000..543bd0c
--- /dev/null
+++ b/com/android/internal/backup/LocalTransport.java
@@ -0,0 +1,802 @@
+/*
+ * 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.internal.backup;
+
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.org.bouncycastle.util.encoders.Base64;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Backup transport for stashing stuff into a known location on disk, and
+ * later restoring from there.  For testing only.
+ */
+
+public class LocalTransport extends BackupTransport {
+    private static final String TAG = "LocalTransport";
+    private static final boolean DEBUG = false;
+
+    private static final String TRANSPORT_DIR_NAME
+            = "com.android.internal.backup.LocalTransport";
+
+    private static final String TRANSPORT_DESTINATION_STRING
+            = "Backing up to debug-only private cache";
+
+    private static final String TRANSPORT_DATA_MANAGEMENT_LABEL
+            = "";
+
+    private static final String INCREMENTAL_DIR = "_delta";
+    private static final String FULL_DATA_DIR = "_full";
+
+    // The currently-active restore set always has the same (nonzero!) token
+    private static final long CURRENT_SET_TOKEN = 1;
+
+    // Size quotas at reasonable values, similar to the current cloud-storage limits
+    private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024;
+    private static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024;
+
+    private Context mContext;
+    private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup");
+    private File mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN));
+    private File mCurrentSetIncrementalDir = new File(mCurrentSetDir, INCREMENTAL_DIR);
+    private File mCurrentSetFullDir = new File(mCurrentSetDir, FULL_DATA_DIR);
+
+    private PackageInfo[] mRestorePackages = null;
+    private int mRestorePackage = -1;  // Index into mRestorePackages
+    private int mRestoreType;
+    private File mRestoreSetDir;
+    private File mRestoreSetIncrementalDir;
+    private File mRestoreSetFullDir;
+
+    // Additional bookkeeping for full backup
+    private String mFullTargetPackage;
+    private ParcelFileDescriptor mSocket;
+    private FileInputStream mSocketInputStream;
+    private BufferedOutputStream mFullBackupOutputStream;
+    private byte[] mFullBackupBuffer;
+    private long mFullBackupSize;
+
+    private FileInputStream mCurFullRestoreStream;
+    private FileOutputStream mFullRestoreSocketStream;
+    private byte[] mFullRestoreBuffer;
+
+    private void makeDataDirs() {
+        mCurrentSetDir.mkdirs();
+        mCurrentSetFullDir.mkdir();
+        mCurrentSetIncrementalDir.mkdir();
+    }
+
+    public LocalTransport(Context context) {
+        mContext = context;
+        makeDataDirs();
+    }
+
+    @Override
+    public String name() {
+        return new ComponentName(mContext, this.getClass()).flattenToShortString();
+    }
+
+    @Override
+    public Intent configurationIntent() {
+        // The local transport is not user-configurable
+        return null;
+    }
+
+    @Override
+    public String currentDestinationString() {
+        return TRANSPORT_DESTINATION_STRING;
+    }
+
+    public Intent dataManagementIntent() {
+        // The local transport does not present a data-management UI
+        // TODO: consider adding simple UI to wipe the archives entirely,
+        // for cleaning up the cache partition.
+        return null;
+    }
+
+    public String dataManagementLabel() {
+        return TRANSPORT_DATA_MANAGEMENT_LABEL;
+    }
+
+    @Override
+    public String transportDirName() {
+        return TRANSPORT_DIR_NAME;
+    }
+
+    @Override
+    public long requestBackupTime() {
+        // any time is a good time for local backup
+        return 0;
+    }
+
+    @Override
+    public int initializeDevice() {
+        if (DEBUG) Log.v(TAG, "wiping all data");
+        deleteContents(mCurrentSetDir);
+        makeDataDirs();
+        return TRANSPORT_OK;
+    }
+
+    // Encapsulation of a single k/v element change
+    private class KVOperation {
+        final String key;     // Element filename, not the raw key, for efficiency
+        final byte[] value;   // null when this is a deletion operation
+
+        KVOperation(String k, byte[] v) {
+            key = k;
+            value = v;
+        }
+    }
+
+    @Override
+    public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
+        if (DEBUG) {
+            try {
+            StructStat ss = Os.fstat(data.getFileDescriptor());
+            Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName
+                    + " size=" + ss.st_size);
+            } catch (ErrnoException e) {
+                Log.w(TAG, "Unable to stat input file in performBackup() on "
+                        + packageInfo.packageName);
+            }
+        }
+
+        File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
+        packageDir.mkdirs();
+
+        // Each 'record' in the restore set is kept in its own file, named by
+        // the record key.  Wind through the data file, extracting individual
+        // record operations and building a list of all the updates to apply
+        // in this update.
+        final ArrayList<KVOperation> changeOps;
+        try {
+            changeOps = parseBackupStream(data);
+        } catch (IOException e) {
+            // oops, something went wrong.  abort the operation and return error.
+            Log.v(TAG, "Exception reading backup input", e);
+            return TRANSPORT_ERROR;
+        }
+
+        // Okay, now we've parsed out the delta's individual operations.  We need to measure
+        // the effect against what we already have in the datastore to detect quota overrun.
+        // So, we first need to tally up the current in-datastore size per key.
+        final ArrayMap<String, Integer> datastore = new ArrayMap<>();
+        int totalSize = parseKeySizes(packageDir, datastore);
+
+        // ... and now figure out the datastore size that will result from applying the
+        // sequence of delta operations
+        if (DEBUG) {
+            if (changeOps.size() > 0) {
+                Log.v(TAG, "Calculating delta size impact");
+            } else {
+                Log.v(TAG, "No operations in backup stream, so no size change");
+            }
+        }
+        int updatedSize = totalSize;
+        for (KVOperation op : changeOps) {
+            // Deduct the size of the key we're about to replace, if any
+            final Integer curSize = datastore.get(op.key);
+            if (curSize != null) {
+                updatedSize -= curSize.intValue();
+                if (DEBUG && op.value == null) {
+                    Log.v(TAG, "  delete " + op.key + ", updated total " + updatedSize);
+                }
+            }
+
+            // And add back the size of the value we're about to store, if any
+            if (op.value != null) {
+                updatedSize += op.value.length;
+                if (DEBUG) {
+                    Log.v(TAG, ((curSize == null) ? "  new " : "  replace ")
+                            +  op.key + ", updated total " + updatedSize);
+                }
+            }
+        }
+
+        // If our final size is over quota, report the failure
+        if (updatedSize > KEY_VALUE_BACKUP_SIZE_QUOTA) {
+            if (DEBUG) {
+                Log.i(TAG, "New datastore size " + updatedSize
+                        + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA);
+            }
+            return TRANSPORT_QUOTA_EXCEEDED;
+        }
+
+        // No problem with storage size, so go ahead and apply the delta operations
+        // (in the order that the app provided them)
+        for (KVOperation op : changeOps) {
+            File element = new File(packageDir, op.key);
+
+            // this is either a deletion or a rewrite-from-zero, so we can just remove
+            // the existing file and proceed in either case.
+            element.delete();
+
+            // if this wasn't a deletion, put the new data in place
+            if (op.value != null) {
+                try (FileOutputStream out = new FileOutputStream(element)) {
+                    out.write(op.value, 0, op.value.length);
+                } catch (IOException e) {
+                    Log.e(TAG, "Unable to update key file " + element);
+                    return TRANSPORT_ERROR;
+                }
+            }
+        }
+        return TRANSPORT_OK;
+    }
+
+    // Parses a backup stream into individual key/value operations
+    private ArrayList<KVOperation> parseBackupStream(ParcelFileDescriptor data)
+            throws IOException {
+        ArrayList<KVOperation> changeOps = new ArrayList<>();
+        BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor());
+        while (changeSet.readNextHeader()) {
+            String key = changeSet.getKey();
+            String base64Key = new String(Base64.encode(key.getBytes()));
+            int dataSize = changeSet.getDataSize();
+            if (DEBUG) {
+                Log.v(TAG, "  Delta operation key " + key + "   size " + dataSize
+                        + "   key64 " + base64Key);
+            }
+
+            byte[] buf = (dataSize >= 0) ? new byte[dataSize] : null;
+            if (dataSize >= 0) {
+                changeSet.readEntityData(buf, 0, dataSize);
+            }
+            changeOps.add(new KVOperation(base64Key, buf));
+        }
+        return changeOps;
+    }
+
+    // Reads the given datastore directory, building a table of the value size of each
+    // keyed element, and returning the summed total.
+    private int parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore) {
+        int totalSize = 0;
+        final String[] elements = packageDir.list();
+        if (elements != null) {
+            if (DEBUG) {
+                Log.v(TAG, "Existing datastore contents:");
+            }
+            for (String file : elements) {
+                File element = new File(packageDir, file);
+                String key = file;  // filename
+                int size = (int) element.length();
+                totalSize += size;
+                if (DEBUG) {
+                    Log.v(TAG, "  key " + key + "   size " + size);
+                }
+                datastore.put(key, size);
+            }
+            if (DEBUG) {
+                Log.v(TAG, "  TOTAL: " + totalSize);
+            }
+        } else {
+            if (DEBUG) {
+                Log.v(TAG, "No existing data for this package");
+            }
+        }
+        return totalSize;
+    }
+
+    // Deletes the contents but not the given directory
+    private void deleteContents(File dirname) {
+        File[] contents = dirname.listFiles();
+        if (contents != null) {
+            for (File f : contents) {
+                if (f.isDirectory()) {
+                    // delete the directory's contents then fall through
+                    // and delete the directory itself.
+                    deleteContents(f);
+                }
+                f.delete();
+            }
+        }
+    }
+
+    @Override
+    public int clearBackupData(PackageInfo packageInfo) {
+        if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName);
+
+        File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
+        final File[] fileset = packageDir.listFiles();
+        if (fileset != null) {
+            for (File f : fileset) {
+                f.delete();
+            }
+            packageDir.delete();
+        }
+
+        packageDir = new File(mCurrentSetFullDir, packageInfo.packageName);
+        final File[] tarballs = packageDir.listFiles();
+        if (tarballs != null) {
+            for (File f : tarballs) {
+                f.delete();
+            }
+            packageDir.delete();
+        }
+
+        return TRANSPORT_OK;
+    }
+
+    @Override
+    public int finishBackup() {
+        if (DEBUG) Log.v(TAG, "finishBackup() of " + mFullTargetPackage);
+        return tearDownFullBackup();
+    }
+
+    // ------------------------------------------------------------------------------------
+    // Full backup handling
+
+    private int tearDownFullBackup() {
+        if (mSocket != null) {
+            try {
+                if (mFullBackupOutputStream != null) {
+                    mFullBackupOutputStream.flush();
+                    mFullBackupOutputStream.close();
+                }
+                mSocketInputStream = null;
+                mFullTargetPackage = null;
+                mSocket.close();
+            } catch (IOException e) {
+                if (DEBUG) {
+                    Log.w(TAG, "Exception caught in tearDownFullBackup()", e);
+                }
+                return TRANSPORT_ERROR;
+            } finally {
+                mSocket = null;
+                mFullBackupOutputStream = null;
+            }
+        }
+        return TRANSPORT_OK;
+    }
+
+    private File tarballFile(String pkgName) {
+        return new File(mCurrentSetFullDir, pkgName);
+    }
+
+    @Override
+    public long requestFullBackupTime() {
+        return 0;
+    }
+
+    @Override
+    public int checkFullBackupSize(long size) {
+        int result = TRANSPORT_OK;
+        // Decline zero-size "backups"
+        if (size <= 0) {
+            result = TRANSPORT_PACKAGE_REJECTED;
+        } else if (size > FULL_BACKUP_SIZE_QUOTA) {
+            result = TRANSPORT_QUOTA_EXCEEDED;
+        }
+        if (result != TRANSPORT_OK) {
+            if (DEBUG) {
+                Log.v(TAG, "Declining backup of size " + size);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) {
+        if (mSocket != null) {
+            Log.e(TAG, "Attempt to initiate full backup while one is in progress");
+            return TRANSPORT_ERROR;
+        }
+
+        if (DEBUG) {
+            Log.i(TAG, "performFullBackup : " + targetPackage);
+        }
+
+        // We know a priori that we run in the system process, so we need to make
+        // sure to dup() our own copy of the socket fd.  Transports which run in
+        // their own processes must not do this.
+        try {
+            mFullBackupSize = 0;
+            mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
+            mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor());
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to process socket for full backup");
+            return TRANSPORT_ERROR;
+        }
+
+        mFullTargetPackage = targetPackage.packageName;
+        mFullBackupBuffer = new byte[4096];
+
+        return TRANSPORT_OK;
+    }
+
+    @Override
+    public int sendBackupData(final int numBytes) {
+        if (mSocket == null) {
+            Log.w(TAG, "Attempted sendBackupData before performFullBackup");
+            return TRANSPORT_ERROR;
+        }
+
+        mFullBackupSize += numBytes;
+        if (mFullBackupSize > FULL_BACKUP_SIZE_QUOTA) {
+            return TRANSPORT_QUOTA_EXCEEDED;
+        }
+
+        if (numBytes > mFullBackupBuffer.length) {
+            mFullBackupBuffer = new byte[numBytes];
+        }
+
+        if (mFullBackupOutputStream == null) {
+            FileOutputStream tarstream;
+            try {
+                File tarball = tarballFile(mFullTargetPackage);
+                tarstream = new FileOutputStream(tarball);
+            } catch (FileNotFoundException e) {
+                return TRANSPORT_ERROR;
+            }
+            mFullBackupOutputStream = new BufferedOutputStream(tarstream);
+        }
+
+        int bytesLeft = numBytes;
+        while (bytesLeft > 0) {
+            try {
+            int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, bytesLeft);
+            if (nRead < 0) {
+                // Something went wrong if we expect data but saw EOD
+                Log.w(TAG, "Unexpected EOD; failing backup");
+                return TRANSPORT_ERROR;
+            }
+            mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead);
+            bytesLeft -= nRead;
+            } catch (IOException e) {
+                Log.e(TAG, "Error handling backup data for " + mFullTargetPackage);
+                return TRANSPORT_ERROR;
+            }
+        }
+        if (DEBUG) {
+            Log.v(TAG, "   stored " + numBytes + " of data");
+        }
+        return TRANSPORT_OK;
+    }
+
+    // For now we can't roll back, so just tear everything down.
+    @Override
+    public void cancelFullBackup() {
+        if (DEBUG) {
+            Log.i(TAG, "Canceling full backup of " + mFullTargetPackage);
+        }
+        File archive = tarballFile(mFullTargetPackage);
+        tearDownFullBackup();
+        if (archive.exists()) {
+            archive.delete();
+        }
+    }
+
+    // ------------------------------------------------------------------------------------
+    // Restore handling
+    static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 };
+
+    @Override
+    public RestoreSet[] getAvailableRestoreSets() {
+        long[] existing = new long[POSSIBLE_SETS.length + 1];
+        int num = 0;
+
+        // see which possible non-current sets exist...
+        for (long token : POSSIBLE_SETS) {
+            if ((new File(mDataDir, Long.toString(token))).exists()) {
+                existing[num++] = token;
+            }
+        }
+        // ...and always the currently-active set last
+        existing[num++] = CURRENT_SET_TOKEN;
+
+        RestoreSet[] available = new RestoreSet[num];
+        for (int i = 0; i < available.length; i++) {
+            available[i] = new RestoreSet("Local disk image", "flash", existing[i]);
+        }
+        return available;
+    }
+
+    @Override
+    public long getCurrentRestoreSet() {
+        // The current restore set always has the same token
+        return CURRENT_SET_TOKEN;
+    }
+
+    @Override
+    public int startRestore(long token, PackageInfo[] packages) {
+        if (DEBUG) Log.v(TAG, "start restore " + token + " : " + packages.length
+                + " matching packages");
+        mRestorePackages = packages;
+        mRestorePackage = -1;
+        mRestoreSetDir = new File(mDataDir, Long.toString(token));
+        mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR);
+        mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR);
+        return TRANSPORT_OK;
+    }
+
+    @Override
+    public RestoreDescription nextRestorePackage() {
+        if (DEBUG) {
+            Log.v(TAG, "nextRestorePackage() : mRestorePackage=" + mRestorePackage
+                    + " length=" + mRestorePackages.length);
+        }
+        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
+
+        boolean found = false;
+        while (++mRestorePackage < mRestorePackages.length) {
+            String name = mRestorePackages[mRestorePackage].packageName;
+
+            // If we have key/value data for this package, deliver that
+            // skip packages where we have a data dir but no actual contents
+            String[] contents = (new File(mRestoreSetIncrementalDir, name)).list();
+            if (contents != null && contents.length > 0) {
+                if (DEBUG) {
+                    Log.v(TAG, "  nextRestorePackage(TYPE_KEY_VALUE) @ "
+                        + mRestorePackage + " = " + name);
+                }
+                mRestoreType = RestoreDescription.TYPE_KEY_VALUE;
+                found = true;
+            }
+
+            if (!found) {
+                // No key/value data; check for [non-empty] full data
+                File maybeFullData = new File(mRestoreSetFullDir, name);
+                if (maybeFullData.length() > 0) {
+                    if (DEBUG) {
+                        Log.v(TAG, "  nextRestorePackage(TYPE_FULL_STREAM) @ "
+                                + mRestorePackage + " = " + name);
+                    }
+                    mRestoreType = RestoreDescription.TYPE_FULL_STREAM;
+                    mCurFullRestoreStream = null;   // ensure starting from the ground state
+                    found = true;
+                }
+            }
+
+            if (found) {
+                return new RestoreDescription(name, mRestoreType);
+            }
+
+            if (DEBUG) {
+                Log.v(TAG, "  ... package @ " + mRestorePackage + " = " + name
+                        + " has no data; skipping");
+            }
+        }
+
+        if (DEBUG) Log.v(TAG, "  no more packages to restore");
+        return RestoreDescription.NO_MORE_PACKAGES;
+    }
+
+    @Override
+    public int getRestoreData(ParcelFileDescriptor outFd) {
+        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
+        if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called");
+        if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) {
+            throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset");
+        }
+        File packageDir = new File(mRestoreSetIncrementalDir,
+                mRestorePackages[mRestorePackage].packageName);
+
+        // The restore set is the concatenation of the individual record blobs,
+        // each of which is a file in the package's directory.  We return the
+        // data in lexical order sorted by key, so that apps which use synthetic
+        // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious
+        // order.
+        ArrayList<DecodedFilename> blobs = contentsByKey(packageDir);
+        if (blobs == null) {  // nextRestorePackage() ensures the dir exists, so this is an error
+            Log.e(TAG, "No keys for package: " + packageDir);
+            return TRANSPORT_ERROR;
+        }
+
+        // We expect at least some data if the directory exists in the first place
+        if (DEBUG) Log.v(TAG, "  getRestoreData() found " + blobs.size() + " key files");
+        BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor());
+        try {
+            for (DecodedFilename keyEntry : blobs) {
+                File f = keyEntry.file;
+                FileInputStream in = new FileInputStream(f);
+                try {
+                    int size = (int) f.length();
+                    byte[] buf = new byte[size];
+                    in.read(buf);
+                    if (DEBUG) Log.v(TAG, "    ... key=" + keyEntry.key + " size=" + size);
+                    out.writeEntityHeader(keyEntry.key, size);
+                    out.writeEntityData(buf, size);
+                } finally {
+                    in.close();
+                }
+            }
+            return TRANSPORT_OK;
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to read backup records", e);
+            return TRANSPORT_ERROR;
+        }
+    }
+
+    static class DecodedFilename implements Comparable<DecodedFilename> {
+        public File file;
+        public String key;
+
+        public DecodedFilename(File f) {
+            file = f;
+            key = new String(Base64.decode(f.getName()));
+        }
+
+        @Override
+        public int compareTo(DecodedFilename other) {
+            // sorts into ascending lexical order by decoded key
+            return key.compareTo(other.key);
+        }
+    }
+
+    // Return a list of the files in the given directory, sorted lexically by
+    // the Base64-decoded file name, not by the on-disk filename
+    private ArrayList<DecodedFilename> contentsByKey(File dir) {
+        File[] allFiles = dir.listFiles();
+        if (allFiles == null || allFiles.length == 0) {
+            return null;
+        }
+
+        // Decode the filenames into keys then sort lexically by key
+        ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>();
+        for (File f : allFiles) {
+            contents.add(new DecodedFilename(f));
+        }
+        Collections.sort(contents);
+        return contents;
+    }
+
+    @Override
+    public void finishRestore() {
+        if (DEBUG) Log.v(TAG, "finishRestore()");
+        if (mRestoreType == RestoreDescription.TYPE_FULL_STREAM) {
+            resetFullRestoreState();
+        }
+        mRestoreType = 0;
+    }
+
+    // ------------------------------------------------------------------------------------
+    // Full restore handling
+
+    private void resetFullRestoreState() {
+        IoUtils.closeQuietly(mCurFullRestoreStream);
+        mCurFullRestoreStream = null;
+        mFullRestoreSocketStream = null;
+        mFullRestoreBuffer = null;
+    }
+
+    /**
+     * Ask the transport to provide data for the "current" package being restored.  The
+     * transport then writes some data to the socket supplied to this call, and returns
+     * the number of bytes written.  The system will then read that many bytes and
+     * stream them to the application's agent for restore, then will call this method again
+     * to receive the next chunk of the archive.  This sequence will be repeated until the
+     * transport returns zero indicating that all of the package's data has been delivered
+     * (or returns a negative value indicating some sort of hard error condition at the
+     * transport level).
+     *
+     * <p>After this method returns zero, the system will then call
+     * {@link #getNextFullRestorePackage()} to begin the restore process for the next
+     * application, and the sequence begins again.
+     *
+     * @param socket The file descriptor that the transport will use for delivering the
+     *    streamed archive.
+     * @return 0 when no more data for the current package is available.  A positive value
+     *    indicates the presence of that much data to be delivered to the app.  A negative
+     *    return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR},
+     *    indicating a fatal error condition that precludes further restore operations
+     *    on the current dataset.
+     */
+    @Override
+    public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
+        if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) {
+            throw new IllegalStateException("Asked for full restore data for non-stream package");
+        }
+
+        // first chunk?
+        if (mCurFullRestoreStream == null) {
+            final String name = mRestorePackages[mRestorePackage].packageName;
+            if (DEBUG) Log.i(TAG, "Starting full restore of " + name);
+            File dataset = new File(mRestoreSetFullDir, name);
+            try {
+                mCurFullRestoreStream = new FileInputStream(dataset);
+            } catch (IOException e) {
+                // If we can't open the target package's tarball, we return the single-package
+                // error code and let the caller go on to the next package.
+                Log.e(TAG, "Unable to read archive for " + name);
+                return TRANSPORT_PACKAGE_REJECTED;
+            }
+            mFullRestoreSocketStream = new FileOutputStream(socket.getFileDescriptor());
+            mFullRestoreBuffer = new byte[2*1024];
+        }
+
+        int nRead;
+        try {
+            nRead = mCurFullRestoreStream.read(mFullRestoreBuffer);
+            if (nRead < 0) {
+                // EOF: tell the caller we're done
+                nRead = NO_MORE_DATA;
+            } else if (nRead == 0) {
+                // This shouldn't happen when reading a FileInputStream; we should always
+                // get either a positive nonzero byte count or -1.  Log the situation and
+                // treat it as EOF.
+                Log.w(TAG, "read() of archive file returned 0; treating as EOF");
+                nRead = NO_MORE_DATA;
+            } else {
+                if (DEBUG) {
+                    Log.i(TAG, "   delivering restore chunk: " + nRead);
+                }
+                mFullRestoreSocketStream.write(mFullRestoreBuffer, 0, nRead);
+            }
+        } catch (IOException e) {
+            return TRANSPORT_ERROR;  // Hard error accessing the file; shouldn't happen
+        } finally {
+            // Most transports will need to explicitly close 'socket' here, but this transport
+            // is in the same process as the caller so it can leave it up to the backup manager
+            // to manage both socket fds.
+        }
+
+        return nRead;
+    }
+
+    /**
+     * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM}
+     * data for restore, it will invoke this method to tell the transport that it should
+     * abandon the data download for the current package.  The OS will then either call
+     * {@link #nextRestorePackage()} again to move on to restoring the next package in the
+     * set being iterated over, or will call {@link #finishRestore()} to shut down the restore
+     * operation.
+     *
+     * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the
+     *    current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious
+     *    transport-level failure.  If the transport reports an error here, the entire restore
+     *    operation will immediately be finished with no further attempts to restore app data.
+     */
+    @Override
+    public int abortFullRestore() {
+        if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) {
+            throw new IllegalStateException("abortFullRestore() but not currently restoring");
+        }
+        resetFullRestoreState();
+        mRestoreType = 0;
+        return TRANSPORT_OK;
+    }
+
+    @Override
+    public long getBackupQuota(String packageName, boolean isFullBackup) {
+        return isFullBackup ? FULL_BACKUP_SIZE_QUOTA : KEY_VALUE_BACKUP_SIZE_QUOTA;
+    }
+}
diff --git a/com/android/internal/backup/LocalTransportService.java b/com/android/internal/backup/LocalTransportService.java
new file mode 100644
index 0000000..77ac313
--- /dev/null
+++ b/com/android/internal/backup/LocalTransportService.java
@@ -0,0 +1,37 @@
+/*
+ * 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.internal.backup;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class LocalTransportService extends Service {
+    private static LocalTransport sTransport = null;
+
+    @Override
+    public void onCreate() {
+        if (sTransport == null) {
+            sTransport = new LocalTransport(this);
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return sTransport.getBinder();
+    }
+}
diff --git a/com/android/internal/colorextraction/ColorExtractor.java b/com/android/internal/colorextraction/ColorExtractor.java
new file mode 100644
index 0000000..c171fa6
--- /dev/null
+++ b/com/android/internal/colorextraction/ColorExtractor.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2017 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.internal.colorextraction;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.types.ExtractionType;
+import com.android.internal.colorextraction.types.Tonal;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Class to process wallpaper colors and generate a tonal palette based on them.
+ */
+public class ColorExtractor implements WallpaperManager.OnColorsChangedListener {
+
+    public static final int TYPE_NORMAL = 0;
+    public static final int TYPE_DARK = 1;
+    public static final int TYPE_EXTRA_DARK = 2;
+    private static final int[] sGradientTypes = new int[]{TYPE_NORMAL, TYPE_DARK, TYPE_EXTRA_DARK};
+
+    private static final String TAG = "ColorExtractor";
+    private static final boolean DEBUG = false;
+
+    protected final SparseArray<GradientColors[]> mGradientColors;
+    private final ArrayList<WeakReference<OnColorsChangedListener>> mOnColorsChangedListeners;
+    private final Context mContext;
+    private final ExtractionType mExtractionType;
+    protected WallpaperColors mSystemColors;
+    protected WallpaperColors mLockColors;
+
+    public ColorExtractor(Context context) {
+        this(context, new Tonal(context));
+    }
+
+    @VisibleForTesting
+    public ColorExtractor(Context context, ExtractionType extractionType) {
+        mContext = context;
+        mExtractionType = extractionType;
+
+        mGradientColors = new SparseArray<>();
+        for (int which : new int[] { WallpaperManager.FLAG_LOCK, WallpaperManager.FLAG_SYSTEM}) {
+            GradientColors[] colors = new GradientColors[sGradientTypes.length];
+            mGradientColors.append(which, colors);
+            for (int type : sGradientTypes) {
+                colors[type] = new GradientColors();
+            }
+        }
+
+        mOnColorsChangedListeners = new ArrayList<>();
+        GradientColors[] systemColors = mGradientColors.get(WallpaperManager.FLAG_SYSTEM);
+        GradientColors[] lockColors = mGradientColors.get(WallpaperManager.FLAG_LOCK);
+
+        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
+        if (wallpaperManager == null) {
+            Log.w(TAG, "Can't listen to color changes!");
+        } else {
+            wallpaperManager.addOnColorsChangedListener(this, null /* handler */);
+
+            // Initialize all gradients with the current colors
+            Trace.beginSection("ColorExtractor#getWallpaperColors");
+            mSystemColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+            mLockColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK);
+            Trace.endSection();
+        }
+
+        // Initialize all gradients with the current colors
+        extractInto(mSystemColors,
+                systemColors[TYPE_NORMAL],
+                systemColors[TYPE_DARK],
+                systemColors[TYPE_EXTRA_DARK]);
+        extractInto(mLockColors,
+                lockColors[TYPE_NORMAL],
+                lockColors[TYPE_DARK],
+                lockColors[TYPE_EXTRA_DARK]);
+    }
+
+    /**
+     * Retrieve gradient colors for a specific wallpaper.
+     *
+     * @param which FLAG_LOCK or FLAG_SYSTEM
+     * @return colors
+     */
+    @NonNull
+    public GradientColors getColors(int which) {
+        return getColors(which, TYPE_DARK);
+    }
+
+    /**
+     * Get current gradient colors for one of the possible gradient types
+     *
+     * @param which FLAG_LOCK or FLAG_SYSTEM
+     * @param type TYPE_NORMAL, TYPE_DARK or TYPE_EXTRA_DARK
+     * @return colors
+     */
+    @NonNull
+    public GradientColors getColors(int which, int type) {
+        if (type != TYPE_NORMAL && type != TYPE_DARK && type != TYPE_EXTRA_DARK) {
+            throw new IllegalArgumentException(
+                    "type should be TYPE_NORMAL, TYPE_DARK or TYPE_EXTRA_DARK");
+        }
+        if (which != WallpaperManager.FLAG_LOCK && which != WallpaperManager.FLAG_SYSTEM) {
+            throw new IllegalArgumentException("which should be FLAG_SYSTEM or FLAG_NORMAL");
+        }
+        return mGradientColors.get(which)[type];
+    }
+
+    /**
+     * Get the last available WallpaperColors without forcing new extraction.
+     *
+     * @param which FLAG_LOCK or FLAG_SYSTEM
+     * @return Last cached colors
+     */
+    @Nullable
+    public WallpaperColors getWallpaperColors(int which) {
+        if (which == WallpaperManager.FLAG_LOCK) {
+            return mLockColors;
+        } else if (which == WallpaperManager.FLAG_SYSTEM) {
+            return mSystemColors;
+        } else {
+            throw new IllegalArgumentException("Invalid value for which: " + which);
+        }
+    }
+
+    @Override
+    public void onColorsChanged(WallpaperColors colors, int which) {
+        if (DEBUG) {
+            Log.d(TAG, "New wallpaper colors for " + which + ": " + colors);
+        }
+        boolean changed = false;
+        if ((which & WallpaperManager.FLAG_LOCK) != 0) {
+            mLockColors = colors;
+            GradientColors[] lockColors = mGradientColors.get(WallpaperManager.FLAG_LOCK);
+            extractInto(colors, lockColors[TYPE_NORMAL], lockColors[TYPE_DARK],
+                    lockColors[TYPE_EXTRA_DARK]);
+            changed = true;
+        }
+        if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
+            mSystemColors = colors;
+            GradientColors[] systemColors = mGradientColors.get(WallpaperManager.FLAG_SYSTEM);
+            extractInto(colors, systemColors[TYPE_NORMAL], systemColors[TYPE_DARK],
+                    systemColors[TYPE_EXTRA_DARK]);
+            changed = true;
+        }
+
+        if (changed) {
+            triggerColorsChanged(which);
+        }
+    }
+
+    protected void triggerColorsChanged(int which) {
+        ArrayList<WeakReference<OnColorsChangedListener>> references =
+                new ArrayList<>(mOnColorsChangedListeners);
+        final int size = references.size();
+        for (int i = 0; i < size; i++) {
+            final WeakReference<OnColorsChangedListener> weakReference = references.get(i);
+            final OnColorsChangedListener listener = weakReference.get();
+            if (listener == null) {
+                mOnColorsChangedListeners.remove(weakReference);
+            } else {
+                listener.onColorsChanged(this, which);
+            }
+        }
+    }
+
+    private void extractInto(WallpaperColors inWallpaperColors,
+            GradientColors outGradientColorsNormal, GradientColors outGradientColorsDark,
+            GradientColors outGradientColorsExtraDark) {
+        mExtractionType.extractInto(inWallpaperColors, outGradientColorsNormal,
+                outGradientColorsDark, outGradientColorsExtraDark);
+    }
+
+    public void destroy() {
+        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
+        if (wallpaperManager != null) {
+            wallpaperManager.removeOnColorsChangedListener(this);
+        }
+    }
+
+    public void addOnColorsChangedListener(@NonNull OnColorsChangedListener listener) {
+        mOnColorsChangedListeners.add(new WeakReference<>(listener));
+    }
+
+    public void removeOnColorsChangedListener(@NonNull OnColorsChangedListener listener) {
+        ArrayList<WeakReference<OnColorsChangedListener>> references =
+                new ArrayList<>(mOnColorsChangedListeners);
+        final int size = references.size();
+        for (int i = 0; i < size; i++) {
+            final WeakReference<OnColorsChangedListener> weakReference = references.get(i);
+            if (weakReference.get() == listener) {
+                mOnColorsChangedListeners.remove(weakReference);
+                break;
+            }
+        }
+    }
+
+    public static class GradientColors {
+        private int mMainColor;
+        private int mSecondaryColor;
+        private boolean mSupportsDarkText;
+
+        public void setMainColor(int mainColor) {
+            mMainColor = mainColor;
+        }
+
+        public void setSecondaryColor(int secondaryColor) {
+            mSecondaryColor = secondaryColor;
+        }
+
+        public void setSupportsDarkText(boolean supportsDarkText) {
+            mSupportsDarkText = supportsDarkText;
+        }
+
+        public void set(GradientColors other) {
+            mMainColor = other.mMainColor;
+            mSecondaryColor = other.mSecondaryColor;
+            mSupportsDarkText = other.mSupportsDarkText;
+        }
+
+        public int getMainColor() {
+            return mMainColor;
+        }
+
+        public int getSecondaryColor() {
+            return mSecondaryColor;
+        }
+
+        public boolean supportsDarkText() {
+            return mSupportsDarkText;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null || o.getClass() != getClass()) {
+                return false;
+            }
+            GradientColors other = (GradientColors) o;
+            return other.mMainColor == mMainColor &&
+                    other.mSecondaryColor == mSecondaryColor &&
+                    other.mSupportsDarkText == mSupportsDarkText;
+        }
+
+        @Override
+        public int hashCode() {
+            int code = mMainColor;
+            code = 31 * code + mSecondaryColor;
+            code = 31 * code + (mSupportsDarkText ? 0 : 1);
+            return code;
+        }
+
+        @Override
+        public String toString() {
+            return "GradientColors(" + Integer.toHexString(mMainColor) + ", "
+                    + Integer.toHexString(mSecondaryColor) + ")";
+        }
+    }
+
+    public interface OnColorsChangedListener {
+        void onColorsChanged(ColorExtractor colorExtractor, int which);
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/colorextraction/drawable/GradientDrawable.java b/com/android/internal/colorextraction/drawable/GradientDrawable.java
new file mode 100644
index 0000000..500c028
--- /dev/null
+++ b/com/android/internal/colorextraction/drawable/GradientDrawable.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2017 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.internal.colorextraction.drawable;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Xfermode;
+import android.graphics.drawable.Drawable;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.graphics.ColorUtils;
+
+/**
+ * Draws a gradient based on a Palette
+ */
+public class GradientDrawable extends Drawable {
+    private static final String TAG = "GradientDrawable";
+
+    private static final float CENTRALIZED_CIRCLE_1 = -2;
+    private static final int GRADIENT_RADIUS = 480; // in dp
+    private static final long COLOR_ANIMATION_DURATION = 2000;
+
+    private int mAlpha = 255;
+
+    private float mDensity;
+    private final Paint mPaint;
+    private final Rect mWindowBounds;
+    private final Splat mSplat;
+
+    private int mMainColor;
+    private int mSecondaryColor;
+    private ValueAnimator mColorAnimation;
+
+    public GradientDrawable(@NonNull Context context) {
+        mDensity = context.getResources().getDisplayMetrics().density;
+        mSplat = new Splat(0.50f, 1.00f, GRADIENT_RADIUS, CENTRALIZED_CIRCLE_1);
+        mWindowBounds = new Rect();
+
+        mPaint = new Paint();
+        mPaint.setStyle(Paint.Style.FILL);
+    }
+
+    public void setColors(@NonNull ColorExtractor.GradientColors colors) {
+        setColors(colors.getMainColor(), colors.getSecondaryColor(), true);
+    }
+
+    public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) {
+        setColors(colors.getMainColor(), colors.getSecondaryColor(), animated);
+    }
+
+    public void setColors(int mainColor, int secondaryColor, boolean animated) {
+        if (mainColor == mMainColor && secondaryColor == mSecondaryColor) {
+            return;
+        }
+
+        if (mColorAnimation != null && mColorAnimation.isRunning()) {
+            mColorAnimation.cancel();
+        }
+
+        if (animated) {
+            final int mainFrom = mMainColor;
+            final int secFrom = mSecondaryColor;
+
+            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+            anim.setDuration(COLOR_ANIMATION_DURATION);
+            anim.addUpdateListener(animation -> {
+                float ratio = (float) animation.getAnimatedValue();
+                mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio);
+                mSecondaryColor = ColorUtils.blendARGB(secFrom, secondaryColor, ratio);
+                buildPaints();
+                invalidateSelf();
+            });
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation, boolean isReverse) {
+                    if (mColorAnimation == animation) {
+                        mColorAnimation = null;
+                    }
+                }
+            });
+            anim.setInterpolator(new DecelerateInterpolator());
+            anim.start();
+            mColorAnimation = anim;
+        } else {
+            mMainColor = mainColor;
+            mSecondaryColor = secondaryColor;
+            buildPaints();
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        if (alpha != mAlpha) {
+            mAlpha = alpha;
+            mPaint.setAlpha(mAlpha);
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public void setXfermode(@Nullable Xfermode mode) {
+        mPaint.setXfermode(mode);
+        invalidateSelf();
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+    }
+
+    @Override
+    public ColorFilter getColorFilter() {
+        return mPaint.getColorFilter();
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    public void setScreenSize(int width, int height) {
+        mWindowBounds.set(0, 0, width, height);
+        setBounds(0, 0, width, height);
+        buildPaints();
+    }
+
+    private void buildPaints() {
+        Rect bounds = mWindowBounds;
+        if (bounds.width() == 0) {
+            return;
+        }
+
+        float w = bounds.width();
+        float h = bounds.height();
+
+        float x = mSplat.x * w;
+        float y = mSplat.y * h;
+
+        float radius = mSplat.radius * mDensity;
+
+        // When we have only a single alpha gradient, we increase quality
+        // (avoiding banding) by merging the background solid color into
+        // the gradient directly
+        RadialGradient radialGradient = new RadialGradient(x, y, radius,
+                mSecondaryColor, mMainColor, Shader.TileMode.CLAMP);
+        mPaint.setShader(radialGradient);
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        Rect bounds = mWindowBounds;
+        if (bounds.width() == 0) {
+            throw new IllegalStateException("You need to call setScreenSize before drawing.");
+        }
+
+        // Splat each gradient
+        float w = bounds.width();
+        float h = bounds.height();
+
+        float x = mSplat.x * w;
+        float y = mSplat.y * h;
+
+        float radius = Math.max(w, h);
+        canvas.drawRect(x - radius, y - radius, x + radius, y + radius, mPaint);
+    }
+
+    @VisibleForTesting
+    public int getMainColor() {
+        return mMainColor;
+    }
+
+    @VisibleForTesting
+    public int getSecondaryColor() {
+        return mSecondaryColor;
+    }
+
+    static final class Splat {
+        final float x;
+        final float y;
+        final float radius;
+        final float colorIndex;
+
+        Splat(float x, float y, float radius, float colorIndex) {
+            this.x = x;
+            this.y = y;
+            this.radius = radius;
+            this.colorIndex = colorIndex;
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/colorextraction/types/ExtractionType.java b/com/android/internal/colorextraction/types/ExtractionType.java
new file mode 100644
index 0000000..7000e79
--- /dev/null
+++ b/com/android/internal/colorextraction/types/ExtractionType.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 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.internal.colorextraction.types;
+
+import android.app.WallpaperColors;
+
+import com.android.internal.colorextraction.ColorExtractor;
+
+/**
+ * Interface to allow various color extraction implementations.
+ */
+public interface ExtractionType {
+
+    /**
+     * Executes color extraction by reading WallpaperColors and setting
+     * main and secondary colors on GradientColors.
+     *
+     * Extraction is expected to happen with 3 different gradient types:
+     *     Normal, with the main extracted colors
+     *     Dark, with extra contrast
+     *     ExtraDark, for places where GAR is mandatory, like the emergency dialer
+     *
+     * @param inWallpaperColors where to read from
+     * @param outGradientColorsNormal object that should receive normal colors
+     * @param outGradientColorsDark object that should receive dark colors
+     * @param outGradientColorsExtraDark object that should receive extra dark colors
+     */
+    void extractInto(WallpaperColors inWallpaperColors,
+            ColorExtractor.GradientColors outGradientColorsNormal,
+            ColorExtractor.GradientColors outGradientColorsDark,
+            ColorExtractor.GradientColors outGradientColorsExtraDark);
+}
diff --git a/com/android/internal/colorextraction/types/Tonal.java b/com/android/internal/colorextraction/types/Tonal.java
new file mode 100644
index 0000000..e6ef10b
--- /dev/null
+++ b/com/android/internal/colorextraction/types/Tonal.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2017 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.internal.colorextraction.types;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.WallpaperColors;
+import android.content.Context;
+import android.graphics.Color;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Range;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
+import com.android.internal.graphics.ColorUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Implementation of tonal color extraction
+ */
+public class Tonal implements ExtractionType {
+    private static final String TAG = "Tonal";
+
+    // Used for tonal palette fitting
+    private static final float FIT_WEIGHT_H = 1.0f;
+    private static final float FIT_WEIGHT_S = 1.0f;
+    private static final float FIT_WEIGHT_L = 10.0f;
+
+    private static final boolean DEBUG = true;
+
+    public static final int MAIN_COLOR_LIGHT = 0xffe0e0e0;
+    public static final int SECONDARY_COLOR_LIGHT = 0xff9e9e9e;
+    public static final int MAIN_COLOR_DARK = 0xff212121;
+    public static final int SECONDARY_COLOR_DARK = 0xff000000;
+
+    private final TonalPalette mGreyPalette;
+    private final ArrayList<TonalPalette> mTonalPalettes;
+    private final ArrayList<ColorRange> mBlacklistedColors;
+
+    // Temporary variable to avoid allocations
+    private float[] mTmpHSL = new float[3];
+
+    public Tonal(Context context) {
+
+        ConfigParser parser = new ConfigParser(context);
+        mTonalPalettes = parser.getTonalPalettes();
+        mBlacklistedColors = parser.getBlacklistedColors();
+
+        mGreyPalette = mTonalPalettes.get(0);
+        mTonalPalettes.remove(0);
+    }
+
+    /**
+     * Grab colors from WallpaperColors and set them into GradientColors.
+     * Also applies the default gradient in case extraction fails.
+     *
+     * @param inWallpaperColors Input.
+     * @param outColorsNormal Colors for normal theme.
+     * @param outColorsDark Colors for dar theme.
+     * @param outColorsExtraDark Colors for extra dark theme.
+     */
+    public void extractInto(@Nullable WallpaperColors inWallpaperColors,
+            @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
+            @NonNull GradientColors outColorsExtraDark) {
+        boolean success = runTonalExtraction(inWallpaperColors, outColorsNormal, outColorsDark,
+                outColorsExtraDark);
+        if (!success) {
+            applyFallback(inWallpaperColors, outColorsNormal, outColorsDark, outColorsExtraDark);
+        }
+    }
+
+    /**
+     * Grab colors from WallpaperColors and set them into GradientColors.
+     *
+     * @param inWallpaperColors Input.
+     * @param outColorsNormal Colors for normal theme.
+     * @param outColorsDark Colors for dar theme.
+     * @param outColorsExtraDark Colors for extra dark theme.
+     * @return True if succeeded or false if failed.
+     */
+    private boolean runTonalExtraction(@Nullable WallpaperColors inWallpaperColors,
+            @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
+            @NonNull GradientColors outColorsExtraDark) {
+
+        if (inWallpaperColors == null) {
+            return false;
+        }
+
+        final List<Color> mainColors = inWallpaperColors.getMainColors();
+        final int mainColorsSize = mainColors.size();
+        final int hints = inWallpaperColors.getColorHints();
+        final boolean supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0;
+        final boolean generatedFromBitmap = (hints & WallpaperColors.HINT_FROM_BITMAP) != 0;
+
+        if (mainColorsSize == 0) {
+            return false;
+        }
+
+        // Decide what's the best color to use.
+        // We have 2 options:
+        // • Just pick the primary color
+        // • Filter out blacklisted colors. This is useful when palette is generated
+        //   automatically from a bitmap.
+        Color bestColor = null;
+        final float[] hsl = new float[3];
+        for (int i = 0; i < mainColorsSize; i++) {
+            final Color color = mainColors.get(i);
+            final int colorValue = color.toArgb();
+            ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
+                    Color.blue(colorValue), hsl);
+
+            // Stop when we find a color that meets our criteria
+            if (!generatedFromBitmap || !isBlacklisted(hsl)) {
+                bestColor = color;
+                break;
+            }
+        }
+
+        // Fail if not found
+        if (bestColor == null) {
+            return false;
+        }
+
+        // Tonal is not really a sort, it takes a color from the extracted
+        // palette and finds a best fit amongst a collection of pre-defined
+        // palettes. The best fit is tweaked to be closer to the source color
+        // and replaces the original palette.
+        int colorValue = bestColor.toArgb();
+        ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
+                hsl);
+
+        // The Android HSL definition requires the hue to go from 0 to 360 but
+        // the Material Tonal Palette defines hues from 0 to 1.
+        hsl[0] /= 360f;
+
+        // Find the palette that contains the closest color
+        TonalPalette palette = findTonalPalette(hsl[0], hsl[1]);
+        if (palette == null) {
+            Log.w(TAG, "Could not find a tonal palette!");
+            return false;
+        }
+
+        // Figure out what's the main color index in the optimal palette
+        int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
+        if (fitIndex == -1) {
+            Log.w(TAG, "Could not find best fit!");
+            return false;
+        }
+
+        // Generate the 10 colors palette by offsetting each one of them
+        float[] h = fit(palette.h, hsl[0], fitIndex,
+                Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
+        float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
+        float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);
+
+        if (DEBUG) {
+            StringBuilder builder = new StringBuilder("Tonal Palette - index: " + fitIndex +
+                    ". Main color: " + Integer.toHexString(getColorInt(fitIndex, h, s, l)) +
+                    "\nColors: ");
+
+            for (int i=0; i < h.length; i++) {
+                builder.append(Integer.toHexString(getColorInt(i, h, s, l)));
+                if (i < h.length - 1) {
+                    builder.append(", ");
+                }
+            }
+            Log.d(TAG, builder.toString());
+        }
+
+        int primaryIndex = fitIndex;
+        int mainColor = getColorInt(primaryIndex, h, s, l);
+
+        // We might want use the fallback in case the extracted color is brighter than our
+        // light fallback or darker than our dark fallback.
+        ColorUtils.colorToHSL(mainColor, mTmpHSL);
+        final float mainLuminosity = mTmpHSL[2];
+        ColorUtils.colorToHSL(MAIN_COLOR_LIGHT, mTmpHSL);
+        final float lightLuminosity = mTmpHSL[2];
+        if (mainLuminosity > lightLuminosity) {
+            return false;
+        }
+        ColorUtils.colorToHSL(MAIN_COLOR_DARK, mTmpHSL);
+        final float darkLuminosity = mTmpHSL[2];
+        if (mainLuminosity < darkLuminosity) {
+            return false;
+        }
+
+        // Normal colors:
+        // best fit + a 2 colors offset
+        outColorsNormal.setMainColor(mainColor);
+        int secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
+        outColorsNormal.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
+
+        // Dark colors:
+        // Stops at 4th color, only lighter if dark text is supported
+        if (supportsDarkText) {
+            primaryIndex = h.length - 1;
+        } else if (fitIndex < 2) {
+            primaryIndex = 0;
+        } else {
+            primaryIndex = Math.min(fitIndex, 3);
+        }
+        secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
+        outColorsDark.setMainColor(getColorInt(primaryIndex, h, s, l));
+        outColorsDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
+
+        // Extra Dark:
+        // Stay close to dark colors until dark text is supported
+        if (supportsDarkText) {
+            primaryIndex = h.length - 1;
+        } else if (fitIndex < 2) {
+            primaryIndex = 0;
+        } else {
+            primaryIndex = 2;
+        }
+        secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
+        outColorsExtraDark.setMainColor(getColorInt(primaryIndex, h, s, l));
+        outColorsExtraDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
+
+        outColorsNormal.setSupportsDarkText(supportsDarkText);
+        outColorsDark.setSupportsDarkText(supportsDarkText);
+        outColorsExtraDark.setSupportsDarkText(supportsDarkText);
+
+        if (DEBUG) {
+            Log.d(TAG, "Gradients: \n\tNormal " + outColorsNormal + "\n\tDark " + outColorsDark
+                    + "\n\tExtra dark: " + outColorsExtraDark);
+        }
+
+        return true;
+    }
+
+    private void applyFallback(@Nullable WallpaperColors inWallpaperColors,
+            GradientColors outColorsNormal, GradientColors outColorsDark,
+            GradientColors outColorsExtraDark) {
+        applyFallback(inWallpaperColors, outColorsNormal);
+        applyFallback(inWallpaperColors, outColorsDark);
+        applyFallback(inWallpaperColors, outColorsExtraDark);
+    }
+
+    /**
+     * Sets the gradient to the light or dark fallbacks based on the current wallpaper colors.
+     *
+     * @param inWallpaperColors Colors to read.
+     * @param outGradientColors Destination.
+     */
+    public static void applyFallback(@Nullable WallpaperColors inWallpaperColors,
+            @NonNull GradientColors outGradientColors) {
+        boolean light = inWallpaperColors != null
+                && (inWallpaperColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT)
+                != 0;
+        int innerColor = light ? MAIN_COLOR_LIGHT : MAIN_COLOR_DARK;
+        int outerColor = light ? SECONDARY_COLOR_LIGHT : SECONDARY_COLOR_DARK;
+
+        outGradientColors.setMainColor(innerColor);
+        outGradientColors.setSecondaryColor(outerColor);
+        outGradientColors.setSupportsDarkText(light);
+    }
+
+    private int getColorInt(int fitIndex, float[] h, float[] s, float[] l) {
+        mTmpHSL[0] = fract(h[fitIndex]) * 360.0f;
+        mTmpHSL[1] = s[fitIndex];
+        mTmpHSL[2] = l[fitIndex];
+        return ColorUtils.HSLToColor(mTmpHSL);
+    }
+
+    /**
+     * Checks if a given color exists in the blacklist
+     * @param hsl float array with 3 components (H 0..360, S 0..1 and L 0..1)
+     * @return true if color should be avoided
+     */
+    private boolean isBlacklisted(float[] hsl) {
+        for (int i = mBlacklistedColors.size() - 1; i >= 0; i--) {
+            ColorRange badRange = mBlacklistedColors.get(i);
+            if (badRange.containsColor(hsl[0], hsl[1], hsl[2])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Offsets all colors by a delta, clamping values that go beyond what's
+     * supported on the color space.
+     * @param data what you want to fit
+     * @param v how big should be the offset
+     * @param index which index to calculate the delta against
+     * @param min minimum accepted value (clamp)
+     * @param max maximum accepted value (clamp)
+     * @return new shifted palette
+     */
+    private static float[] fit(float[] data, float v, int index, float min, float max) {
+        float[] fitData = new float[data.length];
+        float delta = v - data[index];
+
+        for (int i = 0; i < data.length; i++) {
+            fitData[i] = MathUtils.constrain(data[i] + delta, min, max);
+        }
+
+        return fitData;
+    }
+
+    /**
+     * Finds the closest color in a palette, given another HSL color
+     *
+     * @param palette where to search
+     * @param h hue
+     * @param s saturation
+     * @param l lightness
+     * @return closest index or -1 if palette is empty.
+     */
+    private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) {
+        int minErrorIndex = -1;
+        float minError = Float.POSITIVE_INFINITY;
+
+        for (int i = 0; i < palette.h.length; i++) {
+            float error =
+                    FIT_WEIGHT_H * Math.abs(h - palette.h[i])
+                            + FIT_WEIGHT_S * Math.abs(s - palette.s[i])
+                            + FIT_WEIGHT_L * Math.abs(l - palette.l[i]);
+            if (error < minError) {
+                minError = error;
+                minErrorIndex = i;
+            }
+        }
+
+        return minErrorIndex;
+    }
+
+    @VisibleForTesting
+    public List<ColorRange> getBlacklistedColors() {
+        return mBlacklistedColors;
+    }
+
+    @Nullable
+    private TonalPalette findTonalPalette(float h, float s) {
+        // Fallback to a grey palette if the color is too desaturated.
+        // This avoids hue shifts.
+        if (s < 0.05f) {
+            return mGreyPalette;
+        }
+
+        TonalPalette best = null;
+        float error = Float.POSITIVE_INFINITY;
+
+        final int tonalPalettesCount = mTonalPalettes.size();
+        for (int i = 0; i < tonalPalettesCount; i++) {
+            final TonalPalette candidate = mTonalPalettes.get(i);
+
+            if (h >= candidate.minHue && h <= candidate.maxHue) {
+                best = candidate;
+                break;
+            }
+
+            if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) {
+                best = candidate;
+                break;
+            }
+
+            if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) {
+                best = candidate;
+                break;
+            }
+
+            if (h <= candidate.minHue && candidate.minHue - h < error) {
+                best = candidate;
+                error = candidate.minHue - h;
+            } else if (h >= candidate.maxHue && h - candidate.maxHue < error) {
+                best = candidate;
+                error = h - candidate.maxHue;
+            } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue)
+                    && h - fract(candidate.maxHue) < error) {
+                best = candidate;
+                error = h - fract(candidate.maxHue);
+            } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue)
+                    && fract(candidate.minHue) - h < error) {
+                best = candidate;
+                error = fract(candidate.minHue) - h;
+            }
+        }
+
+        return best;
+    }
+
+    private static float fract(float v) {
+        return v - (float) Math.floor(v);
+    }
+
+    static class TonalPalette {
+        final float[] h;
+        final float[] s;
+        final float[] l;
+        final float minHue;
+        final float maxHue;
+
+        TonalPalette(float[] h, float[] s, float[] l) {
+            if (h.length != s.length || s.length != l.length) {
+                throw new IllegalArgumentException("All arrays should have the same size. h: "
+                        + Arrays.toString(h) + " s: " + Arrays.toString(s) + " l: "
+                        + Arrays.toString(l));
+            }
+            this.h = h;
+            this.s = s;
+            this.l = l;
+
+            float minHue = Float.POSITIVE_INFINITY;
+            float maxHue = Float.NEGATIVE_INFINITY;
+
+            for (float v : h) {
+                minHue = Math.min(v, minHue);
+                maxHue = Math.max(v, maxHue);
+            }
+
+            this.minHue = minHue;
+            this.maxHue = maxHue;
+        }
+    }
+
+    /**
+     * Representation of an HSL color range.
+     * <ul>
+     * <li>hsl[0] is Hue [0 .. 360)</li>
+     * <li>hsl[1] is Saturation [0...1]</li>
+     * <li>hsl[2] is Lightness [0...1]</li>
+     * </ul>
+     */
+    @VisibleForTesting
+    public static class ColorRange {
+        private Range<Float> mHue;
+        private Range<Float> mSaturation;
+        private Range<Float> mLightness;
+
+        public ColorRange(Range<Float> hue, Range<Float> saturation, Range<Float> lightness) {
+            mHue = hue;
+            mSaturation = saturation;
+            mLightness = lightness;
+        }
+
+        public boolean containsColor(float h, float s, float l) {
+            if (!mHue.contains(h)) {
+                return false;
+            } else if (!mSaturation.contains(s)) {
+                return false;
+            } else if (!mLightness.contains(l)) {
+                return false;
+            }
+            return true;
+        }
+
+        public float[] getCenter() {
+            return new float[] {
+                    mHue.getLower() + (mHue.getUpper() - mHue.getLower()) / 2f,
+                    mSaturation.getLower() + (mSaturation.getUpper() - mSaturation.getLower()) / 2f,
+                    mLightness.getLower() + (mLightness.getUpper() - mLightness.getLower()) / 2f
+            };
+        }
+
+        @Override
+        public String toString() {
+            return String.format("H: %s, S: %s, L %s", mHue, mSaturation, mLightness);
+        }
+    }
+
+    @VisibleForTesting
+    public static class ConfigParser {
+        private final ArrayList<TonalPalette> mTonalPalettes;
+        private final ArrayList<ColorRange> mBlacklistedColors;
+
+        public ConfigParser(Context context) {
+            mTonalPalettes = new ArrayList<>();
+            mBlacklistedColors = new ArrayList<>();
+
+            // Load all palettes and the blacklist from an XML.
+            try {
+                XmlPullParser parser = context.getResources().getXml(R.xml.color_extraction);
+                int eventType = parser.getEventType();
+                while (eventType != XmlPullParser.END_DOCUMENT) {
+                    if (eventType == XmlPullParser.START_DOCUMENT ||
+                            eventType == XmlPullParser.END_TAG) {
+                        // just skip
+                    } else if (eventType == XmlPullParser.START_TAG) {
+                        String tagName = parser.getName();
+                        if (tagName.equals("palettes")) {
+                            parsePalettes(parser);
+                        } else if (tagName.equals("blacklist")) {
+                            parseBlacklist(parser);
+                        }
+                    } else {
+                        throw new XmlPullParserException("Invalid XML event " + eventType + " - "
+                                + parser.getName(), parser, null);
+                    }
+                    eventType = parser.next();
+                }
+            } catch (XmlPullParserException | IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public ArrayList<TonalPalette> getTonalPalettes() {
+            return mTonalPalettes;
+        }
+
+        public ArrayList<ColorRange> getBlacklistedColors() {
+            return mBlacklistedColors;
+        }
+
+        private void parseBlacklist(XmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            parser.require(XmlPullParser.START_TAG, null, "blacklist");
+            while (parser.next() != XmlPullParser.END_TAG) {
+                if (parser.getEventType() != XmlPullParser.START_TAG) {
+                    continue;
+                }
+                String name = parser.getName();
+                // Starts by looking for the entry tag
+                if (name.equals("range")) {
+                    mBlacklistedColors.add(readRange(parser));
+                    parser.next();
+                } else {
+                    throw new XmlPullParserException("Invalid tag: " + name, parser, null);
+                }
+            }
+        }
+
+        private ColorRange readRange(XmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            parser.require(XmlPullParser.START_TAG, null, "range");
+            float[] h = readFloatArray(parser.getAttributeValue(null, "h"));
+            float[] s = readFloatArray(parser.getAttributeValue(null, "s"));
+            float[] l = readFloatArray(parser.getAttributeValue(null, "l"));
+
+            if (h == null || s == null || l == null) {
+                throw new XmlPullParserException("Incomplete range tag.", parser, null);
+            }
+
+            return new ColorRange(new Range<>(h[0], h[1]), new Range<>(s[0], s[1]),
+                    new Range<>(l[0], l[1]));
+        }
+
+        private void parsePalettes(XmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            parser.require(XmlPullParser.START_TAG, null, "palettes");
+            while (parser.next() != XmlPullParser.END_TAG) {
+                if (parser.getEventType() != XmlPullParser.START_TAG) {
+                    continue;
+                }
+                String name = parser.getName();
+                // Starts by looking for the entry tag
+                if (name.equals("palette")) {
+                    mTonalPalettes.add(readPalette(parser));
+                    parser.next();
+                } else {
+                    throw new XmlPullParserException("Invalid tag: " + name);
+                }
+            }
+        }
+
+        private TonalPalette readPalette(XmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            parser.require(XmlPullParser.START_TAG, null, "palette");
+
+            float[] h = readFloatArray(parser.getAttributeValue(null, "h"));
+            float[] s = readFloatArray(parser.getAttributeValue(null, "s"));
+            float[] l = readFloatArray(parser.getAttributeValue(null, "l"));
+
+            if (h == null || s == null || l == null) {
+                throw new XmlPullParserException("Incomplete range tag.", parser, null);
+            }
+
+            return new TonalPalette(h, s, l);
+        }
+
+        private float[] readFloatArray(String attributeValue)
+                throws IOException, XmlPullParserException {
+            String[] tokens = attributeValue.replaceAll(" ", "").replaceAll("\n", "").split(",");
+            float[] numbers = new float[tokens.length];
+            for (int i = 0; i < tokens.length; i++) {
+                numbers[i] = Float.parseFloat(tokens[i]);
+            }
+            return numbers;
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/content/FileSystemProvider.java b/com/android/internal/content/FileSystemProvider.java
new file mode 100644
index 0000000..d49d572
--- /dev/null
+++ b/com/android/internal/content/FileSystemProvider.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2017 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.internal.content;
+
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.FileObserver;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsProvider;
+import android.provider.MediaStore;
+import android.provider.MetadataReader;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.internal.annotations.GuardedBy;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local
+ * files.
+ */
+public abstract class FileSystemProvider extends DocumentsProvider {
+
+    private static final String TAG = "FileSystemProvider";
+
+    private static final boolean LOG_INOTIFY = false;
+
+    private String[] mDefaultProjection;
+
+    @GuardedBy("mObservers")
+    private final ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();
+
+    private Handler mHandler;
+
+    private static final String MIMETYPE_JPEG = "image/jpeg";
+    private static final String MIMETYPE_JPG = "image/jpg";
+    private static final String MIMETYPE_OCTET_STREAM = "application/octet-stream";
+
+    protected abstract File getFileForDocId(String docId, boolean visible)
+            throws FileNotFoundException;
+
+    protected abstract String getDocIdForFile(File file) throws FileNotFoundException;
+
+    protected abstract Uri buildNotificationUri(String docId);
+
+    @Override
+    public boolean onCreate() {
+        throw new UnsupportedOperationException(
+                "Subclass should override this and call onCreate(defaultDocumentProjection)");
+    }
+
+    @CallSuper
+    protected void onCreate(String[] defaultProjection) {
+        mHandler = new Handler();
+        mDefaultProjection = defaultProjection;
+    }
+
+    @Override
+    public boolean isChildDocument(String parentDocId, String docId) {
+        try {
+            final File parent = getFileForDocId(parentDocId).getCanonicalFile();
+            final File doc = getFileForDocId(docId).getCanonicalFile();
+            return FileUtils.contains(parent, doc);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(
+                    "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
+        }
+    }
+
+    @Override
+    public @Nullable Bundle getDocumentMetadata(String documentId)
+            throws FileNotFoundException {
+        File file = getFileForDocId(documentId);
+
+        if (!file.exists()) {
+            throw new FileNotFoundException("Can't find the file for documentId: " + documentId);
+        }
+
+        if (!file.isFile()) {
+            Log.w(TAG, "Can't stream non-regular file. Returning empty metadata.");
+            return null;
+        }
+
+        if (!file.canRead()) {
+            Log.w(TAG, "Can't stream non-readable file. Returning empty metadata.");
+            return null;
+        }
+
+        String mimeType = getTypeForFile(file);
+        if (!MetadataReader.isSupportedMimeType(mimeType)) {
+            return null;
+        }
+
+        InputStream stream = null;
+        try {
+            Bundle metadata = new Bundle();
+            stream = new FileInputStream(file.getAbsolutePath());
+            MetadataReader.getMetadata(metadata, stream, mimeType, null);
+            return metadata;
+        } catch (IOException e) {
+            Log.e(TAG, "An error occurred retrieving the metadata", e);
+            return null;
+        } finally {
+            IoUtils.closeQuietly(stream);
+        }
+    }
+
+    protected final List<String> findDocumentPath(File parent, File doc)
+            throws FileNotFoundException {
+
+        if (!doc.exists()) {
+            throw new FileNotFoundException(doc + " is not found.");
+        }
+
+        if (!FileUtils.contains(parent, doc)) {
+            throw new FileNotFoundException(doc + " is not found under " + parent);
+        }
+
+        LinkedList<String> path = new LinkedList<>();
+        while (doc != null && FileUtils.contains(parent, doc)) {
+            path.addFirst(getDocIdForFile(doc));
+
+            doc = doc.getParentFile();
+        }
+
+        return path;
+    }
+
+    @Override
+    public String createDocument(String docId, String mimeType, String displayName)
+            throws FileNotFoundException {
+        displayName = FileUtils.buildValidFatFilename(displayName);
+
+        final File parent = getFileForDocId(docId);
+        if (!parent.isDirectory()) {
+            throw new IllegalArgumentException("Parent document isn't a directory");
+        }
+
+        final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
+        final String childId;
+        if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+            if (!file.mkdir()) {
+                throw new IllegalStateException("Failed to mkdir " + file);
+            }
+            childId = getDocIdForFile(file);
+            addFolderToMediaStore(getFileForDocId(childId, true));
+        } else {
+            try {
+                if (!file.createNewFile()) {
+                    throw new IllegalStateException("Failed to touch " + file);
+                }
+                childId = getDocIdForFile(file);
+            } catch (IOException e) {
+                throw new IllegalStateException("Failed to touch " + file + ": " + e);
+            }
+        }
+
+        return childId;
+    }
+
+    private void addFolderToMediaStore(@Nullable File visibleFolder) {
+        // visibleFolder is null if we're adding a folder to external thumb drive or SD card.
+        if (visibleFolder != null) {
+            assert (visibleFolder.isDirectory());
+
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                final ContentResolver resolver = getContext().getContentResolver();
+                final Uri uri = MediaStore.Files.getDirectoryUri("external");
+                ContentValues values = new ContentValues();
+                values.put(MediaStore.Files.FileColumns.DATA, visibleFolder.getAbsolutePath());
+                resolver.insert(uri, values);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    @Override
+    public String renameDocument(String docId, String displayName) throws FileNotFoundException {
+        // Since this provider treats renames as generating a completely new
+        // docId, we're okay with letting the MIME type change.
+        displayName = FileUtils.buildValidFatFilename(displayName);
+
+        final File before = getFileForDocId(docId);
+        final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName);
+        final File visibleFileBefore = getFileForDocId(docId, true);
+        if (!before.renameTo(after)) {
+            throw new IllegalStateException("Failed to rename to " + after);
+        }
+
+        final String afterDocId = getDocIdForFile(after);
+        moveInMediaStore(visibleFileBefore, getFileForDocId(afterDocId, true));
+
+        if (!TextUtils.equals(docId, afterDocId)) {
+            return afterDocId;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
+            String targetParentDocumentId)
+            throws FileNotFoundException {
+        final File before = getFileForDocId(sourceDocumentId);
+        final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
+        final File visibleFileBefore = getFileForDocId(sourceDocumentId, true);
+
+        if (after.exists()) {
+            throw new IllegalStateException("Already exists " + after);
+        }
+        if (!before.renameTo(after)) {
+            throw new IllegalStateException("Failed to move to " + after);
+        }
+
+        final String docId = getDocIdForFile(after);
+        moveInMediaStore(visibleFileBefore, getFileForDocId(docId, true));
+
+        return docId;
+    }
+
+    private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) {
+        // visibleFolders are null if we're moving a document in external thumb drive or SD card.
+        //
+        // They should be all null or not null at the same time. File#renameTo() doesn't work across
+        // volumes so an exception will be thrown before calling this method.
+        if (oldVisibleFile != null && newVisibleFile != null) {
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                final ContentResolver resolver = getContext().getContentResolver();
+                final Uri externalUri = newVisibleFile.isDirectory()
+                        ? MediaStore.Files.getDirectoryUri("external")
+                        : MediaStore.Files.getContentUri("external");
+
+                ContentValues values = new ContentValues();
+                values.put(MediaStore.Files.FileColumns.DATA, newVisibleFile.getAbsolutePath());
+
+                // Logic borrowed from MtpDatabase.
+                // note - we are relying on a special case in MediaProvider.update() to update
+                // the paths for all children in the case where this is a directory.
+                final String path = oldVisibleFile.getAbsolutePath();
+                resolver.update(externalUri,
+                        values,
+                        "_data LIKE ? AND lower(_data)=lower(?)",
+                        new String[]{path, path});
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    @Override
+    public void deleteDocument(String docId) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        final File visibleFile = getFileForDocId(docId, true);
+
+        final boolean isDirectory = file.isDirectory();
+        if (isDirectory) {
+            FileUtils.deleteContents(file);
+        }
+        if (!file.delete()) {
+            throw new IllegalStateException("Failed to delete " + file);
+        }
+
+        removeFromMediaStore(visibleFile, isDirectory);
+    }
+
+    private void removeFromMediaStore(@Nullable File visibleFile, boolean isFolder)
+            throws FileNotFoundException {
+        // visibleFolder is null if we're removing a document from external thumb drive or SD card.
+        if (visibleFile != null) {
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                final ContentResolver resolver = getContext().getContentResolver();
+                final Uri externalUri = MediaStore.Files.getContentUri("external");
+
+                // Remove media store entries for any files inside this directory, using
+                // path prefix match. Logic borrowed from MtpDatabase.
+                if (isFolder) {
+                    final String path = visibleFile.getAbsolutePath() + "/";
+                    resolver.delete(externalUri,
+                            "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
+                            new String[]{path + "%", Integer.toString(path.length()), path});
+                }
+
+                // Remove media store entry for this exact file.
+                final String path = visibleFile.getAbsolutePath();
+                resolver.delete(externalUri,
+                        "_data LIKE ?1 AND lower(_data)=lower(?2)",
+                        new String[]{path, path});
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
+        includeFile(result, documentId, null);
+        return result;
+    }
+
+    @Override
+    public Cursor queryChildDocuments(
+            String parentDocumentId, String[] projection, String sortOrder)
+            throws FileNotFoundException {
+
+        final File parent = getFileForDocId(parentDocumentId);
+        final MatrixCursor result = new DirectoryCursor(
+                resolveProjection(projection), parentDocumentId, parent);
+        for (File file : parent.listFiles()) {
+            includeFile(result, null, file);
+        }
+        return result;
+    }
+
+    /**
+     * Searches documents under the given folder.
+     *
+     * To avoid runtime explosion only returns the at most 23 items.
+     *
+     * @param folder the root folder where recursive search begins
+     * @param query the search condition used to match file names
+     * @param projection projection of the returned cursor
+     * @param exclusion absolute file paths to exclude from result
+     * @return cursor containing search result
+     * @throws FileNotFoundException when root folder doesn't exist or search fails
+     */
+    protected final Cursor querySearchDocuments(
+            File folder, String query, String[] projection, Set<String> exclusion)
+            throws FileNotFoundException {
+
+        query = query.toLowerCase();
+        final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
+        final LinkedList<File> pending = new LinkedList<>();
+        pending.add(folder);
+        while (!pending.isEmpty() && result.getCount() < 24) {
+            final File file = pending.removeFirst();
+            if (file.isDirectory()) {
+                for (File child : file.listFiles()) {
+                    pending.add(child);
+                }
+            }
+            if (file.getName().toLowerCase().contains(query)
+                    && !exclusion.contains(file.getAbsolutePath())) {
+                includeFile(result, null, file);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String getDocumentType(String documentId) throws FileNotFoundException {
+        final File file = getFileForDocId(documentId);
+        return getTypeForFile(file);
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(
+            String documentId, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        final File file = getFileForDocId(documentId);
+        final File visibleFile = getFileForDocId(documentId, true);
+
+        final int pfdMode = ParcelFileDescriptor.parseMode(mode);
+        if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
+            return ParcelFileDescriptor.open(file, pfdMode);
+        } else {
+            try {
+                // When finished writing, kick off media scanner
+                return ParcelFileDescriptor.open(
+                        file, pfdMode, mHandler, (IOException e) -> scanFile(visibleFile));
+            } catch (IOException e) {
+                throw new FileNotFoundException("Failed to open for writing: " + e);
+            }
+        }
+    }
+
+    private void scanFile(File visibleFile) {
+        final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+        intent.setData(Uri.fromFile(visibleFile));
+        getContext().sendBroadcast(intent);
+    }
+
+    @Override
+    public AssetFileDescriptor openDocumentThumbnail(
+            String documentId, Point sizeHint, CancellationSignal signal)
+            throws FileNotFoundException {
+        final File file = getFileForDocId(documentId);
+        return DocumentsContract.openImageThumbnail(file);
+    }
+
+    protected RowBuilder includeFile(MatrixCursor result, String docId, File file)
+            throws FileNotFoundException {
+        if (docId == null) {
+            docId = getDocIdForFile(file);
+        } else {
+            file = getFileForDocId(docId);
+        }
+
+        int flags = 0;
+
+        if (file.canWrite()) {
+            if (file.isDirectory()) {
+                flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
+                flags |= Document.FLAG_SUPPORTS_DELETE;
+                flags |= Document.FLAG_SUPPORTS_RENAME;
+                flags |= Document.FLAG_SUPPORTS_MOVE;
+            } else {
+                flags |= Document.FLAG_SUPPORTS_WRITE;
+                flags |= Document.FLAG_SUPPORTS_DELETE;
+                flags |= Document.FLAG_SUPPORTS_RENAME;
+                flags |= Document.FLAG_SUPPORTS_MOVE;
+            }
+        }
+
+        final String mimeType = getTypeForFile(file);
+        final String displayName = file.getName();
+        if (mimeType.startsWith("image/")) {
+            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
+        }
+
+        if (typeSupportsMetadata(mimeType)) {
+            flags |= Document.FLAG_SUPPORTS_METADATA;
+        }
+
+        final RowBuilder row = result.newRow();
+        row.add(Document.COLUMN_DOCUMENT_ID, docId);
+        row.add(Document.COLUMN_DISPLAY_NAME, displayName);
+        row.add(Document.COLUMN_SIZE, file.length());
+        row.add(Document.COLUMN_MIME_TYPE, mimeType);
+        row.add(Document.COLUMN_FLAGS, flags);
+
+        // Only publish dates reasonably after epoch
+        long lastModified = file.lastModified();
+        if (lastModified > 31536000000L) {
+            row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
+        }
+
+        // Return the row builder just in case any subclass want to add more stuff to it.
+        return row;
+    }
+
+    private static String getTypeForFile(File file) {
+        if (file.isDirectory()) {
+            return Document.MIME_TYPE_DIR;
+        } else {
+            return getTypeForName(file.getName());
+        }
+    }
+
+    protected boolean typeSupportsMetadata(String mimeType) {
+        return MetadataReader.isSupportedMimeType(mimeType);
+    }
+
+    private static String getTypeForName(String name) {
+        final int lastDot = name.lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = name.substring(lastDot + 1).toLowerCase();
+            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+            if (mime != null) {
+                return mime;
+            }
+        }
+
+        return MIMETYPE_OCTET_STREAM;
+    }
+
+    protected final File getFileForDocId(String docId) throws FileNotFoundException {
+        return getFileForDocId(docId, false);
+    }
+
+    private String[] resolveProjection(String[] projection) {
+        return projection == null ? mDefaultProjection : projection;
+    }
+
+    private void startObserving(File file, Uri notifyUri) {
+        synchronized (mObservers) {
+            DirectoryObserver observer = mObservers.get(file);
+            if (observer == null) {
+                observer = new DirectoryObserver(
+                        file, getContext().getContentResolver(), notifyUri);
+                observer.startWatching();
+                mObservers.put(file, observer);
+            }
+            observer.mRefCount++;
+
+            if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
+        }
+    }
+
+    private void stopObserving(File file) {
+        synchronized (mObservers) {
+            DirectoryObserver observer = mObservers.get(file);
+            if (observer == null) return;
+
+            observer.mRefCount--;
+            if (observer.mRefCount == 0) {
+                mObservers.remove(file);
+                observer.stopWatching();
+            }
+
+            if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
+        }
+    }
+
+    private static class DirectoryObserver extends FileObserver {
+        private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
+                | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
+
+        private final File mFile;
+        private final ContentResolver mResolver;
+        private final Uri mNotifyUri;
+
+        private int mRefCount = 0;
+
+        public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
+            super(file.getAbsolutePath(), NOTIFY_EVENTS);
+            mFile = file;
+            mResolver = resolver;
+            mNotifyUri = notifyUri;
+        }
+
+        @Override
+        public void onEvent(int event, String path) {
+            if ((event & NOTIFY_EVENTS) != 0) {
+                if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
+                mResolver.notifyChange(mNotifyUri, null, false);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
+        }
+    }
+
+    private class DirectoryCursor extends MatrixCursor {
+        private final File mFile;
+
+        public DirectoryCursor(String[] columnNames, String docId, File file) {
+            super(columnNames);
+
+            final Uri notifyUri = buildNotificationUri(docId);
+            setNotificationUri(getContext().getContentResolver(), notifyUri);
+
+            mFile = file;
+            startObserving(mFile, notifyUri);
+        }
+
+        @Override
+        public void close() {
+            super.close();
+            stopObserving(mFile);
+        }
+    }
+}
diff --git a/com/android/internal/content/NativeLibraryHelper.java b/com/android/internal/content/NativeLibraryHelper.java
new file mode 100644
index 0000000..83b7d2f
--- /dev/null
+++ b/com/android/internal/content/NativeLibraryHelper.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2010 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.internal.content;
+
+import static android.content.pm.PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS;
+import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
+import static android.content.pm.PackageManager.NO_NATIVE_LIBRARIES;
+import static android.system.OsConstants.S_IRGRP;
+import static android.system.OsConstants.S_IROTH;
+import static android.system.OsConstants.S_IRWXU;
+import static android.system.OsConstants.S_IXGRP;
+import static android.system.OsConstants.S_IXOTH;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.Package;
+import android.content.pm.PackageParser.PackageLite;
+import android.content.pm.PackageParser.PackageParserException;
+import android.os.Build;
+import android.os.SELinux;
+import android.os.SystemProperties;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Slog;
+
+import dalvik.system.CloseGuard;
+import dalvik.system.VMRuntime;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Native libraries helper.
+ *
+ * @hide
+ */
+public class NativeLibraryHelper {
+    private static final String TAG = "NativeHelper";
+    private static final boolean DEBUG_NATIVE = false;
+
+    public static final String LIB_DIR_NAME = "lib";
+    public static final String LIB64_DIR_NAME = "lib64";
+
+    // Special value for {@code PackageParser.Package#cpuAbiOverride} to indicate
+    // that the cpuAbiOverride must be clear.
+    public static final String CLEAR_ABI_OVERRIDE = "-";
+
+    /**
+     * A handle to an opened package, consisting of one or more APKs. Used as
+     * input to the various NativeLibraryHelper methods. Allows us to scan and
+     * parse the APKs exactly once instead of doing it multiple times.
+     *
+     * @hide
+     */
+    public static class Handle implements Closeable {
+        private final CloseGuard mGuard = CloseGuard.get();
+        private volatile boolean mClosed;
+
+        final long[] apkHandles;
+        final boolean multiArch;
+        final boolean extractNativeLibs;
+        final boolean debuggable;
+
+        public static Handle create(File packageFile) throws IOException {
+            try {
+                final PackageLite lite = PackageParser.parsePackageLite(packageFile, 0);
+                return create(lite);
+            } catch (PackageParserException e) {
+                throw new IOException("Failed to parse package: " + packageFile, e);
+            }
+        }
+
+        public static Handle create(Package pkg) throws IOException {
+            return create(pkg.getAllCodePaths(),
+                    (pkg.applicationInfo.flags & ApplicationInfo.FLAG_MULTIARCH) != 0,
+                    (pkg.applicationInfo.flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) != 0,
+                    (pkg.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
+        }
+
+        public static Handle create(PackageLite lite) throws IOException {
+            return create(lite.getAllCodePaths(), lite.multiArch, lite.extractNativeLibs,
+                    lite.debuggable);
+        }
+
+        private static Handle create(List<String> codePaths, boolean multiArch,
+                boolean extractNativeLibs, boolean debuggable) throws IOException {
+            final int size = codePaths.size();
+            final long[] apkHandles = new long[size];
+            for (int i = 0; i < size; i++) {
+                final String path = codePaths.get(i);
+                apkHandles[i] = nativeOpenApk(path);
+                if (apkHandles[i] == 0) {
+                    // Unwind everything we've opened so far
+                    for (int j = 0; j < i; j++) {
+                        nativeClose(apkHandles[j]);
+                    }
+                    throw new IOException("Unable to open APK: " + path);
+                }
+            }
+
+            return new Handle(apkHandles, multiArch, extractNativeLibs, debuggable);
+        }
+
+        Handle(long[] apkHandles, boolean multiArch, boolean extractNativeLibs,
+                boolean debuggable) {
+            this.apkHandles = apkHandles;
+            this.multiArch = multiArch;
+            this.extractNativeLibs = extractNativeLibs;
+            this.debuggable = debuggable;
+            mGuard.open("close");
+        }
+
+        @Override
+        public void close() {
+            for (long apkHandle : apkHandles) {
+                nativeClose(apkHandle);
+            }
+            mGuard.close();
+            mClosed = true;
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            if (mGuard != null) {
+                mGuard.warnIfOpen();
+            }
+            try {
+                if (!mClosed) {
+                    close();
+                }
+            } finally {
+                super.finalize();
+            }
+        }
+    }
+
+    private static native long nativeOpenApk(String path);
+    private static native void nativeClose(long handle);
+
+    private static native long nativeSumNativeBinaries(long handle, String cpuAbi,
+            boolean debuggable);
+
+    private native static int nativeCopyNativeBinaries(long handle, String sharedLibraryPath,
+            String abiToCopy, boolean extractNativeLibs, boolean hasNativeBridge,
+            boolean debuggable);
+
+    private static long sumNativeBinaries(Handle handle, String abi) {
+        long sum = 0;
+        for (long apkHandle : handle.apkHandles) {
+            sum += nativeSumNativeBinaries(apkHandle, abi, handle.debuggable);
+        }
+        return sum;
+    }
+
+    /**
+     * Copies native binaries to a shared library directory.
+     *
+     * @param handle APK file to scan for native libraries
+     * @param sharedLibraryDir directory for libraries to be copied to
+     * @return {@link PackageManager#INSTALL_SUCCEEDED} if successful or another
+     *         error code from that class if not
+     */
+    public static int copyNativeBinaries(Handle handle, File sharedLibraryDir, String abi) {
+        for (long apkHandle : handle.apkHandles) {
+            int res = nativeCopyNativeBinaries(apkHandle, sharedLibraryDir.getPath(), abi,
+                    handle.extractNativeLibs, HAS_NATIVE_BRIDGE, handle.debuggable);
+            if (res != INSTALL_SUCCEEDED) {
+                return res;
+            }
+        }
+        return INSTALL_SUCCEEDED;
+    }
+
+    /**
+     * Checks if a given APK contains native code for any of the provided
+     * {@code supportedAbis}. Returns an index into {@code supportedAbis} if a matching
+     * ABI is found, {@link PackageManager#NO_NATIVE_LIBRARIES} if the
+     * APK doesn't contain any native code, and
+     * {@link PackageManager#INSTALL_FAILED_NO_MATCHING_ABIS} if none of the ABIs match.
+     */
+    public static int findSupportedAbi(Handle handle, String[] supportedAbis) {
+        int finalRes = NO_NATIVE_LIBRARIES;
+        for (long apkHandle : handle.apkHandles) {
+            final int res = nativeFindSupportedAbi(apkHandle, supportedAbis, handle.debuggable);
+            if (res == NO_NATIVE_LIBRARIES) {
+                // No native code, keep looking through all APKs.
+            } else if (res == INSTALL_FAILED_NO_MATCHING_ABIS) {
+                // Found some native code, but no ABI match; update our final
+                // result if we haven't found other valid code.
+                if (finalRes < 0) {
+                    finalRes = INSTALL_FAILED_NO_MATCHING_ABIS;
+                }
+            } else if (res >= 0) {
+                // Found valid native code, track the best ABI match
+                if (finalRes < 0 || res < finalRes) {
+                    finalRes = res;
+                }
+            } else {
+                // Unexpected error; bail
+                return res;
+            }
+        }
+        return finalRes;
+    }
+
+    private native static int nativeFindSupportedAbi(long handle, String[] supportedAbis,
+            boolean debuggable);
+
+    // Convenience method to call removeNativeBinariesFromDirLI(File)
+    public static void removeNativeBinariesLI(String nativeLibraryPath) {
+        if (nativeLibraryPath == null) return;
+        removeNativeBinariesFromDirLI(new File(nativeLibraryPath), false /* delete root dir */);
+    }
+
+    /**
+     * Remove the native binaries of a given package. This deletes the files
+     */
+    public static void removeNativeBinariesFromDirLI(File nativeLibraryRoot,
+            boolean deleteRootDir) {
+        if (DEBUG_NATIVE) {
+            Slog.w(TAG, "Deleting native binaries from: " + nativeLibraryRoot.getPath());
+        }
+
+        /*
+         * Just remove any file in the directory. Since the directory is owned
+         * by the 'system' UID, the application is not supposed to have written
+         * anything there.
+         */
+        if (nativeLibraryRoot.exists()) {
+            final File[] files = nativeLibraryRoot.listFiles();
+            if (files != null) {
+                for (int nn = 0; nn < files.length; nn++) {
+                    if (DEBUG_NATIVE) {
+                        Slog.d(TAG, "    Deleting " + files[nn].getName());
+                    }
+
+                    if (files[nn].isDirectory()) {
+                        removeNativeBinariesFromDirLI(files[nn], true /* delete root dir */);
+                    } else if (!files[nn].delete()) {
+                        Slog.w(TAG, "Could not delete native binary: " + files[nn].getPath());
+                    }
+                }
+            }
+            // Do not delete 'lib' directory itself, unless we're specifically
+            // asked to or this will prevent installation of future updates.
+            if (deleteRootDir) {
+                if (!nativeLibraryRoot.delete()) {
+                    Slog.w(TAG, "Could not delete native binary directory: " +
+                            nativeLibraryRoot.getPath());
+                }
+            }
+        }
+    }
+
+    private static void createNativeLibrarySubdir(File path) throws IOException {
+        if (!path.isDirectory()) {
+            path.delete();
+
+            if (!path.mkdir()) {
+                throw new IOException("Cannot create " + path.getPath());
+            }
+
+            try {
+                Os.chmod(path.getPath(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
+            } catch (ErrnoException e) {
+                throw new IOException("Cannot chmod native library directory "
+                        + path.getPath(), e);
+            }
+        } else if (!SELinux.restorecon(path)) {
+            throw new IOException("Cannot set SELinux context for " + path.getPath());
+        }
+    }
+
+    private static long sumNativeBinariesForSupportedAbi(Handle handle, String[] abiList) {
+        int abi = findSupportedAbi(handle, abiList);
+        if (abi >= 0) {
+            return sumNativeBinaries(handle, abiList[abi]);
+        } else {
+            return 0;
+        }
+    }
+
+    public static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot,
+            String[] abiList, boolean useIsaSubdir) throws IOException {
+        createNativeLibrarySubdir(libraryRoot);
+
+        /*
+         * If this is an internal application or our nativeLibraryPath points to
+         * the app-lib directory, unpack the libraries if necessary.
+         */
+        int abi = findSupportedAbi(handle, abiList);
+        if (abi >= 0) {
+            /*
+             * If we have a matching instruction set, construct a subdir under the native
+             * library root that corresponds to this instruction set.
+             */
+            final String instructionSet = VMRuntime.getInstructionSet(abiList[abi]);
+            final File subDir;
+            if (useIsaSubdir) {
+                final File isaSubdir = new File(libraryRoot, instructionSet);
+                createNativeLibrarySubdir(isaSubdir);
+                subDir = isaSubdir;
+            } else {
+                subDir = libraryRoot;
+            }
+
+            int copyRet = copyNativeBinaries(handle, subDir, abiList[abi]);
+            if (copyRet != PackageManager.INSTALL_SUCCEEDED) {
+                return copyRet;
+            }
+        }
+
+        return abi;
+    }
+
+    public static int copyNativeBinariesWithOverride(Handle handle, File libraryRoot,
+            String abiOverride) {
+        try {
+            if (handle.multiArch) {
+                // Warn if we've set an abiOverride for multi-lib packages..
+                // By definition, we need to copy both 32 and 64 bit libraries for
+                // such packages.
+                if (abiOverride != null && !CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
+                    Slog.w(TAG, "Ignoring abiOverride for multi arch application.");
+                }
+
+                int copyRet = PackageManager.NO_NATIVE_LIBRARIES;
+                if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
+                    copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot,
+                            Build.SUPPORTED_32_BIT_ABIS, true /* use isa specific subdirs */);
+                    if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES &&
+                            copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) {
+                        Slog.w(TAG, "Failure copying 32 bit native libraries; copyRet=" +copyRet);
+                        return copyRet;
+                    }
+                }
+
+                if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
+                    copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot,
+                            Build.SUPPORTED_64_BIT_ABIS, true /* use isa specific subdirs */);
+                    if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES &&
+                            copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) {
+                        Slog.w(TAG, "Failure copying 64 bit native libraries; copyRet=" +copyRet);
+                        return copyRet;
+                    }
+                }
+            } else {
+                String cpuAbiOverride = null;
+                if (CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
+                    cpuAbiOverride = null;
+                } else if (abiOverride != null) {
+                    cpuAbiOverride = abiOverride;
+                }
+
+                String[] abiList = (cpuAbiOverride != null) ?
+                        new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS;
+                if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null &&
+                        hasRenderscriptBitcode(handle)) {
+                    abiList = Build.SUPPORTED_32_BIT_ABIS;
+                }
+
+                int copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, abiList,
+                        true /* use isa specific subdirs */);
+                if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) {
+                    Slog.w(TAG, "Failure copying native libraries [errorCode=" + copyRet + "]");
+                    return copyRet;
+                }
+            }
+
+            return PackageManager.INSTALL_SUCCEEDED;
+        } catch (IOException e) {
+            Slog.e(TAG, "Copying native libraries failed", e);
+            return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
+        }
+    }
+
+    public static long sumNativeBinariesWithOverride(Handle handle, String abiOverride)
+            throws IOException {
+        long sum = 0;
+        if (handle.multiArch) {
+            // Warn if we've set an abiOverride for multi-lib packages..
+            // By definition, we need to copy both 32 and 64 bit libraries for
+            // such packages.
+            if (abiOverride != null && !CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
+                Slog.w(TAG, "Ignoring abiOverride for multi arch application.");
+            }
+
+            if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
+                sum += sumNativeBinariesForSupportedAbi(handle, Build.SUPPORTED_32_BIT_ABIS);
+            }
+
+            if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
+                sum += sumNativeBinariesForSupportedAbi(handle, Build.SUPPORTED_64_BIT_ABIS);
+            }
+        } else {
+            String cpuAbiOverride = null;
+            if (CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
+                cpuAbiOverride = null;
+            } else if (abiOverride != null) {
+                cpuAbiOverride = abiOverride;
+            }
+
+            String[] abiList = (cpuAbiOverride != null) ?
+                    new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS;
+            if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null &&
+                    hasRenderscriptBitcode(handle)) {
+                abiList = Build.SUPPORTED_32_BIT_ABIS;
+            }
+
+            sum += sumNativeBinariesForSupportedAbi(handle, abiList);
+        }
+        return sum;
+    }
+
+    // We don't care about the other return values for now.
+    private static final int BITCODE_PRESENT = 1;
+
+    private static final boolean HAS_NATIVE_BRIDGE =
+            !"0".equals(SystemProperties.get("ro.dalvik.vm.native.bridge", "0"));
+
+    private static native int hasRenderscriptBitcode(long apkHandle);
+
+    public static boolean hasRenderscriptBitcode(Handle handle) throws IOException {
+        for (long apkHandle : handle.apkHandles) {
+            final int res = hasRenderscriptBitcode(apkHandle);
+            if (res < 0) {
+                throw new IOException("Error scanning APK, code: " + res);
+            } else if (res == BITCODE_PRESENT) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/com/android/internal/content/PackageHelper.java b/com/android/internal/content/PackageHelper.java
new file mode 100644
index 0000000..e923223
--- /dev/null
+++ b/com/android/internal/content/PackageHelper.java
@@ -0,0 +1,687 @@
+/*
+ * 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.internal.content;
+
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.os.storage.VolumeInfo.ID_PRIVATE_INTERNAL;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageParser.PackageLite;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.storage.IStorageManager;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageResultCode;
+import android.os.storage.StorageVolume;
+import android.os.storage.VolumeInfo;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Constants used internally between the PackageManager
+ * and media container service transports.
+ * Some utility methods to invoke StorageManagerService api.
+ */
+public class PackageHelper {
+    public static final int RECOMMEND_INSTALL_INTERNAL = 1;
+    public static final int RECOMMEND_INSTALL_EXTERNAL = 2;
+    public static final int RECOMMEND_INSTALL_EPHEMERAL = 3;
+    public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1;
+    public static final int RECOMMEND_FAILED_INVALID_APK = -2;
+    public static final int RECOMMEND_FAILED_INVALID_LOCATION = -3;
+    public static final int RECOMMEND_FAILED_ALREADY_EXISTS = -4;
+    public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5;
+    public static final int RECOMMEND_FAILED_INVALID_URI = -6;
+    public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7;
+
+    private static final boolean localLOGV = false;
+    private static final String TAG = "PackageHelper";
+    // App installation location settings values
+    public static final int APP_INSTALL_AUTO = 0;
+    public static final int APP_INSTALL_INTERNAL = 1;
+    public static final int APP_INSTALL_EXTERNAL = 2;
+
+    private static TestableInterface sDefaultTestableInterface = null;
+
+    public static IStorageManager getStorageManager() throws RemoteException {
+        IBinder service = ServiceManager.getService("mount");
+        if (service != null) {
+            return IStorageManager.Stub.asInterface(service);
+        } else {
+            Log.e(TAG, "Can't get storagemanager service");
+            throw new RemoteException("Could not contact storagemanager service");
+        }
+    }
+
+    public static String createSdDir(long sizeBytes, String cid, String sdEncKey, int uid,
+            boolean isExternal) {
+        // Round up to nearest MB, plus another MB for filesystem overhead
+        final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1;
+        try {
+            IStorageManager storageManager = getStorageManager();
+
+            if (localLOGV)
+                Log.i(TAG, "Size of container " + sizeMb + " MB");
+
+            int rc = storageManager.createSecureContainer(cid, sizeMb, "ext4", sdEncKey, uid,
+                    isExternal);
+            if (rc != StorageResultCode.OperationSucceeded) {
+                Log.e(TAG, "Failed to create secure container " + cid);
+                return null;
+            }
+            String cachePath = storageManager.getSecureContainerPath(cid);
+            if (localLOGV) Log.i(TAG, "Created secure container " + cid +
+                    " at " + cachePath);
+                return cachePath;
+        } catch (RemoteException e) {
+            Log.e(TAG, "StorageManagerService running?");
+        }
+        return null;
+    }
+
+    public static boolean resizeSdDir(long sizeBytes, String cid, String sdEncKey) {
+        // Round up to nearest MB, plus another MB for filesystem overhead
+        final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1;
+        try {
+            IStorageManager storageManager = getStorageManager();
+            int rc = storageManager.resizeSecureContainer(cid, sizeMb, sdEncKey);
+            if (rc == StorageResultCode.OperationSucceeded) {
+                return true;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "StorageManagerService running?");
+        }
+        Log.e(TAG, "Failed to create secure container " + cid);
+        return false;
+    }
+
+    public static String mountSdDir(String cid, String key, int ownerUid) {
+        return mountSdDir(cid, key, ownerUid, true);
+    }
+
+    public static String mountSdDir(String cid, String key, int ownerUid, boolean readOnly) {
+        try {
+            int rc = getStorageManager().mountSecureContainer(cid, key, ownerUid, readOnly);
+            if (rc != StorageResultCode.OperationSucceeded) {
+                Log.i(TAG, "Failed to mount container " + cid + " rc : " + rc);
+                return null;
+            }
+            return getStorageManager().getSecureContainerPath(cid);
+        } catch (RemoteException e) {
+            Log.e(TAG, "StorageManagerService running?");
+        }
+        return null;
+    }
+
+   public static boolean unMountSdDir(String cid) {
+    try {
+        int rc = getStorageManager().unmountSecureContainer(cid, true);
+        if (rc != StorageResultCode.OperationSucceeded) {
+            Log.e(TAG, "Failed to unmount " + cid + " with rc " + rc);
+            return false;
+        }
+        return true;
+    } catch (RemoteException e) {
+        Log.e(TAG, "StorageManagerService running?");
+    }
+        return false;
+   }
+
+   public static boolean renameSdDir(String oldId, String newId) {
+       try {
+           int rc = getStorageManager().renameSecureContainer(oldId, newId);
+           if (rc != StorageResultCode.OperationSucceeded) {
+               Log.e(TAG, "Failed to rename " + oldId + " to " +
+                       newId + "with rc " + rc);
+               return false;
+           }
+           return true;
+       } catch (RemoteException e) {
+           Log.i(TAG, "Failed ot rename  " + oldId + " to " + newId +
+                   " with exception : " + e);
+       }
+       return false;
+   }
+
+   public static String getSdDir(String cid) {
+       try {
+            return getStorageManager().getSecureContainerPath(cid);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to get container path for " + cid +
+                " with exception " + e);
+        }
+        return null;
+   }
+
+   public static String getSdFilesystem(String cid) {
+       try {
+            return getStorageManager().getSecureContainerFilesystemPath(cid);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to get container path for " + cid +
+                " with exception " + e);
+        }
+        return null;
+   }
+
+    public static boolean finalizeSdDir(String cid) {
+        try {
+            int rc = getStorageManager().finalizeSecureContainer(cid);
+            if (rc != StorageResultCode.OperationSucceeded) {
+                Log.i(TAG, "Failed to finalize container " + cid);
+                return false;
+            }
+            return true;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to finalize container " + cid +
+                    " with exception " + e);
+        }
+        return false;
+    }
+
+    public static boolean destroySdDir(String cid) {
+        try {
+            if (localLOGV) Log.i(TAG, "Forcibly destroying container " + cid);
+            int rc = getStorageManager().destroySecureContainer(cid, true);
+            if (rc != StorageResultCode.OperationSucceeded) {
+                Log.i(TAG, "Failed to destroy container " + cid);
+                return false;
+            }
+            return true;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to destroy container " + cid +
+                    " with exception " + e);
+        }
+        return false;
+    }
+
+    public static String[] getSecureContainerList() {
+        try {
+            return getStorageManager().getSecureContainerList();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to get secure container list with exception" +
+                    e);
+        }
+        return null;
+    }
+
+   public static boolean isContainerMounted(String cid) {
+       try {
+           return getStorageManager().isSecureContainerMounted(cid);
+       } catch (RemoteException e) {
+           Log.e(TAG, "Failed to find out if container " + cid + " mounted");
+       }
+       return false;
+   }
+
+    /**
+     * Extract public files for the single given APK.
+     */
+    public static long extractPublicFiles(File apkFile, File publicZipFile)
+            throws IOException {
+        final FileOutputStream fstr;
+        final ZipOutputStream publicZipOutStream;
+
+        if (publicZipFile == null) {
+            fstr = null;
+            publicZipOutStream = null;
+        } else {
+            fstr = new FileOutputStream(publicZipFile);
+            publicZipOutStream = new ZipOutputStream(fstr);
+            Log.d(TAG, "Extracting " + apkFile + " to " + publicZipFile);
+        }
+
+        long size = 0L;
+
+        try {
+            final ZipFile privateZip = new ZipFile(apkFile.getAbsolutePath());
+            try {
+                // Copy manifest, resources.arsc and res directory to public zip
+                for (final ZipEntry zipEntry : Collections.list(privateZip.entries())) {
+                    final String zipEntryName = zipEntry.getName();
+                    if ("AndroidManifest.xml".equals(zipEntryName)
+                            || "resources.arsc".equals(zipEntryName)
+                            || zipEntryName.startsWith("res/")) {
+                        size += zipEntry.getSize();
+                        if (publicZipFile != null) {
+                            copyZipEntry(zipEntry, privateZip, publicZipOutStream);
+                        }
+                    }
+                }
+            } finally {
+                try { privateZip.close(); } catch (IOException e) {}
+            }
+
+            if (publicZipFile != null) {
+                publicZipOutStream.finish();
+                publicZipOutStream.flush();
+                FileUtils.sync(fstr);
+                publicZipOutStream.close();
+                FileUtils.setPermissions(publicZipFile.getAbsolutePath(), FileUtils.S_IRUSR
+                        | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IROTH, -1, -1);
+            }
+        } finally {
+            IoUtils.closeQuietly(publicZipOutStream);
+        }
+
+        return size;
+    }
+
+    private static void copyZipEntry(ZipEntry zipEntry, ZipFile inZipFile,
+            ZipOutputStream outZipStream) throws IOException {
+        byte[] buffer = new byte[4096];
+        int num;
+
+        ZipEntry newEntry;
+        if (zipEntry.getMethod() == ZipEntry.STORED) {
+            // Preserve the STORED method of the input entry.
+            newEntry = new ZipEntry(zipEntry);
+        } else {
+            // Create a new entry so that the compressed len is recomputed.
+            newEntry = new ZipEntry(zipEntry.getName());
+        }
+        outZipStream.putNextEntry(newEntry);
+
+        final InputStream data = inZipFile.getInputStream(zipEntry);
+        try {
+            while ((num = data.read(buffer)) > 0) {
+                outZipStream.write(buffer, 0, num);
+            }
+            outZipStream.flush();
+        } finally {
+            IoUtils.closeQuietly(data);
+        }
+    }
+
+    public static boolean fixSdPermissions(String cid, int gid, String filename) {
+        try {
+            int rc = getStorageManager().fixPermissionsSecureContainer(cid, gid, filename);
+            if (rc != StorageResultCode.OperationSucceeded) {
+                Log.i(TAG, "Failed to fixperms container " + cid);
+                return false;
+            }
+            return true;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to fixperms container " + cid + " with exception " + e);
+        }
+        return false;
+    }
+
+    /**
+     * A group of external dependencies used in
+     * {@link #resolveInstallVolume(Context, String, int, long)}. It can be backed by real values
+     * from the system or mocked ones for testing purposes.
+     */
+    public static abstract class TestableInterface {
+        abstract public StorageManager getStorageManager(Context context);
+        abstract public boolean getForceAllowOnExternalSetting(Context context);
+        abstract public boolean getAllow3rdPartyOnInternalConfig(Context context);
+        abstract public ApplicationInfo getExistingAppInfo(Context context, String packageName);
+        abstract public File getDataDirectory();
+
+        public boolean fitsOnInternalStorage(Context context, SessionParams params)
+                throws IOException {
+            StorageManager storage = getStorageManager(context);
+            final UUID target = storage.getUuidForPath(getDataDirectory());
+            return (params.sizeBytes <= storage.getAllocatableBytes(target,
+                    translateAllocateFlags(params.installFlags)));
+        }
+    }
+
+    private synchronized static TestableInterface getDefaultTestableInterface() {
+        if (sDefaultTestableInterface == null) {
+            sDefaultTestableInterface = new TestableInterface() {
+                @Override
+                public StorageManager getStorageManager(Context context) {
+                    return context.getSystemService(StorageManager.class);
+                }
+
+                @Override
+                public boolean getForceAllowOnExternalSetting(Context context) {
+                    return Settings.Global.getInt(context.getContentResolver(),
+                            Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0;
+                }
+
+                @Override
+                public boolean getAllow3rdPartyOnInternalConfig(Context context) {
+                    return context.getResources().getBoolean(
+                            com.android.internal.R.bool.config_allow3rdPartyAppOnInternal);
+                }
+
+                @Override
+                public ApplicationInfo getExistingAppInfo(Context context, String packageName) {
+                    ApplicationInfo existingInfo = null;
+                    try {
+                        existingInfo = context.getPackageManager().getApplicationInfo(packageName,
+                                PackageManager.MATCH_ANY_USER);
+                    } catch (NameNotFoundException ignored) {
+                    }
+                    return existingInfo;
+                }
+
+                @Override
+                public File getDataDirectory() {
+                    return Environment.getDataDirectory();
+                }
+            };
+        }
+        return sDefaultTestableInterface;
+    }
+
+    @VisibleForTesting
+    @Deprecated
+    public static String resolveInstallVolume(Context context, String packageName,
+            int installLocation, long sizeBytes, TestableInterface testInterface)
+            throws IOException {
+        final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);
+        params.appPackageName = packageName;
+        params.installLocation = installLocation;
+        params.sizeBytes = sizeBytes;
+        return resolveInstallVolume(context, params, testInterface);
+    }
+
+    /**
+     * Given a requested {@link PackageInfo#installLocation} and calculated
+     * install size, pick the actual volume to install the app. Only considers
+     * internal and private volumes, and prefers to keep an existing package on
+     * its current volume.
+     *
+     * @return the {@link VolumeInfo#fsUuid} to install onto, or {@code null}
+     *         for internal storage.
+     */
+    public static String resolveInstallVolume(Context context, SessionParams params)
+            throws IOException {
+        TestableInterface testableInterface = getDefaultTestableInterface();
+        return resolveInstallVolume(context, params.appPackageName, params.installLocation,
+                params.sizeBytes, testableInterface);
+    }
+
+    @VisibleForTesting
+    public static String resolveInstallVolume(Context context, SessionParams params,
+            TestableInterface testInterface) throws IOException {
+        final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context);
+        final boolean allow3rdPartyOnInternal =
+                testInterface.getAllow3rdPartyOnInternalConfig(context);
+        // TODO: handle existing apps installed in ASEC; currently assumes
+        // they'll end up back on internal storage
+        ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context,
+                params.appPackageName);
+
+        final boolean fitsOnInternal = testInterface.fitsOnInternalStorage(context, params);
+        final StorageManager storageManager =
+                testInterface.getStorageManager(context);
+
+        // System apps always forced to internal storage
+        if (existingInfo != null && existingInfo.isSystemApp()) {
+            if (fitsOnInternal) {
+                return StorageManager.UUID_PRIVATE_INTERNAL;
+            } else {
+                throw new IOException("Not enough space on existing volume "
+                        + existingInfo.volumeUuid + " for system app " + params.appPackageName
+                        + " upgrade");
+            }
+        }
+
+        // Now deal with non-system apps.
+        final ArraySet<String> allCandidates = new ArraySet<>();
+        VolumeInfo bestCandidate = null;
+        long bestCandidateAvailBytes = Long.MIN_VALUE;
+        for (VolumeInfo vol : storageManager.getVolumes()) {
+            boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id);
+            if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()
+                    && (!isInternalStorage || allow3rdPartyOnInternal)) {
+                final UUID target = storageManager.getUuidForPath(new File(vol.path));
+                final long availBytes = storageManager.getAllocatableBytes(target,
+                        translateAllocateFlags(params.installFlags));
+                if (availBytes >= params.sizeBytes) {
+                    allCandidates.add(vol.fsUuid);
+                }
+                if (availBytes >= bestCandidateAvailBytes) {
+                    bestCandidate = vol;
+                    bestCandidateAvailBytes = availBytes;
+                }
+            }
+        }
+
+        // If app expresses strong desire for internal storage, honor it
+        if (!forceAllowOnExternal
+                && params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
+            if (existingInfo != null && !Objects.equals(existingInfo.volumeUuid,
+                    StorageManager.UUID_PRIVATE_INTERNAL)) {
+                throw new IOException("Cannot automatically move " + params.appPackageName
+                        + " from " + existingInfo.volumeUuid + " to internal storage");
+            }
+
+            if (!allow3rdPartyOnInternal) {
+                throw new IOException("Not allowed to install non-system apps on internal storage");
+            }
+
+            if (fitsOnInternal) {
+                return StorageManager.UUID_PRIVATE_INTERNAL;
+            } else {
+                throw new IOException("Requested internal only, but not enough space");
+            }
+        }
+
+        // If app already exists somewhere, we must stay on that volume
+        if (existingInfo != null) {
+            if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)
+                    && fitsOnInternal) {
+                return StorageManager.UUID_PRIVATE_INTERNAL;
+            } else if (allCandidates.contains(existingInfo.volumeUuid)) {
+                return existingInfo.volumeUuid;
+            } else {
+                throw new IOException("Not enough space on existing volume "
+                        + existingInfo.volumeUuid + " for " + params.appPackageName + " upgrade");
+            }
+        }
+
+        // We're left with new installations with either preferring external or auto, so just pick
+        // volume with most space
+        if (bestCandidate != null) {
+            return bestCandidate.fsUuid;
+        } else {
+            throw new IOException("No special requests, but no room on allowed volumes. "
+                + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
+        }
+    }
+
+    public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException {
+        final StorageManager storage = context.getSystemService(StorageManager.class);
+        final UUID target = storage.getUuidForPath(Environment.getDataDirectory());
+        return (params.sizeBytes <= storage.getAllocatableBytes(target,
+                translateAllocateFlags(params.installFlags)));
+    }
+
+    public static boolean fitsOnExternal(Context context, SessionParams params) {
+        final StorageManager storage = context.getSystemService(StorageManager.class);
+        final StorageVolume primary = storage.getPrimaryVolume();
+        return (params.sizeBytes > 0) && !primary.isEmulated()
+                && Environment.MEDIA_MOUNTED.equals(primary.getState())
+                && params.sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile());
+    }
+
+    @Deprecated
+    public static int resolveInstallLocation(Context context, String packageName,
+            int installLocation, long sizeBytes, int installFlags) {
+        final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);
+        params.appPackageName = packageName;
+        params.installLocation = installLocation;
+        params.sizeBytes = sizeBytes;
+        params.installFlags = installFlags;
+        try {
+            return resolveInstallLocation(context, params);
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Given a requested {@link PackageInfo#installLocation} and calculated
+     * install size, pick the actual location to install the app.
+     */
+    public static int resolveInstallLocation(Context context, SessionParams params)
+            throws IOException {
+        ApplicationInfo existingInfo = null;
+        try {
+            existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName,
+                    PackageManager.MATCH_ANY_USER);
+        } catch (NameNotFoundException ignored) {
+        }
+
+        final int prefer;
+        final boolean checkBoth;
+        boolean ephemeral = false;
+        if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
+            prefer = RECOMMEND_INSTALL_INTERNAL;
+            ephemeral = true;
+            checkBoth = false;
+        } else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
+            prefer = RECOMMEND_INSTALL_INTERNAL;
+            checkBoth = false;
+        } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {
+            prefer = RECOMMEND_INSTALL_EXTERNAL;
+            checkBoth = false;
+        } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
+            prefer = RECOMMEND_INSTALL_INTERNAL;
+            checkBoth = false;
+        } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
+            prefer = RECOMMEND_INSTALL_EXTERNAL;
+            checkBoth = true;
+        } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
+            // When app is already installed, prefer same medium
+            if (existingInfo != null) {
+                // TODO: distinguish if this is external ASEC
+                if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
+                    prefer = RECOMMEND_INSTALL_EXTERNAL;
+                } else {
+                    prefer = RECOMMEND_INSTALL_INTERNAL;
+                }
+            } else {
+                prefer = RECOMMEND_INSTALL_INTERNAL;
+            }
+            checkBoth = true;
+        } else {
+            prefer = RECOMMEND_INSTALL_INTERNAL;
+            checkBoth = false;
+        }
+
+        boolean fitsOnInternal = false;
+        if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {
+            fitsOnInternal = fitsOnInternal(context, params);
+        }
+
+        boolean fitsOnExternal = false;
+        if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {
+            fitsOnExternal = fitsOnExternal(context, params);
+        }
+
+        if (prefer == RECOMMEND_INSTALL_INTERNAL) {
+            // The ephemeral case will either fit and return EPHEMERAL, or will not fit
+            // and will fall through to return INSUFFICIENT_STORAGE
+            if (fitsOnInternal) {
+                return (ephemeral)
+                        ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL
+                        : PackageHelper.RECOMMEND_INSTALL_INTERNAL;
+            }
+        } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {
+            if (fitsOnExternal) {
+                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
+            }
+        }
+
+        if (checkBoth) {
+            if (fitsOnInternal) {
+                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
+            } else if (fitsOnExternal) {
+                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
+            }
+        }
+
+        return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
+    }
+
+    public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
+            String abiOverride) throws IOException {
+        NativeLibraryHelper.Handle handle = null;
+        try {
+            handle = NativeLibraryHelper.Handle.create(pkg);
+            return calculateInstalledSize(pkg, handle, isForwardLocked, abiOverride);
+        } finally {
+            IoUtils.closeQuietly(handle);
+        }
+    }
+
+    public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle,
+            boolean isForwardLocked, String abiOverride) throws IOException {
+        long sizeBytes = 0;
+
+        // Include raw APKs, and possibly unpacked resources
+        for (String codePath : pkg.getAllCodePaths()) {
+            final File codeFile = new File(codePath);
+            sizeBytes += codeFile.length();
+
+            if (isForwardLocked) {
+                sizeBytes += PackageHelper.extractPublicFiles(codeFile, null);
+            }
+        }
+
+        // Include all relevant native code
+        sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
+
+        return sizeBytes;
+    }
+
+    public static String replaceEnd(String str, String before, String after) {
+        if (!str.endsWith(before)) {
+            throw new IllegalArgumentException(
+                    "Expected " + str + " to end with " + before);
+        }
+        return str.substring(0, str.length() - before.length()) + after;
+    }
+
+    public static int translateAllocateFlags(int installFlags) {
+        if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) {
+            return StorageManager.FLAG_ALLOCATE_AGGRESSIVE;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/com/android/internal/content/PackageMonitor.java b/com/android/internal/content/PackageMonitor.java
new file mode 100644
index 0000000..d2e9789
--- /dev/null
+++ b/com/android/internal/content/PackageMonitor.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2010 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.internal.content;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.Slog;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.Preconditions;
+
+import java.util.HashSet;
+
+/**
+ * Helper class for monitoring the state of packages: adding, removing,
+ * updating, and disappearing and reappearing on the SD card.
+ */
+public abstract class PackageMonitor extends android.content.BroadcastReceiver {
+    static final IntentFilter sPackageFilt = new IntentFilter();
+    static final IntentFilter sNonDataFilt = new IntentFilter();
+    static final IntentFilter sExternalFilt = new IntentFilter();
+
+    static {
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_ADDED);
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        sPackageFilt.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+        sPackageFilt.addDataScheme("package");
+        sNonDataFilt.addAction(Intent.ACTION_UID_REMOVED);
+        sNonDataFilt.addAction(Intent.ACTION_USER_STOPPED);
+        sNonDataFilt.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+        sNonDataFilt.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+        sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+    }
+    
+    final HashSet<String> mUpdatingPackages = new HashSet<String>();
+    
+    Context mRegisteredContext;
+    Handler mRegisteredHandler;
+    String[] mDisappearingPackages;
+    String[] mAppearingPackages;
+    String[] mModifiedPackages;
+    int mChangeType;
+    int mChangeUserId = UserHandle.USER_NULL;
+    boolean mSomePackagesChanged;
+    String[] mModifiedComponents;
+
+    String[] mTempArray = new String[1];
+
+    public void register(Context context, Looper thread, boolean externalStorage) {
+        register(context, thread, null, externalStorage);
+    }
+
+    public void register(Context context, Looper thread, UserHandle user,
+            boolean externalStorage) {
+        register(context, user, externalStorage,
+                (thread == null) ? BackgroundThread.getHandler() : new Handler(thread));
+    }
+
+    public void register(Context context, UserHandle user,
+        boolean externalStorage, Handler handler) {
+        if (mRegisteredContext != null) {
+            throw new IllegalStateException("Already registered");
+        }
+        mRegisteredContext = context;
+        mRegisteredHandler = Preconditions.checkNotNull(handler);
+        if (user != null) {
+            context.registerReceiverAsUser(this, user, sPackageFilt, null, mRegisteredHandler);
+            context.registerReceiverAsUser(this, user, sNonDataFilt, null, mRegisteredHandler);
+            if (externalStorage) {
+                context.registerReceiverAsUser(this, user, sExternalFilt, null,
+                        mRegisteredHandler);
+            }
+        } else {
+            context.registerReceiver(this, sPackageFilt, null, mRegisteredHandler);
+            context.registerReceiver(this, sNonDataFilt, null, mRegisteredHandler);
+            if (externalStorage) {
+                context.registerReceiver(this, sExternalFilt, null, mRegisteredHandler);
+            }
+        }
+    }
+
+    public Handler getRegisteredHandler() {
+        return mRegisteredHandler;
+    }
+
+    public void unregister() {
+        if (mRegisteredContext == null) {
+            throw new IllegalStateException("Not registered");
+        }
+        mRegisteredContext.unregisterReceiver(this);
+        mRegisteredContext = null;
+    }
+    
+    //not yet implemented
+    boolean isPackageUpdating(String packageName) {
+        synchronized (mUpdatingPackages) {
+            return mUpdatingPackages.contains(packageName);
+        }
+    }
+    
+    public void onBeginPackageChanges() {
+    }
+
+    /**
+     * Called when a package is really added (and not replaced).
+     */
+    public void onPackageAdded(String packageName, int uid) {
+    }
+
+    /**
+     * Called when a package is really removed (and not replaced).
+     */
+    public void onPackageRemoved(String packageName, int uid) {
+    }
+
+    /**
+     * Called when a package is really removed (and not replaced) for
+     * all users on the device.
+     */
+    public void onPackageRemovedAllUsers(String packageName, int uid) {
+    }
+
+    public void onPackageUpdateStarted(String packageName, int uid) {
+    }
+
+    public void onPackageUpdateFinished(String packageName, int uid) {
+    }
+
+    /**
+     * Direct reflection of {@link Intent#ACTION_PACKAGE_CHANGED
+     * Intent.ACTION_PACKAGE_CHANGED} being received, informing you of
+     * changes to the enabled/disabled state of components in a package
+     * and/or of the overall package.
+     *
+     * @param packageName The name of the package that is changing.
+     * @param uid The user ID the package runs under.
+     * @param components Any components in the package that are changing.  If
+     * the overall package is changing, this will contain an entry of the
+     * package name itself.
+     * @return Return true to indicate you care about this change, which will
+     * result in {@link #onSomePackagesChanged()} being called later.  If you
+     * return false, no further callbacks will happen about this change.  The
+     * default implementation returns true if this is a change to the entire
+     * package.
+     */
+    public boolean onPackageChanged(String packageName, int uid, String[] components) {
+        if (components != null) {
+            for (String name : components) {
+                if (packageName.equals(name)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+    
+    public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
+        return false;
+    }
+
+    public void onHandleUserStop(Intent intent, int userHandle) {
+    }
+    
+    public void onUidRemoved(int uid) {
+    }
+    
+    public void onPackagesAvailable(String[] packages) {
+    }
+    
+    public void onPackagesUnavailable(String[] packages) {
+    }
+
+    public void onPackagesSuspended(String[] packages) {
+    }
+
+    public void onPackagesUnsuspended(String[] packages) {
+    }
+
+    public static final int PACKAGE_UNCHANGED = 0;
+    public static final int PACKAGE_UPDATING = 1;
+    public static final int PACKAGE_TEMPORARY_CHANGE = 2;
+    public static final int PACKAGE_PERMANENT_CHANGE = 3;
+
+    /**
+     * Called when a package disappears for any reason.
+     */
+    public void onPackageDisappeared(String packageName, int reason) {
+    }
+
+    /**
+     * Called when a package appears for any reason.
+     */
+    public void onPackageAppeared(String packageName, int reason) {
+    }
+
+    /**
+     * Called when an existing package is updated or its disabled state changes.
+     */
+    public void onPackageModified(String packageName) {
+    }
+    
+    public boolean didSomePackagesChange() {
+        return mSomePackagesChanged;
+    }
+    
+    public int isPackageAppearing(String packageName) {
+        if (mAppearingPackages != null) {
+            for (int i=mAppearingPackages.length-1; i>=0; i--) {
+                if (packageName.equals(mAppearingPackages[i])) {
+                    return mChangeType;
+                }
+            }
+        }
+        return PACKAGE_UNCHANGED;
+    }
+    
+    public boolean anyPackagesAppearing() {
+        return mAppearingPackages != null;
+    }
+    
+    public int isPackageDisappearing(String packageName) {
+        if (mDisappearingPackages != null) {
+            for (int i=mDisappearingPackages.length-1; i>=0; i--) {
+                if (packageName.equals(mDisappearingPackages[i])) {
+                    return mChangeType;
+                }
+            }
+        }
+        return PACKAGE_UNCHANGED;
+    }
+    
+    public boolean anyPackagesDisappearing() {
+        return mDisappearingPackages != null;
+    }
+
+    public boolean isReplacing() {
+        return mChangeType == PACKAGE_UPDATING;
+    }
+
+    public boolean isPackageModified(String packageName) {
+        if (mModifiedPackages != null) {
+            for (int i=mModifiedPackages.length-1; i>=0; i--) {
+                if (packageName.equals(mModifiedPackages[i])) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public boolean isComponentModified(String className) {
+        if (className == null || mModifiedComponents == null) {
+            return false;
+        }
+        for (int i = mModifiedComponents.length - 1; i >= 0; i--) {
+            if (className.equals(mModifiedComponents[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    public void onSomePackagesChanged() {
+    }
+    
+    public void onFinishPackageChanges() {
+    }
+
+    public void onPackageDataCleared(String packageName, int uid) {
+    }
+
+    public int getChangingUserId() {
+        return mChangeUserId;
+    }
+
+    String getPackageName(Intent intent) {
+        Uri uri = intent.getData();
+        String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
+        return pkg;
+    }
+    
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        mChangeUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+                UserHandle.USER_NULL);
+        if (mChangeUserId == UserHandle.USER_NULL) {
+            Slog.w("PackageMonitor", "Intent broadcast does not contain user handle: " + intent);
+            return;
+        }
+        onBeginPackageChanges();
+        
+        mDisappearingPackages = mAppearingPackages = null;
+        mSomePackagesChanged = false;
+        mModifiedComponents = null;
+        
+        String action = intent.getAction();
+        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+            String pkg = getPackageName(intent);
+            int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+            // We consider something to have changed regardless of whether
+            // this is just an update, because the update is now finished
+            // and the contents of the package may have changed.
+            mSomePackagesChanged = true;
+            if (pkg != null) {
+                mAppearingPackages = mTempArray;
+                mTempArray[0] = pkg;
+                if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+                    mModifiedPackages = mTempArray;
+                    mChangeType = PACKAGE_UPDATING;
+                    onPackageUpdateFinished(pkg, uid);
+                    onPackageModified(pkg);
+                } else {
+                    mChangeType = PACKAGE_PERMANENT_CHANGE;
+                    onPackageAdded(pkg, uid);
+                }
+                onPackageAppeared(pkg, mChangeType);
+                if (mChangeType == PACKAGE_UPDATING) {
+                    synchronized (mUpdatingPackages) {
+                        mUpdatingPackages.remove(pkg);
+                    }
+                }
+            }
+        } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+            String pkg = getPackageName(intent);
+            int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+            if (pkg != null) {
+                mDisappearingPackages = mTempArray;
+                mTempArray[0] = pkg;
+                if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+                    mChangeType = PACKAGE_UPDATING;
+                    synchronized (mUpdatingPackages) {
+                        //not used for now
+                        //mUpdatingPackages.add(pkg);
+                    }
+                    onPackageUpdateStarted(pkg, uid);
+                } else {
+                    mChangeType = PACKAGE_PERMANENT_CHANGE;
+                    // We only consider something to have changed if this is
+                    // not a replace; for a replace, we just need to consider
+                    // it when it is re-added.
+                    mSomePackagesChanged = true;
+                    onPackageRemoved(pkg, uid);
+                    if (intent.getBooleanExtra(Intent.EXTRA_REMOVED_FOR_ALL_USERS, false)) {
+                        onPackageRemovedAllUsers(pkg, uid);
+                    }
+                }
+                onPackageDisappeared(pkg, mChangeType);
+            }
+        } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+            String pkg = getPackageName(intent);
+            int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+            mModifiedComponents = intent.getStringArrayExtra(
+                    Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+            if (pkg != null) {
+                mModifiedPackages = mTempArray;
+                mTempArray[0] = pkg;
+                mChangeType = PACKAGE_PERMANENT_CHANGE;
+                if (onPackageChanged(pkg, uid, mModifiedComponents)) {
+                    mSomePackagesChanged = true;
+                }
+                onPackageModified(pkg);
+            }
+        } else if (Intent.ACTION_PACKAGE_DATA_CLEARED.equals(action)) {
+            String pkg = getPackageName(intent);
+            int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+            if (pkg != null) {
+                onPackageDataCleared(pkg, uid);
+            }
+        } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
+            mDisappearingPackages = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
+            mChangeType = PACKAGE_TEMPORARY_CHANGE;
+            boolean canRestart = onHandleForceStop(intent,
+                    mDisappearingPackages,
+                    intent.getIntExtra(Intent.EXTRA_UID, 0), false);
+            if (canRestart) setResultCode(Activity.RESULT_OK);
+        } else if (Intent.ACTION_PACKAGE_RESTARTED.equals(action)) {
+            mDisappearingPackages = new String[] {getPackageName(intent)};
+            mChangeType = PACKAGE_TEMPORARY_CHANGE;
+            onHandleForceStop(intent, mDisappearingPackages,
+                    intent.getIntExtra(Intent.EXTRA_UID, 0), true);
+        } else if (Intent.ACTION_UID_REMOVED.equals(action)) {
+            onUidRemoved(intent.getIntExtra(Intent.EXTRA_UID, 0));
+        } else if (Intent.ACTION_USER_STOPPED.equals(action)) {
+            if (intent.hasExtra(Intent.EXTRA_USER_HANDLE)) {
+                onHandleUserStop(intent, intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+            }
+        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
+            String[] pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            mAppearingPackages = pkgList;
+            mChangeType = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
+                    ? PACKAGE_UPDATING : PACKAGE_TEMPORARY_CHANGE;
+            mSomePackagesChanged = true;
+            if (pkgList != null) {
+                onPackagesAvailable(pkgList);
+                for (int i=0; i<pkgList.length; i++) {
+                    onPackageAppeared(pkgList[i], mChangeType);
+                }
+            }
+        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
+            String[] pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            mDisappearingPackages = pkgList;
+            mChangeType = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
+                    ? PACKAGE_UPDATING : PACKAGE_TEMPORARY_CHANGE;
+            mSomePackagesChanged = true;
+            if (pkgList != null) {
+                onPackagesUnavailable(pkgList);
+                for (int i=0; i<pkgList.length; i++) {
+                    onPackageDisappeared(pkgList[i], mChangeType);
+                }
+            }
+        } else if (Intent.ACTION_PACKAGES_SUSPENDED.equals(action)) {
+            String[] pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            mSomePackagesChanged = true;
+            onPackagesSuspended(pkgList);
+        } else if (Intent.ACTION_PACKAGES_UNSUSPENDED.equals(action)) {
+            String[] pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            mSomePackagesChanged = true;
+            onPackagesUnsuspended(pkgList);
+        }
+
+        if (mSomePackagesChanged) {
+            onSomePackagesChanged();
+        }
+        
+        onFinishPackageChanges();
+        mChangeUserId = UserHandle.USER_NULL;
+    }
+}
diff --git a/com/android/internal/content/ReferrerIntent.java b/com/android/internal/content/ReferrerIntent.java
new file mode 100644
index 0000000..8d9a1cf
--- /dev/null
+++ b/com/android/internal/content/ReferrerIntent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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.internal.content;
+
+import android.content.Intent;
+import android.os.Parcel;
+
+/**
+ * Subclass of Intent that also contains referrer (as a package name) information.
+ */
+public class ReferrerIntent extends Intent {
+    public final String mReferrer;
+
+    public ReferrerIntent(Intent baseIntent, String referrer) {
+        super(baseIntent);
+        mReferrer = referrer;
+    }
+
+    public void writeToParcel(Parcel dest, int parcelableFlags) {
+        super.writeToParcel(dest, parcelableFlags);
+        dest.writeString(mReferrer);
+    }
+
+    ReferrerIntent(Parcel in) {
+        readFromParcel(in);
+        mReferrer = in.readString();
+    }
+
+    public static final Creator<ReferrerIntent> CREATOR = new Creator<ReferrerIntent>() {
+        public ReferrerIntent createFromParcel(Parcel source) {
+            return new ReferrerIntent(source);
+        }
+        public ReferrerIntent[] newArray(int size) {
+            return new ReferrerIntent[size];
+        }
+    };
+}
diff --git a/com/android/internal/content/SelectionBuilder.java b/com/android/internal/content/SelectionBuilder.java
new file mode 100644
index 0000000..b194756
--- /dev/null
+++ b/com/android/internal/content/SelectionBuilder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2010 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.internal.content;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Helper for building selection clauses for {@link SQLiteDatabase}. Each
+ * appended clause is combined using {@code AND}. This class is <em>not</em>
+ * thread safe.
+ *
+ * @hide
+ */
+public class SelectionBuilder {
+    private StringBuilder mSelection = new StringBuilder();
+    private ArrayList<String> mSelectionArgs = new ArrayList<String>();
+
+    /**
+     * Reset any internal state, allowing this builder to be recycled.
+     */
+    public SelectionBuilder reset() {
+        mSelection.setLength(0);
+        mSelectionArgs.clear();
+        return this;
+    }
+
+    /**
+     * Append the given selection clause to the internal state. Each clause is
+     * surrounded with parenthesis and combined using {@code AND}.
+     */
+    public SelectionBuilder append(String selection, Object... selectionArgs) {
+        if (TextUtils.isEmpty(selection)) {
+            if (selectionArgs != null && selectionArgs.length > 0) {
+                throw new IllegalArgumentException(
+                        "Valid selection required when including arguments");
+            }
+
+            // Shortcut when clause is empty
+            return this;
+        }
+
+        if (mSelection.length() > 0) {
+            mSelection.append(" AND ");
+        }
+
+        mSelection.append("(").append(selection).append(")");
+        if (selectionArgs != null) {
+            for (Object arg : selectionArgs) {
+                // TODO: switch to storing direct Object instances once
+                // http://b/2464440 is fixed
+                mSelectionArgs.add(String.valueOf(arg));
+            }
+        }
+
+        return this;
+    }
+
+    /**
+     * Return selection string for current internal state.
+     *
+     * @see #getSelectionArgs()
+     */
+    public String getSelection() {
+        return mSelection.toString();
+    }
+
+    /**
+     * Return selection arguments for current internal state.
+     *
+     * @see #getSelection()
+     */
+    public String[] getSelectionArgs() {
+        return mSelectionArgs.toArray(new String[mSelectionArgs.size()]);
+    }
+
+    /**
+     * Execute query using the current internal state as {@code WHERE} clause.
+     * Missing arguments as treated as {@code null}.
+     */
+    public Cursor query(SQLiteDatabase db, String table, String[] columns, String orderBy) {
+        return query(db, table, columns, null, null, orderBy, null);
+    }
+
+    /**
+     * Execute query using the current internal state as {@code WHERE} clause.
+     */
+    public Cursor query(SQLiteDatabase db, String table, String[] columns, String groupBy,
+            String having, String orderBy, String limit) {
+        return db.query(table, columns, getSelection(), getSelectionArgs(), groupBy, having,
+                orderBy, limit);
+    }
+
+    /**
+     * Execute update using the current internal state as {@code WHERE} clause.
+     */
+    public int update(SQLiteDatabase db, String table, ContentValues values) {
+        return db.update(table, values, getSelection(), getSelectionArgs());
+    }
+
+    /**
+     * Execute delete using the current internal state as {@code WHERE} clause.
+     */
+    public int delete(SQLiteDatabase db, String table) {
+        return db.delete(table, getSelection(), getSelectionArgs());
+    }
+}
diff --git a/com/android/internal/database/SortCursor.java b/com/android/internal/database/SortCursor.java
new file mode 100644
index 0000000..0025512
--- /dev/null
+++ b/com/android/internal/database/SortCursor.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2007 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.internal.database;
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.util.Log;
+
+/**
+ * A variant of MergeCursor that sorts the cursors being merged. If decent
+ * performance is ever obtained, it can be put back under android.database.
+ */
+public class SortCursor extends AbstractCursor
+{
+    private static final String TAG = "SortCursor";
+    private Cursor mCursor; // updated in onMove
+    private Cursor[] mCursors;
+    private int [] mSortColumns;
+    private final int ROWCACHESIZE = 64;
+    private int mRowNumCache[] = new int[ROWCACHESIZE];
+    private int mCursorCache[] = new int[ROWCACHESIZE];
+    private int mCurRowNumCache[][];
+    private int mLastCacheHit = -1;
+
+    private DataSetObserver mObserver = new DataSetObserver() {
+
+        @Override
+        public void onChanged() {
+            // Reset our position so the optimizations in move-related code
+            // don't screw us over
+            mPos = -1;
+        }
+
+        @Override
+        public void onInvalidated() {
+            mPos = -1;
+        }
+    };
+    
+    public SortCursor(Cursor[] cursors, String sortcolumn)
+    {
+        mCursors = cursors;
+
+        int length = mCursors.length;
+        mSortColumns = new int[length];
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] == null) continue;
+            
+            // Register ourself as a data set observer
+            mCursors[i].registerDataSetObserver(mObserver);
+            
+            mCursors[i].moveToFirst();
+
+            // We don't catch the exception
+            mSortColumns[i] = mCursors[i].getColumnIndexOrThrow(sortcolumn);
+        }
+        mCursor = null;
+        String smallest = "";
+        for (int j = 0 ; j < length; j++) {
+            if (mCursors[j] == null || mCursors[j].isAfterLast())
+                continue;
+            String current = mCursors[j].getString(mSortColumns[j]);
+            if (mCursor == null || current.compareToIgnoreCase(smallest) < 0) {
+                smallest = current;
+                mCursor = mCursors[j];
+            }
+        }
+
+        for (int i = mRowNumCache.length - 1; i >= 0; i--) {
+            mRowNumCache[i] = -2;
+        }
+        mCurRowNumCache = new int[ROWCACHESIZE][length];
+    }
+
+    @Override
+    public int getCount()
+    {
+        int count = 0;
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                count += mCursors[i].getCount();
+            }
+        }
+        return count;
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition)
+    {
+        if (oldPosition == newPosition)
+            return true;
+
+        /* Find the right cursor
+         * Because the client of this cursor (the listadapter/view) tends
+         * to jump around in the cursor somewhat, a simple cache strategy
+         * is used to avoid having to search all cursors from the start.
+         * TODO: investigate strategies for optimizing random access and
+         * reverse-order access.
+         */
+
+        int cache_entry = newPosition % ROWCACHESIZE;
+
+        if (mRowNumCache[cache_entry] == newPosition) {
+            int which = mCursorCache[cache_entry];
+            mCursor = mCursors[which];
+            if (mCursor == null) {
+                Log.w(TAG, "onMove: cache results in a null cursor.");
+                return false;
+            }
+            mCursor.moveToPosition(mCurRowNumCache[cache_entry][which]);
+            mLastCacheHit = cache_entry;
+            return true;
+        }
+
+        mCursor = null;
+        int length = mCursors.length;
+
+        if (mLastCacheHit >= 0) {
+            for (int i = 0; i < length; i++) {
+                if (mCursors[i] == null) continue;
+                mCursors[i].moveToPosition(mCurRowNumCache[mLastCacheHit][i]);
+            }
+        }
+
+        if (newPosition < oldPosition || oldPosition == -1) {
+            for (int i = 0 ; i < length; i++) {
+                if (mCursors[i] == null) continue;
+                mCursors[i].moveToFirst();
+            }
+            oldPosition = 0;
+        }
+        if (oldPosition < 0) {
+            oldPosition = 0;
+        }
+
+        // search forward to the new position
+        int smallestIdx = -1;
+        for(int i = oldPosition; i <= newPosition; i++) {
+            String smallest = "";
+            smallestIdx = -1;
+            for (int j = 0 ; j < length; j++) {
+                if (mCursors[j] == null || mCursors[j].isAfterLast()) {
+                    continue;
+                }
+                String current = mCursors[j].getString(mSortColumns[j]);
+                if (smallestIdx < 0 || current.compareToIgnoreCase(smallest) < 0) {
+                    smallest = current;
+                    smallestIdx = j;
+                }
+            }
+            if (i == newPosition) break;
+            if (mCursors[smallestIdx] != null) {
+                mCursors[smallestIdx].moveToNext();
+            }
+        }
+        mCursor = mCursors[smallestIdx];
+        mRowNumCache[cache_entry] = newPosition;
+        mCursorCache[cache_entry] = smallestIdx;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] != null) {
+                mCurRowNumCache[cache_entry][i] = mCursors[i].getPosition();
+            }
+        }
+        mLastCacheHit = -1;
+        return true;
+    }
+
+    @Override
+    public String getString(int column)
+    {
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public short getShort(int column)
+    {
+        return mCursor.getShort(column);
+    }
+
+    @Override
+    public int getInt(int column)
+    {
+        return mCursor.getInt(column);
+    }
+
+    @Override
+    public long getLong(int column)
+    {
+        return mCursor.getLong(column);
+    }
+
+    @Override
+    public float getFloat(int column)
+    {
+        return mCursor.getFloat(column);
+    }
+
+    @Override
+    public double getDouble(int column)
+    {
+        return mCursor.getDouble(column);
+    }
+
+    @Override
+    public int getType(int column) {
+        return mCursor.getType(column);
+    }
+
+    @Override
+    public boolean isNull(int column)
+    {
+        return mCursor.isNull(column);
+    }
+
+    @Override
+    public byte[] getBlob(int column)
+    {
+        return mCursor.getBlob(column);   
+    }
+    
+    @Override
+    public String[] getColumnNames()
+    {
+        if (mCursor != null) {
+            return mCursor.getColumnNames();
+        } else {
+            // All of the cursors may be empty, but they can still return
+            // this information.
+            int length = mCursors.length;
+            for (int i = 0 ; i < length ; i++) {
+                if (mCursors[i] != null) {
+                    return mCursors[i].getColumnNames();
+                }
+            }
+            throw new IllegalStateException("No cursor that can return names");
+        }
+    }
+
+    @Override
+    public void deactivate()
+    {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] == null) continue;
+            mCursors[i].deactivate();
+        }
+    }
+
+    @Override
+    public void close() {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] == null) continue;
+            mCursors[i].close();
+        }
+    }
+
+    @Override
+    public void registerDataSetObserver(DataSetObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].registerDataSetObserver(observer);
+            }
+        }
+    }
+    
+    @Override
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].unregisterDataSetObserver(observer);
+            }
+        }
+    }
+    
+    @Override
+    public boolean requery()
+    {
+        int length = mCursors.length;
+        for (int i = 0 ; i < length ; i++) {
+            if (mCursors[i] == null) continue;
+            
+            if (mCursors[i].requery() == false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/com/android/internal/graphics/ColorUtils.java b/com/android/internal/graphics/ColorUtils.java
new file mode 100644
index 0000000..8b2a2dc
--- /dev/null
+++ b/com/android/internal/graphics/ColorUtils.java
@@ -0,0 +1,665 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.graphics.Color;
+
+/**
+ * Copied from: frameworks/support/core-utils/java/android/support/v4/graphics/ColorUtils.java
+ *
+ * A set of color-related utility methods, building upon those available in {@code Color}.
+ */
+public final class ColorUtils {
+
+    private static final double XYZ_WHITE_REFERENCE_X = 95.047;
+    private static final double XYZ_WHITE_REFERENCE_Y = 100;
+    private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
+    private static final double XYZ_EPSILON = 0.008856;
+    private static final double XYZ_KAPPA = 903.3;
+
+    private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
+    private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
+
+    private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
+
+    private ColorUtils() {}
+
+    /**
+     * Composite two potentially translucent colors over each other and returns the result.
+     */
+    public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
+        int bgAlpha = Color.alpha(background);
+        int fgAlpha = Color.alpha(foreground);
+        int a = compositeAlpha(fgAlpha, bgAlpha);
+
+        int r = compositeComponent(Color.red(foreground), fgAlpha,
+                Color.red(background), bgAlpha, a);
+        int g = compositeComponent(Color.green(foreground), fgAlpha,
+                Color.green(background), bgAlpha, a);
+        int b = compositeComponent(Color.blue(foreground), fgAlpha,
+                Color.blue(background), bgAlpha, a);
+
+        return Color.argb(a, r, g, b);
+    }
+
+    private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
+        return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
+    }
+
+    private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
+        if (a == 0) return 0;
+        return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
+    }
+
+    /**
+     * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
+     * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
+     */
+    @FloatRange(from = 0.0, to = 1.0)
+    public static double calculateLuminance(@ColorInt int color) {
+        final double[] result = getTempDouble3Array();
+        colorToXYZ(color, result);
+        // Luminance is the Y component
+        return result[1] / 100;
+    }
+
+    /**
+     * Returns the contrast ratio between {@code foreground} and {@code background}.
+     * {@code background} must be opaque.
+     * <p>
+     * Formula defined
+     * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
+     */
+    public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
+        if (Color.alpha(background) != 255) {
+            throw new IllegalArgumentException("background can not be translucent: #"
+                    + Integer.toHexString(background));
+        }
+        if (Color.alpha(foreground) < 255) {
+            // If the foreground is translucent, composite the foreground over the background
+            foreground = compositeColors(foreground, background);
+        }
+
+        final double luminance1 = calculateLuminance(foreground) + 0.05;
+        final double luminance2 = calculateLuminance(background) + 0.05;
+
+        // Now return the lighter luminance divided by the darker luminance
+        return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
+    }
+
+    /**
+     * Calculates the minimum alpha value which can be applied to {@code background} so that would
+     * have a contrast value of at least {@code minContrastRatio} when alpha blended to
+     * {@code foreground}.
+     *
+     * @param foreground       the foreground color
+     * @param background       the background color, opacity will be ignored
+     * @param minContrastRatio the minimum contrast ratio
+     * @return the alpha value in the range 0-255, or -1 if no value could be calculated
+     */
+    public static int calculateMinimumBackgroundAlpha(@ColorInt int foreground,
+            @ColorInt int background, float minContrastRatio) {
+        // Ignore initial alpha that the background might have since this is
+        // what we're trying to calculate.
+        background = setAlphaComponent(background, 255);
+        final int leastContrastyColor = setAlphaComponent(foreground, 255);
+        return binaryAlphaSearch(foreground, background, minContrastRatio, (fg, bg, alpha) -> {
+            int testBackground = blendARGB(leastContrastyColor, bg, alpha/255f);
+            // Float rounding might set this alpha to something other that 255,
+            // raising an exception in calculateContrast.
+            testBackground = setAlphaComponent(testBackground, 255);
+            return calculateContrast(fg, testBackground);
+        });
+    }
+
+    /**
+     * Calculates the minimum alpha value which can be applied to {@code foreground} so that would
+     * have a contrast value of at least {@code minContrastRatio} when compared to
+     * {@code background}.
+     *
+     * @param foreground       the foreground color
+     * @param background       the opaque background color
+     * @param minContrastRatio the minimum contrast ratio
+     * @return the alpha value in the range 0-255, or -1 if no value could be calculated
+     */
+    public static int calculateMinimumAlpha(@ColorInt int foreground, @ColorInt int background,
+            float minContrastRatio) {
+        if (Color.alpha(background) != 255) {
+            throw new IllegalArgumentException("background can not be translucent: #"
+                    + Integer.toHexString(background));
+        }
+
+        ContrastCalculator contrastCalculator = (fg, bg, alpha) -> {
+            int testForeground = setAlphaComponent(fg, alpha);
+            return calculateContrast(testForeground, bg);
+        };
+
+        // First lets check that a fully opaque foreground has sufficient contrast
+        double testRatio = contrastCalculator.calculateContrast(foreground, background, 255);
+        if (testRatio < minContrastRatio) {
+            // Fully opaque foreground does not have sufficient contrast, return error
+            return -1;
+        }
+        foreground = setAlphaComponent(foreground, 255);
+        return binaryAlphaSearch(foreground, background, minContrastRatio, contrastCalculator);
+    }
+
+    /**
+     * Calculates the alpha value using binary search based on a given contrast evaluation function
+     * and target contrast that needs to be satisfied.
+     *
+     * @param foreground         the foreground color
+     * @param background         the opaque background color
+     * @param minContrastRatio   the minimum contrast ratio
+     * @param calculator function that calculates contrast
+     * @return the alpha value in the range 0-255, or -1 if no value could be calculated
+     */
+    private static int binaryAlphaSearch(@ColorInt int foreground, @ColorInt int background,
+            float minContrastRatio, ContrastCalculator calculator) {
+        // Binary search to find a value with the minimum value which provides sufficient contrast
+        int numIterations = 0;
+        int minAlpha = 0;
+        int maxAlpha = 255;
+
+        while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS &&
+                (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) {
+            final int testAlpha = (minAlpha + maxAlpha) / 2;
+
+            final double testRatio = calculator.calculateContrast(foreground, background,
+                    testAlpha);
+            if (testRatio < minContrastRatio) {
+                minAlpha = testAlpha;
+            } else {
+                maxAlpha = testAlpha;
+            }
+
+            numIterations++;
+        }
+
+        // Conservatively return the max of the range of possible alphas, which is known to pass.
+        return maxAlpha;
+    }
+
+    /**
+     * Convert RGB components to HSL (hue-saturation-lightness).
+     * <ul>
+     * <li>outHsl[0] is Hue [0 .. 360)</li>
+     * <li>outHsl[1] is Saturation [0...1]</li>
+     * <li>outHsl[2] is Lightness [0...1]</li>
+     * </ul>
+     *
+     * @param r      red component value [0..255]
+     * @param g      green component value [0..255]
+     * @param b      blue component value [0..255]
+     * @param outHsl 3-element array which holds the resulting HSL components
+     */
+    public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
+            @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+            @NonNull float[] outHsl) {
+        final float rf = r / 255f;
+        final float gf = g / 255f;
+        final float bf = b / 255f;
+
+        final float max = Math.max(rf, Math.max(gf, bf));
+        final float min = Math.min(rf, Math.min(gf, bf));
+        final float deltaMaxMin = max - min;
+
+        float h, s;
+        float l = (max + min) / 2f;
+
+        if (max == min) {
+            // Monochromatic
+            h = s = 0f;
+        } else {
+            if (max == rf) {
+                h = ((gf - bf) / deltaMaxMin) % 6f;
+            } else if (max == gf) {
+                h = ((bf - rf) / deltaMaxMin) + 2f;
+            } else {
+                h = ((rf - gf) / deltaMaxMin) + 4f;
+            }
+
+            s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
+        }
+
+        h = (h * 60f) % 360f;
+        if (h < 0) {
+            h += 360f;
+        }
+
+        outHsl[0] = constrain(h, 0f, 360f);
+        outHsl[1] = constrain(s, 0f, 1f);
+        outHsl[2] = constrain(l, 0f, 1f);
+    }
+
+    /**
+     * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
+     * <ul>
+     * <li>outHsl[0] is Hue [0 .. 360)</li>
+     * <li>outHsl[1] is Saturation [0...1]</li>
+     * <li>outHsl[2] is Lightness [0...1]</li>
+     * </ul>
+     *
+     * @param color  the ARGB color to convert. The alpha component is ignored
+     * @param outHsl 3-element array which holds the resulting HSL components
+     */
+    public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
+        RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
+    }
+
+    /**
+     * Convert HSL (hue-saturation-lightness) components to a RGB color.
+     * <ul>
+     * <li>hsl[0] is Hue [0 .. 360)</li>
+     * <li>hsl[1] is Saturation [0...1]</li>
+     * <li>hsl[2] is Lightness [0...1]</li>
+     * </ul>
+     * If hsv values are out of range, they are pinned.
+     *
+     * @param hsl 3-element array which holds the input HSL components
+     * @return the resulting RGB color
+     */
+    @ColorInt
+    public static int HSLToColor(@NonNull float[] hsl) {
+        final float h = hsl[0];
+        final float s = hsl[1];
+        final float l = hsl[2];
+
+        final float c = (1f - Math.abs(2 * l - 1f)) * s;
+        final float m = l - 0.5f * c;
+        final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
+
+        final int hueSegment = (int) h / 60;
+
+        int r = 0, g = 0, b = 0;
+
+        switch (hueSegment) {
+            case 0:
+                r = Math.round(255 * (c + m));
+                g = Math.round(255 * (x + m));
+                b = Math.round(255 * m);
+                break;
+            case 1:
+                r = Math.round(255 * (x + m));
+                g = Math.round(255 * (c + m));
+                b = Math.round(255 * m);
+                break;
+            case 2:
+                r = Math.round(255 * m);
+                g = Math.round(255 * (c + m));
+                b = Math.round(255 * (x + m));
+                break;
+            case 3:
+                r = Math.round(255 * m);
+                g = Math.round(255 * (x + m));
+                b = Math.round(255 * (c + m));
+                break;
+            case 4:
+                r = Math.round(255 * (x + m));
+                g = Math.round(255 * m);
+                b = Math.round(255 * (c + m));
+                break;
+            case 5:
+            case 6:
+                r = Math.round(255 * (c + m));
+                g = Math.round(255 * m);
+                b = Math.round(255 * (x + m));
+                break;
+        }
+
+        r = constrain(r, 0, 255);
+        g = constrain(g, 0, 255);
+        b = constrain(b, 0, 255);
+
+        return Color.rgb(r, g, b);
+    }
+
+    /**
+     * Set the alpha component of {@code color} to be {@code alpha}.
+     */
+    @ColorInt
+    public static int setAlphaComponent(@ColorInt int color,
+            @IntRange(from = 0x0, to = 0xFF) int alpha) {
+        if (alpha < 0 || alpha > 255) {
+            throw new IllegalArgumentException("alpha must be between 0 and 255.");
+        }
+        return (color & 0x00ffffff) | (alpha << 24);
+    }
+
+    /**
+     * Convert the ARGB color to its CIE Lab representative components.
+     *
+     * @param color  the ARGB color to convert. The alpha component is ignored
+     * @param outLab 3-element array which holds the resulting LAB components
+     */
+    public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
+        RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
+    }
+
+    /**
+     * Convert RGB components to its CIE Lab representative components.
+     *
+     * <ul>
+     * <li>outLab[0] is L [0 ...1)</li>
+     * <li>outLab[1] is a [-128...127)</li>
+     * <li>outLab[2] is b [-128...127)</li>
+     * </ul>
+     *
+     * @param r      red component value [0..255]
+     * @param g      green component value [0..255]
+     * @param b      blue component value [0..255]
+     * @param outLab 3-element array which holds the resulting LAB components
+     */
+    public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
+            @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+            @NonNull double[] outLab) {
+        // First we convert RGB to XYZ
+        RGBToXYZ(r, g, b, outLab);
+        // outLab now contains XYZ
+        XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
+        // outLab now contains LAB representation
+    }
+
+    /**
+     * Convert the ARGB color to its CIE XYZ representative components.
+     *
+     * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>outXyz[0] is X [0 ...95.047)</li>
+     * <li>outXyz[1] is Y [0...100)</li>
+     * <li>outXyz[2] is Z [0...108.883)</li>
+     * </ul>
+     *
+     * @param color  the ARGB color to convert. The alpha component is ignored
+     * @param outXyz 3-element array which holds the resulting LAB components
+     */
+    public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
+        RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
+    }
+
+    /**
+     * Convert RGB components to its CIE XYZ representative components.
+     *
+     * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>outXyz[0] is X [0 ...95.047)</li>
+     * <li>outXyz[1] is Y [0...100)</li>
+     * <li>outXyz[2] is Z [0...108.883)</li>
+     * </ul>
+     *
+     * @param r      red component value [0..255]
+     * @param g      green component value [0..255]
+     * @param b      blue component value [0..255]
+     * @param outXyz 3-element array which holds the resulting XYZ components
+     */
+    public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
+            @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+            @NonNull double[] outXyz) {
+        if (outXyz.length != 3) {
+            throw new IllegalArgumentException("outXyz must have a length of 3.");
+        }
+
+        double sr = r / 255.0;
+        sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
+        double sg = g / 255.0;
+        sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
+        double sb = b / 255.0;
+        sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
+
+        outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
+        outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
+        outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
+    }
+
+    /**
+     * Converts a color from CIE XYZ to CIE Lab representation.
+     *
+     * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>outLab[0] is L [0 ...1)</li>
+     * <li>outLab[1] is a [-128...127)</li>
+     * <li>outLab[2] is b [-128...127)</li>
+     * </ul>
+     *
+     * @param x      X component value [0...95.047)
+     * @param y      Y component value [0...100)
+     * @param z      Z component value [0...108.883)
+     * @param outLab 3-element array which holds the resulting Lab components
+     */
+    public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
+            @NonNull double[] outLab) {
+        if (outLab.length != 3) {
+            throw new IllegalArgumentException("outLab must have a length of 3.");
+        }
+        x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
+        y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
+        z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
+        outLab[0] = Math.max(0, 116 * y - 16);
+        outLab[1] = 500 * (x - y);
+        outLab[2] = 200 * (y - z);
+    }
+
+    /**
+     * Converts a color from CIE Lab to CIE XYZ representation.
+     *
+     * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>outXyz[0] is X [0 ...95.047)</li>
+     * <li>outXyz[1] is Y [0...100)</li>
+     * <li>outXyz[2] is Z [0...108.883)</li>
+     * </ul>
+     *
+     * @param l      L component value [0...100)
+     * @param a      A component value [-128...127)
+     * @param b      B component value [-128...127)
+     * @param outXyz 3-element array which holds the resulting XYZ components
+     */
+    public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
+            @FloatRange(from = -128, to = 127) final double a,
+            @FloatRange(from = -128, to = 127) final double b,
+            @NonNull double[] outXyz) {
+        final double fy = (l + 16) / 116;
+        final double fx = a / 500 + fy;
+        final double fz = fy - b / 200;
+
+        double tmp = Math.pow(fx, 3);
+        final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
+        final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
+
+        tmp = Math.pow(fz, 3);
+        final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
+
+        outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
+        outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
+        outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
+    }
+
+    /**
+     * Converts a color from CIE XYZ to its RGB representation.
+     *
+     * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * @param x X component value [0...95.047)
+     * @param y Y component value [0...100)
+     * @param z Z component value [0...108.883)
+     * @return int containing the RGB representation
+     */
+    @ColorInt
+    public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
+        double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
+        double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
+        double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
+
+        r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
+        g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
+        b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
+
+        return Color.rgb(
+                constrain((int) Math.round(r * 255), 0, 255),
+                constrain((int) Math.round(g * 255), 0, 255),
+                constrain((int) Math.round(b * 255), 0, 255));
+    }
+
+    /**
+     * Converts a color from CIE Lab to its RGB representation.
+     *
+     * @param l L component value [0...100]
+     * @param a A component value [-128...127]
+     * @param b B component value [-128...127]
+     * @return int containing the RGB representation
+     */
+    @ColorInt
+    public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
+            @FloatRange(from = -128, to = 127) final double a,
+            @FloatRange(from = -128, to = 127) final double b) {
+        final double[] result = getTempDouble3Array();
+        LABToXYZ(l, a, b, result);
+        return XYZToColor(result[0], result[1], result[2]);
+    }
+
+    /**
+     * Returns the euclidean distance between two LAB colors.
+     */
+    public static double distanceEuclidean(@NonNull double[] labX, @NonNull double[] labY) {
+        return Math.sqrt(Math.pow(labX[0] - labY[0], 2)
+                + Math.pow(labX[1] - labY[1], 2)
+                + Math.pow(labX[2] - labY[2], 2));
+    }
+
+    private static float constrain(float amount, float low, float high) {
+        return amount < low ? low : (amount > high ? high : amount);
+    }
+
+    private static int constrain(int amount, int low, int high) {
+        return amount < low ? low : (amount > high ? high : amount);
+    }
+
+    private static double pivotXyzComponent(double component) {
+        return component > XYZ_EPSILON
+                ? Math.pow(component, 1 / 3.0)
+                : (XYZ_KAPPA * component + 16) / 116;
+    }
+
+    /**
+     * Blend between two ARGB colors using the given ratio.
+     *
+     * <p>A blend ratio of 0.0 will result in {@code color1}, 0.5 will give an even blend,
+     * 1.0 will result in {@code color2}.</p>
+     *
+     * @param color1 the first ARGB color
+     * @param color2 the second ARGB color
+     * @param ratio  the blend ratio of {@code color1} to {@code color2}
+     */
+    @ColorInt
+    public static int blendARGB(@ColorInt int color1, @ColorInt int color2,
+            @FloatRange(from = 0.0, to = 1.0) float ratio) {
+        final float inverseRatio = 1 - ratio;
+        float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio;
+        float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio;
+        float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio;
+        float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio;
+        return Color.argb((int) a, (int) r, (int) g, (int) b);
+    }
+
+    /**
+     * Blend between {@code hsl1} and {@code hsl2} using the given ratio. This will interpolate
+     * the hue using the shortest angle.
+     *
+     * <p>A blend ratio of 0.0 will result in {@code hsl1}, 0.5 will give an even blend,
+     * 1.0 will result in {@code hsl2}.</p>
+     *
+     * @param hsl1      3-element array which holds the first HSL color
+     * @param hsl2      3-element array which holds the second HSL color
+     * @param ratio     the blend ratio of {@code hsl1} to {@code hsl2}
+     * @param outResult 3-element array which holds the resulting HSL components
+     */
+    public static void blendHSL(@NonNull float[] hsl1, @NonNull float[] hsl2,
+            @FloatRange(from = 0.0, to = 1.0) float ratio, @NonNull float[] outResult) {
+        if (outResult.length != 3) {
+            throw new IllegalArgumentException("result must have a length of 3.");
+        }
+        final float inverseRatio = 1 - ratio;
+        // Since hue is circular we will need to interpolate carefully
+        outResult[0] = circularInterpolate(hsl1[0], hsl2[0], ratio);
+        outResult[1] = hsl1[1] * inverseRatio + hsl2[1] * ratio;
+        outResult[2] = hsl1[2] * inverseRatio + hsl2[2] * ratio;
+    }
+
+    /**
+     * Blend between two CIE-LAB colors using the given ratio.
+     *
+     * <p>A blend ratio of 0.0 will result in {@code lab1}, 0.5 will give an even blend,
+     * 1.0 will result in {@code lab2}.</p>
+     *
+     * @param lab1      3-element array which holds the first LAB color
+     * @param lab2      3-element array which holds the second LAB color
+     * @param ratio     the blend ratio of {@code lab1} to {@code lab2}
+     * @param outResult 3-element array which holds the resulting LAB components
+     */
+    public static void blendLAB(@NonNull double[] lab1, @NonNull double[] lab2,
+            @FloatRange(from = 0.0, to = 1.0) double ratio, @NonNull double[] outResult) {
+        if (outResult.length != 3) {
+            throw new IllegalArgumentException("outResult must have a length of 3.");
+        }
+        final double inverseRatio = 1 - ratio;
+        outResult[0] = lab1[0] * inverseRatio + lab2[0] * ratio;
+        outResult[1] = lab1[1] * inverseRatio + lab2[1] * ratio;
+        outResult[2] = lab1[2] * inverseRatio + lab2[2] * ratio;
+    }
+
+    static float circularInterpolate(float a, float b, float f) {
+        if (Math.abs(b - a) > 180) {
+            if (b > a) {
+                a += 360;
+            } else {
+                b += 360;
+            }
+        }
+        return (a + ((b - a) * f)) % 360;
+    }
+
+    private static double[] getTempDouble3Array() {
+        double[] result = TEMP_ARRAY.get();
+        if (result == null) {
+            result = new double[3];
+            TEMP_ARRAY.set(result);
+        }
+        return result;
+    }
+
+    private interface ContrastCalculator {
+        double calculateContrast(int foreground, int background, int alpha);
+    }
+
+}
\ No newline at end of file
diff --git a/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java b/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java
new file mode 100644
index 0000000..931eb99
--- /dev/null
+++ b/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics;
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider;
+import android.view.Choreographer;
+
+/**
+ * Provider of timing pulse that uses SurfaceFlinger Vsync Choreographer for frame callbacks.
+ *
+ * @hide
+ */
+public final class SfVsyncFrameCallbackProvider implements AnimationFrameCallbackProvider {
+
+    private final Choreographer mChoreographer = Choreographer.getSfInstance();
+
+    @Override
+    public void postFrameCallback(Choreographer.FrameCallback callback) {
+        mChoreographer.postFrameCallback(callback);
+    }
+
+    @Override
+    public void postCommitCallback(Runnable runnable) {
+        mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
+    }
+
+    @Override
+    public long getFrameTime() {
+        return mChoreographer.getFrameTime();
+    }
+
+    @Override
+    public long getFrameDelay() {
+        return Choreographer.getFrameDelay();
+    }
+
+    @Override
+    public void setFrameDelay(long delay) {
+        Choreographer.setFrameDelay(delay);
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/graphics/drawable/AnimationScaleListDrawable.java b/com/android/internal/graphics/drawable/AnimationScaleListDrawable.java
new file mode 100644
index 0000000..62f18ea
--- /dev/null
+++ b/com/android/internal/graphics/drawable/AnimationScaleListDrawable.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2016 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.internal.graphics.drawable;
+
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableContainer;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * An internal DrawableContainer class, used to draw different things depending on animation scale.
+ * i.e: animation scale can be 0 in battery saver mode.
+ * This class contains 2 drawable, one is animatable, the other is static. When animation scale is
+ * not 0, the animatable drawable will the drawn. Otherwise, the static drawable will be drawn.
+ * <p>This class implements Animatable since ProgressBar can pick this up similarly as an
+ * AnimatedVectorDrawable.
+ * <p>It can be defined in an XML file with the {@code <AnimationScaleListDrawable>}
+ * element.
+ */
+public class AnimationScaleListDrawable extends DrawableContainer implements Animatable {
+    private static final String TAG = "AnimationScaleListDrawable";
+    private AnimationScaleListState mAnimationScaleListState;
+    private boolean mMutated;
+
+    public AnimationScaleListDrawable() {
+        this(null, null);
+    }
+
+    private AnimationScaleListDrawable(@Nullable AnimationScaleListState state,
+            @Nullable Resources res) {
+        // Every scale list drawable has its own constant state.
+        final AnimationScaleListState newState = new AnimationScaleListState(state, this, res);
+        setConstantState(newState);
+        onStateChange(getState());
+    }
+
+    /**
+     * Set the current drawable according to the animation scale. If scale is 0, then pick the
+     * static drawable, otherwise, pick the animatable drawable.
+     */
+    @Override
+    protected boolean onStateChange(int[] stateSet) {
+        final boolean changed = super.onStateChange(stateSet);
+        int idx = mAnimationScaleListState.getCurrentDrawableIndexBasedOnScale();
+        return selectDrawable(idx) || changed;
+    }
+
+
+    @Override
+    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
+            @NonNull AttributeSet attrs, @Nullable Theme theme)
+            throws XmlPullParserException, IOException {
+        final TypedArray a = obtainAttributes(r, theme, attrs,
+                R.styleable.AnimationScaleListDrawable);
+        updateDensity(r);
+        a.recycle();
+
+        inflateChildElements(r, parser, attrs, theme);
+
+        onStateChange(getState());
+    }
+
+    /**
+     * Inflates child elements from XML.
+     */
+    private void inflateChildElements(@NonNull Resources r, @NonNull XmlPullParser parser,
+            @NonNull AttributeSet attrs, @Nullable Theme theme)
+            throws XmlPullParserException, IOException {
+        final AnimationScaleListState state = mAnimationScaleListState;
+        final int innerDepth = parser.getDepth() + 1;
+        int type;
+        int depth;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && ((depth = parser.getDepth()) >= innerDepth
+                || type != XmlPullParser.END_TAG)) {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+
+            if (depth > innerDepth || !parser.getName().equals("item")) {
+                continue;
+            }
+
+            // Either pick up the android:drawable attribute.
+            final TypedArray a = obtainAttributes(r, theme, attrs,
+                    R.styleable.AnimationScaleListDrawableItem);
+            Drawable dr = a.getDrawable(R.styleable.AnimationScaleListDrawableItem_drawable);
+            a.recycle();
+
+            // Or parse the child element under <item>.
+            if (dr == null) {
+                while ((type = parser.next()) == XmlPullParser.TEXT) {
+                }
+                if (type != XmlPullParser.START_TAG) {
+                    throw new XmlPullParserException(
+                            parser.getPositionDescription()
+                                    + ": <item> tag requires a 'drawable' attribute or "
+                                    + "child tag defining a drawable");
+                }
+                dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
+            }
+
+            state.addDrawable(dr);
+        }
+    }
+
+    @Override
+    public Drawable mutate() {
+        if (!mMutated && super.mutate() == this) {
+            mAnimationScaleListState.mutate();
+            mMutated = true;
+        }
+        return this;
+    }
+
+    @Override
+    public void clearMutated() {
+        super.clearMutated();
+        mMutated = false;
+    }
+
+    @Override
+    public void start() {
+        Drawable dr = getCurrent();
+        if (dr != null && dr instanceof Animatable) {
+            ((Animatable) dr).start();
+        }
+    }
+
+    @Override
+    public void stop() {
+        Drawable dr = getCurrent();
+        if (dr != null && dr instanceof Animatable) {
+            ((Animatable) dr).stop();
+        }
+    }
+
+    @Override
+    public boolean isRunning() {
+        boolean result = false;
+        Drawable dr = getCurrent();
+        if (dr != null && dr instanceof Animatable) {
+            result = ((Animatable) dr).isRunning();
+        }
+        return result;
+    }
+
+    static class AnimationScaleListState extends DrawableContainerState {
+        int[] mThemeAttrs = null;
+        // The index of the last static drawable.
+        int mStaticDrawableIndex = -1;
+        // The index of the last animatable drawable.
+        int mAnimatableDrawableIndex = -1;
+
+        AnimationScaleListState(AnimationScaleListState orig, AnimationScaleListDrawable owner,
+                Resources res) {
+            super(orig, owner, res);
+
+            if (orig != null) {
+                // Perform a shallow copy and rely on mutate() to deep-copy.
+                mThemeAttrs = orig.mThemeAttrs;
+
+                mStaticDrawableIndex = orig.mStaticDrawableIndex;
+                mAnimatableDrawableIndex = orig.mAnimatableDrawableIndex;
+            }
+
+        }
+
+        void mutate() {
+            mThemeAttrs = mThemeAttrs != null ? mThemeAttrs.clone() : null;
+        }
+
+        /**
+         * Add the drawable into the container.
+         * This class only keep track one animatable drawable, and one static. If there are multiple
+         * defined in the XML, then pick the last one.
+         */
+        int addDrawable(Drawable drawable) {
+            final int pos = addChild(drawable);
+            if (drawable instanceof Animatable) {
+                mAnimatableDrawableIndex = pos;
+            } else {
+                mStaticDrawableIndex = pos;
+            }
+            return pos;
+        }
+
+        @Override
+        public Drawable newDrawable() {
+            return new AnimationScaleListDrawable(this, null);
+        }
+
+        @Override
+        public Drawable newDrawable(Resources res) {
+            return new AnimationScaleListDrawable(this, res);
+        }
+
+        @Override
+        public boolean canApplyTheme() {
+            return mThemeAttrs != null || super.canApplyTheme();
+        }
+
+        public int getCurrentDrawableIndexBasedOnScale() {
+            if (ValueAnimator.getDurationScale() == 0) {
+                return mStaticDrawableIndex;
+            }
+            return mAnimatableDrawableIndex;
+        }
+    }
+
+    @Override
+    public void applyTheme(@NonNull Theme theme) {
+        super.applyTheme(theme);
+
+        onStateChange(getState());
+    }
+
+    @Override
+    protected void setConstantState(@NonNull DrawableContainerState state) {
+        super.setConstantState(state);
+
+        if (state instanceof AnimationScaleListState) {
+            mAnimationScaleListState = (AnimationScaleListState) state;
+        }
+    }
+}
+
diff --git a/com/android/internal/graphics/palette/ColorCutQuantizer.java b/com/android/internal/graphics/palette/ColorCutQuantizer.java
new file mode 100644
index 0000000..9ac753b
--- /dev/null
+++ b/com/android/internal/graphics/palette/ColorCutQuantizer.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics.palette;
+
+/*
+ * Copyright 2014 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.
+ */
+
+import android.graphics.Color;
+import android.util.TimingLogger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.graphics.palette.Palette.Swatch;
+
+/**
+ * Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/
+ * graphics/ColorCutQuantizer.java
+ *
+ * An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct
+ * colors rather than representation colors.
+ *
+ * The color space is represented as a 3-dimensional cube with each dimension being an RGB
+ * component. The cube is then repeatedly divided until we have reduced the color space to the
+ * requested number of colors. An average color is then generated from each cube.
+ *
+ * What makes this different to median-cut is that median-cut divided cubes so that all of the cubes
+ * have roughly the same population, where this quantizer divides boxes based on their color volume.
+ * This means that the color space is divided into distinct colors, rather than representative
+ * colors.
+ */
+final class ColorCutQuantizer implements Quantizer {
+
+    private static final String LOG_TAG = "ColorCutQuantizer";
+    private static final boolean LOG_TIMINGS = false;
+
+    static final int COMPONENT_RED = -3;
+    static final int COMPONENT_GREEN = -2;
+    static final int COMPONENT_BLUE = -1;
+
+    private static final int QUANTIZE_WORD_WIDTH = 5;
+    private static final int QUANTIZE_WORD_MASK = (1 << QUANTIZE_WORD_WIDTH) - 1;
+
+    int[] mColors;
+    int[] mHistogram;
+    List<Swatch> mQuantizedColors;
+    TimingLogger mTimingLogger;
+    Palette.Filter[] mFilters;
+
+    private final float[] mTempHsl = new float[3];
+
+    /**
+     * Execute color quantization.
+     *
+     * @param pixels histogram representing an image's pixel data
+     * @param maxColors The maximum number of colors that should be in the result palette.
+     * @param filters Set of filters to use in the quantization stage
+     */
+    public void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
+        mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
+        mFilters = filters;
+
+        final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
+        for (int i = 0; i < pixels.length; i++) {
+            final int quantizedColor = quantizeFromRgb888(pixels[i]);
+            // Now update the pixel value to the quantized value
+            pixels[i] = quantizedColor;
+            // And update the histogram
+            hist[quantizedColor]++;
+        }
+
+        if (LOG_TIMINGS) {
+            mTimingLogger.addSplit("Histogram created");
+        }
+
+        // Now let's count the number of distinct colors
+        int distinctColorCount = 0;
+        for (int color = 0; color < hist.length; color++) {
+            if (hist[color] > 0 && shouldIgnoreColor(color)) {
+                // If we should ignore the color, set the population to 0
+                hist[color] = 0;
+            }
+            if (hist[color] > 0) {
+                // If the color has population, increase the distinct color count
+                distinctColorCount++;
+            }
+        }
+
+        if (LOG_TIMINGS) {
+            mTimingLogger.addSplit("Filtered colors and distinct colors counted");
+        }
+
+        // Now lets go through create an array consisting of only distinct colors
+        final int[] colors = mColors = new int[distinctColorCount];
+        int distinctColorIndex = 0;
+        for (int color = 0; color < hist.length; color++) {
+            if (hist[color] > 0) {
+                colors[distinctColorIndex++] = color;
+            }
+        }
+
+        if (LOG_TIMINGS) {
+            mTimingLogger.addSplit("Distinct colors copied into array");
+        }
+
+        if (distinctColorCount <= maxColors) {
+            // The image has fewer colors than the maximum requested, so just return the colors
+            mQuantizedColors = new ArrayList<>();
+            for (int color : colors) {
+                mQuantizedColors.add(new Swatch(approximateToRgb888(color), hist[color]));
+            }
+
+            if (LOG_TIMINGS) {
+                mTimingLogger.addSplit("Too few colors present. Copied to Swatches");
+                mTimingLogger.dumpToLog();
+            }
+        } else {
+            // We need use quantization to reduce the number of colors
+            mQuantizedColors = quantizePixels(maxColors);
+
+            if (LOG_TIMINGS) {
+                mTimingLogger.addSplit("Quantized colors computed");
+                mTimingLogger.dumpToLog();
+            }
+        }
+    }
+
+    /**
+     * @return the list of quantized colors
+     */
+    public List<Swatch> getQuantizedColors() {
+        return mQuantizedColors;
+    }
+
+    private List<Swatch> quantizePixels(int maxColors) {
+        // Create the priority queue which is sorted by volume descending. This means we always
+        // split the largest box in the queue
+        final PriorityQueue<Vbox> pq = new PriorityQueue<>(maxColors, VBOX_COMPARATOR_VOLUME);
+
+        // To start, offer a box which contains all of the colors
+        pq.offer(new Vbox(0, mColors.length - 1));
+
+        // Now go through the boxes, splitting them until we have reached maxColors or there are no
+        // more boxes to split
+        splitBoxes(pq, maxColors);
+
+        // Finally, return the average colors of the color boxes
+        return generateAverageColors(pq);
+    }
+
+    /**
+     * Iterate through the {@link java.util.Queue}, popping
+     * {@link ColorCutQuantizer.Vbox} objects from the queue
+     * and splitting them. Once split, the new box and the remaining box are offered back to the
+     * queue.
+     *
+     * @param queue {@link java.util.PriorityQueue} to poll for boxes
+     * @param maxSize Maximum amount of boxes to split
+     */
+    private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) {
+        while (queue.size() < maxSize) {
+            final Vbox vbox = queue.poll();
+
+            if (vbox != null && vbox.canSplit()) {
+                // First split the box, and offer the result
+                queue.offer(vbox.splitBox());
+
+                if (LOG_TIMINGS) {
+                    mTimingLogger.addSplit("Box split");
+                }
+                // Then offer the box back
+                queue.offer(vbox);
+            } else {
+                if (LOG_TIMINGS) {
+                    mTimingLogger.addSplit("All boxes split");
+                }
+                // If we get here then there are no more boxes to split, so return
+                return;
+            }
+        }
+    }
+
+    private List<Swatch> generateAverageColors(Collection<Vbox> vboxes) {
+        ArrayList<Swatch> colors = new ArrayList<>(vboxes.size());
+        for (Vbox vbox : vboxes) {
+            Swatch swatch = vbox.getAverageColor();
+            if (!shouldIgnoreColor(swatch)) {
+                // As we're averaging a color box, we can still get colors which we do not want, so
+                // we check again here
+                colors.add(swatch);
+            }
+        }
+        return colors;
+    }
+
+    /**
+     * Represents a tightly fitting box around a color space.
+     */
+    private class Vbox {
+        // lower and upper index are inclusive
+        private int mLowerIndex;
+        private int mUpperIndex;
+        // Population of colors within this box
+        private int mPopulation;
+
+        private int mMinRed, mMaxRed;
+        private int mMinGreen, mMaxGreen;
+        private int mMinBlue, mMaxBlue;
+
+        Vbox(int lowerIndex, int upperIndex) {
+            mLowerIndex = lowerIndex;
+            mUpperIndex = upperIndex;
+            fitBox();
+        }
+
+        final int getVolume() {
+            return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) *
+                    (mMaxBlue - mMinBlue + 1);
+        }
+
+        final boolean canSplit() {
+            return getColorCount() > 1;
+        }
+
+        final int getColorCount() {
+            return 1 + mUpperIndex - mLowerIndex;
+        }
+
+        /**
+         * Recomputes the boundaries of this box to tightly fit the colors within the box.
+         */
+        final void fitBox() {
+            final int[] colors = mColors;
+            final int[] hist = mHistogram;
+
+            // Reset the min and max to opposite values
+            int minRed, minGreen, minBlue;
+            minRed = minGreen = minBlue = Integer.MAX_VALUE;
+            int maxRed, maxGreen, maxBlue;
+            maxRed = maxGreen = maxBlue = Integer.MIN_VALUE;
+            int count = 0;
+
+            for (int i = mLowerIndex; i <= mUpperIndex; i++) {
+                final int color = colors[i];
+                count += hist[color];
+
+                final int r = quantizedRed(color);
+                final int g = quantizedGreen(color);
+                final int b = quantizedBlue(color);
+                if (r > maxRed) {
+                    maxRed = r;
+                }
+                if (r < minRed) {
+                    minRed = r;
+                }
+                if (g > maxGreen) {
+                    maxGreen = g;
+                }
+                if (g < minGreen) {
+                    minGreen = g;
+                }
+                if (b > maxBlue) {
+                    maxBlue = b;
+                }
+                if (b < minBlue) {
+                    minBlue = b;
+                }
+            }
+
+            mMinRed = minRed;
+            mMaxRed = maxRed;
+            mMinGreen = minGreen;
+            mMaxGreen = maxGreen;
+            mMinBlue = minBlue;
+            mMaxBlue = maxBlue;
+            mPopulation = count;
+        }
+
+        /**
+         * Split this color box at the mid-point along its longest dimension
+         *
+         * @return the new ColorBox
+         */
+        final Vbox splitBox() {
+            if (!canSplit()) {
+                throw new IllegalStateException("Can not split a box with only 1 color");
+            }
+
+            // find median along the longest dimension
+            final int splitPoint = findSplitPoint();
+
+            Vbox newBox = new Vbox(splitPoint + 1, mUpperIndex);
+
+            // Now change this box's upperIndex and recompute the color boundaries
+            mUpperIndex = splitPoint;
+            fitBox();
+
+            return newBox;
+        }
+
+        /**
+         * @return the dimension which this box is largest in
+         */
+        final int getLongestColorDimension() {
+            final int redLength = mMaxRed - mMinRed;
+            final int greenLength = mMaxGreen - mMinGreen;
+            final int blueLength = mMaxBlue - mMinBlue;
+
+            if (redLength >= greenLength && redLength >= blueLength) {
+                return COMPONENT_RED;
+            } else if (greenLength >= redLength && greenLength >= blueLength) {
+                return COMPONENT_GREEN;
+            } else {
+                return COMPONENT_BLUE;
+            }
+        }
+
+        /**
+         * Finds the point within this box's lowerIndex and upperIndex index of where to split.
+         *
+         * This is calculated by finding the longest color dimension, and then sorting the
+         * sub-array based on that dimension value in each color. The colors are then iterated over
+         * until a color is found with at least the midpoint of the whole box's dimension midpoint.
+         *
+         * @return the index of the colors array to split from
+         */
+        final int findSplitPoint() {
+            final int longestDimension = getLongestColorDimension();
+            final int[] colors = mColors;
+            final int[] hist = mHistogram;
+
+            // We need to sort the colors in this box based on the longest color dimension.
+            // As we can't use a Comparator to define the sort logic, we modify each color so that
+            // its most significant is the desired dimension
+            modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);
+
+            // Now sort... Arrays.sort uses a exclusive toIndex so we need to add 1
+            Arrays.sort(colors, mLowerIndex, mUpperIndex + 1);
+
+            // Now revert all of the colors so that they are packed as RGB again
+            modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);
+
+            final int midPoint = mPopulation / 2;
+            for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++)  {
+                count += hist[colors[i]];
+                if (count >= midPoint) {
+                    // we never want to split on the upperIndex, as this will result in the same
+                    // box
+                    return Math.min(mUpperIndex - 1, i);
+                }
+            }
+
+            return mLowerIndex;
+        }
+
+        /**
+         * @return the average color of this box.
+         */
+        final Swatch getAverageColor() {
+            final int[] colors = mColors;
+            final int[] hist = mHistogram;
+            int redSum = 0;
+            int greenSum = 0;
+            int blueSum = 0;
+            int totalPopulation = 0;
+
+            for (int i = mLowerIndex; i <= mUpperIndex; i++) {
+                final int color = colors[i];
+                final int colorPopulation = hist[color];
+
+                totalPopulation += colorPopulation;
+                redSum += colorPopulation * quantizedRed(color);
+                greenSum += colorPopulation * quantizedGreen(color);
+                blueSum += colorPopulation * quantizedBlue(color);
+            }
+
+            final int redMean = Math.round(redSum / (float) totalPopulation);
+            final int greenMean = Math.round(greenSum / (float) totalPopulation);
+            final int blueMean = Math.round(blueSum / (float) totalPopulation);
+
+            return new Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation);
+        }
+    }
+
+    /**
+     * Modify the significant octet in a packed color int. Allows sorting based on the value of a
+     * single color component. This relies on all components being the same word size.
+     *
+     * @see Vbox#findSplitPoint()
+     */
+    static void modifySignificantOctet(final int[] a, final int dimension,
+            final int lower, final int upper) {
+        switch (dimension) {
+            case COMPONENT_RED:
+                // Already in RGB, no need to do anything
+                break;
+            case COMPONENT_GREEN:
+                // We need to do a RGB to GRB swap, or vice-versa
+                for (int i = lower; i <= upper; i++) {
+                    final int color = a[i];
+                    a[i] = quantizedGreen(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
+                            | quantizedRed(color) << QUANTIZE_WORD_WIDTH
+                            | quantizedBlue(color);
+                }
+                break;
+            case COMPONENT_BLUE:
+                // We need to do a RGB to BGR swap, or vice-versa
+                for (int i = lower; i <= upper; i++) {
+                    final int color = a[i];
+                    a[i] = quantizedBlue(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
+                            | quantizedGreen(color) << QUANTIZE_WORD_WIDTH
+                            | quantizedRed(color);
+                }
+                break;
+        }
+    }
+
+    private boolean shouldIgnoreColor(int color565) {
+        final int rgb = approximateToRgb888(color565);
+        ColorUtils.colorToHSL(rgb, mTempHsl);
+        return shouldIgnoreColor(rgb, mTempHsl);
+    }
+
+    private boolean shouldIgnoreColor(Swatch color) {
+        return shouldIgnoreColor(color.getRgb(), color.getHsl());
+    }
+
+    private boolean shouldIgnoreColor(int rgb, float[] hsl) {
+        if (mFilters != null && mFilters.length > 0) {
+            for (int i = 0, count = mFilters.length; i < count; i++) {
+                if (!mFilters[i].isAllowed(rgb, hsl)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Comparator which sorts {@link Vbox} instances based on their volume, in descending order
+     */
+    private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
+        @Override
+        public int compare(Vbox lhs, Vbox rhs) {
+            return rhs.getVolume() - lhs.getVolume();
+        }
+    };
+
+    /**
+     * Quantized a RGB888 value to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
+     */
+    private static int quantizeFromRgb888(int color) {
+        int r = modifyWordWidth(Color.red(color), 8, QUANTIZE_WORD_WIDTH);
+        int g = modifyWordWidth(Color.green(color), 8, QUANTIZE_WORD_WIDTH);
+        int b = modifyWordWidth(Color.blue(color), 8, QUANTIZE_WORD_WIDTH);
+        return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b;
+    }
+
+    /**
+     * Quantized RGB888 values to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
+     */
+    static int approximateToRgb888(int r, int g, int b) {
+        return Color.rgb(modifyWordWidth(r, QUANTIZE_WORD_WIDTH, 8),
+                modifyWordWidth(g, QUANTIZE_WORD_WIDTH, 8),
+                modifyWordWidth(b, QUANTIZE_WORD_WIDTH, 8));
+    }
+
+    private static int approximateToRgb888(int color) {
+        return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color));
+    }
+
+    /**
+     * @return red component of the quantized color
+     */
+    static int quantizedRed(int color) {
+        return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK;
+    }
+
+    /**
+     * @return green component of a quantized color
+     */
+    static int quantizedGreen(int color) {
+        return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK;
+    }
+
+    /**
+     * @return blue component of a quantized color
+     */
+    static int quantizedBlue(int color) {
+        return color & QUANTIZE_WORD_MASK;
+    }
+
+    private static int modifyWordWidth(int value, int currentWidth, int targetWidth) {
+        final int newValue;
+        if (targetWidth > currentWidth) {
+            // If we're approximating up in word width, we'll shift up
+            newValue = value << (targetWidth - currentWidth);
+        } else {
+            // Else, we will just shift and keep the MSB
+            newValue = value >> (currentWidth - targetWidth);
+        }
+        return newValue & ((1 << targetWidth) - 1);
+    }
+
+}
\ No newline at end of file
diff --git a/com/android/internal/graphics/palette/Palette.java b/com/android/internal/graphics/palette/Palette.java
new file mode 100644
index 0000000..a4f9a59
--- /dev/null
+++ b/com/android/internal/graphics/palette/Palette.java
@@ -0,0 +1,1006 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics.palette;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.util.TimingLogger;
+
+import com.android.internal.graphics.ColorUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Copied from: /frameworks/support/v7/palette/src/main/java/android/support/v7/
+ * graphics/Palette.java
+ *
+ * A helper class to extract prominent colors from an image.
+ * <p>
+ * A number of colors with different profiles are extracted from the image:
+ * <ul>
+ *     <li>Vibrant</li>
+ *     <li>Vibrant Dark</li>
+ *     <li>Vibrant Light</li>
+ *     <li>Muted</li>
+ *     <li>Muted Dark</li>
+ *     <li>Muted Light</li>
+ * </ul>
+ * These can be retrieved from the appropriate getter method.
+ *
+ * <p>
+ * Instances are created with a {@link Palette.Builder} which supports several options to tweak the
+ * generated Palette. See that class' documentation for more information.
+ * <p>
+ * Generation should always be completed on a background thread, ideally the one in
+ * which you load your image on. {@link Palette.Builder} supports both synchronous and asynchronous
+ * generation:
+ *
+ * <pre>
+ * // Synchronous
+ * Palette p = Palette.from(bitmap).generate();
+ *
+ * // Asynchronous
+ * Palette.from(bitmap).generate(new PaletteAsyncListener() {
+ *     public void onGenerated(Palette p) {
+ *         // Use generated instance
+ *     }
+ * });
+ * </pre>
+ */
+public final class Palette {
+
+    /**
+     * Listener to be used with {@link #generateAsync(Bitmap, Palette.PaletteAsyncListener)} or
+     * {@link #generateAsync(Bitmap, int, Palette.PaletteAsyncListener)}
+     */
+    public interface PaletteAsyncListener {
+
+        /**
+         * Called when the {@link Palette} has been generated.
+         */
+        void onGenerated(Palette palette);
+    }
+
+    static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112;
+    static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
+
+    static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
+    static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
+
+    static final String LOG_TAG = "Palette";
+    static final boolean LOG_TIMINGS = false;
+
+    /**
+     * Start generating a {@link Palette} with the returned {@link Palette.Builder} instance.
+     */
+    public static Palette.Builder from(Bitmap bitmap) {
+        return new Palette.Builder(bitmap);
+    }
+
+    /**
+     * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches.
+     * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a
+     * list of swatches. Will return null if the {@code swatches} is null.
+     */
+    public static Palette from(List<Palette.Swatch> swatches) {
+        return new Palette.Builder(swatches).generate();
+    }
+
+    /**
+     * @deprecated Use {@link Palette.Builder} to generate the Palette.
+     */
+    @Deprecated
+    public static Palette generate(Bitmap bitmap) {
+        return from(bitmap).generate();
+    }
+
+    /**
+     * @deprecated Use {@link Palette.Builder} to generate the Palette.
+     */
+    @Deprecated
+    public static Palette generate(Bitmap bitmap, int numColors) {
+        return from(bitmap).maximumColorCount(numColors).generate();
+    }
+
+    /**
+     * @deprecated Use {@link Palette.Builder} to generate the Palette.
+     */
+    @Deprecated
+    public static AsyncTask<Bitmap, Void, Palette> generateAsync(
+            Bitmap bitmap, Palette.PaletteAsyncListener listener) {
+        return from(bitmap).generate(listener);
+    }
+
+    /**
+     * @deprecated Use {@link Palette.Builder} to generate the Palette.
+     */
+    @Deprecated
+    public static AsyncTask<Bitmap, Void, Palette> generateAsync(
+            final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener) {
+        return from(bitmap).maximumColorCount(numColors).generate(listener);
+    }
+
+    private final List<Palette.Swatch> mSwatches;
+    private final List<Target> mTargets;
+
+    private final Map<Target, Palette.Swatch> mSelectedSwatches;
+    private final SparseBooleanArray mUsedColors;
+
+    private final Palette.Swatch mDominantSwatch;
+
+    Palette(List<Palette.Swatch> swatches, List<Target> targets) {
+        mSwatches = swatches;
+        mTargets = targets;
+
+        mUsedColors = new SparseBooleanArray();
+        mSelectedSwatches = new ArrayMap<>();
+
+        mDominantSwatch = findDominantSwatch();
+    }
+
+    /**
+     * Returns all of the swatches which make up the palette.
+     */
+    @NonNull
+    public List<Palette.Swatch> getSwatches() {
+        return Collections.unmodifiableList(mSwatches);
+    }
+
+    /**
+     * Returns the targets used to generate this palette.
+     */
+    @NonNull
+    public List<Target> getTargets() {
+        return Collections.unmodifiableList(mTargets);
+    }
+
+    /**
+     * Returns the most vibrant swatch in the palette. Might be null.
+     *
+     * @see Target#VIBRANT
+     */
+    @Nullable
+    public Palette.Swatch getVibrantSwatch() {
+        return getSwatchForTarget(Target.VIBRANT);
+    }
+
+    /**
+     * Returns a light and vibrant swatch from the palette. Might be null.
+     *
+     * @see Target#LIGHT_VIBRANT
+     */
+    @Nullable
+    public Palette.Swatch getLightVibrantSwatch() {
+        return getSwatchForTarget(Target.LIGHT_VIBRANT);
+    }
+
+    /**
+     * Returns a dark and vibrant swatch from the palette. Might be null.
+     *
+     * @see Target#DARK_VIBRANT
+     */
+    @Nullable
+    public Palette.Swatch getDarkVibrantSwatch() {
+        return getSwatchForTarget(Target.DARK_VIBRANT);
+    }
+
+    /**
+     * Returns a muted swatch from the palette. Might be null.
+     *
+     * @see Target#MUTED
+     */
+    @Nullable
+    public Palette.Swatch getMutedSwatch() {
+        return getSwatchForTarget(Target.MUTED);
+    }
+
+    /**
+     * Returns a muted and light swatch from the palette. Might be null.
+     *
+     * @see Target#LIGHT_MUTED
+     */
+    @Nullable
+    public Palette.Swatch getLightMutedSwatch() {
+        return getSwatchForTarget(Target.LIGHT_MUTED);
+    }
+
+    /**
+     * Returns a muted and dark swatch from the palette. Might be null.
+     *
+     * @see Target#DARK_MUTED
+     */
+    @Nullable
+    public Palette.Swatch getDarkMutedSwatch() {
+        return getSwatchForTarget(Target.DARK_MUTED);
+    }
+
+    /**
+     * Returns the most vibrant color in the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getVibrantSwatch()
+     */
+    @ColorInt
+    public int getVibrantColor(@ColorInt final int defaultColor) {
+        return getColorForTarget(Target.VIBRANT, defaultColor);
+    }
+
+    /**
+     * Returns a light and vibrant color from the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getLightVibrantSwatch()
+     */
+    @ColorInt
+    public int getLightVibrantColor(@ColorInt final int defaultColor) {
+        return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor);
+    }
+
+    /**
+     * Returns a dark and vibrant color from the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getDarkVibrantSwatch()
+     */
+    @ColorInt
+    public int getDarkVibrantColor(@ColorInt final int defaultColor) {
+        return getColorForTarget(Target.DARK_VIBRANT, defaultColor);
+    }
+
+    /**
+     * Returns a muted color from the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getMutedSwatch()
+     */
+    @ColorInt
+    public int getMutedColor(@ColorInt final int defaultColor) {
+        return getColorForTarget(Target.MUTED, defaultColor);
+    }
+
+    /**
+     * Returns a muted and light color from the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getLightMutedSwatch()
+     */
+    @ColorInt
+    public int getLightMutedColor(@ColorInt final int defaultColor) {
+        return getColorForTarget(Target.LIGHT_MUTED, defaultColor);
+    }
+
+    /**
+     * Returns a muted and dark color from the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getDarkMutedSwatch()
+     */
+    @ColorInt
+    public int getDarkMutedColor(@ColorInt final int defaultColor) {
+        return getColorForTarget(Target.DARK_MUTED, defaultColor);
+    }
+
+    /**
+     * Returns the selected swatch for the given target from the palette, or {@code null} if one
+     * could not be found.
+     */
+    @Nullable
+    public Palette.Swatch getSwatchForTarget(@NonNull final Target target) {
+        return mSelectedSwatches.get(target);
+    }
+
+    /**
+     * Returns the selected color for the given target from the palette as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     */
+    @ColorInt
+    public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
+        Palette.Swatch swatch = getSwatchForTarget(target);
+        return swatch != null ? swatch.getRgb() : defaultColor;
+    }
+
+    /**
+     * Returns the dominant swatch from the palette.
+     *
+     * <p>The dominant swatch is defined as the swatch with the greatest population (frequency)
+     * within the palette.</p>
+     */
+    @Nullable
+    public Palette.Swatch getDominantSwatch() {
+        return mDominantSwatch;
+    }
+
+    /**
+     * Returns the color of the dominant swatch from the palette, as an RGB packed int.
+     *
+     * @param defaultColor value to return if the swatch isn't available
+     * @see #getDominantSwatch()
+     */
+    @ColorInt
+    public int getDominantColor(@ColorInt int defaultColor) {
+        return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor;
+    }
+
+    void generate() {
+        // We need to make sure that the scored targets are generated first. This is so that
+        // inherited targets have something to inherit from
+        for (int i = 0, count = mTargets.size(); i < count; i++) {
+            final Target target = mTargets.get(i);
+            target.normalizeWeights();
+            mSelectedSwatches.put(target, generateScoredTarget(target));
+        }
+        // We now clear out the used colors
+        mUsedColors.clear();
+    }
+
+    private Palette.Swatch generateScoredTarget(final Target target) {
+        final Palette.Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target);
+        if (maxScoreSwatch != null && target.isExclusive()) {
+            // If we have a swatch, and the target is exclusive, add the color to the used list
+            mUsedColors.append(maxScoreSwatch.getRgb(), true);
+        }
+        return maxScoreSwatch;
+    }
+
+    private Palette.Swatch getMaxScoredSwatchForTarget(final Target target) {
+        float maxScore = 0;
+        Palette.Swatch maxScoreSwatch = null;
+        for (int i = 0, count = mSwatches.size(); i < count; i++) {
+            final Palette.Swatch swatch = mSwatches.get(i);
+            if (shouldBeScoredForTarget(swatch, target)) {
+                final float score = generateScore(swatch, target);
+                if (maxScoreSwatch == null || score > maxScore) {
+                    maxScoreSwatch = swatch;
+                    maxScore = score;
+                }
+            }
+        }
+        return maxScoreSwatch;
+    }
+
+    private boolean shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target) {
+        // Check whether the HSL values are within the correct ranges, and this color hasn't
+        // been used yet.
+        final float hsl[] = swatch.getHsl();
+        return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation()
+                && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness()
+                && !mUsedColors.get(swatch.getRgb());
+    }
+
+    private float generateScore(Palette.Swatch swatch, Target target) {
+        final float[] hsl = swatch.getHsl();
+
+        float saturationScore = 0;
+        float luminanceScore = 0;
+        float populationScore = 0;
+
+        final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1;
+
+        if (target.getSaturationWeight() > 0) {
+            saturationScore = target.getSaturationWeight()
+                    * (1f - Math.abs(hsl[1] - target.getTargetSaturation()));
+        }
+        if (target.getLightnessWeight() > 0) {
+            luminanceScore = target.getLightnessWeight()
+                    * (1f - Math.abs(hsl[2] - target.getTargetLightness()));
+        }
+        if (target.getPopulationWeight() > 0) {
+            populationScore = target.getPopulationWeight()
+                    * (swatch.getPopulation() / (float) maxPopulation);
+        }
+
+        return saturationScore + luminanceScore + populationScore;
+    }
+
+    private Palette.Swatch findDominantSwatch() {
+        int maxPop = Integer.MIN_VALUE;
+        Palette.Swatch maxSwatch = null;
+        for (int i = 0, count = mSwatches.size(); i < count; i++) {
+            Palette.Swatch swatch = mSwatches.get(i);
+            if (swatch.getPopulation() > maxPop) {
+                maxSwatch = swatch;
+                maxPop = swatch.getPopulation();
+            }
+        }
+        return maxSwatch;
+    }
+
+    private static float[] copyHslValues(Palette.Swatch color) {
+        final float[] newHsl = new float[3];
+        System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
+        return newHsl;
+    }
+
+    /**
+     * Represents a color swatch generated from an image's palette. The RGB color can be retrieved
+     * by calling {@link #getRgb()}.
+     */
+    public static final class Swatch {
+        private final int mRed, mGreen, mBlue;
+        private final int mRgb;
+        private final int mPopulation;
+
+        private boolean mGeneratedTextColors;
+        private int mTitleTextColor;
+        private int mBodyTextColor;
+
+        private float[] mHsl;
+
+        public Swatch(@ColorInt int color, int population) {
+            mRed = Color.red(color);
+            mGreen = Color.green(color);
+            mBlue = Color.blue(color);
+            mRgb = color;
+            mPopulation = population;
+        }
+
+        Swatch(int red, int green, int blue, int population) {
+            mRed = red;
+            mGreen = green;
+            mBlue = blue;
+            mRgb = Color.rgb(red, green, blue);
+            mPopulation = population;
+        }
+
+        Swatch(float[] hsl, int population) {
+            this(ColorUtils.HSLToColor(hsl), population);
+            mHsl = hsl;
+        }
+
+        /**
+         * @return this swatch's RGB color value
+         */
+        @ColorInt
+        public int getRgb() {
+            return mRgb;
+        }
+
+        /**
+         * Return this swatch's HSL values.
+         *     hsv[0] is Hue [0 .. 360)
+         *     hsv[1] is Saturation [0...1]
+         *     hsv[2] is Lightness [0...1]
+         */
+        public float[] getHsl() {
+            if (mHsl == null) {
+                mHsl = new float[3];
+            }
+            ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl);
+            return mHsl;
+        }
+
+        /**
+         * @return the number of pixels represented by this swatch
+         */
+        public int getPopulation() {
+            return mPopulation;
+        }
+
+        /**
+         * Returns an appropriate color to use for any 'title' text which is displayed over this
+         * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast.
+         */
+        @ColorInt
+        public int getTitleTextColor() {
+            ensureTextColorsGenerated();
+            return mTitleTextColor;
+        }
+
+        /**
+         * Returns an appropriate color to use for any 'body' text which is displayed over this
+         * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast.
+         */
+        @ColorInt
+        public int getBodyTextColor() {
+            ensureTextColorsGenerated();
+            return mBodyTextColor;
+        }
+
+        private void ensureTextColorsGenerated() {
+            if (!mGeneratedTextColors) {
+                // First check white, as most colors will be dark
+                final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha(
+                        Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT);
+                final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha(
+                        Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT);
+
+                if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
+                    // If we found valid light values, use them and return
+                    mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha);
+                    mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha);
+                    mGeneratedTextColors = true;
+                    return;
+                }
+
+                final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha(
+                        Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT);
+                final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha(
+                        Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT);
+
+                if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
+                    // If we found valid dark values, use them and return
+                    mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
+                    mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
+                    mGeneratedTextColors = true;
+                    return;
+                }
+
+                // If we reach here then we can not find title and body values which use the same
+                // lightness, we need to use mismatched values
+                mBodyTextColor = lightBodyAlpha != -1
+                        ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha)
+                        : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
+                mTitleTextColor = lightTitleAlpha != -1
+                        ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha)
+                        : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
+                mGeneratedTextColors = true;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder(getClass().getSimpleName())
+                    .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']')
+                    .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']')
+                    .append(" [Population: ").append(mPopulation).append(']')
+                    .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor()))
+                    .append(']')
+                    .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor()))
+                    .append(']').toString();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            Palette.Swatch
+                    swatch = (Palette.Swatch) o;
+            return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb;
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * mRgb + mPopulation;
+        }
+    }
+
+    /**
+     * Builder class for generating {@link Palette} instances.
+     */
+    public static final class Builder {
+        private final List<Palette.Swatch> mSwatches;
+        private final Bitmap mBitmap;
+
+        private final List<Target> mTargets = new ArrayList<>();
+
+        private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS;
+        private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA;
+        private int mResizeMaxDimension = -1;
+
+        private final List<Palette.Filter> mFilters = new ArrayList<>();
+        private Rect mRegion;
+
+        private Quantizer mQuantizer;
+
+        /**
+         * Construct a new {@link Palette.Builder} using a source {@link Bitmap}
+         */
+        public Builder(Bitmap bitmap) {
+            if (bitmap == null || bitmap.isRecycled()) {
+                throw new IllegalArgumentException("Bitmap is not valid");
+            }
+            mFilters.add(DEFAULT_FILTER);
+            mBitmap = bitmap;
+            mSwatches = null;
+
+            // Add the default targets
+            mTargets.add(Target.LIGHT_VIBRANT);
+            mTargets.add(Target.VIBRANT);
+            mTargets.add(Target.DARK_VIBRANT);
+            mTargets.add(Target.LIGHT_MUTED);
+            mTargets.add(Target.MUTED);
+            mTargets.add(Target.DARK_MUTED);
+        }
+
+        /**
+         * Construct a new {@link Palette.Builder} using a list of {@link Palette.Swatch} instances.
+         * Typically only used for testing.
+         */
+        public Builder(List<Palette.Swatch> swatches) {
+            if (swatches == null || swatches.isEmpty()) {
+                throw new IllegalArgumentException("List of Swatches is not valid");
+            }
+            mFilters.add(DEFAULT_FILTER);
+            mSwatches = swatches;
+            mBitmap = null;
+        }
+
+        /**
+         * Set the maximum number of colors to use in the quantization step when using a
+         * {@link android.graphics.Bitmap} as the source.
+         * <p>
+         * Good values for depend on the source image type. For landscapes, good values are in
+         * the range 10-16. For images which are largely made up of people's faces then this
+         * value should be increased to ~24.
+         */
+        @NonNull
+        public Palette.Builder maximumColorCount(int colors) {
+            mMaxColors = colors;
+            return this;
+        }
+
+        /**
+         * Set the resize value when using a {@link android.graphics.Bitmap} as the source.
+         * If the bitmap's largest dimension is greater than the value specified, then the bitmap
+         * will be resized so that its largest dimension matches {@code maxDimension}. If the
+         * bitmap is smaller or equal, the original is used as-is.
+         *
+         * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle
+         * abnormal aspect ratios more gracefully.
+         *
+         * @param maxDimension the number of pixels that the max dimension should be scaled down to,
+         *                     or any value <= 0 to disable resizing.
+         */
+        @NonNull
+        @Deprecated
+        public Palette.Builder resizeBitmapSize(final int maxDimension) {
+            mResizeMaxDimension = maxDimension;
+            mResizeArea = -1;
+            return this;
+        }
+
+        /**
+         * Set the resize value when using a {@link android.graphics.Bitmap} as the source.
+         * If the bitmap's area is greater than the value specified, then the bitmap
+         * will be resized so that its area matches {@code area}. If the
+         * bitmap is smaller or equal, the original is used as-is.
+         * <p>
+         * This value has a large effect on the processing time. The larger the resized image is,
+         * the greater time it will take to generate the palette. The smaller the image is, the
+         * more detail is lost in the resulting image and thus less precision for color selection.
+         *
+         * @param area the number of pixels that the intermediary scaled down Bitmap should cover,
+         *             or any value <= 0 to disable resizing.
+         */
+        @NonNull
+        public Palette.Builder resizeBitmapArea(final int area) {
+            mResizeArea = area;
+            mResizeMaxDimension = -1;
+            return this;
+        }
+
+        /**
+         * Clear all added filters. This includes any default filters added automatically by
+         * {@link Palette}.
+         */
+        @NonNull
+        public Palette.Builder clearFilters() {
+            mFilters.clear();
+            return this;
+        }
+
+        /**
+         * Add a filter to be able to have fine grained control over which colors are
+         * allowed in the resulting palette.
+         *
+         * @param filter filter to add.
+         */
+        @NonNull
+        public Palette.Builder addFilter(
+                Palette.Filter filter) {
+            if (filter != null) {
+                mFilters.add(filter);
+            }
+            return this;
+        }
+
+        /**
+         * Set a specific quantization algorithm. {@link ColorCutQuantizer} will
+         * be used if unspecified.
+         *
+         * @param quantizer Quantizer implementation.
+         */
+        @NonNull
+        public Palette.Builder setQuantizer(Quantizer quantizer) {
+            mQuantizer = quantizer;
+            return this;
+        }
+
+        /**
+         * Set a region of the bitmap to be used exclusively when calculating the palette.
+         * <p>This only works when the original input is a {@link Bitmap}.</p>
+         *
+         * @param left The left side of the rectangle used for the region.
+         * @param top The top of the rectangle used for the region.
+         * @param right The right side of the rectangle used for the region.
+         * @param bottom The bottom of the rectangle used for the region.
+         */
+        @NonNull
+        public Palette.Builder setRegion(int left, int top, int right, int bottom) {
+            if (mBitmap != null) {
+                if (mRegion == null) mRegion = new Rect();
+                // Set the Rect to be initially the whole Bitmap
+                mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+                // Now just get the intersection with the region
+                if (!mRegion.intersect(left, top, right, bottom)) {
+                    throw new IllegalArgumentException("The given region must intersect with "
+                            + "the Bitmap's dimensions.");
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Clear any previously region set via {@link #setRegion(int, int, int, int)}.
+         */
+        @NonNull
+        public Palette.Builder clearRegion() {
+            mRegion = null;
+            return this;
+        }
+
+        /**
+         * Add a target profile to be generated in the palette.
+         *
+         * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p>
+         */
+        @NonNull
+        public Palette.Builder addTarget(@NonNull final Target target) {
+            if (!mTargets.contains(target)) {
+                mTargets.add(target);
+            }
+            return this;
+        }
+
+        /**
+         * Clear all added targets. This includes any default targets added automatically by
+         * {@link Palette}.
+         */
+        @NonNull
+        public Palette.Builder clearTargets() {
+            if (mTargets != null) {
+                mTargets.clear();
+            }
+            return this;
+        }
+
+        /**
+         * Generate and return the {@link Palette} synchronously.
+         */
+        @NonNull
+        public Palette generate() {
+            final TimingLogger logger = LOG_TIMINGS
+                    ? new TimingLogger(LOG_TAG, "Generation")
+                    : null;
+
+            List<Palette.Swatch> swatches;
+
+            if (mBitmap != null) {
+                // We have a Bitmap so we need to use quantization to reduce the number of colors
+
+                // First we'll scale down the bitmap if needed
+                final Bitmap bitmap = scaleBitmapDown(mBitmap);
+
+                if (logger != null) {
+                    logger.addSplit("Processed Bitmap");
+                }
+
+                final Rect region = mRegion;
+                if (bitmap != mBitmap && region != null) {
+                    // If we have a scaled bitmap and a selected region, we need to scale down the
+                    // region to match the new scale
+                    final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
+                    region.left = (int) Math.floor(region.left * scale);
+                    region.top = (int) Math.floor(region.top * scale);
+                    region.right = Math.min((int) Math.ceil(region.right * scale),
+                            bitmap.getWidth());
+                    region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
+                            bitmap.getHeight());
+                }
+
+                // Now generate a quantizer from the Bitmap
+                if (mQuantizer == null) {
+                    mQuantizer = new ColorCutQuantizer();
+                }
+                mQuantizer.quantize(getPixelsFromBitmap(bitmap),
+                            mMaxColors, mFilters.isEmpty() ? null :
+                            mFilters.toArray(new Palette.Filter[mFilters.size()]));
+
+                // If created a new bitmap, recycle it
+                if (bitmap != mBitmap) {
+                    bitmap.recycle();
+                }
+
+                swatches = mQuantizer.getQuantizedColors();
+
+                if (logger != null) {
+                    logger.addSplit("Color quantization completed");
+                }
+            } else {
+                // Else we're using the provided swatches
+                swatches = mSwatches;
+            }
+
+            // Now create a Palette instance
+            final Palette p = new Palette(swatches, mTargets);
+            // And make it generate itself
+            p.generate();
+
+            if (logger != null) {
+                logger.addSplit("Created Palette");
+                logger.dumpToLog();
+            }
+
+            return p;
+        }
+
+        /**
+         * Generate the {@link Palette} asynchronously. The provided listener's
+         * {@link Palette.PaletteAsyncListener#onGenerated} method will be called with the palette when
+         * generated.
+         */
+        @NonNull
+        public AsyncTask<Bitmap, Void, Palette> generate(final Palette.PaletteAsyncListener listener) {
+            if (listener == null) {
+                throw new IllegalArgumentException("listener can not be null");
+            }
+
+            return new AsyncTask<Bitmap, Void, Palette>() {
+                @Override
+                protected Palette doInBackground(Bitmap... params) {
+                    try {
+                        return generate();
+                    } catch (Exception e) {
+                        Log.e(LOG_TAG, "Exception thrown during async generate", e);
+                        return null;
+                    }
+                }
+
+                @Override
+                protected void onPostExecute(Palette colorExtractor) {
+                    listener.onGenerated(colorExtractor);
+                }
+            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap);
+        }
+
+        private int[] getPixelsFromBitmap(Bitmap bitmap) {
+            final int bitmapWidth = bitmap.getWidth();
+            final int bitmapHeight = bitmap.getHeight();
+            final int[] pixels = new int[bitmapWidth * bitmapHeight];
+            bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight);
+
+            if (mRegion == null) {
+                // If we don't have a region, return all of the pixels
+                return pixels;
+            } else {
+                // If we do have a region, lets create a subset array containing only the region's
+                // pixels
+                final int regionWidth = mRegion.width();
+                final int regionHeight = mRegion.height();
+                // pixels contains all of the pixels, so we need to iterate through each row and
+                // copy the regions pixels into a new smaller array
+                final int[] subsetPixels = new int[regionWidth * regionHeight];
+                for (int row = 0; row < regionHeight; row++) {
+                    System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left,
+                            subsetPixels, row * regionWidth, regionWidth);
+                }
+                return subsetPixels;
+            }
+        }
+
+        /**
+         * Scale the bitmap down as needed.
+         */
+        private Bitmap scaleBitmapDown(final Bitmap bitmap) {
+            double scaleRatio = -1;
+
+            if (mResizeArea > 0) {
+                final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
+                if (bitmapArea > mResizeArea) {
+                    scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea);
+                }
+            } else if (mResizeMaxDimension > 0) {
+                final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+                if (maxDimension > mResizeMaxDimension) {
+                    scaleRatio = mResizeMaxDimension / (double) maxDimension;
+                }
+            }
+
+            if (scaleRatio <= 0) {
+                // Scaling has been disabled or not needed so just return the Bitmap
+                return bitmap;
+            }
+
+            return Bitmap.createScaledBitmap(bitmap,
+                    (int) Math.ceil(bitmap.getWidth() * scaleRatio),
+                    (int) Math.ceil(bitmap.getHeight() * scaleRatio),
+                    false);
+        }
+    }
+
+    /**
+     * A Filter provides a mechanism for exercising fine-grained control over which colors
+     * are valid within a resulting {@link Palette}.
+     */
+    public interface Filter {
+        /**
+         * Hook to allow clients to be able filter colors from resulting palette.
+         *
+         * @param rgb the color in RGB888.
+         * @param hsl HSL representation of the color.
+         *
+         * @return true if the color is allowed, false if not.
+         *
+         * @see Palette.Builder#addFilter(Palette.Filter)
+         */
+        boolean isAllowed(int rgb, float[] hsl);
+    }
+
+    /**
+     * The default filter.
+     */
+    static final Palette.Filter
+            DEFAULT_FILTER = new Palette.Filter() {
+        private static final float BLACK_MAX_LIGHTNESS = 0.05f;
+        private static final float WHITE_MIN_LIGHTNESS = 0.95f;
+
+        @Override
+        public boolean isAllowed(int rgb, float[] hsl) {
+            return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl);
+        }
+
+        /**
+         * @return true if the color represents a color which is close to black.
+         */
+        private boolean isBlack(float[] hslColor) {
+            return hslColor[2] <= BLACK_MAX_LIGHTNESS;
+        }
+
+        /**
+         * @return true if the color represents a color which is close to white.
+         */
+        private boolean isWhite(float[] hslColor) {
+            return hslColor[2] >= WHITE_MIN_LIGHTNESS;
+        }
+
+        /**
+         * @return true if the color lies close to the red side of the I line.
+         */
+        private boolean isNearRedILine(float[] hslColor) {
+            return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
+        }
+    };
+}
diff --git a/com/android/internal/graphics/palette/Quantizer.java b/com/android/internal/graphics/palette/Quantizer.java
new file mode 100644
index 0000000..db60f2e
--- /dev/null
+++ b/com/android/internal/graphics/palette/Quantizer.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics.palette;
+
+import java.util.List;
+
+/**
+ * Definition of an algorithm that receives pixels and outputs a list of colors.
+ */
+public interface Quantizer {
+    void quantize(final int[] pixels, final int maxColors, final Palette.Filter[] filters);
+    List<Palette.Swatch> getQuantizedColors();
+}
diff --git a/com/android/internal/graphics/palette/Target.java b/com/android/internal/graphics/palette/Target.java
new file mode 100644
index 0000000..0540d80
--- /dev/null
+++ b/com/android/internal/graphics/palette/Target.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics.palette;
+
+/*
+ * Copyright 2015 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.
+ */
+
+import android.annotation.FloatRange;
+
+/**
+ * Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/graphics/Target.java
+ *
+ * A class which allows custom selection of colors in a {@link Palette}'s generation. Instances
+ * can be created via the {@link android.support.v7.graphics.Target.Builder} class.
+ *
+ * <p>To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a
+ * Palette.</p>
+ */
+public final class Target {
+
+    private static final float TARGET_DARK_LUMA = 0.26f;
+    private static final float MAX_DARK_LUMA = 0.45f;
+
+    private static final float MIN_LIGHT_LUMA = 0.55f;
+    private static final float TARGET_LIGHT_LUMA = 0.74f;
+
+    private static final float MIN_NORMAL_LUMA = 0.3f;
+    private static final float TARGET_NORMAL_LUMA = 0.5f;
+    private static final float MAX_NORMAL_LUMA = 0.7f;
+
+    private static final float TARGET_MUTED_SATURATION = 0.3f;
+    private static final float MAX_MUTED_SATURATION = 0.4f;
+
+    private static final float TARGET_VIBRANT_SATURATION = 1f;
+    private static final float MIN_VIBRANT_SATURATION = 0.35f;
+
+    private static final float WEIGHT_SATURATION = 0.24f;
+    private static final float WEIGHT_LUMA = 0.52f;
+    private static final float WEIGHT_POPULATION = 0.24f;
+
+    static final int INDEX_MIN = 0;
+    static final int INDEX_TARGET = 1;
+    static final int INDEX_MAX = 2;
+
+    static final int INDEX_WEIGHT_SAT = 0;
+    static final int INDEX_WEIGHT_LUMA = 1;
+    static final int INDEX_WEIGHT_POP = 2;
+
+    /**
+     * A target which has the characteristics of a vibrant color which is light in luminance.
+     */
+    public static final Target LIGHT_VIBRANT;
+
+    /**
+     * A target which has the characteristics of a vibrant color which is neither light or dark.
+     */
+    public static final Target VIBRANT;
+
+    /**
+     * A target which has the characteristics of a vibrant color which is dark in luminance.
+     */
+    public static final Target DARK_VIBRANT;
+
+    /**
+     * A target which has the characteristics of a muted color which is light in luminance.
+     */
+    public static final Target LIGHT_MUTED;
+
+    /**
+     * A target which has the characteristics of a muted color which is neither light or dark.
+     */
+    public static final Target MUTED;
+
+    /**
+     * A target which has the characteristics of a muted color which is dark in luminance.
+     */
+    public static final Target DARK_MUTED;
+
+    static {
+        LIGHT_VIBRANT = new Target();
+        setDefaultLightLightnessValues(LIGHT_VIBRANT);
+        setDefaultVibrantSaturationValues(LIGHT_VIBRANT);
+
+        VIBRANT = new Target();
+        setDefaultNormalLightnessValues(VIBRANT);
+        setDefaultVibrantSaturationValues(VIBRANT);
+
+        DARK_VIBRANT = new Target();
+        setDefaultDarkLightnessValues(DARK_VIBRANT);
+        setDefaultVibrantSaturationValues(DARK_VIBRANT);
+
+        LIGHT_MUTED = new Target();
+        setDefaultLightLightnessValues(LIGHT_MUTED);
+        setDefaultMutedSaturationValues(LIGHT_MUTED);
+
+        MUTED = new Target();
+        setDefaultNormalLightnessValues(MUTED);
+        setDefaultMutedSaturationValues(MUTED);
+
+        DARK_MUTED = new Target();
+        setDefaultDarkLightnessValues(DARK_MUTED);
+        setDefaultMutedSaturationValues(DARK_MUTED);
+    }
+
+    final float[] mSaturationTargets = new float[3];
+    final float[] mLightnessTargets = new float[3];
+    final float[] mWeights = new float[3];
+    boolean mIsExclusive = true; // default to true
+
+    Target() {
+        setTargetDefaultValues(mSaturationTargets);
+        setTargetDefaultValues(mLightnessTargets);
+        setDefaultWeights();
+    }
+
+    Target(Target from) {
+        System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0,
+                mSaturationTargets.length);
+        System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0,
+                mLightnessTargets.length);
+        System.arraycopy(from.mWeights, 0, mWeights, 0, mWeights.length);
+    }
+
+    /**
+     * The minimum saturation value for this target.
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getMinimumSaturation() {
+        return mSaturationTargets[INDEX_MIN];
+    }
+
+    /**
+     * The target saturation value for this target.
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getTargetSaturation() {
+        return mSaturationTargets[INDEX_TARGET];
+    }
+
+    /**
+     * The maximum saturation value for this target.
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getMaximumSaturation() {
+        return mSaturationTargets[INDEX_MAX];
+    }
+
+    /**
+     * The minimum lightness value for this target.
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getMinimumLightness() {
+        return mLightnessTargets[INDEX_MIN];
+    }
+
+    /**
+     * The target lightness value for this target.
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getTargetLightness() {
+        return mLightnessTargets[INDEX_TARGET];
+    }
+
+    /**
+     * The maximum lightness value for this target.
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getMaximumLightness() {
+        return mLightnessTargets[INDEX_MAX];
+    }
+
+    /**
+     * Returns the weight of importance that this target places on a color's saturation within
+     * the image.
+     *
+     * <p>The larger the weight, relative to the other weights, the more important that a color
+     * being close to the target value has on selection.</p>
+     *
+     * @see #getTargetSaturation()
+     */
+    public float getSaturationWeight() {
+        return mWeights[INDEX_WEIGHT_SAT];
+    }
+
+    /**
+     * Returns the weight of importance that this target places on a color's lightness within
+     * the image.
+     *
+     * <p>The larger the weight, relative to the other weights, the more important that a color
+     * being close to the target value has on selection.</p>
+     *
+     * @see #getTargetLightness()
+     */
+    public float getLightnessWeight() {
+        return mWeights[INDEX_WEIGHT_LUMA];
+    }
+
+    /**
+     * Returns the weight of importance that this target places on a color's population within
+     * the image.
+     *
+     * <p>The larger the weight, relative to the other weights, the more important that a
+     * color's population being close to the most populous has on selection.</p>
+     */
+    public float getPopulationWeight() {
+        return mWeights[INDEX_WEIGHT_POP];
+    }
+
+    /**
+     * Returns whether any color selected for this target is exclusive for this target only.
+     *
+     * <p>If false, then the color can be selected for other targets.</p>
+     */
+    public boolean isExclusive() {
+        return mIsExclusive;
+    }
+
+    private static void setTargetDefaultValues(final float[] values) {
+        values[INDEX_MIN] = 0f;
+        values[INDEX_TARGET] = 0.5f;
+        values[INDEX_MAX] = 1f;
+    }
+
+    private void setDefaultWeights() {
+        mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION;
+        mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA;
+        mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION;
+    }
+
+    void normalizeWeights() {
+        float sum = 0;
+        for (int i = 0, z = mWeights.length; i < z; i++) {
+            float weight = mWeights[i];
+            if (weight > 0) {
+                sum += weight;
+            }
+        }
+        if (sum != 0) {
+            for (int i = 0, z = mWeights.length; i < z; i++) {
+                if (mWeights[i] > 0) {
+                    mWeights[i] /= sum;
+                }
+            }
+        }
+    }
+
+    private static void setDefaultDarkLightnessValues(Target target) {
+        target.mLightnessTargets[INDEX_TARGET] = TARGET_DARK_LUMA;
+        target.mLightnessTargets[INDEX_MAX] = MAX_DARK_LUMA;
+    }
+
+    private static void setDefaultNormalLightnessValues(Target target) {
+        target.mLightnessTargets[INDEX_MIN] = MIN_NORMAL_LUMA;
+        target.mLightnessTargets[INDEX_TARGET] = TARGET_NORMAL_LUMA;
+        target.mLightnessTargets[INDEX_MAX] = MAX_NORMAL_LUMA;
+    }
+
+    private static void setDefaultLightLightnessValues(Target target) {
+        target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA;
+        target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA;
+    }
+
+    private static void setDefaultVibrantSaturationValues(Target target) {
+        target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION;
+        target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION;
+    }
+
+    private static void setDefaultMutedSaturationValues(Target target) {
+        target.mSaturationTargets[INDEX_TARGET] = TARGET_MUTED_SATURATION;
+        target.mSaturationTargets[INDEX_MAX] = MAX_MUTED_SATURATION;
+    }
+
+    /**
+     * Builder class for generating custom {@link Target} instances.
+     */
+    public final static class Builder {
+        private final Target mTarget;
+
+        /**
+         * Create a new {@link Target} builder from scratch.
+         */
+        public Builder() {
+            mTarget = new Target();
+        }
+
+        /**
+         * Create a new builder based on an existing {@link Target}.
+         */
+        public Builder(Target target) {
+            mTarget = new Target(target);
+        }
+
+        /**
+         * Set the minimum saturation value for this target.
+         */
+        public Target.Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) {
+            mTarget.mSaturationTargets[INDEX_MIN] = value;
+            return this;
+        }
+
+        /**
+         * Set the target/ideal saturation value for this target.
+         */
+        public Target.Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) {
+            mTarget.mSaturationTargets[INDEX_TARGET] = value;
+            return this;
+        }
+
+        /**
+         * Set the maximum saturation value for this target.
+         */
+        public Target.Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) {
+            mTarget.mSaturationTargets[INDEX_MAX] = value;
+            return this;
+        }
+
+        /**
+         * Set the minimum lightness value for this target.
+         */
+        public Target.Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) {
+            mTarget.mLightnessTargets[INDEX_MIN] = value;
+            return this;
+        }
+
+        /**
+         * Set the target/ideal lightness value for this target.
+         */
+        public Target.Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) {
+            mTarget.mLightnessTargets[INDEX_TARGET] = value;
+            return this;
+        }
+
+        /**
+         * Set the maximum lightness value for this target.
+         */
+        public Target.Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) {
+            mTarget.mLightnessTargets[INDEX_MAX] = value;
+            return this;
+        }
+
+        /**
+         * Set the weight of importance that this target will place on saturation values.
+         *
+         * <p>The larger the weight, relative to the other weights, the more important that a color
+         * being close to the target value has on selection.</p>
+         *
+         * <p>A weight of 0 means that it has no weight, and thus has no
+         * bearing on the selection.</p>
+         *
+         * @see #setTargetSaturation(float)
+         */
+        public Target.Builder setSaturationWeight(@FloatRange(from = 0) float weight) {
+            mTarget.mWeights[INDEX_WEIGHT_SAT] = weight;
+            return this;
+        }
+
+        /**
+         * Set the weight of importance that this target will place on lightness values.
+         *
+         * <p>The larger the weight, relative to the other weights, the more important that a color
+         * being close to the target value has on selection.</p>
+         *
+         * <p>A weight of 0 means that it has no weight, and thus has no
+         * bearing on the selection.</p>
+         *
+         * @see #setTargetLightness(float)
+         */
+        public Target.Builder setLightnessWeight(@FloatRange(from = 0) float weight) {
+            mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight;
+            return this;
+        }
+
+        /**
+         * Set the weight of importance that this target will place on a color's population within
+         * the image.
+         *
+         * <p>The larger the weight, relative to the other weights, the more important that a
+         * color's population being close to the most populous has on selection.</p>
+         *
+         * <p>A weight of 0 means that it has no weight, and thus has no
+         * bearing on the selection.</p>
+         */
+        public Target.Builder setPopulationWeight(@FloatRange(from = 0) float weight) {
+            mTarget.mWeights[INDEX_WEIGHT_POP] = weight;
+            return this;
+        }
+
+        /**
+         * Set whether any color selected for this target is exclusive to this target only.
+         * Defaults to true.
+         *
+         * @param exclusive true if any the color is exclusive to this target, or false is the
+         *                  color can be selected for other targets.
+         */
+        public Target.Builder setExclusive(boolean exclusive) {
+            mTarget.mIsExclusive = exclusive;
+            return this;
+        }
+
+        /**
+         * Builds and returns the resulting {@link Target}.
+         */
+        public Target build() {
+            return mTarget;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java b/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java
new file mode 100644
index 0000000..b035535
--- /dev/null
+++ b/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2017 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.internal.graphics.palette;
+
+import android.util.Log;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.ml.clustering.KMeans;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * A quantizer that uses k-means
+ */
+public class VariationalKMeansQuantizer implements Quantizer {
+
+    private static final String TAG = "KMeansQuantizer";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Clusters closer than this value will me merged.
+     */
+    private final float mMinClusterSqDistance;
+
+    /**
+     * K-means can get stuck in local optima, this can be avoided by
+     * repeating it and getting the "best" execution.
+     */
+    private final int mInitializations;
+
+    /**
+     * Initialize KMeans with a fixed random state to have
+     * consistent results across multiple runs.
+     */
+    private final KMeans mKMeans = new KMeans(new Random(0), 30, 0);
+
+    private List<Palette.Swatch> mQuantizedColors;
+
+    public VariationalKMeansQuantizer() {
+        this(0.25f /* cluster distance */);
+    }
+
+    public VariationalKMeansQuantizer(float minClusterDistance) {
+        this(minClusterDistance, 1 /* initializations */);
+    }
+
+    public VariationalKMeansQuantizer(float minClusterDistance, int initializations) {
+        mMinClusterSqDistance = minClusterDistance * minClusterDistance;
+        mInitializations = initializations;
+    }
+
+    /**
+     * K-Means quantizer.
+     *
+     * @param pixels Pixels to quantize.
+     * @param maxColors Maximum number of clusters to extract.
+     * @param filters Colors that should be ignored
+     */
+    @Override
+    public void quantize(int[] pixels, int maxColors, Palette.Filter[] filters) {
+        // Start by converting all colors to HSL.
+        // HLS is way more meaningful for clustering than RGB.
+        final float[] hsl = {0, 0, 0};
+        final float[][] hslPixels = new float[pixels.length][3];
+        for (int i = 0; i < pixels.length; i++) {
+            ColorUtils.colorToHSL(pixels[i], hsl);
+            // Normalize hue so all values go from 0 to 1.
+            hslPixels[i][0] = hsl[0] / 360f;
+            hslPixels[i][1] = hsl[1];
+            hslPixels[i][2] = hsl[2];
+        }
+
+        final List<KMeans.Mean> optimalMeans = getOptimalKMeans(maxColors, hslPixels);
+
+        // Ideally we should run k-means again to merge clusters but it would be too expensive,
+        // instead we just merge all clusters that are closer than a threshold.
+        for (int i = 0; i < optimalMeans.size(); i++) {
+            KMeans.Mean current = optimalMeans.get(i);
+            float[] currentCentroid = current.getCentroid();
+            for (int j = i + 1; j < optimalMeans.size(); j++) {
+                KMeans.Mean compareTo = optimalMeans.get(j);
+                float[] compareToCentroid = compareTo.getCentroid();
+                float sqDistance = KMeans.sqDistance(currentCentroid, compareToCentroid);
+                // Merge them
+                if (sqDistance < mMinClusterSqDistance) {
+                    optimalMeans.remove(compareTo);
+                    current.getItems().addAll(compareTo.getItems());
+                    for (int k = 0; k < currentCentroid.length; k++) {
+                        currentCentroid[k] += (compareToCentroid[k] - currentCentroid[k]) / 2.0;
+                    }
+                    j--;
+                }
+            }
+        }
+
+        // Convert data to final format, de-normalizing the hue.
+        mQuantizedColors = new ArrayList<>();
+        for (KMeans.Mean mean : optimalMeans) {
+            if (mean.getItems().size() == 0) {
+                continue;
+            }
+            float[] centroid = mean.getCentroid();
+            mQuantizedColors.add(new Palette.Swatch(new float[]{
+                    centroid[0] * 360f,
+                    centroid[1],
+                    centroid[2]
+            }, mean.getItems().size()));
+        }
+    }
+
+    private List<KMeans.Mean> getOptimalKMeans(int k, float[][] inputData) {
+        List<KMeans.Mean> optimal = null;
+        double optimalScore = -Double.MAX_VALUE;
+        int runs = mInitializations;
+        while (runs > 0) {
+            if (DEBUG) {
+                Log.d(TAG, "k-means run: " + runs);
+            }
+            List<KMeans.Mean> means = mKMeans.predict(k, inputData);
+            double score = KMeans.score(means);
+            if (optimal == null || score > optimalScore) {
+                if (DEBUG) {
+                    Log.d(TAG, "\tnew optimal score: " + score);
+                }
+                optimalScore = score;
+                optimal = means;
+            }
+            runs--;
+        }
+
+        return optimal;
+    }
+
+    @Override
+    public List<Palette.Swatch> getQuantizedColors() {
+        return mQuantizedColors;
+    }
+}
diff --git a/com/android/internal/hardware/AmbientDisplayConfiguration.java b/com/android/internal/hardware/AmbientDisplayConfiguration.java
new file mode 100644
index 0000000..1811800
--- /dev/null
+++ b/com/android/internal/hardware/AmbientDisplayConfiguration.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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.internal.hardware;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+public class AmbientDisplayConfiguration {
+
+    private final Context mContext;
+
+    public AmbientDisplayConfiguration(Context context) {
+        mContext = context;
+    }
+
+    public boolean enabled(int user) {
+        return pulseOnNotificationEnabled(user)
+                || pulseOnPickupEnabled(user)
+                || pulseOnDoubleTapEnabled(user)
+                || pulseOnLongPressEnabled(user)
+                || alwaysOnEnabled(user);
+    }
+
+    public boolean available() {
+        return pulseOnNotificationAvailable() || pulseOnPickupAvailable()
+                || pulseOnDoubleTapAvailable();
+    }
+
+    public boolean pulseOnNotificationEnabled(int user) {
+        return boolSettingDefaultOn(Settings.Secure.DOZE_ENABLED, user) && pulseOnNotificationAvailable();
+    }
+
+    public boolean pulseOnNotificationAvailable() {
+        return ambientDisplayAvailable();
+    }
+
+    public boolean pulseOnPickupEnabled(int user) {
+        boolean settingEnabled = boolSettingDefaultOn(Settings.Secure.DOZE_PULSE_ON_PICK_UP, user);
+        return (settingEnabled || alwaysOnEnabled(user)) && pulseOnPickupAvailable();
+    }
+
+    public boolean pulseOnPickupAvailable() {
+        return mContext.getResources().getBoolean(R.bool.config_dozePulsePickup)
+                && ambientDisplayAvailable();
+    }
+
+    public boolean pulseOnPickupCanBeModified(int user) {
+        return !alwaysOnEnabled(user);
+    }
+
+    public boolean pulseOnDoubleTapEnabled(int user) {
+        return boolSettingDefaultOn(Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP, user)
+                && pulseOnDoubleTapAvailable();
+    }
+
+    public boolean pulseOnDoubleTapAvailable() {
+        return !TextUtils.isEmpty(doubleTapSensorType()) && ambientDisplayAvailable();
+    }
+
+    public String doubleTapSensorType() {
+        return mContext.getResources().getString(R.string.config_dozeDoubleTapSensorType);
+    }
+
+    public String longPressSensorType() {
+        return mContext.getResources().getString(R.string.config_dozeLongPressSensorType);
+    }
+
+    public boolean pulseOnLongPressEnabled(int user) {
+        return pulseOnLongPressAvailable() && boolSettingDefaultOff(
+                Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, user);
+    }
+
+    private boolean pulseOnLongPressAvailable() {
+        return !TextUtils.isEmpty(longPressSensorType());
+    }
+
+    public boolean alwaysOnEnabled(int user) {
+        return boolSettingDefaultOn(Settings.Secure.DOZE_ALWAYS_ON, user) && alwaysOnAvailable()
+                && !accessibilityInversionEnabled(user);
+    }
+
+    public boolean alwaysOnAvailable() {
+        return (alwaysOnDisplayDebuggingEnabled() || alwaysOnDisplayAvailable())
+                && ambientDisplayAvailable();
+    }
+
+    public boolean alwaysOnAvailableForUser(int user) {
+        return alwaysOnAvailable() && !accessibilityInversionEnabled(user);
+    }
+
+    public String ambientDisplayComponent() {
+        return mContext.getResources().getString(R.string.config_dozeComponent);
+    }
+
+    public boolean accessibilityInversionEnabled(int user) {
+        return boolSettingDefaultOff(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, user);
+    }
+
+    private boolean ambientDisplayAvailable() {
+        return !TextUtils.isEmpty(ambientDisplayComponent());
+    }
+
+    private boolean alwaysOnDisplayAvailable() {
+        return mContext.getResources().getBoolean(R.bool.config_dozeAlwaysOnDisplayAvailable);
+    }
+
+    private boolean alwaysOnDisplayDebuggingEnabled() {
+        return SystemProperties.getBoolean("debug.doze.aod", false) && Build.IS_DEBUGGABLE;
+    }
+
+
+    private boolean boolSettingDefaultOn(String name, int user) {
+        return boolSetting(name, user, 1);
+    }
+
+    private boolean boolSettingDefaultOff(String name, int user) {
+        return boolSetting(name, user, 0);
+    }
+
+    private boolean boolSetting(String name, int user, int def) {
+        return Settings.Secure.getIntForUser(mContext.getContentResolver(), name, def, user) != 0;
+    }
+}
diff --git a/com/android/internal/http/HttpDateTime.java b/com/android/internal/http/HttpDateTime.java
new file mode 100644
index 0000000..8ebd4aa
--- /dev/null
+++ b/com/android/internal/http/HttpDateTime.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2007 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.internal.http;
+
+import android.text.format.Time;
+
+import java.util.Calendar;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper for parsing an HTTP date.
+ */
+public final class HttpDateTime {
+
+    /*
+     * Regular expression for parsing HTTP-date.
+     *
+     * Wdy, DD Mon YYYY HH:MM:SS GMT
+     * RFC 822, updated by RFC 1123
+     *
+     * Weekday, DD-Mon-YY HH:MM:SS GMT
+     * RFC 850, obsoleted by RFC 1036
+     *
+     * Wdy Mon DD HH:MM:SS YYYY
+     * ANSI C's asctime() format
+     *
+     * with following variations
+     *
+     * Wdy, DD-Mon-YYYY HH:MM:SS GMT
+     * Wdy, (SP)D Mon YYYY HH:MM:SS GMT
+     * Wdy,DD Mon YYYY HH:MM:SS GMT
+     * Wdy, DD-Mon-YY HH:MM:SS GMT
+     * Wdy, DD Mon YYYY HH:MM:SS -HHMM
+     * Wdy, DD Mon YYYY HH:MM:SS
+     * Wdy Mon (SP)D HH:MM:SS YYYY
+     * Wdy Mon DD HH:MM:SS YYYY GMT
+     * 
+     * HH can be H if the first digit is zero.
+     * 
+     * Mon can be the full name of the month.
+     */
+    private static final String HTTP_DATE_RFC_REGEXP =
+            "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
+            + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
+
+    private static final String HTTP_DATE_ANSIC_REGEXP =
+            "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
+            + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
+
+    /**
+     * The compiled version of the HTTP-date regular expressions.
+     */
+    private static final Pattern HTTP_DATE_RFC_PATTERN =
+            Pattern.compile(HTTP_DATE_RFC_REGEXP);
+    private static final Pattern HTTP_DATE_ANSIC_PATTERN =
+            Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
+
+    private static class TimeOfDay {
+        TimeOfDay(int h, int m, int s) {
+            this.hour = h;
+            this.minute = m;
+            this.second = s;
+        }
+        
+        int hour;
+        int minute;
+        int second;
+    }
+
+    public static long parse(String timeString)
+            throws IllegalArgumentException {
+
+        int date = 1;
+        int month = Calendar.JANUARY;
+        int year = 1970;
+        TimeOfDay timeOfDay;
+
+        Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
+        if (rfcMatcher.find()) {
+            date = getDate(rfcMatcher.group(1));
+            month = getMonth(rfcMatcher.group(2));
+            year = getYear(rfcMatcher.group(3));
+            timeOfDay = getTime(rfcMatcher.group(4));
+        } else {
+            Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
+            if (ansicMatcher.find()) {
+                month = getMonth(ansicMatcher.group(1));
+                date = getDate(ansicMatcher.group(2));
+                timeOfDay = getTime(ansicMatcher.group(3));
+                year = getYear(ansicMatcher.group(4));
+            } else {
+                throw new IllegalArgumentException();
+            }
+        }
+
+        // FIXME: Y2038 BUG!
+        if (year >= 2038) {
+            year = 2038;
+            month = Calendar.JANUARY;
+            date = 1;
+        }
+
+        Time time = new Time(Time.TIMEZONE_UTC);
+        time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
+                month, year);
+        return time.toMillis(false /* use isDst */);
+    }
+
+    private static int getDate(String dateString) {
+        if (dateString.length() == 2) {
+            return (dateString.charAt(0) - '0') * 10
+                    + (dateString.charAt(1) - '0');
+        } else {
+            return (dateString.charAt(0) - '0');
+        }
+    }
+
+    /*
+     * jan = 9 + 0 + 13 = 22
+     * feb = 5 + 4 + 1 = 10
+     * mar = 12 + 0 + 17 = 29
+     * apr = 0 + 15 + 17 = 32
+     * may = 12 + 0 + 24 = 36
+     * jun = 9 + 20 + 13 = 42
+     * jul = 9 + 20 + 11 = 40
+     * aug = 0 + 20 + 6 = 26
+     * sep = 18 + 4 + 15 = 37
+     * oct = 14 + 2 + 19 = 35
+     * nov = 13 + 14 + 21 = 48
+     * dec = 3 + 4 + 2 = 9
+     */
+    private static int getMonth(String monthString) {
+        int hash = Character.toLowerCase(monthString.charAt(0)) +
+                Character.toLowerCase(monthString.charAt(1)) +
+                Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
+        switch (hash) {
+            case 22:
+                return Calendar.JANUARY;
+            case 10:
+                return Calendar.FEBRUARY;
+            case 29:
+                return Calendar.MARCH;
+            case 32:
+                return Calendar.APRIL;
+            case 36:
+                return Calendar.MAY;
+            case 42:
+                return Calendar.JUNE;
+            case 40:
+                return Calendar.JULY;
+            case 26:
+                return Calendar.AUGUST;
+            case 37:
+                return Calendar.SEPTEMBER;
+            case 35:
+                return Calendar.OCTOBER;
+            case 48:
+                return Calendar.NOVEMBER;
+            case 9:
+                return Calendar.DECEMBER;
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    private static int getYear(String yearString) {
+        if (yearString.length() == 2) {
+            int year = (yearString.charAt(0) - '0') * 10
+                    + (yearString.charAt(1) - '0');
+            if (year >= 70) {
+                return year + 1900;
+            } else {
+                return year + 2000;
+            }
+        } else if (yearString.length() == 3) {
+            // According to RFC 2822, three digit years should be added to 1900.
+            int year = (yearString.charAt(0) - '0') * 100
+                    + (yearString.charAt(1) - '0') * 10
+                    + (yearString.charAt(2) - '0');
+            return year + 1900;
+        } else if (yearString.length() == 4) {
+             return (yearString.charAt(0) - '0') * 1000
+                    + (yearString.charAt(1) - '0') * 100
+                    + (yearString.charAt(2) - '0') * 10
+                    + (yearString.charAt(3) - '0');
+        } else {
+             return 1970;
+        }
+    }
+
+    private static TimeOfDay getTime(String timeString) {
+        // HH might be H
+        int i = 0;
+        int hour = timeString.charAt(i++) - '0';
+        if (timeString.charAt(i) != ':')
+            hour = hour * 10 + (timeString.charAt(i++) - '0');
+        // Skip ':'
+        i++;
+        
+        int minute = (timeString.charAt(i++) - '0') * 10
+                    + (timeString.charAt(i++) - '0');
+        // Skip ':'
+        i++;
+        
+        int second = (timeString.charAt(i++) - '0') * 10
+                  + (timeString.charAt(i++) - '0');
+
+        return new TimeOfDay(hour, minute, second);        
+    }
+}
diff --git a/com/android/internal/inputmethod/InputMethodSubtypeHandle.java b/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
new file mode 100644
index 0000000..04d7f9b
--- /dev/null
+++ b/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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.internal.inputmethod;
+
+import android.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.util.Objects;
+
+public class InputMethodSubtypeHandle {
+    private final String mInputMethodId;
+    private final int mSubtypeId;
+
+    public InputMethodSubtypeHandle(InputMethodInfo info, @Nullable InputMethodSubtype subtype) {
+        mInputMethodId = info.getId();
+        if (subtype != null) {
+            mSubtypeId = subtype.hashCode();
+        } else {
+            mSubtypeId = InputMethodUtils.NOT_A_SUBTYPE_ID;
+        }
+    }
+
+    public InputMethodSubtypeHandle(String inputMethodId, int subtypeId) {
+        mInputMethodId = inputMethodId;
+        mSubtypeId = subtypeId;
+    }
+
+    public String getInputMethodId() {
+        return mInputMethodId;
+    }
+
+    public int getSubtypeId() {
+        return mSubtypeId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || !(o instanceof InputMethodSubtypeHandle)) {
+            return false;
+        }
+        InputMethodSubtypeHandle other = (InputMethodSubtypeHandle) o;
+        return TextUtils.equals(mInputMethodId, other.getInputMethodId())
+                && mSubtypeId == other.getSubtypeId();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mInputMethodId) * 31 + mSubtypeId;
+    }
+
+    @Override
+    public String toString() {
+        return "InputMethodSubtypeHandle{mInputMethodId=" + mInputMethodId
+            + ", mSubtypeId=" + mSubtypeId + "}";
+    }
+}
diff --git a/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java b/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java
new file mode 100644
index 0000000..dfc0696
--- /dev/null
+++ b/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -0,0 +1,609 @@
+/*
+ * 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.internal.inputmethod;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Printer;
+import android.util.Slog;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TreeMap;
+
+/**
+ * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes.
+ * <p>
+ * This class is designed to be used from and only from
+ * {@link com.android.server.InputMethodManagerService} by using
+ * {@link com.android.server.InputMethodManagerService#mMethodMap} as a global lock.
+ * </p>
+ */
+public class InputMethodSubtypeSwitchingController {
+    private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
+
+    public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
+        public final CharSequence mImeName;
+        public final CharSequence mSubtypeName;
+        public final InputMethodInfo mImi;
+        public final int mSubtypeId;
+        public final boolean mIsSystemLocale;
+        public final boolean mIsSystemLanguage;
+
+        public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
+                InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
+            mImeName = imeName;
+            mSubtypeName = subtypeName;
+            mImi = imi;
+            mSubtypeId = subtypeId;
+            if (TextUtils.isEmpty(subtypeLocale)) {
+                mIsSystemLocale = false;
+                mIsSystemLanguage = false;
+            } else {
+                mIsSystemLocale = subtypeLocale.equals(systemLocale);
+                if (mIsSystemLocale) {
+                    mIsSystemLanguage = true;
+                } else {
+                    // TODO: Use Locale#getLanguage or Locale#toLanguageTag
+                    final String systemLanguage = parseLanguageFromLocaleString(systemLocale);
+                    final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
+                    mIsSystemLanguage = systemLanguage.length() >= 2 &&
+                            systemLanguage.equals(subtypeLanguage);
+                }
+            }
+        }
+
+        /**
+         * Returns the language component of a given locale string.
+         * TODO: Use {@link Locale#getLanguage()} instead.
+         */
+        private static String parseLanguageFromLocaleString(final String locale) {
+            final int idx = locale.indexOf('_');
+            if (idx < 0) {
+                return locale;
+            } else {
+                return locale.substring(0, idx);
+            }
+        }
+
+        private static int compareNullableCharSequences(@Nullable CharSequence c1,
+                @Nullable CharSequence c2) {
+            // For historical reasons, an empty text needs to put at the last.
+            final boolean empty1 = TextUtils.isEmpty(c1);
+            final boolean empty2 = TextUtils.isEmpty(c2);
+            if (empty1 || empty2) {
+                return (empty1 ? 1 : 0) - (empty2 ? 1 : 0);
+            }
+            return c1.toString().compareTo(c2.toString());
+        }
+
+        /**
+         * Compares this object with the specified object for order. The fields of this class will
+         * be compared in the following order.
+         * <ol>
+         *   <li>{@link #mImeName}</li>
+         *   <li>{@link #mIsSystemLocale}</li>
+         *   <li>{@link #mIsSystemLanguage}</li>
+         *   <li>{@link #mSubtypeName}</li>
+         * </ol>
+         * Note: this class has a natural ordering that is inconsistent with {@link #equals(Object).
+         * This method doesn't compare {@link #mSubtypeId} but {@link #equals(Object)} does.
+         *
+         * @param other the object to be compared.
+         * @return a negative integer, zero, or positive integer as this object is less than, equal
+         *         to, or greater than the specified <code>other</code> object.
+         */
+        @Override
+        public int compareTo(ImeSubtypeListItem other) {
+            int result = compareNullableCharSequences(mImeName, other.mImeName);
+            if (result != 0) {
+                return result;
+            }
+            // Subtype that has the same locale of the system's has higher priority.
+            result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0);
+            if (result != 0) {
+                return result;
+            }
+            // Subtype that has the same language of the system's has higher priority.
+            result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0);
+            if (result != 0) {
+                return result;
+            }
+            return compareNullableCharSequences(mSubtypeName, other.mSubtypeName);
+        }
+
+        @Override
+        public String toString() {
+            return "ImeSubtypeListItem{"
+                    + "mImeName=" + mImeName
+                    + " mSubtypeName=" + mSubtypeName
+                    + " mSubtypeId=" + mSubtypeId
+                    + " mIsSystemLocale=" + mIsSystemLocale
+                    + " mIsSystemLanguage=" + mIsSystemLanguage
+                    + "}";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (o instanceof ImeSubtypeListItem) {
+                final ImeSubtypeListItem that = (ImeSubtypeListItem)o;
+                return Objects.equals(this.mImi, that.mImi) && this.mSubtypeId == that.mSubtypeId;
+            }
+            return false;
+        }
+    }
+
+    private static class InputMethodAndSubtypeList {
+        private final Context mContext;
+        // Used to load label
+        private final PackageManager mPm;
+        private final String mSystemLocaleStr;
+        private final InputMethodSettings mSettings;
+
+        public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) {
+            mContext = context;
+            mSettings = settings;
+            mPm = context.getPackageManager();
+            final Locale locale = context.getResources().getConfiguration().locale;
+            mSystemLocaleStr = locale != null ? locale.toString() : "";
+        }
+
+        private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis =
+                new TreeMap<>(
+                        new Comparator<InputMethodInfo>() {
+                            @Override
+                            public int compare(InputMethodInfo imi1, InputMethodInfo imi2) {
+                                if (imi2 == null)
+                                    return 0;
+                                if (imi1 == null)
+                                    return 1;
+                                if (mPm == null) {
+                                    return imi1.getId().compareTo(imi2.getId());
+                                }
+                                CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId();
+                                CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId();
+                                return imiId1.toString().compareTo(imiId2.toString());
+                            }
+                        });
+
+        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
+                boolean includeAuxiliarySubtypes, boolean isScreenLocked) {
+            final ArrayList<ImeSubtypeListItem> imList = new ArrayList<>();
+            final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
+                    mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(
+                            mContext);
+            if (immis == null || immis.size() == 0) {
+                return Collections.emptyList();
+            }
+            if (isScreenLocked && includeAuxiliarySubtypes) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Auxiliary subtypes are not allowed to be shown in lock screen.");
+                }
+                includeAuxiliarySubtypes = false;
+            }
+            mSortedImmis.clear();
+            mSortedImmis.putAll(immis);
+            for (InputMethodInfo imi : mSortedImmis.keySet()) {
+                if (imi == null) {
+                    continue;
+                }
+                List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
+                HashSet<String> enabledSubtypeSet = new HashSet<>();
+                for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
+                    enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
+                }
+                final CharSequence imeLabel = imi.loadLabel(mPm);
+                if (enabledSubtypeSet.size() > 0) {
+                    final int subtypeCount = imi.getSubtypeCount();
+                    if (DEBUG) {
+                        Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
+                    }
+                    for (int j = 0; j < subtypeCount; ++j) {
+                        final InputMethodSubtype subtype = imi.getSubtypeAt(j);
+                        final String subtypeHashCode = String.valueOf(subtype.hashCode());
+                        // We show all enabled IMEs and subtypes when an IME is shown.
+                        if (enabledSubtypeSet.contains(subtypeHashCode)
+                                && (includeAuxiliarySubtypes || !subtype.isAuxiliary())) {
+                            final CharSequence subtypeLabel =
+                                    subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
+                                            .getDisplayName(mContext, imi.getPackageName(),
+                                                    imi.getServiceInfo().applicationInfo);
+                            imList.add(new ImeSubtypeListItem(imeLabel,
+                                    subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
+
+                            // Removing this subtype from enabledSubtypeSet because we no
+                            // longer need to add an entry of this subtype to imList to avoid
+                            // duplicated entries.
+                            enabledSubtypeSet.remove(subtypeHashCode);
+                        }
+                    }
+                } else {
+                    imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
+                            mSystemLocaleStr));
+                }
+            }
+            Collections.sort(imList);
+            return imList;
+        }
+    }
+
+    private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) {
+        return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi,
+                subtype.hashCode()) : NOT_A_SUBTYPE_ID;
+    }
+
+    private static class StaticRotationList {
+        private final List<ImeSubtypeListItem> mImeSubtypeList;
+        public StaticRotationList(final List<ImeSubtypeListItem> imeSubtypeList) {
+            mImeSubtypeList = imeSubtypeList;
+        }
+
+        /**
+         * Returns the index of the specified input method and subtype in the given list.
+         * @param imi The {@link InputMethodInfo} to be searched.
+         * @param subtype The {@link InputMethodSubtype} to be searched. null if the input method
+         * does not have a subtype.
+         * @return The index in the given list. -1 if not found.
+         */
+        private int getIndex(InputMethodInfo imi, InputMethodSubtype subtype) {
+            final int currentSubtypeId = calculateSubtypeId(imi, subtype);
+            final int N = mImeSubtypeList.size();
+            for (int i = 0; i < N; ++i) {
+                final ImeSubtypeListItem isli = mImeSubtypeList.get(i);
+                // Skip until the current IME/subtype is found.
+                if (imi.equals(isli.mImi) && isli.mSubtypeId == currentSubtypeId) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        /**
+         * Provides the basic operation to implement bi-directional IME rotation.
+         * @param onlyCurrentIme {@code true} to limit the search space to IME subtypes that belong
+         * to {@code imi}.
+         * @param imi {@link InputMethodInfo} that will be used in conjunction with {@code subtype}
+         * from which we find the adjacent IME subtype.
+         * @param subtype {@link InputMethodSubtype} that will be used in conjunction with
+         * {@code imi} from which we find the next IME subtype.  {@code null} if the input method
+         * does not have a subtype.
+         * @param forward {@code true} to do forward search the next IME subtype. Specify
+         * {@code false} to do backward search.
+         * @return The IME subtype found. {@code null} if no IME subtype is found.
+         */
+        @Nullable
+        public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
+                InputMethodInfo imi, @Nullable InputMethodSubtype subtype, boolean forward) {
+            if (imi == null) {
+                return null;
+            }
+            if (mImeSubtypeList.size() <= 1) {
+                return null;
+            }
+            final int currentIndex = getIndex(imi, subtype);
+            if (currentIndex < 0) {
+                return null;
+            }
+            final int N = mImeSubtypeList.size();
+            for (int i = 1; i < N; ++i) {
+                // Start searching the next IME/subtype from +/- 1 indices.
+                final int offset = forward ? i : N - i;
+                final int candidateIndex = (currentIndex + offset) % N;
+                final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex);
+                // Skip if searching inside the current IME only, but the candidate is not
+                // the current IME.
+                if (onlyCurrentIme && !imi.equals(candidate.mImi)) {
+                    continue;
+                }
+                return candidate;
+            }
+            return null;
+        }
+
+        protected void dump(final Printer pw, final String prefix) {
+            final int N = mImeSubtypeList.size();
+            for (int i = 0; i < N; ++i) {
+                final int rank = i;
+                final ImeSubtypeListItem item = mImeSubtypeList.get(i);
+                pw.println(prefix + "rank=" + rank + " item=" + item);
+            }
+        }
+    }
+
+    private static class DynamicRotationList {
+        private static final String TAG = DynamicRotationList.class.getSimpleName();
+        private final List<ImeSubtypeListItem> mImeSubtypeList;
+        private final int[] mUsageHistoryOfSubtypeListItemIndex;
+
+        private DynamicRotationList(final List<ImeSubtypeListItem> imeSubtypeListItems) {
+            mImeSubtypeList = imeSubtypeListItems;
+            mUsageHistoryOfSubtypeListItemIndex = new int[mImeSubtypeList.size()];
+            final int N = mImeSubtypeList.size();
+            for (int i = 0; i < N; i++) {
+                mUsageHistoryOfSubtypeListItemIndex[i] = i;
+            }
+        }
+
+        /**
+         * Returns the index of the specified object in
+         * {@link #mUsageHistoryOfSubtypeListItemIndex}.
+         * <p>We call the index of {@link #mUsageHistoryOfSubtypeListItemIndex} as "Usage Rank"
+         * so as not to be confused with the index in {@link #mImeSubtypeList}.
+         * @return -1 when the specified item doesn't belong to {@link #mImeSubtypeList} actually.
+         */
+        private int getUsageRank(final InputMethodInfo imi, InputMethodSubtype subtype) {
+            final int currentSubtypeId = calculateSubtypeId(imi, subtype);
+            final int N = mUsageHistoryOfSubtypeListItemIndex.length;
+            for (int usageRank = 0; usageRank < N; usageRank++) {
+                final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[usageRank];
+                final ImeSubtypeListItem subtypeListItem =
+                        mImeSubtypeList.get(subtypeListItemIndex);
+                if (subtypeListItem.mImi.equals(imi) &&
+                        subtypeListItem.mSubtypeId == currentSubtypeId) {
+                    return usageRank;
+                }
+            }
+            // Not found in the known IME/Subtype list.
+            return -1;
+        }
+
+        public void onUserAction(InputMethodInfo imi, InputMethodSubtype subtype) {
+            final int currentUsageRank = getUsageRank(imi, subtype);
+            // Do nothing if currentUsageRank == -1 (not found), or currentUsageRank == 0
+            if (currentUsageRank <= 0) {
+                return;
+            }
+            final int currentItemIndex = mUsageHistoryOfSubtypeListItemIndex[currentUsageRank];
+            System.arraycopy(mUsageHistoryOfSubtypeListItemIndex, 0,
+                    mUsageHistoryOfSubtypeListItemIndex, 1, currentUsageRank);
+            mUsageHistoryOfSubtypeListItemIndex[0] = currentItemIndex;
+        }
+
+        /**
+         * Provides the basic operation to implement bi-directional IME rotation.
+         * @param onlyCurrentIme {@code true} to limit the search space to IME subtypes that belong
+         * to {@code imi}.
+         * @param imi {@link InputMethodInfo} that will be used in conjunction with {@code subtype}
+         * from which we find the adjacent IME subtype.
+         * @param subtype {@link InputMethodSubtype} that will be used in conjunction with
+         * {@code imi} from which we find the next IME subtype.  {@code null} if the input method
+         * does not have a subtype.
+         * @param forward {@code true} to do forward search the next IME subtype. Specify
+         * {@code false} to do backward search.
+         * @return The IME subtype found. {@code null} if no IME subtype is found.
+         */
+        @Nullable
+        public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
+                InputMethodInfo imi, @Nullable InputMethodSubtype subtype, boolean forward) {
+            int currentUsageRank = getUsageRank(imi, subtype);
+            if (currentUsageRank < 0) {
+                if (DEBUG) {
+                    Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype);
+                }
+                return null;
+            }
+            final int N = mUsageHistoryOfSubtypeListItemIndex.length;
+            for (int i = 1; i < N; i++) {
+                final int offset = forward ? i : N - i;
+                final int subtypeListItemRank = (currentUsageRank + offset) % N;
+                final int subtypeListItemIndex =
+                        mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank];
+                final ImeSubtypeListItem subtypeListItem =
+                        mImeSubtypeList.get(subtypeListItemIndex);
+                if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) {
+                    continue;
+                }
+                return subtypeListItem;
+            }
+            return null;
+        }
+
+        protected void dump(final Printer pw, final String prefix) {
+            for (int i = 0; i < mUsageHistoryOfSubtypeListItemIndex.length; ++i) {
+                final int rank = mUsageHistoryOfSubtypeListItemIndex[i];
+                final ImeSubtypeListItem item = mImeSubtypeList.get(i);
+                pw.println(prefix + "rank=" + rank + " item=" + item);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public static class ControllerImpl {
+        private final DynamicRotationList mSwitchingAwareRotationList;
+        private final StaticRotationList mSwitchingUnawareRotationList;
+
+        public static ControllerImpl createFrom(final ControllerImpl currentInstance,
+                final List<ImeSubtypeListItem> sortedEnabledItems) {
+            DynamicRotationList switchingAwareRotationList = null;
+            {
+                final List<ImeSubtypeListItem> switchingAwareImeSubtypes =
+                        filterImeSubtypeList(sortedEnabledItems,
+                                true /* supportsSwitchingToNextInputMethod */);
+                if (currentInstance != null &&
+                        currentInstance.mSwitchingAwareRotationList != null &&
+                        Objects.equals(currentInstance.mSwitchingAwareRotationList.mImeSubtypeList,
+                                switchingAwareImeSubtypes)) {
+                    // Can reuse the current instance.
+                    switchingAwareRotationList = currentInstance.mSwitchingAwareRotationList;
+                }
+                if (switchingAwareRotationList == null) {
+                    switchingAwareRotationList = new DynamicRotationList(switchingAwareImeSubtypes);
+                }
+            }
+
+            StaticRotationList switchingUnawareRotationList = null;
+            {
+                final List<ImeSubtypeListItem> switchingUnawareImeSubtypes = filterImeSubtypeList(
+                        sortedEnabledItems, false /* supportsSwitchingToNextInputMethod */);
+                if (currentInstance != null &&
+                        currentInstance.mSwitchingUnawareRotationList != null &&
+                        Objects.equals(
+                                currentInstance.mSwitchingUnawareRotationList.mImeSubtypeList,
+                                switchingUnawareImeSubtypes)) {
+                    // Can reuse the current instance.
+                    switchingUnawareRotationList = currentInstance.mSwitchingUnawareRotationList;
+                }
+                if (switchingUnawareRotationList == null) {
+                    switchingUnawareRotationList =
+                            new StaticRotationList(switchingUnawareImeSubtypes);
+                }
+            }
+
+            return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList);
+        }
+
+        private ControllerImpl(final DynamicRotationList switchingAwareRotationList,
+                final StaticRotationList switchingUnawareRotationList) {
+            mSwitchingAwareRotationList = switchingAwareRotationList;
+            mSwitchingUnawareRotationList = switchingUnawareRotationList;
+        }
+
+        /**
+         * Provides the basic operation to implement bi-directional IME rotation.
+         * @param onlyCurrentIme {@code true} to limit the search space to IME subtypes that belong
+         * to {@code imi}.
+         * @param imi {@link InputMethodInfo} that will be used in conjunction with {@code subtype}
+         * from which we find the adjacent IME subtype.
+         * @param subtype {@link InputMethodSubtype} that will be used in conjunction with
+         * {@code imi} from which we find the next IME subtype.  {@code null} if the input method
+         * does not have a subtype.
+         * @param forward {@code true} to do forward search the next IME subtype. Specify
+         * {@code false} to do backward search.
+         * @return The IME subtype found. {@code null} if no IME subtype is found.
+         */
+        @Nullable
+        public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi,
+                @Nullable InputMethodSubtype subtype, boolean forward) {
+            if (imi == null) {
+                return null;
+            }
+            if (imi.supportsSwitchingToNextInputMethod()) {
+                return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
+                        subtype, forward);
+            } else {
+                return mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
+                        subtype, forward);
+            }
+        }
+
+        public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
+            if (imi == null) {
+                return;
+            }
+            if (imi.supportsSwitchingToNextInputMethod()) {
+                mSwitchingAwareRotationList.onUserAction(imi, subtype);
+            }
+        }
+
+        private static List<ImeSubtypeListItem> filterImeSubtypeList(
+                final List<ImeSubtypeListItem> items,
+                final boolean supportsSwitchingToNextInputMethod) {
+            final ArrayList<ImeSubtypeListItem> result = new ArrayList<>();
+            final int ALL_ITEMS_COUNT = items.size();
+            for (int i = 0; i < ALL_ITEMS_COUNT; i++) {
+                final ImeSubtypeListItem item = items.get(i);
+                if (item.mImi.supportsSwitchingToNextInputMethod() ==
+                        supportsSwitchingToNextInputMethod) {
+                    result.add(item);
+                }
+            }
+            return result;
+        }
+
+        protected void dump(final Printer pw) {
+            pw.println("    mSwitchingAwareRotationList:");
+            mSwitchingAwareRotationList.dump(pw, "      ");
+            pw.println("    mSwitchingUnawareRotationList:");
+            mSwitchingUnawareRotationList.dump(pw, "      ");
+        }
+    }
+
+    private final InputMethodSettings mSettings;
+    private InputMethodAndSubtypeList mSubtypeList;
+    private ControllerImpl mController;
+
+    private InputMethodSubtypeSwitchingController(InputMethodSettings settings, Context context) {
+        mSettings = settings;
+        resetCircularListLocked(context);
+    }
+
+    public static InputMethodSubtypeSwitchingController createInstanceLocked(
+            InputMethodSettings settings, Context context) {
+        return new InputMethodSubtypeSwitchingController(settings, context);
+    }
+
+    public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
+        if (mController == null) {
+            if (DEBUG) {
+                Log.e(TAG, "mController shouldn't be null.");
+            }
+            return;
+        }
+        mController.onUserActionLocked(imi, subtype);
+    }
+
+    public void resetCircularListLocked(Context context) {
+        mSubtypeList = new InputMethodAndSubtypeList(context, mSettings);
+        mController = ControllerImpl.createFrom(mController,
+                mSubtypeList.getSortedInputMethodAndSubtypeList(
+                        false /* includeAuxiliarySubtypes */, false /* isScreenLocked */));
+    }
+
+    public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi,
+            InputMethodSubtype subtype, boolean forward) {
+        if (mController == null) {
+            if (DEBUG) {
+                Log.e(TAG, "mController shouldn't be null.");
+            }
+            return null;
+        }
+        return mController.getNextInputMethod(onlyCurrentIme, imi, subtype, forward);
+    }
+
+    public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(
+            boolean includingAuxiliarySubtypes, boolean isScreenLocked) {
+        return mSubtypeList.getSortedInputMethodAndSubtypeList(
+                includingAuxiliarySubtypes, isScreenLocked);
+    }
+
+    public void dump(final Printer pw) {
+        if (mController != null) {
+            mController.dump(pw);
+        } else {
+            pw.println("    mController=null");
+        }
+    }
+}
diff --git a/com/android/internal/inputmethod/InputMethodUtils.java b/com/android/internal/inputmethod/InputMethodUtils.java
new file mode 100644
index 0000000..3e231d0
--- /dev/null
+++ b/com/android/internal/inputmethod/InputMethodUtils.java
@@ -0,0 +1,1515 @@
+/*
+ * 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.internal.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppOpsManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.text.TextUtils.SimpleStringSplitter;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Printer;
+import android.util.Slog;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+import android.view.textservice.SpellCheckerInfo;
+import android.view.textservice.TextServicesManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * InputMethodManagerUtils contains some static methods that provides IME informations.
+ * This methods are supposed to be used in both the framework and the Settings application.
+ */
+public class InputMethodUtils {
+    public static final boolean DEBUG = false;
+    public static final int NOT_A_SUBTYPE_ID = -1;
+    public static final String SUBTYPE_MODE_ANY = null;
+    public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
+    public static final String SUBTYPE_MODE_VOICE = "voice";
+    private static final String TAG = "InputMethodUtils";
+    private static final Locale ENGLISH_LOCALE = new Locale("en");
+    private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
+    private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
+            "EnabledWhenDefaultIsNotAsciiCapable";
+    private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
+
+    // The string for enabled input method is saved as follows:
+    // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
+    private static final char INPUT_METHOD_SEPARATOR = ':';
+    private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
+    /**
+     * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
+     * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
+     * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
+     * doesn't automatically match {@code Locale("en", "IN")}.
+     */
+    private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
+        Locale.ENGLISH, // "en"
+        Locale.US, // "en_US"
+        Locale.UK, // "en_GB"
+    };
+
+    // A temporary workaround for the performance concerns in
+    // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
+    // TODO: Optimize all the critical paths including this one.
+    private static final Object sCacheLock = new Object();
+    @GuardedBy("sCacheLock")
+    private static LocaleList sCachedSystemLocales;
+    @GuardedBy("sCacheLock")
+    private static InputMethodInfo sCachedInputMethodInfo;
+    @GuardedBy("sCacheLock")
+    private static ArrayList<InputMethodSubtype> sCachedResult;
+
+    private InputMethodUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    // ----------------------------------------------------------------------
+    // Utilities for debug
+    public static String getApiCallStack() {
+        String apiCallStack = "";
+        try {
+            throw new RuntimeException();
+        } catch (RuntimeException e) {
+            final StackTraceElement[] frames = e.getStackTrace();
+            for (int j = 1; j < frames.length; ++j) {
+                final String tempCallStack = frames[j].toString();
+                if (TextUtils.isEmpty(apiCallStack)) {
+                    // Overwrite apiCallStack if it's empty
+                    apiCallStack = tempCallStack;
+                } else if (tempCallStack.indexOf("Transact(") < 0) {
+                    // Overwrite apiCallStack if it's not a binder call
+                    apiCallStack = tempCallStack;
+                } else {
+                    break;
+                }
+            }
+        }
+        return apiCallStack;
+    }
+    // ----------------------------------------------------------------------
+
+    public static boolean isSystemIme(InputMethodInfo inputMethod) {
+        return (inputMethod.getServiceInfo().applicationInfo.flags
+                & ApplicationInfo.FLAG_SYSTEM) != 0;
+    }
+
+    public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi,
+            final Context context, final boolean checkDefaultAttribute,
+            @Nullable final Locale requiredLocale, final boolean checkCountry,
+            final String requiredSubtypeMode) {
+        if (!isSystemIme(imi)) {
+            return false;
+        }
+        if (checkDefaultAttribute && !imi.isDefault(context)) {
+            return false;
+        }
+        if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Nullable
+    public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis,
+            final Context context) {
+        // At first, find the fallback locale from the IMEs that are declared as "default" in the
+        // current locale.  Note that IME developers can declare an IME as "default" only for
+        // some particular locales but "not default" for other locales.
+        for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
+            for (int i = 0; i < imis.size(); ++i) {
+                if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
+                        true /* checkDefaultAttribute */, fallbackLocale,
+                        true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
+                    return fallbackLocale;
+                }
+            }
+        }
+        // If no fallback locale is found in the above condition, find fallback locales regardless
+        // of the "default" attribute as a last resort.
+        for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
+            for (int i = 0; i < imis.size(); ++i) {
+                if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
+                        false /* checkDefaultAttribute */, fallbackLocale,
+                        true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
+                    return fallbackLocale;
+                }
+            }
+        }
+        Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
+        return null;
+    }
+
+    private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi,
+            final Context context, final boolean checkDefaultAttribute) {
+        if (!isSystemIme(imi)) {
+            return false;
+        }
+        if (checkDefaultAttribute && !imi.isDefault(context)) {
+            return false;
+        }
+        if (!imi.isAuxiliaryIme()) {
+            return false;
+        }
+        final int subtypeCount = imi.getSubtypeCount();
+        for (int i = 0; i < subtypeCount; ++i) {
+            final InputMethodSubtype s = imi.getSubtypeAt(i);
+            if (s.overridesImplicitlyEnabledSubtype()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static Locale getSystemLocaleFromContext(final Context context) {
+        try {
+            return context.getResources().getConfiguration().locale;
+        } catch (Resources.NotFoundException ex) {
+            return null;
+        }
+    }
+
+    private static final class InputMethodListBuilder {
+        // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
+        // order can have non-trivial effect in the call sites.
+        @NonNull
+        private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
+
+        public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis,
+                final Context context, final boolean checkDefaultAttribute,
+                @Nullable final Locale locale, final boolean checkCountry,
+                final String requiredSubtypeMode) {
+            for (int i = 0; i < imis.size(); ++i) {
+                final InputMethodInfo imi = imis.get(i);
+                if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
+                        checkCountry, requiredSubtypeMode)) {
+                    mInputMethodSet.add(imi);
+                }
+            }
+            return this;
+        }
+
+        // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
+        // documented more clearly.
+        public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis,
+                final Context context) {
+            // If one or more auxiliary input methods are available, OK to stop populating the list.
+            for (final InputMethodInfo imi : mInputMethodSet) {
+                if (imi.isAuxiliaryIme()) {
+                    return this;
+                }
+            }
+            boolean added = false;
+            for (int i = 0; i < imis.size(); ++i) {
+                final InputMethodInfo imi = imis.get(i);
+                if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
+                        true /* checkDefaultAttribute */)) {
+                    mInputMethodSet.add(imi);
+                    added = true;
+                }
+            }
+            if (added) {
+                return this;
+            }
+            for (int i = 0; i < imis.size(); ++i) {
+                final InputMethodInfo imi = imis.get(i);
+                if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
+                        false /* checkDefaultAttribute */)) {
+                    mInputMethodSet.add(imi);
+                }
+            }
+            return this;
+        }
+
+        public boolean isEmpty() {
+            return mInputMethodSet.isEmpty();
+        }
+
+        @NonNull
+        public ArrayList<InputMethodInfo> build() {
+            return new ArrayList<>(mInputMethodSet);
+        }
+    }
+
+    private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
+            final ArrayList<InputMethodInfo> imis, final Context context,
+            @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
+        // Once the system becomes ready, we pick up at least one keyboard in the following order.
+        // Secondary users fall into this category in general.
+        // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
+        // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
+        // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
+        // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
+        // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
+        // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
+        // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
+
+        final InputMethodListBuilder builder = new InputMethodListBuilder();
+        builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
+                true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
+        if (!builder.isEmpty()) {
+            return builder;
+        }
+        builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
+                false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
+        if (!builder.isEmpty()) {
+            return builder;
+        }
+        builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
+                true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
+        if (!builder.isEmpty()) {
+            return builder;
+        }
+        builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
+                false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
+        if (!builder.isEmpty()) {
+            return builder;
+        }
+        builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
+                true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
+        if (!builder.isEmpty()) {
+            return builder;
+        }
+        builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
+                false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
+        if (!builder.isEmpty()) {
+            return builder;
+        }
+        Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
+                + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
+        return builder;
+    }
+
+    public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
+            final ArrayList<InputMethodInfo> imis) {
+        final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
+        // We will primarily rely on the system locale, but also keep relying on the fallback locale
+        // as a last resort.
+        // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
+        // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
+        // subtype)
+        final Locale systemLocale = getSystemLocaleFromContext(context);
+        return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
+                .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
+                        true /* checkCountry */, SUBTYPE_MODE_ANY)
+                .fillAuxiliaryImes(imis, context)
+                .build();
+    }
+
+    public static Locale constructLocaleFromString(String localeStr) {
+        if (TextUtils.isEmpty(localeStr)) {
+            return null;
+        }
+        // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
+        String[] localeParams = localeStr.split("_", 3);
+        if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
+             // Convert a locale whose language is "tl" to one whose language is "fil".
+             // For example, "tl_PH" will get converted to "fil_PH".
+             // Versions of Android earlier than Lollipop did not support three letter language
+             // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
+             // On Lollipop and above, the current three letter version must be used.
+             localeParams[0] = "fil";
+        }
+        // The length of localeStr is guaranteed to always return a 1 <= value <= 3
+        // because localeStr is not empty.
+        if (localeParams.length == 1) {
+            return new Locale(localeParams[0]);
+        } else if (localeParams.length == 2) {
+            return new Locale(localeParams[0], localeParams[1]);
+        } else if (localeParams.length == 3) {
+            return new Locale(localeParams[0], localeParams[1], localeParams[2]);
+        }
+        return null;
+    }
+
+    public static boolean containsSubtypeOf(final InputMethodInfo imi,
+            @Nullable final Locale locale, final boolean checkCountry, final String mode) {
+        if (locale == null) {
+            return false;
+        }
+        final int N = imi.getSubtypeCount();
+        for (int i = 0; i < N; ++i) {
+            final InputMethodSubtype subtype = imi.getSubtypeAt(i);
+            if (checkCountry) {
+                final Locale subtypeLocale = subtype.getLocaleObject();
+                if (subtypeLocale == null ||
+                        !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
+                        !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
+                    continue;
+                }
+            } else {
+                final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
+                        subtype.getLocale()));
+                if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
+                    continue;
+                }
+            }
+            if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
+                    mode.equalsIgnoreCase(subtype.getMode())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
+        ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
+        final int subtypeCount = imi.getSubtypeCount();
+        for (int i = 0; i < subtypeCount; ++i) {
+            subtypes.add(imi.getSubtypeAt(i));
+        }
+        return subtypes;
+    }
+
+    public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
+            InputMethodInfo imi, String mode) {
+        ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
+        final int subtypeCount = imi.getSubtypeCount();
+        for (int i = 0; i < subtypeCount; ++i) {
+            final InputMethodSubtype subtype = imi.getSubtypeAt(i);
+            if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
+                subtypes.add(subtype);
+            }
+        }
+        return subtypes;
+    }
+
+    public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
+        if (enabledImes == null || enabledImes.isEmpty()) {
+            return null;
+        }
+        // We'd prefer to fall back on a system IME, since that is safer.
+        int i = enabledImes.size();
+        int firstFoundSystemIme = -1;
+        while (i > 0) {
+            i--;
+            final InputMethodInfo imi = enabledImes.get(i);
+            if (imi.isAuxiliaryIme()) {
+                continue;
+            }
+            if (InputMethodUtils.isSystemIme(imi)
+                    && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
+                            SUBTYPE_MODE_KEYBOARD)) {
+                return imi;
+            }
+            if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
+                firstFoundSystemIme = i;
+            }
+        }
+        return enabledImes.get(Math.max(firstFoundSystemIme, 0));
+    }
+
+    public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
+        return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
+    }
+
+    public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
+        if (imi != null) {
+            final int subtypeCount = imi.getSubtypeCount();
+            for (int i = 0; i < subtypeCount; ++i) {
+                InputMethodSubtype ims = imi.getSubtypeAt(i);
+                if (subtypeHashCode == ims.hashCode()) {
+                    return i;
+                }
+            }
+        }
+        return NOT_A_SUBTYPE_ID;
+    }
+
+    private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
+            new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
+                @Override
+                public Locale get(InputMethodSubtype source) {
+                    return source != null ? source.getLocaleObject() : null;
+                }
+            };
+
+    @VisibleForTesting
+    public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
+            Resources res, InputMethodInfo imi) {
+        final LocaleList systemLocales = res.getConfiguration().getLocales();
+
+        synchronized (sCacheLock) {
+            // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
+            // it does not check if subtypes are also identical.
+            if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
+                return new ArrayList<>(sCachedResult);
+            }
+        }
+
+        // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
+        // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
+        // LocaleList rather than Resource.
+        final ArrayList<InputMethodSubtype> result =
+                getImplicitlyApplicableSubtypesLockedImpl(res, imi);
+        synchronized (sCacheLock) {
+            // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
+            sCachedSystemLocales = systemLocales;
+            sCachedInputMethodInfo = imi;
+            sCachedResult = new ArrayList<>(result);
+        }
+        return result;
+    }
+
+    private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
+            Resources res, InputMethodInfo imi) {
+        final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
+        final LocaleList systemLocales = res.getConfiguration().getLocales();
+        final String systemLocale = systemLocales.get(0).toString();
+        if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
+        final int numSubtypes = subtypes.size();
+
+        // Handle overridesImplicitlyEnabledSubtype mechanism.
+        final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
+        for (int i = 0; i < numSubtypes; ++i) {
+            // scan overriding implicitly enabled subtypes.
+            final InputMethodSubtype subtype = subtypes.get(i);
+            if (subtype.overridesImplicitlyEnabledSubtype()) {
+                final String mode = subtype.getMode();
+                if (!applicableModeAndSubtypesMap.containsKey(mode)) {
+                    applicableModeAndSubtypesMap.put(mode, subtype);
+                }
+            }
+        }
+        if (applicableModeAndSubtypesMap.size() > 0) {
+            return new ArrayList<>(applicableModeAndSubtypesMap.values());
+        }
+
+        final HashMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
+                new HashMap<>();
+        final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
+
+        for (int i = 0; i < numSubtypes; ++i) {
+            final InputMethodSubtype subtype = subtypes.get(i);
+            final String mode = subtype.getMode();
+            if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
+                keyboardSubtypes.add(subtype);
+            } else {
+                if (!nonKeyboardSubtypesMap.containsKey(mode)) {
+                    nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
+                }
+                nonKeyboardSubtypesMap.get(mode).add(subtype);
+            }
+        }
+
+        final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
+        LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
+                applicableSubtypes);
+
+        if (!applicableSubtypes.isEmpty()) {
+            boolean hasAsciiCapableKeyboard = false;
+            final int numApplicationSubtypes = applicableSubtypes.size();
+            for (int i = 0; i < numApplicationSubtypes; ++i) {
+                final InputMethodSubtype subtype = applicableSubtypes.get(i);
+                if (subtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
+                    hasAsciiCapableKeyboard = true;
+                    break;
+                }
+            }
+            if (!hasAsciiCapableKeyboard) {
+                final int numKeyboardSubtypes = keyboardSubtypes.size();
+                for (int i = 0; i < numKeyboardSubtypes; ++i) {
+                    final InputMethodSubtype subtype = keyboardSubtypes.get(i);
+                    final String mode = subtype.getMode();
+                    if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
+                            TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
+                        applicableSubtypes.add(subtype);
+                    }
+                }
+            }
+        }
+
+        if (applicableSubtypes.isEmpty()) {
+            InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
+                    res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
+            if (lastResortKeyboardSubtype != null) {
+                applicableSubtypes.add(lastResortKeyboardSubtype);
+            }
+        }
+
+        // For each non-keyboard mode, extract subtypes with system locales.
+        for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
+            LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
+                    applicableSubtypes);
+        }
+
+        return applicableSubtypes;
+    }
+
+    /**
+     * Returns the language component of a given locale string.
+     * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
+     */
+    public static String getLanguageFromLocaleString(String locale) {
+        final int idx = locale.indexOf('_');
+        if (idx < 0) {
+            return locale;
+        } else {
+            return locale.substring(0, idx);
+        }
+    }
+
+    /**
+     * If there are no selected subtypes, tries finding the most applicable one according to the
+     * given locale.
+     * @param subtypes this function will search the most applicable subtype in subtypes
+     * @param mode subtypes will be filtered by mode
+     * @param locale subtypes will be filtered by locale
+     * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
+     * it will return the first subtype matched with mode
+     * @return the most applicable subtypeId
+     */
+    public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
+            Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
+            boolean canIgnoreLocaleAsLastResort) {
+        if (subtypes == null || subtypes.size() == 0) {
+            return null;
+        }
+        if (TextUtils.isEmpty(locale)) {
+            locale = res.getConfiguration().locale.toString();
+        }
+        final String language = getLanguageFromLocaleString(locale);
+        boolean partialMatchFound = false;
+        InputMethodSubtype applicableSubtype = null;
+        InputMethodSubtype firstMatchedModeSubtype = null;
+        final int N = subtypes.size();
+        for (int i = 0; i < N; ++i) {
+            InputMethodSubtype subtype = subtypes.get(i);
+            final String subtypeLocale = subtype.getLocale();
+            final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
+            // An applicable subtype should match "mode". If mode is null, mode will be ignored,
+            // and all subtypes with all modes can be candidates.
+            if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
+                if (firstMatchedModeSubtype == null) {
+                    firstMatchedModeSubtype = subtype;
+                }
+                if (locale.equals(subtypeLocale)) {
+                    // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
+                    applicableSubtype = subtype;
+                    break;
+                } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
+                    // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
+                    applicableSubtype = subtype;
+                    partialMatchFound = true;
+                }
+            }
+        }
+
+        if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
+            return firstMatchedModeSubtype;
+        }
+
+        // The first subtype applicable to the system locale will be defined as the most applicable
+        // subtype.
+        if (DEBUG) {
+            if (applicableSubtype != null) {
+                Slog.d(TAG, "Applicable InputMethodSubtype was found: "
+                        + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
+            }
+        }
+        return applicableSubtype;
+    }
+
+    public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
+        if (subtype == null) return true;
+        return !subtype.isAuxiliary();
+    }
+
+    public static void setNonSelectedSystemImesDisabledUntilUsed(
+            IPackageManager packageManager, List<InputMethodInfo> enabledImis,
+            int userId, String callingPackage) {
+        if (DEBUG) {
+            Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
+        }
+        final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
+                com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
+        if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
+            return;
+        }
+        // Only the current spell checker should be treated as an enabled one.
+        final SpellCheckerInfo currentSpellChecker =
+                TextServicesManager.getInstance().getCurrentSpellChecker();
+        for (final String packageName : systemImesDisabledUntilUsed) {
+            if (DEBUG) {
+                Slog.d(TAG, "check " + packageName);
+            }
+            boolean enabledIme = false;
+            for (int j = 0; j < enabledImis.size(); ++j) {
+                final InputMethodInfo imi = enabledImis.get(j);
+                if (packageName.equals(imi.getPackageName())) {
+                    enabledIme = true;
+                    break;
+                }
+            }
+            if (enabledIme) {
+                // enabled ime. skip
+                continue;
+            }
+            if (currentSpellChecker != null
+                    && packageName.equals(currentSpellChecker.getPackageName())) {
+                // enabled spell checker. skip
+                if (DEBUG) {
+                    Slog.d(TAG, packageName + " is the current spell checker. skip");
+                }
+                continue;
+            }
+            ApplicationInfo ai = null;
+            try {
+                ai = packageManager.getApplicationInfo(packageName,
+                        PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
+                        + " userId=" + userId, e);
+                continue;
+            }
+            if (ai == null) {
+                // No app found for packageName
+                continue;
+            }
+            final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+            if (!isSystemPackage) {
+                continue;
+            }
+            setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
+        }
+    }
+
+    private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
+            int userId, String callingPackage) {
+        final int state;
+        try {
+            state = packageManager.getApplicationEnabledSetting(packageName, userId);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
+                    + " userId=" + userId, e);
+            return;
+        }
+        if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+                || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
+            if (DEBUG) {
+                Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
+            }
+            try {
+                packageManager.setApplicationEnabledSetting(packageName,
+                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
+                        0 /* newState */, userId, callingPackage);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
+                        + " userId=" + userId + " callingPackage=" + callingPackage, e);
+                return;
+            }
+        } else {
+            if (DEBUG) {
+                Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
+            }
+        }
+    }
+
+    public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
+            InputMethodSubtype subtype) {
+        final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
+        return subtype != null
+                ? TextUtils.concat(subtype.getDisplayName(context,
+                        imi.getPackageName(), imi.getServiceInfo().applicationInfo),
+                                (TextUtils.isEmpty(imiLabel) ?
+                                        "" : " - " + imiLabel))
+                : imiLabel;
+    }
+
+    /**
+     * Returns true if a package name belongs to a UID.
+     *
+     * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
+     * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
+     * @param uid the UID to be validated.
+     * @param packageName the package name.
+     * @return {@code true} if the package name belongs to the UID.
+     */
+    public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
+            final int uid, final String packageName) {
+        try {
+            appOpsManager.checkPackage(uid, packageName);
+            return true;
+        } catch (SecurityException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Parses the setting stored input methods and subtypes string value.
+     *
+     * @param inputMethodsAndSubtypesString The input method subtypes value stored in settings.
+     * @return Map from input method ID to set of input method subtypes IDs.
+     */
+    @VisibleForTesting
+    public static ArrayMap<String, ArraySet<String>> parseInputMethodsAndSubtypesString(
+            @Nullable final String inputMethodsAndSubtypesString) {
+
+        final ArrayMap<String, ArraySet<String>> imeMap = new ArrayMap<>();
+        if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
+            return imeMap;
+        }
+
+        final SimpleStringSplitter typeSplitter =
+                new SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
+        final SimpleStringSplitter subtypeSplitter =
+                new SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
+
+        List<Pair<String, ArrayList<String>>> allImeSettings =
+                InputMethodSettings.buildInputMethodsAndSubtypeList(inputMethodsAndSubtypesString,
+                        typeSplitter,
+                        subtypeSplitter);
+        for (Pair<String, ArrayList<String>> ime : allImeSettings) {
+            ArraySet<String> subtypes = new ArraySet<>();
+            if (ime.second != null) {
+                subtypes.addAll(ime.second);
+            }
+            imeMap.put(ime.first, subtypes);
+        }
+        return imeMap;
+    }
+
+    @NonNull
+    public static String buildInputMethodsAndSubtypesString(
+            @NonNull final ArrayMap<String, ArraySet<String>> map) {
+        // we want to use the canonical InputMethodSettings implementation,
+        // so we convert data structures first.
+        List<Pair<String, ArrayList<String>>> imeMap = new ArrayList<>(4);
+        for (ArrayMap.Entry<String, ArraySet<String>> entry : map.entrySet()) {
+            final String imeName = entry.getKey();
+            final ArraySet<String> subtypeSet = entry.getValue();
+            final ArrayList<String> subtypes = new ArrayList<>(2);
+            if (subtypeSet != null) {
+                subtypes.addAll(subtypeSet);
+            }
+            imeMap.add(new Pair<>(imeName, subtypes));
+        }
+        return InputMethodSettings.buildInputMethodsSettingString(imeMap);
+    }
+
+    /**
+     * Utility class for putting and getting settings for InputMethod
+     * TODO: Move all putters and getters of settings to this class.
+     */
+    public static class InputMethodSettings {
+        private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
+
+        private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
+
+        private final Resources mRes;
+        private final ContentResolver mResolver;
+        private final HashMap<String, InputMethodInfo> mMethodMap;
+        private final ArrayList<InputMethodInfo> mMethodList;
+
+        /**
+         * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
+         */
+        private final HashMap<String, String> mCopyOnWriteDataStore = new HashMap<>();
+
+        private boolean mCopyOnWrite = false;
+        @NonNull
+        private String mEnabledInputMethodsStrCache = "";
+        @UserIdInt
+        private int mCurrentUserId;
+        private int[] mCurrentProfileIds = new int[0];
+
+        private static void buildEnabledInputMethodsSettingString(
+                StringBuilder builder, Pair<String, ArrayList<String>> ime) {
+            builder.append(ime.first);
+            // Inputmethod and subtypes are saved in the settings as follows:
+            // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
+            for (String subtypeId: ime.second) {
+                builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
+            }
+        }
+
+        public static String buildInputMethodsSettingString(
+                List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
+            final StringBuilder b = new StringBuilder();
+            boolean needsSeparator = false;
+            for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
+                if (needsSeparator) {
+                    b.append(INPUT_METHOD_SEPARATOR);
+                }
+                buildEnabledInputMethodsSettingString(b, ime);
+                needsSeparator = true;
+            }
+            return b.toString();
+        }
+
+        public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
+                String enabledInputMethodsStr,
+                TextUtils.SimpleStringSplitter inputMethodSplitter,
+                TextUtils.SimpleStringSplitter subtypeSplitter) {
+            ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
+            if (TextUtils.isEmpty(enabledInputMethodsStr)) {
+                return imsList;
+            }
+            inputMethodSplitter.setString(enabledInputMethodsStr);
+            while (inputMethodSplitter.hasNext()) {
+                String nextImsStr = inputMethodSplitter.next();
+                subtypeSplitter.setString(nextImsStr);
+                if (subtypeSplitter.hasNext()) {
+                    ArrayList<String> subtypeHashes = new ArrayList<>();
+                    // The first element is ime id.
+                    String imeId = subtypeSplitter.next();
+                    while (subtypeSplitter.hasNext()) {
+                        subtypeHashes.add(subtypeSplitter.next());
+                    }
+                    imsList.add(new Pair<>(imeId, subtypeHashes));
+                }
+            }
+            return imsList;
+        }
+
+        public InputMethodSettings(
+                Resources res, ContentResolver resolver,
+                HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
+                @UserIdInt int userId, boolean copyOnWrite) {
+            mRes = res;
+            mResolver = resolver;
+            mMethodMap = methodMap;
+            mMethodList = methodList;
+            switchCurrentUser(userId, copyOnWrite);
+        }
+
+        /**
+         * Must be called when the current user is changed.
+         *
+         * @param userId The user ID.
+         * @param copyOnWrite If {@code true}, for each settings key
+         * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
+         * settings on the {@link Settings.Secure} until we do the first write operation.
+         */
+        public void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
+            if (DEBUG) {
+                Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
+            }
+            if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
+                mCopyOnWriteDataStore.clear();
+                mEnabledInputMethodsStrCache = "";
+                // TODO: mCurrentProfileIds should be cleared here.
+            }
+            mCurrentUserId = userId;
+            mCopyOnWrite = copyOnWrite;
+            // TODO: mCurrentProfileIds should be updated here.
+        }
+
+        private void putString(@NonNull final String key, @Nullable final String str) {
+            if (mCopyOnWrite) {
+                mCopyOnWriteDataStore.put(key, str);
+            } else {
+                Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId);
+            }
+        }
+
+        @Nullable
+        private String getString(@NonNull final String key, @Nullable final String defaultValue) {
+            final String result;
+            if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
+                result = mCopyOnWriteDataStore.get(key);
+            } else {
+                result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
+            }
+            return result != null ? result : defaultValue;
+        }
+
+        private void putInt(final String key, final int value) {
+            if (mCopyOnWrite) {
+                mCopyOnWriteDataStore.put(key, String.valueOf(value));
+            } else {
+                Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId);
+            }
+        }
+
+        private int getInt(final String key, final int defaultValue) {
+            if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
+                final String result = mCopyOnWriteDataStore.get(key);
+                return result != null ? Integer.parseInt(result) : 0;
+            }
+            return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
+        }
+
+        private void putBoolean(final String key, final boolean value) {
+            putInt(key, value ? 1 : 0);
+        }
+
+        private boolean getBoolean(final String key, final boolean defaultValue) {
+            return getInt(key, defaultValue ? 1 : 0) == 1;
+        }
+
+        public void setCurrentProfileIds(int[] currentProfileIds) {
+            synchronized (this) {
+                mCurrentProfileIds = currentProfileIds;
+            }
+        }
+
+        public boolean isCurrentProfile(int userId) {
+            synchronized (this) {
+                if (userId == mCurrentUserId) return true;
+                for (int i = 0; i < mCurrentProfileIds.length; i++) {
+                    if (userId == mCurrentProfileIds[i]) return true;
+                }
+                return false;
+            }
+        }
+
+        public ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
+            return createEnabledInputMethodListLocked(
+                    getEnabledInputMethodsAndSubtypeListLocked());
+        }
+
+        public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
+                Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
+            List<InputMethodSubtype> enabledSubtypes =
+                    getEnabledInputMethodSubtypeListLocked(imi);
+            if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
+                enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
+                        context.getResources(), imi);
+            }
+            return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
+        }
+
+        public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
+                InputMethodInfo imi) {
+            List<Pair<String, ArrayList<String>>> imsList =
+                    getEnabledInputMethodsAndSubtypeListLocked();
+            ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
+            if (imi != null) {
+                for (Pair<String, ArrayList<String>> imsPair : imsList) {
+                    InputMethodInfo info = mMethodMap.get(imsPair.first);
+                    if (info != null && info.getId().equals(imi.getId())) {
+                        final int subtypeCount = info.getSubtypeCount();
+                        for (int i = 0; i < subtypeCount; ++i) {
+                            InputMethodSubtype ims = info.getSubtypeAt(i);
+                            for (String s: imsPair.second) {
+                                if (String.valueOf(ims.hashCode()).equals(s)) {
+                                    enabledSubtypes.add(ims);
+                                }
+                            }
+                        }
+                        break;
+                    }
+                }
+            }
+            return enabledSubtypes;
+        }
+
+        public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
+            return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
+                    mInputMethodSplitter,
+                    mSubtypeSplitter);
+        }
+
+        public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
+            if (reloadInputMethodStr) {
+                getEnabledInputMethodsStr();
+            }
+            if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
+                // Add in the newly enabled input method.
+                putEnabledInputMethodsStr(id);
+            } else {
+                putEnabledInputMethodsStr(
+                        mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
+            }
+        }
+
+        /**
+         * Build and put a string of EnabledInputMethods with removing specified Id.
+         * @return the specified id was removed or not.
+         */
+        public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+                StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
+            boolean isRemoved = false;
+            boolean needsAppendSeparator = false;
+            for (Pair<String, ArrayList<String>> ims: imsList) {
+                String curId = ims.first;
+                if (curId.equals(id)) {
+                    // We are disabling this input method, and it is
+                    // currently enabled.  Skip it to remove from the
+                    // new list.
+                    isRemoved = true;
+                } else {
+                    if (needsAppendSeparator) {
+                        builder.append(INPUT_METHOD_SEPARATOR);
+                    } else {
+                        needsAppendSeparator = true;
+                    }
+                    buildEnabledInputMethodsSettingString(builder, ims);
+                }
+            }
+            if (isRemoved) {
+                // Update the setting with the new list of input methods.
+                putEnabledInputMethodsStr(builder.toString());
+            }
+            return isRemoved;
+        }
+
+        private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
+                List<Pair<String, ArrayList<String>>> imsList) {
+            final ArrayList<InputMethodInfo> res = new ArrayList<>();
+            for (Pair<String, ArrayList<String>> ims: imsList) {
+                InputMethodInfo info = mMethodMap.get(ims.first);
+                if (info != null) {
+                    res.add(info);
+                }
+            }
+            return res;
+        }
+
+        private void putEnabledInputMethodsStr(@Nullable String str) {
+            if (DEBUG) {
+                Slog.d(TAG, "putEnabledInputMethodStr: " + str);
+            }
+            if (TextUtils.isEmpty(str)) {
+                // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
+                // empty data scenario.
+                putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
+            } else {
+                putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
+            }
+            // TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
+            mEnabledInputMethodsStrCache = (str != null ? str : "");
+        }
+
+        @NonNull
+        public String getEnabledInputMethodsStr() {
+            mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
+            if (DEBUG) {
+                Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
+                        + ", " + mCurrentUserId);
+            }
+            return mEnabledInputMethodsStrCache;
+        }
+
+        private void saveSubtypeHistory(
+                List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
+            StringBuilder builder = new StringBuilder();
+            boolean isImeAdded = false;
+            if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
+                builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
+                        newSubtypeId);
+                isImeAdded = true;
+            }
+            for (Pair<String, String> ime: savedImes) {
+                String imeId = ime.first;
+                String subtypeId = ime.second;
+                if (TextUtils.isEmpty(subtypeId)) {
+                    subtypeId = NOT_A_SUBTYPE_ID_STR;
+                }
+                if (isImeAdded) {
+                    builder.append(INPUT_METHOD_SEPARATOR);
+                } else {
+                    isImeAdded = true;
+                }
+                builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
+                        subtypeId);
+            }
+            // Remove the last INPUT_METHOD_SEPARATOR
+            putSubtypeHistoryStr(builder.toString());
+        }
+
+        private void addSubtypeToHistory(String imeId, String subtypeId) {
+            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+            for (Pair<String, String> ime: subtypeHistory) {
+                if (ime.first.equals(imeId)) {
+                    if (DEBUG) {
+                        Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
+                                + ime.second);
+                    }
+                    // We should break here
+                    subtypeHistory.remove(ime);
+                    break;
+                }
+            }
+            if (DEBUG) {
+                Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
+            }
+            saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
+        }
+
+        private void putSubtypeHistoryStr(@NonNull String str) {
+            if (DEBUG) {
+                Slog.d(TAG, "putSubtypeHistoryStr: " + str);
+            }
+            if (TextUtils.isEmpty(str)) {
+                // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
+                // data scenario.
+                putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
+            } else {
+                putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
+            }
+        }
+
+        public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
+            // Gets the first one from the history
+            return getLastSubtypeForInputMethodLockedInternal(null);
+        }
+
+        public String getLastSubtypeForInputMethodLocked(String imeId) {
+            Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
+            if (ime != null) {
+                return ime.second;
+            } else {
+                return null;
+            }
+        }
+
+        private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
+            List<Pair<String, ArrayList<String>>> enabledImes =
+                    getEnabledInputMethodsAndSubtypeListLocked();
+            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+            for (Pair<String, String> imeAndSubtype : subtypeHistory) {
+                final String imeInTheHistory = imeAndSubtype.first;
+                // If imeId is empty, returns the first IME and subtype in the history
+                if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
+                    final String subtypeInTheHistory = imeAndSubtype.second;
+                    final String subtypeHashCode =
+                            getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
+                                    enabledImes, imeInTheHistory, subtypeInTheHistory);
+                    if (!TextUtils.isEmpty(subtypeHashCode)) {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
+                        }
+                        return new Pair<>(imeInTheHistory, subtypeHashCode);
+                    }
+                }
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "No enabled IME found in the history");
+            }
+            return null;
+        }
+
+        private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
+                ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
+            for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
+                if (enabledIme.first.equals(imeId)) {
+                    final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
+                    final InputMethodInfo imi = mMethodMap.get(imeId);
+                    if (explicitlyEnabledSubtypes.size() == 0) {
+                        // If there are no explicitly enabled subtypes, applicable subtypes are
+                        // enabled implicitly.
+                        // If IME is enabled and no subtypes are enabled, applicable subtypes
+                        // are enabled implicitly, so needs to treat them to be enabled.
+                        if (imi != null && imi.getSubtypeCount() > 0) {
+                            List<InputMethodSubtype> implicitlySelectedSubtypes =
+                                    getImplicitlyApplicableSubtypesLocked(mRes, imi);
+                            if (implicitlySelectedSubtypes != null) {
+                                final int N = implicitlySelectedSubtypes.size();
+                                for (int i = 0; i < N; ++i) {
+                                    final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
+                                    if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
+                                        return subtypeHashCode;
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        for (String s: explicitlyEnabledSubtypes) {
+                            if (s.equals(subtypeHashCode)) {
+                                // If both imeId and subtypeId are enabled, return subtypeId.
+                                try {
+                                    final int hashCode = Integer.parseInt(subtypeHashCode);
+                                    // Check whether the subtype id is valid or not
+                                    if (isValidSubtypeId(imi, hashCode)) {
+                                        return s;
+                                    } else {
+                                        return NOT_A_SUBTYPE_ID_STR;
+                                    }
+                                } catch (NumberFormatException e) {
+                                    return NOT_A_SUBTYPE_ID_STR;
+                                }
+                            }
+                        }
+                    }
+                    // If imeId was enabled but subtypeId was disabled.
+                    return NOT_A_SUBTYPE_ID_STR;
+                }
+            }
+            // If both imeId and subtypeId are disabled, return null
+            return null;
+        }
+
+        private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
+            ArrayList<Pair<String, String>> imsList = new ArrayList<>();
+            final String subtypeHistoryStr = getSubtypeHistoryStr();
+            if (TextUtils.isEmpty(subtypeHistoryStr)) {
+                return imsList;
+            }
+            mInputMethodSplitter.setString(subtypeHistoryStr);
+            while (mInputMethodSplitter.hasNext()) {
+                String nextImsStr = mInputMethodSplitter.next();
+                mSubtypeSplitter.setString(nextImsStr);
+                if (mSubtypeSplitter.hasNext()) {
+                    String subtypeId = NOT_A_SUBTYPE_ID_STR;
+                    // The first element is ime id.
+                    String imeId = mSubtypeSplitter.next();
+                    while (mSubtypeSplitter.hasNext()) {
+                        subtypeId = mSubtypeSplitter.next();
+                        break;
+                    }
+                    imsList.add(new Pair<>(imeId, subtypeId));
+                }
+            }
+            return imsList;
+        }
+
+        @NonNull
+        private String getSubtypeHistoryStr() {
+            final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
+            if (DEBUG) {
+                Slog.d(TAG, "getSubtypeHistoryStr: " + history);
+            }
+            return history;
+        }
+
+        public void putSelectedInputMethod(String imeId) {
+            if (DEBUG) {
+                Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
+                        + mCurrentUserId);
+            }
+            putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
+        }
+
+        public void putSelectedSubtype(int subtypeId) {
+            if (DEBUG) {
+                Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
+                        + mCurrentUserId);
+            }
+            putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
+        }
+
+        @Nullable
+        public String getSelectedInputMethod() {
+            final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
+            if (DEBUG) {
+                Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
+            }
+            return imi;
+        }
+
+        public boolean isSubtypeSelected() {
+            return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
+        }
+
+        private int getSelectedInputMethodSubtypeHashCode() {
+            return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
+        }
+
+        public boolean isShowImeWithHardKeyboardEnabled() {
+            return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
+        }
+
+        public void setShowImeWithHardKeyboard(boolean show) {
+            putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
+        }
+
+        @UserIdInt
+        public int getCurrentUserId() {
+            return mCurrentUserId;
+        }
+
+        public int getSelectedInputMethodSubtypeId(String selectedImiId) {
+            final InputMethodInfo imi = mMethodMap.get(selectedImiId);
+            if (imi == null) {
+                return NOT_A_SUBTYPE_ID;
+            }
+            final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
+            return getSubtypeIdFromHashCode(imi, subtypeHashCode);
+        }
+
+        public void saveCurrentInputMethodAndSubtypeToHistory(
+                String curMethodId, InputMethodSubtype currentSubtype) {
+            String subtypeId = NOT_A_SUBTYPE_ID_STR;
+            if (currentSubtype != null) {
+                subtypeId = String.valueOf(currentSubtype.hashCode());
+            }
+            if (canAddToLastInputMethod(currentSubtype)) {
+                addSubtypeToHistory(curMethodId, subtypeId);
+            }
+        }
+
+        public HashMap<InputMethodInfo, List<InputMethodSubtype>>
+                getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
+            HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
+                    new HashMap<>();
+            for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
+                enabledInputMethodAndSubtypes.put(
+                        imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
+            }
+            return enabledInputMethodAndSubtypes;
+        }
+
+        public void dumpLocked(final Printer pw, final String prefix) {
+            pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
+            pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
+            pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
+            pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
+        }
+    }
+
+    // For spell checker service manager.
+    // TODO: Should we have TextServicesUtils.java?
+    private static final Locale LOCALE_EN_US = new Locale("en", "US");
+    private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
+
+    /**
+     * Returns a list of {@link Locale} in the order of appropriateness for the default spell
+     * checker service.
+     *
+     * <p>If the system language is English, and the region is also explicitly specified in the
+     * system locale, the following fallback order will be applied.</p>
+     * <ul>
+     * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
+     * <li>(system-locale-language, system-locale-region)</li>
+     * <li>("en", "US")</li>
+     * <li>("en", "GB")</li>
+     * <li>("en")</li>
+     * </ul>
+     *
+     * <p>If the system language is English, but no region is specified in the system locale,
+     * the following fallback order will be applied.</p>
+     * <ul>
+     * <li>("en")</li>
+     * <li>("en", "US")</li>
+     * <li>("en", "GB")</li>
+     * </ul>
+     *
+     * <p>If the system language is not English, the following fallback order will be applied.</p>
+     * <ul>
+     * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
+     * <li>(system-locale-language, system-locale-region) (if exists)</li>
+     * <li>(system-locale-language) (if exists)</li>
+     * <li>("en", "US")</li>
+     * <li>("en", "GB")</li>
+     * <li>("en")</li>
+     * </ul>
+     *
+     * @param systemLocale the current system locale to be taken into consideration.
+     * @return a list of {@link Locale}. The first one is considered to be most appropriate.
+     */
+    @VisibleForTesting
+    public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
+            @Nullable final Locale systemLocale) {
+        final Locale systemLocaleLanguageCountryVariant;
+        final Locale systemLocaleLanguageCountry;
+        final Locale systemLocaleLanguage;
+        if (systemLocale != null) {
+            final String language = systemLocale.getLanguage();
+            final boolean hasLanguage = !TextUtils.isEmpty(language);
+            final String country = systemLocale.getCountry();
+            final boolean hasCountry = !TextUtils.isEmpty(country);
+            final String variant = systemLocale.getVariant();
+            final boolean hasVariant = !TextUtils.isEmpty(variant);
+            if (hasLanguage && hasCountry && hasVariant) {
+                systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
+            } else {
+                systemLocaleLanguageCountryVariant = null;
+            }
+            if (hasLanguage && hasCountry) {
+                systemLocaleLanguageCountry = new Locale(language, country);
+            } else {
+                systemLocaleLanguageCountry = null;
+            }
+            if (hasLanguage) {
+                systemLocaleLanguage = new Locale(language);
+            } else {
+                systemLocaleLanguage = null;
+            }
+        } else {
+            systemLocaleLanguageCountryVariant = null;
+            systemLocaleLanguageCountry = null;
+            systemLocaleLanguage = null;
+        }
+
+        final ArrayList<Locale> locales = new ArrayList<>();
+        if (systemLocaleLanguageCountryVariant != null) {
+            locales.add(systemLocaleLanguageCountryVariant);
+        }
+
+        if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
+            if (systemLocaleLanguageCountry != null) {
+                // If the system language is English, and the region is also explicitly specified,
+                // following fallback order will be applied.
+                // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
+                // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
+                // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
+                // - en
+                if (systemLocaleLanguageCountry != null) {
+                    locales.add(systemLocaleLanguageCountry);
+                }
+                if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
+                    locales.add(LOCALE_EN_US);
+                }
+                if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
+                    locales.add(LOCALE_EN_GB);
+                }
+                locales.add(Locale.ENGLISH);
+            } else {
+                // If the system language is English, but no region is specified, following
+                // fallback order will be applied.
+                // - en
+                // - en_US
+                // - en_GB
+                locales.add(Locale.ENGLISH);
+                locales.add(LOCALE_EN_US);
+                locales.add(LOCALE_EN_GB);
+            }
+        } else {
+            // If the system language is not English, the fallback order will be
+            // - systemLocaleLanguageCountry  [if non-null]
+            // - systemLocaleLanguage  [if non-null]
+            // - en_US
+            // - en_GB
+            // - en
+            if (systemLocaleLanguageCountry != null) {
+                locales.add(systemLocaleLanguageCountry);
+            }
+            if (systemLocaleLanguage != null) {
+                locales.add(systemLocaleLanguage);
+            }
+            locales.add(LOCALE_EN_US);
+            locales.add(LOCALE_EN_GB);
+            locales.add(Locale.ENGLISH);
+        }
+        return locales;
+    }
+}
diff --git a/com/android/internal/inputmethod/LocaleUtils.java b/com/android/internal/inputmethod/LocaleUtils.java
new file mode 100644
index 0000000..eeb3854
--- /dev/null
+++ b/com/android/internal/inputmethod/LocaleUtils.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2016 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.internal.inputmethod;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.icu.util.ULocale;
+import android.os.LocaleList;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+public final class LocaleUtils {
+
+    @VisibleForTesting
+    public interface LocaleExtractor<T> {
+        @Nullable
+        Locale get(@Nullable T source);
+    }
+
+    /**
+     * Calculates a matching score for the single desired locale.
+     *
+     * @see LocaleUtils#filterByLanguage(List, LocaleExtractor, LocaleList, ArrayList)
+     *
+     * @param supported The locale supported by IME subtype.
+     * @param desired The locale preferred by user.
+     * @return A score based on the locale matching for the default subtype enabling.
+     */
+    @IntRange(from=1, to=3)
+    private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
+            @NonNull final ULocale desired) {
+        // Assuming supported/desired is fully expanded.
+        if (supported.equals(desired)) {
+            return 3;  // Exact match.
+        }
+
+        // Skip language matching since it was already done in calculateMatchingScore.
+
+        final String supportedScript = supported.getScript();
+        if (supportedScript.isEmpty() || !supportedScript.equals(desired.getScript())) {
+            // TODO: Need subscript matching. For example, Hanb should match with Bopo.
+            return 1;
+        }
+
+        final String supportedCountry = supported.getCountry();
+        if (supportedCountry.isEmpty() || !supportedCountry.equals(desired.getCountry())) {
+            return 2;
+        }
+
+        // Ignore others e.g. variants, extensions.
+        return 3;
+    }
+
+    private static final class ScoreEntry implements Comparable<ScoreEntry> {
+        public int mIndex = -1;
+        @NonNull public final byte[] mScore;  // matching score of the i-th system languages.
+
+        ScoreEntry(@NonNull byte[] score, int index) {
+            mScore = new byte[score.length];
+            set(score, index);
+        }
+
+        private void set(@NonNull byte[] score, int index) {
+            for (int i = 0; i < mScore.length; ++i) {
+                mScore[i] = score[i];
+            }
+            mIndex = index;
+        }
+
+        /**
+         * Update score and index if the given score is better than this.
+         */
+        public void updateIfBetter(@NonNull byte[] score, int index) {
+            if (compare(mScore, score) == -1) {  // mScore < score
+                set(score, index);
+            }
+        }
+
+        /**
+         * Provides comaprison for bytes[].
+         *
+         * <p> Comparison does as follows. If the first value of {@code left} is larger than the
+         * first value of {@code right}, {@code left} is large than {@code right}.  If the first
+         * value of {@code left} is less than the first value of {@code right}, {@code left} is less
+         * than {@code right}. If the first value of {@code left} and the first value of
+         * {@code right} is equal, do the same comparison to the next value. Finally if all values
+         * in {@code left} and {@code right} are equal, {@code left} and {@code right} is equal.</p>
+         *
+         * @param left The length must be equal to {@code right}.
+         * @param right The length must be equal to {@code left}.
+         * @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
+         * {@code right}. 0 if {@code left} and {@code right} is equal.
+         */
+        @IntRange(from=-1, to=1)
+        private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
+            for (int i = 0; i < left.length; ++i) {
+                if (left[i] > right[i]) {
+                    return 1;
+                } else if (left[i] < right[i]) {
+                    return -1;
+                }
+            }
+            return 0;
+        }
+
+        @Override
+        public int compareTo(final ScoreEntry other) {
+            return -1 * compare(mScore, other.mScore);  // Order by descending order.
+        }
+    }
+
+    /**
+     * Filters the given items based on language preferences.
+     *
+     * <p>For each language found in {@code preferredLocales}, this method tries to copy at most
+     * one best-match item from {@code source} to {@code dest}.  For example, if
+     * {@code "en-GB", "ja", "en-AU", "fr-CA", "en-IN"} is specified to {@code preferredLocales},
+     * this method tries to copy at most one English locale, at most one Japanese, and at most one
+     * French locale from {@code source} to {@code dest}.  Here the best matching English locale
+     * will be searched from {@code source} based on matching score. For the score design, see
+     * {@link LocaleUtils#calculateMatchingSubScore(ULocale, ULocale)}</p>
+     *
+     * @param sources Source items to be filtered.
+     * @param extractor Type converter from the source items to {@link Locale} object.
+     * @param preferredLocales Ordered list of locales with which the input items will be
+     * filtered.
+     * @param dest Destination into which the filtered items will be added.
+     * @param <T> Type of the data items.
+     */
+    @VisibleForTesting
+    public static <T> void filterByLanguage(
+            @NonNull List<T> sources,
+            @NonNull LocaleExtractor<T> extractor,
+            @NonNull LocaleList preferredLocales,
+            @NonNull ArrayList<T> dest) {
+        if (preferredLocales.isEmpty()) {
+            return;
+        }
+
+        final int numPreferredLocales = preferredLocales.size();
+        final HashMap<String, ScoreEntry> scoreboard = new HashMap<>();
+        final byte[] score = new byte[numPreferredLocales];
+        final ULocale[] preferredULocaleCache = new ULocale[numPreferredLocales];
+
+        final int sourceSize = sources.size();
+        for (int i = 0; i < sourceSize; ++i) {
+            final Locale locale = extractor.get(sources.get(i));
+            if (locale == null) {
+                continue;
+            }
+
+            boolean canSkip = true;
+            for (int j = 0; j < numPreferredLocales; ++j) {
+                final Locale preferredLocale = preferredLocales.get(j);
+                if (!TextUtils.equals(locale.getLanguage(), preferredLocale.getLanguage())) {
+                    score[j] = 0;
+                    continue;
+                }
+                if (preferredULocaleCache[j] == null) {
+                    preferredULocaleCache[j] = ULocale.addLikelySubtags(
+                            ULocale.forLocale(preferredLocale));
+                }
+                score[j] = calculateMatchingSubScore(
+                        preferredULocaleCache[j],
+                        ULocale.addLikelySubtags(ULocale.forLocale(locale)));
+                if (canSkip && score[j] != 0) {
+                    canSkip = false;
+                }
+            }
+            if (canSkip) {
+                continue;
+            }
+
+            final String lang = locale.getLanguage();
+            final ScoreEntry bestScore = scoreboard.get(lang);
+            if (bestScore == null) {
+                scoreboard.put(lang, new ScoreEntry(score, i));
+            } else {
+                bestScore.updateIfBetter(score, i);
+            }
+        }
+
+        final ScoreEntry[] result = scoreboard.values().toArray(new ScoreEntry[scoreboard.size()]);
+        Arrays.sort(result);
+        for (final ScoreEntry entry : result) {
+            dest.add(sources.get(entry.mIndex));
+        }
+    }
+}
diff --git a/com/android/internal/location/GpsNetInitiatedHandler.java b/com/android/internal/location/GpsNetInitiatedHandler.java
new file mode 100644
index 0000000..9bd5994
--- /dev/null
+++ b/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -0,0 +1,601 @@
+/*
+ * Copyright (C) 2008 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.internal.location;
+
+import java.io.UnsupportedEncodingException;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.LocationManager;
+import android.location.INetInitiatedListener;
+import android.telephony.TelephonyManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.PhoneStateListener;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.R;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.TelephonyProperties;
+
+/**
+ * A GPS Network-initiated Handler class used by LocationManager.
+ *
+ * {@hide}
+ */
+public class GpsNetInitiatedHandler {
+
+    private static final String TAG = "GpsNetInitiatedHandler";
+
+    private static final boolean DEBUG = true;
+    private static final boolean VERBOSE = false;
+
+    // NI verify activity for bringing up UI (not used yet)
+    public static final String ACTION_NI_VERIFY = "android.intent.action.NETWORK_INITIATED_VERIFY";
+
+    // string constants for defining data fields in NI Intent
+    public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id";
+    public static final String NI_INTENT_KEY_TITLE = "title";
+    public static final String NI_INTENT_KEY_MESSAGE = "message";
+    public static final String NI_INTENT_KEY_TIMEOUT = "timeout";
+    public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp";
+
+    // the extra command to send NI response to GnssLocationProvider
+    public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response";
+
+    // the extra command parameter names in the Bundle
+    public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id";
+    public static final String NI_EXTRA_CMD_RESPONSE = "response";
+
+    // these need to match GpsNiType constants in gps_ni.h
+    public static final int GPS_NI_TYPE_VOICE = 1;
+    public static final int GPS_NI_TYPE_UMTS_SUPL = 2;
+    public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3;
+    public static final int GPS_NI_TYPE_EMERGENCY_SUPL = 4;
+
+    // these need to match GpsUserResponseType constants in gps_ni.h
+    public static final int GPS_NI_RESPONSE_ACCEPT = 1;
+    public static final int GPS_NI_RESPONSE_DENY = 2;
+    public static final int GPS_NI_RESPONSE_NORESP = 3;
+    public static final int GPS_NI_RESPONSE_IGNORE = 4;
+
+    // these need to match GpsNiNotifyFlags constants in gps_ni.h
+    public static final int GPS_NI_NEED_NOTIFY = 0x0001;
+    public static final int GPS_NI_NEED_VERIFY = 0x0002;
+    public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004;
+
+    // these need to match GpsNiEncodingType in gps_ni.h
+    public static final int GPS_ENC_NONE = 0;
+    public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1;
+    public static final int GPS_ENC_SUPL_UTF8 = 2;
+    public static final int GPS_ENC_SUPL_UCS2 = 3;
+    public static final int GPS_ENC_UNKNOWN = -1;
+
+    private final Context mContext;
+    private final TelephonyManager mTelephonyManager;
+    private final PhoneStateListener mPhoneStateListener;
+
+    // parent gps location provider
+    private final LocationManager mLocationManager;
+
+    // configuration of notificaiton behavior
+    private boolean mPlaySounds = false;
+    private boolean mPopupImmediately = true;
+
+    // read the SUPL_ES form gps.conf
+    private volatile boolean mIsSuplEsEnabled;
+
+    // Set to true if the phone is having emergency call.
+    private volatile boolean mIsInEmergency;
+
+    // If Location function is enabled.
+    private volatile boolean mIsLocationEnabled = false;
+
+    private final INetInitiatedListener mNetInitiatedListener;
+
+    // Set to true if string from HAL is encoded as Hex, e.g., "3F0039"
+    static private boolean mIsHexInput = true;
+
+    public static class GpsNiNotification
+    {
+        public int notificationId;
+        public int niType;
+        public boolean needNotify;
+        public boolean needVerify;
+        public boolean privacyOverride;
+        public int timeout;
+        public int defaultResponse;
+        public String requestorId;
+        public String text;
+        public int requestorIdEncoding;
+        public int textEncoding;
+    };
+
+    public static class GpsNiResponse {
+        /* User response, one of the values in GpsUserResponseType */
+        int userResponse;
+    };
+
+    private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() {
+
+        @Override public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
+                String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
+                /*
+                   Emergency Mode is when during emergency call or in emergency call back mode.
+                   For checking if it is during emergency call:
+                       mIsInEmergency records if the phone is in emergency call or not. It will
+                       be set to true when the phone is having emergency call, and then will
+                       be set to false by mPhoneStateListener when the emergency call ends.
+                   For checking if it is in emergency call back mode:
+                       Emergency call back mode will be checked by reading system properties
+                       when necessary: SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE)
+                */
+                setInEmergency(PhoneNumberUtils.isEmergencyNumber(phoneNumber));
+                if (DEBUG) Log.v(TAG, "ACTION_NEW_OUTGOING_CALL - " + getInEmergency());
+            } else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) {
+                updateLocationMode();
+                if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled());
+            }
+        }
+    };
+
+    /**
+     * The notification that is shown when a network-initiated notification
+     * (and verification) event is received.
+     * <p>
+     * This is lazily created, so use {@link #setNINotification()}.
+     */
+    private Notification.Builder mNiNotificationBuilder;
+
+    public GpsNetInitiatedHandler(Context context,
+                                  INetInitiatedListener netInitiatedListener,
+                                  boolean isSuplEsEnabled) {
+        mContext = context;
+
+        if (netInitiatedListener == null) {
+            throw new IllegalArgumentException("netInitiatedListener is null");
+        } else {
+            mNetInitiatedListener = netInitiatedListener;
+        }
+
+        setSuplEsEnabled(isSuplEsEnabled);
+        mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
+        updateLocationMode();
+        mTelephonyManager =
+            (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
+
+        mPhoneStateListener = new PhoneStateListener() {
+            @Override
+            public void onCallStateChanged(int state, String incomingNumber) {
+                if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is "+ state);
+                // listening for emergency call ends
+                if (state == TelephonyManager.CALL_STATE_IDLE) {
+                    setInEmergency(false);
+                }
+            }
+        };
+        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
+        intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
+        mContext.registerReceiver(mBroadcastReciever, intentFilter);
+    }
+
+    public void setSuplEsEnabled(boolean isEnabled) {
+        mIsSuplEsEnabled = isEnabled;
+    }
+
+    public boolean getSuplEsEnabled() {
+        return mIsSuplEsEnabled;
+    }
+
+    /**
+     * Updates Location enabler based on location setting.
+     */
+    public void updateLocationMode() {
+        mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+    }
+
+    /**
+     * Checks if user agreed to use location.
+     */
+    public boolean getLocationEnabled() {
+        return mIsLocationEnabled;
+    }
+
+    // Note: Currently, there are two mechanisms involved to determine if a
+    // phone is in emergency mode:
+    // 1. If the user is making an emergency call, this is provided by activly
+    //    monitoring the outgoing phone number;
+    // 2. If the device is in a emergency callback state, this is provided by
+    //    system properties.
+    // If either one of above exists, the phone is considered in an emergency
+    // mode. Because of this complexity, we need to be careful about how to set
+    // and clear the emergency state.
+    public void setInEmergency(boolean isInEmergency) {
+        mIsInEmergency = isInEmergency;
+    }
+
+    public boolean getInEmergency() {
+        boolean isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+        return mIsInEmergency || isInEmergencyCallback;
+    }
+
+
+    // Handles NI events from HAL
+    public void handleNiNotification(GpsNiNotification notif) {
+        if (DEBUG) Log.d(TAG, "in handleNiNotification () :"
+                        + " notificationId: " + notif.notificationId
+                        + " requestorId: " + notif.requestorId
+                        + " text: " + notif.text
+                        + " mIsSuplEsEnabled" + getSuplEsEnabled()
+                        + " mIsLocationEnabled" + getLocationEnabled());
+
+        if (getSuplEsEnabled()) {
+            handleNiInEs(notif);
+        } else {
+            handleNi(notif);
+        }
+
+        //////////////////////////////////////////////////////////////////////////
+        //   A note about timeout
+        //   According to the protocol, in the need_notify and need_verify case,
+        //   a default response should be sent when time out.
+        //
+        //   In some GPS hardware, the GPS driver (under HAL) can handle the timeout case
+        //   and this class GpsNetInitiatedHandler does not need to do anything.
+        //
+        //   However, the UI should at least close the dialog when timeout. Further,
+        //   for more general handling, timeout response should be added to the Handler here.
+        //
+    }
+
+    // handle NI form HAL when SUPL_ES is disabled.
+    private void handleNi(GpsNiNotification notif) {
+        if (DEBUG) Log.d(TAG, "in handleNi () :"
+                        + " needNotify: " + notif.needNotify
+                        + " needVerify: " + notif.needVerify
+                        + " privacyOverride: " + notif.privacyOverride
+                        + " mPopupImmediately: " + mPopupImmediately
+                        + " mInEmergency: " + getInEmergency());
+
+        if (!getLocationEnabled() && !getInEmergency()) {
+            // Location is currently disabled, ignore all NI requests.
+            try {
+                mNetInitiatedListener.sendNiResponse(notif.notificationId,
+                                                     GPS_NI_RESPONSE_IGNORE);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in sendNiResponse");
+            }
+        }
+        if (notif.needNotify) {
+        // If NI does not need verify or the dialog is not requested
+        // to pop up immediately, the dialog box will not pop up.
+            if (notif.needVerify && mPopupImmediately) {
+                // Popup the dialog box now
+                openNiDialog(notif);
+            } else {
+                // Show the notification
+                setNiNotification(notif);
+            }
+        }
+        // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify;
+        // 3. privacy override.
+        if (!notif.needVerify || notif.privacyOverride) {
+            try {
+                mNetInitiatedListener.sendNiResponse(notif.notificationId,
+                                                     GPS_NI_RESPONSE_ACCEPT);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in sendNiResponse");
+            }
+        }
+    }
+
+    // handle NI from HAL when the SUPL_ES is enabled
+    private void handleNiInEs(GpsNiNotification notif) {
+
+        if (DEBUG) Log.d(TAG, "in handleNiInEs () :"
+                    + " niType: " + notif.niType
+                    + " notificationId: " + notif.notificationId);
+
+        // UE is in emergency mode when in emergency call mode or in emergency call back mode
+        /*
+           1. When SUPL ES bit is off and UE is not in emergency mode:
+                  Call handleNi() to do legacy behaviour.
+           2. When SUPL ES bit is on and UE is in emergency mode:
+                  Call handleNi() to do acceptance behaviour.
+           3. When SUPL ES bit is off but UE is in emergency mode:
+                  Ignore the emergency SUPL INIT.
+           4. When SUPL ES bit is on but UE is not in emergency mode:
+                  Ignore the emergency SUPL INIT.
+        */
+        boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL);
+        if (isNiTypeES != getInEmergency()) {
+            try {
+                mNetInitiatedListener.sendNiResponse(notif.notificationId,
+                                                     GPS_NI_RESPONSE_IGNORE);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in sendNiResponse");
+            }
+        } else {
+            handleNi(notif);
+        }
+    }
+
+    // Sets the NI notification.
+    private synchronized void setNiNotification(GpsNiNotification notif) {
+        NotificationManager notificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        if (notificationManager == null) {
+            return;
+        }
+
+        String title = getNotifTitle(notif, mContext);
+        String message = getNotifMessage(notif, mContext);
+
+        if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId +
+                ", title: " + title +
+                ", message: " + message);
+
+        // Construct Notification
+        if (mNiNotificationBuilder == null) {
+            mNiNotificationBuilder = new Notification.Builder(mContext,
+                SystemNotificationChannels.NETWORK_ALERTS)
+                    .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
+                    .setWhen(0)
+                    .setOngoing(true)
+                    .setAutoCancel(true)
+                    .setColor(mContext.getColor(
+                            com.android.internal.R.color.system_notification_accent_color));
+        }
+
+        if (mPlaySounds) {
+            mNiNotificationBuilder.setDefaults(Notification.DEFAULT_SOUND);
+        } else {
+            mNiNotificationBuilder.setDefaults(0);
+        }
+
+        // if not to popup dialog immediately, pending intent will open the dialog
+        Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent();
+        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        mNiNotificationBuilder.setTicker(getNotifTicker(notif, mContext))
+                .setContentTitle(title)
+                .setContentText(message)
+                .setContentIntent(pi);
+
+        notificationManager.notifyAsUser(null, notif.notificationId, mNiNotificationBuilder.build(),
+                UserHandle.ALL);
+    }
+
+    // Opens the notification dialog and waits for user input
+    private void openNiDialog(GpsNiNotification notif)
+    {
+        Intent intent = getDlgIntent(notif);
+
+        if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId +
+                ", requestorId: " + notif.requestorId +
+                ", text: " + notif.text);
+
+        mContext.startActivity(intent);
+    }
+
+    // Construct the intent for bringing up the dialog activity, which shows the
+    // notification and takes user input
+    private Intent getDlgIntent(GpsNiNotification notif)
+    {
+        Intent intent = new Intent();
+        String title = getDialogTitle(notif, mContext);
+        String message = getDialogMessage(notif, mContext);
+
+        // directly bring up the NI activity
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class);
+
+        // put data in the intent
+        intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId);
+        intent.putExtra(NI_INTENT_KEY_TITLE, title);
+        intent.putExtra(NI_INTENT_KEY_MESSAGE, message);
+        intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout);
+        intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse);
+
+        if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message +
+                ", timeout: " + notif.timeout);
+
+        return intent;
+    }
+
+    // Converts a string (or Hex string) to a char array
+    static byte[] stringToByteArray(String original, boolean isHex)
+    {
+        int length = isHex ? original.length() / 2 : original.length();
+        byte[] output = new byte[length];
+        int i;
+
+        if (isHex)
+        {
+            for (i = 0; i < length; i++)
+            {
+                output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16);
+            }
+        }
+        else {
+            for (i = 0; i < length; i++)
+            {
+                output[i] = (byte) original.charAt(i);
+            }
+        }
+
+        return output;
+    }
+
+    /**
+     * Unpacks an byte array containing 7-bit packed characters into a String.
+     *
+     * @param input a 7-bit packed char array
+     * @return the unpacked String
+     */
+    static String decodeGSMPackedString(byte[] input)
+    {
+        final char PADDING_CHAR = 0x00;
+        int lengthBytes = input.length;
+        int lengthSeptets = (lengthBytes * 8) / 7;
+        String decoded;
+
+        /* Special case where the last 7 bits in the last byte could hold a valid
+         * 7-bit character or a padding character. Drop the last 7-bit character
+         * if it is a padding character.
+         */
+        if (lengthBytes % 7 == 0) {
+            if (lengthBytes > 0) {
+                if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) {
+                    lengthSeptets = lengthSeptets - 1;
+                }
+            }
+        }
+
+        decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets);
+
+        // Return "" if decoding of GSM packed string fails
+        if (null == decoded) {
+            Log.e(TAG, "Decoding of GSM packed string failed");
+            decoded = "";
+        }
+
+        return decoded;
+    }
+
+    static String decodeUTF8String(byte[] input)
+    {
+        String decoded = "";
+        try {
+            decoded = new String(input, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new AssertionError();
+        }
+        return decoded;
+    }
+
+    static String decodeUCS2String(byte[] input)
+    {
+        String decoded = "";
+        try {
+            decoded = new String(input, "UTF-16");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new AssertionError();
+        }
+        return decoded;
+    }
+
+    /** Decode NI string
+     *
+     * @param original   The text string to be decoded
+     * @param isHex      Specifies whether the content of the string has been encoded as a Hex string. Encoding
+     *                   a string as Hex can allow zeros inside the coded text.
+     * @param coding     Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme
+     *                      needs to match those used passed to HAL from the native GPS driver. Decoding is done according
+     *                   to the <code> coding </code>, after a Hex string is decoded. Generally, if the
+     *                   notification strings don't need further decoding, <code> coding </code> encoding can be
+     *                   set to -1, and <code> isHex </code> can be false.
+     * @return the decoded string
+     */
+    static private String decodeString(String original, boolean isHex, int coding)
+    {
+        String decoded = original;
+        byte[] input = stringToByteArray(original, isHex);
+
+        switch (coding) {
+        case GPS_ENC_NONE:
+            decoded = original;
+            break;
+
+        case GPS_ENC_SUPL_GSM_DEFAULT:
+            decoded = decodeGSMPackedString(input);
+            break;
+
+        case GPS_ENC_SUPL_UTF8:
+            decoded = decodeUTF8String(input);
+            break;
+
+        case GPS_ENC_SUPL_UCS2:
+            decoded = decodeUCS2String(input);
+            break;
+
+        case GPS_ENC_UNKNOWN:
+            decoded = original;
+            break;
+
+        default:
+            Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original);
+            break;
+        }
+        return decoded;
+    }
+
+    // change this to configure notification display
+    static private String getNotifTicker(GpsNiNotification notif, Context context)
+    {
+        String ticker = String.format(context.getString(R.string.gpsNotifTicker),
+                decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
+                decodeString(notif.text, mIsHexInput, notif.textEncoding));
+        return ticker;
+    }
+
+    // change this to configure notification display
+    static private String getNotifTitle(GpsNiNotification notif, Context context)
+    {
+        String title = String.format(context.getString(R.string.gpsNotifTitle));
+        return title;
+    }
+
+    // change this to configure notification display
+    static private String getNotifMessage(GpsNiNotification notif, Context context)
+    {
+        String message = String.format(context.getString(R.string.gpsNotifMessage),
+                decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
+                decodeString(notif.text, mIsHexInput, notif.textEncoding));
+        return message;
+    }
+
+    // change this to configure dialog display (for verification)
+    static public String getDialogTitle(GpsNiNotification notif, Context context)
+    {
+        return getNotifTitle(notif, context);
+    }
+
+    // change this to configure dialog display (for verification)
+    static private String getDialogMessage(GpsNiNotification notif, Context context)
+    {
+        return getNotifMessage(notif, context);
+    }
+
+}
diff --git a/com/android/internal/location/ProviderProperties.java b/com/android/internal/location/ProviderProperties.java
new file mode 100644
index 0000000..def96f0
--- /dev/null
+++ b/com/android/internal/location/ProviderProperties.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2012 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.internal.location;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A Parcelable containing (legacy) location provider properties.
+ * This object is just used inside the framework and system services.
+ * @hide
+ */
+public final class ProviderProperties implements Parcelable {
+    /**
+     * True if provider requires access to a
+     * data network (e.g., the Internet), false otherwise.
+     */
+    public final boolean mRequiresNetwork;
+
+    /**
+     * True if the provider requires access to a
+     * satellite-based positioning system (e.g., GPS), false
+     * otherwise.
+     */
+    public final boolean mRequiresSatellite;
+
+    /**
+     * True if the provider requires access to an appropriate
+     * cellular network (e.g., to make use of cell tower IDs), false
+     * otherwise.
+     */
+    public final boolean mRequiresCell;
+
+    /**
+     * True if the use of this provider may result in a
+     * monetary charge to the user, false if use is free.  It is up to
+     * each provider to give accurate information. Cell (network) usage
+     * is not considered monetary cost.
+     */
+    public final boolean mHasMonetaryCost;
+
+    /**
+     * True if the provider is able to provide altitude
+     * information, false otherwise.  A provider that reports altitude
+     * under most circumstances but may occasionally not report it
+     * should return true.
+     */
+    public final boolean mSupportsAltitude;
+
+    /**
+     * True if the provider is able to provide speed
+     * information, false otherwise.  A provider that reports speed
+     * under most circumstances but may occasionally not report it
+     * should return true.
+     */
+    public final boolean mSupportsSpeed;
+
+    /**
+     * True if the provider is able to provide bearing
+     * information, false otherwise.  A provider that reports bearing
+     * under most circumstances but may occasionally not report it
+     * should return true.
+     */
+    public final boolean mSupportsBearing;
+
+    /**
+     * Power requirement for this provider.
+     *
+     * @return the power requirement for this provider, as one of the
+     * constants Criteria.POWER_*.
+     */
+    public final int mPowerRequirement;
+
+    /**
+     * Constant describing the horizontal accuracy returned
+     * by this provider.
+     *
+     * @return the horizontal accuracy for this provider, as one of the
+     * constants Criteria.ACCURACY_COARSE or Criteria.ACCURACY_FINE
+     */
+    public final int mAccuracy;
+
+    public ProviderProperties(boolean mRequiresNetwork,
+            boolean mRequiresSatellite, boolean mRequiresCell, boolean mHasMonetaryCost,
+            boolean mSupportsAltitude, boolean mSupportsSpeed, boolean mSupportsBearing,
+            int mPowerRequirement, int mAccuracy) {
+        this.mRequiresNetwork = mRequiresNetwork;
+        this.mRequiresSatellite = mRequiresSatellite;
+        this.mRequiresCell = mRequiresCell;
+        this.mHasMonetaryCost = mHasMonetaryCost;
+        this.mSupportsAltitude = mSupportsAltitude;
+        this.mSupportsSpeed = mSupportsSpeed;
+        this.mSupportsBearing = mSupportsBearing;
+        this.mPowerRequirement = mPowerRequirement;
+        this.mAccuracy = mAccuracy;
+    }
+
+    public static final Parcelable.Creator<ProviderProperties> CREATOR =
+            new Parcelable.Creator<ProviderProperties>() {
+        @Override
+        public ProviderProperties createFromParcel(Parcel in) {
+            boolean requiresNetwork = in.readInt() == 1;
+            boolean requiresSatellite = in.readInt() == 1;
+            boolean requiresCell = in.readInt() == 1;
+            boolean hasMonetaryCost = in.readInt() == 1;
+            boolean supportsAltitude = in.readInt() == 1;
+            boolean supportsSpeed = in.readInt() == 1;
+            boolean supportsBearing = in.readInt() == 1;
+            int powerRequirement = in.readInt();
+            int accuracy = in.readInt();
+            return new ProviderProperties(requiresNetwork, requiresSatellite,
+                    requiresCell, hasMonetaryCost, supportsAltitude, supportsSpeed, supportsBearing,
+                    powerRequirement, accuracy);
+        }
+        @Override
+        public ProviderProperties[] newArray(int size) {
+            return new ProviderProperties[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeInt(mRequiresNetwork ? 1 : 0);
+        parcel.writeInt(mRequiresSatellite ? 1 : 0);
+        parcel.writeInt(mRequiresCell ? 1 : 0);
+        parcel.writeInt(mHasMonetaryCost ? 1 : 0);
+        parcel.writeInt(mSupportsAltitude ? 1 : 0);
+        parcel.writeInt(mSupportsSpeed ? 1 : 0);
+        parcel.writeInt(mSupportsBearing ? 1 : 0);
+        parcel.writeInt(mPowerRequirement);
+        parcel.writeInt(mAccuracy);
+    }
+}
diff --git a/com/android/internal/location/ProviderRequest.java b/com/android/internal/location/ProviderRequest.java
new file mode 100644
index 0000000..26243e7
--- /dev/null
+++ b/com/android/internal/location/ProviderRequest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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.internal.location;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.location.LocationRequest;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.TimeUtils;
+
+/** @hide */
+public final class ProviderRequest implements Parcelable {
+    /** Location reporting is requested (true) */
+    public boolean reportLocation = false;
+
+    /** The smallest requested interval */
+    public long interval = Long.MAX_VALUE;
+
+    /**
+     * A more detailed set of requests.
+     * <p>Location Providers can optionally use this to
+     * fine tune location updates, for example when there
+     * is a high power slow interval request and a
+     * low power fast interval request.
+     */
+    public List<LocationRequest> locationRequests = new ArrayList<LocationRequest>();
+
+    public ProviderRequest() { }
+
+    public static final Parcelable.Creator<ProviderRequest> CREATOR =
+            new Parcelable.Creator<ProviderRequest>() {
+        @Override
+        public ProviderRequest createFromParcel(Parcel in) {
+            ProviderRequest request = new ProviderRequest();
+            request.reportLocation = in.readInt() == 1;
+            request.interval = in.readLong();
+            int count = in.readInt();
+            for (int i = 0; i < count; i++) {
+                request.locationRequests.add(LocationRequest.CREATOR.createFromParcel(in));
+            }
+            return request;
+        }
+        @Override
+        public ProviderRequest[] newArray(int size) {
+            return new ProviderRequest[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeInt(reportLocation ? 1 : 0);
+        parcel.writeLong(interval);
+        parcel.writeInt(locationRequests.size());
+        for (LocationRequest request : locationRequests) {
+            request.writeToParcel(parcel, flags);
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder s = new StringBuilder();
+        s.append("ProviderRequest[");
+        if (reportLocation) {
+            s.append("ON");
+            s.append(" interval=");
+            TimeUtils.formatDuration(interval, s);
+        } else {
+            s.append("OFF");
+        }
+        s.append(']');
+        return s.toString();
+    }
+}
diff --git a/com/android/internal/location/gnssmetrics/GnssMetrics.java b/com/android/internal/location/gnssmetrics/GnssMetrics.java
new file mode 100644
index 0000000..833376c
--- /dev/null
+++ b/com/android/internal/location/gnssmetrics/GnssMetrics.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2016 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.internal.location.gnssmetrics;
+
+import android.os.SystemClock;
+
+import android.util.Base64;
+import android.util.TimeUtils;
+
+import java.util.Arrays;
+
+import com.android.internal.location.nano.GnssLogsProto.GnssLog;
+
+/**
+ * GnssMetrics: Is used for logging GNSS metrics
+ * @hide
+ */
+public class GnssMetrics {
+
+  /** Default time between location fixes (in millisecs) */
+  private static final int DEFAULT_TIME_BETWEEN_FIXES_MILLISECS = 1000;
+
+  /* The time since boot when logging started */
+  private String logStartInElapsedRealTime;
+
+  /** Constructor */
+  public GnssMetrics() {
+    locationFailureStatistics = new Statistics();
+    timeToFirstFixSecStatistics = new Statistics();
+    positionAccuracyMeterStatistics = new Statistics();
+    topFourAverageCn0Statistics = new Statistics();
+    reset();
+  }
+
+  /**
+   * Logs the status of a location report received from the HAL
+   *
+   * @param isSuccessful
+   */
+  public void logReceivedLocationStatus(boolean isSuccessful) {
+    if (!isSuccessful) {
+      locationFailureStatistics.addItem(1.0);
+      return;
+    }
+    locationFailureStatistics.addItem(0.0);
+    return;
+  }
+
+  /**
+   * Logs missed reports
+   *
+   * @param desiredTimeBetweenFixesMilliSeconds
+   * @param actualTimeBetweenFixesMilliSeconds
+   */
+  public void logMissedReports(int desiredTimeBetweenFixesMilliSeconds,
+      int actualTimeBetweenFixesMilliSeconds) {
+    int numReportMissed = (actualTimeBetweenFixesMilliSeconds /
+        Math.max(DEFAULT_TIME_BETWEEN_FIXES_MILLISECS, desiredTimeBetweenFixesMilliSeconds)) - 1;
+    if (numReportMissed > 0) {
+      for (int i = 0; i < numReportMissed; i++) {
+        locationFailureStatistics.addItem(1.0);
+      }
+    }
+    return;
+  }
+
+  /**
+   * Logs time to first fix
+   *
+   * @param timeToFirstFixMilliSeconds
+   */
+  public void logTimeToFirstFixMilliSecs(int timeToFirstFixMilliSeconds) {
+    timeToFirstFixSecStatistics.addItem((double) (timeToFirstFixMilliSeconds/1000));
+    return;
+  }
+
+  /**
+   * Logs position accuracy
+   *
+   * @param positionAccuracyMeters
+   */
+  public void logPositionAccuracyMeters(float positionAccuracyMeters) {
+    positionAccuracyMeterStatistics.addItem((double) positionAccuracyMeters);
+    return;
+  }
+
+  /*
+  * Logs CN0 when at least 4 SVs are available
+  *
+  */
+  public void logCn0(float[] cn0s, int numSv) {
+    if (numSv < 4) {
+      return;
+    }
+    float[] cn0Array = Arrays.copyOf(cn0s, numSv);
+    Arrays.sort(cn0Array);
+    if (cn0Array[numSv - 4] > 0.0) {
+      double top4AvgCn0 = 0.0;
+      for (int i = numSv - 4; i < numSv; i++) {
+        top4AvgCn0 += (double) cn0Array[i];
+      }
+      top4AvgCn0 /= 4;
+      topFourAverageCn0Statistics.addItem(top4AvgCn0);
+    }
+    return;
+  }
+
+  /**
+   * Dumps GNSS metrics as a proto string
+   * @return
+   */
+  public String dumpGnssMetricsAsProtoString() {
+    GnssLog msg = new GnssLog();
+    if (locationFailureStatistics.getCount() > 0) {
+      msg.numLocationReportProcessed = locationFailureStatistics.getCount();
+      msg.percentageLocationFailure = (int) (100.0 * locationFailureStatistics.getMean());
+    }
+    if (timeToFirstFixSecStatistics.getCount() > 0) {
+      msg.numTimeToFirstFixProcessed = timeToFirstFixSecStatistics.getCount();
+      msg.meanTimeToFirstFixSecs = (int) timeToFirstFixSecStatistics.getMean();
+      msg.standardDeviationTimeToFirstFixSecs
+          = (int) timeToFirstFixSecStatistics.getStandardDeviation();
+    }
+    if (positionAccuracyMeterStatistics.getCount() > 0) {
+      msg.numPositionAccuracyProcessed = positionAccuracyMeterStatistics.getCount();
+      msg.meanPositionAccuracyMeters = (int) positionAccuracyMeterStatistics.getMean();
+      msg.standardDeviationPositionAccuracyMeters
+          = (int) positionAccuracyMeterStatistics.getStandardDeviation();
+    }
+    if (topFourAverageCn0Statistics.getCount() > 0) {
+      msg.numTopFourAverageCn0Processed = topFourAverageCn0Statistics.getCount();
+      msg.meanTopFourAverageCn0DbHz = topFourAverageCn0Statistics.getMean();
+      msg.standardDeviationTopFourAverageCn0DbHz
+          = topFourAverageCn0Statistics.getStandardDeviation();
+    }
+    String s = Base64.encodeToString(GnssLog.toByteArray(msg), Base64.DEFAULT);
+    reset();
+    return s;
+  }
+
+  /**
+   * Dumps GNSS Metrics as text
+   *
+   * @return GNSS Metrics
+   */
+  public String dumpGnssMetricsAsText() {
+    StringBuilder s = new StringBuilder();
+    s.append("GNSS_KPI_START").append('\n');
+    s.append("  KPI logging start time: ").append(logStartInElapsedRealTime).append("\n");
+    s.append("  KPI logging end time: ");
+    TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s);
+    s.append("\n");
+    s.append("  Number of location reports: ").append(
+        locationFailureStatistics.getCount()).append("\n");
+    if (locationFailureStatistics.getCount() > 0) {
+      s.append("  Percentage location failure: ").append(
+          100.0 * locationFailureStatistics.getMean()).append("\n");
+    }
+    s.append("  Number of TTFF reports: ").append(
+        timeToFirstFixSecStatistics.getCount()).append("\n");
+    if (timeToFirstFixSecStatistics.getCount() > 0) {
+      s.append("  TTFF mean (sec): ").append(timeToFirstFixSecStatistics.getMean()).append("\n");
+      s.append("  TTFF standard deviation (sec): ").append(
+          timeToFirstFixSecStatistics.getStandardDeviation()).append("\n");
+    }
+    s.append("  Number of position accuracy reports: ").append(
+        positionAccuracyMeterStatistics.getCount()).append("\n");
+    if (positionAccuracyMeterStatistics.getCount() > 0) {
+      s.append("  Position accuracy mean (m): ").append(
+          positionAccuracyMeterStatistics.getMean()).append("\n");
+      s.append("  Position accuracy standard deviation (m): ").append(
+          positionAccuracyMeterStatistics.getStandardDeviation()).append("\n");
+    }
+    s.append("  Number of CN0 reports: ").append(
+        topFourAverageCn0Statistics.getCount()).append("\n");
+    if (topFourAverageCn0Statistics.getCount() > 0) {
+      s.append("  Top 4 Avg CN0 mean (dB-Hz): ").append(
+          topFourAverageCn0Statistics.getMean()).append("\n");
+      s.append("  Top 4 Avg CN0 standard deviation (dB-Hz): ").append(
+          topFourAverageCn0Statistics.getStandardDeviation()).append("\n");
+    }
+    s.append("GNSS_KPI_END").append("\n");
+    return s.toString();
+  }
+
+   /** Class for storing statistics */
+  private class Statistics {
+
+    /** Resets statistics */
+    public void reset() {
+      count = 0;
+      sum = 0.0;
+      sumSquare = 0.0;
+    }
+
+    /** Adds an item */
+    public void addItem(double item) {
+      count++;
+      sum += item;
+      sumSquare += item * item;
+    }
+
+    /** Returns number of items added */
+    public int getCount() {
+      return count;
+    }
+
+    /** Returns mean */
+    public double getMean() {
+      return sum/count;
+    }
+
+    /** Returns standard deviation */
+    public double getStandardDeviation() {
+      double m = sum/count;
+      m = m * m;
+      double v = sumSquare/count;
+      if (v > m) {
+        return Math.sqrt(v - m);
+      }
+      return 0;
+    }
+
+    private int count;
+    private double sum;
+    private double sumSquare;
+  }
+
+  /** Location failure statistics */
+  private Statistics locationFailureStatistics;
+
+  /** Time to first fix statistics */
+  private Statistics timeToFirstFixSecStatistics;
+
+  /** Position accuracy statistics */
+  private Statistics positionAccuracyMeterStatistics;
+
+  /** Top 4 average CN0 statistics */
+  private Statistics topFourAverageCn0Statistics;
+
+  /**
+   * Resets GNSS metrics
+   */
+  private void reset() {
+    StringBuilder s = new StringBuilder();
+    TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s);
+    logStartInElapsedRealTime = s.toString();
+    locationFailureStatistics.reset();
+    timeToFirstFixSecStatistics.reset();
+    positionAccuracyMeterStatistics.reset();
+    topFourAverageCn0Statistics.reset();
+    return;
+  }
+}
\ No newline at end of file
diff --git a/com/android/internal/logging/AndroidConfig.java b/com/android/internal/logging/AndroidConfig.java
new file mode 100644
index 0000000..f8002c6
--- /dev/null
+++ b/com/android/internal/logging/AndroidConfig.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2008 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.internal.logging;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Implements the java.util.logging configuration for Android. Activates a log
+ * handler that writes to the Android log.
+ */
+public class AndroidConfig {
+
+    /**
+     * This looks a bit weird, but it's the way the logging config works: A
+     * named class is instantiated, the constructor is assumed to tweak the
+     * configuration, the instance itself is of no interest.
+     */
+    public AndroidConfig() {
+        super();
+        
+        try {
+            Logger rootLogger = Logger.getLogger("");
+            rootLogger.addHandler(new AndroidHandler());
+            rootLogger.setLevel(Level.INFO);
+
+            // Turn down logging in Apache libraries.
+            Logger.getLogger("org.apache").setLevel(Level.WARNING);
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+    }    
+}
diff --git a/com/android/internal/logging/AndroidHandler.java b/com/android/internal/logging/AndroidHandler.java
new file mode 100644
index 0000000..f55a31f
--- /dev/null
+++ b/com/android/internal/logging/AndroidHandler.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2008 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.internal.logging;
+
+import android.util.Log;
+import com.android.internal.util.FastPrintWriter;
+import dalvik.system.DalvikLogging;
+import dalvik.system.DalvikLogHandler;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * Implements a {@link java.util.logging.Logger} handler that writes to the Android log. The
+ * implementation is rather straightforward. The name of the logger serves as
+ * the log tag. Only the log levels need to be converted appropriately. For
+ * this purpose, the following mapping is being used:
+ * 
+ * <table>
+ *   <tr>
+ *     <th>logger level</th>
+ *     <th>Android level</th>
+ *   </tr>
+ *   <tr>
+ *     <td>
+ *       SEVERE
+ *     </td>
+ *     <td>
+ *       ERROR
+ *     </td>
+ *   </tr>
+ *   <tr>
+ *     <td>
+ *       WARNING
+ *     </td>
+ *     <td>
+ *       WARN
+ *     </td>
+ *   </tr>
+ *   <tr>
+ *     <td>
+ *       INFO
+ *     </td>
+ *     <td>
+ *       INFO
+ *     </td>
+ *   </tr>
+ *   <tr>
+ *     <td>
+ *       CONFIG
+ *     </td>
+ *     <td>
+ *       DEBUG
+ *     </td>
+ *   </tr>
+ *   <tr>
+ *     <td>
+ *       FINE, FINER, FINEST
+ *     </td>
+ *     <td>
+ *       VERBOSE
+ *     </td>
+ *   </tr>
+ * </table>
+ */
+public class AndroidHandler extends Handler implements DalvikLogHandler {
+    /**
+     * Holds the formatter for all Android log handlers.
+     */
+    private static final Formatter THE_FORMATTER = new Formatter() {
+        @Override
+        public String format(LogRecord r) {
+            Throwable thrown = r.getThrown();
+            if (thrown != null) {
+                StringWriter sw = new StringWriter();
+                PrintWriter pw = new FastPrintWriter(sw, false, 256);
+                sw.write(r.getMessage());
+                sw.write("\n");
+                thrown.printStackTrace(pw);
+                pw.flush();
+                return sw.toString();
+            } else {
+                return r.getMessage();
+            }
+        }
+    };
+
+    /**
+     * Constructs a new instance of the Android log handler.
+     */
+    public AndroidHandler() {
+        setFormatter(THE_FORMATTER);
+    }
+
+    @Override
+    public void close() {
+        // No need to close, but must implement abstract method.
+    }
+
+    @Override
+    public void flush() {
+        // No need to flush, but must implement abstract method.
+    }
+
+    @Override
+    public void publish(LogRecord record) {
+        int level = getAndroidLevel(record.getLevel());
+        String tag = DalvikLogging.loggerNameToTag(record.getLoggerName());
+        if (!Log.isLoggable(tag, level)) {
+            return;
+        }
+
+        try {
+            String message = getFormatter().format(record);
+            Log.println(level, tag, message);
+        } catch (RuntimeException e) {
+            Log.e("AndroidHandler", "Error logging message.", e);
+        }
+    }
+
+    public void publish(Logger source, String tag, Level level, String message) {
+        // TODO: avoid ducking into native 2x; we aren't saving any formatter calls
+        int priority = getAndroidLevel(level);
+        if (!Log.isLoggable(tag, priority)) {
+            return;
+        }
+
+        try {
+            Log.println(priority, tag, message);
+        } catch (RuntimeException e) {
+            Log.e("AndroidHandler", "Error logging message.", e);
+        }
+    }
+
+    /**
+     * Converts a {@link java.util.logging.Logger} logging level into an Android one.
+     *
+     * @param level The {@link java.util.logging.Logger} logging level.
+     *
+     * @return The resulting Android logging level.
+     */
+    static int getAndroidLevel(Level level) {
+        int value = level.intValue();
+        if (value >= 1000) { // SEVERE
+            return Log.ERROR;
+        } else if (value >= 900) { // WARNING
+            return Log.WARN;
+        } else if (value >= 800) { // INFO
+            return Log.INFO;
+        } else {
+            return Log.DEBUG;
+        }
+    }
+}
diff --git a/com/android/internal/logging/MetricsLogger.java b/com/android/internal/logging/MetricsLogger.java
new file mode 100644
index 0000000..a482929
--- /dev/null
+++ b/com/android/internal/logging/MetricsLogger.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2015 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.internal.logging;
+
+import android.content.Context;
+import android.metrics.LogMaker;
+import android.os.Build;
+import android.view.View;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+/**
+ * Log all the things.
+ *
+ * @hide
+ */
+public class MetricsLogger {
+    // define metric categories in frameworks/base/proto/src/metrics_constants.proto.
+    // mirror changes in native version at system/core/libmetricslogger/metrics_logger.cpp
+
+    private static MetricsLogger sMetricsLogger;
+
+    private static MetricsLogger getLogger() {
+        if (sMetricsLogger == null) {
+            sMetricsLogger = new MetricsLogger();
+        }
+        return sMetricsLogger;
+    }
+
+    protected void saveLog(Object[] rep) {
+        EventLogTags.writeSysuiMultiAction(rep);
+    }
+
+    public static final int VIEW_UNKNOWN = MetricsEvent.VIEW_UNKNOWN;
+    public static final int LOGTAG = EventLogTags.SYSUI_MULTI_ACTION;
+
+    public void write(LogMaker content) {
+        if (content.getType() == MetricsEvent.TYPE_UNKNOWN) {
+            content.setType(MetricsEvent.TYPE_ACTION);
+        }
+        saveLog(content.serialize());
+    }
+
+    public void visible(int category) throws IllegalArgumentException {
+        if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) {
+            throw new IllegalArgumentException("Must define metric category");
+        }
+        EventLogTags.writeSysuiViewVisibility(category, 100);
+        saveLog(new LogMaker(category)
+                        .setType(MetricsEvent.TYPE_OPEN)
+                        .serialize());
+    }
+
+    public void hidden(int category) throws IllegalArgumentException {
+        if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) {
+            throw new IllegalArgumentException("Must define metric category");
+        }
+        EventLogTags.writeSysuiViewVisibility(category, 0);
+        saveLog(new LogMaker(category)
+                        .setType(MetricsEvent.TYPE_CLOSE)
+                        .serialize());
+    }
+
+    public void visibility(int category, boolean visibile)
+            throws IllegalArgumentException {
+        if (visibile) {
+            visible(category);
+        } else {
+            hidden(category);
+        }
+    }
+
+    public void visibility(int category, int vis)
+            throws IllegalArgumentException {
+        visibility(category, vis == View.VISIBLE);
+    }
+
+    public void action(int category) {
+        EventLogTags.writeSysuiAction(category, "");
+        saveLog(new LogMaker(category)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .serialize());
+    }
+
+    public void action(int category, int value) {
+        EventLogTags.writeSysuiAction(category, Integer.toString(value));
+        saveLog(new LogMaker(category)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .setSubtype(value)
+                        .serialize());
+    }
+
+    public void action(int category, boolean value) {
+        EventLogTags.writeSysuiAction(category, Boolean.toString(value));
+        saveLog(new LogMaker(category)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .setSubtype(value ? 1 : 0)
+                        .serialize());
+    }
+
+    public void action(int category, String pkg) {
+        if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) {
+            throw new IllegalArgumentException("Must define metric category");
+        }
+        EventLogTags.writeSysuiAction(category, pkg);
+        saveLog(new LogMaker(category)
+                .setType(MetricsEvent.TYPE_ACTION)
+                .setPackageName(pkg)
+                .serialize());
+    }
+
+    /** Add an integer value to the monotonically increasing counter with the given name. */
+    public void count(String name, int value) {
+        EventLogTags.writeSysuiCount(name, value);
+        saveLog(new LogMaker(MetricsEvent.RESERVED_FOR_LOGBUILDER_COUNTER)
+                        .setCounterName(name)
+                        .setCounterValue(value)
+                        .serialize());
+    }
+
+    /** Increment the bucket with the integer label on the histogram with the given name. */
+    public void histogram(String name, int bucket) {
+        // see LogHistogram in system/core/libmetricslogger/metrics_logger.cpp
+        EventLogTags.writeSysuiHistogram(name, bucket);
+        saveLog(new LogMaker(MetricsEvent.RESERVED_FOR_LOGBUILDER_HISTOGRAM)
+                        .setCounterName(name)
+                        .setCounterBucket(bucket)
+                        .setCounterValue(1)
+                        .serialize());
+    }
+
+    /** @deprecated use {@link #visible(int)} */
+    @Deprecated
+    public static void visible(Context context, int category) throws IllegalArgumentException {
+        getLogger().visible(category);
+    }
+
+    /** @deprecated use {@link #hidden(int)} */
+    @Deprecated
+    public static void hidden(Context context, int category) throws IllegalArgumentException {
+        getLogger().hidden(category);
+    }
+
+    /** @deprecated use {@link #visibility(int, boolean)} */
+    @Deprecated
+    public static void visibility(Context context, int category, boolean visibile)
+            throws IllegalArgumentException {
+        getLogger().visibility(category, visibile);
+    }
+
+    /** @deprecated use {@link #visibility(int, int)} */
+    @Deprecated
+    public static void visibility(Context context, int category, int vis)
+            throws IllegalArgumentException {
+        visibility(context, category, vis == View.VISIBLE);
+    }
+
+    /** @deprecated use {@link #action(int)} */
+    @Deprecated
+    public static void action(Context context, int category) {
+        getLogger().action(category);
+    }
+
+    /** @deprecated use {@link #action(int, int)} */
+    @Deprecated
+    public static void action(Context context, int category, int value) {
+        getLogger().action(category, value);
+    }
+
+    /** @deprecated use {@link #action(int, boolean)} */
+    @Deprecated
+    public static void action(Context context, int category, boolean value) {
+        getLogger().action(category, value);
+    }
+
+    /** @deprecated use {@link #write(LogMaker)} */
+    @Deprecated
+    public static void action(LogMaker content) {
+        getLogger().write(content);
+    }
+
+    /** @deprecated use {@link #action(int, String)} */
+    @Deprecated
+    public static void action(Context context, int category, String pkg) {
+        getLogger().action(category, pkg);
+    }
+
+    /**
+     * Add an integer value to the monotonically increasing counter with the given name.
+     * @deprecated use {@link #count(String, int)}
+     */
+    @Deprecated
+    public static void count(Context context, String name, int value) {
+        getLogger().count(name, value);
+    }
+
+    /**
+     * Increment the bucket with the integer label on the histogram with the given name.
+     * @deprecated use {@link #histogram(String, int)}
+     */
+    @Deprecated
+    public static void histogram(Context context, String name, int bucket) {
+        getLogger().histogram(name, bucket);
+    }
+}
diff --git a/com/android/internal/logging/testing/FakeMetricsLogger.java b/com/android/internal/logging/testing/FakeMetricsLogger.java
new file mode 100644
index 0000000..fbaf87a
--- /dev/null
+++ b/com/android/internal/logging/testing/FakeMetricsLogger.java
@@ -0,0 +1,30 @@
+package com.android.internal.logging.testing;
+
+import android.metrics.LogMaker;
+
+import com.android.internal.logging.MetricsLogger;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Fake logger that queues up logged events for inspection.
+ *
+ * @hide.
+ */
+public class FakeMetricsLogger extends MetricsLogger {
+    private Queue<LogMaker> logs = new LinkedList<>();
+
+    @Override
+    protected void saveLog(Object[] rep) {
+        logs.offer(new LogMaker(rep));
+    }
+
+    public Queue<LogMaker> getLogs() {
+        return logs;
+    }
+
+    public void reset() {
+        logs.clear();
+    }
+}
diff --git a/com/android/internal/midi/EventScheduler.java b/com/android/internal/midi/EventScheduler.java
new file mode 100644
index 0000000..506902f
--- /dev/null
+++ b/com/android/internal/midi/EventScheduler.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2014 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.internal.midi;
+
+import java.util.Iterator;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Store arbitrary timestamped events using a Long timestamp.
+ * Only one Thread can write into the buffer.
+ * And only one Thread can read from the buffer.
+ */
+public class EventScheduler {
+    private static final long NANOS_PER_MILLI = 1000000;
+
+    private final Object mLock = new Object();
+    volatile private SortedMap<Long, FastEventQueue> mEventBuffer;
+    private FastEventQueue mEventPool = null;
+    private int mMaxPoolSize = 200;
+    private boolean mClosed;
+
+    public EventScheduler() {
+        mEventBuffer = new TreeMap<Long, FastEventQueue>();
+    }
+
+    // If we keep at least one node in the list then it can be atomic
+    // and non-blocking.
+    private class FastEventQueue {
+        // One thread takes from the beginning of the list.
+        volatile SchedulableEvent mFirst;
+        // A second thread returns events to the end of the list.
+        volatile SchedulableEvent mLast;
+        volatile long mEventsAdded;
+        volatile long mEventsRemoved;
+
+        FastEventQueue(SchedulableEvent event) {
+            mFirst = event;
+            mLast = mFirst;
+            mEventsAdded = 1;
+            mEventsRemoved = 0;
+        }
+
+        int size() {
+            return (int)(mEventsAdded - mEventsRemoved);
+        }
+
+        /**
+         * Do not call this unless there is more than one event
+         * in the list.
+         * @return first event in the list
+         */
+        public SchedulableEvent remove() {
+            // Take first event.
+            mEventsRemoved++;
+            SchedulableEvent event = mFirst;
+            mFirst = event.mNext;
+            event.mNext = null;
+            return event;
+        }
+
+        /**
+         * @param event
+         */
+        public void add(SchedulableEvent event) {
+            event.mNext = null;
+            mLast.mNext = event;
+            mLast = event;
+            mEventsAdded++;
+        }
+    }
+
+    /**
+     * Base class for events that can be stored in the EventScheduler.
+     */
+    public static class SchedulableEvent {
+        private long mTimestamp;
+        volatile private SchedulableEvent mNext = null;
+
+        /**
+         * @param timestamp
+         */
+        public SchedulableEvent(long timestamp) {
+            mTimestamp = timestamp;
+        }
+
+        /**
+         * @return timestamp
+         */
+        public long getTimestamp() {
+            return mTimestamp;
+        }
+
+        /**
+         * The timestamp should not be modified when the event is in the
+         * scheduling buffer.
+         */
+        public void setTimestamp(long timestamp) {
+            mTimestamp = timestamp;
+        }
+    }
+
+    /**
+     * Get an event from the pool.
+     * Always leave at least one event in the pool.
+     * @return event or null
+     */
+    public SchedulableEvent removeEventfromPool() {
+        SchedulableEvent event = null;
+        if (mEventPool != null && (mEventPool.size() > 1)) {
+            event = mEventPool.remove();
+        }
+        return event;
+    }
+
+    /**
+     * Return events to a pool so they can be reused.
+     *
+     * @param event
+     */
+    public void addEventToPool(SchedulableEvent event) {
+        if (mEventPool == null) {
+            mEventPool = new FastEventQueue(event);
+        // If we already have enough items in the pool then just
+        // drop the event. This prevents unbounded memory leaks.
+        } else if (mEventPool.size() < mMaxPoolSize) {
+            mEventPool.add(event);
+        }
+    }
+
+    /**
+     * Add an event to the scheduler. Events with the same time will be
+     * processed in order.
+     *
+     * @param event
+     */
+    public void add(SchedulableEvent event) {
+        synchronized (mLock) {
+            FastEventQueue list = mEventBuffer.get(event.getTimestamp());
+            if (list == null) {
+                long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
+                        : mEventBuffer.firstKey();
+                list = new FastEventQueue(event);
+                mEventBuffer.put(event.getTimestamp(), list);
+                // If the event we added is earlier than the previous earliest
+                // event then notify any threads waiting for the next event.
+                if (event.getTimestamp() < lowestTime) {
+                    mLock.notify();
+                }
+            } else {
+                list.add(event);
+            }
+        }
+    }
+
+    private SchedulableEvent removeNextEventLocked(long lowestTime) {
+        SchedulableEvent event;
+        FastEventQueue list = mEventBuffer.get(lowestTime);
+        // Remove list from tree if this is the last node.
+        if ((list.size() == 1)) {
+            mEventBuffer.remove(lowestTime);
+        }
+        event = list.remove();
+        return event;
+    }
+
+    /**
+     * Check to see if any scheduled events are ready to be processed.
+     *
+     * @param timestamp
+     * @return next event or null if none ready
+     */
+    public SchedulableEvent getNextEvent(long time) {
+        SchedulableEvent event = null;
+        synchronized (mLock) {
+            if (!mEventBuffer.isEmpty()) {
+                long lowestTime = mEventBuffer.firstKey();
+                // Is it time for this list to be processed?
+                if (lowestTime <= time) {
+                    event = removeNextEventLocked(lowestTime);
+                }
+            }
+        }
+        // Log.i(TAG, "getNextEvent: event = " + event);
+        return event;
+    }
+
+    /**
+     * Return the next available event or wait until there is an event ready to
+     * be processed. This method assumes that the timestamps are in nanoseconds
+     * and that the current time is System.nanoTime().
+     *
+     * @return event
+     * @throws InterruptedException
+     */
+    public SchedulableEvent waitNextEvent() throws InterruptedException {
+        SchedulableEvent event = null;
+        synchronized (mLock) {
+            while (!mClosed) {
+                long millisToWait = Integer.MAX_VALUE;
+                if (!mEventBuffer.isEmpty()) {
+                    long now = System.nanoTime();
+                    long lowestTime = mEventBuffer.firstKey();
+                    // Is it time for the earliest list to be processed?
+                    if (lowestTime <= now) {
+                        event = removeNextEventLocked(lowestTime);
+                        break;
+                    } else {
+                        // Figure out how long to sleep until next event.
+                        long nanosToWait = lowestTime - now;
+                        // Add 1 millisecond so we don't wake up before it is
+                        // ready.
+                        millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI);
+                        // Clip 64-bit value to 32-bit max.
+                        if (millisToWait > Integer.MAX_VALUE) {
+                            millisToWait = Integer.MAX_VALUE;
+                        }
+                    }
+                }
+                mLock.wait((int) millisToWait);
+            }
+        }
+        return event;
+    }
+
+    protected void flush() {
+        // Replace our event buffer with a fresh empty one
+        mEventBuffer = new TreeMap<Long, FastEventQueue>();
+    }
+
+    public void close() {
+        synchronized (mLock) {
+            mClosed = true;
+            mLock.notify();
+        }
+    }
+}
diff --git a/com/android/internal/midi/MidiConstants.java b/com/android/internal/midi/MidiConstants.java
new file mode 100644
index 0000000..b6b8bf0
--- /dev/null
+++ b/com/android/internal/midi/MidiConstants.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 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.internal.midi;
+
+/**
+ * MIDI related constants and static methods.
+ */
+public final class MidiConstants {
+    public static final byte STATUS_COMMAND_MASK = (byte) 0xF0;
+    public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F;
+
+    // Channel voice messages.
+    public static final byte STATUS_NOTE_OFF = (byte) 0x80;
+    public static final byte STATUS_NOTE_ON = (byte) 0x90;
+    public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0;
+    public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0;
+    public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0;
+    public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0;
+    public static final byte STATUS_PITCH_BEND = (byte) 0xE0;
+
+    // System Common Messages.
+    public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0;
+    public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1;
+    public static final byte STATUS_SONG_POSITION = (byte) 0xF2;
+    public static final byte STATUS_SONG_SELECT = (byte) 0xF3;
+    public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6;
+    public static final byte STATUS_END_SYSEX = (byte) 0xF7;
+
+    // System Real-Time Messages
+    public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8;
+    public static final byte STATUS_START = (byte) 0xFA;
+    public static final byte STATUS_CONTINUE = (byte) 0xFB;
+    public static final byte STATUS_STOP = (byte) 0xFC;
+    public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE;
+    public static final byte STATUS_RESET = (byte) 0xFF;
+
+    /** Number of bytes in a message nc from 8c to Ec */
+    public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 };
+
+    /** Number of bytes in a message Fn from F0 to FF */
+    public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1,
+            1, 1, 1, 1, 1, 1, 1 };
+
+
+    /**
+     * MIDI messages, except for SysEx, are 1,2 or 3 bytes long.
+     * You can tell how long a MIDI message is from the first status byte.
+     * Do not call this for SysEx, which has variable length.
+     * @param statusByte
+     * @return number of bytes in a complete message or zero if data byte passed
+     */
+    public static int getBytesPerMessage(byte statusByte) {
+        // Java bytes are signed so we need to mask off the high bits
+        // to get a value between 0 and 255.
+        int statusInt = statusByte & 0xFF;
+        if (statusInt >= 0xF0) {
+            // System messages use low nibble for size.
+            return SYSTEM_BYTE_LENGTHS[statusInt & 0x0F];
+        } else if(statusInt >= 0x80) {
+            // Channel voice messages use high nibble for size.
+            return CHANNEL_BYTE_LENGTHS[(statusInt >> 4) - 8];
+        } else {
+            return 0; // data byte
+        }
+    }
+
+
+    /**
+     * @param msg
+     * @param offset
+     * @param count
+     * @return true if the entire message is ActiveSensing commands
+     */
+    public static boolean isAllActiveSensing(byte[] msg, int offset,
+            int count) {
+        // Count bytes that are not active sensing.
+        int goodBytes = 0;
+        for (int i = 0; i < count; i++) {
+            byte b = msg[offset + i];
+            if (b != MidiConstants.STATUS_ACTIVE_SENSING) {
+                goodBytes++;
+            }
+        }
+        return (goodBytes == 0);
+    }
+
+    // Returns true if this command can be used for running status
+    public static boolean allowRunningStatus(byte command) {
+        // only Channel Voice and Channel Mode commands can use running status
+        return (command >= STATUS_NOTE_OFF && command < STATUS_SYSTEM_EXCLUSIVE);
+    }
+
+    // Returns true if this command cancels running status
+    public static boolean cancelsRunningStatus(byte command) {
+        // System Common messages cancel running status
+        return (command >= STATUS_SYSTEM_EXCLUSIVE && command <= STATUS_END_SYSEX);
+    }
+}
diff --git a/com/android/internal/midi/MidiDispatcher.java b/com/android/internal/midi/MidiDispatcher.java
new file mode 100644
index 0000000..c16628a
--- /dev/null
+++ b/com/android/internal/midi/MidiDispatcher.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2015 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.internal.midi;
+
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiSender;
+
+import java.io.IOException;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Utility class for dispatching MIDI data to a list of {@link android.media.midi.MidiReceiver}s.
+ * This class subclasses {@link android.media.midi.MidiReceiver} and dispatches any data it receives
+ * to its receiver list. Any receivers that throw an exception upon receiving data will
+ * be automatically removed from the receiver list. If a MidiReceiverFailureHandler has been
+ * provided to the MidiDispatcher, it will be notified about the failure, but the exception
+ * itself will be swallowed.
+ */
+public final class MidiDispatcher extends MidiReceiver {
+
+    // MidiDispatcher's client and MidiReceiver's owner can be different
+    // classes (e.g. MidiDeviceService is a client, but MidiDeviceServer is
+    // the owner), and errors occuring during sending need to be reported
+    // to the owner rather than to the sender.
+    //
+    // Note that the callbacks will be called on the sender's thread.
+    public interface MidiReceiverFailureHandler {
+        void onReceiverFailure(MidiReceiver receiver, IOException failure);
+    }
+
+    private final MidiReceiverFailureHandler mFailureHandler;
+    private final CopyOnWriteArrayList<MidiReceiver> mReceivers
+            = new CopyOnWriteArrayList<MidiReceiver>();
+
+    private final MidiSender mSender = new MidiSender() {
+        @Override
+        public void onConnect(MidiReceiver receiver) {
+            mReceivers.add(receiver);
+        }
+
+        @Override
+        public void onDisconnect(MidiReceiver receiver) {
+            mReceivers.remove(receiver);
+        }
+    };
+
+    public MidiDispatcher() {
+        this(null);
+    }
+
+    public MidiDispatcher(MidiReceiverFailureHandler failureHandler) {
+        mFailureHandler = failureHandler;
+    }
+
+    /**
+     * Returns the number of {@link android.media.midi.MidiReceiver}s this dispatcher contains.
+     * @return the number of receivers
+     */
+    public int getReceiverCount() {
+        return mReceivers.size();
+    }
+
+    /**
+     * Returns a {@link android.media.midi.MidiSender} which is used to add and remove
+     * {@link android.media.midi.MidiReceiver}s
+     * to the dispatcher's receiver list.
+     * @return the dispatcher's MidiSender
+     */
+    public MidiSender getSender() {
+        return mSender;
+    }
+
+    @Override
+    public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
+       for (MidiReceiver receiver : mReceivers) {
+            try {
+                receiver.send(msg, offset, count, timestamp);
+            } catch (IOException e) {
+                // If the receiver fails we remove the receiver but do not propagate the exception.
+                // Note that this may also happen if the client code stalls, and thus underlying
+                // MidiInputPort.onSend has raised IOException for EAGAIN / EWOULDBLOCK error.
+                mReceivers.remove(receiver);
+                if (mFailureHandler != null) {
+                    mFailureHandler.onReceiverFailure(receiver, e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onFlush() throws IOException {
+       for (MidiReceiver receiver : mReceivers) {
+            try {
+                receiver.flush();
+            } catch (IOException e) {
+                // This is just a special case of 'send' thus handle in the same way.
+                mReceivers.remove(receiver);
+                if (mFailureHandler != null) {
+                    mFailureHandler.onReceiverFailure(receiver, e);
+                }
+            }
+       }
+    }
+}
diff --git a/com/android/internal/midi/MidiEventScheduler.java b/com/android/internal/midi/MidiEventScheduler.java
new file mode 100644
index 0000000..7b01904
--- /dev/null
+++ b/com/android/internal/midi/MidiEventScheduler.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2014 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.internal.midi;
+
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Add MIDI Events to an EventScheduler
+ */
+public class MidiEventScheduler extends EventScheduler {
+    private static final String TAG = "MidiEventScheduler";
+    // Maintain a pool of scheduled events to reduce memory allocation.
+    // This pool increases performance by about 14%.
+    private final static int POOL_EVENT_SIZE = 16;
+    private MidiReceiver mReceiver = new SchedulingReceiver();
+
+    private class SchedulingReceiver extends MidiReceiver {
+        /**
+         * Store these bytes in the EventScheduler to be delivered at the specified
+         * time.
+         */
+        @Override
+        public void onSend(byte[] msg, int offset, int count, long timestamp)
+                throws IOException {
+            MidiEvent event = createScheduledEvent(msg, offset, count, timestamp);
+            if (event != null) {
+                add(event);
+            }
+        }
+
+        @Override
+        public void onFlush() {
+            MidiEventScheduler.this.flush();
+        }
+    }
+
+    public static class MidiEvent extends SchedulableEvent {
+        public int count = 0;
+        public byte[] data;
+
+        private MidiEvent(int count) {
+            super(0);
+            data = new byte[count];
+        }
+
+        private MidiEvent(byte[] msg, int offset, int count, long timestamp) {
+            super(timestamp);
+            data = new byte[count];
+            System.arraycopy(msg, offset, data, 0, count);
+            this.count = count;
+        }
+
+        @Override
+        public String toString() {
+            String text = "Event: ";
+            for (int i = 0; i < count; i++) {
+                text += data[i] + ", ";
+            }
+            return text;
+        }
+    }
+
+    /**
+     * Create an event that contains the message.
+     */
+    private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
+            long timestamp) {
+        MidiEvent event;
+        if (count > POOL_EVENT_SIZE) {
+            event = new MidiEvent(msg, offset, count, timestamp);
+        } else {
+            event = (MidiEvent) removeEventfromPool();
+            if (event == null) {
+                event = new MidiEvent(POOL_EVENT_SIZE);
+            }
+            System.arraycopy(msg, offset, event.data, 0, count);
+            event.count = count;
+            event.setTimestamp(timestamp);
+        }
+        return event;
+    }
+
+    /**
+     * Return events to a pool so they can be reused.
+     *
+     * @param event
+     */
+    @Override
+    public void addEventToPool(SchedulableEvent event) {
+        // Make sure the event is suitable for the pool.
+        if (event instanceof MidiEvent) {
+            MidiEvent midiEvent = (MidiEvent) event;
+            if (midiEvent.data.length == POOL_EVENT_SIZE) {
+                super.addEventToPool(event);
+            }
+        }
+    }
+
+    /**
+     * This MidiReceiver will write date to the scheduling buffer.
+     * @return the MidiReceiver
+     */
+    public MidiReceiver getReceiver() {
+        return mReceiver;
+    }
+
+}
diff --git a/com/android/internal/midi/MidiFramer.java b/com/android/internal/midi/MidiFramer.java
new file mode 100644
index 0000000..62517fa
--- /dev/null
+++ b/com/android/internal/midi/MidiFramer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 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.internal.midi;
+
+import android.media.midi.MidiReceiver;
+//import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Convert stream of bytes to discrete messages.
+ *
+ * Parses the incoming bytes and then posts individual messages to the receiver
+ * specified in the constructor. Short messages of 1-3 bytes will be complete.
+ * System Exclusive messages may be posted in pieces.
+ *
+ * Resolves Running Status and
+ * interleaved System Real-Time messages.
+ */
+public class MidiFramer extends MidiReceiver {
+
+    public String TAG = "MidiFramer";
+    private MidiReceiver mReceiver;
+    private byte[] mBuffer = new byte[3];
+    private int mCount;
+    private byte mRunningStatus;
+    private int mNeeded;
+    private boolean mInSysEx;
+
+    public MidiFramer(MidiReceiver receiver) {
+        mReceiver = receiver;
+    }
+
+    public static String formatMidiData(byte[] data, int offset, int count) {
+        String text = "MIDI+" + offset + " : ";
+        for (int i = 0; i < count; i++) {
+            text += String.format("0x%02X, ", data[offset + i]);
+        }
+        return text;
+    }
+
+    /*
+     * @see android.midi.MidiReceiver#onSend(byte[], int, int, long)
+     */
+    @Override
+    public void onSend(byte[] data, int offset, int count, long timestamp)
+            throws IOException {
+        int sysExStartOffset = (mInSysEx ? offset : -1);
+
+        for (int i = 0; i < count; i++) {
+            final byte currentByte = data[offset];
+            final int currentInt = currentByte & 0xFF;
+            if (currentInt >= 0x80) { // status byte?
+                if (currentInt < 0xF0) { // channel message?
+                    mRunningStatus = currentByte;
+                    mCount = 1;
+                    mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+                } else if (currentInt < 0xF8) { // system common?
+                    if (currentInt == 0xF0 /* SysEx Start */) {
+                        // Log.i(TAG, "SysEx Start");
+                        mInSysEx = true;
+                        sysExStartOffset = offset;
+                    } else if (currentInt == 0xF7 /* SysEx End */) {
+                        // Log.i(TAG, "SysEx End");
+                        if (mInSysEx) {
+                            mReceiver.send(data, sysExStartOffset,
+                                offset - sysExStartOffset + 1, timestamp);
+                            mInSysEx = false;
+                            sysExStartOffset = -1;
+                        }
+                    } else {
+                        mBuffer[0] = currentByte;
+                        mRunningStatus = 0;
+                        mCount = 1;
+                        mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+                    }
+                } else { // real-time?
+                    // Single byte message interleaved with other data.
+                    if (mInSysEx) {
+                        mReceiver.send(data, sysExStartOffset,
+                                offset - sysExStartOffset, timestamp);
+                        sysExStartOffset = offset + 1;
+                    }
+                    mReceiver.send(data, offset, 1, timestamp);
+                }
+            } else { // data byte
+                if (!mInSysEx) {
+                    mBuffer[mCount++] = currentByte;
+                    if (--mNeeded == 0) {
+                        if (mRunningStatus != 0) {
+                            mBuffer[0] = mRunningStatus;
+                        }
+                        mReceiver.send(mBuffer, 0, mCount, timestamp);
+                        mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1;
+                        mCount = 1;
+                    }
+                }
+            }
+            ++offset;
+        }
+
+        // send any accumulatedSysEx data
+        if (sysExStartOffset >= 0 && sysExStartOffset < offset) {
+            mReceiver.send(data, sysExStartOffset,
+                    offset - sysExStartOffset, timestamp);
+        }
+    }
+
+}
diff --git a/com/android/internal/ml/clustering/KMeans.java b/com/android/internal/ml/clustering/KMeans.java
new file mode 100644
index 0000000..4d5b333
--- /dev/null
+++ b/com/android/internal/ml/clustering/KMeans.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2017 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.internal.ml.clustering;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Simple K-Means implementation
+ */
+public class KMeans {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "KMeans";
+    private final Random mRandomState;
+    private final int mMaxIterations;
+    private float mSqConvergenceEpsilon;
+
+    public KMeans() {
+        this(new Random());
+    }
+
+    public KMeans(Random random) {
+        this(random, 30 /* maxIterations */, 0.005f /* convergenceEpsilon */);
+    }
+    public KMeans(Random random, int maxIterations, float convergenceEpsilon) {
+        mRandomState = random;
+        mMaxIterations = maxIterations;
+        mSqConvergenceEpsilon = convergenceEpsilon * convergenceEpsilon;
+    }
+
+    /**
+     * Runs k-means on the input data (X) trying to find k means.
+     *
+     * K-Means is known for getting stuck into local optima, so you might
+     * want to run it multiple time and argmax on {@link KMeans#score(List)}
+     *
+     * @param k The number of points to return.
+     * @param inputData Input data.
+     * @return An array of k Means, each representing a centroid and data points that belong to it.
+     */
+    public List<Mean> predict(final int k, final float[][] inputData) {
+        checkDataSetSanity(inputData);
+        int dimension = inputData[0].length;
+
+        final ArrayList<Mean> means = new ArrayList<>();
+        for (int i = 0; i < k; i++) {
+            Mean m = new Mean(dimension);
+            for (int j = 0; j < dimension; j++) {
+                m.mCentroid[j] = mRandomState.nextFloat();
+            }
+            means.add(m);
+        }
+
+        // Iterate until we converge or run out of iterations
+        boolean converged = false;
+        for (int i = 0; i < mMaxIterations; i++) {
+            converged = step(means, inputData);
+            if (converged) {
+                if (DEBUG) Log.d(TAG, "Converged at iteration: " + i);
+                break;
+            }
+        }
+        if (!converged && DEBUG) Log.d(TAG, "Did not converge");
+
+        return means;
+    }
+
+    /**
+     * Score calculates the inertia between means.
+     * This can be considered as an E step of an EM algorithm.
+     *
+     * @param means Means to use when calculating score.
+     * @return The score
+     */
+    public static double score(@NonNull List<Mean> means) {
+        double score = 0;
+        final int meansSize = means.size();
+        for (int i = 0; i < meansSize; i++) {
+            Mean mean = means.get(i);
+            for (int j = 0; j < meansSize; j++) {
+                Mean compareTo = means.get(j);
+                if (mean == compareTo) {
+                    continue;
+                }
+                double distance = Math.sqrt(sqDistance(mean.mCentroid, compareTo.mCentroid));
+                score += distance;
+            }
+        }
+        return score;
+    }
+
+    @VisibleForTesting
+    public void checkDataSetSanity(float[][] inputData) {
+        if (inputData == null) {
+            throw new IllegalArgumentException("Data set is null.");
+        } else if (inputData.length == 0) {
+            throw new IllegalArgumentException("Data set is empty.");
+        } else if (inputData[0] == null) {
+            throw new IllegalArgumentException("Bad data set format.");
+        }
+
+        final int dimension = inputData[0].length;
+        final int length = inputData.length;
+        for (int i = 1; i < length; i++) {
+            if (inputData[i] == null || inputData[i].length != dimension) {
+                throw new IllegalArgumentException("Bad data set format.");
+            }
+        }
+    }
+
+    /**
+     * K-Means iteration.
+     *
+     * @param means Current means
+     * @param inputData Input data
+     * @return True if data set converged
+     */
+    private boolean step(final ArrayList<Mean> means, final float[][] inputData) {
+
+        // Clean up the previous state because we need to compute
+        // which point belongs to each mean again.
+        for (int i = means.size() - 1; i >= 0; i--) {
+            final Mean mean = means.get(i);
+            mean.mClosestItems.clear();
+        }
+        for (int i = inputData.length - 1; i >= 0; i--) {
+            final float[] current = inputData[i];
+            final Mean nearest = nearestMean(current, means);
+            nearest.mClosestItems.add(current);
+        }
+
+        boolean converged = true;
+        // Move each mean towards the nearest data set points
+        for (int i = means.size() - 1; i >= 0; i--) {
+            final Mean mean = means.get(i);
+            if (mean.mClosestItems.size() == 0) {
+                continue;
+            }
+
+            // Compute the new mean centroid:
+            //   1. Sum all all points
+            //   2. Average them
+            final float[] oldCentroid = mean.mCentroid;
+            mean.mCentroid = new float[oldCentroid.length];
+            for (int j = 0; j < mean.mClosestItems.size(); j++) {
+                // Update each centroid component
+                for (int p = 0; p < mean.mCentroid.length; p++) {
+                    mean.mCentroid[p] += mean.mClosestItems.get(j)[p];
+                }
+            }
+            for (int j = 0; j < mean.mCentroid.length; j++) {
+                mean.mCentroid[j] /= mean.mClosestItems.size();
+            }
+
+            // We converged if the centroid didn't move for any of the means.
+            if (sqDistance(oldCentroid, mean.mCentroid) > mSqConvergenceEpsilon) {
+                converged = false;
+            }
+        }
+        return converged;
+    }
+
+    @VisibleForTesting
+    public static Mean nearestMean(float[] point, List<Mean> means) {
+        Mean nearest = null;
+        float nearestDistance = Float.MAX_VALUE;
+
+        final int meanCount = means.size();
+        for (int i = 0; i < meanCount; i++) {
+            Mean next = means.get(i);
+            // We don't need the sqrt when comparing distances in euclidean space
+            // because they exist on both sides of the equation and cancel each other out.
+            float nextDistance = sqDistance(point, next.mCentroid);
+            if (nextDistance < nearestDistance) {
+                nearest = next;
+                nearestDistance = nextDistance;
+            }
+        }
+        return nearest;
+    }
+
+    @VisibleForTesting
+    public static float sqDistance(float[] a, float[] b) {
+        float dist = 0;
+        final int length = a.length;
+        for (int i = 0; i < length; i++) {
+            dist += (a[i] - b[i]) * (a[i] - b[i]);
+        }
+        return dist;
+    }
+
+    /**
+     * Definition of a mean, contains a centroid and points on its cluster.
+     */
+    public static class Mean {
+        float[] mCentroid;
+        final ArrayList<float[]> mClosestItems = new ArrayList<>();
+
+        public Mean(int dimension) {
+            mCentroid = new float[dimension];
+        }
+
+        public Mean(float ...centroid) {
+            mCentroid = centroid;
+        }
+
+        public float[] getCentroid() {
+            return mCentroid;
+        }
+
+        public List<float[]> getItems() {
+            return mClosestItems;
+        }
+
+        @Override
+        public String toString() {
+            return "Mean(centroid: " + Arrays.toString(mCentroid) + ", size: "
+                    + mClosestItems.size() + ")";
+        }
+    }
+}
diff --git a/com/android/internal/net/LegacyVpnInfo.java b/com/android/internal/net/LegacyVpnInfo.java
new file mode 100644
index 0000000..d6f6d0b
--- /dev/null
+++ b/com/android/internal/net/LegacyVpnInfo.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2011 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.internal.net;
+
+import android.app.PendingIntent;
+import android.net.NetworkInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+/**
+ * A simple container used to carry information of the ongoing legacy VPN.
+ * Internal use only.
+ *
+ * @hide
+ */
+public class LegacyVpnInfo implements Parcelable {
+    private static final String TAG = "LegacyVpnInfo";
+
+    public static final int STATE_DISCONNECTED = 0;
+    public static final int STATE_INITIALIZING = 1;
+    public static final int STATE_CONNECTING = 2;
+    public static final int STATE_CONNECTED = 3;
+    public static final int STATE_TIMEOUT = 4;
+    public static final int STATE_FAILED = 5;
+
+    public String key;
+    public int state = -1;
+    public PendingIntent intent;
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(key);
+        out.writeInt(state);
+        out.writeParcelable(intent, flags);
+    }
+
+    public static final Parcelable.Creator<LegacyVpnInfo> CREATOR =
+            new Parcelable.Creator<LegacyVpnInfo>() {
+        @Override
+        public LegacyVpnInfo createFromParcel(Parcel in) {
+            LegacyVpnInfo info = new LegacyVpnInfo();
+            info.key = in.readString();
+            info.state = in.readInt();
+            info.intent = in.readParcelable(null);
+            return info;
+        }
+
+        @Override
+        public LegacyVpnInfo[] newArray(int size) {
+            return new LegacyVpnInfo[size];
+        }
+    };
+
+    /**
+     * Return best matching {@link LegacyVpnInfo} state based on given
+     * {@link NetworkInfo}.
+     */
+    public static int stateFromNetworkInfo(NetworkInfo info) {
+        switch (info.getDetailedState()) {
+            case CONNECTING:
+                return STATE_CONNECTING;
+            case CONNECTED:
+                return STATE_CONNECTED;
+            case DISCONNECTED:
+                return STATE_DISCONNECTED;
+            case FAILED:
+                return STATE_FAILED;
+            default:
+                Log.w(TAG, "Unhandled state " + info.getDetailedState()
+                        + " ; treating as disconnected");
+                return STATE_DISCONNECTED;
+        }
+    }
+}
diff --git a/com/android/internal/net/NetworkStatsFactory.java b/com/android/internal/net/NetworkStatsFactory.java
new file mode 100644
index 0000000..3d3e148
--- /dev/null
+++ b/com/android/internal/net/NetworkStatsFactory.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2011 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.internal.net;
+
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.TAG_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static com.android.server.NetworkManagementSocketTagger.kernelToTag;
+
+import android.net.NetworkStats;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.ProcFileReader;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Objects;
+
+/**
+ * Creates {@link NetworkStats} instances by parsing various {@code /proc/}
+ * files as needed.
+ */
+public class NetworkStatsFactory {
+    private static final String TAG = "NetworkStatsFactory";
+
+    private static final boolean USE_NATIVE_PARSING = true;
+    private static final boolean SANITY_CHECK_NATIVE = false;
+
+    private static final String CLATD_INTERFACE_PREFIX = "v4-";
+    // Delta between IPv4 header (20b) and IPv6 header (40b).
+    // Used for correct stats accounting on clatd interfaces.
+    private static final int IPV4V6_HEADER_DELTA = 20;
+
+    /** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */
+    private final File mStatsXtIfaceAll;
+    /** Path to {@code /proc/net/xt_qtaguid/iface_stat_fmt}. */
+    private final File mStatsXtIfaceFmt;
+    /** Path to {@code /proc/net/xt_qtaguid/stats}. */
+    private final File mStatsXtUid;
+
+    // TODO: to improve testability and avoid global state, do not use a static variable.
+    @GuardedBy("sStackedIfaces")
+    private static final ArrayMap<String, String> sStackedIfaces = new ArrayMap<>();
+
+    public static void noteStackedIface(String stackedIface, String baseIface) {
+        synchronized (sStackedIfaces) {
+            if (baseIface != null) {
+                sStackedIfaces.put(stackedIface, baseIface);
+            } else {
+                sStackedIfaces.remove(stackedIface);
+            }
+        }
+    }
+
+    public NetworkStatsFactory() {
+        this(new File("/proc/"));
+    }
+
+    @VisibleForTesting
+    public NetworkStatsFactory(File procRoot) {
+        mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
+        mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
+        mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
+    }
+
+    /**
+     * Parse and return interface-level summary {@link NetworkStats} measured
+     * using {@code /proc/net/dev} style hooks, which may include non IP layer
+     * traffic. Values monotonically increase since device boot, and may include
+     * details about inactive interfaces.
+     *
+     * @throws IllegalStateException when problem parsing stats.
+     */
+    public NetworkStats readNetworkStatsSummaryDev() throws IOException {
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+        ProcFileReader reader = null;
+        try {
+            reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceAll));
+
+            while (reader.hasMoreData()) {
+                entry.iface = reader.nextString();
+                entry.uid = UID_ALL;
+                entry.set = SET_ALL;
+                entry.tag = TAG_NONE;
+
+                final boolean active = reader.nextInt() != 0;
+
+                // always include snapshot values
+                entry.rxBytes = reader.nextLong();
+                entry.rxPackets = reader.nextLong();
+                entry.txBytes = reader.nextLong();
+                entry.txPackets = reader.nextLong();
+
+                // fold in active numbers, but only when active
+                if (active) {
+                    entry.rxBytes += reader.nextLong();
+                    entry.rxPackets += reader.nextLong();
+                    entry.txBytes += reader.nextLong();
+                    entry.txPackets += reader.nextLong();
+                }
+
+                stats.addValues(entry);
+                reader.finishLine();
+            }
+        } catch (NullPointerException|NumberFormatException e) {
+            throw new ProtocolException("problem parsing stats", e);
+        } finally {
+            IoUtils.closeQuietly(reader);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+        return stats;
+    }
+
+    /**
+     * Parse and return interface-level summary {@link NetworkStats}. Designed
+     * to return only IP layer traffic. Values monotonically increase since
+     * device boot, and may include details about inactive interfaces.
+     *
+     * @throws IllegalStateException when problem parsing stats.
+     */
+    public NetworkStats readNetworkStatsSummaryXt() throws IOException {
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        // return null when kernel doesn't support
+        if (!mStatsXtIfaceFmt.exists()) return null;
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+        ProcFileReader reader = null;
+        try {
+            // open and consume header line
+            reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceFmt));
+            reader.finishLine();
+
+            while (reader.hasMoreData()) {
+                entry.iface = reader.nextString();
+                entry.uid = UID_ALL;
+                entry.set = SET_ALL;
+                entry.tag = TAG_NONE;
+
+                entry.rxBytes = reader.nextLong();
+                entry.rxPackets = reader.nextLong();
+                entry.txBytes = reader.nextLong();
+                entry.txPackets = reader.nextLong();
+
+                stats.addValues(entry);
+                reader.finishLine();
+            }
+        } catch (NullPointerException|NumberFormatException e) {
+            throw new ProtocolException("problem parsing stats", e);
+        } finally {
+            IoUtils.closeQuietly(reader);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+        return stats;
+    }
+
+    public NetworkStats readNetworkStatsDetail() throws IOException {
+        return readNetworkStatsDetail(UID_ALL, null, TAG_ALL, null);
+    }
+
+    public NetworkStats readNetworkStatsDetail(int limitUid, String[] limitIfaces, int limitTag,
+            NetworkStats lastStats) throws IOException {
+        final NetworkStats stats =
+              readNetworkStatsDetailInternal(limitUid, limitIfaces, limitTag, lastStats);
+        final ArrayMap<String, String> stackedIfaces;
+        synchronized (sStackedIfaces) {
+            stackedIfaces = new ArrayMap<>(sStackedIfaces);
+        }
+        // Total 464xlat traffic to subtract from uid 0 on all base interfaces.
+        final NetworkStats adjustments = new NetworkStats(0, stackedIfaces.size());
+
+        NetworkStats.Entry entry = null; // For recycling
+
+        // For 464xlat traffic, xt_qtaguid sees every IPv4 packet twice, once as a native IPv4
+        // packet on the stacked interface, and once as translated to an IPv6 packet on the
+        // base interface. For correct stats accounting on the base interface, every 464xlat
+        // packet needs to be subtracted from the root UID on the base interface both for tx
+        // and rx traffic (http://b/12249687, http:/b/33681750).
+        for (int i = 0; i < stats.size(); i++) {
+            entry = stats.getValues(i, entry);
+            if (entry.iface == null || !entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) {
+                continue;
+            }
+            final String baseIface = stackedIfaces.get(entry.iface);
+            if (baseIface == null) {
+                continue;
+            }
+
+            NetworkStats.Entry adjust =
+                    new NetworkStats.Entry(baseIface, 0, 0, 0, 0L, 0L, 0L, 0L, 0L);
+            // Subtract any 464lat traffic seen for the root UID on the current base interface.
+            adjust.rxBytes -= (entry.rxBytes + entry.rxPackets * IPV4V6_HEADER_DELTA);
+            adjust.txBytes -= (entry.txBytes + entry.txPackets * IPV4V6_HEADER_DELTA);
+            adjust.rxPackets -= entry.rxPackets;
+            adjust.txPackets -= entry.txPackets;
+            adjustments.combineValues(adjust);
+
+            // For 464xlat traffic, xt_qtaguid only counts the bytes of the native IPv4 packet sent
+            // on the stacked interface with prefix "v4-" and drops the IPv6 header size after
+            // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes
+            // difference for all packets (http://b/12249687, http:/b/33681750).
+            entry.rxBytes = entry.rxPackets * IPV4V6_HEADER_DELTA;
+            entry.txBytes = entry.txPackets * IPV4V6_HEADER_DELTA;
+            entry.rxPackets = 0;
+            entry.txPackets = 0;
+            stats.combineValues(entry);
+        }
+
+        stats.combineAllValues(adjustments);
+
+        return stats;
+    }
+
+    private NetworkStats readNetworkStatsDetailInternal(int limitUid, String[] limitIfaces,
+            int limitTag, NetworkStats lastStats) throws IOException {
+        if (USE_NATIVE_PARSING) {
+            final NetworkStats stats;
+            if (lastStats != null) {
+                stats = lastStats;
+                stats.setElapsedRealtime(SystemClock.elapsedRealtime());
+            } else {
+                stats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
+            }
+            if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), limitUid,
+                    limitIfaces, limitTag) != 0) {
+                throw new IOException("Failed to parse network stats");
+            }
+            if (SANITY_CHECK_NATIVE) {
+                final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid, limitUid,
+                        limitIfaces, limitTag);
+                assertEquals(javaStats, stats);
+            }
+            return stats;
+        } else {
+            return javaReadNetworkStatsDetail(mStatsXtUid, limitUid, limitIfaces, limitTag);
+        }
+    }
+
+    /**
+     * Parse and return {@link NetworkStats} with UID-level details. Values are
+     * expected to monotonically increase since device boot.
+     */
+    @VisibleForTesting
+    public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid,
+            String[] limitIfaces, int limitTag)
+            throws IOException {
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+        int idx = 1;
+        int lastIdx = 1;
+
+        ProcFileReader reader = null;
+        try {
+            // open and consume header line
+            reader = new ProcFileReader(new FileInputStream(detailPath));
+            reader.finishLine();
+
+            while (reader.hasMoreData()) {
+                idx = reader.nextInt();
+                if (idx != lastIdx + 1) {
+                    throw new ProtocolException(
+                            "inconsistent idx=" + idx + " after lastIdx=" + lastIdx);
+                }
+                lastIdx = idx;
+
+                entry.iface = reader.nextString();
+                entry.tag = kernelToTag(reader.nextString());
+                entry.uid = reader.nextInt();
+                entry.set = reader.nextInt();
+                entry.rxBytes = reader.nextLong();
+                entry.rxPackets = reader.nextLong();
+                entry.txBytes = reader.nextLong();
+                entry.txPackets = reader.nextLong();
+
+                if ((limitIfaces == null || ArrayUtils.contains(limitIfaces, entry.iface))
+                        && (limitUid == UID_ALL || limitUid == entry.uid)
+                        && (limitTag == TAG_ALL || limitTag == entry.tag)) {
+                    stats.addValues(entry);
+                }
+
+                reader.finishLine();
+            }
+        } catch (NullPointerException|NumberFormatException e) {
+            throw new ProtocolException("problem parsing idx " + idx, e);
+        } finally {
+            IoUtils.closeQuietly(reader);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+
+        return stats;
+    }
+
+    public void assertEquals(NetworkStats expected, NetworkStats actual) {
+        if (expected.size() != actual.size()) {
+            throw new AssertionError(
+                    "Expected size " + expected.size() + ", actual size " + actual.size());
+        }
+
+        NetworkStats.Entry expectedRow = null;
+        NetworkStats.Entry actualRow = null;
+        for (int i = 0; i < expected.size(); i++) {
+            expectedRow = expected.getValues(i, expectedRow);
+            actualRow = actual.getValues(i, actualRow);
+            if (!expectedRow.equals(actualRow)) {
+                throw new AssertionError(
+                        "Expected row " + i + ": " + expectedRow + ", actual row " + actualRow);
+            }
+        }
+    }
+
+    /**
+     * Parse statistics from file into given {@link NetworkStats} object. Values
+     * are expected to monotonically increase since device boot.
+     */
+    @VisibleForTesting
+    public static native int nativeReadNetworkStatsDetail(
+            NetworkStats stats, String path, int limitUid, String[] limitIfaces, int limitTag);
+}
diff --git a/com/android/internal/net/VpnConfig.java b/com/android/internal/net/VpnConfig.java
new file mode 100644
index 0000000..921f1fe
--- /dev/null
+++ b/com/android/internal/net/VpnConfig.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2011 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.internal.net;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.RouteInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A simple container used to carry information in VpnBuilder, VpnDialogs,
+ * and com.android.server.connectivity.Vpn. Internal use only.
+ *
+ * @hide
+ */
+public class VpnConfig implements Parcelable {
+
+    public static final String SERVICE_INTERFACE = "android.net.VpnService";
+
+    public static final String DIALOGS_PACKAGE = "com.android.vpndialogs";
+
+    public static final String LEGACY_VPN = "[Legacy VPN]";
+
+    public static Intent getIntentForConfirmation() {
+        Intent intent = new Intent();
+        ComponentName componentName = ComponentName.unflattenFromString(
+                Resources.getSystem().getString(
+                        com.android.internal.R.string.config_customVpnConfirmDialogComponent));
+        intent.setClassName(componentName.getPackageName(), componentName.getClassName());
+        return intent;
+    }
+
+    /** NOTE: This should only be used for legacy VPN. */
+    public static PendingIntent getIntentForStatusPanel(Context context) {
+        Intent intent = new Intent();
+        intent.setClassName(DIALOGS_PACKAGE, DIALOGS_PACKAGE + ".ManageDialog");
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_HISTORY |
+                Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        return PendingIntent.getActivityAsUser(context, 0, intent, 0, null, UserHandle.CURRENT);
+    }
+
+    public static CharSequence getVpnLabel(Context context, String packageName)
+            throws NameNotFoundException {
+        PackageManager pm = context.getPackageManager();
+        Intent intent = new Intent(SERVICE_INTERFACE);
+        intent.setPackage(packageName);
+        List<ResolveInfo> services = pm.queryIntentServices(intent, 0 /* flags */);
+        if (services != null && services.size() == 1) {
+            // This app contains exactly one VPN service. Call loadLabel, which will attempt to
+            // load the service's label, and fall back to the app label if none is present.
+            return services.get(0).loadLabel(pm);
+        } else {
+            return pm.getApplicationInfo(packageName, 0).loadLabel(pm);
+        }
+    }
+
+    public String user;
+    public String interfaze;
+    public String session;
+    public int mtu = -1;
+    public List<LinkAddress> addresses = new ArrayList<LinkAddress>();
+    public List<RouteInfo> routes = new ArrayList<RouteInfo>();
+    public List<String> dnsServers;
+    public List<String> searchDomains;
+    public List<String> allowedApplications;
+    public List<String> disallowedApplications;
+    public PendingIntent configureIntent;
+    public long startTime = -1;
+    public boolean legacy;
+    public boolean blocking;
+    public boolean allowBypass;
+    public boolean allowIPv4;
+    public boolean allowIPv6;
+    public Network[] underlyingNetworks;
+
+    public void updateAllowedFamilies(InetAddress address) {
+        if (address instanceof Inet4Address) {
+            allowIPv4 = true;
+        } else {
+            allowIPv6 = true;
+        }
+    }
+
+    public void addLegacyRoutes(String routesStr) {
+        if (routesStr.trim().equals("")) {
+            return;
+        }
+        String[] routes = routesStr.trim().split(" ");
+        for (String route : routes) {
+            //each route is ip/prefix
+            RouteInfo info = new RouteInfo(new IpPrefix(route), null);
+            this.routes.add(info);
+            updateAllowedFamilies(info.getDestination().getAddress());
+        }
+    }
+
+    public void addLegacyAddresses(String addressesStr) {
+        if (addressesStr.trim().equals("")) {
+            return;
+        }
+        String[] addresses = addressesStr.trim().split(" ");
+        for (String address : addresses) {
+            //each address is ip/prefix
+            LinkAddress addr = new LinkAddress(address);
+            this.addresses.add(addr);
+            updateAllowedFamilies(addr.getAddress());
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(user);
+        out.writeString(interfaze);
+        out.writeString(session);
+        out.writeInt(mtu);
+        out.writeTypedList(addresses);
+        out.writeTypedList(routes);
+        out.writeStringList(dnsServers);
+        out.writeStringList(searchDomains);
+        out.writeStringList(allowedApplications);
+        out.writeStringList(disallowedApplications);
+        out.writeParcelable(configureIntent, flags);
+        out.writeLong(startTime);
+        out.writeInt(legacy ? 1 : 0);
+        out.writeInt(blocking ? 1 : 0);
+        out.writeInt(allowBypass ? 1 : 0);
+        out.writeInt(allowIPv4 ? 1 : 0);
+        out.writeInt(allowIPv6 ? 1 : 0);
+        out.writeTypedArray(underlyingNetworks, flags);
+    }
+
+    public static final Parcelable.Creator<VpnConfig> CREATOR =
+            new Parcelable.Creator<VpnConfig>() {
+        @Override
+        public VpnConfig createFromParcel(Parcel in) {
+            VpnConfig config = new VpnConfig();
+            config.user = in.readString();
+            config.interfaze = in.readString();
+            config.session = in.readString();
+            config.mtu = in.readInt();
+            in.readTypedList(config.addresses, LinkAddress.CREATOR);
+            in.readTypedList(config.routes, RouteInfo.CREATOR);
+            config.dnsServers = in.createStringArrayList();
+            config.searchDomains = in.createStringArrayList();
+            config.allowedApplications = in.createStringArrayList();
+            config.disallowedApplications = in.createStringArrayList();
+            config.configureIntent = in.readParcelable(null);
+            config.startTime = in.readLong();
+            config.legacy = in.readInt() != 0;
+            config.blocking = in.readInt() != 0;
+            config.allowBypass = in.readInt() != 0;
+            config.allowIPv4 = in.readInt() != 0;
+            config.allowIPv6 = in.readInt() != 0;
+            config.underlyingNetworks = in.createTypedArray(Network.CREATOR);
+            return config;
+        }
+
+        @Override
+        public VpnConfig[] newArray(int size) {
+            return new VpnConfig[size];
+        }
+    };
+}
diff --git a/com/android/internal/net/VpnInfo.java b/com/android/internal/net/VpnInfo.java
new file mode 100644
index 0000000..a676dac
--- /dev/null
+++ b/com/android/internal/net/VpnInfo.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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.internal.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A lightweight container used to carry information of the ongoing VPN.
+ * Internal use only..
+ *
+ * @hide
+ */
+public class VpnInfo implements Parcelable {
+    public int ownerUid;
+    public String vpnIface;
+    public String primaryUnderlyingIface;
+
+    @Override
+    public String toString() {
+        return "VpnInfo{" +
+                "ownerUid=" + ownerUid +
+                ", vpnIface='" + vpnIface + '\'' +
+                ", primaryUnderlyingIface='" + primaryUnderlyingIface + '\'' +
+                '}';
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(ownerUid);
+        dest.writeString(vpnIface);
+        dest.writeString(primaryUnderlyingIface);
+    }
+
+    public static final Parcelable.Creator<VpnInfo> CREATOR = new Parcelable.Creator<VpnInfo>() {
+        @Override
+        public VpnInfo createFromParcel(Parcel source) {
+            VpnInfo info = new VpnInfo();
+            info.ownerUid = source.readInt();
+            info.vpnIface = source.readString();
+            info.primaryUnderlyingIface = source.readString();
+            return info;
+        }
+
+        @Override
+        public VpnInfo[] newArray(int size) {
+            return new VpnInfo[size];
+        }
+    };
+}
diff --git a/com/android/internal/net/VpnProfile.java b/com/android/internal/net/VpnProfile.java
new file mode 100644
index 0000000..b46bfef
--- /dev/null
+++ b/com/android/internal/net/VpnProfile.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2011 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.internal.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Parcel-like entity class for VPN profiles. To keep things simple, all
+ * fields are package private. Methods are provided for serialization, so
+ * storage can be implemented easily. Two rules are set for this class.
+ * First, all fields must be kept non-null. Second, always make a copy
+ * using clone() before modifying.
+ *
+ * @hide
+ */
+public class VpnProfile implements Cloneable, Parcelable {
+    private static final String TAG = "VpnProfile";
+
+    // Match these constants with R.array.vpn_types.
+    public static final int TYPE_PPTP = 0;
+    public static final int TYPE_L2TP_IPSEC_PSK = 1;
+    public static final int TYPE_L2TP_IPSEC_RSA = 2;
+    public static final int TYPE_IPSEC_XAUTH_PSK = 3;
+    public static final int TYPE_IPSEC_XAUTH_RSA = 4;
+    public static final int TYPE_IPSEC_HYBRID_RSA = 5;
+    public static final int TYPE_MAX = 5;
+
+    // Entity fields.
+    public final String key;           // -1
+    public String name = "";           // 0
+    public int type = TYPE_PPTP;       // 1
+    public String server = "";         // 2
+    public String username = "";       // 3
+    public String password = "";       // 4
+    public String dnsServers = "";     // 5
+    public String searchDomains = "";  // 6
+    public String routes = "";         // 7
+    public boolean mppe = true;        // 8
+    public String l2tpSecret = "";     // 9
+    public String ipsecIdentifier = "";// 10
+    public String ipsecSecret = "";    // 11
+    public String ipsecUserCert = "";  // 12
+    public String ipsecCaCert = "";    // 13
+    public String ipsecServerCert = "";// 14
+
+    // Helper fields.
+    public boolean saveLogin = false;
+
+    public VpnProfile(String key) {
+        this.key = key;
+    }
+
+    public VpnProfile(Parcel in) {
+        key = in.readString();
+        name = in.readString();
+        type = in.readInt();
+        server = in.readString();
+        username = in.readString();
+        password = in.readString();
+        dnsServers = in.readString();
+        searchDomains = in.readString();
+        routes = in.readString();
+        mppe = in.readInt() != 0;
+        l2tpSecret = in.readString();
+        ipsecIdentifier = in.readString();
+        ipsecSecret = in.readString();
+        ipsecUserCert = in.readString();
+        ipsecCaCert = in.readString();
+        ipsecServerCert = in.readString();
+        saveLogin = in.readInt() != 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(key);
+        out.writeString(name);
+        out.writeInt(type);
+        out.writeString(server);
+        out.writeString(username);
+        out.writeString(password);
+        out.writeString(dnsServers);
+        out.writeString(searchDomains);
+        out.writeString(routes);
+        out.writeInt(mppe ? 1 : 0);
+        out.writeString(l2tpSecret);
+        out.writeString(ipsecIdentifier);
+        out.writeString(ipsecSecret);
+        out.writeString(ipsecUserCert);
+        out.writeString(ipsecCaCert);
+        out.writeString(ipsecServerCert);
+        out.writeInt(saveLogin ? 1 : 0);
+    }
+
+    public static VpnProfile decode(String key, byte[] value) {
+        try {
+            if (key == null) {
+                return null;
+            }
+
+            String[] values = new String(value, StandardCharsets.UTF_8).split("\0", -1);
+            // There can be 14 or 15 values in ICS MR1.
+            if (values.length < 14 || values.length > 15) {
+                return null;
+            }
+
+            VpnProfile profile = new VpnProfile(key);
+            profile.name = values[0];
+            profile.type = Integer.parseInt(values[1]);
+            if (profile.type < 0 || profile.type > TYPE_MAX) {
+                return null;
+            }
+            profile.server = values[2];
+            profile.username = values[3];
+            profile.password = values[4];
+            profile.dnsServers = values[5];
+            profile.searchDomains = values[6];
+            profile.routes = values[7];
+            profile.mppe = Boolean.parseBoolean(values[8]);
+            profile.l2tpSecret = values[9];
+            profile.ipsecIdentifier = values[10];
+            profile.ipsecSecret = values[11];
+            profile.ipsecUserCert = values[12];
+            profile.ipsecCaCert = values[13];
+            profile.ipsecServerCert = (values.length > 14) ? values[14] : "";
+
+            profile.saveLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
+            return profile;
+        } catch (Exception e) {
+            // ignore
+        }
+        return null;
+    }
+
+    public byte[] encode() {
+        StringBuilder builder = new StringBuilder(name);
+        builder.append('\0').append(type);
+        builder.append('\0').append(server);
+        builder.append('\0').append(saveLogin ? username : "");
+        builder.append('\0').append(saveLogin ? password : "");
+        builder.append('\0').append(dnsServers);
+        builder.append('\0').append(searchDomains);
+        builder.append('\0').append(routes);
+        builder.append('\0').append(mppe);
+        builder.append('\0').append(l2tpSecret);
+        builder.append('\0').append(ipsecIdentifier);
+        builder.append('\0').append(ipsecSecret);
+        builder.append('\0').append(ipsecUserCert);
+        builder.append('\0').append(ipsecCaCert);
+        builder.append('\0').append(ipsecServerCert);
+        return builder.toString().getBytes(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Tests if profile is valid for lockdown, which requires IPv4 address for
+     * both server and DNS. Server hostnames would require using DNS before
+     * connection.
+     */
+    public boolean isValidLockdownProfile() {
+        return isTypeValidForLockdown()
+                && isServerAddressNumeric()
+                && hasDns()
+                && areDnsAddressesNumeric();
+    }
+
+    /** Returns {@code true} if the VPN type is valid for lockdown. */
+    public boolean isTypeValidForLockdown() {
+        // b/7064069: lockdown firewall blocks ports used for PPTP
+        return type != TYPE_PPTP;
+    }
+
+    /** Returns {@code true} if the server address is numeric, e.g. 8.8.8.8 */
+    public boolean isServerAddressNumeric() {
+        try {
+            InetAddress.parseNumericAddress(server);
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /** Returns {@code true} if one or more DNS servers are specified. */
+    public boolean hasDns() {
+        return !TextUtils.isEmpty(dnsServers);
+    }
+
+    /**
+     * Returns {@code true} if all DNS servers have numeric addresses,
+     * e.g. 8.8.8.8
+     */
+    public boolean areDnsAddressesNumeric() {
+        try {
+            for (String dnsServer : dnsServers.split(" +")) {
+                InetAddress.parseNumericAddress(dnsServer);
+            }
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+        return true;
+    }
+
+    public static final Creator<VpnProfile> CREATOR = new Creator<VpnProfile>() {
+        @Override
+        public VpnProfile createFromParcel(Parcel in) {
+            return new VpnProfile(in);
+        }
+
+        @Override
+        public VpnProfile[] newArray(int size) {
+            return new VpnProfile[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java b/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java
new file mode 100644
index 0000000..4ce6f60
--- /dev/null
+++ b/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 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.internal.notification;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+public final class NotificationAccessConfirmationActivityContract {
+    private static final ComponentName COMPONENT_NAME = new ComponentName(
+            "com.android.settings",
+            "com.android.settings.notification.NotificationAccessConfirmationActivity");
+    public static final String EXTRA_USER_ID = "user_id";
+    public static final String EXTRA_COMPONENT_NAME = "component_name";
+    public static final String EXTRA_PACKAGE_TITLE = "package_title";
+
+    public static Intent launcherIntent(int userId, ComponentName component, String packageTitle) {
+        return new Intent()
+                .setComponent(COMPONENT_NAME)
+                .putExtra(EXTRA_USER_ID, userId)
+                .putExtra(EXTRA_COMPONENT_NAME, component)
+                .putExtra(EXTRA_PACKAGE_TITLE, packageTitle);
+    }
+}
diff --git a/com/android/internal/notification/SystemNotificationChannels.java b/com/android/internal/notification/SystemNotificationChannels.java
new file mode 100644
index 0000000..d64c9a1
--- /dev/null
+++ b/com/android/internal/notification/SystemNotificationChannels.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2017 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.internal.notification;
+
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.RemoteException;
+import android.provider.Settings;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+// Manages the NotificationChannels used by the frameworks itself.
+public class SystemNotificationChannels {
+    public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
+    public static String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD";
+    public static String SECURITY = "SECURITY";
+    public static String CAR_MODE = "CAR_MODE";
+    public static String ACCOUNT = "ACCOUNT";
+    public static String DEVELOPER = "DEVELOPER";
+    public static String UPDATES = "UPDATES";
+    public static String NETWORK_STATUS = "NETWORK_STATUS";
+    public static String NETWORK_ALERTS = "NETWORK_ALERTS";
+    public static String NETWORK_AVAILABLE = "NETWORK_AVAILABLE";
+    public static String VPN = "VPN";
+    public static String DEVICE_ADMIN = "DEVICE_ADMIN";
+    public static String ALERTS = "ALERTS";
+    public static String RETAIL_MODE = "RETAIL_MODE";
+    public static String USB = "USB";
+    public static String FOREGROUND_SERVICE = "FOREGROUND_SERVICE";
+
+    public static void createAll(Context context) {
+        final NotificationManager nm = context.getSystemService(NotificationManager.class);
+        List<NotificationChannel> channelsList = new ArrayList<NotificationChannel>();
+        channelsList.add(new NotificationChannel(
+                VIRTUAL_KEYBOARD,
+                context.getString(R.string.notification_channel_virtual_keyboard),
+                NotificationManager.IMPORTANCE_LOW));
+
+        final NotificationChannel physicalKeyboardChannel = new NotificationChannel(
+                PHYSICAL_KEYBOARD,
+                context.getString(R.string.notification_channel_physical_keyboard),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        physicalKeyboardChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+                Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        channelsList.add(physicalKeyboardChannel);
+
+        channelsList.add(new NotificationChannel(
+                SECURITY,
+                context.getString(R.string.notification_channel_security),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(new NotificationChannel(
+                CAR_MODE,
+                context.getString(R.string.notification_channel_car_mode),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(newAccountChannel(context));
+
+        channelsList.add(new NotificationChannel(
+                DEVELOPER,
+                context.getString(R.string.notification_channel_developer),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(new NotificationChannel(
+                UPDATES,
+                context.getString(R.string.notification_channel_updates),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(new NotificationChannel(
+                NETWORK_STATUS,
+                context.getString(R.string.notification_channel_network_status),
+                NotificationManager.IMPORTANCE_LOW));
+
+        final NotificationChannel networkAlertsChannel = new NotificationChannel(
+                NETWORK_ALERTS,
+                context.getString(R.string.notification_channel_network_alerts),
+                NotificationManager.IMPORTANCE_HIGH);
+        networkAlertsChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+                Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        channelsList.add(networkAlertsChannel);
+
+        channelsList.add(new NotificationChannel(
+                NETWORK_AVAILABLE,
+                context.getString(R.string.notification_channel_network_available),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(new NotificationChannel(
+                VPN,
+                context.getString(R.string.notification_channel_vpn),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(new NotificationChannel(
+                DEVICE_ADMIN,
+                context.getString(R.string.notification_channel_device_admin),
+                NotificationManager.IMPORTANCE_LOW));
+
+        final NotificationChannel alertsChannel = new NotificationChannel(
+                ALERTS,
+                context.getString(R.string.notification_channel_alerts),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        alertsChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+                Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        channelsList.add(alertsChannel);
+
+        channelsList.add(new NotificationChannel(
+                RETAIL_MODE,
+                context.getString(R.string.notification_channel_retail_mode),
+                NotificationManager.IMPORTANCE_LOW));
+
+        channelsList.add(new NotificationChannel(
+                USB,
+                context.getString(R.string.notification_channel_usb),
+                NotificationManager.IMPORTANCE_MIN));
+
+        NotificationChannel foregroundChannel = new NotificationChannel(
+                FOREGROUND_SERVICE,
+                context.getString(R.string.notification_channel_foreground_service),
+                NotificationManager.IMPORTANCE_LOW);
+        foregroundChannel.setBlockableSystem(true);
+        channelsList.add(foregroundChannel);
+
+        nm.createNotificationChannels(channelsList);
+    }
+
+    public static void createAccountChannelForPackage(String pkg, int uid, Context context) {
+        final INotificationManager iNotificationManager = NotificationManager.getService();
+        try {
+            iNotificationManager.createNotificationChannelsForPackage(pkg, uid,
+                    new ParceledListSlice(Arrays.asList(newAccountChannel(context))));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static NotificationChannel newAccountChannel(Context context) {
+        return new NotificationChannel(
+                ACCOUNT,
+                context.getString(R.string.notification_channel_account),
+                NotificationManager.IMPORTANCE_LOW);
+    }
+
+    private SystemNotificationChannels() {}
+}
diff --git a/com/android/internal/os/AndroidPrintStream.java b/com/android/internal/os/AndroidPrintStream.java
new file mode 100644
index 0000000..7f4807a
--- /dev/null
+++ b/com/android/internal/os/AndroidPrintStream.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2008 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.internal.os;
+
+import android.util.Log;
+
+/**
+ * Print stream which log lines using Android's logging system.
+ *
+ * {@hide}
+ */
+class AndroidPrintStream extends LoggingPrintStream {
+
+    private final int priority;
+    private final String tag;
+
+    /**
+     * Constructs a new logging print stream.
+     *
+     * @param priority from {@link android.util.Log}
+     * @param tag to log
+     */
+    public AndroidPrintStream(int priority, String tag) {
+        if (tag == null) {
+            throw new NullPointerException("tag");
+        }
+
+        this.priority = priority;
+        this.tag = tag;
+    }
+
+    protected void log(String line) {
+        Log.println(priority, tag, line);
+    }
+}
diff --git a/com/android/internal/os/AppFuseMount.java b/com/android/internal/os/AppFuseMount.java
new file mode 100644
index 0000000..04d7211
--- /dev/null
+++ b/com/android/internal/os/AppFuseMount.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 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.internal.os;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.storage.IStorageManager;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Parcelable class representing AppFuse mount.
+ * This conveys the result for IStorageManager#openProxyFileDescriptor.
+ * @see IStorageManager#openProxyFileDescriptor
+ */
+public class AppFuseMount implements Parcelable {
+    final public int mountPointId;
+    final public ParcelFileDescriptor fd;
+
+    /**
+     * @param mountPointId Integer number for mount point that is unique in the lifetime of
+     *     StorageManagerService.
+     * @param fd File descriptor pointing /dev/fuse and tagged with the mount point.
+     */
+    public AppFuseMount(int mountPointId, ParcelFileDescriptor fd) {
+        Preconditions.checkNotNull(fd);
+        this.mountPointId = mountPointId;
+        this.fd = fd;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(this.mountPointId);
+        dest.writeParcelable(fd, flags);
+    }
+
+    public static final Parcelable.Creator<AppFuseMount> CREATOR =
+            new Parcelable.Creator<AppFuseMount>() {
+        @Override
+        public AppFuseMount createFromParcel(Parcel in) {
+            return new AppFuseMount(in.readInt(), in.readParcelable(null));
+        }
+
+        @Override
+        public AppFuseMount[] newArray(int size) {
+            return new AppFuseMount[size];
+        }
+    };
+}
diff --git a/com/android/internal/os/AtomicFile.java b/com/android/internal/os/AtomicFile.java
new file mode 100644
index 0000000..5a83f33
--- /dev/null
+++ b/com/android/internal/os/AtomicFile.java
@@ -0,0 +1,177 @@
+/*
+ * 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.internal.os;
+
+import android.os.FileUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Helper class for performing atomic operations on a file, by creating a
+ * backup file until a write has successfully completed.
+ * <p>
+ * Atomic file guarantees file integrity by ensuring that a file has
+ * been completely written and sync'd to disk before removing its backup.
+ * As long as the backup file exists, the original file is considered
+ * to be invalid (left over from a previous attempt to write the file).
+ * </p><p>
+ * Atomic file does not confer any file locking semantics.
+ * Do not use this class when the file may be accessed or modified concurrently
+ * by multiple threads or processes.  The caller is responsible for ensuring
+ * appropriate mutual exclusion invariants whenever it accesses the file.
+ * </p>
+ */
+public final class AtomicFile {
+    private final File mBaseName;
+    private final File mBackupName;
+    
+    public AtomicFile(File baseName) {
+        mBaseName = baseName;
+        mBackupName = new File(baseName.getPath() + ".bak");
+    }
+    
+    public File getBaseFile() {
+        return mBaseName;
+    }
+    
+    public FileOutputStream startWrite() throws IOException {
+        // Rename the current file so it may be used as a backup during the next read
+        if (mBaseName.exists()) {
+            if (!mBackupName.exists()) {
+                if (!mBaseName.renameTo(mBackupName)) {
+                    Log.w("AtomicFile", "Couldn't rename file " + mBaseName
+                            + " to backup file " + mBackupName);
+                }
+            } else {
+                mBaseName.delete();
+            }
+        }
+        FileOutputStream str = null;
+        try {
+            str = new FileOutputStream(mBaseName);
+        } catch (FileNotFoundException e) {
+            File parent = mBaseName.getParentFile();
+            if (!parent.mkdir()) {
+                throw new IOException("Couldn't create directory " + mBaseName);
+            }
+            FileUtils.setPermissions(
+                parent.getPath(),
+                FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+                -1, -1);
+            try {
+                str = new FileOutputStream(mBaseName);
+            } catch (FileNotFoundException e2) {
+                throw new IOException("Couldn't create " + mBaseName);
+            }
+        }
+        return str;
+    }
+    
+    public void finishWrite(FileOutputStream str) {
+        if (str != null) {
+            FileUtils.sync(str);
+            try {
+                str.close();
+                mBackupName.delete();
+            } catch (IOException e) {
+                Log.w("AtomicFile", "finishWrite: Got exception:", e);
+            }
+        }
+    }
+    
+    public void failWrite(FileOutputStream str) {
+        if (str != null) {
+            FileUtils.sync(str);
+            try {
+                str.close();
+                mBaseName.delete();
+                mBackupName.renameTo(mBaseName);
+            } catch (IOException e) {
+                Log.w("AtomicFile", "failWrite: Got exception:", e);
+            }
+        }
+    }
+    
+    public FileOutputStream openAppend() throws IOException {
+        try {
+            return new FileOutputStream(mBaseName, true);
+        } catch (FileNotFoundException e) {
+            throw new IOException("Couldn't append " + mBaseName);
+        }
+    }
+    
+    public void truncate() throws IOException {
+        try {
+            FileOutputStream fos = new FileOutputStream(mBaseName);
+            FileUtils.sync(fos);
+            fos.close();
+        } catch (FileNotFoundException e) {
+            throw new IOException("Couldn't append " + mBaseName);
+        } catch (IOException e) {
+        }
+    }
+
+    public boolean exists() {
+        return mBaseName.exists() || mBackupName.exists();
+    }
+
+    public void delete() {
+        mBaseName.delete();
+        mBackupName.delete();
+    }
+
+    public FileInputStream openRead() throws FileNotFoundException {
+        if (mBackupName.exists()) {
+            mBaseName.delete();
+            mBackupName.renameTo(mBaseName);
+        }
+        return new FileInputStream(mBaseName);
+    }
+    
+    public byte[] readFully() throws IOException {
+        FileInputStream stream = openRead();
+        try {
+            int pos = 0;
+            int avail = stream.available();
+            byte[] data = new byte[avail];
+            while (true) {
+                int amt = stream.read(data, pos, data.length-pos);
+                //Log.i("foo", "Read " + amt + " bytes at " + pos
+                //        + " of avail " + data.length);
+                if (amt <= 0) {
+                    //Log.i("foo", "**** FINISHED READING: pos=" + pos
+                    //        + " len=" + data.length);
+                    return data;
+                }
+                pos += amt;
+                avail = stream.available();
+                if (avail > data.length-pos) {
+                    byte[] newData = new byte[pos+avail];
+                    System.arraycopy(data, 0, newData, 0, pos);
+                    data = newData;
+                }
+            }
+        } finally {
+            stream.close();
+        }
+    }
+}
diff --git a/com/android/internal/os/BackgroundThread.java b/com/android/internal/os/BackgroundThread.java
new file mode 100644
index 0000000..cffba01
--- /dev/null
+++ b/com/android/internal/os/BackgroundThread.java
@@ -0,0 +1,56 @@
+/*
+ * 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.internal.os;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Trace;
+
+/**
+ * Shared singleton background thread for each process.
+ */
+public final class BackgroundThread extends HandlerThread {
+    private static BackgroundThread sInstance;
+    private static Handler sHandler;
+
+    private BackgroundThread() {
+        super("android.bg", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+    }
+
+    private static void ensureThreadLocked() {
+        if (sInstance == null) {
+            sInstance = new BackgroundThread();
+            sInstance.start();
+            sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+            sHandler = new Handler(sInstance.getLooper());
+        }
+    }
+
+    public static BackgroundThread get() {
+        synchronized (BackgroundThread.class) {
+            ensureThreadLocked();
+            return sInstance;
+        }
+    }
+
+    public static Handler getHandler() {
+        synchronized (BackgroundThread.class) {
+            ensureThreadLocked();
+            return sHandler;
+        }
+    }
+}
diff --git a/com/android/internal/os/BaseCommand.java b/com/android/internal/os/BaseCommand.java
new file mode 100644
index 0000000..3baccee
--- /dev/null
+++ b/com/android/internal/os/BaseCommand.java
@@ -0,0 +1,122 @@
+/*
+**
+** Copyright 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.internal.os;
+
+import android.os.ShellCommand;
+
+import java.io.PrintStream;
+
+public abstract class BaseCommand {
+
+    final protected ShellCommand mArgs = new ShellCommand() {
+        @Override public int onCommand(String cmd) {
+            return 0;
+        }
+        @Override public void onHelp() {
+        }
+    };
+
+    // These are magic strings understood by the Eclipse plugin.
+    public static final String FATAL_ERROR_CODE = "Error type 1";
+    public static final String NO_SYSTEM_ERROR_CODE = "Error type 2";
+    public static final String NO_CLASS_ERROR_CODE = "Error type 3";
+
+    private String[] mRawArgs;
+
+    /**
+     * Call to run the command.
+     */
+    public void run(String[] args) {
+        if (args.length < 1) {
+            onShowUsage(System.out);
+            return;
+        }
+
+        mRawArgs = args;
+        mArgs.init(null, null, null, null, args, null, 0);
+
+        try {
+            onRun();
+        } catch (IllegalArgumentException e) {
+            onShowUsage(System.err);
+            System.err.println();
+            System.err.println("Error: " + e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace(System.err);
+            System.exit(1);
+        }
+    }
+
+    /**
+     * Convenience to show usage information to error output.
+     */
+    public void showUsage() {
+        onShowUsage(System.err);
+    }
+
+    /**
+     * Convenience to show usage information to error output along
+     * with an error message.
+     */
+    public void showError(String message) {
+        onShowUsage(System.err);
+        System.err.println();
+        System.err.println(message);
+    }
+
+    /**
+     * Implement the command.
+     */
+    public abstract void onRun() throws Exception;
+
+    /**
+     * Print help text for the command.
+     */
+    public abstract void onShowUsage(PrintStream out);
+
+    /**
+     * Return the next option on the command line -- that is an argument that
+     * starts with '-'.  If the next argument is not an option, null is returned.
+     */
+    public String nextOption() {
+        return mArgs.getNextOption();
+    }
+
+    /**
+     * Return the next argument on the command line, whatever it is; if there are
+     * no arguments left, return null.
+     */
+    public String nextArg() {
+        return mArgs.getNextArg();
+    }
+
+    /**
+     * Return the next argument on the command line, whatever it is; if there are
+     * no arguments left, throws an IllegalArgumentException to report this to the user.
+     */
+    public String nextArgRequired() {
+        return mArgs.getNextArgRequired();
+    }
+
+    /**
+     * Return the original raw argument list supplied to the command.
+     */
+    public String[] getRawArgs() {
+        return mRawArgs;
+    }
+}
diff --git a/com/android/internal/os/BatterySipper.java b/com/android/internal/os/BatterySipper.java
new file mode 100644
index 0000000..5457c1d
--- /dev/null
+++ b/com/android/internal/os/BatterySipper.java
@@ -0,0 +1,227 @@
+/*
+ * 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.internal.os;
+
+import android.os.BatteryStats.Uid;
+
+import java.util.List;
+
+/**
+ * Contains power usage of an application, system service, or hardware type.
+ */
+public class BatterySipper implements Comparable<BatterySipper> {
+    public int userId;
+    public Uid uidObj;
+    public DrainType drainType;
+
+    /**
+     * Smeared power from screen usage.
+     * We split the screen usage power and smear them among apps, based on activity time.
+     */
+    public double screenPowerMah;
+
+    /**
+     * Smeared power using proportional method.
+     *
+     * we smear power usage from hidden sippers to all apps proportionally.(except for screen usage)
+     *
+     * @see BatteryStatsHelper#shouldHideSipper(BatterySipper)
+     * @see BatteryStatsHelper#removeHiddenBatterySippers(List)
+     */
+    public double proportionalSmearMah;
+
+    /**
+     * Total power that adding the smeared power.
+     *
+     * @see #sumPower()
+     */
+    public double totalSmearedPowerMah;
+
+    /**
+     * Total power before smearing
+     */
+    public double totalPowerMah;
+
+    /**
+     * Whether we should hide this sipper
+     *
+     * @see BatteryStatsHelper#shouldHideSipper(BatterySipper)
+     */
+    public boolean shouldHide;
+
+    /**
+     * Generic usage time in milliseconds.
+     */
+    public long usageTimeMs;
+
+    /**
+     * Generic power usage in mAh.
+     */
+    public double usagePowerMah;
+
+    // Subsystem usage times.
+    public long cpuTimeMs;
+    public long gpsTimeMs;
+    public long wifiRunningTimeMs;
+    public long cpuFgTimeMs;
+    public long wakeLockTimeMs;
+    public long cameraTimeMs;
+    public long flashlightTimeMs;
+    public long bluetoothRunningTimeMs;
+
+    public long mobileRxPackets;
+    public long mobileTxPackets;
+    public long mobileActive;
+    public int mobileActiveCount;
+    public double mobilemspp;         // milliseconds per packet
+    public long wifiRxPackets;
+    public long wifiTxPackets;
+    public long mobileRxBytes;
+    public long mobileTxBytes;
+    public long wifiRxBytes;
+    public long wifiTxBytes;
+    public long btRxBytes;
+    public long btTxBytes;
+    public double percent;
+    public double noCoveragePercent;
+    public String[] mPackages;
+    public String packageWithHighestDrain;
+
+    // Measured in mAh (milli-ampere per hour).
+    // These are included when summed.
+    public double wifiPowerMah;
+    public double cpuPowerMah;
+    public double wakeLockPowerMah;
+    public double mobileRadioPowerMah;
+    public double gpsPowerMah;
+    public double sensorPowerMah;
+    public double cameraPowerMah;
+    public double flashlightPowerMah;
+    public double bluetoothPowerMah;
+
+    public enum DrainType {
+        IDLE,
+        CELL,
+        PHONE,
+        WIFI,
+        BLUETOOTH,
+        FLASHLIGHT,
+        SCREEN,
+        APP,
+        USER,
+        UNACCOUNTED,
+        OVERCOUNTED,
+        CAMERA,
+        MEMORY
+    }
+
+    public BatterySipper(DrainType drainType, Uid uid, double value) {
+        this.totalPowerMah = value;
+        this.drainType = drainType;
+        uidObj = uid;
+    }
+
+    public void computeMobilemspp() {
+        long packets = mobileRxPackets + mobileTxPackets;
+        mobilemspp = packets > 0 ? (mobileActive / (double) packets) : 0;
+    }
+
+    @Override
+    public int compareTo(BatterySipper other) {
+        // Over-counted always goes to the bottom.
+        if (drainType != other.drainType) {
+            if (drainType == DrainType.OVERCOUNTED) {
+                // This is "larger"
+                return 1;
+            } else if (other.drainType == DrainType.OVERCOUNTED) {
+                return -1;
+            }
+        }
+        // Return the flipped value because we want the items in descending order
+        return Double.compare(other.totalPowerMah, totalPowerMah);
+    }
+
+    /**
+     * Gets a list of packages associated with the current user
+     */
+    public String[] getPackages() {
+        return mPackages;
+    }
+
+    public int getUid() {
+        // Bail out if the current sipper is not an App sipper.
+        if (uidObj == null) {
+            return 0;
+        }
+        return uidObj.getUid();
+    }
+
+    /**
+     * Add stats from other to this BatterySipper.
+     */
+    public void add(BatterySipper other) {
+        totalPowerMah += other.totalPowerMah;
+        usageTimeMs += other.usageTimeMs;
+        usagePowerMah += other.usagePowerMah;
+        cpuTimeMs += other.cpuTimeMs;
+        gpsTimeMs += other.gpsTimeMs;
+        wifiRunningTimeMs += other.wifiRunningTimeMs;
+        cpuFgTimeMs += other.cpuFgTimeMs;
+        wakeLockTimeMs += other.wakeLockTimeMs;
+        cameraTimeMs += other.cameraTimeMs;
+        flashlightTimeMs += other.flashlightTimeMs;
+        bluetoothRunningTimeMs += other.bluetoothRunningTimeMs;
+        mobileRxPackets += other.mobileRxPackets;
+        mobileTxPackets += other.mobileTxPackets;
+        mobileActive += other.mobileActive;
+        mobileActiveCount += other.mobileActiveCount;
+        wifiRxPackets += other.wifiRxPackets;
+        wifiTxPackets += other.wifiTxPackets;
+        mobileRxBytes += other.mobileRxBytes;
+        mobileTxBytes += other.mobileTxBytes;
+        wifiRxBytes += other.wifiRxBytes;
+        wifiTxBytes += other.wifiTxBytes;
+        btRxBytes += other.btRxBytes;
+        btTxBytes += other.btTxBytes;
+        wifiPowerMah += other.wifiPowerMah;
+        gpsPowerMah += other.gpsPowerMah;
+        cpuPowerMah += other.cpuPowerMah;
+        sensorPowerMah += other.sensorPowerMah;
+        mobileRadioPowerMah += other.mobileRadioPowerMah;
+        wakeLockPowerMah += other.wakeLockPowerMah;
+        cameraPowerMah += other.cameraPowerMah;
+        flashlightPowerMah += other.flashlightPowerMah;
+        bluetoothPowerMah += other.bluetoothPowerMah;
+        screenPowerMah += other.screenPowerMah;
+        proportionalSmearMah += other.proportionalSmearMah;
+        totalSmearedPowerMah += other.totalSmearedPowerMah;
+    }
+
+    /**
+     * Sum all the powers and store the value into `value`.
+     * Also sum the {@code smearedTotalPowerMah} by adding smeared powerMah.
+     *
+     * @return the sum of all the power in this BatterySipper.
+     */
+    public double sumPower() {
+        totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah +
+                sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah +
+                flashlightPowerMah + bluetoothPowerMah;
+        totalSmearedPowerMah = totalPowerMah + screenPowerMah + proportionalSmearMah;
+
+        return totalPowerMah;
+    }
+}
diff --git a/com/android/internal/os/BatteryStatsHelper.java b/com/android/internal/os/BatteryStatsHelper.java
new file mode 100644
index 0000000..f085e29
--- /dev/null
+++ b/com/android/internal/os/BatteryStatsHelper.java
@@ -0,0 +1,1024 @@
+/*
+ * 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.internal.os;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.hardware.SensorManager;
+import android.net.ConnectivityManager;
+import android.os.BatteryStats;
+import android.os.BatteryStats.Uid;
+import android.os.Bundle;
+import android.os.MemoryFile;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseLongArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.os.BatterySipper.DrainType;
+import com.android.internal.util.ArrayUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A helper class for retrieving the power usage information for all applications and services.
+ *
+ * The caller must initialize this class as soon as activity object is ready to use (for example, in
+ * onAttach() for Fragment), call create() in onCreate() and call destroy() in onDestroy().
+ */
+public class BatteryStatsHelper {
+    static final boolean DEBUG = false;
+
+    private static final String TAG = BatteryStatsHelper.class.getSimpleName();
+
+    private static BatteryStats sStatsXfer;
+    private static Intent sBatteryBroadcastXfer;
+    private static ArrayMap<File, BatteryStats> sFileXfer = new ArrayMap<>();
+
+    final private Context mContext;
+    final private boolean mCollectBatteryBroadcast;
+    final private boolean mWifiOnly;
+
+    private IBatteryStats mBatteryInfo;
+    private BatteryStats mStats;
+    private Intent mBatteryBroadcast;
+    private PowerProfile mPowerProfile;
+
+    private String[] mSystemPackageArray;
+    private String[] mServicepackageArray;
+    private PackageManager mPackageManager;
+
+    /**
+     * List of apps using power.
+     */
+    private final List<BatterySipper> mUsageList = new ArrayList<>();
+
+    /**
+     * List of apps using wifi power.
+     */
+    private final List<BatterySipper> mWifiSippers = new ArrayList<>();
+
+    /**
+     * List of apps using bluetooth power.
+     */
+    private final List<BatterySipper> mBluetoothSippers = new ArrayList<>();
+
+    private final SparseArray<List<BatterySipper>> mUserSippers = new SparseArray<>();
+
+    private final List<BatterySipper> mMobilemsppList = new ArrayList<>();
+
+    private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;
+
+    long mRawRealtimeUs;
+    long mRawUptimeUs;
+    long mBatteryRealtimeUs;
+    long mBatteryUptimeUs;
+    long mTypeBatteryRealtimeUs;
+    long mTypeBatteryUptimeUs;
+    long mBatteryTimeRemainingUs;
+    long mChargeTimeRemainingUs;
+
+    private long mStatsPeriod = 0;
+
+    // The largest entry by power.
+    private double mMaxPower = 1;
+
+    // The largest real entry by power (not undercounted or overcounted).
+    private double mMaxRealPower = 1;
+
+    // Total computed power.
+    private double mComputedPower;
+    private double mTotalPower;
+    private double mMinDrainedPower;
+    private double mMaxDrainedPower;
+
+    PowerCalculator mCpuPowerCalculator;
+    PowerCalculator mWakelockPowerCalculator;
+    MobileRadioPowerCalculator mMobileRadioPowerCalculator;
+    PowerCalculator mWifiPowerCalculator;
+    PowerCalculator mBluetoothPowerCalculator;
+    PowerCalculator mSensorPowerCalculator;
+    PowerCalculator mCameraPowerCalculator;
+    PowerCalculator mFlashlightPowerCalculator;
+    PowerCalculator mMemoryPowerCalculator;
+
+    boolean mHasWifiPowerReporting = false;
+    boolean mHasBluetoothPowerReporting = false;
+
+    public static boolean checkWifiOnly(Context context) {
+        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        return !cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+    }
+
+    public static boolean checkHasWifiPowerReporting(BatteryStats stats, PowerProfile profile) {
+        return stats.hasWifiActivityReporting() &&
+                profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE) != 0 &&
+                profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX) != 0 &&
+                profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX) != 0;
+    }
+
+    public static boolean checkHasBluetoothPowerReporting(BatteryStats stats,
+            PowerProfile profile) {
+        return stats.hasBluetoothActivityReporting() &&
+                profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE) != 0 &&
+                profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX) != 0 &&
+                profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_TX) != 0;
+    }
+
+    public BatteryStatsHelper(Context context) {
+        this(context, true);
+    }
+
+    public BatteryStatsHelper(Context context, boolean collectBatteryBroadcast) {
+        this(context, collectBatteryBroadcast, checkWifiOnly(context));
+    }
+
+    public BatteryStatsHelper(Context context, boolean collectBatteryBroadcast, boolean wifiOnly) {
+        mContext = context;
+        mCollectBatteryBroadcast = collectBatteryBroadcast;
+        mWifiOnly = wifiOnly;
+        mPackageManager = context.getPackageManager();
+
+        final Resources resources = context.getResources();
+        mSystemPackageArray = resources.getStringArray(
+                com.android.internal.R.array.config_batteryPackageTypeSystem);
+        mServicepackageArray = resources.getStringArray(
+                com.android.internal.R.array.config_batteryPackageTypeService);
+    }
+
+    public void storeStatsHistoryInFile(String fname) {
+        synchronized (sFileXfer) {
+            File path = makeFilePath(mContext, fname);
+            sFileXfer.put(path, this.getStats());
+            FileOutputStream fout = null;
+            try {
+                fout = new FileOutputStream(path);
+                Parcel hist = Parcel.obtain();
+                getStats().writeToParcelWithoutUids(hist, 0);
+                byte[] histData = hist.marshall();
+                fout.write(histData);
+            } catch (IOException e) {
+                Log.w(TAG, "Unable to write history to file", e);
+            } finally {
+                if (fout != null) {
+                    try {
+                        fout.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        }
+    }
+
+    public static BatteryStats statsFromFile(Context context, String fname) {
+        synchronized (sFileXfer) {
+            File path = makeFilePath(context, fname);
+            BatteryStats stats = sFileXfer.get(path);
+            if (stats != null) {
+                return stats;
+            }
+            FileInputStream fin = null;
+            try {
+                fin = new FileInputStream(path);
+                byte[] data = readFully(fin);
+                Parcel parcel = Parcel.obtain();
+                parcel.unmarshall(data, 0, data.length);
+                parcel.setDataPosition(0);
+                return com.android.internal.os.BatteryStatsImpl.CREATOR.createFromParcel(parcel);
+            } catch (IOException e) {
+                Log.w(TAG, "Unable to read history to file", e);
+            } finally {
+                if (fin != null) {
+                    try {
+                        fin.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        }
+        return getStats(IBatteryStats.Stub.asInterface(
+                ServiceManager.getService(BatteryStats.SERVICE_NAME)));
+    }
+
+    public static void dropFile(Context context, String fname) {
+        makeFilePath(context, fname).delete();
+    }
+
+    private static File makeFilePath(Context context, String fname) {
+        return new File(context.getFilesDir(), fname);
+    }
+
+    /** Clears the current stats and forces recreating for future use. */
+    public void clearStats() {
+        mStats = null;
+    }
+
+    public BatteryStats getStats() {
+        if (mStats == null) {
+            load();
+        }
+        return mStats;
+    }
+
+    public Intent getBatteryBroadcast() {
+        if (mBatteryBroadcast == null && mCollectBatteryBroadcast) {
+            load();
+        }
+        return mBatteryBroadcast;
+    }
+
+    public PowerProfile getPowerProfile() {
+        return mPowerProfile;
+    }
+
+    public void create(BatteryStats stats) {
+        mPowerProfile = new PowerProfile(mContext);
+        mStats = stats;
+    }
+
+    public void create(Bundle icicle) {
+        if (icicle != null) {
+            mStats = sStatsXfer;
+            mBatteryBroadcast = sBatteryBroadcastXfer;
+        }
+        mBatteryInfo = IBatteryStats.Stub.asInterface(
+                ServiceManager.getService(BatteryStats.SERVICE_NAME));
+        mPowerProfile = new PowerProfile(mContext);
+    }
+
+    public void storeState() {
+        sStatsXfer = mStats;
+        sBatteryBroadcastXfer = mBatteryBroadcast;
+    }
+
+    public static String makemAh(double power) {
+        if (power == 0) return "0";
+
+        final String format;
+        if (power < .00001) {
+            format = "%.8f";
+        } else if (power < .0001) {
+            format = "%.7f";
+        } else if (power < .001) {
+            format = "%.6f";
+        } else if (power < .01) {
+            format = "%.5f";
+        } else if (power < .1) {
+            format = "%.4f";
+        } else if (power < 1) {
+            format = "%.3f";
+        } else if (power < 10) {
+            format = "%.2f";
+        } else if (power < 100) {
+            format = "%.1f";
+        } else {
+            format = "%.0f";
+        }
+
+        // Use English locale because this is never used in UI (only in checkin and dump).
+        return String.format(Locale.ENGLISH, format, power);
+    }
+
+    /**
+     * Refreshes the power usage list.
+     */
+    public void refreshStats(int statsType, int asUser) {
+        SparseArray<UserHandle> users = new SparseArray<>(1);
+        users.put(asUser, new UserHandle(asUser));
+        refreshStats(statsType, users);
+    }
+
+    /**
+     * Refreshes the power usage list.
+     */
+    public void refreshStats(int statsType, List<UserHandle> asUsers) {
+        final int n = asUsers.size();
+        SparseArray<UserHandle> users = new SparseArray<>(n);
+        for (int i = 0; i < n; ++i) {
+            UserHandle userHandle = asUsers.get(i);
+            users.put(userHandle.getIdentifier(), userHandle);
+        }
+        refreshStats(statsType, users);
+    }
+
+    /**
+     * Refreshes the power usage list.
+     */
+    public void refreshStats(int statsType, SparseArray<UserHandle> asUsers) {
+        refreshStats(statsType, asUsers, SystemClock.elapsedRealtime() * 1000,
+                SystemClock.uptimeMillis() * 1000);
+    }
+
+    public void refreshStats(int statsType, SparseArray<UserHandle> asUsers, long rawRealtimeUs,
+            long rawUptimeUs) {
+        // Initialize mStats if necessary.
+        getStats();
+
+        mMaxPower = 0;
+        mMaxRealPower = 0;
+        mComputedPower = 0;
+        mTotalPower = 0;
+
+        mUsageList.clear();
+        mWifiSippers.clear();
+        mBluetoothSippers.clear();
+        mUserSippers.clear();
+        mMobilemsppList.clear();
+
+        if (mStats == null) {
+            return;
+        }
+
+        if (mCpuPowerCalculator == null) {
+            mCpuPowerCalculator = new CpuPowerCalculator(mPowerProfile);
+        }
+        mCpuPowerCalculator.reset();
+
+        if (mMemoryPowerCalculator == null) {
+            mMemoryPowerCalculator = new MemoryPowerCalculator(mPowerProfile);
+        }
+        mMemoryPowerCalculator.reset();
+
+        if (mWakelockPowerCalculator == null) {
+            mWakelockPowerCalculator = new WakelockPowerCalculator(mPowerProfile);
+        }
+        mWakelockPowerCalculator.reset();
+
+        if (mMobileRadioPowerCalculator == null) {
+            mMobileRadioPowerCalculator = new MobileRadioPowerCalculator(mPowerProfile, mStats);
+        }
+        mMobileRadioPowerCalculator.reset(mStats);
+
+        // checkHasWifiPowerReporting can change if we get energy data at a later point, so
+        // always check this field.
+        final boolean hasWifiPowerReporting = checkHasWifiPowerReporting(mStats, mPowerProfile);
+        if (mWifiPowerCalculator == null || hasWifiPowerReporting != mHasWifiPowerReporting) {
+            mWifiPowerCalculator = hasWifiPowerReporting ?
+                    new WifiPowerCalculator(mPowerProfile) :
+                    new WifiPowerEstimator(mPowerProfile);
+            mHasWifiPowerReporting = hasWifiPowerReporting;
+        }
+        mWifiPowerCalculator.reset();
+
+        final boolean hasBluetoothPowerReporting = checkHasBluetoothPowerReporting(mStats,
+                mPowerProfile);
+        if (mBluetoothPowerCalculator == null ||
+                hasBluetoothPowerReporting != mHasBluetoothPowerReporting) {
+            mBluetoothPowerCalculator = new BluetoothPowerCalculator(mPowerProfile);
+            mHasBluetoothPowerReporting = hasBluetoothPowerReporting;
+        }
+        mBluetoothPowerCalculator.reset();
+
+        if (mSensorPowerCalculator == null) {
+            mSensorPowerCalculator = new SensorPowerCalculator(mPowerProfile,
+                    (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE));
+        }
+        mSensorPowerCalculator.reset();
+
+        if (mCameraPowerCalculator == null) {
+            mCameraPowerCalculator = new CameraPowerCalculator(mPowerProfile);
+        }
+        mCameraPowerCalculator.reset();
+
+        if (mFlashlightPowerCalculator == null) {
+            mFlashlightPowerCalculator = new FlashlightPowerCalculator(mPowerProfile);
+        }
+        mFlashlightPowerCalculator.reset();
+
+        mStatsType = statsType;
+        mRawUptimeUs = rawUptimeUs;
+        mRawRealtimeUs = rawRealtimeUs;
+        mBatteryUptimeUs = mStats.getBatteryUptime(rawUptimeUs);
+        mBatteryRealtimeUs = mStats.getBatteryRealtime(rawRealtimeUs);
+        mTypeBatteryUptimeUs = mStats.computeBatteryUptime(rawUptimeUs, mStatsType);
+        mTypeBatteryRealtimeUs = mStats.computeBatteryRealtime(rawRealtimeUs, mStatsType);
+        mBatteryTimeRemainingUs = mStats.computeBatteryTimeRemaining(rawRealtimeUs);
+        mChargeTimeRemainingUs = mStats.computeChargeTimeRemaining(rawRealtimeUs);
+
+        if (DEBUG) {
+            Log.d(TAG, "Raw time: realtime=" + (rawRealtimeUs / 1000) + " uptime="
+                    + (rawUptimeUs / 1000));
+            Log.d(TAG, "Battery time: realtime=" + (mBatteryRealtimeUs / 1000) + " uptime="
+                    + (mBatteryUptimeUs / 1000));
+            Log.d(TAG, "Battery type time: realtime=" + (mTypeBatteryRealtimeUs / 1000) + " uptime="
+                    + (mTypeBatteryUptimeUs / 1000));
+        }
+        mMinDrainedPower = (mStats.getLowDischargeAmountSinceCharge()
+                * mPowerProfile.getBatteryCapacity()) / 100;
+        mMaxDrainedPower = (mStats.getHighDischargeAmountSinceCharge()
+                * mPowerProfile.getBatteryCapacity()) / 100;
+
+        processAppUsage(asUsers);
+
+        // Before aggregating apps in to users, collect all apps to sort by their ms per packet.
+        for (int i = 0; i < mUsageList.size(); i++) {
+            BatterySipper bs = mUsageList.get(i);
+            bs.computeMobilemspp();
+            if (bs.mobilemspp != 0) {
+                mMobilemsppList.add(bs);
+            }
+        }
+
+        for (int i = 0; i < mUserSippers.size(); i++) {
+            List<BatterySipper> user = mUserSippers.valueAt(i);
+            for (int j = 0; j < user.size(); j++) {
+                BatterySipper bs = user.get(j);
+                bs.computeMobilemspp();
+                if (bs.mobilemspp != 0) {
+                    mMobilemsppList.add(bs);
+                }
+            }
+        }
+        Collections.sort(mMobilemsppList, new Comparator<BatterySipper>() {
+            @Override
+            public int compare(BatterySipper lhs, BatterySipper rhs) {
+                return Double.compare(rhs.mobilemspp, lhs.mobilemspp);
+            }
+        });
+
+        processMiscUsage();
+
+        Collections.sort(mUsageList);
+
+        // At this point, we've sorted the list so we are guaranteed the max values are at the top.
+        // We have only added real powers so far.
+        if (!mUsageList.isEmpty()) {
+            mMaxRealPower = mMaxPower = mUsageList.get(0).totalPowerMah;
+            final int usageListCount = mUsageList.size();
+            for (int i = 0; i < usageListCount; i++) {
+                mComputedPower += mUsageList.get(i).totalPowerMah;
+            }
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Accuracy: total computed=" + makemAh(mComputedPower) + ", min discharge="
+                    + makemAh(mMinDrainedPower) + ", max discharge=" + makemAh(mMaxDrainedPower));
+        }
+
+        mTotalPower = mComputedPower;
+        if (mStats.getLowDischargeAmountSinceCharge() > 1) {
+            if (mMinDrainedPower > mComputedPower) {
+                double amount = mMinDrainedPower - mComputedPower;
+                mTotalPower = mMinDrainedPower;
+                BatterySipper bs = new BatterySipper(DrainType.UNACCOUNTED, null, amount);
+
+                // Insert the BatterySipper in its sorted position.
+                int index = Collections.binarySearch(mUsageList, bs);
+                if (index < 0) {
+                    index = -(index + 1);
+                }
+                mUsageList.add(index, bs);
+                mMaxPower = Math.max(mMaxPower, amount);
+            } else if (mMaxDrainedPower < mComputedPower) {
+                double amount = mComputedPower - mMaxDrainedPower;
+
+                // Insert the BatterySipper in its sorted position.
+                BatterySipper bs = new BatterySipper(DrainType.OVERCOUNTED, null, amount);
+                int index = Collections.binarySearch(mUsageList, bs);
+                if (index < 0) {
+                    index = -(index + 1);
+                }
+                mUsageList.add(index, bs);
+                mMaxPower = Math.max(mMaxPower, amount);
+            }
+        }
+
+        // Smear it!
+        final double hiddenPowerMah = removeHiddenBatterySippers(mUsageList);
+        final double totalRemainingPower = getTotalPower() - hiddenPowerMah;
+        if (Math.abs(totalRemainingPower) > 1e-3) {
+            for (int i = 0, size = mUsageList.size(); i < size; i++) {
+                final BatterySipper sipper = mUsageList.get(i);
+                if (!sipper.shouldHide) {
+                    sipper.proportionalSmearMah = hiddenPowerMah
+                            * ((sipper.totalPowerMah + sipper.screenPowerMah)
+                            / totalRemainingPower);
+                    sipper.sumPower();
+                }
+            }
+        }
+    }
+
+    private void processAppUsage(SparseArray<UserHandle> asUsers) {
+        final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null);
+        mStatsPeriod = mTypeBatteryRealtimeUs;
+
+        BatterySipper osSipper = null;
+        final SparseArray<? extends Uid> uidStats = mStats.getUidStats();
+        final int NU = uidStats.size();
+        for (int iu = 0; iu < NU; iu++) {
+            final Uid u = uidStats.valueAt(iu);
+            final BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, 0);
+
+            mCpuPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
+            mWakelockPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
+            mMobileRadioPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs,
+                    mStatsType);
+            mWifiPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
+            mBluetoothPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs,
+                    mStatsType);
+            mSensorPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
+            mCameraPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
+            mFlashlightPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs,
+                    mStatsType);
+
+            final double totalPower = app.sumPower();
+            if (DEBUG && totalPower != 0) {
+                Log.d(TAG, String.format("UID %d: total power=%s", u.getUid(),
+                        makemAh(totalPower)));
+            }
+
+            // Add the app to the list if it is consuming power.
+            if (totalPower != 0 || u.getUid() == 0) {
+                //
+                // Add the app to the app list, WiFi, Bluetooth, etc, or into "Other Users" list.
+                //
+                final int uid = app.getUid();
+                final int userId = UserHandle.getUserId(uid);
+                if (uid == Process.WIFI_UID) {
+                    mWifiSippers.add(app);
+                } else if (uid == Process.BLUETOOTH_UID) {
+                    mBluetoothSippers.add(app);
+                } else if (!forAllUsers && asUsers.get(userId) == null
+                        && UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) {
+                    // We are told to just report this user's apps as one large entry.
+                    List<BatterySipper> list = mUserSippers.get(userId);
+                    if (list == null) {
+                        list = new ArrayList<>();
+                        mUserSippers.put(userId, list);
+                    }
+                    list.add(app);
+                } else {
+                    mUsageList.add(app);
+                }
+
+                if (uid == 0) {
+                    osSipper = app;
+                }
+            }
+        }
+
+        if (osSipper != null) {
+            // The device has probably been awake for longer than the screen on
+            // time and application wake lock time would account for.  Assign
+            // this remainder to the OS, if possible.
+            mWakelockPowerCalculator.calculateRemaining(osSipper, mStats, mRawRealtimeUs,
+                    mRawUptimeUs, mStatsType);
+            osSipper.sumPower();
+        }
+    }
+
+    private void addPhoneUsage() {
+        long phoneOnTimeMs = mStats.getPhoneOnTime(mRawRealtimeUs, mStatsType) / 1000;
+        double phoneOnPower = mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE)
+                * phoneOnTimeMs / (60 * 60 * 1000);
+        if (phoneOnPower != 0) {
+            addEntry(BatterySipper.DrainType.PHONE, phoneOnTimeMs, phoneOnPower);
+        }
+    }
+
+    /**
+     * Screen power is the additional power the screen takes while the device is running.
+     */
+    private void addScreenUsage() {
+        double power = 0;
+        long screenOnTimeMs = mStats.getScreenOnTime(mRawRealtimeUs, mStatsType) / 1000;
+        power += screenOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_ON);
+        final double screenFullPower =
+                mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);
+        for (int i = 0; i < BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            double screenBinPower = screenFullPower * (i + 0.5f)
+                    / BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS;
+            long brightnessTime = mStats.getScreenBrightnessTime(i, mRawRealtimeUs, mStatsType)
+                    / 1000;
+            double p = screenBinPower * brightnessTime;
+            if (DEBUG && p != 0) {
+                Log.d(TAG, "Screen bin #" + i + ": time=" + brightnessTime
+                        + " power=" + makemAh(p / (60 * 60 * 1000)));
+            }
+            power += p;
+        }
+        power /= (60 * 60 * 1000); // To hours
+        if (power != 0) {
+            addEntry(BatterySipper.DrainType.SCREEN, screenOnTimeMs, power);
+        }
+    }
+
+    private void addRadioUsage() {
+        BatterySipper radio = new BatterySipper(BatterySipper.DrainType.CELL, null, 0);
+        mMobileRadioPowerCalculator.calculateRemaining(radio, mStats, mRawRealtimeUs, mRawUptimeUs,
+                mStatsType);
+        radio.sumPower();
+        if (radio.totalPowerMah > 0) {
+            mUsageList.add(radio);
+        }
+    }
+
+    private void aggregateSippers(BatterySipper bs, List<BatterySipper> from, String tag) {
+        for (int i = 0; i < from.size(); i++) {
+            BatterySipper wbs = from.get(i);
+            if (DEBUG) Log.d(TAG, tag + " adding sipper " + wbs + ": cpu=" + wbs.cpuTimeMs);
+            bs.add(wbs);
+        }
+        bs.computeMobilemspp();
+        bs.sumPower();
+    }
+
+    /**
+     * Calculate the baseline power usage for the device when it is in suspend and idle.
+     * The device is drawing POWER_CPU_IDLE power at its lowest power state.
+     * The device is drawing POWER_CPU_IDLE + POWER_CPU_AWAKE power when a wakelock is held.
+     */
+    private void addIdleUsage() {
+        final double suspendPowerMaMs = (mTypeBatteryRealtimeUs / 1000) *
+                mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE);
+        final double idlePowerMaMs = (mTypeBatteryUptimeUs / 1000) *
+                mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
+        final double totalPowerMah = (suspendPowerMaMs + idlePowerMaMs) / (60 * 60 * 1000);
+        if (DEBUG && totalPowerMah != 0) {
+            Log.d(TAG, "Suspend: time=" + (mTypeBatteryRealtimeUs / 1000)
+                    + " power=" + makemAh(suspendPowerMaMs / (60 * 60 * 1000)));
+            Log.d(TAG, "Idle: time=" + (mTypeBatteryUptimeUs / 1000)
+                    + " power=" + makemAh(idlePowerMaMs / (60 * 60 * 1000)));
+        }
+
+        if (totalPowerMah != 0) {
+            addEntry(BatterySipper.DrainType.IDLE, mTypeBatteryRealtimeUs / 1000, totalPowerMah);
+        }
+    }
+
+    /**
+     * We do per-app blaming of WiFi activity. If energy info is reported from the controller,
+     * then only the WiFi process gets blamed here since we normalize power calculations and
+     * assign all the power drain to apps. If energy info is not reported, we attribute the
+     * difference between total running time of WiFi for all apps and the actual running time
+     * of WiFi to the WiFi subsystem.
+     */
+    private void addWiFiUsage() {
+        BatterySipper bs = new BatterySipper(DrainType.WIFI, null, 0);
+        mWifiPowerCalculator.calculateRemaining(bs, mStats, mRawRealtimeUs, mRawUptimeUs,
+                mStatsType);
+        aggregateSippers(bs, mWifiSippers, "WIFI");
+        if (bs.totalPowerMah > 0) {
+            mUsageList.add(bs);
+        }
+    }
+
+    /**
+     * Bluetooth usage is not attributed to any apps yet, so the entire blame goes to the
+     * Bluetooth Category.
+     */
+    private void addBluetoothUsage() {
+        BatterySipper bs = new BatterySipper(BatterySipper.DrainType.BLUETOOTH, null, 0);
+        mBluetoothPowerCalculator.calculateRemaining(bs, mStats, mRawRealtimeUs, mRawUptimeUs,
+                mStatsType);
+        aggregateSippers(bs, mBluetoothSippers, "Bluetooth");
+        if (bs.totalPowerMah > 0) {
+            mUsageList.add(bs);
+        }
+    }
+
+    private void addUserUsage() {
+        for (int i = 0; i < mUserSippers.size(); i++) {
+            final int userId = mUserSippers.keyAt(i);
+            BatterySipper bs = new BatterySipper(DrainType.USER, null, 0);
+            bs.userId = userId;
+            aggregateSippers(bs, mUserSippers.valueAt(i), "User");
+            mUsageList.add(bs);
+        }
+    }
+
+    private void addMemoryUsage() {
+        BatterySipper memory = new BatterySipper(DrainType.MEMORY, null, 0);
+        mMemoryPowerCalculator.calculateRemaining(memory, mStats, mRawRealtimeUs, mRawUptimeUs,
+                mStatsType);
+        memory.sumPower();
+        if (memory.totalPowerMah > 0) {
+            mUsageList.add(memory);
+        }
+    }
+
+    private void processMiscUsage() {
+        addUserUsage();
+        addPhoneUsage();
+        addScreenUsage();
+        addWiFiUsage();
+        addBluetoothUsage();
+        addMemoryUsage();
+        addIdleUsage(); // Not including cellular idle power
+        // Don't compute radio usage if it's a wifi-only device
+        if (!mWifiOnly) {
+            addRadioUsage();
+        }
+    }
+
+    private BatterySipper addEntry(DrainType drainType, long time, double power) {
+        BatterySipper bs = new BatterySipper(drainType, null, 0);
+        bs.usagePowerMah = power;
+        bs.usageTimeMs = time;
+        bs.sumPower();
+        mUsageList.add(bs);
+        return bs;
+    }
+
+    public List<BatterySipper> getUsageList() {
+        return mUsageList;
+    }
+
+    public List<BatterySipper> getMobilemsppList() {
+        return mMobilemsppList;
+    }
+
+    public long getStatsPeriod() {
+        return mStatsPeriod;
+    }
+
+    public int getStatsType() {
+        return mStatsType;
+    }
+
+    public double getMaxPower() {
+        return mMaxPower;
+    }
+
+    public double getMaxRealPower() {
+        return mMaxRealPower;
+    }
+
+    public double getTotalPower() {
+        return mTotalPower;
+    }
+
+    public double getComputedPower() {
+        return mComputedPower;
+    }
+
+    public double getMinDrainedPower() {
+        return mMinDrainedPower;
+    }
+
+    public double getMaxDrainedPower() {
+        return mMaxDrainedPower;
+    }
+
+    public static byte[] readFully(FileInputStream stream) throws java.io.IOException {
+        return readFully(stream, stream.available());
+    }
+
+    public static byte[] readFully(FileInputStream stream, int avail) throws java.io.IOException {
+        int pos = 0;
+        byte[] data = new byte[avail];
+        while (true) {
+            int amt = stream.read(data, pos, data.length - pos);
+            //Log.i("foo", "Read " + amt + " bytes at " + pos
+            //        + " of avail " + data.length);
+            if (amt <= 0) {
+                //Log.i("foo", "**** FINISHED READING: pos=" + pos
+                //        + " len=" + data.length);
+                return data;
+            }
+            pos += amt;
+            avail = stream.available();
+            if (avail > data.length - pos) {
+                byte[] newData = new byte[pos + avail];
+                System.arraycopy(data, 0, newData, 0, pos);
+                data = newData;
+            }
+        }
+    }
+
+    /**
+     * Mark the {@link BatterySipper} that we should hide and smear the screen usage based on
+     * foreground activity time.
+     *
+     * @param sippers sipper list that need to check and remove
+     * @return the total power of the hidden items of {@link BatterySipper}
+     * for proportional smearing
+     */
+    public double removeHiddenBatterySippers(List<BatterySipper> sippers) {
+        double proportionalSmearPowerMah = 0;
+        BatterySipper screenSipper = null;
+        for (int i = sippers.size() - 1; i >= 0; i--) {
+            final BatterySipper sipper = sippers.get(i);
+            sipper.shouldHide = shouldHideSipper(sipper);
+            if (sipper.shouldHide) {
+                if (sipper.drainType != BatterySipper.DrainType.OVERCOUNTED
+                        && sipper.drainType != BatterySipper.DrainType.SCREEN
+                        && sipper.drainType != BatterySipper.DrainType.UNACCOUNTED
+                        && sipper.drainType != BatterySipper.DrainType.BLUETOOTH
+                        && sipper.drainType != BatterySipper.DrainType.WIFI
+                        && sipper.drainType != BatterySipper.DrainType.IDLE) {
+                    // Don't add it if it is overcounted, unaccounted or screen
+                    proportionalSmearPowerMah += sipper.totalPowerMah;
+                }
+            }
+
+            if (sipper.drainType == BatterySipper.DrainType.SCREEN) {
+                screenSipper = sipper;
+            }
+        }
+
+        smearScreenBatterySipper(sippers, screenSipper);
+
+        return proportionalSmearPowerMah;
+    }
+
+    /**
+     * Smear the screen on power usage among {@code sippers}, based on ratio of foreground activity
+     * time.
+     */
+    public void smearScreenBatterySipper(List<BatterySipper> sippers, BatterySipper screenSipper) {
+        long totalActivityTimeMs = 0;
+        final SparseLongArray activityTimeArray = new SparseLongArray();
+        for (int i = 0, size = sippers.size(); i < size; i++) {
+            final BatteryStats.Uid uid = sippers.get(i).uidObj;
+            if (uid != null) {
+                final long timeMs = getProcessForegroundTimeMs(uid,
+                        BatteryStats.STATS_SINCE_CHARGED);
+                activityTimeArray.put(uid.getUid(), timeMs);
+                totalActivityTimeMs += timeMs;
+            }
+        }
+
+        if (screenSipper != null && totalActivityTimeMs >= 10 * DateUtils.MINUTE_IN_MILLIS) {
+            final double screenPowerMah = screenSipper.totalPowerMah;
+            for (int i = 0, size = sippers.size(); i < size; i++) {
+                final BatterySipper sipper = sippers.get(i);
+                sipper.screenPowerMah = screenPowerMah * activityTimeArray.get(sipper.getUid(), 0)
+                        / totalActivityTimeMs;
+            }
+        }
+    }
+
+    /**
+     * Check whether we should hide the battery sipper.
+     */
+    public boolean shouldHideSipper(BatterySipper sipper) {
+        final BatterySipper.DrainType drainType = sipper.drainType;
+
+        return drainType == BatterySipper.DrainType.IDLE
+                || drainType == BatterySipper.DrainType.CELL
+                || drainType == BatterySipper.DrainType.SCREEN
+                || drainType == BatterySipper.DrainType.UNACCOUNTED
+                || drainType == BatterySipper.DrainType.OVERCOUNTED
+                || isTypeService(sipper)
+                || isTypeSystem(sipper);
+    }
+
+    /**
+     * Check whether {@code sipper} is type service
+     */
+    public boolean isTypeService(BatterySipper sipper) {
+        final String[] packages = mPackageManager.getPackagesForUid(sipper.getUid());
+        if (packages == null) {
+            return false;
+        }
+
+        for (String packageName : packages) {
+            if (ArrayUtils.contains(mServicepackageArray, packageName)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check whether {@code sipper} is type system
+     */
+    public boolean isTypeSystem(BatterySipper sipper) {
+        final int uid = sipper.uidObj == null ? -1 : sipper.getUid();
+        sipper.mPackages = mPackageManager.getPackagesForUid(uid);
+        // Classify all the sippers to type system if the range of uid is 0...FIRST_APPLICATION_UID
+        if (uid >= Process.ROOT_UID && uid < Process.FIRST_APPLICATION_UID) {
+            return true;
+        } else if (sipper.mPackages != null) {
+            for (final String packageName : sipper.mPackages) {
+                if (ArrayUtils.contains(mSystemPackageArray, packageName)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public long convertUsToMs(long timeUs) {
+        return timeUs / 1000;
+    }
+
+    public long convertMsToUs(long timeMs) {
+        return timeMs * 1000;
+    }
+
+    @VisibleForTesting
+    public long getForegroundActivityTotalTimeUs(BatteryStats.Uid uid, long rawRealtimeUs) {
+        final BatteryStats.Timer timer = uid.getForegroundActivityTimer();
+        if (timer != null) {
+            return timer.getTotalTimeLocked(rawRealtimeUs, BatteryStats.STATS_SINCE_CHARGED);
+        }
+
+        return 0;
+    }
+
+    @VisibleForTesting
+    public long getProcessForegroundTimeMs(BatteryStats.Uid uid, int which) {
+        final long rawRealTimeUs = convertMsToUs(SystemClock.elapsedRealtime());
+        final int foregroundTypes[] = {BatteryStats.Uid.PROCESS_STATE_TOP};
+
+        long timeUs = 0;
+        for (int type : foregroundTypes) {
+            final long localTime = uid.getProcessStateTime(type, rawRealTimeUs, which);
+            timeUs += localTime;
+        }
+
+        // Return the min value of STATE_TOP time and foreground activity time, since both of these
+        // time have some errors.
+        return convertUsToMs(
+                Math.min(timeUs, getForegroundActivityTotalTimeUs(uid, rawRealTimeUs)));
+    }
+
+    @VisibleForTesting
+    public void setPackageManager(PackageManager packageManager) {
+        mPackageManager = packageManager;
+    }
+
+    @VisibleForTesting
+    public void setSystemPackageArray(String[] array) {
+        mSystemPackageArray = array;
+    }
+
+    @VisibleForTesting
+    public void setServicePackageArray(String[] array) {
+        mServicepackageArray = array;
+    }
+
+    private void load() {
+        if (mBatteryInfo == null) {
+            return;
+        }
+        mStats = getStats(mBatteryInfo);
+        if (mCollectBatteryBroadcast) {
+            mBatteryBroadcast = mContext.registerReceiver(null,
+                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+        }
+    }
+
+    private static BatteryStatsImpl getStats(IBatteryStats service) {
+        try {
+            ParcelFileDescriptor pfd = service.getStatisticsStream();
+            if (pfd != null) {
+                try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+                    byte[] data = readFully(fis, MemoryFile.getSize(pfd.getFileDescriptor()));
+                    Parcel parcel = Parcel.obtain();
+                    parcel.unmarshall(data, 0, data.length);
+                    parcel.setDataPosition(0);
+                    BatteryStatsImpl stats = com.android.internal.os.BatteryStatsImpl.CREATOR
+                            .createFromParcel(parcel);
+                    return stats;
+                } catch (IOException e) {
+                    Log.w(TAG, "Unable to read statistics stream", e);
+                }
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "RemoteException:", e);
+        }
+        return new BatteryStatsImpl();
+    }
+}
diff --git a/com/android/internal/os/BatteryStatsImpl.java b/com/android/internal/os/BatteryStatsImpl.java
new file mode 100644
index 0000000..c58ff05
--- /dev/null
+++ b/com/android/internal/os/BatteryStatsImpl.java
@@ -0,0 +1,12873 @@
+/*
+ * Copyright (C) 2006-2007 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.internal.os;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothActivityEnergyInfo;
+import android.bluetooth.UidTraffic;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkStats;
+import android.net.wifi.WifiActivityEnergyInfo;
+import android.net.wifi.WifiManager;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
+import android.os.Build;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBatteryPropertiesRegistrar;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.ParcelFormatException;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.telephony.DataConnectionRealTimeInfo;
+import android.telephony.ModemActivityInfo;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.LogWriter;
+import android.util.LongSparseArray;
+import android.util.LongSparseLongArray;
+import android.util.MutableInt;
+import android.util.Pools;
+import android.util.PrintWriterPrinter;
+import android.util.Printer;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.SparseLongArray;
+import android.util.TimeUtils;
+import android.util.Xml;
+import android.view.Display;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.net.NetworkStatsFactory;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.JournaledFile;
+import com.android.internal.util.XmlUtils;
+
+import libcore.util.EmptyArray;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * All information we are collecting about things that can happen that impact
+ * battery life.  All times are represented in microseconds except where indicated
+ * otherwise.
+ */
+public class BatteryStatsImpl extends BatteryStats {
+    private static final String TAG = "BatteryStatsImpl";
+    private static final boolean DEBUG = false;
+    public static final boolean DEBUG_ENERGY = false;
+    private static final boolean DEBUG_ENERGY_CPU = DEBUG_ENERGY;
+    private static final boolean DEBUG_MEMORY = false;
+    private static final boolean DEBUG_HISTORY = false;
+    private static final boolean USE_OLD_HISTORY = false;   // for debugging.
+
+    // TODO: remove "tcp" from network methods, since we measure total stats.
+
+    // In-memory Parcel magic number, used to detect attempts to unmarshall bad data
+    private static final int MAGIC = 0xBA757475; // 'BATSTATS'
+
+    // Current on-disk Parcel version
+    private static final int VERSION = 165 + (USE_OLD_HISTORY ? 1000 : 0);
+
+    // Maximum number of items we will record in the history.
+    private static final int MAX_HISTORY_ITEMS;
+
+    // No, really, THIS is the maximum number of items we will record in the history.
+    private static final int MAX_MAX_HISTORY_ITEMS;
+
+    // The maximum number of names wakelocks we will keep track of
+    // per uid; once the limit is reached, we batch the remaining wakelocks
+    // in to one common name.
+    private static final int MAX_WAKELOCKS_PER_UID;
+
+    static final int MAX_HISTORY_BUFFER; // 256KB
+    static final int MAX_MAX_HISTORY_BUFFER; // 320KB
+
+    static {
+        if (ActivityManager.isLowRamDeviceStatic()) {
+            MAX_HISTORY_ITEMS = 800;
+            MAX_MAX_HISTORY_ITEMS = 1200;
+            MAX_WAKELOCKS_PER_UID = 40;
+            MAX_HISTORY_BUFFER = 96*1024;  // 96KB
+            MAX_MAX_HISTORY_BUFFER = 128*1024; // 128KB
+        } else {
+            MAX_HISTORY_ITEMS = 2000;
+            MAX_MAX_HISTORY_ITEMS = 3000;
+            MAX_WAKELOCKS_PER_UID = 100;
+            MAX_HISTORY_BUFFER = 256*1024;  // 256KB
+            MAX_MAX_HISTORY_BUFFER = 320*1024;  // 256KB
+        }
+    }
+
+    // Number of transmit power states the Wifi controller can be in.
+    private static final int NUM_WIFI_TX_LEVELS = 1;
+
+    // Number of transmit power states the Bluetooth controller can be in.
+    private static final int NUM_BT_TX_LEVELS = 1;
+
+    /**
+     * Holding a wakelock costs more than just using the cpu.
+     * Currently, we assign only half the cpu time to an app that is running but
+     * not holding a wakelock. The apps holding wakelocks get the rest of the blame.
+     * If no app is holding a wakelock, then the distribution is normal.
+     */
+    @VisibleForTesting
+    public static final int WAKE_LOCK_WEIGHT = 50;
+
+    protected Clocks mClocks;
+
+    private final JournaledFile mFile;
+    public final AtomicFile mCheckinFile;
+    public final AtomicFile mDailyFile;
+
+    static final int MSG_UPDATE_WAKELOCKS = 1;
+    static final int MSG_REPORT_POWER_CHANGE = 2;
+    static final int MSG_REPORT_CHARGING = 3;
+    static final long DELAY_UPDATE_WAKELOCKS = 5*1000;
+
+    private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader();
+    private final KernelWakelockStats mTmpWakelockStats = new KernelWakelockStats();
+
+    @VisibleForTesting
+    protected KernelUidCpuTimeReader mKernelUidCpuTimeReader = new KernelUidCpuTimeReader();
+    @VisibleForTesting
+    protected KernelCpuSpeedReader[] mKernelCpuSpeedReaders;
+    @VisibleForTesting
+    protected KernelUidCpuFreqTimeReader mKernelUidCpuFreqTimeReader =
+            new KernelUidCpuFreqTimeReader();
+
+    private final KernelMemoryBandwidthStats mKernelMemoryBandwidthStats
+            = new KernelMemoryBandwidthStats();
+    private final LongSparseArray<SamplingTimer> mKernelMemoryStats = new LongSparseArray<>();
+    public LongSparseArray<SamplingTimer> getKernelMemoryStats() {
+        return mKernelMemoryStats;
+    }
+
+    public interface BatteryCallback {
+        public void batteryNeedsCpuUpdate();
+        public void batteryPowerChanged(boolean onBattery);
+        public void batterySendBroadcast(Intent intent);
+    }
+
+    public interface PlatformIdleStateCallback {
+        public String getPlatformLowPowerStats();
+        public String getSubsystemLowPowerStats();
+    }
+
+    public static abstract class UserInfoProvider {
+        private int[] userIds;
+        protected abstract @Nullable int[] getUserIds();
+        @VisibleForTesting
+        public final void refreshUserIds() {
+            userIds = getUserIds();
+        }
+        @VisibleForTesting
+        public boolean exists(int userId) {
+            return userIds != null ? ArrayUtils.contains(userIds, userId) : true;
+        }
+    }
+
+    private final PlatformIdleStateCallback mPlatformIdleStateCallback;
+
+    final class MyHandler extends Handler {
+        public MyHandler(Looper looper) {
+            super(looper, null, true);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            BatteryCallback cb = mCallback;
+            switch (msg.what) {
+                case MSG_UPDATE_WAKELOCKS:
+                    synchronized (BatteryStatsImpl.this) {
+                        updateCpuTimeLocked();
+                    }
+                    if (cb != null) {
+                        cb.batteryNeedsCpuUpdate();
+                    }
+                    break;
+                case MSG_REPORT_POWER_CHANGE:
+                    if (cb != null) {
+                        cb.batteryPowerChanged(msg.arg1 != 0);
+                    }
+                    break;
+                case MSG_REPORT_CHARGING:
+                    if (cb != null) {
+                        final String action;
+                        synchronized (BatteryStatsImpl.this) {
+                            action = mCharging ? BatteryManager.ACTION_CHARGING
+                                    : BatteryManager.ACTION_DISCHARGING;
+                        }
+                        Intent intent = new Intent(action);
+                        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                        cb.batterySendBroadcast(intent);
+                    }
+                    break;
+            }
+        }
+    }
+
+    public interface Clocks {
+        public long elapsedRealtime();
+        public long uptimeMillis();
+    }
+
+    public static class SystemClocks implements Clocks {
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+
+        public long uptimeMillis() {
+            return SystemClock.uptimeMillis();
+        }
+    }
+
+    public interface ExternalStatsSync {
+        int UPDATE_CPU = 0x01;
+        int UPDATE_WIFI = 0x02;
+        int UPDATE_RADIO = 0x04;
+        int UPDATE_BT = 0x08;
+        int UPDATE_ALL = UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
+
+        Future<?> scheduleSync(String reason, int flags);
+        Future<?> scheduleCpuSyncDueToRemovedUid(int uid);
+    }
+
+    public final MyHandler mHandler;
+    private ExternalStatsSync mExternalSync = null;
+    @VisibleForTesting
+    protected UserInfoProvider mUserInfoProvider = null;
+
+    private BatteryCallback mCallback;
+
+    /**
+     * Mapping isolated uids to the actual owning app uid.
+     */
+    final SparseIntArray mIsolatedUids = new SparseIntArray();
+
+    /**
+     * The statistics we have collected organized by uids.
+     */
+    final SparseArray<BatteryStatsImpl.Uid> mUidStats = new SparseArray<>();
+
+    // A set of pools of currently active timers.  When a timer is queried, we will divide the
+    // elapsed time by the number of active timers to arrive at that timer's share of the time.
+    // In order to do this, we must refresh each timer whenever the number of active timers
+    // changes.
+    @VisibleForTesting
+    protected ArrayList<StopwatchTimer> mPartialTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mFullTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mWindowTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mDrawTimers = new ArrayList<>();
+    final SparseArray<ArrayList<StopwatchTimer>> mSensorTimers = new SparseArray<>();
+    final ArrayList<StopwatchTimer> mWifiRunningTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mFullWifiLockTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mWifiMulticastTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mWifiScanTimers = new ArrayList<>();
+    final SparseArray<ArrayList<StopwatchTimer>> mWifiBatchedScanTimers = new SparseArray<>();
+    final ArrayList<StopwatchTimer> mAudioTurnedOnTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mVideoTurnedOnTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mFlashlightTurnedOnTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mCameraTurnedOnTimers = new ArrayList<>();
+    final ArrayList<StopwatchTimer> mBluetoothScanOnTimers = new ArrayList<>();
+
+    // Last partial timers we use for distributing CPU usage.
+    @VisibleForTesting
+    protected ArrayList<StopwatchTimer> mLastPartialTimers = new ArrayList<>();
+
+    // These are the objects that will want to do something when the device
+    // is unplugged from power.
+    protected final TimeBase mOnBatteryTimeBase = new TimeBase();
+
+    // These are the objects that will want to do something when the device
+    // is unplugged from power *and* the screen is off.
+    final TimeBase mOnBatteryScreenOffTimeBase = new TimeBase();
+
+    // Set to true when we want to distribute CPU across wakelocks for the next
+    // CPU update, even if we aren't currently running wake locks.
+    boolean mDistributeWakelockCpu;
+
+    boolean mShuttingDown;
+
+    final HistoryEventTracker mActiveEvents = new HistoryEventTracker();
+
+    long mHistoryBaseTime;
+    boolean mHaveBatteryLevel = false;
+    boolean mRecordingHistory = false;
+    int mNumHistoryItems;
+
+    final Parcel mHistoryBuffer = Parcel.obtain();
+    final HistoryItem mHistoryLastWritten = new HistoryItem();
+    final HistoryItem mHistoryLastLastWritten = new HistoryItem();
+    final HistoryItem mHistoryReadTmp = new HistoryItem();
+    final HistoryItem mHistoryAddTmp = new HistoryItem();
+    final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap<>();
+    String[] mReadHistoryStrings;
+    int[] mReadHistoryUids;
+    int mReadHistoryChars;
+    int mNextHistoryTagIdx = 0;
+    int mNumHistoryTagChars = 0;
+    int mHistoryBufferLastPos = -1;
+    boolean mHistoryOverflow = false;
+    int mActiveHistoryStates = 0xffffffff;
+    int mActiveHistoryStates2 = 0xffffffff;
+    long mLastHistoryElapsedRealtime = 0;
+    long mTrackRunningHistoryElapsedRealtime = 0;
+    long mTrackRunningHistoryUptime = 0;
+
+    final HistoryItem mHistoryCur = new HistoryItem();
+
+    HistoryItem mHistory;
+    HistoryItem mHistoryEnd;
+    HistoryItem mHistoryLastEnd;
+    HistoryItem mHistoryCache;
+
+    // Used by computeHistoryStepDetails
+    HistoryStepDetails mLastHistoryStepDetails = null;
+    byte mLastHistoryStepLevel = 0;
+    final HistoryStepDetails mCurHistoryStepDetails = new HistoryStepDetails();
+    final HistoryStepDetails mReadHistoryStepDetails = new HistoryStepDetails();
+    final HistoryStepDetails mTmpHistoryStepDetails = new HistoryStepDetails();
+
+    /**
+     * Total time (in milliseconds) spent executing in user code.
+     */
+    long mLastStepCpuUserTime;
+    long mCurStepCpuUserTime;
+    /**
+     * Total time (in milliseconds) spent executing in kernel code.
+     */
+    long mLastStepCpuSystemTime;
+    long mCurStepCpuSystemTime;
+    /**
+     * Times from /proc/stat (but measured in milliseconds).
+     */
+    long mLastStepStatUserTime;
+    long mLastStepStatSystemTime;
+    long mLastStepStatIOWaitTime;
+    long mLastStepStatIrqTime;
+    long mLastStepStatSoftIrqTime;
+    long mLastStepStatIdleTime;
+    long mCurStepStatUserTime;
+    long mCurStepStatSystemTime;
+    long mCurStepStatIOWaitTime;
+    long mCurStepStatIrqTime;
+    long mCurStepStatSoftIrqTime;
+    long mCurStepStatIdleTime;
+
+    private HistoryItem mHistoryIterator;
+    private boolean mReadOverflow;
+    private boolean mIteratingHistory;
+
+    int mStartCount;
+
+    long mStartClockTime;
+    String mStartPlatformVersion;
+    String mEndPlatformVersion;
+
+    long mUptime;
+    long mUptimeStart;
+    long mRealtime;
+    long mRealtimeStart;
+
+    int mWakeLockNesting;
+    boolean mWakeLockImportant;
+    public boolean mRecordAllHistory;
+    boolean mNoAutoReset;
+
+    int mScreenState = Display.STATE_UNKNOWN;
+    StopwatchTimer mScreenOnTimer;
+
+    int mScreenBrightnessBin = -1;
+    final StopwatchTimer[] mScreenBrightnessTimer = new StopwatchTimer[NUM_SCREEN_BRIGHTNESS_BINS];
+
+    boolean mPretendScreenOff;
+
+    boolean mInteractive;
+    StopwatchTimer mInteractiveTimer;
+
+    boolean mPowerSaveModeEnabled;
+    StopwatchTimer mPowerSaveModeEnabledTimer;
+
+    boolean mDeviceIdling;
+    StopwatchTimer mDeviceIdlingTimer;
+
+    boolean mDeviceLightIdling;
+    StopwatchTimer mDeviceLightIdlingTimer;
+
+    int mDeviceIdleMode;
+    long mLastIdleTimeStart;
+    long mLongestLightIdleTime;
+    long mLongestFullIdleTime;
+    StopwatchTimer mDeviceIdleModeLightTimer;
+    StopwatchTimer mDeviceIdleModeFullTimer;
+
+    boolean mPhoneOn;
+    StopwatchTimer mPhoneOnTimer;
+
+    int mAudioOnNesting;
+    StopwatchTimer mAudioOnTimer;
+
+    int mVideoOnNesting;
+    StopwatchTimer mVideoOnTimer;
+
+    int mFlashlightOnNesting;
+    StopwatchTimer mFlashlightOnTimer;
+
+    int mCameraOnNesting;
+    StopwatchTimer mCameraOnTimer;
+
+    int mPhoneSignalStrengthBin = -1;
+    int mPhoneSignalStrengthBinRaw = -1;
+    final StopwatchTimer[] mPhoneSignalStrengthsTimer =
+            new StopwatchTimer[SignalStrength.NUM_SIGNAL_STRENGTH_BINS];
+
+    StopwatchTimer mPhoneSignalScanningTimer;
+
+    int mPhoneDataConnectionType = -1;
+    final StopwatchTimer[] mPhoneDataConnectionsTimer =
+            new StopwatchTimer[NUM_DATA_CONNECTION_TYPES];
+
+    final LongSamplingCounter[] mNetworkByteActivityCounters =
+            new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
+    final LongSamplingCounter[] mNetworkPacketActivityCounters =
+            new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
+
+    /**
+     * The WiFi controller activity (time in tx, rx, idle, and power consumed) for the device.
+     */
+    ControllerActivityCounterImpl mWifiActivity;
+
+    /**
+     * The Bluetooth controller activity (time in tx, rx, idle, and power consumed) for the device.
+     */
+    ControllerActivityCounterImpl mBluetoothActivity;
+
+    /**
+     * The Modem controller activity (time in tx, rx, idle, and power consumed) for the device.
+     */
+    ControllerActivityCounterImpl mModemActivity;
+
+    /**
+     * Whether the device supports WiFi controller energy reporting. This is set to true on
+     * the first WiFi energy report. See {@link #mWifiActivity}.
+     */
+    boolean mHasWifiReporting = false;
+
+    /**
+     * Whether the device supports Bluetooth controller energy reporting. This is set to true on
+     * the first Bluetooth energy report. See {@link #mBluetoothActivity}.
+     */
+    boolean mHasBluetoothReporting = false;
+
+    /**
+     * Whether the device supports Modem controller energy reporting. This is set to true on
+     * the first Modem energy report. See {@link #mModemActivity}.
+     */
+    boolean mHasModemReporting = false;
+
+    boolean mWifiOn;
+    StopwatchTimer mWifiOnTimer;
+
+    boolean mGlobalWifiRunning;
+    StopwatchTimer mGlobalWifiRunningTimer;
+
+    int mWifiState = -1;
+    final StopwatchTimer[] mWifiStateTimer = new StopwatchTimer[NUM_WIFI_STATES];
+
+    int mWifiSupplState = -1;
+    final StopwatchTimer[] mWifiSupplStateTimer = new StopwatchTimer[NUM_WIFI_SUPPL_STATES];
+
+    int mWifiSignalStrengthBin = -1;
+    final StopwatchTimer[] mWifiSignalStrengthsTimer =
+            new StopwatchTimer[NUM_WIFI_SIGNAL_STRENGTH_BINS];
+
+    int mBluetoothScanNesting;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    protected StopwatchTimer mBluetoothScanTimer;
+
+    int mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+    long mMobileRadioActiveStartTime;
+    StopwatchTimer mMobileRadioActiveTimer;
+    StopwatchTimer mMobileRadioActivePerAppTimer;
+    LongSamplingCounter mMobileRadioActiveAdjustedTime;
+    LongSamplingCounter mMobileRadioActiveUnknownTime;
+    LongSamplingCounter mMobileRadioActiveUnknownCount;
+
+    int mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+
+    /**
+     * These provide time bases that discount the time the device is plugged
+     * in to power.
+     */
+    boolean mOnBattery;
+    @VisibleForTesting
+    protected boolean mOnBatteryInternal;
+
+    /**
+     * External reporting of whether the device is actually charging.
+     */
+    boolean mCharging = true;
+    int mLastChargingStateLevel;
+
+    /*
+     * These keep track of battery levels (1-100) at the last plug event and the last unplug event.
+     */
+    int mDischargeStartLevel;
+    int mDischargeUnplugLevel;
+    int mDischargePlugLevel;
+    int mDischargeCurrentLevel;
+    int mCurrentBatteryLevel;
+    int mLowDischargeAmountSinceCharge;
+    int mHighDischargeAmountSinceCharge;
+    int mDischargeScreenOnUnplugLevel;
+    int mDischargeScreenOffUnplugLevel;
+    int mDischargeAmountScreenOn;
+    int mDischargeAmountScreenOnSinceCharge;
+    int mDischargeAmountScreenOff;
+    int mDischargeAmountScreenOffSinceCharge;
+
+    private LongSamplingCounter mDischargeScreenOffCounter;
+    private LongSamplingCounter mDischargeCounter;
+
+    static final int MAX_LEVEL_STEPS = 200;
+
+    int mInitStepMode = 0;
+    int mCurStepMode = 0;
+    int mModStepMode = 0;
+
+    int mLastDischargeStepLevel;
+    int mMinDischargeStepLevel;
+    final LevelStepTracker mDischargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS);
+    final LevelStepTracker mDailyDischargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS*2);
+    ArrayList<PackageChange> mDailyPackageChanges;
+
+    int mLastChargeStepLevel;
+    int mMaxChargeStepLevel;
+    final LevelStepTracker mChargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS);
+    final LevelStepTracker mDailyChargeStepTracker = new LevelStepTracker(MAX_LEVEL_STEPS*2);
+
+    static final int MAX_DAILY_ITEMS = 10;
+
+    long mDailyStartTime = 0;
+    long mNextMinDailyDeadline = 0;
+    long mNextMaxDailyDeadline = 0;
+
+    final ArrayList<DailyItem> mDailyItems = new ArrayList<>();
+
+    long mLastWriteTime = 0; // Milliseconds
+
+    private int mPhoneServiceState = -1;
+    private int mPhoneServiceStateRaw = -1;
+    private int mPhoneSimStateRaw = -1;
+
+    private int mNumConnectivityChange;
+    private int mLoadedNumConnectivityChange;
+    private int mUnpluggedNumConnectivityChange;
+
+    private int mEstimatedBatteryCapacity = -1;
+
+    private int mMinLearnedBatteryCapacity = -1;
+    private int mMaxLearnedBatteryCapacity = -1;
+
+    private long[] mCpuFreqs;
+
+    @VisibleForTesting
+    protected PowerProfile mPowerProfile;
+
+    /*
+     * Holds a SamplingTimer associated with each kernel wakelock name being tracked.
+     */
+    private final HashMap<String, SamplingTimer> mKernelWakelockStats = new HashMap<>();
+
+    public Map<String, ? extends Timer> getKernelWakelockStats() {
+        return mKernelWakelockStats;
+    }
+
+    String mLastWakeupReason = null;
+    long mLastWakeupUptimeMs = 0;
+    private final HashMap<String, SamplingTimer> mWakeupReasonStats = new HashMap<>();
+
+    public Map<String, ? extends Timer> getWakeupReasonStats() {
+        return mWakeupReasonStats;
+    }
+
+    @Override
+    public LongCounter getDischargeScreenOffCoulombCounter() {
+        return mDischargeScreenOffCounter;
+    }
+
+    @Override
+    public LongCounter getDischargeCoulombCounter() {
+        return mDischargeCounter;
+    }
+
+    @Override
+    public int getEstimatedBatteryCapacity() {
+        return mEstimatedBatteryCapacity;
+    }
+
+    @Override
+    public int getMinLearnedBatteryCapacity() {
+        return mMinLearnedBatteryCapacity;
+    }
+
+    @Override
+    public int getMaxLearnedBatteryCapacity() {
+        return mMaxLearnedBatteryCapacity;
+    }
+
+    public BatteryStatsImpl() {
+        this(new SystemClocks());
+    }
+
+    public BatteryStatsImpl(Clocks clocks) {
+        init(clocks);
+        mFile = null;
+        mCheckinFile = null;
+        mDailyFile = null;
+        mHandler = null;
+        mPlatformIdleStateCallback = null;
+        mUserInfoProvider = null;
+        clearHistoryLocked();
+    }
+
+    private void init(Clocks clocks) {
+        mClocks = clocks;
+    }
+
+    public interface TimeBaseObs {
+        void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime);
+        void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime);
+    }
+
+    // methods are protected not private to be VisibleForTesting
+    public static class TimeBase {
+        protected final ArrayList<TimeBaseObs> mObservers = new ArrayList<>();
+
+        protected long mUptime;
+        protected long mRealtime;
+
+        protected boolean mRunning;
+
+        protected long mPastUptime;
+        protected long mUptimeStart;
+        protected long mPastRealtime;
+        protected long mRealtimeStart;
+        protected long mUnpluggedUptime;
+        protected long mUnpluggedRealtime;
+
+        public void dump(PrintWriter pw, String prefix) {
+            StringBuilder sb = new StringBuilder(128);
+            pw.print(prefix); pw.print("mRunning="); pw.println(mRunning);
+            sb.setLength(0);
+            sb.append(prefix);
+                    sb.append("mUptime=");
+                    formatTimeMs(sb, mUptime / 1000);
+            pw.println(sb.toString());
+            sb.setLength(0);
+            sb.append(prefix);
+                    sb.append("mRealtime=");
+                    formatTimeMs(sb, mRealtime / 1000);
+            pw.println(sb.toString());
+            sb.setLength(0);
+            sb.append(prefix);
+                    sb.append("mPastUptime=");
+                    formatTimeMs(sb, mPastUptime / 1000); sb.append("mUptimeStart=");
+                    formatTimeMs(sb, mUptimeStart / 1000);
+                    sb.append("mUnpluggedUptime="); formatTimeMs(sb, mUnpluggedUptime / 1000);
+            pw.println(sb.toString());
+            sb.setLength(0);
+            sb.append(prefix);
+                    sb.append("mPastRealtime=");
+                    formatTimeMs(sb, mPastRealtime / 1000); sb.append("mRealtimeStart=");
+                    formatTimeMs(sb, mRealtimeStart / 1000);
+                    sb.append("mUnpluggedRealtime="); formatTimeMs(sb, mUnpluggedRealtime / 1000);
+            pw.println(sb.toString());
+        }
+
+        public void add(TimeBaseObs observer) {
+            mObservers.add(observer);
+        }
+
+        public void remove(TimeBaseObs observer) {
+            if (!mObservers.remove(observer)) {
+                Slog.wtf(TAG, "Removed unknown observer: " + observer);
+            }
+        }
+
+        public boolean hasObserver(TimeBaseObs observer) {
+            return mObservers.contains(observer);
+        }
+
+        public void init(long uptime, long realtime) {
+            mRealtime = 0;
+            mUptime = 0;
+            mPastUptime = 0;
+            mPastRealtime = 0;
+            mUptimeStart = uptime;
+            mRealtimeStart = realtime;
+            mUnpluggedUptime = getUptime(mUptimeStart);
+            mUnpluggedRealtime = getRealtime(mRealtimeStart);
+        }
+
+        public void reset(long uptime, long realtime) {
+            if (!mRunning) {
+                mPastUptime = 0;
+                mPastRealtime = 0;
+            } else {
+                mUptimeStart = uptime;
+                mRealtimeStart = realtime;
+                // TODO: Since mUptimeStart was just reset and we are running, getUptime will
+                // just return mPastUptime. Also, are we sure we don't want to reset that?
+                mUnpluggedUptime = getUptime(uptime);
+                // TODO: likewise.
+                mUnpluggedRealtime = getRealtime(realtime);
+            }
+        }
+
+        public long computeUptime(long curTime, int which) {
+            switch (which) {
+                case STATS_SINCE_CHARGED:
+                    return mUptime + getUptime(curTime);
+                case STATS_CURRENT:
+                    return getUptime(curTime);
+                case STATS_SINCE_UNPLUGGED:
+                    return getUptime(curTime) - mUnpluggedUptime;
+            }
+            return 0;
+        }
+
+        public long computeRealtime(long curTime, int which) {
+            switch (which) {
+                case STATS_SINCE_CHARGED:
+                    return mRealtime + getRealtime(curTime);
+                case STATS_CURRENT:
+                    return getRealtime(curTime);
+                case STATS_SINCE_UNPLUGGED:
+                    return getRealtime(curTime) - mUnpluggedRealtime;
+            }
+            return 0;
+        }
+
+        public long getUptime(long curTime) {
+            long time = mPastUptime;
+            if (mRunning) {
+                time += curTime - mUptimeStart;
+            }
+            return time;
+        }
+
+        public long getRealtime(long curTime) {
+            long time = mPastRealtime;
+            if (mRunning) {
+                time += curTime - mRealtimeStart;
+            }
+            return time;
+        }
+
+        public long getUptimeStart() {
+            return mUptimeStart;
+        }
+
+        public long getRealtimeStart() {
+            return mRealtimeStart;
+        }
+
+        public boolean isRunning() {
+            return mRunning;
+        }
+
+        public boolean setRunning(boolean running, long uptime, long realtime) {
+            if (mRunning != running) {
+                mRunning = running;
+                if (running) {
+                    mUptimeStart = uptime;
+                    mRealtimeStart = realtime;
+                    long batteryUptime = mUnpluggedUptime = getUptime(uptime);
+                    long batteryRealtime = mUnpluggedRealtime = getRealtime(realtime);
+
+                    for (int i = mObservers.size() - 1; i >= 0; i--) {
+                        mObservers.get(i).onTimeStarted(realtime, batteryUptime, batteryRealtime);
+                    }
+                } else {
+                    mPastUptime += uptime - mUptimeStart;
+                    mPastRealtime += realtime - mRealtimeStart;
+
+                    long batteryUptime = getUptime(uptime);
+                    long batteryRealtime = getRealtime(realtime);
+
+                    for (int i = mObservers.size() - 1; i >= 0; i--) {
+                        mObservers.get(i).onTimeStopped(realtime, batteryUptime, batteryRealtime);
+                    }
+                }
+                return true;
+            }
+            return false;
+        }
+
+        public void readSummaryFromParcel(Parcel in) {
+            mUptime = in.readLong();
+            mRealtime = in.readLong();
+        }
+
+        public void writeSummaryToParcel(Parcel out, long uptime, long realtime) {
+            out.writeLong(computeUptime(uptime, STATS_SINCE_CHARGED));
+            out.writeLong(computeRealtime(realtime, STATS_SINCE_CHARGED));
+        }
+
+        public void readFromParcel(Parcel in) {
+            mRunning = false;
+            mUptime = in.readLong();
+            mPastUptime = in.readLong();
+            mUptimeStart = in.readLong();
+            mRealtime = in.readLong();
+            mPastRealtime = in.readLong();
+            mRealtimeStart = in.readLong();
+            mUnpluggedUptime = in.readLong();
+            mUnpluggedRealtime = in.readLong();
+        }
+
+        public void writeToParcel(Parcel out, long uptime, long realtime) {
+            final long runningUptime = getUptime(uptime);
+            final long runningRealtime = getRealtime(realtime);
+            out.writeLong(mUptime);
+            out.writeLong(runningUptime);
+            out.writeLong(mUptimeStart);
+            out.writeLong(mRealtime);
+            out.writeLong(runningRealtime);
+            out.writeLong(mRealtimeStart);
+            out.writeLong(mUnpluggedUptime);
+            out.writeLong(mUnpluggedRealtime);
+        }
+    }
+
+    /**
+     * State for keeping track of counting information.
+     */
+    public static class Counter extends BatteryStats.Counter implements TimeBaseObs {
+        final AtomicInteger mCount = new AtomicInteger();
+        final TimeBase mTimeBase;
+        int mLoadedCount;
+        int mUnpluggedCount;
+        int mPluggedCount;
+
+        public Counter(TimeBase timeBase, Parcel in) {
+            mTimeBase = timeBase;
+            mPluggedCount = in.readInt();
+            mCount.set(mPluggedCount);
+            mLoadedCount = in.readInt();
+            mUnpluggedCount = in.readInt();
+            timeBase.add(this);
+        }
+
+        public Counter(TimeBase timeBase) {
+            mTimeBase = timeBase;
+            timeBase.add(this);
+        }
+
+        public void writeToParcel(Parcel out) {
+            out.writeInt(mCount.get());
+            out.writeInt(mLoadedCount);
+            out.writeInt(mUnpluggedCount);
+        }
+
+        @Override
+        public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            mUnpluggedCount = mPluggedCount;
+        }
+
+        @Override
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            mPluggedCount = mCount.get();
+        }
+
+        /**
+         * Writes a possibly null Counter to a Parcel.
+         *
+         * @param out the Parcel to be written to.
+         * @param counter a Counter, or null.
+         */
+        public static void writeCounterToParcel(Parcel out, Counter counter) {
+            if (counter == null) {
+                out.writeInt(0); // indicates null
+                return;
+            }
+            out.writeInt(1); // indicates non-null
+
+            counter.writeToParcel(out);
+        }
+
+        @Override
+        public int getCountLocked(int which) {
+            int val = mCount.get();
+            if (which == STATS_SINCE_UNPLUGGED) {
+                val -= mUnpluggedCount;
+            } else if (which != STATS_SINCE_CHARGED) {
+                val -= mLoadedCount;
+            }
+
+            return val;
+        }
+
+        public void logState(Printer pw, String prefix) {
+            pw.println(prefix + "mCount=" + mCount.get()
+                    + " mLoadedCount=" + mLoadedCount
+                    + " mUnpluggedCount=" + mUnpluggedCount
+                    + " mPluggedCount=" + mPluggedCount);
+        }
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void stepAtomic() {
+            if (mTimeBase.isRunning()) {
+                mCount.incrementAndGet();
+            }
+        }
+
+        void addAtomic(int delta) {
+            if (mTimeBase.isRunning()) {
+                mCount.addAndGet(delta);
+            }
+        }
+
+        /**
+         * Clear state of this counter.
+         */
+        void reset(boolean detachIfReset) {
+            mCount.set(0);
+            mLoadedCount = mPluggedCount = mUnpluggedCount = 0;
+            if (detachIfReset) {
+                detach();
+            }
+        }
+
+        void detach() {
+            mTimeBase.remove(this);
+        }
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void writeSummaryFromParcelLocked(Parcel out) {
+            int count = mCount.get();
+            out.writeInt(count);
+        }
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void readSummaryFromParcelLocked(Parcel in) {
+            mLoadedCount = in.readInt();
+            mCount.set(mLoadedCount);
+            mUnpluggedCount = mPluggedCount = mLoadedCount;
+        }
+    }
+
+    @VisibleForTesting
+    public static class LongSamplingCounterArray extends LongCounterArray implements TimeBaseObs {
+        final TimeBase mTimeBase;
+        public long[] mCounts;
+        public long[] mLoadedCounts;
+        public long[] mUnpluggedCounts;
+        public long[] mPluggedCounts;
+
+        private LongSamplingCounterArray(TimeBase timeBase, Parcel in) {
+            mTimeBase = timeBase;
+            mPluggedCounts = in.createLongArray();
+            mCounts = copyArray(mPluggedCounts, mCounts);
+            mLoadedCounts = in.createLongArray();
+            mUnpluggedCounts = in.createLongArray();
+            timeBase.add(this);
+        }
+
+        public LongSamplingCounterArray(TimeBase timeBase) {
+            mTimeBase = timeBase;
+            timeBase.add(this);
+        }
+
+        private void writeToParcel(Parcel out) {
+            out.writeLongArray(mCounts);
+            out.writeLongArray(mLoadedCounts);
+            out.writeLongArray(mUnpluggedCounts);
+        }
+
+        @Override
+        public void onTimeStarted(long elapsedRealTime, long baseUptime, long baseRealtime) {
+            mUnpluggedCounts = copyArray(mPluggedCounts, mUnpluggedCounts);
+        }
+
+        @Override
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            mPluggedCounts = copyArray(mCounts, mPluggedCounts);
+        }
+
+        @Override
+        public long[] getCountsLocked(int which) {
+            long[] val = copyArray(mTimeBase.isRunning() ? mCounts : mPluggedCounts, null);
+            if (which == STATS_SINCE_UNPLUGGED) {
+                subtract(val, mUnpluggedCounts);
+            } else if (which != STATS_SINCE_CHARGED) {
+                subtract(val, mLoadedCounts);
+            }
+            return val;
+        }
+
+        @Override
+        public void logState(Printer pw, String prefix) {
+            pw.println(prefix + "mCounts=" + Arrays.toString(mCounts)
+                    + " mLoadedCounts=" + Arrays.toString(mLoadedCounts)
+                    + " mUnpluggedCounts=" + Arrays.toString(mUnpluggedCounts)
+                    + " mPluggedCounts=" + Arrays.toString(mPluggedCounts));
+        }
+
+        public void addCountLocked(long[] counts) {
+            if (counts == null) {
+                return;
+            }
+            if (mTimeBase.isRunning()) {
+                if (mCounts == null) {
+                    mCounts = new long[counts.length];
+                }
+                for (int i = 0; i < counts.length; ++i) {
+                    mCounts[i] += counts[i];
+                }
+            }
+        }
+
+        public int getSize() {
+            return mCounts == null ? 0 : mCounts.length;
+        }
+
+        /**
+         * Clear state of this counter.
+         */
+        public void reset(boolean detachIfReset) {
+            fillArray(mCounts, 0);
+            fillArray(mLoadedCounts, 0);
+            fillArray(mPluggedCounts, 0);
+            fillArray(mUnpluggedCounts, 0);
+            if (detachIfReset) {
+                detach();
+            }
+        }
+
+        public void detach() {
+            mTimeBase.remove(this);
+        }
+
+        private void writeSummaryToParcelLocked(Parcel out) {
+            out.writeLongArray(mCounts);
+        }
+
+        private void readSummaryFromParcelLocked(Parcel in) {
+            mCounts = in.createLongArray();
+            mLoadedCounts = copyArray(mCounts, mLoadedCounts);
+            mUnpluggedCounts = copyArray(mCounts, mUnpluggedCounts);
+            mPluggedCounts = copyArray(mCounts, mPluggedCounts);
+        }
+
+        public static void writeToParcel(Parcel out, LongSamplingCounterArray counterArray) {
+            if (counterArray != null) {
+                out.writeInt(1);
+                counterArray.writeToParcel(out);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        public static LongSamplingCounterArray readFromParcel(Parcel in, TimeBase timeBase) {
+            if (in.readInt() != 0) {
+                return new LongSamplingCounterArray(timeBase, in);
+            } else {
+                return null;
+            }
+        }
+
+        public static void writeSummaryToParcelLocked(Parcel out,
+                LongSamplingCounterArray counterArray) {
+            if (counterArray != null) {
+                out.writeInt(1);
+                counterArray.writeSummaryToParcelLocked(out);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        public static LongSamplingCounterArray readSummaryFromParcelLocked(Parcel in,
+                TimeBase timeBase) {
+            if (in.readInt() != 0) {
+                final LongSamplingCounterArray counterArray
+                        = new LongSamplingCounterArray(timeBase);
+                counterArray.readSummaryFromParcelLocked(in);
+                return counterArray;
+            } else {
+                return null;
+            }
+        }
+
+        private static void fillArray(long[] a, long val) {
+            if (a != null) {
+                Arrays.fill(a, val);
+            }
+        }
+
+        private static void subtract(@NonNull long[] val, long[] toSubtract) {
+            if (toSubtract == null) {
+                return;
+            }
+            for (int i = 0; i < val.length; i++) {
+                val[i] -= toSubtract[i];
+            }
+        }
+
+        private static long[] copyArray(long[] src, long[] dest) {
+            if (src == null) {
+                return null;
+            } else {
+                if (dest == null) {
+                    dest = new long[src.length];
+                }
+                System.arraycopy(src, 0, dest, 0, src.length);
+                return dest;
+            }
+        }
+    }
+
+    public static class LongSamplingCounter extends LongCounter implements TimeBaseObs {
+        final TimeBase mTimeBase;
+        long mCount;
+        long mLoadedCount;
+        long mUnpluggedCount;
+        long mPluggedCount;
+
+        LongSamplingCounter(TimeBase timeBase, Parcel in) {
+            mTimeBase = timeBase;
+            mPluggedCount = in.readLong();
+            mCount = mPluggedCount;
+            mLoadedCount = in.readLong();
+            mUnpluggedCount = in.readLong();
+            timeBase.add(this);
+        }
+
+        LongSamplingCounter(TimeBase timeBase) {
+            mTimeBase = timeBase;
+            timeBase.add(this);
+        }
+
+        public void writeToParcel(Parcel out) {
+            out.writeLong(mCount);
+            out.writeLong(mLoadedCount);
+            out.writeLong(mUnpluggedCount);
+        }
+
+        @Override
+        public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            mUnpluggedCount = mPluggedCount;
+        }
+
+        @Override
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            mPluggedCount = mCount;
+        }
+
+        public long getCountLocked(int which) {
+            long val = mTimeBase.isRunning() ? mCount : mPluggedCount;
+            if (which == STATS_SINCE_UNPLUGGED) {
+                val -= mUnpluggedCount;
+            } else if (which != STATS_SINCE_CHARGED) {
+                val -= mLoadedCount;
+            }
+            return val;
+        }
+
+        @Override
+        public void logState(Printer pw, String prefix) {
+            pw.println(prefix + "mCount=" + mCount
+                    + " mLoadedCount=" + mLoadedCount
+                    + " mUnpluggedCount=" + mUnpluggedCount
+                    + " mPluggedCount=" + mPluggedCount);
+        }
+
+        void addCountLocked(long count) {
+            if (mTimeBase.isRunning()) {
+                mCount += count;
+            }
+        }
+
+        /**
+         * Clear state of this counter.
+         */
+        void reset(boolean detachIfReset) {
+            mCount = 0;
+            mLoadedCount = mPluggedCount = mUnpluggedCount = 0;
+            if (detachIfReset) {
+                detach();
+            }
+        }
+
+        void detach() {
+            mTimeBase.remove(this);
+        }
+
+        void writeSummaryFromParcelLocked(Parcel out) {
+            out.writeLong(mCount);
+        }
+
+        void readSummaryFromParcelLocked(Parcel in) {
+            mLoadedCount = in.readLong();
+            mCount = mLoadedCount;
+            mUnpluggedCount = mPluggedCount = mLoadedCount;
+        }
+    }
+
+    /**
+     * State for keeping track of timing information.
+     */
+    public static abstract class Timer extends BatteryStats.Timer implements TimeBaseObs {
+        protected final Clocks mClocks;
+        protected final int mType;
+        protected final TimeBase mTimeBase;
+
+        protected int mCount;
+        protected int mLoadedCount;
+        protected int mLastCount;
+        protected int mUnpluggedCount;
+
+        // Times are in microseconds for better accuracy when dividing by the
+        // lock count, and are in "battery realtime" units.
+
+        /**
+         * The total time we have accumulated since the start of the original
+         * boot, to the last time something interesting happened in the
+         * current run.
+         */
+        protected long mTotalTime;
+
+        /**
+         * The total time we loaded for the previous runs.  Subtract this from
+         * mTotalTime to find the time for the current run of the system.
+         */
+        protected long mLoadedTime;
+
+        /**
+         * The run time of the last run of the system, as loaded from the
+         * saved data.
+         */
+        protected long mLastTime;
+
+        /**
+         * The value of mTotalTime when unplug() was last called.  Subtract
+         * this from mTotalTime to find the time since the last unplug from
+         * power.
+         */
+        protected long mUnpluggedTime;
+
+        /**
+         * The total time this timer has been running until the latest mark has been set.
+         * Subtract this from mTotalTime to get the time spent running since the mark was set.
+         */
+        protected long mTimeBeforeMark;
+
+        /**
+         * Constructs from a parcel.
+         * @param type
+         * @param timeBase
+         * @param in
+         */
+        public Timer(Clocks clocks, int type, TimeBase timeBase, Parcel in) {
+            mClocks = clocks;
+            mType = type;
+            mTimeBase = timeBase;
+
+            mCount = in.readInt();
+            mLoadedCount = in.readInt();
+            mLastCount = 0;
+            mUnpluggedCount = in.readInt();
+            mTotalTime = in.readLong();
+            mLoadedTime = in.readLong();
+            mLastTime = 0;
+            mUnpluggedTime = in.readLong();
+            mTimeBeforeMark = in.readLong();
+            timeBase.add(this);
+            if (DEBUG) Log.i(TAG, "**** READ TIMER #" + mType + ": mTotalTime=" + mTotalTime);
+        }
+
+        public Timer(Clocks clocks, int type, TimeBase timeBase) {
+            mClocks = clocks;
+            mType = type;
+            mTimeBase = timeBase;
+            timeBase.add(this);
+        }
+
+        protected abstract long computeRunTimeLocked(long curBatteryRealtime);
+
+        protected abstract int computeCurrentCountLocked();
+
+        /**
+         * Clear state of this timer.  Returns true if the timer is inactive
+         * so can be completely dropped.
+         */
+        public boolean reset(boolean detachIfReset) {
+            mTotalTime = mLoadedTime = mLastTime = mTimeBeforeMark = 0;
+            mCount = mLoadedCount = mLastCount = 0;
+            if (detachIfReset) {
+                detach();
+            }
+            return true;
+        }
+
+        public void detach() {
+            mTimeBase.remove(this);
+        }
+
+        public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
+            if (DEBUG) Log.i(TAG, "**** WRITING TIMER #" + mType + ": mTotalTime="
+                    + computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs)));
+            out.writeInt(computeCurrentCountLocked());
+            out.writeInt(mLoadedCount);
+            out.writeInt(mUnpluggedCount);
+            out.writeLong(computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs)));
+            out.writeLong(mLoadedTime);
+            out.writeLong(mUnpluggedTime);
+            out.writeLong(mTimeBeforeMark);
+        }
+
+        @Override
+        public void onTimeStarted(long elapsedRealtime, long timeBaseUptime, long baseRealtime) {
+            if (DEBUG && mType < 0) {
+                Log.v(TAG, "unplug #" + mType + ": realtime=" + baseRealtime
+                        + " old mUnpluggedTime=" + mUnpluggedTime
+                        + " old mUnpluggedCount=" + mUnpluggedCount);
+            }
+            mUnpluggedTime = computeRunTimeLocked(baseRealtime);
+            mUnpluggedCount = computeCurrentCountLocked();
+            if (DEBUG && mType < 0) {
+                Log.v(TAG, "unplug #" + mType
+                        + ": new mUnpluggedTime=" + mUnpluggedTime
+                        + " new mUnpluggedCount=" + mUnpluggedCount);
+            }
+        }
+
+        @Override
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            if (DEBUG && mType < 0) {
+                Log.v(TAG, "plug #" + mType + ": realtime=" + baseRealtime
+                        + " old mTotalTime=" + mTotalTime);
+            }
+            mTotalTime = computeRunTimeLocked(baseRealtime);
+            mCount = computeCurrentCountLocked();
+            if (DEBUG && mType < 0) {
+                Log.v(TAG, "plug #" + mType
+                        + ": new mTotalTime=" + mTotalTime);
+            }
+        }
+
+        /**
+         * Writes a possibly null Timer to a Parcel.
+         *
+         * @param out the Parcel to be written to.
+         * @param timer a Timer, or null.
+         */
+        public static void writeTimerToParcel(Parcel out, Timer timer, long elapsedRealtimeUs) {
+            if (timer == null) {
+                out.writeInt(0); // indicates null
+                return;
+            }
+            out.writeInt(1); // indicates non-null
+
+            timer.writeToParcel(out, elapsedRealtimeUs);
+        }
+
+        @Override
+        public long getTotalTimeLocked(long elapsedRealtimeUs, int which) {
+            long val = computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs));
+            if (which == STATS_SINCE_UNPLUGGED) {
+                val -= mUnpluggedTime;
+            } else if (which != STATS_SINCE_CHARGED) {
+                val -= mLoadedTime;
+            }
+
+            return val;
+        }
+
+        @Override
+        public int getCountLocked(int which) {
+            int val = computeCurrentCountLocked();
+            if (which == STATS_SINCE_UNPLUGGED) {
+                val -= mUnpluggedCount;
+            } else if (which != STATS_SINCE_CHARGED) {
+                val -= mLoadedCount;
+            }
+
+            return val;
+        }
+
+        @Override
+        public long getTimeSinceMarkLocked(long elapsedRealtimeUs) {
+            long val = computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs));
+            return val - mTimeBeforeMark;
+        }
+
+        @Override
+        public void logState(Printer pw, String prefix) {
+            pw.println(prefix + "mCount=" + mCount
+                    + " mLoadedCount=" + mLoadedCount + " mLastCount=" + mLastCount
+                    + " mUnpluggedCount=" + mUnpluggedCount);
+            pw.println(prefix + "mTotalTime=" + mTotalTime
+                    + " mLoadedTime=" + mLoadedTime);
+            pw.println(prefix + "mLastTime=" + mLastTime
+                    + " mUnpluggedTime=" + mUnpluggedTime);
+        }
+
+
+        public void writeSummaryFromParcelLocked(Parcel out, long elapsedRealtimeUs) {
+            long runTime = computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs));
+            out.writeLong(runTime);
+            out.writeInt(computeCurrentCountLocked());
+        }
+
+        public void readSummaryFromParcelLocked(Parcel in) {
+            // Multiply by 1000 for backwards compatibility
+            mTotalTime = mLoadedTime = in.readLong();
+            mLastTime = 0;
+            mUnpluggedTime = mTotalTime;
+            mCount = mLoadedCount = in.readInt();
+            mLastCount = 0;
+            mUnpluggedCount = mCount;
+
+            // When reading the summary, we set the mark to be the latest information.
+            mTimeBeforeMark = mTotalTime;
+        }
+    }
+
+    /**
+     * A counter meant to accept monotonically increasing values to its {@link #update(long, int)}
+     * method. The state of the timer according to its {@link TimeBase} will determine how much
+     * of the value is recorded.
+     *
+     * If the value being recorded resets, {@link #endSample()} can be called in order to
+     * account for the change. If the value passed in to {@link #update(long, int)} decreased
+     * between calls, the {@link #endSample()} is automatically called and the new value is
+     * expected to increase monotonically from that point on.
+     */
+    public static class SamplingTimer extends Timer {
+
+        /**
+         * The most recent reported count from /proc/wakelocks.
+         */
+        int mCurrentReportedCount;
+
+        /**
+         * The reported count from /proc/wakelocks when unplug() was last
+         * called.
+         */
+        int mUnpluggedReportedCount;
+
+        /**
+         * The most recent reported total_time from /proc/wakelocks.
+         */
+        long mCurrentReportedTotalTime;
+
+
+        /**
+         * The reported total_time from /proc/wakelocks when unplug() was last
+         * called.
+         */
+        long mUnpluggedReportedTotalTime;
+
+        /**
+         * Whether we are currently in a discharge cycle.
+         */
+        boolean mTimeBaseRunning;
+
+        /**
+         * Whether we are currently recording reported values.
+         */
+        boolean mTrackingReportedValues;
+
+        /*
+         * A sequence counter, incremented once for each update of the stats.
+         */
+        int mUpdateVersion;
+
+        @VisibleForTesting
+        public SamplingTimer(Clocks clocks, TimeBase timeBase, Parcel in) {
+            super(clocks, 0, timeBase, in);
+            mCurrentReportedCount = in.readInt();
+            mUnpluggedReportedCount = in.readInt();
+            mCurrentReportedTotalTime = in.readLong();
+            mUnpluggedReportedTotalTime = in.readLong();
+            mTrackingReportedValues = in.readInt() == 1;
+            mTimeBaseRunning = timeBase.isRunning();
+        }
+
+        @VisibleForTesting
+        public SamplingTimer(Clocks clocks, TimeBase timeBase) {
+            super(clocks, 0, timeBase);
+            mTrackingReportedValues = false;
+            mTimeBaseRunning = timeBase.isRunning();
+        }
+
+        /**
+         * Ends the current sample, allowing subsequent values to {@link #update(long, int)} to
+         * be less than the values used for a previous invocation.
+         */
+        public void endSample() {
+            mTotalTime = computeRunTimeLocked(0 /* unused by us */);
+            mCount = computeCurrentCountLocked();
+            mUnpluggedReportedTotalTime = mCurrentReportedTotalTime = 0;
+            mUnpluggedReportedCount = mCurrentReportedCount = 0;
+        }
+
+        public void setUpdateVersion(int version) {
+            mUpdateVersion = version;
+        }
+
+        public int getUpdateVersion() {
+            return mUpdateVersion;
+        }
+
+        /**
+         * Updates the current recorded values. These are meant to be monotonically increasing
+         * and cumulative. If you are dealing with deltas, use {@link #add(long, int)}.
+         *
+         * If the values being recorded have been reset, the monotonically increasing requirement
+         * will be broken. In this case, {@link #endSample()} is automatically called and
+         * the total value of totalTime and count are recorded, starting a new monotonically
+         * increasing sample.
+         *
+         * @param totalTime total time of sample in microseconds.
+         * @param count total number of times the event being sampled occurred.
+         */
+        public void update(long totalTime, int count) {
+            if (mTimeBaseRunning && !mTrackingReportedValues) {
+                // Updating the reported value for the first time.
+                mUnpluggedReportedTotalTime = totalTime;
+                mUnpluggedReportedCount = count;
+            }
+
+            mTrackingReportedValues = true;
+
+            if (totalTime < mCurrentReportedTotalTime || count < mCurrentReportedCount) {
+                endSample();
+            }
+
+            mCurrentReportedTotalTime = totalTime;
+            mCurrentReportedCount = count;
+        }
+
+        /**
+         * Adds deltaTime and deltaCount to the current sample.
+         *
+         * @param deltaTime additional time recorded since the last sampled event, in microseconds.
+         * @param deltaCount additional number of times the event being sampled occurred.
+         */
+        public void add(long deltaTime, int deltaCount) {
+            update(mCurrentReportedTotalTime + deltaTime, mCurrentReportedCount + deltaCount);
+        }
+
+        @Override
+        public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            super.onTimeStarted(elapsedRealtime, baseUptime, baseRealtime);
+            if (mTrackingReportedValues) {
+                mUnpluggedReportedTotalTime = mCurrentReportedTotalTime;
+                mUnpluggedReportedCount = mCurrentReportedCount;
+            }
+            mTimeBaseRunning = true;
+        }
+
+        @Override
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            super.onTimeStopped(elapsedRealtime, baseUptime, baseRealtime);
+            mTimeBaseRunning = false;
+        }
+
+        @Override
+        public void logState(Printer pw, String prefix) {
+            super.logState(pw, prefix);
+            pw.println(prefix + "mCurrentReportedCount=" + mCurrentReportedCount
+                    + " mUnpluggedReportedCount=" + mUnpluggedReportedCount
+                    + " mCurrentReportedTotalTime=" + mCurrentReportedTotalTime
+                    + " mUnpluggedReportedTotalTime=" + mUnpluggedReportedTotalTime);
+        }
+
+        @Override
+        protected long computeRunTimeLocked(long curBatteryRealtime) {
+            return mTotalTime + (mTimeBaseRunning && mTrackingReportedValues
+                    ? mCurrentReportedTotalTime - mUnpluggedReportedTotalTime : 0);
+        }
+
+        @Override
+        protected int computeCurrentCountLocked() {
+            return mCount + (mTimeBaseRunning && mTrackingReportedValues
+                    ? mCurrentReportedCount - mUnpluggedReportedCount : 0);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
+            super.writeToParcel(out, elapsedRealtimeUs);
+            out.writeInt(mCurrentReportedCount);
+            out.writeInt(mUnpluggedReportedCount);
+            out.writeLong(mCurrentReportedTotalTime);
+            out.writeLong(mUnpluggedReportedTotalTime);
+            out.writeInt(mTrackingReportedValues ? 1 : 0);
+        }
+
+        @Override
+        public boolean reset(boolean detachIfReset) {
+            super.reset(detachIfReset);
+            mTrackingReportedValues = false;
+            mUnpluggedReportedTotalTime = 0;
+            mUnpluggedReportedCount = 0;
+            return true;
+        }
+    }
+
+    /**
+     * A timer that increments in batches.  It does not run for durations, but just jumps
+     * for a pre-determined amount.
+     */
+    public static class BatchTimer extends Timer {
+        final Uid mUid;
+
+        /**
+         * The last time at which we updated the timer.  This is in elapsed realtime microseconds.
+         */
+        long mLastAddedTime;
+
+        /**
+         * The last duration that we added to the timer.  This is in microseconds.
+         */
+        long mLastAddedDuration;
+
+        /**
+         * Whether we are currently in a discharge cycle.
+         */
+        boolean mInDischarge;
+
+        BatchTimer(Clocks clocks, Uid uid, int type, TimeBase timeBase, Parcel in) {
+            super(clocks, type, timeBase, in);
+            mUid = uid;
+            mLastAddedTime = in.readLong();
+            mLastAddedDuration = in.readLong();
+            mInDischarge = timeBase.isRunning();
+        }
+
+        BatchTimer(Clocks clocks, Uid uid, int type, TimeBase timeBase) {
+            super(clocks, type, timeBase);
+            mUid = uid;
+            mInDischarge = timeBase.isRunning();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
+            super.writeToParcel(out, elapsedRealtimeUs);
+            out.writeLong(mLastAddedTime);
+            out.writeLong(mLastAddedDuration);
+        }
+
+        @Override
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            recomputeLastDuration(mClocks.elapsedRealtime() * 1000, false);
+            mInDischarge = false;
+            super.onTimeStopped(elapsedRealtime, baseUptime, baseRealtime);
+        }
+
+        @Override
+        public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            recomputeLastDuration(elapsedRealtime, false);
+            mInDischarge = true;
+            // If we are still within the last added duration, then re-added whatever remains.
+            if (mLastAddedTime == elapsedRealtime) {
+                mTotalTime += mLastAddedDuration;
+            }
+            super.onTimeStarted(elapsedRealtime, baseUptime, baseRealtime);
+        }
+
+        @Override
+        public void logState(Printer pw, String prefix) {
+            super.logState(pw, prefix);
+            pw.println(prefix + "mLastAddedTime=" + mLastAddedTime
+                    + " mLastAddedDuration=" + mLastAddedDuration);
+        }
+
+        private long computeOverage(long curTime) {
+            if (mLastAddedTime > 0) {
+                return mLastTime + mLastAddedDuration - curTime;
+            }
+            return 0;
+        }
+
+        private void recomputeLastDuration(long curTime, boolean abort) {
+            final long overage = computeOverage(curTime);
+            if (overage > 0) {
+                // Aborting before the duration ran out -- roll back the remaining
+                // duration.  Only do this if currently discharging; otherwise we didn't
+                // actually add the time.
+                if (mInDischarge) {
+                    mTotalTime -= overage;
+                }
+                if (abort) {
+                    mLastAddedTime = 0;
+                } else {
+                    mLastAddedTime = curTime;
+                    mLastAddedDuration -= overage;
+                }
+            }
+        }
+
+        public void addDuration(BatteryStatsImpl stats, long durationMillis) {
+            final long now = mClocks.elapsedRealtime() * 1000;
+            recomputeLastDuration(now, true);
+            mLastAddedTime = now;
+            mLastAddedDuration = durationMillis * 1000;
+            if (mInDischarge) {
+                mTotalTime += mLastAddedDuration;
+                mCount++;
+            }
+        }
+
+        public void abortLastDuration(BatteryStatsImpl stats) {
+            final long now = mClocks.elapsedRealtime() * 1000;
+            recomputeLastDuration(now, true);
+        }
+
+        @Override
+        protected int computeCurrentCountLocked() {
+            return mCount;
+        }
+
+        @Override
+        protected long computeRunTimeLocked(long curBatteryRealtime) {
+            final long overage = computeOverage(mClocks.elapsedRealtime() * 1000);
+            if (overage > 0) {
+                return mTotalTime = overage;
+            }
+            return mTotalTime;
+        }
+
+        @Override
+        public boolean reset(boolean detachIfReset) {
+            final long now = mClocks.elapsedRealtime() * 1000;
+            recomputeLastDuration(now, true);
+            boolean stillActive = mLastAddedTime == now;
+            super.reset(!stillActive && detachIfReset);
+            return !stillActive;
+        }
+    }
+
+
+    /**
+     * A StopwatchTimer that also tracks the total and max individual
+     * time spent active according to the given timebase.  Whereas
+     * StopwatchTimer apportions the time amongst all in the pool,
+     * the total and max durations are not apportioned.
+     */
+    public static class DurationTimer extends StopwatchTimer {
+        /**
+         * The time (in ms) that the timer was last acquired or the time base
+         * last (re-)started. Increasing the nesting depth does not reset this time.
+         *
+         * -1 if the timer is currently not running or the time base is not running.
+         *
+         * If written to a parcel, the start time is reset, as is mNesting in the base class
+         * StopwatchTimer.
+         */
+        long mStartTimeMs = -1;
+
+        /**
+         * The longest time period (in ms) that the timer has been active. Not pooled.
+         */
+        long mMaxDurationMs;
+
+        /**
+         * The time (in ms) that that the timer has been active since most recent
+         * stopRunningLocked() or reset(). Not pooled.
+         */
+        long mCurrentDurationMs;
+
+        /**
+         * The total time (in ms) that that the timer has been active since most recent reset()
+         * prior to the current startRunningLocked. This is the sum of all past currentDurations
+         * (but not including the present currentDuration) since reset. Not pooled.
+         */
+        long mTotalDurationMs;
+
+        public DurationTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool,
+                TimeBase timeBase, Parcel in) {
+            super(clocks, uid, type, timerPool, timeBase, in);
+            mMaxDurationMs = in.readLong();
+            mTotalDurationMs = in.readLong();
+            mCurrentDurationMs = in.readLong();
+        }
+
+        public DurationTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool,
+                TimeBase timeBase) {
+            super(clocks, uid, type, timerPool, timeBase);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
+            super.writeToParcel(out, elapsedRealtimeUs);
+            out.writeLong(getMaxDurationMsLocked(elapsedRealtimeUs / 1000));
+            out.writeLong(mTotalDurationMs);
+            out.writeLong(getCurrentDurationMsLocked(elapsedRealtimeUs / 1000));
+        }
+
+        /**
+         * Write the summary to the parcel.
+         *
+         * Since the time base is probably meaningless after we come back, reading
+         * from this will have the effect of stopping the timer. So here all we write
+         * is the max and total durations.
+         */
+        @Override
+        public void writeSummaryFromParcelLocked(Parcel out, long elapsedRealtimeUs) {
+            super.writeSummaryFromParcelLocked(out, elapsedRealtimeUs);
+            out.writeLong(getMaxDurationMsLocked(elapsedRealtimeUs / 1000));
+            out.writeLong(getTotalDurationMsLocked(elapsedRealtimeUs / 1000));
+        }
+
+        /**
+         * Read the summary parcel.
+         *
+         * Has the side effect of stopping the timer.
+         */
+        @Override
+        public void readSummaryFromParcelLocked(Parcel in) {
+            super.readSummaryFromParcelLocked(in);
+            mMaxDurationMs = in.readLong();
+            mTotalDurationMs = in.readLong();
+            mStartTimeMs = -1;
+            mCurrentDurationMs = 0;
+        }
+
+        /**
+         * The TimeBase time started (again).
+         *
+         * If the timer is also running, store the start time.
+         */
+        public void onTimeStarted(long elapsedRealtimeUs, long baseUptime, long baseRealtime) {
+            super.onTimeStarted(elapsedRealtimeUs, baseUptime, baseRealtime);
+            if (mNesting > 0) {
+                mStartTimeMs = baseRealtime / 1000;
+            }
+        }
+
+        /**
+         * The TimeBase stopped running.
+         *
+         * If the timer is running, add the duration into mCurrentDurationMs.
+         */
+        @Override
+        public void onTimeStopped(long elapsedRealtimeUs, long baseUptime, long baseRealtimeUs) {
+            super.onTimeStopped(elapsedRealtimeUs, baseUptime, baseRealtimeUs);
+            if (mNesting > 0) {
+                // baseRealtimeUs has already been converted to the timebase's realtime.
+                mCurrentDurationMs += (baseRealtimeUs / 1000) - mStartTimeMs;
+            }
+            mStartTimeMs = -1;
+        }
+
+        @Override
+        public void logState(Printer pw, String prefix) {
+            super.logState(pw, prefix);
+        }
+
+        @Override
+        public void startRunningLocked(long elapsedRealtimeMs) {
+            super.startRunningLocked(elapsedRealtimeMs);
+            if (mNesting == 1 && mTimeBase.isRunning()) {
+                // Just started
+                mStartTimeMs = mTimeBase.getRealtime(elapsedRealtimeMs * 1000) / 1000;
+            }
+        }
+
+        /**
+         * Decrements the mNesting ref-count on this timer.
+         *
+         * If it actually stopped (mNesting went to 0), then possibly update
+         * mMaxDuration if the current duration was the longest ever.
+         */
+        @Override
+        public void stopRunningLocked(long elapsedRealtimeMs) {
+            if (mNesting == 1) {
+                final long durationMs = getCurrentDurationMsLocked(elapsedRealtimeMs);
+                mTotalDurationMs += durationMs;
+                if (durationMs > mMaxDurationMs) {
+                    mMaxDurationMs = durationMs;
+                }
+                mStartTimeMs = -1;
+                mCurrentDurationMs = 0;
+            }
+            // super method decrements mNesting, which getCurrentDurationMsLocked relies on,
+            // so call super.stopRunningLocked after calling getCurrentDurationMsLocked.
+            super.stopRunningLocked(elapsedRealtimeMs);
+        }
+
+        @Override
+        public boolean reset(boolean detachIfReset) {
+            boolean result = super.reset(detachIfReset);
+            mMaxDurationMs = 0;
+            mTotalDurationMs = 0;
+            mCurrentDurationMs = 0;
+            if (mNesting > 0) {
+                mStartTimeMs = mTimeBase.getRealtime(mClocks.elapsedRealtime()*1000) / 1000;
+            } else {
+                mStartTimeMs = -1;
+            }
+            return result;
+        }
+
+        /**
+         * Returns the max duration that this timer has ever seen.
+         *
+         * Note that this time is NOT split between the timers in the timer group that
+         * this timer is attached to.  It is the TOTAL time.
+         */
+        @Override
+        public long getMaxDurationMsLocked(long elapsedRealtimeMs) {
+            if (mNesting > 0) {
+                final long durationMs = getCurrentDurationMsLocked(elapsedRealtimeMs);
+                if (durationMs > mMaxDurationMs) {
+                    return durationMs;
+                }
+            }
+            return mMaxDurationMs;
+        }
+
+        /**
+         * Returns the time since the timer was started.
+         * Returns 0 if the timer is not currently running.
+         *
+         * Note that this time is NOT split between the timers in the timer group that
+         * this timer is attached to.  It is the TOTAL time.
+         *
+         * Note that if running timer is parceled and unparceled, this method will return
+         * current duration value at the time of parceling even though timer may not be
+         * currently running.
+         */
+        @Override
+        public long getCurrentDurationMsLocked(long elapsedRealtimeMs) {
+            long durationMs = mCurrentDurationMs;
+            if (mNesting > 0 && mTimeBase.isRunning()) {
+                durationMs += (mTimeBase.getRealtime(elapsedRealtimeMs*1000)/1000)
+                        - mStartTimeMs;
+            }
+            return durationMs;
+        }
+
+        /**
+         * Returns the total cumulative duration that this timer has been on since reset().
+         * If mTimerPool == null, this should be the same
+         * as getTotalTimeLocked(elapsedRealtimeMs*1000, STATS_SINCE_CHARGED)/1000.
+         *
+         * Note that this time is NOT split between the timers in the timer group that
+         * this timer is attached to.  It is the TOTAL time. For this reason, if mTimerPool != null,
+         * the result will not be equivalent to getTotalTimeLocked.
+         */
+        @Override
+        public long getTotalDurationMsLocked(long elapsedRealtimeMs) {
+            return mTotalDurationMs + getCurrentDurationMsLocked(elapsedRealtimeMs);
+        }
+    }
+
+    /**
+     * State for keeping track of timing information.
+     */
+    public static class StopwatchTimer extends Timer {
+        final Uid mUid;
+        final ArrayList<StopwatchTimer> mTimerPool;
+
+        int mNesting;
+
+        /**
+         * The last time at which we updated the timer.  If mNesting is > 0,
+         * subtract this from the current battery time to find the amount of
+         * time we have been running since we last computed an update.
+         */
+        long mUpdateTime;
+
+        /**
+         * The total time at which the timer was acquired, to determine if it
+         * was actually held for an interesting duration. If time base was not running when timer
+         * was acquired, will be -1.
+         */
+        long mAcquireTime = -1;
+
+        long mTimeout;
+
+        /**
+         * For partial wake locks, keep track of whether we are in the list
+         * to consume CPU cycles.
+         */
+        @VisibleForTesting
+        public boolean mInList;
+
+        public StopwatchTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool,
+                TimeBase timeBase, Parcel in) {
+            super(clocks, type, timeBase, in);
+            mUid = uid;
+            mTimerPool = timerPool;
+            mUpdateTime = in.readLong();
+        }
+
+        public StopwatchTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool,
+                TimeBase timeBase) {
+            super(clocks, type, timeBase);
+            mUid = uid;
+            mTimerPool = timerPool;
+        }
+
+        public void setTimeout(long timeout) {
+            mTimeout = timeout;
+        }
+
+        public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
+            super.writeToParcel(out, elapsedRealtimeUs);
+            out.writeLong(mUpdateTime);
+        }
+
+        public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            if (mNesting > 0) {
+                if (DEBUG && mType < 0) {
+                    Log.v(TAG, "old mUpdateTime=" + mUpdateTime);
+                }
+                super.onTimeStopped(elapsedRealtime, baseUptime, baseRealtime);
+                mUpdateTime = baseRealtime;
+                if (DEBUG && mType < 0) {
+                    Log.v(TAG, "new mUpdateTime=" + mUpdateTime);
+                }
+            }
+        }
+
+        public void logState(Printer pw, String prefix) {
+            super.logState(pw, prefix);
+            pw.println(prefix + "mNesting=" + mNesting + " mUpdateTime=" + mUpdateTime
+                    + " mAcquireTime=" + mAcquireTime);
+        }
+
+        public void startRunningLocked(long elapsedRealtimeMs) {
+            if (mNesting++ == 0) {
+                final long batteryRealtime = mTimeBase.getRealtime(elapsedRealtimeMs * 1000);
+                mUpdateTime = batteryRealtime;
+                if (mTimerPool != null) {
+                    // Accumulate time to all currently active timers before adding
+                    // this new one to the pool.
+                    refreshTimersLocked(batteryRealtime, mTimerPool, null);
+                    // Add this timer to the active pool
+                    mTimerPool.add(this);
+                }
+                if (mTimeBase.isRunning()) {
+                    // Increment the count
+                    mCount++;
+                    mAcquireTime = mTotalTime;
+                } else {
+                    mAcquireTime = -1;
+                }
+                if (DEBUG && mType < 0) {
+                    Log.v(TAG, "start #" + mType + ": mUpdateTime=" + mUpdateTime
+                            + " mTotalTime=" + mTotalTime + " mCount=" + mCount
+                            + " mAcquireTime=" + mAcquireTime);
+                }
+            }
+        }
+
+        public boolean isRunningLocked() {
+            return mNesting > 0;
+        }
+
+        public void stopRunningLocked(long elapsedRealtimeMs) {
+            // Ignore attempt to stop a timer that isn't running
+            if (mNesting == 0) {
+                return;
+            }
+            if (--mNesting == 0) {
+                final long batteryRealtime = mTimeBase.getRealtime(elapsedRealtimeMs * 1000);
+                if (mTimerPool != null) {
+                    // Accumulate time to all active counters, scaled by the total
+                    // active in the pool, before taking this one out of the pool.
+                    refreshTimersLocked(batteryRealtime, mTimerPool, null);
+                    // Remove this timer from the active pool
+                    mTimerPool.remove(this);
+                } else {
+                    mNesting = 1;
+                    mTotalTime = computeRunTimeLocked(batteryRealtime);
+                    mNesting = 0;
+                }
+
+                if (DEBUG && mType < 0) {
+                    Log.v(TAG, "stop #" + mType + ": mUpdateTime=" + mUpdateTime
+                            + " mTotalTime=" + mTotalTime + " mCount=" + mCount
+                            + " mAcquireTime=" + mAcquireTime);
+                }
+
+                if (mAcquireTime >= 0 && mTotalTime == mAcquireTime) {
+                    // If there was no change in the time, then discard this
+                    // count.  A somewhat cheezy strategy, but hey.
+                    mCount--;
+                }
+            }
+        }
+
+        public void stopAllRunningLocked(long elapsedRealtimeMs) {
+            if (mNesting > 0) {
+                mNesting = 1;
+                stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        // Update the total time for all other running Timers with the same type as this Timer
+        // due to a change in timer count
+        private static long refreshTimersLocked(long batteryRealtime,
+                final ArrayList<StopwatchTimer> pool, StopwatchTimer self) {
+            long selfTime = 0;
+            final int N = pool.size();
+            for (int i=N-1; i>= 0; i--) {
+                final StopwatchTimer t = pool.get(i);
+                long heldTime = batteryRealtime - t.mUpdateTime;
+                if (heldTime > 0) {
+                    final long myTime = heldTime / N;
+                    if (t == self) {
+                        selfTime = myTime;
+                    }
+                    t.mTotalTime += myTime;
+                }
+                t.mUpdateTime = batteryRealtime;
+            }
+            return selfTime;
+        }
+
+        @Override
+        protected long computeRunTimeLocked(long curBatteryRealtime) {
+            if (mTimeout > 0 && curBatteryRealtime > mUpdateTime + mTimeout) {
+                curBatteryRealtime = mUpdateTime + mTimeout;
+            }
+            return mTotalTime + (mNesting > 0
+                    ? (curBatteryRealtime - mUpdateTime)
+                            / (mTimerPool != null ? mTimerPool.size() : 1)
+                    : 0);
+        }
+
+        @Override
+        protected int computeCurrentCountLocked() {
+            return mCount;
+        }
+
+        @Override
+        public boolean reset(boolean detachIfReset) {
+            boolean canDetach = mNesting <= 0;
+            super.reset(canDetach && detachIfReset);
+            if (mNesting > 0) {
+                mUpdateTime = mTimeBase.getRealtime(mClocks.elapsedRealtime() * 1000);
+            }
+            mAcquireTime = -1; // to ensure mCount isn't decreased to -1 if timer is stopped later.
+            return canDetach;
+        }
+
+        @Override
+        public void detach() {
+            super.detach();
+            if (mTimerPool != null) {
+                mTimerPool.remove(this);
+            }
+        }
+
+        @Override
+        public void readSummaryFromParcelLocked(Parcel in) {
+            super.readSummaryFromParcelLocked(in);
+            mNesting = 0;
+        }
+
+        /**
+         * Set the mark so that we can query later for the total time the timer has
+         * accumulated since this point. The timer can be running or not.
+         *
+         * @param elapsedRealtimeMs the current elapsed realtime in milliseconds.
+         */
+        public void setMark(long elapsedRealtimeMs) {
+            final long batteryRealtime = mTimeBase.getRealtime(elapsedRealtimeMs * 1000);
+            if (mNesting > 0) {
+                // We are running.
+                if (mTimerPool != null) {
+                    refreshTimersLocked(batteryRealtime, mTimerPool, this);
+                } else {
+                    mTotalTime += batteryRealtime - mUpdateTime;
+                    mUpdateTime = batteryRealtime;
+                }
+            }
+            mTimeBeforeMark = mTotalTime;
+        }
+    }
+
+    /**
+     * State for keeping track of two DurationTimers with different TimeBases, presumably where one
+     * TimeBase is effectively a subset of the other.
+     */
+    public static class DualTimer extends DurationTimer {
+        // This class both is a DurationTimer and also holds a second DurationTimer.
+        // The main timer (this) typically tracks the total time. It may be pooled (but since it's a
+        // durationTimer, it also has the unpooled getTotalDurationMsLocked() for
+        // STATS_SINCE_CHARGED).
+        // mSubTimer typically tracks only part of the total time, such as background time, as
+        // determined by a subTimeBase. It is NOT pooled.
+        private final DurationTimer mSubTimer;
+
+        /**
+         * Creates a DualTimer to hold a main timer (this) and a mSubTimer.
+         * The main timer (this) is based on the given timeBase and timerPool.
+         * The mSubTimer is based on the given subTimeBase. The mSubTimer is not pooled, even if
+         * the main timer is.
+         */
+        public DualTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool,
+                TimeBase timeBase, TimeBase subTimeBase, Parcel in) {
+            super(clocks, uid, type, timerPool, timeBase, in);
+            mSubTimer = new DurationTimer(clocks, uid, type, null, subTimeBase, in);
+        }
+
+        /**
+         * Creates a DualTimer to hold a main timer (this) and a mSubTimer.
+         * The main timer (this) is based on the given timeBase and timerPool.
+         * The mSubTimer is based on the given subTimeBase. The mSubTimer is not pooled, even if
+         * the main timer is.
+         */
+        public DualTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool,
+                TimeBase timeBase, TimeBase subTimeBase) {
+            super(clocks, uid, type, timerPool, timeBase);
+            mSubTimer = new DurationTimer(clocks, uid, type, null, subTimeBase);
+        }
+
+        /** Get the secondary timer. */
+        @Override
+        public DurationTimer getSubTimer() {
+            return mSubTimer;
+        }
+
+        @Override
+        public void startRunningLocked(long elapsedRealtimeMs) {
+            super.startRunningLocked(elapsedRealtimeMs);
+            mSubTimer.startRunningLocked(elapsedRealtimeMs);
+        }
+
+        @Override
+        public void stopRunningLocked(long elapsedRealtimeMs) {
+            super.stopRunningLocked(elapsedRealtimeMs);
+            mSubTimer.stopRunningLocked(elapsedRealtimeMs);
+        }
+
+        @Override
+        public void stopAllRunningLocked(long elapsedRealtimeMs) {
+            super.stopAllRunningLocked(elapsedRealtimeMs);
+            mSubTimer.stopAllRunningLocked(elapsedRealtimeMs);
+        }
+
+        @Override
+        public boolean reset(boolean detachIfReset) {
+            boolean active = false;
+            // Do not detach the subTimer explicitly since that'll be done by DualTimer.detach().
+            active |= !mSubTimer.reset(false);
+            active |= !super.reset(detachIfReset);
+            return !active;
+        }
+
+        @Override
+        public void detach() {
+            mSubTimer.detach();
+            super.detach();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
+            super.writeToParcel(out, elapsedRealtimeUs);
+            mSubTimer.writeToParcel(out, elapsedRealtimeUs);
+        }
+
+        @Override
+        public void writeSummaryFromParcelLocked(Parcel out, long elapsedRealtimeUs) {
+            super.writeSummaryFromParcelLocked(out, elapsedRealtimeUs);
+            mSubTimer.writeSummaryFromParcelLocked(out, elapsedRealtimeUs);
+        }
+
+        @Override
+        public void readSummaryFromParcelLocked(Parcel in) {
+            super.readSummaryFromParcelLocked(in);
+            mSubTimer.readSummaryFromParcelLocked(in);
+        }
+    }
+
+
+    public abstract class OverflowArrayMap<T> {
+        private static final String OVERFLOW_NAME = "*overflow*";
+
+        final int mUid;
+        final ArrayMap<String, T> mMap = new ArrayMap<>();
+        T mCurOverflow;
+        ArrayMap<String, MutableInt> mActiveOverflow;
+        long mLastOverflowTime;
+        long mLastOverflowFinishTime;
+        long mLastClearTime;
+        long mLastCleanupTime;
+
+        public OverflowArrayMap(int uid) {
+            mUid = uid;
+        }
+
+        public ArrayMap<String, T> getMap() {
+            return mMap;
+        }
+
+        public void clear() {
+            mLastClearTime = SystemClock.elapsedRealtime();
+            mMap.clear();
+            mCurOverflow = null;
+            mActiveOverflow = null;
+        }
+
+        public void add(String name, T obj) {
+            if (name == null) {
+                name = "";
+            }
+            mMap.put(name, obj);
+            if (OVERFLOW_NAME.equals(name)) {
+                mCurOverflow = obj;
+            }
+        }
+
+        public void cleanup() {
+            mLastCleanupTime = SystemClock.elapsedRealtime();
+            if (mActiveOverflow != null) {
+                if (mActiveOverflow.size() == 0) {
+                    mActiveOverflow = null;
+                }
+            }
+            if (mActiveOverflow == null) {
+                // There is no currently active overflow, so we should no longer have
+                // an overflow entry.
+                if (mMap.containsKey(OVERFLOW_NAME)) {
+                    Slog.wtf(TAG, "Cleaning up with no active overflow, but have overflow entry "
+                            + mMap.get(OVERFLOW_NAME));
+                    mMap.remove(OVERFLOW_NAME);
+                }
+                mCurOverflow = null;
+            } else {
+                // There is currently active overflow, so we should still have an overflow entry.
+                if (mCurOverflow == null || !mMap.containsKey(OVERFLOW_NAME)) {
+                    Slog.wtf(TAG, "Cleaning up with active overflow, but no overflow entry: cur="
+                            + mCurOverflow + " map=" + mMap.get(OVERFLOW_NAME));
+                }
+            }
+        }
+
+        public T startObject(String name) {
+            if (name == null) {
+                name = "";
+            }
+            T obj = mMap.get(name);
+            if (obj != null) {
+                return obj;
+            }
+
+            // No object exists for the given name, but do we currently have it
+            // running as part of the overflow?
+            if (mActiveOverflow != null) {
+                MutableInt over = mActiveOverflow.get(name);
+                if (over != null) {
+                    // We are already actively counting this name in the overflow object.
+                    obj = mCurOverflow;
+                    if (obj == null) {
+                        // Shouldn't be here, but we'll try to recover.
+                        Slog.wtf(TAG, "Have active overflow " + name + " but null overflow");
+                        obj = mCurOverflow = instantiateObject();
+                        mMap.put(OVERFLOW_NAME, obj);
+                    }
+                    over.value++;
+                    return obj;
+                }
+            }
+
+            // No object exists for given name nor in the overflow; we need to make
+            // a new one.
+            final int N = mMap.size();
+            if (N >= MAX_WAKELOCKS_PER_UID) {
+                // Went over the limit on number of objects to track; this one goes
+                // in to the overflow.
+                obj = mCurOverflow;
+                if (obj == null) {
+                    // Need to start overflow now...
+                    obj = mCurOverflow = instantiateObject();
+                    mMap.put(OVERFLOW_NAME, obj);
+                }
+                if (mActiveOverflow == null) {
+                    mActiveOverflow = new ArrayMap<>();
+                }
+                mActiveOverflow.put(name, new MutableInt(1));
+                mLastOverflowTime = SystemClock.elapsedRealtime();
+                return obj;
+            }
+
+            // Normal case where we just need to make a new object.
+            obj = instantiateObject();
+            mMap.put(name, obj);
+            return obj;
+        }
+
+        public T stopObject(String name) {
+            if (name == null) {
+                name = "";
+            }
+            T obj = mMap.get(name);
+            if (obj != null) {
+                return obj;
+            }
+
+            // No object exists for the given name, but do we currently have it
+            // running as part of the overflow?
+            if (mActiveOverflow != null) {
+                MutableInt over = mActiveOverflow.get(name);
+                if (over != null) {
+                    // We are already actively counting this name in the overflow object.
+                    obj = mCurOverflow;
+                    if (obj != null) {
+                        over.value--;
+                        if (over.value <= 0) {
+                            mActiveOverflow.remove(name);
+                            mLastOverflowFinishTime = SystemClock.elapsedRealtime();
+                        }
+                        return obj;
+                    }
+                }
+            }
+
+            // Huh, they are stopping an active operation but we can't find one!
+            // That's not good.
+            StringBuilder sb = new StringBuilder();
+            sb.append("Unable to find object for ");
+            sb.append(name);
+            sb.append(" in uid ");
+            sb.append(mUid);
+            sb.append(" mapsize=");
+            sb.append(mMap.size());
+            sb.append(" activeoverflow=");
+            sb.append(mActiveOverflow);
+            sb.append(" curoverflow=");
+            sb.append(mCurOverflow);
+            long now = SystemClock.elapsedRealtime();
+            if (mLastOverflowTime != 0) {
+                sb.append(" lastOverflowTime=");
+                TimeUtils.formatDuration(mLastOverflowTime-now, sb);
+            }
+            if (mLastOverflowFinishTime != 0) {
+                sb.append(" lastOverflowFinishTime=");
+                TimeUtils.formatDuration(mLastOverflowFinishTime-now, sb);
+            }
+            if (mLastClearTime != 0) {
+                sb.append(" lastClearTime=");
+                TimeUtils.formatDuration(mLastClearTime-now, sb);
+            }
+            if (mLastCleanupTime != 0) {
+                sb.append(" lastCleanupTime=");
+                TimeUtils.formatDuration(mLastCleanupTime-now, sb);
+            }
+            Slog.wtf(TAG, sb.toString());
+            return null;
+        }
+
+        public abstract T instantiateObject();
+    }
+
+    public static class ControllerActivityCounterImpl extends ControllerActivityCounter
+            implements Parcelable {
+        private final LongSamplingCounter mIdleTimeMillis;
+        private final LongSamplingCounter mRxTimeMillis;
+        private final LongSamplingCounter[] mTxTimeMillis;
+        private final LongSamplingCounter mPowerDrainMaMs;
+
+        public ControllerActivityCounterImpl(TimeBase timeBase, int numTxStates) {
+            mIdleTimeMillis = new LongSamplingCounter(timeBase);
+            mRxTimeMillis = new LongSamplingCounter(timeBase);
+            mTxTimeMillis = new LongSamplingCounter[numTxStates];
+            for (int i = 0; i < numTxStates; i++) {
+                mTxTimeMillis[i] = new LongSamplingCounter(timeBase);
+            }
+            mPowerDrainMaMs = new LongSamplingCounter(timeBase);
+        }
+
+        public ControllerActivityCounterImpl(TimeBase timeBase, int numTxStates, Parcel in) {
+            mIdleTimeMillis = new LongSamplingCounter(timeBase, in);
+            mRxTimeMillis = new LongSamplingCounter(timeBase, in);
+            final int recordedTxStates = in.readInt();
+            if (recordedTxStates != numTxStates) {
+                throw new ParcelFormatException("inconsistent tx state lengths");
+            }
+
+            mTxTimeMillis = new LongSamplingCounter[numTxStates];
+            for (int i = 0; i < numTxStates; i++) {
+                mTxTimeMillis[i] = new LongSamplingCounter(timeBase, in);
+            }
+            mPowerDrainMaMs = new LongSamplingCounter(timeBase, in);
+        }
+
+        public void readSummaryFromParcel(Parcel in) {
+            mIdleTimeMillis.readSummaryFromParcelLocked(in);
+            mRxTimeMillis.readSummaryFromParcelLocked(in);
+            final int recordedTxStates = in.readInt();
+            if (recordedTxStates != mTxTimeMillis.length) {
+                throw new ParcelFormatException("inconsistent tx state lengths");
+            }
+            for (LongSamplingCounter counter : mTxTimeMillis) {
+                counter.readSummaryFromParcelLocked(in);
+            }
+            mPowerDrainMaMs.readSummaryFromParcelLocked(in);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        public void writeSummaryToParcel(Parcel dest) {
+            mIdleTimeMillis.writeSummaryFromParcelLocked(dest);
+            mRxTimeMillis.writeSummaryFromParcelLocked(dest);
+            dest.writeInt(mTxTimeMillis.length);
+            for (LongSamplingCounter counter : mTxTimeMillis) {
+                counter.writeSummaryFromParcelLocked(dest);
+            }
+            mPowerDrainMaMs.writeSummaryFromParcelLocked(dest);
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            mIdleTimeMillis.writeToParcel(dest);
+            mRxTimeMillis.writeToParcel(dest);
+            dest.writeInt(mTxTimeMillis.length);
+            for (LongSamplingCounter counter : mTxTimeMillis) {
+                counter.writeToParcel(dest);
+            }
+            mPowerDrainMaMs.writeToParcel(dest);
+        }
+
+        public void reset(boolean detachIfReset) {
+            mIdleTimeMillis.reset(detachIfReset);
+            mRxTimeMillis.reset(detachIfReset);
+            for (LongSamplingCounter counter : mTxTimeMillis) {
+                counter.reset(detachIfReset);
+            }
+            mPowerDrainMaMs.reset(detachIfReset);
+        }
+
+        public void detach() {
+            mIdleTimeMillis.detach();
+            mRxTimeMillis.detach();
+            for (LongSamplingCounter counter : mTxTimeMillis) {
+                counter.detach();
+            }
+            mPowerDrainMaMs.detach();
+        }
+
+        /**
+         * @return a LongSamplingCounter, measuring time spent in the idle state in
+         * milliseconds.
+         */
+        @Override
+        public LongSamplingCounter getIdleTimeCounter() {
+            return mIdleTimeMillis;
+        }
+
+        /**
+         * @return a LongSamplingCounter, measuring time spent in the receive state in
+         * milliseconds.
+         */
+        @Override
+        public LongSamplingCounter getRxTimeCounter() {
+            return mRxTimeMillis;
+        }
+
+        /**
+         * @return a LongSamplingCounter[], measuring time spent in various transmit states in
+         * milliseconds.
+         */
+        @Override
+        public LongSamplingCounter[] getTxTimeCounters() {
+            return mTxTimeMillis;
+        }
+
+        /**
+         * @return a LongSamplingCounter, measuring power use in milli-ampere milliseconds (mAmS).
+         */
+        @Override
+        public LongSamplingCounter getPowerCounter() {
+            return mPowerDrainMaMs;
+        }
+    }
+
+    /*
+     * Get the wakeup reason counter, and create a new one if one
+     * doesn't already exist.
+     */
+    public SamplingTimer getWakeupReasonTimerLocked(String name) {
+        SamplingTimer timer = mWakeupReasonStats.get(name);
+        if (timer == null) {
+            timer = new SamplingTimer(mClocks, mOnBatteryTimeBase);
+            mWakeupReasonStats.put(name, timer);
+        }
+        return timer;
+    }
+
+    /*
+     * Get the KernelWakelockTimer associated with name, and create a new one if one
+     * doesn't already exist.
+     */
+    public SamplingTimer getKernelWakelockTimerLocked(String name) {
+        SamplingTimer kwlt = mKernelWakelockStats.get(name);
+        if (kwlt == null) {
+            kwlt = new SamplingTimer(mClocks, mOnBatteryScreenOffTimeBase);
+            mKernelWakelockStats.put(name, kwlt);
+        }
+        return kwlt;
+    }
+
+    public SamplingTimer getKernelMemoryTimerLocked(long bucket) {
+        SamplingTimer kmt = mKernelMemoryStats.get(bucket);
+        if (kmt == null) {
+            kmt = new SamplingTimer(mClocks, mOnBatteryTimeBase);
+            mKernelMemoryStats.put(bucket, kmt);
+        }
+        return kmt;
+    }
+
+    private int writeHistoryTag(HistoryTag tag) {
+        Integer idxObj = mHistoryTagPool.get(tag);
+        int idx;
+        if (idxObj != null) {
+            idx = idxObj;
+        } else {
+            idx = mNextHistoryTagIdx;
+            HistoryTag key = new HistoryTag();
+            key.setTo(tag);
+            tag.poolIdx = idx;
+            mHistoryTagPool.put(key, idx);
+            mNextHistoryTagIdx++;
+            mNumHistoryTagChars += key.string.length() + 1;
+        }
+        return idx;
+    }
+
+    private void readHistoryTag(int index, HistoryTag tag) {
+        tag.string = mReadHistoryStrings[index];
+        tag.uid = mReadHistoryUids[index];
+        tag.poolIdx = index;
+    }
+
+    /*
+        The history delta format uses flags to denote further data in subsequent ints in the parcel.
+
+        There is always the first token, which may contain the delta time, or an indicator of
+        the length of the time (int or long) following this token.
+
+        First token: always present,
+        31              23              15               7             0
+        â–ˆM|L|K|J|I|H|G|Fâ–ˆE|D|C|B|A|T|T|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆ
+
+        T: the delta time if it is <= 0x7fffd. Otherwise 0x7fffe indicates an int immediately
+           follows containing the time, and 0x7ffff indicates a long immediately follows with the
+           delta time.
+        A: battery level changed and an int follows with battery data.
+        B: state changed and an int follows with state change data.
+        C: state2 has changed and an int follows with state2 change data.
+        D: wakelock/wakereason has changed and an wakelock/wakereason struct follows.
+        E: event data has changed and an event struct follows.
+        F: battery charge in coulombs has changed and an int with the charge follows.
+        G: state flag denoting that the mobile radio was active.
+        H: state flag denoting that the wifi radio was active.
+        I: state flag denoting that a wifi scan occurred.
+        J: state flag denoting that a wifi full lock was held.
+        K: state flag denoting that the gps was on.
+        L: state flag denoting that a wakelock was held.
+        M: state flag denoting that the cpu was running.
+
+        Time int/long: if T in the first token is 0x7ffff or 0x7fffe, then an int or long follows
+        with the time delta.
+
+        Battery level int: if A in the first token is set,
+        31              23              15               7             0
+        â–ˆL|L|L|L|L|L|L|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆT|V|V|V|V|V|V|Vâ–ˆV|V|V|V|V|V|V|Dâ–ˆ
+
+        D: indicates that extra history details follow.
+        V: the battery voltage.
+        T: the battery temperature.
+        L: the battery level (out of 100).
+
+        State change int: if B in the first token is set,
+        31              23              15               7             0
+        â–ˆS|S|S|H|H|H|P|Pâ–ˆF|E|D|C|B| | |Aâ–ˆ | | | | | | | â–ˆ | | | | | | | â–ˆ
+
+        A: wifi multicast was on.
+        B: battery was plugged in.
+        C: screen was on.
+        D: phone was scanning for signal.
+        E: audio was on.
+        F: a sensor was active.
+
+        State2 change int: if C in the first token is set,
+        31              23              15               7             0
+        â–ˆM|L|K|J|I|H|H|Gâ–ˆF|E|D|C| | | | â–ˆ | | | | | | | â–ˆ |B|B|B|A|A|A|Aâ–ˆ
+
+        A: 4 bits indicating the wifi supplicant state: {@link BatteryStats#WIFI_SUPPL_STATE_NAMES}.
+        B: 3 bits indicating the wifi signal strength: 0, 1, 2, 3, 4.
+        C: a bluetooth scan was active.
+        D: the camera was active.
+        E: bluetooth was on.
+        F: a phone call was active.
+        G: the device was charging.
+        H: 2 bits indicating the device-idle (doze) state: off, light, full
+        I: the flashlight was on.
+        J: wifi was on.
+        K: wifi was running.
+        L: video was playing.
+        M: power save mode was on.
+
+        Wakelock/wakereason struct: if D in the first token is set,
+        TODO(adamlesinski): describe wakelock/wakereason struct.
+
+        Event struct: if E in the first token is set,
+        TODO(adamlesinski): describe the event struct.
+
+        History step details struct: if D in the battery level int is set,
+        TODO(adamlesinski): describe the history step details struct.
+
+        Battery charge int: if F in the first token is set, an int representing the battery charge
+        in coulombs follows.
+     */
+
+    // Part of initial delta int that specifies the time delta.
+    static final int DELTA_TIME_MASK = 0x7ffff;
+    static final int DELTA_TIME_LONG = 0x7ffff;   // The delta is a following long
+    static final int DELTA_TIME_INT = 0x7fffe;    // The delta is a following int
+    static final int DELTA_TIME_ABS = 0x7fffd;    // Following is an entire abs update.
+    // Flag in delta int: a new battery level int follows.
+    static final int DELTA_BATTERY_LEVEL_FLAG               = 0x00080000;
+    // Flag in delta int: a new full state and battery status int follows.
+    static final int DELTA_STATE_FLAG                       = 0x00100000;
+    // Flag in delta int: a new full state2 int follows.
+    static final int DELTA_STATE2_FLAG                      = 0x00200000;
+    // Flag in delta int: contains a wakelock or wakeReason tag.
+    static final int DELTA_WAKELOCK_FLAG                    = 0x00400000;
+    // Flag in delta int: contains an event description.
+    static final int DELTA_EVENT_FLAG                       = 0x00800000;
+    // Flag in delta int: contains the battery charge count in uAh.
+    static final int DELTA_BATTERY_CHARGE_FLAG              = 0x01000000;
+    // These upper bits are the frequently changing state bits.
+    static final int DELTA_STATE_MASK                       = 0xfe000000;
+
+    // These are the pieces of battery state that are packed in to the upper bits of
+    // the state int that have been packed in to the first delta int.  They must fit
+    // in STATE_BATTERY_MASK.
+    static final int STATE_BATTERY_MASK         = 0xff000000;
+    static final int STATE_BATTERY_STATUS_MASK  = 0x00000007;
+    static final int STATE_BATTERY_STATUS_SHIFT = 29;
+    static final int STATE_BATTERY_HEALTH_MASK  = 0x00000007;
+    static final int STATE_BATTERY_HEALTH_SHIFT = 26;
+    static final int STATE_BATTERY_PLUG_MASK    = 0x00000003;
+    static final int STATE_BATTERY_PLUG_SHIFT   = 24;
+
+    // We use the low bit of the battery state int to indicate that we have full details
+    // from a battery level change.
+    static final int BATTERY_DELTA_LEVEL_FLAG   = 0x00000001;
+
+    public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
+        if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) {
+            dest.writeInt(DELTA_TIME_ABS);
+            cur.writeToParcel(dest, 0);
+            return;
+        }
+
+        final long deltaTime = cur.time - last.time;
+        final int lastBatteryLevelInt = buildBatteryLevelInt(last);
+        final int lastStateInt = buildStateInt(last);
+
+        int deltaTimeToken;
+        if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) {
+            deltaTimeToken = DELTA_TIME_LONG;
+        } else if (deltaTime >= DELTA_TIME_ABS) {
+            deltaTimeToken = DELTA_TIME_INT;
+        } else {
+            deltaTimeToken = (int)deltaTime;
+        }
+        int firstToken = deltaTimeToken | (cur.states&DELTA_STATE_MASK);
+        final int includeStepDetails = mLastHistoryStepLevel > cur.batteryLevel
+                ? BATTERY_DELTA_LEVEL_FLAG : 0;
+        final boolean computeStepDetails = includeStepDetails != 0
+                || mLastHistoryStepDetails == null;
+        final int batteryLevelInt = buildBatteryLevelInt(cur) | includeStepDetails;
+        final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt;
+        if (batteryLevelIntChanged) {
+            firstToken |= DELTA_BATTERY_LEVEL_FLAG;
+        }
+        final int stateInt = buildStateInt(cur);
+        final boolean stateIntChanged = stateInt != lastStateInt;
+        if (stateIntChanged) {
+            firstToken |= DELTA_STATE_FLAG;
+        }
+        final boolean state2IntChanged = cur.states2 != last.states2;
+        if (state2IntChanged) {
+            firstToken |= DELTA_STATE2_FLAG;
+        }
+        if (cur.wakelockTag != null || cur.wakeReasonTag != null) {
+            firstToken |= DELTA_WAKELOCK_FLAG;
+        }
+        if (cur.eventCode != HistoryItem.EVENT_NONE) {
+            firstToken |= DELTA_EVENT_FLAG;
+        }
+
+        final boolean batteryChargeChanged = cur.batteryChargeUAh != last.batteryChargeUAh;
+        if (batteryChargeChanged) {
+            firstToken |= DELTA_BATTERY_CHARGE_FLAG;
+        }
+        dest.writeInt(firstToken);
+        if (DEBUG) Slog.i(TAG, "WRITE DELTA: firstToken=0x" + Integer.toHexString(firstToken)
+                + " deltaTime=" + deltaTime);
+
+        if (deltaTimeToken >= DELTA_TIME_INT) {
+            if (deltaTimeToken == DELTA_TIME_INT) {
+                if (DEBUG) Slog.i(TAG, "WRITE DELTA: int deltaTime=" + (int)deltaTime);
+                dest.writeInt((int)deltaTime);
+            } else {
+                if (DEBUG) Slog.i(TAG, "WRITE DELTA: long deltaTime=" + deltaTime);
+                dest.writeLong(deltaTime);
+            }
+        }
+        if (batteryLevelIntChanged) {
+            dest.writeInt(batteryLevelInt);
+            if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryToken=0x"
+                    + Integer.toHexString(batteryLevelInt)
+                    + " batteryLevel=" + cur.batteryLevel
+                    + " batteryTemp=" + cur.batteryTemperature
+                    + " batteryVolt=" + (int)cur.batteryVoltage);
+        }
+        if (stateIntChanged) {
+            dest.writeInt(stateInt);
+            if (DEBUG) Slog.i(TAG, "WRITE DELTA: stateToken=0x"
+                    + Integer.toHexString(stateInt)
+                    + " batteryStatus=" + cur.batteryStatus
+                    + " batteryHealth=" + cur.batteryHealth
+                    + " batteryPlugType=" + cur.batteryPlugType
+                    + " states=0x" + Integer.toHexString(cur.states));
+        }
+        if (state2IntChanged) {
+            dest.writeInt(cur.states2);
+            if (DEBUG) Slog.i(TAG, "WRITE DELTA: states2=0x"
+                    + Integer.toHexString(cur.states2));
+        }
+        if (cur.wakelockTag != null || cur.wakeReasonTag != null) {
+            int wakeLockIndex;
+            int wakeReasonIndex;
+            if (cur.wakelockTag != null) {
+                wakeLockIndex = writeHistoryTag(cur.wakelockTag);
+                if (DEBUG) Slog.i(TAG, "WRITE DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx
+                    + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string);
+            } else {
+                wakeLockIndex = 0xffff;
+            }
+            if (cur.wakeReasonTag != null) {
+                wakeReasonIndex = writeHistoryTag(cur.wakeReasonTag);
+                if (DEBUG) Slog.i(TAG, "WRITE DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx
+                    + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string);
+            } else {
+                wakeReasonIndex = 0xffff;
+            }
+            dest.writeInt((wakeReasonIndex<<16) | wakeLockIndex);
+        }
+        if (cur.eventCode != HistoryItem.EVENT_NONE) {
+            int index = writeHistoryTag(cur.eventTag);
+            int codeAndIndex = (cur.eventCode&0xffff) | (index<<16);
+            dest.writeInt(codeAndIndex);
+            if (DEBUG) Slog.i(TAG, "WRITE DELTA: event=" + cur.eventCode + " tag=#"
+                    + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":"
+                    + cur.eventTag.string);
+        }
+        if (computeStepDetails) {
+            if (mPlatformIdleStateCallback != null) {
+                mCurHistoryStepDetails.statPlatformIdleState =
+                        mPlatformIdleStateCallback.getPlatformLowPowerStats();
+                if (DEBUG) Slog.i(TAG, "WRITE PlatformIdleState:" +
+                        mCurHistoryStepDetails.statPlatformIdleState);
+
+                mCurHistoryStepDetails.statSubsystemPowerState =
+                        mPlatformIdleStateCallback.getSubsystemLowPowerStats();
+                if (DEBUG) Slog.i(TAG, "WRITE SubsystemPowerState:" +
+                        mCurHistoryStepDetails.statSubsystemPowerState);
+
+            }
+            computeHistoryStepDetails(mCurHistoryStepDetails, mLastHistoryStepDetails);
+            if (includeStepDetails != 0) {
+                mCurHistoryStepDetails.writeToParcel(dest);
+            }
+            cur.stepDetails = mCurHistoryStepDetails;
+            mLastHistoryStepDetails = mCurHistoryStepDetails;
+        } else {
+            cur.stepDetails = null;
+        }
+        if (mLastHistoryStepLevel < cur.batteryLevel) {
+            mLastHistoryStepDetails = null;
+        }
+        mLastHistoryStepLevel = cur.batteryLevel;
+
+        if (batteryChargeChanged) {
+            if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryChargeUAh=" + cur.batteryChargeUAh);
+            dest.writeInt(cur.batteryChargeUAh);
+        }
+    }
+
+    private int buildBatteryLevelInt(HistoryItem h) {
+        return ((((int)h.batteryLevel)<<25)&0xfe000000)
+                | ((((int)h.batteryTemperature)<<15)&0x01ff8000)
+                | ((((int)h.batteryVoltage)<<1)&0x00007ffe);
+    }
+
+    private void readBatteryLevelInt(int batteryLevelInt, HistoryItem out) {
+        out.batteryLevel = (byte)((batteryLevelInt & 0xfe000000) >>> 25);
+        out.batteryTemperature = (short)((batteryLevelInt & 0x01ff8000) >>> 15);
+        out.batteryVoltage = (char)((batteryLevelInt & 0x00007ffe) >>> 1);
+    }
+
+    private int buildStateInt(HistoryItem h) {
+        int plugType = 0;
+        if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_AC) != 0) {
+            plugType = 1;
+        } else if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_USB) != 0) {
+            plugType = 2;
+        } else if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0) {
+            plugType = 3;
+        }
+        return ((h.batteryStatus&STATE_BATTERY_STATUS_MASK)<<STATE_BATTERY_STATUS_SHIFT)
+                | ((h.batteryHealth&STATE_BATTERY_HEALTH_MASK)<<STATE_BATTERY_HEALTH_SHIFT)
+                | ((plugType&STATE_BATTERY_PLUG_MASK)<<STATE_BATTERY_PLUG_SHIFT)
+                | (h.states&(~STATE_BATTERY_MASK));
+    }
+
+    private void computeHistoryStepDetails(final HistoryStepDetails out,
+            final HistoryStepDetails last) {
+        final HistoryStepDetails tmp = last != null ? mTmpHistoryStepDetails : out;
+
+        // Perform a CPU update right after we do this collection, so we have started
+        // collecting good data for the next step.
+        requestImmediateCpuUpdate();
+
+        if (last == null) {
+            // We are not generating a delta, so all we need to do is reset the stats
+            // we will later be doing a delta from.
+            final int NU = mUidStats.size();
+            for (int i=0; i<NU; i++) {
+                final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.mLastStepUserTime = uid.mCurStepUserTime;
+                uid.mLastStepSystemTime = uid.mCurStepSystemTime;
+            }
+            mLastStepCpuUserTime = mCurStepCpuUserTime;
+            mLastStepCpuSystemTime = mCurStepCpuSystemTime;
+            mLastStepStatUserTime = mCurStepStatUserTime;
+            mLastStepStatSystemTime = mCurStepStatSystemTime;
+            mLastStepStatIOWaitTime = mCurStepStatIOWaitTime;
+            mLastStepStatIrqTime = mCurStepStatIrqTime;
+            mLastStepStatSoftIrqTime = mCurStepStatSoftIrqTime;
+            mLastStepStatIdleTime = mCurStepStatIdleTime;
+            tmp.clear();
+            return;
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "Step stats last: user=" + mLastStepCpuUserTime + " sys="
+                    + mLastStepStatSystemTime + " io=" + mLastStepStatIOWaitTime
+                    + " irq=" + mLastStepStatIrqTime + " sirq="
+                    + mLastStepStatSoftIrqTime + " idle=" + mLastStepStatIdleTime);
+            Slog.d(TAG, "Step stats cur: user=" + mCurStepCpuUserTime + " sys="
+                    + mCurStepStatSystemTime + " io=" + mCurStepStatIOWaitTime
+                    + " irq=" + mCurStepStatIrqTime + " sirq="
+                    + mCurStepStatSoftIrqTime + " idle=" + mCurStepStatIdleTime);
+        }
+        out.userTime = (int)(mCurStepCpuUserTime - mLastStepCpuUserTime);
+        out.systemTime = (int)(mCurStepCpuSystemTime - mLastStepCpuSystemTime);
+        out.statUserTime = (int)(mCurStepStatUserTime - mLastStepStatUserTime);
+        out.statSystemTime = (int)(mCurStepStatSystemTime - mLastStepStatSystemTime);
+        out.statIOWaitTime = (int)(mCurStepStatIOWaitTime - mLastStepStatIOWaitTime);
+        out.statIrqTime = (int)(mCurStepStatIrqTime - mLastStepStatIrqTime);
+        out.statSoftIrqTime = (int)(mCurStepStatSoftIrqTime - mLastStepStatSoftIrqTime);
+        out.statIdlTime = (int)(mCurStepStatIdleTime - mLastStepStatIdleTime);
+        out.appCpuUid1 = out.appCpuUid2 = out.appCpuUid3 = -1;
+        out.appCpuUTime1 = out.appCpuUTime2 = out.appCpuUTime3 = 0;
+        out.appCpuSTime1 = out.appCpuSTime2 = out.appCpuSTime3 = 0;
+        final int NU = mUidStats.size();
+        for (int i=0; i<NU; i++) {
+            final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+            final int totalUTime = (int)(uid.mCurStepUserTime - uid.mLastStepUserTime);
+            final int totalSTime = (int)(uid.mCurStepSystemTime - uid.mLastStepSystemTime);
+            final int totalTime = totalUTime + totalSTime;
+            uid.mLastStepUserTime = uid.mCurStepUserTime;
+            uid.mLastStepSystemTime = uid.mCurStepSystemTime;
+            if (totalTime <= (out.appCpuUTime3+out.appCpuSTime3)) {
+                continue;
+            }
+            if (totalTime <= (out.appCpuUTime2+out.appCpuSTime2)) {
+                out.appCpuUid3 = uid.mUid;
+                out.appCpuUTime3 = totalUTime;
+                out.appCpuSTime3 = totalSTime;
+            } else {
+                out.appCpuUid3 = out.appCpuUid2;
+                out.appCpuUTime3 = out.appCpuUTime2;
+                out.appCpuSTime3 = out.appCpuSTime2;
+                if (totalTime <= (out.appCpuUTime1+out.appCpuSTime1)) {
+                    out.appCpuUid2 = uid.mUid;
+                    out.appCpuUTime2 = totalUTime;
+                    out.appCpuSTime2 = totalSTime;
+                } else {
+                    out.appCpuUid2 = out.appCpuUid1;
+                    out.appCpuUTime2 = out.appCpuUTime1;
+                    out.appCpuSTime2 = out.appCpuSTime1;
+                    out.appCpuUid1 = uid.mUid;
+                    out.appCpuUTime1 = totalUTime;
+                    out.appCpuSTime1 = totalSTime;
+                }
+            }
+        }
+        mLastStepCpuUserTime = mCurStepCpuUserTime;
+        mLastStepCpuSystemTime = mCurStepCpuSystemTime;
+        mLastStepStatUserTime = mCurStepStatUserTime;
+        mLastStepStatSystemTime = mCurStepStatSystemTime;
+        mLastStepStatIOWaitTime = mCurStepStatIOWaitTime;
+        mLastStepStatIrqTime = mCurStepStatIrqTime;
+        mLastStepStatSoftIrqTime = mCurStepStatSoftIrqTime;
+        mLastStepStatIdleTime = mCurStepStatIdleTime;
+    }
+
+    public void readHistoryDelta(Parcel src, HistoryItem cur) {
+        int firstToken = src.readInt();
+        int deltaTimeToken = firstToken&DELTA_TIME_MASK;
+        cur.cmd = HistoryItem.CMD_UPDATE;
+        cur.numReadInts = 1;
+        if (DEBUG) Slog.i(TAG, "READ DELTA: firstToken=0x" + Integer.toHexString(firstToken)
+                + " deltaTimeToken=" + deltaTimeToken);
+
+        if (deltaTimeToken < DELTA_TIME_ABS) {
+            cur.time += deltaTimeToken;
+        } else if (deltaTimeToken == DELTA_TIME_ABS) {
+            cur.time = src.readLong();
+            cur.numReadInts += 2;
+            if (DEBUG) Slog.i(TAG, "READ DELTA: ABS time=" + cur.time);
+            cur.readFromParcel(src);
+            return;
+        } else if (deltaTimeToken == DELTA_TIME_INT) {
+            int delta = src.readInt();
+            cur.time += delta;
+            cur.numReadInts += 1;
+            if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + cur.time);
+        } else {
+            long delta = src.readLong();
+            if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + cur.time);
+            cur.time += delta;
+            cur.numReadInts += 2;
+        }
+
+        final int batteryLevelInt;
+        if ((firstToken&DELTA_BATTERY_LEVEL_FLAG) != 0) {
+            batteryLevelInt = src.readInt();
+            readBatteryLevelInt(batteryLevelInt, cur);
+            cur.numReadInts += 1;
+            if (DEBUG) Slog.i(TAG, "READ DELTA: batteryToken=0x"
+                    + Integer.toHexString(batteryLevelInt)
+                    + " batteryLevel=" + cur.batteryLevel
+                    + " batteryTemp=" + cur.batteryTemperature
+                    + " batteryVolt=" + (int)cur.batteryVoltage);
+        } else {
+            batteryLevelInt = 0;
+        }
+
+        if ((firstToken&DELTA_STATE_FLAG) != 0) {
+            int stateInt = src.readInt();
+            cur.states = (firstToken&DELTA_STATE_MASK) | (stateInt&(~STATE_BATTERY_MASK));
+            cur.batteryStatus = (byte)((stateInt>>STATE_BATTERY_STATUS_SHIFT)
+                    & STATE_BATTERY_STATUS_MASK);
+            cur.batteryHealth = (byte)((stateInt>>STATE_BATTERY_HEALTH_SHIFT)
+                    & STATE_BATTERY_HEALTH_MASK);
+            cur.batteryPlugType = (byte)((stateInt>>STATE_BATTERY_PLUG_SHIFT)
+                    & STATE_BATTERY_PLUG_MASK);
+            switch (cur.batteryPlugType) {
+                case 1:
+                    cur.batteryPlugType = BatteryManager.BATTERY_PLUGGED_AC;
+                    break;
+                case 2:
+                    cur.batteryPlugType = BatteryManager.BATTERY_PLUGGED_USB;
+                    break;
+                case 3:
+                    cur.batteryPlugType = BatteryManager.BATTERY_PLUGGED_WIRELESS;
+                    break;
+            }
+            cur.numReadInts += 1;
+            if (DEBUG) Slog.i(TAG, "READ DELTA: stateToken=0x"
+                    + Integer.toHexString(stateInt)
+                    + " batteryStatus=" + cur.batteryStatus
+                    + " batteryHealth=" + cur.batteryHealth
+                    + " batteryPlugType=" + cur.batteryPlugType
+                    + " states=0x" + Integer.toHexString(cur.states));
+        } else {
+            cur.states = (firstToken&DELTA_STATE_MASK) | (cur.states&(~STATE_BATTERY_MASK));
+        }
+
+        if ((firstToken&DELTA_STATE2_FLAG) != 0) {
+            cur.states2 = src.readInt();
+            if (DEBUG) Slog.i(TAG, "READ DELTA: states2=0x"
+                    + Integer.toHexString(cur.states2));
+        }
+
+        if ((firstToken&DELTA_WAKELOCK_FLAG) != 0) {
+            int indexes = src.readInt();
+            int wakeLockIndex = indexes&0xffff;
+            int wakeReasonIndex = (indexes>>16)&0xffff;
+            if (wakeLockIndex != 0xffff) {
+                cur.wakelockTag = cur.localWakelockTag;
+                readHistoryTag(wakeLockIndex, cur.wakelockTag);
+                if (DEBUG) Slog.i(TAG, "READ DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx
+                    + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string);
+            } else {
+                cur.wakelockTag = null;
+            }
+            if (wakeReasonIndex != 0xffff) {
+                cur.wakeReasonTag = cur.localWakeReasonTag;
+                readHistoryTag(wakeReasonIndex, cur.wakeReasonTag);
+                if (DEBUG) Slog.i(TAG, "READ DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx
+                    + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string);
+            } else {
+                cur.wakeReasonTag = null;
+            }
+            cur.numReadInts += 1;
+        } else {
+            cur.wakelockTag = null;
+            cur.wakeReasonTag = null;
+        }
+
+        if ((firstToken&DELTA_EVENT_FLAG) != 0) {
+            cur.eventTag = cur.localEventTag;
+            final int codeAndIndex = src.readInt();
+            cur.eventCode = (codeAndIndex&0xffff);
+            final int index = ((codeAndIndex>>16)&0xffff);
+            readHistoryTag(index, cur.eventTag);
+            cur.numReadInts += 1;
+            if (DEBUG) Slog.i(TAG, "READ DELTA: event=" + cur.eventCode + " tag=#"
+                    + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":"
+                    + cur.eventTag.string);
+        } else {
+            cur.eventCode = HistoryItem.EVENT_NONE;
+        }
+
+        if ((batteryLevelInt&BATTERY_DELTA_LEVEL_FLAG) != 0) {
+            cur.stepDetails = mReadHistoryStepDetails;
+            cur.stepDetails.readFromParcel(src);
+        } else {
+            cur.stepDetails = null;
+        }
+
+        if ((firstToken&DELTA_BATTERY_CHARGE_FLAG) != 0) {
+            cur.batteryChargeUAh = src.readInt();
+        }
+    }
+
+    @Override
+    public void commitCurrentHistoryBatchLocked() {
+        mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+    }
+
+    void addHistoryBufferLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
+        if (!mHaveBatteryLevel || !mRecordingHistory) {
+            return;
+        }
+
+        final long timeDiff = (mHistoryBaseTime+elapsedRealtimeMs) - mHistoryLastWritten.time;
+        final int diffStates = mHistoryLastWritten.states^(cur.states&mActiveHistoryStates);
+        final int diffStates2 = mHistoryLastWritten.states2^(cur.states2&mActiveHistoryStates2);
+        final int lastDiffStates = mHistoryLastWritten.states^mHistoryLastLastWritten.states;
+        final int lastDiffStates2 = mHistoryLastWritten.states2^mHistoryLastLastWritten.states2;
+        if (DEBUG) Slog.i(TAG, "ADD: tdelta=" + timeDiff + " diff="
+                + Integer.toHexString(diffStates) + " lastDiff="
+                + Integer.toHexString(lastDiffStates) + " diff2="
+                + Integer.toHexString(diffStates2) + " lastDiff2="
+                + Integer.toHexString(lastDiffStates2));
+        if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE
+                && timeDiff < 1000 && (diffStates&lastDiffStates) == 0
+                && (diffStates2&lastDiffStates2) == 0
+                && (mHistoryLastWritten.wakelockTag == null || cur.wakelockTag == null)
+                && (mHistoryLastWritten.wakeReasonTag == null || cur.wakeReasonTag == null)
+                && mHistoryLastWritten.stepDetails == null
+                && (mHistoryLastWritten.eventCode == HistoryItem.EVENT_NONE
+                        || cur.eventCode == HistoryItem.EVENT_NONE)
+                && mHistoryLastWritten.batteryLevel == cur.batteryLevel
+                && mHistoryLastWritten.batteryStatus == cur.batteryStatus
+                && mHistoryLastWritten.batteryHealth == cur.batteryHealth
+                && mHistoryLastWritten.batteryPlugType == cur.batteryPlugType
+                && mHistoryLastWritten.batteryTemperature == cur.batteryTemperature
+                && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage) {
+            // We can merge this new change in with the last one.  Merging is
+            // allowed as long as only the states have changed, and within those states
+            // as long as no bit has changed both between now and the last entry, as
+            // well as the last entry and the one before it (so we capture any toggles).
+            if (DEBUG) Slog.i(TAG, "ADD: rewinding back to " + mHistoryBufferLastPos);
+            mHistoryBuffer.setDataSize(mHistoryBufferLastPos);
+            mHistoryBuffer.setDataPosition(mHistoryBufferLastPos);
+            mHistoryBufferLastPos = -1;
+            elapsedRealtimeMs = mHistoryLastWritten.time - mHistoryBaseTime;
+            // If the last written history had a wakelock tag, we need to retain it.
+            // Note that the condition above made sure that we aren't in a case where
+            // both it and the current history item have a wakelock tag.
+            if (mHistoryLastWritten.wakelockTag != null) {
+                cur.wakelockTag = cur.localWakelockTag;
+                cur.wakelockTag.setTo(mHistoryLastWritten.wakelockTag);
+            }
+            // If the last written history had a wake reason tag, we need to retain it.
+            // Note that the condition above made sure that we aren't in a case where
+            // both it and the current history item have a wakelock tag.
+            if (mHistoryLastWritten.wakeReasonTag != null) {
+                cur.wakeReasonTag = cur.localWakeReasonTag;
+                cur.wakeReasonTag.setTo(mHistoryLastWritten.wakeReasonTag);
+            }
+            // If the last written history had an event, we need to retain it.
+            // Note that the condition above made sure that we aren't in a case where
+            // both it and the current history item have an event.
+            if (mHistoryLastWritten.eventCode != HistoryItem.EVENT_NONE) {
+                cur.eventCode = mHistoryLastWritten.eventCode;
+                cur.eventTag = cur.localEventTag;
+                cur.eventTag.setTo(mHistoryLastWritten.eventTag);
+            }
+            mHistoryLastWritten.setTo(mHistoryLastLastWritten);
+        }
+
+        boolean recordResetDueToOverflow = false;
+        final int dataSize = mHistoryBuffer.dataSize();
+        if (dataSize >= MAX_MAX_HISTORY_BUFFER*3) {
+            // Clients can't deal with history buffers this large. This only
+            // really happens when the device is on charger and interacted with
+            // for long periods of time, like in retail mode. Since the device is
+            // most likely charged, when unplugged, stats would have reset anyways.
+            // Reset the stats and mark that we overflowed.
+            // b/32540341
+            resetAllStatsLocked();
+
+            // Mark that we want to set *OVERFLOW* event and the RESET:START
+            // events.
+            recordResetDueToOverflow = true;
+
+        } else if (dataSize >= MAX_HISTORY_BUFFER) {
+            if (!mHistoryOverflow) {
+                mHistoryOverflow = true;
+                addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur);
+                addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_OVERFLOW, cur);
+                return;
+            }
+
+            // After overflow, we allow various bit-wise states to settle to 0.
+            boolean writeAnyway = false;
+            final int curStates = cur.states & HistoryItem.SETTLE_TO_ZERO_STATES
+                    & mActiveHistoryStates;
+            if (mHistoryLastWritten.states != curStates) {
+                // mActiveHistoryStates keeps track of which bits in .states are now being
+                // forced to 0.
+                int old = mActiveHistoryStates;
+                mActiveHistoryStates &= curStates | ~HistoryItem.SETTLE_TO_ZERO_STATES;
+                writeAnyway |= old != mActiveHistoryStates;
+            }
+            final int curStates2 = cur.states2 & HistoryItem.SETTLE_TO_ZERO_STATES2
+                    & mActiveHistoryStates2;
+            if (mHistoryLastWritten.states2 != curStates2) {
+                // mActiveHistoryStates2 keeps track of which bits in .states2 are now being
+                // forced to 0.
+                int old = mActiveHistoryStates2;
+                mActiveHistoryStates2 &= curStates2 | ~HistoryItem.SETTLE_TO_ZERO_STATES2;
+                writeAnyway |= old != mActiveHistoryStates2;
+            }
+
+            // Once we've reached the maximum number of items, we only
+            // record changes to the battery level and the most interesting states.
+            // Once we've reached the maximum maximum number of items, we only
+            // record changes to the battery level.
+            if (!writeAnyway && mHistoryLastWritten.batteryLevel == cur.batteryLevel &&
+                    (dataSize >= MAX_MAX_HISTORY_BUFFER
+                            || ((mHistoryLastWritten.states^cur.states)
+                                    & HistoryItem.MOST_INTERESTING_STATES) == 0
+                            || ((mHistoryLastWritten.states2^cur.states2)
+                                    & HistoryItem.MOST_INTERESTING_STATES2) == 0)) {
+                return;
+            }
+
+            addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur);
+            return;
+        }
+
+        if (dataSize == 0 || recordResetDueToOverflow) {
+            // The history is currently empty; we need it to start with a time stamp.
+            cur.currentTime = System.currentTimeMillis();
+            if (recordResetDueToOverflow) {
+                addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_OVERFLOW, cur);
+            }
+            addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_RESET, cur);
+        }
+        addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur);
+    }
+
+    private void addHistoryBufferLocked(long elapsedRealtimeMs, long uptimeMs, byte cmd,
+            HistoryItem cur) {
+        if (mIteratingHistory) {
+            throw new IllegalStateException("Can't do this while iterating history!");
+        }
+        mHistoryBufferLastPos = mHistoryBuffer.dataPosition();
+        mHistoryLastLastWritten.setTo(mHistoryLastWritten);
+        mHistoryLastWritten.setTo(mHistoryBaseTime + elapsedRealtimeMs, cmd, cur);
+        mHistoryLastWritten.states &= mActiveHistoryStates;
+        mHistoryLastWritten.states2 &= mActiveHistoryStates2;
+        writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten);
+        mLastHistoryElapsedRealtime = elapsedRealtimeMs;
+        cur.wakelockTag = null;
+        cur.wakeReasonTag = null;
+        cur.eventCode = HistoryItem.EVENT_NONE;
+        cur.eventTag = null;
+        if (DEBUG_HISTORY) Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos
+                + " now " + mHistoryBuffer.dataPosition()
+                + " size is now " + mHistoryBuffer.dataSize());
+    }
+
+    int mChangedStates = 0;
+    int mChangedStates2 = 0;
+
+    void addHistoryRecordLocked(long elapsedRealtimeMs, long uptimeMs) {
+        if (mTrackRunningHistoryElapsedRealtime != 0) {
+            final long diffElapsed = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtime;
+            final long diffUptime = uptimeMs - mTrackRunningHistoryUptime;
+            if (diffUptime < (diffElapsed-20)) {
+                final long wakeElapsedTime = elapsedRealtimeMs - (diffElapsed - diffUptime);
+                mHistoryAddTmp.setTo(mHistoryLastWritten);
+                mHistoryAddTmp.wakelockTag = null;
+                mHistoryAddTmp.wakeReasonTag = null;
+                mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
+                mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
+                addHistoryRecordInnerLocked(wakeElapsedTime, uptimeMs, mHistoryAddTmp);
+            }
+        }
+        mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
+        mTrackRunningHistoryElapsedRealtime = elapsedRealtimeMs;
+        mTrackRunningHistoryUptime = uptimeMs;
+        addHistoryRecordInnerLocked(elapsedRealtimeMs, uptimeMs, mHistoryCur);
+    }
+
+    void addHistoryRecordInnerLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
+        addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, cur);
+
+        if (!USE_OLD_HISTORY) {
+            return;
+        }
+
+        if (!mHaveBatteryLevel || !mRecordingHistory) {
+            return;
+        }
+
+        // If the current time is basically the same as the last time,
+        // and no states have since the last recorded entry changed and
+        // are now resetting back to their original value, then just collapse
+        // into one record.
+        if (mHistoryEnd != null && mHistoryEnd.cmd == HistoryItem.CMD_UPDATE
+                && (mHistoryBaseTime+elapsedRealtimeMs) < (mHistoryEnd.time+1000)
+                && ((mHistoryEnd.states^cur.states)&mChangedStates&mActiveHistoryStates) == 0
+                && ((mHistoryEnd.states2^cur.states2)&mChangedStates2&mActiveHistoryStates2) == 0) {
+            // If the current is the same as the one before, then we no
+            // longer need the entry.
+            if (mHistoryLastEnd != null && mHistoryLastEnd.cmd == HistoryItem.CMD_UPDATE
+                    && (mHistoryBaseTime+elapsedRealtimeMs) < (mHistoryEnd.time+500)
+                    && mHistoryLastEnd.sameNonEvent(cur)) {
+                mHistoryLastEnd.next = null;
+                mHistoryEnd.next = mHistoryCache;
+                mHistoryCache = mHistoryEnd;
+                mHistoryEnd = mHistoryLastEnd;
+                mHistoryLastEnd = null;
+            } else {
+                mChangedStates |= mHistoryEnd.states^(cur.states&mActiveHistoryStates);
+                mChangedStates2 |= mHistoryEnd.states^(cur.states2&mActiveHistoryStates2);
+                mHistoryEnd.setTo(mHistoryEnd.time, HistoryItem.CMD_UPDATE, cur);
+            }
+            return;
+        }
+
+        mChangedStates = 0;
+        mChangedStates2 = 0;
+
+        if (mNumHistoryItems == MAX_HISTORY_ITEMS
+                || mNumHistoryItems == MAX_MAX_HISTORY_ITEMS) {
+            addHistoryRecordLocked(elapsedRealtimeMs, HistoryItem.CMD_OVERFLOW);
+        }
+
+        if (mNumHistoryItems >= MAX_HISTORY_ITEMS) {
+            // Once we've reached the maximum number of items, we only
+            // record changes to the battery level and the most interesting states.
+            // Once we've reached the maximum maximum number of items, we only
+            // record changes to the battery level.
+            if (mHistoryEnd != null && mHistoryEnd.batteryLevel
+                    == cur.batteryLevel &&
+                    (mNumHistoryItems >= MAX_MAX_HISTORY_ITEMS
+                            || ((mHistoryEnd.states^(cur.states&mActiveHistoryStates))
+                                    & HistoryItem.MOST_INTERESTING_STATES) == 0)) {
+                return;
+            }
+        }
+
+        addHistoryRecordLocked(elapsedRealtimeMs, HistoryItem.CMD_UPDATE);
+    }
+
+    public void addHistoryEventLocked(long elapsedRealtimeMs, long uptimeMs, int code,
+            String name, int uid) {
+        mHistoryCur.eventCode = code;
+        mHistoryCur.eventTag = mHistoryCur.localEventTag;
+        mHistoryCur.eventTag.string = name;
+        mHistoryCur.eventTag.uid = uid;
+        addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+    }
+
+    void addHistoryRecordLocked(long elapsedRealtimeMs, long uptimeMs, byte cmd, HistoryItem cur) {
+        HistoryItem rec = mHistoryCache;
+        if (rec != null) {
+            mHistoryCache = rec.next;
+        } else {
+            rec = new HistoryItem();
+        }
+        rec.setTo(mHistoryBaseTime + elapsedRealtimeMs, cmd, cur);
+
+        addHistoryRecordLocked(rec);
+    }
+
+    void addHistoryRecordLocked(HistoryItem rec) {
+        mNumHistoryItems++;
+        rec.next = null;
+        mHistoryLastEnd = mHistoryEnd;
+        if (mHistoryEnd != null) {
+            mHistoryEnd.next = rec;
+            mHistoryEnd = rec;
+        } else {
+            mHistory = mHistoryEnd = rec;
+        }
+    }
+
+    void clearHistoryLocked() {
+        if (DEBUG_HISTORY) Slog.i(TAG, "********** CLEARING HISTORY!");
+        if (USE_OLD_HISTORY) {
+            if (mHistory != null) {
+                mHistoryEnd.next = mHistoryCache;
+                mHistoryCache = mHistory;
+                mHistory = mHistoryLastEnd = mHistoryEnd = null;
+            }
+            mNumHistoryItems = 0;
+        }
+
+        mHistoryBaseTime = 0;
+        mLastHistoryElapsedRealtime = 0;
+        mTrackRunningHistoryElapsedRealtime = 0;
+        mTrackRunningHistoryUptime = 0;
+
+        mHistoryBuffer.setDataSize(0);
+        mHistoryBuffer.setDataPosition(0);
+        mHistoryBuffer.setDataCapacity(MAX_HISTORY_BUFFER / 2);
+        mHistoryLastLastWritten.clear();
+        mHistoryLastWritten.clear();
+        mHistoryTagPool.clear();
+        mNextHistoryTagIdx = 0;
+        mNumHistoryTagChars = 0;
+        mHistoryBufferLastPos = -1;
+        mHistoryOverflow = false;
+        mActiveHistoryStates = 0xffffffff;
+        mActiveHistoryStates2 = 0xffffffff;
+    }
+
+    public void updateTimeBasesLocked(boolean unplugged, boolean screenOff, long uptime,
+            long realtime) {
+        final boolean updateOnBatteryTimeBase = unplugged != mOnBatteryTimeBase.isRunning();
+        final boolean updateOnBatteryScreenOffTimeBase =
+                (unplugged && screenOff) != mOnBatteryScreenOffTimeBase.isRunning();
+
+        if (updateOnBatteryScreenOffTimeBase || updateOnBatteryTimeBase) {
+            if (updateOnBatteryScreenOffTimeBase) {
+                updateKernelWakelocksLocked();
+                updateBatteryPropertiesLocked();
+            }
+            if (DEBUG_ENERGY_CPU) {
+                Slog.d(TAG, "Updating cpu time because screen is now " + (screenOff ? "off" : "on")
+                        + " and battery is " + (unplugged ? "on" : "off"));
+            }
+            updateCpuTimeLocked();
+
+            mOnBatteryTimeBase.setRunning(unplugged, uptime, realtime);
+            mOnBatteryScreenOffTimeBase.setRunning(unplugged && screenOff, uptime, realtime);
+            for (int i = mUidStats.size() - 1; i >= 0; --i) {
+                final Uid u = mUidStats.valueAt(i);
+                if (updateOnBatteryTimeBase) {
+                    u.updateOnBatteryBgTimeBase(uptime, realtime);
+                }
+                if (updateOnBatteryScreenOffTimeBase) {
+                    u.updateOnBatteryScreenOffBgTimeBase(uptime, realtime);
+                }
+            }
+        }
+    }
+
+    private void updateBatteryPropertiesLocked() {
+        try {
+            IBatteryPropertiesRegistrar registrar = IBatteryPropertiesRegistrar.Stub.asInterface(
+                    ServiceManager.getService("batteryproperties"));
+            registrar.scheduleUpdate();
+        } catch (RemoteException e) {
+            // Ignore.
+        }
+    }
+
+    public void addIsolatedUidLocked(int isolatedUid, int appUid) {
+        mIsolatedUids.put(isolatedUid, appUid);
+    }
+
+    /**
+     * Schedules a read of the latest cpu times before removing the isolated UID.
+     * @see #removeIsolatedUidLocked(int)
+     */
+    public void scheduleRemoveIsolatedUidLocked(int isolatedUid, int appUid) {
+        int curUid = mIsolatedUids.get(isolatedUid, -1);
+        if (curUid == appUid) {
+            if (mExternalSync != null) {
+                mExternalSync.scheduleCpuSyncDueToRemovedUid(isolatedUid);
+            }
+        }
+    }
+
+    /**
+     * This should only be called after the cpu times have been read.
+     * @see #scheduleRemoveIsolatedUidLocked(int, int)
+     */
+    public void removeIsolatedUidLocked(int isolatedUid) {
+        mIsolatedUids.delete(isolatedUid);
+        mKernelUidCpuTimeReader.removeUid(isolatedUid);
+        mKernelUidCpuFreqTimeReader.removeUid(isolatedUid);
+    }
+
+    public int mapUid(int uid) {
+        int isolated = mIsolatedUids.get(uid, -1);
+        return isolated > 0 ? isolated : uid;
+    }
+
+    public void noteEventLocked(int code, String name, int uid) {
+        uid = mapUid(uid);
+        if (!mActiveEvents.updateState(code, name, uid, 0)) {
+            return;
+        }
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, code, name, uid);
+    }
+
+    boolean ensureStartClockTime(final long currentTime) {
+        final long ABOUT_ONE_YEAR = 365*24*60*60*1000L;
+        if (currentTime > ABOUT_ONE_YEAR && mStartClockTime < (currentTime-ABOUT_ONE_YEAR)) {
+            // If the start clock time has changed by more than a year, then presumably
+            // the previous time was completely bogus.  So we are going to figure out a
+            // new time based on how much time has elapsed since we started counting.
+            mStartClockTime = currentTime - (mClocks.elapsedRealtime()-(mRealtimeStart/1000));
+            return true;
+        }
+        return false;
+    }
+
+    public void noteCurrentTimeChangedLocked() {
+        final long currentTime = System.currentTimeMillis();
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        recordCurrentTimeChangeLocked(currentTime, elapsedRealtime, uptime);
+        ensureStartClockTime(currentTime);
+    }
+
+    public void noteProcessStartLocked(String name, int uid) {
+        uid = mapUid(uid);
+        if (isOnBattery()) {
+            Uid u = getUidStatsLocked(uid);
+            u.getProcessStatsLocked(name).incStartsLocked();
+        }
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_PROC_START, name, uid, 0)) {
+            return;
+        }
+        if (!mRecordAllHistory) {
+            return;
+        }
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_PROC_START, name, uid);
+    }
+
+    public void noteProcessCrashLocked(String name, int uid) {
+        uid = mapUid(uid);
+        if (isOnBattery()) {
+            Uid u = getUidStatsLocked(uid);
+            u.getProcessStatsLocked(name).incNumCrashesLocked();
+        }
+    }
+
+    public void noteProcessAnrLocked(String name, int uid) {
+        uid = mapUid(uid);
+        if (isOnBattery()) {
+            Uid u = getUidStatsLocked(uid);
+            u.getProcessStatsLocked(name).incNumAnrsLocked();
+        }
+    }
+
+    public void noteUidProcessStateLocked(int uid, int state) {
+        int parentUid = mapUid(uid);
+        if (uid != parentUid) {
+            // Isolated UIDs process state is already rolled up into parent, so no need to track
+            // Otherwise the parent's process state will get downgraded incorrectly
+            return;
+        }
+        getUidStatsLocked(uid).updateUidProcessStateLocked(state);
+    }
+
+    public void noteProcessFinishLocked(String name, int uid) {
+        uid = mapUid(uid);
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_PROC_FINISH, name, uid, 0)) {
+            return;
+        }
+        if (!mRecordAllHistory) {
+            return;
+        }
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_PROC_FINISH, name, uid);
+    }
+
+    public void noteSyncStartLocked(String name, int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        getUidStatsLocked(uid).noteStartSyncLocked(name, elapsedRealtime);
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_SYNC_START, name, uid, 0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_SYNC_START, name, uid);
+    }
+
+    public void noteSyncFinishLocked(String name, int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        getUidStatsLocked(uid).noteStopSyncLocked(name, elapsedRealtime);
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_SYNC_FINISH, name, uid, 0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_SYNC_FINISH, name, uid);
+    }
+
+    public void noteJobStartLocked(String name, int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        getUidStatsLocked(uid).noteStartJobLocked(name, elapsedRealtime);
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_JOB_START, name, uid, 0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_JOB_START, name, uid);
+    }
+
+    public void noteJobFinishLocked(String name, int uid, int stopReason) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        getUidStatsLocked(uid).noteStopJobLocked(name, elapsedRealtime, stopReason);
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_JOB_FINISH, name, uid, 0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_JOB_FINISH, name, uid);
+    }
+
+    public void noteAlarmStartLocked(String name, int uid) {
+        if (!mRecordAllHistory) {
+            return;
+        }
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_ALARM_START, name, uid, 0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ALARM_START, name, uid);
+    }
+
+    public void noteAlarmFinishLocked(String name, int uid) {
+        if (!mRecordAllHistory) {
+            return;
+        }
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_ALARM_FINISH, name, uid, 0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ALARM_FINISH, name, uid);
+    }
+
+    private void requestWakelockCpuUpdate() {
+        if (!mHandler.hasMessages(MSG_UPDATE_WAKELOCKS)) {
+            Message m = mHandler.obtainMessage(MSG_UPDATE_WAKELOCKS);
+            mHandler.sendMessageDelayed(m, DELAY_UPDATE_WAKELOCKS);
+        }
+    }
+
+    private void requestImmediateCpuUpdate() {
+        mHandler.removeMessages(MSG_UPDATE_WAKELOCKS);
+        mHandler.sendEmptyMessage(MSG_UPDATE_WAKELOCKS);
+    }
+
+    public void setRecordAllHistoryLocked(boolean enabled) {
+        mRecordAllHistory = enabled;
+        if (!enabled) {
+            // Clear out any existing state.
+            mActiveEvents.removeEvents(HistoryItem.EVENT_WAKE_LOCK);
+            mActiveEvents.removeEvents(HistoryItem.EVENT_ALARM);
+            // Record the currently running processes as stopping, now that we are no
+            // longer tracking them.
+            HashMap<String, SparseIntArray> active = mActiveEvents.getStateForEvent(
+                    HistoryItem.EVENT_PROC);
+            if (active != null) {
+                long mSecRealtime = mClocks.elapsedRealtime();
+                final long mSecUptime = mClocks.uptimeMillis();
+                for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) {
+                    SparseIntArray uids = ent.getValue();
+                    for (int j=0; j<uids.size(); j++) {
+                        addHistoryEventLocked(mSecRealtime, mSecUptime,
+                                HistoryItem.EVENT_PROC_FINISH, ent.getKey(), uids.keyAt(j));
+                    }
+                }
+            }
+        } else {
+            // Record the currently running processes as starting, now that we are tracking them.
+            HashMap<String, SparseIntArray> active = mActiveEvents.getStateForEvent(
+                    HistoryItem.EVENT_PROC);
+            if (active != null) {
+                long mSecRealtime = mClocks.elapsedRealtime();
+                final long mSecUptime = mClocks.uptimeMillis();
+                for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) {
+                    SparseIntArray uids = ent.getValue();
+                    for (int j=0; j<uids.size(); j++) {
+                        addHistoryEventLocked(mSecRealtime, mSecUptime,
+                                HistoryItem.EVENT_PROC_START, ent.getKey(), uids.keyAt(j));
+                    }
+                }
+            }
+        }
+    }
+
+    public void setNoAutoReset(boolean enabled) {
+        mNoAutoReset = enabled;
+    }
+
+    public void setPretendScreenOff(boolean pretendScreenOff) {
+        mPretendScreenOff = pretendScreenOff;
+        noteScreenStateLocked(pretendScreenOff ? Display.STATE_OFF : Display.STATE_ON);
+    }
+
+    private String mInitialAcquireWakeName;
+    private int mInitialAcquireWakeUid = -1;
+
+    public void noteStartWakeLocked(int uid, int pid, String name, String historyName, int type,
+            boolean unimportantForLogging, long elapsedRealtime, long uptime) {
+        uid = mapUid(uid);
+        if (type == WAKE_TYPE_PARTIAL) {
+            // Only care about partial wake locks, since full wake locks
+            // will be canceled when the user puts the screen to sleep.
+            aggregateLastWakeupUptimeLocked(uptime);
+            if (historyName == null) {
+                historyName = name;
+            }
+            if (mRecordAllHistory) {
+                if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_START, historyName,
+                        uid, 0)) {
+                    addHistoryEventLocked(elapsedRealtime, uptime,
+                            HistoryItem.EVENT_WAKE_LOCK_START, historyName, uid);
+                }
+            }
+            if (mWakeLockNesting == 0) {
+                mHistoryCur.states |= HistoryItem.STATE_WAKE_LOCK_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Start wake lock to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+                mHistoryCur.wakelockTag.string = mInitialAcquireWakeName = historyName;
+                mHistoryCur.wakelockTag.uid = mInitialAcquireWakeUid = uid;
+                mWakeLockImportant = !unimportantForLogging;
+                addHistoryRecordLocked(elapsedRealtime, uptime);
+            } else if (!mWakeLockImportant && !unimportantForLogging
+                    && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE) {
+                if (mHistoryLastWritten.wakelockTag != null) {
+                    // We'll try to update the last tag.
+                    mHistoryLastWritten.wakelockTag = null;
+                    mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+                    mHistoryCur.wakelockTag.string = mInitialAcquireWakeName = historyName;
+                    mHistoryCur.wakelockTag.uid = mInitialAcquireWakeUid = uid;
+                    addHistoryRecordLocked(elapsedRealtime, uptime);
+                }
+                mWakeLockImportant = true;
+            }
+            mWakeLockNesting++;
+        }
+        if (uid >= 0) {
+            if (mOnBatteryScreenOffTimeBase.isRunning()) {
+                // We only update the cpu time when a wake lock is acquired if the screen is off.
+                // If the screen is on, we don't distribute the power amongst partial wakelocks.
+                if (DEBUG_ENERGY_CPU) {
+                    Slog.d(TAG, "Updating cpu time because of +wake_lock");
+                }
+                requestWakelockCpuUpdate();
+            }
+            getUidStatsLocked(uid).noteStartWakeLocked(pid, name, type, elapsedRealtime);
+        }
+    }
+
+    public void noteStopWakeLocked(int uid, int pid, String name, String historyName, int type,
+            long elapsedRealtime, long uptime) {
+        uid = mapUid(uid);
+        if (type == WAKE_TYPE_PARTIAL) {
+            mWakeLockNesting--;
+            if (mRecordAllHistory) {
+                if (historyName == null) {
+                    historyName = name;
+                }
+                if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_FINISH, historyName,
+                        uid, 0)) {
+                    addHistoryEventLocked(elapsedRealtime, uptime,
+                            HistoryItem.EVENT_WAKE_LOCK_FINISH, historyName, uid);
+                }
+            }
+            if (mWakeLockNesting == 0) {
+                mHistoryCur.states &= ~HistoryItem.STATE_WAKE_LOCK_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Stop wake lock to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                mInitialAcquireWakeName = null;
+                mInitialAcquireWakeUid = -1;
+                addHistoryRecordLocked(elapsedRealtime, uptime);
+            }
+        }
+        if (uid >= 0) {
+            if (mOnBatteryScreenOffTimeBase.isRunning()) {
+                if (DEBUG_ENERGY_CPU) {
+                    Slog.d(TAG, "Updating cpu time because of -wake_lock");
+                }
+                requestWakelockCpuUpdate();
+            }
+            getUidStatsLocked(uid).noteStopWakeLocked(pid, name, type, elapsedRealtime);
+        }
+    }
+
+    public void noteStartWakeFromSourceLocked(WorkSource ws, int pid, String name,
+            String historyName, int type, boolean unimportantForLogging) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        final int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteStartWakeLocked(ws.get(i), pid, name, historyName, type, unimportantForLogging,
+                    elapsedRealtime, uptime);
+        }
+    }
+
+    public void noteChangeWakelockFromSourceLocked(WorkSource ws, int pid, String name,
+            String historyName, int type, WorkSource newWs, int newPid, String newName,
+            String newHistoryName, int newType, boolean newUnimportantForLogging) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        // For correct semantics, we start the need worksources first, so that we won't
+        // make inappropriate history items as if all wake locks went away and new ones
+        // appeared.  This is okay because tracking of wake locks allows nesting.
+        final int NN = newWs.size();
+        for (int i=0; i<NN; i++) {
+            noteStartWakeLocked(newWs.get(i), newPid, newName, newHistoryName, newType,
+                    newUnimportantForLogging, elapsedRealtime, uptime);
+        }
+        final int NO = ws.size();
+        for (int i=0; i<NO; i++) {
+            noteStopWakeLocked(ws.get(i), pid, name, historyName, type, elapsedRealtime, uptime);
+        }
+    }
+
+    public void noteStopWakeFromSourceLocked(WorkSource ws, int pid, String name,
+            String historyName, int type) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        final int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteStopWakeLocked(ws.get(i), pid, name, historyName, type, elapsedRealtime, uptime);
+        }
+    }
+
+    public void noteLongPartialWakelockStart(String name, String historyName, int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (historyName == null) {
+            historyName = name;
+        }
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_LONG_WAKE_LOCK_START, historyName, uid,
+                0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_LONG_WAKE_LOCK_START,
+                historyName, uid);
+    }
+
+    public void noteLongPartialWakelockFinish(String name, String historyName, int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (historyName == null) {
+            historyName = name;
+        }
+        if (!mActiveEvents.updateState(HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH, historyName, uid,
+                0)) {
+            return;
+        }
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH,
+                historyName, uid);
+    }
+
+    void aggregateLastWakeupUptimeLocked(long uptimeMs) {
+        if (mLastWakeupReason != null) {
+            long deltaUptime = uptimeMs - mLastWakeupUptimeMs;
+            SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason);
+            timer.add(deltaUptime * 1000, 1); // time in in microseconds
+            mLastWakeupReason = null;
+        }
+    }
+
+    public void noteWakeupReasonLocked(String reason) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (DEBUG_HISTORY) Slog.v(TAG, "Wakeup reason \"" + reason +"\": "
+                + Integer.toHexString(mHistoryCur.states));
+        aggregateLastWakeupUptimeLocked(uptime);
+        mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
+        mHistoryCur.wakeReasonTag.string = reason;
+        mHistoryCur.wakeReasonTag.uid = 0;
+        mLastWakeupReason = reason;
+        mLastWakeupUptimeMs = uptime;
+        addHistoryRecordLocked(elapsedRealtime, uptime);
+    }
+
+    public boolean startAddingCpuLocked() {
+        mHandler.removeMessages(MSG_UPDATE_WAKELOCKS);
+        return mOnBatteryInternal;
+    }
+
+    public void finishAddingCpuLocked(int totalUTime, int totalSTime, int statUserTime,
+                                      int statSystemTime, int statIOWaitTime, int statIrqTime,
+                                      int statSoftIrqTime, int statIdleTime) {
+        if (DEBUG) Slog.d(TAG, "Adding cpu: tuser=" + totalUTime + " tsys=" + totalSTime
+                + " user=" + statUserTime + " sys=" + statSystemTime
+                + " io=" + statIOWaitTime + " irq=" + statIrqTime
+                + " sirq=" + statSoftIrqTime + " idle=" + statIdleTime);
+        mCurStepCpuUserTime += totalUTime;
+        mCurStepCpuSystemTime += totalSTime;
+        mCurStepStatUserTime += statUserTime;
+        mCurStepStatSystemTime += statSystemTime;
+        mCurStepStatIOWaitTime += statIOWaitTime;
+        mCurStepStatIrqTime += statIrqTime;
+        mCurStepStatSoftIrqTime += statSoftIrqTime;
+        mCurStepStatIdleTime += statIdleTime;
+    }
+
+    public void noteProcessDiedLocked(int uid, int pid) {
+        uid = mapUid(uid);
+        Uid u = mUidStats.get(uid);
+        if (u != null) {
+            u.mPids.remove(pid);
+        }
+    }
+
+    public long getProcessWakeTime(int uid, int pid, long realtime) {
+        uid = mapUid(uid);
+        Uid u = mUidStats.get(uid);
+        if (u != null) {
+            Uid.Pid p = u.mPids.get(pid);
+            if (p != null) {
+                return p.mWakeSumMs + (p.mWakeNesting > 0 ? (realtime - p.mWakeStartMs) : 0);
+            }
+        }
+        return 0;
+    }
+
+    public void reportExcessiveCpuLocked(int uid, String proc, long overTime, long usedTime) {
+        uid = mapUid(uid);
+        Uid u = mUidStats.get(uid);
+        if (u != null) {
+            u.reportExcessiveCpuLocked(proc, overTime, usedTime);
+        }
+    }
+
+    int mSensorNesting;
+
+    public void noteStartSensorLocked(int uid, int sensor) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mSensorNesting == 0) {
+            mHistoryCur.states |= HistoryItem.STATE_SENSOR_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Start sensor to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        mSensorNesting++;
+        getUidStatsLocked(uid).noteStartSensor(sensor, elapsedRealtime);
+    }
+
+    public void noteStopSensorLocked(int uid, int sensor) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        mSensorNesting--;
+        if (mSensorNesting == 0) {
+            mHistoryCur.states &= ~HistoryItem.STATE_SENSOR_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Stop sensor to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        getUidStatsLocked(uid).noteStopSensor(sensor, elapsedRealtime);
+    }
+
+    int mGpsNesting;
+
+    public void noteStartGpsLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mGpsNesting == 0) {
+            mHistoryCur.states |= HistoryItem.STATE_GPS_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Start GPS to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        mGpsNesting++;
+        getUidStatsLocked(uid).noteStartGps(elapsedRealtime);
+    }
+
+    public void noteStopGpsLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        mGpsNesting--;
+        if (mGpsNesting == 0) {
+            mHistoryCur.states &= ~HistoryItem.STATE_GPS_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Stop GPS to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        getUidStatsLocked(uid).noteStopGps(elapsedRealtime);
+    }
+
+    public void noteScreenStateLocked(int state) {
+        state = mPretendScreenOff ? Display.STATE_OFF : state;
+
+        // Battery stats relies on there being 4 states. To accommodate this, new states beyond the
+        // original 4 are mapped to one of the originals.
+        if (state > MAX_TRACKED_SCREEN_STATE) {
+            switch (state) {
+                case Display.STATE_VR:
+                    state = Display.STATE_ON;
+                    break;
+                default:
+                    Slog.wtf(TAG, "Unknown screen state (not mapped): " + state);
+                    break;
+            }
+        }
+
+        if (mScreenState != state) {
+            recordDailyStatsIfNeededLocked(true);
+            final int oldState = mScreenState;
+            mScreenState = state;
+            if (DEBUG) Slog.v(TAG, "Screen state: oldState=" + Display.stateToString(oldState)
+                    + ", newState=" + Display.stateToString(state));
+
+            if (state != Display.STATE_UNKNOWN) {
+                int stepState = state-1;
+                if ((stepState & STEP_LEVEL_MODE_SCREEN_STATE) == stepState) {
+                    mModStepMode |= (mCurStepMode & STEP_LEVEL_MODE_SCREEN_STATE) ^ stepState;
+                    mCurStepMode = (mCurStepMode & ~STEP_LEVEL_MODE_SCREEN_STATE) | stepState;
+                } else {
+                    Slog.wtf(TAG, "Unexpected screen state: " + state);
+                }
+            }
+
+            if (state == Display.STATE_ON) {
+                // Screen turning on.
+                final long elapsedRealtime = mClocks.elapsedRealtime();
+                final long uptime = mClocks.uptimeMillis();
+                mHistoryCur.states |= HistoryItem.STATE_SCREEN_ON_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Screen on to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                addHistoryRecordLocked(elapsedRealtime, uptime);
+                mScreenOnTimer.startRunningLocked(elapsedRealtime);
+                if (mScreenBrightnessBin >= 0) {
+                    mScreenBrightnessTimer[mScreenBrightnessBin].startRunningLocked(elapsedRealtime);
+                }
+
+                updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), false,
+                        mClocks.uptimeMillis() * 1000, elapsedRealtime * 1000);
+
+                // Fake a wake lock, so we consider the device waked as long
+                // as the screen is on.
+                noteStartWakeLocked(-1, -1, "screen", null, WAKE_TYPE_PARTIAL, false,
+                        elapsedRealtime, uptime);
+
+                // Update discharge amounts.
+                if (mOnBatteryInternal) {
+                    updateDischargeScreenLevelsLocked(false, true);
+                }
+            } else if (oldState == Display.STATE_ON) {
+                // Screen turning off or dozing.
+                final long elapsedRealtime = mClocks.elapsedRealtime();
+                final long uptime = mClocks.uptimeMillis();
+                mHistoryCur.states &= ~HistoryItem.STATE_SCREEN_ON_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Screen off to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                addHistoryRecordLocked(elapsedRealtime, uptime);
+                mScreenOnTimer.stopRunningLocked(elapsedRealtime);
+                if (mScreenBrightnessBin >= 0) {
+                    mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(elapsedRealtime);
+                }
+
+                noteStopWakeLocked(-1, -1, "screen", "screen", WAKE_TYPE_PARTIAL,
+                        elapsedRealtime, uptime);
+
+                updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), true,
+                        mClocks.uptimeMillis() * 1000, elapsedRealtime * 1000);
+
+                // Update discharge amounts.
+                if (mOnBatteryInternal) {
+                    updateDischargeScreenLevelsLocked(true, false);
+                }
+            }
+        }
+    }
+
+    public void noteScreenBrightnessLocked(int brightness) {
+        // Bin the brightness.
+        int bin = brightness / (256/NUM_SCREEN_BRIGHTNESS_BINS);
+        if (bin < 0) bin = 0;
+        else if (bin >= NUM_SCREEN_BRIGHTNESS_BINS) bin = NUM_SCREEN_BRIGHTNESS_BINS-1;
+        if (mScreenBrightnessBin != bin) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_BRIGHTNESS_MASK)
+                    | (bin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
+            if (DEBUG_HISTORY) Slog.v(TAG, "Screen brightness " + bin + " to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            if (mScreenState == Display.STATE_ON) {
+                if (mScreenBrightnessBin >= 0) {
+                    mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(elapsedRealtime);
+                }
+                mScreenBrightnessTimer[bin].startRunningLocked(elapsedRealtime);
+            }
+            mScreenBrightnessBin = bin;
+        }
+    }
+
+    public void noteUserActivityLocked(int uid, int event) {
+        if (mOnBatteryInternal) {
+            uid = mapUid(uid);
+            getUidStatsLocked(uid).noteUserActivityLocked(event);
+        }
+    }
+
+    public void noteWakeUpLocked(String reason, int reasonUid) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_SCREEN_WAKE_UP,
+                reason, reasonUid);
+    }
+
+    public void noteInteractiveLocked(boolean interactive) {
+        if (mInteractive != interactive) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            mInteractive = interactive;
+            if (DEBUG) Slog.v(TAG, "Interactive: " + interactive);
+            if (interactive) {
+                mInteractiveTimer.startRunningLocked(elapsedRealtime);
+            } else {
+                mInteractiveTimer.stopRunningLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    public void noteConnectivityChangedLocked(int type, String extra) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_CONNECTIVITY_CHANGED,
+                extra, type);
+        mNumConnectivityChange++;
+    }
+
+    private void noteMobileRadioApWakeupLocked(final long elapsedRealtimeMillis,
+            final long uptimeMillis, int uid) {
+        uid = mapUid(uid);
+        addHistoryEventLocked(elapsedRealtimeMillis, uptimeMillis, HistoryItem.EVENT_WAKEUP_AP, "",
+                uid);
+        getUidStatsLocked(uid).noteMobileRadioApWakeupLocked();
+    }
+
+    /**
+     * Updates the radio power state and returns true if an external stats collection should occur.
+     */
+    public boolean noteMobileRadioPowerStateLocked(int powerState, long timestampNs, int uid) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mMobileRadioPowerState != powerState) {
+            long realElapsedRealtimeMs;
+            final boolean active =
+                    powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM
+                            || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+            if (active) {
+                if (uid > 0) {
+                    noteMobileRadioApWakeupLocked(elapsedRealtime, uptime, uid);
+                }
+
+                mMobileRadioActiveStartTime = realElapsedRealtimeMs = timestampNs / (1000 * 1000);
+                mHistoryCur.states |= HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG;
+            } else {
+                realElapsedRealtimeMs = timestampNs / (1000*1000);
+                long lastUpdateTimeMs = mMobileRadioActiveStartTime;
+                if (realElapsedRealtimeMs < lastUpdateTimeMs) {
+                    Slog.wtf(TAG, "Data connection inactive timestamp " + realElapsedRealtimeMs
+                            + " is before start time " + lastUpdateTimeMs);
+                    realElapsedRealtimeMs = elapsedRealtime;
+                } else if (realElapsedRealtimeMs < elapsedRealtime) {
+                    mMobileRadioActiveAdjustedTime.addCountLocked(elapsedRealtime
+                            - realElapsedRealtimeMs);
+                }
+                mHistoryCur.states &= ~HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG;
+            }
+            if (DEBUG_HISTORY) Slog.v(TAG, "Mobile network active " + active + " to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mMobileRadioPowerState = powerState;
+            if (active) {
+                mMobileRadioActiveTimer.startRunningLocked(elapsedRealtime);
+                mMobileRadioActivePerAppTimer.startRunningLocked(elapsedRealtime);
+            } else {
+                mMobileRadioActiveTimer.stopRunningLocked(realElapsedRealtimeMs);
+                mMobileRadioActivePerAppTimer.stopRunningLocked(realElapsedRealtimeMs);
+                // Tell the caller to collect radio network/power stats.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void notePowerSaveModeLocked(boolean enabled) {
+        if (mPowerSaveModeEnabled != enabled) {
+            int stepState = enabled ? STEP_LEVEL_MODE_POWER_SAVE : 0;
+            mModStepMode |= (mCurStepMode&STEP_LEVEL_MODE_POWER_SAVE) ^ stepState;
+            mCurStepMode = (mCurStepMode&~STEP_LEVEL_MODE_POWER_SAVE) | stepState;
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mPowerSaveModeEnabled = enabled;
+            if (enabled) {
+                mHistoryCur.states2 |= HistoryItem.STATE2_POWER_SAVE_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Power save mode enabled to: "
+                        + Integer.toHexString(mHistoryCur.states2));
+                mPowerSaveModeEnabledTimer.startRunningLocked(elapsedRealtime);
+            } else {
+                mHistoryCur.states2 &= ~HistoryItem.STATE2_POWER_SAVE_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Power save mode disabled to: "
+                        + Integer.toHexString(mHistoryCur.states2));
+                mPowerSaveModeEnabledTimer.stopRunningLocked(elapsedRealtime);
+            }
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+    }
+
+    public void noteDeviceIdleModeLocked(int mode, String activeReason, int activeUid) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        boolean nowIdling = mode == DEVICE_IDLE_MODE_DEEP;
+        if (mDeviceIdling && !nowIdling && activeReason == null) {
+            // We don't go out of general idling mode until explicitly taken out of
+            // device idle through going active or significant motion.
+            nowIdling = true;
+        }
+        boolean nowLightIdling = mode == DEVICE_IDLE_MODE_LIGHT;
+        if (mDeviceLightIdling && !nowLightIdling && !nowIdling && activeReason == null) {
+            // We don't go out of general light idling mode until explicitly taken out of
+            // device idle through going active or significant motion.
+            nowLightIdling = true;
+        }
+        if (activeReason != null && (mDeviceIdling || mDeviceLightIdling)) {
+            addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ACTIVE,
+                    activeReason, activeUid);
+        }
+        if (mDeviceIdling != nowIdling) {
+            mDeviceIdling = nowIdling;
+            int stepState = nowIdling ? STEP_LEVEL_MODE_DEVICE_IDLE : 0;
+            mModStepMode |= (mCurStepMode&STEP_LEVEL_MODE_DEVICE_IDLE) ^ stepState;
+            mCurStepMode = (mCurStepMode&~STEP_LEVEL_MODE_DEVICE_IDLE) | stepState;
+            if (nowIdling) {
+                mDeviceIdlingTimer.startRunningLocked(elapsedRealtime);
+            } else {
+                mDeviceIdlingTimer.stopRunningLocked(elapsedRealtime);
+            }
+        }
+        if (mDeviceLightIdling != nowLightIdling) {
+            mDeviceLightIdling = nowLightIdling;
+            if (nowLightIdling) {
+                mDeviceLightIdlingTimer.startRunningLocked(elapsedRealtime);
+            } else {
+                mDeviceLightIdlingTimer.stopRunningLocked(elapsedRealtime);
+            }
+        }
+        if (mDeviceIdleMode != mode) {
+            mHistoryCur.states2 = (mHistoryCur.states2 & ~HistoryItem.STATE2_DEVICE_IDLE_MASK)
+                    | (mode << HistoryItem.STATE2_DEVICE_IDLE_SHIFT);
+            if (DEBUG_HISTORY) Slog.v(TAG, "Device idle mode changed to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            long lastDuration = elapsedRealtime - mLastIdleTimeStart;
+            mLastIdleTimeStart = elapsedRealtime;
+            if (mDeviceIdleMode == DEVICE_IDLE_MODE_LIGHT) {
+                if (lastDuration > mLongestLightIdleTime) {
+                    mLongestLightIdleTime = lastDuration;
+                }
+                mDeviceIdleModeLightTimer.stopRunningLocked(elapsedRealtime);
+            } else if (mDeviceIdleMode == DEVICE_IDLE_MODE_DEEP) {
+                if (lastDuration > mLongestFullIdleTime) {
+                    mLongestFullIdleTime = lastDuration;
+                }
+                mDeviceIdleModeFullTimer.stopRunningLocked(elapsedRealtime);
+            }
+            if (mode == DEVICE_IDLE_MODE_LIGHT) {
+                mDeviceIdleModeLightTimer.startRunningLocked(elapsedRealtime);
+            } else if (mode == DEVICE_IDLE_MODE_DEEP) {
+                mDeviceIdleModeFullTimer.startRunningLocked(elapsedRealtime);
+            }
+            mDeviceIdleMode = mode;
+        }
+    }
+
+    public void notePackageInstalledLocked(String pkgName, int versionCode) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_PACKAGE_INSTALLED,
+                pkgName, versionCode);
+        PackageChange pc = new PackageChange();
+        pc.mPackageName = pkgName;
+        pc.mUpdate = true;
+        pc.mVersionCode = versionCode;
+        addPackageChange(pc);
+    }
+
+    public void notePackageUninstalledLocked(String pkgName) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_PACKAGE_UNINSTALLED,
+                pkgName, 0);
+        PackageChange pc = new PackageChange();
+        pc.mPackageName = pkgName;
+        pc.mUpdate = true;
+        addPackageChange(pc);
+    }
+
+    private void addPackageChange(PackageChange pc) {
+        if (mDailyPackageChanges == null) {
+            mDailyPackageChanges = new ArrayList<>();
+        }
+        mDailyPackageChanges.add(pc);
+    }
+
+    public void notePhoneOnLocked() {
+        if (!mPhoneOn) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states2 |= HistoryItem.STATE2_PHONE_IN_CALL_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Phone on to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mPhoneOn = true;
+            mPhoneOnTimer.startRunningLocked(elapsedRealtime);
+        }
+    }
+
+    public void notePhoneOffLocked() {
+        if (mPhoneOn) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_PHONE_IN_CALL_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Phone off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mPhoneOn = false;
+            mPhoneOnTimer.stopRunningLocked(elapsedRealtime);
+        }
+    }
+
+    void stopAllPhoneSignalStrengthTimersLocked(int except) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        for (int i = 0; i < SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            if (i == except) {
+                continue;
+            }
+            while (mPhoneSignalStrengthsTimer[i].isRunningLocked()) {
+                mPhoneSignalStrengthsTimer[i].stopRunningLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    private int fixPhoneServiceState(int state, int signalBin) {
+        if (mPhoneSimStateRaw == TelephonyManager.SIM_STATE_ABSENT) {
+            // In this case we will always be STATE_OUT_OF_SERVICE, so need
+            // to infer that we are scanning from other data.
+            if (state == ServiceState.STATE_OUT_OF_SERVICE
+                    && signalBin > SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) {
+                state = ServiceState.STATE_IN_SERVICE;
+            }
+        }
+
+        return state;
+    }
+
+    private void updateAllPhoneStateLocked(int state, int simState, int strengthBin) {
+        boolean scanning = false;
+        boolean newHistory = false;
+
+        mPhoneServiceStateRaw = state;
+        mPhoneSimStateRaw = simState;
+        mPhoneSignalStrengthBinRaw = strengthBin;
+
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+
+        if (simState == TelephonyManager.SIM_STATE_ABSENT) {
+            // In this case we will always be STATE_OUT_OF_SERVICE, so need
+            // to infer that we are scanning from other data.
+            if (state == ServiceState.STATE_OUT_OF_SERVICE
+                    && strengthBin > SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) {
+                state = ServiceState.STATE_IN_SERVICE;
+            }
+        }
+
+        // If the phone is powered off, stop all timers.
+        if (state == ServiceState.STATE_POWER_OFF) {
+            strengthBin = -1;
+
+        // If we are in service, make sure the correct signal string timer is running.
+        } else if (state == ServiceState.STATE_IN_SERVICE) {
+            // Bin will be changed below.
+
+        // If we're out of service, we are in the lowest signal strength
+        // bin and have the scanning bit set.
+        } else if (state == ServiceState.STATE_OUT_OF_SERVICE) {
+            scanning = true;
+            strengthBin = SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
+            if (!mPhoneSignalScanningTimer.isRunningLocked()) {
+                mHistoryCur.states |= HistoryItem.STATE_PHONE_SCANNING_FLAG;
+                newHistory = true;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Phone started scanning to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                mPhoneSignalScanningTimer.startRunningLocked(elapsedRealtime);
+            }
+        }
+
+        if (!scanning) {
+            // If we are no longer scanning, then stop the scanning timer.
+            if (mPhoneSignalScanningTimer.isRunningLocked()) {
+                mHistoryCur.states &= ~HistoryItem.STATE_PHONE_SCANNING_FLAG;
+                if (DEBUG_HISTORY) Slog.v(TAG, "Phone stopped scanning to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                newHistory = true;
+                mPhoneSignalScanningTimer.stopRunningLocked(elapsedRealtime);
+            }
+        }
+
+        if (mPhoneServiceState != state) {
+            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_PHONE_STATE_MASK)
+                    | (state << HistoryItem.STATE_PHONE_STATE_SHIFT);
+            if (DEBUG_HISTORY) Slog.v(TAG, "Phone state " + state + " to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            newHistory = true;
+            mPhoneServiceState = state;
+        }
+
+        if (mPhoneSignalStrengthBin != strengthBin) {
+            if (mPhoneSignalStrengthBin >= 0) {
+                mPhoneSignalStrengthsTimer[mPhoneSignalStrengthBin].stopRunningLocked(
+                        elapsedRealtime);
+            }
+            if (strengthBin >= 0) {
+                if (!mPhoneSignalStrengthsTimer[strengthBin].isRunningLocked()) {
+                    mPhoneSignalStrengthsTimer[strengthBin].startRunningLocked(elapsedRealtime);
+                }
+                mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK)
+                        | (strengthBin << HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT);
+                if (DEBUG_HISTORY) Slog.v(TAG, "Signal strength " + strengthBin + " to: "
+                        + Integer.toHexString(mHistoryCur.states));
+                newHistory = true;
+            } else {
+                stopAllPhoneSignalStrengthTimersLocked(-1);
+            }
+            mPhoneSignalStrengthBin = strengthBin;
+        }
+
+        if (newHistory) {
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+    }
+
+    /**
+     * Telephony stack updates the phone state.
+     * @param state phone state from ServiceState.getState()
+     */
+    public void notePhoneStateLocked(int state, int simState) {
+        updateAllPhoneStateLocked(state, simState, mPhoneSignalStrengthBinRaw);
+    }
+
+    public void notePhoneSignalStrengthLocked(SignalStrength signalStrength) {
+        // Bin the strength.
+        int bin = signalStrength.getLevel();
+        updateAllPhoneStateLocked(mPhoneServiceStateRaw, mPhoneSimStateRaw, bin);
+    }
+
+    public void notePhoneDataConnectionStateLocked(int dataType, boolean hasData) {
+        int bin = DATA_CONNECTION_NONE;
+        if (hasData) {
+            switch (dataType) {
+                case TelephonyManager.NETWORK_TYPE_EDGE:
+                    bin = DATA_CONNECTION_EDGE;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_GPRS:
+                    bin = DATA_CONNECTION_GPRS;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_UMTS:
+                    bin = DATA_CONNECTION_UMTS;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_CDMA:
+                    bin = DATA_CONNECTION_CDMA;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_EVDO_0:
+                    bin = DATA_CONNECTION_EVDO_0;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_EVDO_A:
+                    bin = DATA_CONNECTION_EVDO_A;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_1xRTT:
+                    bin = DATA_CONNECTION_1xRTT;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_HSDPA:
+                    bin = DATA_CONNECTION_HSDPA;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_HSUPA:
+                    bin = DATA_CONNECTION_HSUPA;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_HSPA:
+                    bin = DATA_CONNECTION_HSPA;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_IDEN:
+                    bin = DATA_CONNECTION_IDEN;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_EVDO_B:
+                    bin = DATA_CONNECTION_EVDO_B;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_LTE:
+                    bin = DATA_CONNECTION_LTE;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_EHRPD:
+                    bin = DATA_CONNECTION_EHRPD;
+                    break;
+                case TelephonyManager.NETWORK_TYPE_HSPAP:
+                    bin = DATA_CONNECTION_HSPAP;
+                    break;
+                default:
+                    bin = DATA_CONNECTION_OTHER;
+                    break;
+            }
+        }
+        if (DEBUG) Log.i(TAG, "Phone Data Connection -> " + dataType + " = " + hasData);
+        if (mPhoneDataConnectionType != bin) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_DATA_CONNECTION_MASK)
+                    | (bin << HistoryItem.STATE_DATA_CONNECTION_SHIFT);
+            if (DEBUG_HISTORY) Slog.v(TAG, "Data connection " + bin + " to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            if (mPhoneDataConnectionType >= 0) {
+                mPhoneDataConnectionsTimer[mPhoneDataConnectionType].stopRunningLocked(
+                        elapsedRealtime);
+            }
+            mPhoneDataConnectionType = bin;
+            mPhoneDataConnectionsTimer[bin].startRunningLocked(elapsedRealtime);
+        }
+    }
+
+    public void noteWifiOnLocked() {
+        if (!mWifiOn) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI on to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mWifiOn = true;
+            mWifiOnTimer.startRunningLocked(elapsedRealtime);
+            scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI);
+        }
+    }
+
+    public void noteWifiOffLocked() {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mWifiOn) {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_WIFI_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mWifiOn = false;
+            mWifiOnTimer.stopRunningLocked(elapsedRealtime);
+            scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI);
+        }
+    }
+
+    public void noteAudioOnLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mAudioOnNesting == 0) {
+            mHistoryCur.states |= HistoryItem.STATE_AUDIO_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Audio on to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mAudioOnTimer.startRunningLocked(elapsedRealtime);
+        }
+        mAudioOnNesting++;
+        getUidStatsLocked(uid).noteAudioTurnedOnLocked(elapsedRealtime);
+    }
+
+    public void noteAudioOffLocked(int uid) {
+        if (mAudioOnNesting == 0) {
+            return;
+        }
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (--mAudioOnNesting == 0) {
+            mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mAudioOnTimer.stopRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteAudioTurnedOffLocked(elapsedRealtime);
+    }
+
+    public void noteVideoOnLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mVideoOnNesting == 0) {
+            mHistoryCur.states2 |= HistoryItem.STATE2_VIDEO_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Video on to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mVideoOnTimer.startRunningLocked(elapsedRealtime);
+        }
+        mVideoOnNesting++;
+        getUidStatsLocked(uid).noteVideoTurnedOnLocked(elapsedRealtime);
+    }
+
+    public void noteVideoOffLocked(int uid) {
+        if (mVideoOnNesting == 0) {
+            return;
+        }
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (--mVideoOnNesting == 0) {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_VIDEO_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Video off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mVideoOnTimer.stopRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteVideoTurnedOffLocked(elapsedRealtime);
+    }
+
+    public void noteResetAudioLocked() {
+        if (mAudioOnNesting > 0) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mAudioOnNesting = 0;
+            mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mAudioOnTimer.stopAllRunningLocked(elapsedRealtime);
+            for (int i=0; i<mUidStats.size(); i++) {
+                BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.noteResetAudioLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    public void noteResetVideoLocked() {
+        if (mVideoOnNesting > 0) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mAudioOnNesting = 0;
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_VIDEO_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Video off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mVideoOnTimer.stopAllRunningLocked(elapsedRealtime);
+            for (int i=0; i<mUidStats.size(); i++) {
+                BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.noteResetVideoLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    public void noteActivityResumedLocked(int uid) {
+        uid = mapUid(uid);
+        getUidStatsLocked(uid).noteActivityResumedLocked(mClocks.elapsedRealtime());
+    }
+
+    public void noteActivityPausedLocked(int uid) {
+        uid = mapUid(uid);
+        getUidStatsLocked(uid).noteActivityPausedLocked(mClocks.elapsedRealtime());
+    }
+
+    public void noteVibratorOnLocked(int uid, long durationMillis) {
+        uid = mapUid(uid);
+        getUidStatsLocked(uid).noteVibratorOnLocked(durationMillis);
+    }
+
+    public void noteVibratorOffLocked(int uid) {
+        uid = mapUid(uid);
+        getUidStatsLocked(uid).noteVibratorOffLocked();
+    }
+
+    public void noteFlashlightOnLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mFlashlightOnNesting++ == 0) {
+            mHistoryCur.states2 |= HistoryItem.STATE2_FLASHLIGHT_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Flashlight on to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mFlashlightOnTimer.startRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteFlashlightTurnedOnLocked(elapsedRealtime);
+    }
+
+    public void noteFlashlightOffLocked(int uid) {
+        if (mFlashlightOnNesting == 0) {
+            return;
+        }
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (--mFlashlightOnNesting == 0) {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_FLASHLIGHT_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Flashlight off to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mFlashlightOnTimer.stopRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteFlashlightTurnedOffLocked(elapsedRealtime);
+    }
+
+    public void noteCameraOnLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mCameraOnNesting++ == 0) {
+            mHistoryCur.states2 |= HistoryItem.STATE2_CAMERA_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Camera on to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mCameraOnTimer.startRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteCameraTurnedOnLocked(elapsedRealtime);
+    }
+
+    public void noteCameraOffLocked(int uid) {
+        if (mCameraOnNesting == 0) {
+            return;
+        }
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (--mCameraOnNesting == 0) {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_CAMERA_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Camera off to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mCameraOnTimer.stopRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteCameraTurnedOffLocked(elapsedRealtime);
+    }
+
+    public void noteResetCameraLocked() {
+        if (mCameraOnNesting > 0) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mCameraOnNesting = 0;
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_CAMERA_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Camera off to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mCameraOnTimer.stopAllRunningLocked(elapsedRealtime);
+            for (int i=0; i<mUidStats.size(); i++) {
+                BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.noteResetCameraLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    public void noteResetFlashlightLocked() {
+        if (mFlashlightOnNesting > 0) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mFlashlightOnNesting = 0;
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_FLASHLIGHT_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Flashlight off to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mFlashlightOnTimer.stopAllRunningLocked(elapsedRealtime);
+            for (int i=0; i<mUidStats.size(); i++) {
+                BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.noteResetFlashlightLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    private void noteBluetoothScanStartedLocked(int uid, boolean isUnoptimized) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mBluetoothScanNesting == 0) {
+            mHistoryCur.states2 |= HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan started for: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mBluetoothScanTimer.startRunningLocked(elapsedRealtime);
+        }
+        mBluetoothScanNesting++;
+        getUidStatsLocked(uid).noteBluetoothScanStartedLocked(elapsedRealtime, isUnoptimized);
+    }
+
+    public void noteBluetoothScanStartedFromSourceLocked(WorkSource ws, boolean isUnoptimized) {
+        final int N = ws.size();
+        for (int i = 0; i < N; i++) {
+            noteBluetoothScanStartedLocked(ws.get(i), isUnoptimized);
+        }
+    }
+
+    private void noteBluetoothScanStoppedLocked(int uid, boolean isUnoptimized) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        mBluetoothScanNesting--;
+        if (mBluetoothScanNesting == 0) {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan stopped for: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mBluetoothScanTimer.stopRunningLocked(elapsedRealtime);
+        }
+        getUidStatsLocked(uid).noteBluetoothScanStoppedLocked(elapsedRealtime, isUnoptimized);
+    }
+
+    public void noteBluetoothScanStoppedFromSourceLocked(WorkSource ws, boolean isUnoptimized) {
+        final int N = ws.size();
+        for (int i = 0; i < N; i++) {
+            noteBluetoothScanStoppedLocked(ws.get(i), isUnoptimized);
+        }
+    }
+
+    public void noteResetBluetoothScanLocked() {
+        if (mBluetoothScanNesting > 0) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mBluetoothScanNesting = 0;
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "BLE can stopped for: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtime);
+            for (int i=0; i<mUidStats.size(); i++) {
+                BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                uid.noteResetBluetoothScanLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    public void noteBluetoothScanResultsFromSourceLocked(WorkSource ws, int numNewResults) {
+        final int N = ws.size();
+        for (int i = 0; i < N; i++) {
+            int uid = mapUid(ws.get(i));
+            getUidStatsLocked(uid).noteBluetoothScanResultsLocked(numNewResults);
+        }
+    }
+
+    private void noteWifiRadioApWakeupLocked(final long elapsedRealtimeMillis,
+            final long uptimeMillis, int uid) {
+        uid = mapUid(uid);
+        addHistoryEventLocked(elapsedRealtimeMillis, uptimeMillis, HistoryItem.EVENT_WAKEUP_AP, "",
+                uid);
+        getUidStatsLocked(uid).noteWifiRadioApWakeupLocked();
+    }
+
+    public void noteWifiRadioPowerState(int powerState, long timestampNs, int uid) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mWifiRadioPowerState != powerState) {
+            final boolean active =
+                    powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM
+                            || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+            if (active) {
+                if (uid > 0) {
+                    noteWifiRadioApWakeupLocked(elapsedRealtime, uptime, uid);
+                }
+                mHistoryCur.states |= HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG;
+            } else {
+                mHistoryCur.states &= ~HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG;
+            }
+            if (DEBUG_HISTORY) Slog.v(TAG, "Wifi network active " + active + " to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mWifiRadioPowerState = powerState;
+        }
+    }
+
+    public void noteWifiRunningLocked(WorkSource ws) {
+        if (!mGlobalWifiRunning) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_RUNNING_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI running to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mGlobalWifiRunning = true;
+            mGlobalWifiRunningTimer.startRunningLocked(elapsedRealtime);
+            int N = ws.size();
+            for (int i=0; i<N; i++) {
+                int uid = mapUid(ws.get(i));
+                getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
+            }
+            scheduleSyncExternalStatsLocked("wifi-running", ExternalStatsSync.UPDATE_WIFI);
+        } else {
+            Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running");
+        }
+    }
+
+    public void noteWifiRunningChangedLocked(WorkSource oldWs, WorkSource newWs) {
+        if (mGlobalWifiRunning) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            int N = oldWs.size();
+            for (int i=0; i<N; i++) {
+                int uid = mapUid(oldWs.get(i));
+                getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
+            }
+            N = newWs.size();
+            for (int i=0; i<N; i++) {
+                int uid = mapUid(newWs.get(i));
+                getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
+            }
+        } else {
+            Log.w(TAG, "noteWifiRunningChangedLocked -- called while WIFI not running");
+        }
+    }
+
+    public void noteWifiStoppedLocked(WorkSource ws) {
+        if (mGlobalWifiRunning) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_WIFI_RUNNING_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI stopped to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+            mGlobalWifiRunning = false;
+            mGlobalWifiRunningTimer.stopRunningLocked(elapsedRealtime);
+            int N = ws.size();
+            for (int i=0; i<N; i++) {
+                int uid = mapUid(ws.get(i));
+                getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
+            }
+            scheduleSyncExternalStatsLocked("wifi-stopped", ExternalStatsSync.UPDATE_WIFI);
+        } else {
+            Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running");
+        }
+    }
+
+    public void noteWifiStateLocked(int wifiState, String accessPoint) {
+        if (DEBUG) Log.i(TAG, "WiFi state -> " + wifiState);
+        if (mWifiState != wifiState) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            if (mWifiState >= 0) {
+                mWifiStateTimer[mWifiState].stopRunningLocked(elapsedRealtime);
+            }
+            mWifiState = wifiState;
+            mWifiStateTimer[wifiState].startRunningLocked(elapsedRealtime);
+            scheduleSyncExternalStatsLocked("wifi-state", ExternalStatsSync.UPDATE_WIFI);
+        }
+    }
+
+    public void noteWifiSupplicantStateChangedLocked(int supplState, boolean failedAuth) {
+        if (DEBUG) Log.i(TAG, "WiFi suppl state -> " + supplState);
+        if (mWifiSupplState != supplState) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            if (mWifiSupplState >= 0) {
+                mWifiSupplStateTimer[mWifiSupplState].stopRunningLocked(elapsedRealtime);
+            }
+            mWifiSupplState = supplState;
+            mWifiSupplStateTimer[supplState].startRunningLocked(elapsedRealtime);
+            mHistoryCur.states2 =
+                    (mHistoryCur.states2&~HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK)
+                    | (supplState << HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT);
+            if (DEBUG_HISTORY) Slog.v(TAG, "Wifi suppl state " + supplState + " to: "
+                    + Integer.toHexString(mHistoryCur.states2));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+    }
+
+    void stopAllWifiSignalStrengthTimersLocked(int except) {
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        for (int i = 0; i < NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            if (i == except) {
+                continue;
+            }
+            while (mWifiSignalStrengthsTimer[i].isRunningLocked()) {
+                mWifiSignalStrengthsTimer[i].stopRunningLocked(elapsedRealtime);
+            }
+        }
+    }
+
+    public void noteWifiRssiChangedLocked(int newRssi) {
+        int strengthBin = WifiManager.calculateSignalLevel(newRssi, NUM_WIFI_SIGNAL_STRENGTH_BINS);
+        if (DEBUG) Log.i(TAG, "WiFi rssi -> " + newRssi + " bin=" + strengthBin);
+        if (mWifiSignalStrengthBin != strengthBin) {
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            if (mWifiSignalStrengthBin >= 0) {
+                mWifiSignalStrengthsTimer[mWifiSignalStrengthBin].stopRunningLocked(
+                        elapsedRealtime);
+            }
+            if (strengthBin >= 0) {
+                if (!mWifiSignalStrengthsTimer[strengthBin].isRunningLocked()) {
+                    mWifiSignalStrengthsTimer[strengthBin].startRunningLocked(elapsedRealtime);
+                }
+                mHistoryCur.states2 =
+                        (mHistoryCur.states2&~HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK)
+                        | (strengthBin << HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT);
+                if (DEBUG_HISTORY) Slog.v(TAG, "Wifi signal strength " + strengthBin + " to: "
+                        + Integer.toHexString(mHistoryCur.states2));
+                addHistoryRecordLocked(elapsedRealtime, uptime);
+            } else {
+                stopAllWifiSignalStrengthTimersLocked(-1);
+            }
+            mWifiSignalStrengthBin = strengthBin;
+        }
+    }
+
+    int mWifiFullLockNesting = 0;
+
+    public void noteFullWifiLockAcquiredLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mWifiFullLockNesting == 0) {
+            mHistoryCur.states |= HistoryItem.STATE_WIFI_FULL_LOCK_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock on to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        mWifiFullLockNesting++;
+        getUidStatsLocked(uid).noteFullWifiLockAcquiredLocked(elapsedRealtime);
+    }
+
+    public void noteFullWifiLockReleasedLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        mWifiFullLockNesting--;
+        if (mWifiFullLockNesting == 0) {
+            mHistoryCur.states &= ~HistoryItem.STATE_WIFI_FULL_LOCK_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        getUidStatsLocked(uid).noteFullWifiLockReleasedLocked(elapsedRealtime);
+    }
+
+    int mWifiScanNesting = 0;
+
+    public void noteWifiScanStartedLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mWifiScanNesting == 0) {
+            mHistoryCur.states |= HistoryItem.STATE_WIFI_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI scan started for: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        mWifiScanNesting++;
+        getUidStatsLocked(uid).noteWifiScanStartedLocked(elapsedRealtime);
+    }
+
+    public void noteWifiScanStoppedLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        mWifiScanNesting--;
+        if (mWifiScanNesting == 0) {
+            mHistoryCur.states &= ~HistoryItem.STATE_WIFI_SCAN_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI scan stopped for: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        getUidStatsLocked(uid).noteWifiScanStoppedLocked(elapsedRealtime);
+    }
+
+    public void noteWifiBatchedScanStartedLocked(int uid, int csph) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        getUidStatsLocked(uid).noteWifiBatchedScanStartedLocked(csph, elapsedRealtime);
+    }
+
+    public void noteWifiBatchedScanStoppedLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        getUidStatsLocked(uid).noteWifiBatchedScanStoppedLocked(elapsedRealtime);
+    }
+
+    int mWifiMulticastNesting = 0;
+
+    public void noteWifiMulticastEnabledLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        if (mWifiMulticastNesting == 0) {
+            mHistoryCur.states |= HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast on to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        mWifiMulticastNesting++;
+        getUidStatsLocked(uid).noteWifiMulticastEnabledLocked(elapsedRealtime);
+    }
+
+    public void noteWifiMulticastDisabledLocked(int uid) {
+        uid = mapUid(uid);
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        final long uptime = mClocks.uptimeMillis();
+        mWifiMulticastNesting--;
+        if (mWifiMulticastNesting == 0) {
+            mHistoryCur.states &= ~HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast off to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(elapsedRealtime, uptime);
+        }
+        getUidStatsLocked(uid).noteWifiMulticastDisabledLocked(elapsedRealtime);
+    }
+
+    public void noteFullWifiLockAcquiredFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteFullWifiLockAcquiredLocked(ws.get(i));
+        }
+    }
+
+    public void noteFullWifiLockReleasedFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteFullWifiLockReleasedLocked(ws.get(i));
+        }
+    }
+
+    public void noteWifiScanStartedFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteWifiScanStartedLocked(ws.get(i));
+        }
+    }
+
+    public void noteWifiScanStoppedFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteWifiScanStoppedLocked(ws.get(i));
+        }
+    }
+
+    public void noteWifiBatchedScanStartedFromSourceLocked(WorkSource ws, int csph) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteWifiBatchedScanStartedLocked(ws.get(i), csph);
+        }
+    }
+
+    public void noteWifiBatchedScanStoppedFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteWifiBatchedScanStoppedLocked(ws.get(i));
+        }
+    }
+
+    public void noteWifiMulticastEnabledFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteWifiMulticastEnabledLocked(ws.get(i));
+        }
+    }
+
+    public void noteWifiMulticastDisabledFromSourceLocked(WorkSource ws) {
+        int N = ws.size();
+        for (int i=0; i<N; i++) {
+            noteWifiMulticastDisabledLocked(ws.get(i));
+        }
+    }
+
+    private static String[] includeInStringArray(String[] array, String str) {
+        if (ArrayUtils.indexOf(array, str) >= 0) {
+            return array;
+        }
+        String[] newArray = new String[array.length+1];
+        System.arraycopy(array, 0, newArray, 0, array.length);
+        newArray[array.length] = str;
+        return newArray;
+    }
+
+    private static String[] excludeFromStringArray(String[] array, String str) {
+        int index = ArrayUtils.indexOf(array, str);
+        if (index >= 0) {
+            String[] newArray = new String[array.length-1];
+            if (index > 0) {
+                System.arraycopy(array, 0, newArray, 0, index);
+            }
+            if (index < array.length-1) {
+                System.arraycopy(array, index+1, newArray, index, array.length-index-1);
+            }
+            return newArray;
+        }
+        return array;
+    }
+
+    public void noteNetworkInterfaceTypeLocked(String iface, int networkType) {
+        if (TextUtils.isEmpty(iface)) return;
+
+        synchronized (mModemNetworkLock) {
+            if (ConnectivityManager.isNetworkTypeMobile(networkType)) {
+                mModemIfaces = includeInStringArray(mModemIfaces, iface);
+                if (DEBUG) Slog.d(TAG, "Note mobile iface " + iface + ": " + mModemIfaces);
+            } else {
+                mModemIfaces = excludeFromStringArray(mModemIfaces, iface);
+                if (DEBUG) Slog.d(TAG, "Note non-mobile iface " + iface + ": " + mModemIfaces);
+            }
+        }
+
+        synchronized (mWifiNetworkLock) {
+            if (ConnectivityManager.isNetworkTypeWifi(networkType)) {
+                mWifiIfaces = includeInStringArray(mWifiIfaces, iface);
+                if (DEBUG) Slog.d(TAG, "Note wifi iface " + iface + ": " + mWifiIfaces);
+            } else {
+                mWifiIfaces = excludeFromStringArray(mWifiIfaces, iface);
+                if (DEBUG) Slog.d(TAG, "Note non-wifi iface " + iface + ": " + mWifiIfaces);
+            }
+        }
+    }
+
+    @Override public long getScreenOnTime(long elapsedRealtimeUs, int which) {
+        return mScreenOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public int getScreenOnCount(int which) {
+        return mScreenOnTimer.getCountLocked(which);
+    }
+
+    @Override public long getScreenBrightnessTime(int brightnessBin,
+            long elapsedRealtimeUs, int which) {
+        return mScreenBrightnessTimer[brightnessBin].getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public long getInteractiveTime(long elapsedRealtimeUs, int which) {
+        return mInteractiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public long getPowerSaveModeEnabledTime(long elapsedRealtimeUs, int which) {
+        return mPowerSaveModeEnabledTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public int getPowerSaveModeEnabledCount(int which) {
+        return mPowerSaveModeEnabledTimer.getCountLocked(which);
+    }
+
+    @Override public long getDeviceIdleModeTime(int mode, long elapsedRealtimeUs,
+            int which) {
+        switch (mode) {
+            case DEVICE_IDLE_MODE_LIGHT:
+                return mDeviceIdleModeLightTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+            case DEVICE_IDLE_MODE_DEEP:
+                return mDeviceIdleModeFullTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+        return 0;
+    }
+
+    @Override public int getDeviceIdleModeCount(int mode, int which) {
+        switch (mode) {
+            case DEVICE_IDLE_MODE_LIGHT:
+                return mDeviceIdleModeLightTimer.getCountLocked(which);
+            case DEVICE_IDLE_MODE_DEEP:
+                return mDeviceIdleModeFullTimer.getCountLocked(which);
+        }
+        return 0;
+    }
+
+    @Override public long getLongestDeviceIdleModeTime(int mode) {
+        switch (mode) {
+            case DEVICE_IDLE_MODE_LIGHT:
+                return mLongestLightIdleTime;
+            case DEVICE_IDLE_MODE_DEEP:
+                return mLongestFullIdleTime;
+        }
+        return 0;
+    }
+
+    @Override public long getDeviceIdlingTime(int mode, long elapsedRealtimeUs, int which) {
+        switch (mode) {
+            case DEVICE_IDLE_MODE_LIGHT:
+                return mDeviceLightIdlingTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+            case DEVICE_IDLE_MODE_DEEP:
+                return mDeviceIdlingTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+        return 0;
+    }
+
+    @Override public int getDeviceIdlingCount(int mode, int which) {
+        switch (mode) {
+            case DEVICE_IDLE_MODE_LIGHT:
+                return mDeviceLightIdlingTimer.getCountLocked(which);
+            case DEVICE_IDLE_MODE_DEEP:
+                return mDeviceIdlingTimer.getCountLocked(which);
+        }
+        return 0;
+    }
+
+    @Override public int getNumConnectivityChange(int which) {
+        int val = mNumConnectivityChange;
+        if (which == STATS_CURRENT) {
+            val -= mLoadedNumConnectivityChange;
+        } else if (which == STATS_SINCE_UNPLUGGED) {
+            val -= mUnpluggedNumConnectivityChange;
+        }
+        return val;
+    }
+
+    @Override public long getPhoneOnTime(long elapsedRealtimeUs, int which) {
+        return mPhoneOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public int getPhoneOnCount(int which) {
+        return mPhoneOnTimer.getCountLocked(which);
+    }
+
+    @Override public long getPhoneSignalStrengthTime(int strengthBin,
+            long elapsedRealtimeUs, int which) {
+        return mPhoneSignalStrengthsTimer[strengthBin].getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public long getPhoneSignalScanningTime(
+            long elapsedRealtimeUs, int which) {
+        return mPhoneSignalScanningTimer.getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public int getPhoneSignalStrengthCount(int strengthBin, int which) {
+        return mPhoneSignalStrengthsTimer[strengthBin].getCountLocked(which);
+    }
+
+    @Override public long getPhoneDataConnectionTime(int dataType,
+            long elapsedRealtimeUs, int which) {
+        return mPhoneDataConnectionsTimer[dataType].getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public int getPhoneDataConnectionCount(int dataType, int which) {
+        return mPhoneDataConnectionsTimer[dataType].getCountLocked(which);
+    }
+
+    @Override public long getMobileRadioActiveTime(long elapsedRealtimeUs, int which) {
+        return mMobileRadioActiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public int getMobileRadioActiveCount(int which) {
+        return mMobileRadioActiveTimer.getCountLocked(which);
+    }
+
+    @Override public long getMobileRadioActiveAdjustedTime(int which) {
+        return mMobileRadioActiveAdjustedTime.getCountLocked(which);
+    }
+
+    @Override public long getMobileRadioActiveUnknownTime(int which) {
+        return mMobileRadioActiveUnknownTime.getCountLocked(which);
+    }
+
+    @Override public int getMobileRadioActiveUnknownCount(int which) {
+        return (int)mMobileRadioActiveUnknownCount.getCountLocked(which);
+    }
+
+    @Override public long getWifiOnTime(long elapsedRealtimeUs, int which) {
+        return mWifiOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public long getGlobalWifiRunningTime(long elapsedRealtimeUs, int which) {
+        return mGlobalWifiRunningTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override public long getWifiStateTime(int wifiState,
+            long elapsedRealtimeUs, int which) {
+        return mWifiStateTimer[wifiState].getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public int getWifiStateCount(int wifiState, int which) {
+        return mWifiStateTimer[wifiState].getCountLocked(which);
+    }
+
+    @Override public long getWifiSupplStateTime(int state,
+            long elapsedRealtimeUs, int which) {
+        return mWifiSupplStateTimer[state].getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public int getWifiSupplStateCount(int state, int which) {
+        return mWifiSupplStateTimer[state].getCountLocked(which);
+    }
+
+    @Override public long getWifiSignalStrengthTime(int strengthBin,
+            long elapsedRealtimeUs, int which) {
+        return mWifiSignalStrengthsTimer[strengthBin].getTotalTimeLocked(
+                elapsedRealtimeUs, which);
+    }
+
+    @Override public int getWifiSignalStrengthCount(int strengthBin, int which) {
+        return mWifiSignalStrengthsTimer[strengthBin].getCountLocked(which);
+    }
+
+    @Override
+    public ControllerActivityCounter getBluetoothControllerActivity() {
+        return mBluetoothActivity;
+    }
+
+    @Override
+    public ControllerActivityCounter getWifiControllerActivity() {
+        return mWifiActivity;
+    }
+
+    @Override
+    public ControllerActivityCounter getModemControllerActivity() {
+        return mModemActivity;
+    }
+
+    @Override
+    public boolean hasBluetoothActivityReporting() {
+        return mHasBluetoothReporting;
+    }
+
+    @Override
+    public boolean hasWifiActivityReporting() {
+        return mHasWifiReporting;
+    }
+
+    @Override
+    public boolean hasModemActivityReporting() {
+        return mHasModemReporting;
+    }
+
+    @Override
+    public long getFlashlightOnTime(long elapsedRealtimeUs, int which) {
+        return mFlashlightOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override
+    public long getFlashlightOnCount(int which) {
+        return mFlashlightOnTimer.getCountLocked(which);
+    }
+
+    @Override
+    public long getCameraOnTime(long elapsedRealtimeUs, int which) {
+        return mCameraOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override
+    public long getBluetoothScanTime(long elapsedRealtimeUs, int which) {
+        return mBluetoothScanTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+    }
+
+    @Override
+    public long getNetworkActivityBytes(int type, int which) {
+        if (type >= 0 && type < mNetworkByteActivityCounters.length) {
+            return mNetworkByteActivityCounters[type].getCountLocked(which);
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public long getNetworkActivityPackets(int type, int which) {
+        if (type >= 0 && type < mNetworkPacketActivityCounters.length) {
+            return mNetworkPacketActivityCounters[type].getCountLocked(which);
+        } else {
+            return 0;
+        }
+    }
+
+    @Override public long getStartClockTime() {
+        final long currentTime = System.currentTimeMillis();
+        if (ensureStartClockTime(currentTime)) {
+            recordCurrentTimeChangeLocked(currentTime, mClocks.elapsedRealtime(),
+                    mClocks.uptimeMillis());
+        }
+        return mStartClockTime;
+    }
+
+    @Override public String getStartPlatformVersion() {
+        return mStartPlatformVersion;
+    }
+
+    @Override public String getEndPlatformVersion() {
+        return mEndPlatformVersion;
+    }
+
+    @Override public int getParcelVersion() {
+        return VERSION;
+    }
+
+    @Override public boolean getIsOnBattery() {
+        return mOnBattery;
+    }
+
+    @Override public SparseArray<? extends BatteryStats.Uid> getUidStats() {
+        return mUidStats;
+    }
+
+    private static void detachTimerIfNotNull(BatteryStatsImpl.Timer timer) {
+        if (timer != null) {
+            timer.detach();
+        }
+    }
+
+    private static boolean resetTimerIfNotNull(BatteryStatsImpl.Timer timer,
+            boolean detachIfReset) {
+        if (timer != null) {
+            return timer.reset(detachIfReset);
+        }
+        return true;
+    }
+
+    private static boolean resetTimerIfNotNull(DualTimer timer, boolean detachIfReset) {
+        if (timer != null) {
+            return timer.reset(detachIfReset);
+        }
+        return true;
+    }
+
+    private static void detachLongCounterIfNotNull(LongSamplingCounter counter) {
+        if (counter != null) {
+            counter.detach();
+        }
+    }
+
+    private static void resetLongCounterIfNotNull(LongSamplingCounter counter,
+            boolean detachIfReset) {
+        if (counter != null) {
+            counter.reset(detachIfReset);
+        }
+    }
+
+    /**
+     * The statistics associated with a particular uid.
+     */
+    public static class Uid extends BatteryStats.Uid {
+        /**
+         * BatteryStatsImpl that we are associated with.
+         */
+        protected BatteryStatsImpl mBsi;
+
+        final int mUid;
+
+        /** TimeBase for when uid is in background and device is on battery. */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public final TimeBase mOnBatteryBackgroundTimeBase;
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public final TimeBase mOnBatteryScreenOffBackgroundTimeBase;
+
+        boolean mWifiRunning;
+        StopwatchTimer mWifiRunningTimer;
+
+        boolean mFullWifiLockOut;
+        StopwatchTimer mFullWifiLockTimer;
+
+        boolean mWifiScanStarted;
+        DualTimer mWifiScanTimer;
+
+        static final int NO_BATCHED_SCAN_STARTED = -1;
+        int mWifiBatchedScanBinStarted = NO_BATCHED_SCAN_STARTED;
+        StopwatchTimer[] mWifiBatchedScanTimer;
+
+        boolean mWifiMulticastEnabled;
+        StopwatchTimer mWifiMulticastTimer;
+
+        StopwatchTimer mAudioTurnedOnTimer;
+        StopwatchTimer mVideoTurnedOnTimer;
+        StopwatchTimer mFlashlightTurnedOnTimer;
+        StopwatchTimer mCameraTurnedOnTimer;
+        StopwatchTimer mForegroundActivityTimer;
+        StopwatchTimer mForegroundServiceTimer;
+        /** Total time spent by the uid holding any partial wakelocks. */
+        DualTimer mAggregatedPartialWakelockTimer;
+        DualTimer mBluetoothScanTimer;
+        DualTimer mBluetoothUnoptimizedScanTimer;
+        Counter mBluetoothScanResultCounter;
+        Counter mBluetoothScanResultBgCounter;
+
+        int mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
+        StopwatchTimer[] mProcessStateTimer;
+
+        boolean mInForegroundService = false;
+
+        BatchTimer mVibratorOnTimer;
+
+        Counter[] mUserActivityCounters;
+
+        LongSamplingCounter[] mNetworkByteActivityCounters;
+        LongSamplingCounter[] mNetworkPacketActivityCounters;
+        LongSamplingCounter mMobileRadioActiveTime;
+        LongSamplingCounter mMobileRadioActiveCount;
+
+        /**
+         * How many times this UID woke up the Application Processor due to a Mobile radio packet.
+         */
+        private LongSamplingCounter mMobileRadioApWakeupCount;
+
+        /**
+         * How many times this UID woke up the Application Processor due to a Wifi packet.
+         */
+        private LongSamplingCounter mWifiRadioApWakeupCount;
+
+        /**
+         * The amount of time this uid has kept the WiFi controller in idle, tx, and rx mode.
+         * Can be null if the UID has had no such activity.
+         */
+        private ControllerActivityCounterImpl mWifiControllerActivity;
+
+        /**
+         * The amount of time this uid has kept the Bluetooth controller in idle, tx, and rx mode.
+         * Can be null if the UID has had no such activity.
+         */
+        private ControllerActivityCounterImpl mBluetoothControllerActivity;
+
+        /**
+         * The amount of time this uid has kept the Modem controller in idle, tx, and rx mode.
+         * Can be null if the UID has had no such activity.
+         */
+        private ControllerActivityCounterImpl mModemControllerActivity;
+
+        /**
+         * The CPU times we had at the last history details update.
+         */
+        long mLastStepUserTime;
+        long mLastStepSystemTime;
+        long mCurStepUserTime;
+        long mCurStepSystemTime;
+
+        LongSamplingCounter mUserCpuTime;
+        LongSamplingCounter mSystemCpuTime;
+        LongSamplingCounter[][] mCpuClusterSpeedTimesUs;
+
+        LongSamplingCounterArray mCpuFreqTimeMs;
+        LongSamplingCounterArray mScreenOffCpuFreqTimeMs;
+
+        /**
+         * The statistics we have collected for this uid's wake locks.
+         */
+        final OverflowArrayMap<Wakelock> mWakelockStats;
+
+        /**
+         * The statistics we have collected for this uid's syncs.
+         */
+        final OverflowArrayMap<DualTimer> mSyncStats;
+
+        /**
+         * The statistics we have collected for this uid's jobs.
+         */
+        final OverflowArrayMap<DualTimer> mJobStats;
+
+        /**
+         * Count of the jobs that have completed and the reasons why they completed.
+         */
+        final ArrayMap<String, SparseIntArray> mJobCompletions = new ArrayMap<>();
+
+        /**
+         * The statistics we have collected for this uid's sensor activations.
+         */
+        final SparseArray<Sensor> mSensorStats = new SparseArray<>();
+
+        /**
+         * The statistics we have collected for this uid's processes.
+         */
+        final ArrayMap<String, Proc> mProcessStats = new ArrayMap<>();
+
+        /**
+         * The statistics we have collected for this uid's processes.
+         */
+        final ArrayMap<String, Pkg> mPackageStats = new ArrayMap<>();
+
+        /**
+         * The transient wake stats we have collected for this uid's pids.
+         */
+        final SparseArray<Pid> mPids = new SparseArray<>();
+
+        public Uid(BatteryStatsImpl bsi, int uid) {
+            mBsi = bsi;
+            mUid = uid;
+
+            mOnBatteryBackgroundTimeBase = new TimeBase();
+            mOnBatteryBackgroundTimeBase.init(mBsi.mClocks.uptimeMillis() * 1000,
+                    mBsi.mClocks.elapsedRealtime() * 1000);
+
+            mOnBatteryScreenOffBackgroundTimeBase = new TimeBase();
+            mOnBatteryScreenOffBackgroundTimeBase.init(mBsi.mClocks.uptimeMillis() * 1000,
+                    mBsi.mClocks.elapsedRealtime() * 1000);
+
+            mUserCpuTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+            mSystemCpuTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+
+            mWakelockStats = mBsi.new OverflowArrayMap<Wakelock>(uid) {
+                @Override public Wakelock instantiateObject() {
+                    return new Wakelock(mBsi, Uid.this);
+                }
+            };
+            mSyncStats = mBsi.new OverflowArrayMap<DualTimer>(uid) {
+                @Override public DualTimer instantiateObject() {
+                    return new DualTimer(mBsi.mClocks, Uid.this, SYNC, null,
+                            mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase);
+                }
+            };
+            mJobStats = mBsi.new OverflowArrayMap<DualTimer>(uid) {
+                @Override public DualTimer instantiateObject() {
+                    return new DualTimer(mBsi.mClocks, Uid.this, JOB, null,
+                            mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase);
+                }
+            };
+
+            mWifiRunningTimer = new StopwatchTimer(mBsi.mClocks, this, WIFI_RUNNING,
+                    mBsi.mWifiRunningTimers, mBsi.mOnBatteryTimeBase);
+            mFullWifiLockTimer = new StopwatchTimer(mBsi.mClocks, this, FULL_WIFI_LOCK,
+                    mBsi.mFullWifiLockTimers, mBsi.mOnBatteryTimeBase);
+            mWifiScanTimer = new DualTimer(mBsi.mClocks, this, WIFI_SCAN,
+                    mBsi.mWifiScanTimers, mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase);
+            mWifiBatchedScanTimer = new StopwatchTimer[NUM_WIFI_BATCHED_SCAN_BINS];
+            mWifiMulticastTimer = new StopwatchTimer(mBsi.mClocks, this, WIFI_MULTICAST_ENABLED,
+                    mBsi.mWifiMulticastTimers, mBsi.mOnBatteryTimeBase);
+            mProcessStateTimer = new StopwatchTimer[NUM_PROCESS_STATE];
+        }
+
+        @Override
+        public long[] getCpuFreqTimes(int which) {
+            if (mCpuFreqTimeMs == null) {
+                return null;
+            }
+            final long[] cpuFreqTimes = mCpuFreqTimeMs.getCountsLocked(which);
+            if (cpuFreqTimes == null) {
+                return null;
+            }
+            // Return cpuFreqTimes only if atleast one of the elements in non-zero.
+            for (int i = 0; i < cpuFreqTimes.length; ++i) {
+                if (cpuFreqTimes[i] != 0) {
+                    return cpuFreqTimes;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public long[] getScreenOffCpuFreqTimes(int which) {
+            if (mScreenOffCpuFreqTimeMs == null) {
+                return null;
+            }
+            final long[] cpuFreqTimes = mScreenOffCpuFreqTimeMs.getCountsLocked(which);
+            if (cpuFreqTimes == null) {
+                return null;
+            }
+            // Return cpuFreqTimes only if atleast one of the elements in non-zero.
+            for (int i = 0; i < cpuFreqTimes.length; ++i) {
+                if (cpuFreqTimes[i] != 0) {
+                    return cpuFreqTimes;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public Timer getAggregatedPartialWakelockTimer() {
+            return mAggregatedPartialWakelockTimer;
+        }
+
+        @Override
+        public ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> getWakelockStats() {
+            return mWakelockStats.getMap();
+        }
+
+        @Override
+        public ArrayMap<String, ? extends BatteryStats.Timer> getSyncStats() {
+            return mSyncStats.getMap();
+        }
+
+        @Override
+        public ArrayMap<String, ? extends BatteryStats.Timer> getJobStats() {
+            return mJobStats.getMap();
+        }
+
+        @Override
+        public ArrayMap<String, SparseIntArray> getJobCompletionStats() {
+            return mJobCompletions;
+        }
+
+        @Override
+        public SparseArray<? extends BatteryStats.Uid.Sensor> getSensorStats() {
+            return mSensorStats;
+        }
+
+        @Override
+        public ArrayMap<String, ? extends BatteryStats.Uid.Proc> getProcessStats() {
+            return mProcessStats;
+        }
+
+        @Override
+        public ArrayMap<String, ? extends BatteryStats.Uid.Pkg> getPackageStats() {
+            return mPackageStats;
+        }
+
+        @Override
+        public int getUid() {
+            return mUid;
+        }
+
+        @Override
+        public void noteWifiRunningLocked(long elapsedRealtimeMs) {
+            if (!mWifiRunning) {
+                mWifiRunning = true;
+                if (mWifiRunningTimer == null) {
+                    mWifiRunningTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, WIFI_RUNNING,
+                            mBsi.mWifiRunningTimers, mBsi.mOnBatteryTimeBase);
+                }
+                mWifiRunningTimer.startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteWifiStoppedLocked(long elapsedRealtimeMs) {
+            if (mWifiRunning) {
+                mWifiRunning = false;
+                mWifiRunningTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteFullWifiLockAcquiredLocked(long elapsedRealtimeMs) {
+            if (!mFullWifiLockOut) {
+                mFullWifiLockOut = true;
+                if (mFullWifiLockTimer == null) {
+                    mFullWifiLockTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, FULL_WIFI_LOCK,
+                            mBsi.mFullWifiLockTimers, mBsi.mOnBatteryTimeBase);
+                }
+                mFullWifiLockTimer.startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteFullWifiLockReleasedLocked(long elapsedRealtimeMs) {
+            if (mFullWifiLockOut) {
+                mFullWifiLockOut = false;
+                mFullWifiLockTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteWifiScanStartedLocked(long elapsedRealtimeMs) {
+            if (!mWifiScanStarted) {
+                mWifiScanStarted = true;
+                if (mWifiScanTimer == null) {
+                    mWifiScanTimer = new DualTimer(mBsi.mClocks, Uid.this, WIFI_SCAN,
+                            mBsi.mWifiScanTimers, mBsi.mOnBatteryTimeBase,
+                            mOnBatteryBackgroundTimeBase);
+                }
+                mWifiScanTimer.startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteWifiScanStoppedLocked(long elapsedRealtimeMs) {
+            if (mWifiScanStarted) {
+                mWifiScanStarted = false;
+                mWifiScanTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteWifiBatchedScanStartedLocked(int csph, long elapsedRealtimeMs) {
+            int bin = 0;
+            while (csph > 8 && bin < NUM_WIFI_BATCHED_SCAN_BINS-1) {
+                csph = csph >> 3;
+                bin++;
+            }
+
+            if (mWifiBatchedScanBinStarted == bin) return;
+
+            if (mWifiBatchedScanBinStarted != NO_BATCHED_SCAN_STARTED) {
+                mWifiBatchedScanTimer[mWifiBatchedScanBinStarted].
+                        stopRunningLocked(elapsedRealtimeMs);
+            }
+            mWifiBatchedScanBinStarted = bin;
+            if (mWifiBatchedScanTimer[bin] == null) {
+                makeWifiBatchedScanBin(bin, null);
+            }
+            mWifiBatchedScanTimer[bin].startRunningLocked(elapsedRealtimeMs);
+        }
+
+        @Override
+        public void noteWifiBatchedScanStoppedLocked(long elapsedRealtimeMs) {
+            if (mWifiBatchedScanBinStarted != NO_BATCHED_SCAN_STARTED) {
+                mWifiBatchedScanTimer[mWifiBatchedScanBinStarted].
+                        stopRunningLocked(elapsedRealtimeMs);
+                mWifiBatchedScanBinStarted = NO_BATCHED_SCAN_STARTED;
+            }
+        }
+
+        @Override
+        public void noteWifiMulticastEnabledLocked(long elapsedRealtimeMs) {
+            if (!mWifiMulticastEnabled) {
+                mWifiMulticastEnabled = true;
+                if (mWifiMulticastTimer == null) {
+                    mWifiMulticastTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                            WIFI_MULTICAST_ENABLED, mBsi.mWifiMulticastTimers, mBsi.mOnBatteryTimeBase);
+                }
+                mWifiMulticastTimer.startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public void noteWifiMulticastDisabledLocked(long elapsedRealtimeMs) {
+            if (mWifiMulticastEnabled) {
+                mWifiMulticastEnabled = false;
+                mWifiMulticastTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        @Override
+        public ControllerActivityCounter getWifiControllerActivity() {
+            return mWifiControllerActivity;
+        }
+
+        @Override
+        public ControllerActivityCounter getBluetoothControllerActivity() {
+            return mBluetoothControllerActivity;
+        }
+
+        @Override
+        public ControllerActivityCounter getModemControllerActivity() {
+            return mModemControllerActivity;
+        }
+
+        public ControllerActivityCounterImpl getOrCreateWifiControllerActivityLocked() {
+            if (mWifiControllerActivity == null) {
+                mWifiControllerActivity = new ControllerActivityCounterImpl(mBsi.mOnBatteryTimeBase,
+                        NUM_BT_TX_LEVELS);
+            }
+            return mWifiControllerActivity;
+        }
+
+        public ControllerActivityCounterImpl getOrCreateBluetoothControllerActivityLocked() {
+            if (mBluetoothControllerActivity == null) {
+                mBluetoothControllerActivity = new ControllerActivityCounterImpl(mBsi.mOnBatteryTimeBase,
+                        NUM_BT_TX_LEVELS);
+            }
+            return mBluetoothControllerActivity;
+        }
+
+        public ControllerActivityCounterImpl getOrCreateModemControllerActivityLocked() {
+            if (mModemControllerActivity == null) {
+                mModemControllerActivity = new ControllerActivityCounterImpl(mBsi.mOnBatteryTimeBase,
+                        ModemActivityInfo.TX_POWER_LEVELS);
+            }
+            return mModemControllerActivity;
+        }
+
+        public StopwatchTimer createAudioTurnedOnTimerLocked() {
+            if (mAudioTurnedOnTimer == null) {
+                mAudioTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, AUDIO_TURNED_ON,
+                        mBsi.mAudioTurnedOnTimers, mBsi.mOnBatteryTimeBase);
+            }
+            return mAudioTurnedOnTimer;
+        }
+
+        public void noteAudioTurnedOnLocked(long elapsedRealtimeMs) {
+            createAudioTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteAudioTurnedOffLocked(long elapsedRealtimeMs) {
+            if (mAudioTurnedOnTimer != null) {
+                mAudioTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteResetAudioLocked(long elapsedRealtimeMs) {
+            if (mAudioTurnedOnTimer != null) {
+                mAudioTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public StopwatchTimer createVideoTurnedOnTimerLocked() {
+            if (mVideoTurnedOnTimer == null) {
+                mVideoTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, VIDEO_TURNED_ON,
+                        mBsi.mVideoTurnedOnTimers, mBsi.mOnBatteryTimeBase);
+            }
+            return mVideoTurnedOnTimer;
+        }
+
+        public void noteVideoTurnedOnLocked(long elapsedRealtimeMs) {
+            createVideoTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteVideoTurnedOffLocked(long elapsedRealtimeMs) {
+            if (mVideoTurnedOnTimer != null) {
+                mVideoTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteResetVideoLocked(long elapsedRealtimeMs) {
+            if (mVideoTurnedOnTimer != null) {
+                mVideoTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public StopwatchTimer createFlashlightTurnedOnTimerLocked() {
+            if (mFlashlightTurnedOnTimer == null) {
+                mFlashlightTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                        FLASHLIGHT_TURNED_ON, mBsi.mFlashlightTurnedOnTimers, mBsi.mOnBatteryTimeBase);
+            }
+            return mFlashlightTurnedOnTimer;
+        }
+
+        public void noteFlashlightTurnedOnLocked(long elapsedRealtimeMs) {
+            createFlashlightTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteFlashlightTurnedOffLocked(long elapsedRealtimeMs) {
+            if (mFlashlightTurnedOnTimer != null) {
+                mFlashlightTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteResetFlashlightLocked(long elapsedRealtimeMs) {
+            if (mFlashlightTurnedOnTimer != null) {
+                mFlashlightTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public StopwatchTimer createCameraTurnedOnTimerLocked() {
+            if (mCameraTurnedOnTimer == null) {
+                mCameraTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, CAMERA_TURNED_ON,
+                        mBsi.mCameraTurnedOnTimers, mBsi.mOnBatteryTimeBase);
+            }
+            return mCameraTurnedOnTimer;
+        }
+
+        public void noteCameraTurnedOnLocked(long elapsedRealtimeMs) {
+            createCameraTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteCameraTurnedOffLocked(long elapsedRealtimeMs) {
+            if (mCameraTurnedOnTimer != null) {
+                mCameraTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteResetCameraLocked(long elapsedRealtimeMs) {
+            if (mCameraTurnedOnTimer != null) {
+                mCameraTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public StopwatchTimer createForegroundActivityTimerLocked() {
+            if (mForegroundActivityTimer == null) {
+                mForegroundActivityTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                        FOREGROUND_ACTIVITY, null, mBsi.mOnBatteryTimeBase);
+            }
+            return mForegroundActivityTimer;
+        }
+
+        public StopwatchTimer createForegroundServiceTimerLocked() {
+            if (mForegroundServiceTimer == null) {
+                mForegroundServiceTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                        FOREGROUND_SERVICE, null, mBsi.mOnBatteryTimeBase);
+            }
+            return mForegroundServiceTimer;
+        }
+
+        public DualTimer createAggregatedPartialWakelockTimerLocked() {
+            if (mAggregatedPartialWakelockTimer == null) {
+                mAggregatedPartialWakelockTimer = new DualTimer(mBsi.mClocks, this,
+                        AGGREGATED_WAKE_TYPE_PARTIAL, null,
+                        mBsi.mOnBatteryScreenOffTimeBase, mOnBatteryScreenOffBackgroundTimeBase);
+            }
+            return mAggregatedPartialWakelockTimer;
+        }
+
+        public DualTimer createBluetoothScanTimerLocked() {
+            if (mBluetoothScanTimer == null) {
+                mBluetoothScanTimer = new DualTimer(mBsi.mClocks, Uid.this, BLUETOOTH_SCAN_ON,
+                        mBsi.mBluetoothScanOnTimers, mBsi.mOnBatteryTimeBase,
+                        mOnBatteryBackgroundTimeBase);
+            }
+            return mBluetoothScanTimer;
+        }
+
+        public DualTimer createBluetoothUnoptimizedScanTimerLocked() {
+            if (mBluetoothUnoptimizedScanTimer == null) {
+                mBluetoothUnoptimizedScanTimer = new DualTimer(mBsi.mClocks, Uid.this,
+                        BLUETOOTH_UNOPTIMIZED_SCAN_ON, null,
+                        mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase);
+            }
+            return mBluetoothUnoptimizedScanTimer;
+        }
+
+        public void noteBluetoothScanStartedLocked(long elapsedRealtimeMs, boolean isUnoptimized) {
+            createBluetoothScanTimerLocked().startRunningLocked(elapsedRealtimeMs);
+            if (isUnoptimized) {
+                createBluetoothUnoptimizedScanTimerLocked().startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteBluetoothScanStoppedLocked(long elapsedRealtimeMs, boolean isUnoptimized) {
+            if (mBluetoothScanTimer != null) {
+                mBluetoothScanTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+            if (isUnoptimized && mBluetoothUnoptimizedScanTimer != null) {
+                mBluetoothUnoptimizedScanTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteResetBluetoothScanLocked(long elapsedRealtimeMs) {
+            if (mBluetoothScanTimer != null) {
+                mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+            if (mBluetoothUnoptimizedScanTimer != null) {
+                mBluetoothUnoptimizedScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public Counter createBluetoothScanResultCounterLocked() {
+            if (mBluetoothScanResultCounter == null) {
+                mBluetoothScanResultCounter = new Counter(mBsi.mOnBatteryTimeBase);
+            }
+            return mBluetoothScanResultCounter;
+        }
+
+        public Counter createBluetoothScanResultBgCounterLocked() {
+            if (mBluetoothScanResultBgCounter == null) {
+                mBluetoothScanResultBgCounter = new Counter(mOnBatteryBackgroundTimeBase);
+            }
+            return mBluetoothScanResultBgCounter;
+        }
+
+        public void noteBluetoothScanResultsLocked(int numNewResults) {
+            createBluetoothScanResultCounterLocked().addAtomic(numNewResults);
+            // Uses background timebase, so the count will only be incremented if uid in background.
+            createBluetoothScanResultBgCounterLocked().addAtomic(numNewResults);
+        }
+
+        @Override
+        public void noteActivityResumedLocked(long elapsedRealtimeMs) {
+            // We always start, since we want multiple foreground PIDs to nest
+            createForegroundActivityTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        @Override
+        public void noteActivityPausedLocked(long elapsedRealtimeMs) {
+            if (mForegroundActivityTimer != null) {
+                mForegroundActivityTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteForegroundServiceResumedLocked(long elapsedRealtimeMs) {
+            createForegroundServiceTimerLocked().startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteForegroundServicePausedLocked(long elapsedRealtimeMs) {
+            if (mForegroundServiceTimer != null) {
+                mForegroundServiceTimer.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public BatchTimer createVibratorOnTimerLocked() {
+            if (mVibratorOnTimer == null) {
+                mVibratorOnTimer = new BatchTimer(mBsi.mClocks, Uid.this, VIBRATOR_ON,
+                        mBsi.mOnBatteryTimeBase);
+            }
+            return mVibratorOnTimer;
+        }
+
+        public void noteVibratorOnLocked(long durationMillis) {
+            createVibratorOnTimerLocked().addDuration(mBsi, durationMillis);
+        }
+
+        public void noteVibratorOffLocked() {
+            if (mVibratorOnTimer != null) {
+                mVibratorOnTimer.abortLastDuration(mBsi);
+            }
+        }
+
+        @Override
+        public long getWifiRunningTime(long elapsedRealtimeUs, int which) {
+            if (mWifiRunningTimer == null) {
+                return 0;
+            }
+            return mWifiRunningTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+
+        @Override
+        public long getFullWifiLockTime(long elapsedRealtimeUs, int which) {
+            if (mFullWifiLockTimer == null) {
+                return 0;
+            }
+            return mFullWifiLockTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+
+        @Override
+        public long getWifiScanTime(long elapsedRealtimeUs, int which) {
+            if (mWifiScanTimer == null) {
+                return 0;
+            }
+            return mWifiScanTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+
+        @Override
+        public int getWifiScanCount(int which) {
+            if (mWifiScanTimer == null) {
+                return 0;
+            }
+            return mWifiScanTimer.getCountLocked(which);
+        }
+
+        @Override
+        public int getWifiScanBackgroundCount(int which) {
+            if (mWifiScanTimer == null || mWifiScanTimer.getSubTimer() == null) {
+                return 0;
+            }
+            return mWifiScanTimer.getSubTimer().getCountLocked(which);
+        }
+
+        @Override
+        public long getWifiScanActualTime(final long elapsedRealtimeUs) {
+            if (mWifiScanTimer == null) {
+                return 0;
+            }
+            final long elapsedRealtimeMs = (elapsedRealtimeUs + 500) / 1000;
+            return mWifiScanTimer.getTotalDurationMsLocked(elapsedRealtimeMs) * 1000;
+        }
+
+        @Override
+        public long getWifiScanBackgroundTime(final long elapsedRealtimeUs) {
+            if (mWifiScanTimer == null || mWifiScanTimer.getSubTimer() == null) {
+                return 0;
+            }
+            final long elapsedRealtimeMs = (elapsedRealtimeUs + 500) / 1000;
+            return mWifiScanTimer.getSubTimer().getTotalDurationMsLocked(elapsedRealtimeMs) * 1000;
+        }
+
+        @Override
+        public long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which) {
+            if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0;
+            if (mWifiBatchedScanTimer[csphBin] == null) {
+                return 0;
+            }
+            return mWifiBatchedScanTimer[csphBin].getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+
+        @Override
+        public int getWifiBatchedScanCount(int csphBin, int which) {
+            if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0;
+            if (mWifiBatchedScanTimer[csphBin] == null) {
+                return 0;
+            }
+            return mWifiBatchedScanTimer[csphBin].getCountLocked(which);
+        }
+
+        @Override
+        public long getWifiMulticastTime(long elapsedRealtimeUs, int which) {
+            if (mWifiMulticastTimer == null) {
+                return 0;
+            }
+            return mWifiMulticastTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+
+        @Override
+        public Timer getAudioTurnedOnTimer() {
+            return mAudioTurnedOnTimer;
+        }
+
+        @Override
+        public Timer getVideoTurnedOnTimer() {
+            return mVideoTurnedOnTimer;
+        }
+
+        @Override
+        public Timer getFlashlightTurnedOnTimer() {
+            return mFlashlightTurnedOnTimer;
+        }
+
+        @Override
+        public Timer getCameraTurnedOnTimer() {
+            return mCameraTurnedOnTimer;
+        }
+
+        @Override
+        public Timer getForegroundActivityTimer() {
+            return mForegroundActivityTimer;
+        }
+
+        @Override
+        public Timer getForegroundServiceTimer() {
+            return mForegroundServiceTimer;
+        }
+
+        @Override
+        public Timer getBluetoothScanTimer() {
+            return mBluetoothScanTimer;
+        }
+
+        @Override
+        public Timer getBluetoothScanBackgroundTimer() {
+            if (mBluetoothScanTimer == null) {
+                return null;
+            }
+            return mBluetoothScanTimer.getSubTimer();
+        }
+
+        @Override
+        public Timer getBluetoothUnoptimizedScanTimer() {
+            return mBluetoothUnoptimizedScanTimer;
+        }
+
+        @Override
+        public Timer getBluetoothUnoptimizedScanBackgroundTimer() {
+            if (mBluetoothUnoptimizedScanTimer == null) {
+                return null;
+            }
+            return mBluetoothUnoptimizedScanTimer.getSubTimer();
+        }
+
+        @Override
+        public Counter getBluetoothScanResultCounter() {
+            return mBluetoothScanResultCounter;
+        }
+
+        @Override
+        public Counter getBluetoothScanResultBgCounter() {
+            return mBluetoothScanResultBgCounter;
+        }
+
+        void makeProcessState(int i, Parcel in) {
+            if (i < 0 || i >= NUM_PROCESS_STATE) return;
+
+            if (in == null) {
+                mProcessStateTimer[i] = new StopwatchTimer(mBsi.mClocks, this, PROCESS_STATE, null,
+                        mBsi.mOnBatteryTimeBase);
+            } else {
+                mProcessStateTimer[i] = new StopwatchTimer(mBsi.mClocks, this, PROCESS_STATE, null,
+                        mBsi.mOnBatteryTimeBase, in);
+            }
+        }
+
+        @Override
+        public long getProcessStateTime(int state, long elapsedRealtimeUs, int which) {
+            if (state < 0 || state >= NUM_PROCESS_STATE) return 0;
+            if (mProcessStateTimer[state] == null) {
+                return 0;
+            }
+            return mProcessStateTimer[state].getTotalTimeLocked(elapsedRealtimeUs, which);
+        }
+
+        @Override
+        public Timer getProcessStateTimer(int state) {
+            if (state < 0 || state >= NUM_PROCESS_STATE) return null;
+            return mProcessStateTimer[state];
+        }
+
+        @Override
+        public Timer getVibratorOnTimer() {
+            return mVibratorOnTimer;
+        }
+
+        @Override
+        public void noteUserActivityLocked(int type) {
+            if (mUserActivityCounters == null) {
+                initUserActivityLocked();
+            }
+            if (type >= 0 && type < NUM_USER_ACTIVITY_TYPES) {
+                mUserActivityCounters[type].stepAtomic();
+            } else {
+                Slog.w(TAG, "Unknown user activity type " + type + " was specified.",
+                        new Throwable());
+            }
+        }
+
+        @Override
+        public boolean hasUserActivity() {
+            return mUserActivityCounters != null;
+        }
+
+        @Override
+        public int getUserActivityCount(int type, int which) {
+            if (mUserActivityCounters == null) {
+                return 0;
+            }
+            return mUserActivityCounters[type].getCountLocked(which);
+        }
+
+        void makeWifiBatchedScanBin(int i, Parcel in) {
+            if (i < 0 || i >= NUM_WIFI_BATCHED_SCAN_BINS) return;
+
+            ArrayList<StopwatchTimer> collected = mBsi.mWifiBatchedScanTimers.get(i);
+            if (collected == null) {
+                collected = new ArrayList<StopwatchTimer>();
+                mBsi.mWifiBatchedScanTimers.put(i, collected);
+            }
+            if (in == null) {
+                mWifiBatchedScanTimer[i] = new StopwatchTimer(mBsi.mClocks, this, WIFI_BATCHED_SCAN,
+                        collected, mBsi.mOnBatteryTimeBase);
+            } else {
+                mWifiBatchedScanTimer[i] = new StopwatchTimer(mBsi.mClocks, this, WIFI_BATCHED_SCAN,
+                        collected, mBsi.mOnBatteryTimeBase, in);
+            }
+        }
+
+
+        void initUserActivityLocked() {
+            mUserActivityCounters = new Counter[NUM_USER_ACTIVITY_TYPES];
+            for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
+                mUserActivityCounters[i] = new Counter(mBsi.mOnBatteryTimeBase);
+            }
+        }
+
+        void noteNetworkActivityLocked(int type, long deltaBytes, long deltaPackets) {
+            if (mNetworkByteActivityCounters == null) {
+                initNetworkActivityLocked();
+            }
+            if (type >= 0 && type < NUM_NETWORK_ACTIVITY_TYPES) {
+                mNetworkByteActivityCounters[type].addCountLocked(deltaBytes);
+                mNetworkPacketActivityCounters[type].addCountLocked(deltaPackets);
+            } else {
+                Slog.w(TAG, "Unknown network activity type " + type + " was specified.",
+                        new Throwable());
+            }
+        }
+
+        void noteMobileRadioActiveTimeLocked(long batteryUptime) {
+            if (mNetworkByteActivityCounters == null) {
+                initNetworkActivityLocked();
+            }
+            mMobileRadioActiveTime.addCountLocked(batteryUptime);
+            mMobileRadioActiveCount.addCountLocked(1);
+        }
+
+        @Override
+        public boolean hasNetworkActivity() {
+            return mNetworkByteActivityCounters != null;
+        }
+
+        @Override
+        public long getNetworkActivityBytes(int type, int which) {
+            if (mNetworkByteActivityCounters != null && type >= 0
+                    && type < mNetworkByteActivityCounters.length) {
+                return mNetworkByteActivityCounters[type].getCountLocked(which);
+            } else {
+                return 0;
+            }
+        }
+
+        @Override
+        public long getNetworkActivityPackets(int type, int which) {
+            if (mNetworkPacketActivityCounters != null && type >= 0
+                    && type < mNetworkPacketActivityCounters.length) {
+                return mNetworkPacketActivityCounters[type].getCountLocked(which);
+            } else {
+                return 0;
+            }
+        }
+
+        @Override
+        public long getMobileRadioActiveTime(int which) {
+            return mMobileRadioActiveTime != null
+                    ? mMobileRadioActiveTime.getCountLocked(which) : 0;
+        }
+
+        @Override
+        public int getMobileRadioActiveCount(int which) {
+            return mMobileRadioActiveCount != null
+                    ? (int)mMobileRadioActiveCount.getCountLocked(which) : 0;
+        }
+
+        @Override
+        public long getUserCpuTimeUs(int which) {
+            return mUserCpuTime.getCountLocked(which);
+        }
+
+        @Override
+        public long getSystemCpuTimeUs(int which) {
+            return mSystemCpuTime.getCountLocked(which);
+        }
+
+        @Override
+        public long getTimeAtCpuSpeed(int cluster, int step, int which) {
+            if (mCpuClusterSpeedTimesUs != null) {
+                if (cluster >= 0 && cluster < mCpuClusterSpeedTimesUs.length) {
+                    final LongSamplingCounter[] cpuSpeedTimesUs = mCpuClusterSpeedTimesUs[cluster];
+                    if (cpuSpeedTimesUs != null) {
+                        if (step >= 0 && step < cpuSpeedTimesUs.length) {
+                            final LongSamplingCounter c = cpuSpeedTimesUs[step];
+                            if (c != null) {
+                                return c.getCountLocked(which);
+                            }
+                        }
+                    }
+                }
+            }
+            return 0;
+        }
+
+        public void noteMobileRadioApWakeupLocked() {
+            if (mMobileRadioApWakeupCount == null) {
+                mMobileRadioApWakeupCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+            }
+            mMobileRadioApWakeupCount.addCountLocked(1);
+        }
+
+        @Override
+        public long getMobileRadioApWakeupCount(int which) {
+            if (mMobileRadioApWakeupCount != null) {
+                return mMobileRadioApWakeupCount.getCountLocked(which);
+            }
+            return 0;
+        }
+
+        public void noteWifiRadioApWakeupLocked() {
+            if (mWifiRadioApWakeupCount == null) {
+                mWifiRadioApWakeupCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+            }
+            mWifiRadioApWakeupCount.addCountLocked(1);
+        }
+
+        @Override
+        public long getWifiRadioApWakeupCount(int which) {
+            if (mWifiRadioApWakeupCount != null) {
+                return mWifiRadioApWakeupCount.getCountLocked(which);
+            }
+            return 0;
+        }
+
+        void initNetworkActivityLocked() {
+            mNetworkByteActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
+            mNetworkPacketActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
+            for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                mNetworkByteActivityCounters[i] = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+                mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+            }
+            mMobileRadioActiveTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+            mMobileRadioActiveCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+        }
+
+        /**
+         * Clear all stats for this uid.  Returns true if the uid is completely
+         * inactive so can be dropped.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public boolean reset(long uptime, long realtime) {
+            boolean active = false;
+
+            mOnBatteryBackgroundTimeBase.init(uptime, realtime);
+            mOnBatteryScreenOffBackgroundTimeBase.init(uptime, realtime);
+
+            if (mWifiRunningTimer != null) {
+                active |= !mWifiRunningTimer.reset(false);
+                active |= mWifiRunning;
+            }
+            if (mFullWifiLockTimer != null) {
+                active |= !mFullWifiLockTimer.reset(false);
+                active |= mFullWifiLockOut;
+            }
+            if (mWifiScanTimer != null) {
+                active |= !mWifiScanTimer.reset(false);
+                active |= mWifiScanStarted;
+            }
+            if (mWifiBatchedScanTimer != null) {
+                for (int i = 0; i < NUM_WIFI_BATCHED_SCAN_BINS; i++) {
+                    if (mWifiBatchedScanTimer[i] != null) {
+                        active |= !mWifiBatchedScanTimer[i].reset(false);
+                    }
+                }
+                active |= (mWifiBatchedScanBinStarted != NO_BATCHED_SCAN_STARTED);
+            }
+            if (mWifiMulticastTimer != null) {
+                active |= !mWifiMulticastTimer.reset(false);
+                active |= mWifiMulticastEnabled;
+            }
+
+            active |= !resetTimerIfNotNull(mAudioTurnedOnTimer, false);
+            active |= !resetTimerIfNotNull(mVideoTurnedOnTimer, false);
+            active |= !resetTimerIfNotNull(mFlashlightTurnedOnTimer, false);
+            active |= !resetTimerIfNotNull(mCameraTurnedOnTimer, false);
+            active |= !resetTimerIfNotNull(mForegroundActivityTimer, false);
+            active |= !resetTimerIfNotNull(mForegroundServiceTimer, false);
+            active |= !resetTimerIfNotNull(mAggregatedPartialWakelockTimer, false);
+            active |= !resetTimerIfNotNull(mBluetoothScanTimer, false);
+            active |= !resetTimerIfNotNull(mBluetoothUnoptimizedScanTimer, false);
+            if (mBluetoothScanResultCounter != null) {
+                mBluetoothScanResultCounter.reset(false);
+            }
+            if (mBluetoothScanResultBgCounter != null) {
+                mBluetoothScanResultBgCounter.reset(false);
+            }
+
+            if (mProcessStateTimer != null) {
+                for (int i = 0; i < NUM_PROCESS_STATE; i++) {
+                    if (mProcessStateTimer[i] != null) {
+                        active |= !mProcessStateTimer[i].reset(false);
+                    }
+                }
+                active |= (mProcessState != ActivityManager.PROCESS_STATE_NONEXISTENT);
+            }
+            if (mVibratorOnTimer != null) {
+                if (mVibratorOnTimer.reset(false)) {
+                    mVibratorOnTimer.detach();
+                    mVibratorOnTimer = null;
+                } else {
+                    active = true;
+                }
+            }
+
+            if (mUserActivityCounters != null) {
+                for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
+                    mUserActivityCounters[i].reset(false);
+                }
+            }
+
+            if (mNetworkByteActivityCounters != null) {
+                for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                    mNetworkByteActivityCounters[i].reset(false);
+                    mNetworkPacketActivityCounters[i].reset(false);
+                }
+                mMobileRadioActiveTime.reset(false);
+                mMobileRadioActiveCount.reset(false);
+            }
+
+            if (mWifiControllerActivity != null) {
+                mWifiControllerActivity.reset(false);
+            }
+
+            if (mBluetoothControllerActivity != null) {
+                mBluetoothControllerActivity.reset(false);
+            }
+
+            if (mModemControllerActivity != null) {
+                mModemControllerActivity.reset(false);
+            }
+
+            mUserCpuTime.reset(false);
+            mSystemCpuTime.reset(false);
+
+            if (mCpuClusterSpeedTimesUs != null) {
+                for (LongSamplingCounter[] speeds : mCpuClusterSpeedTimesUs) {
+                    if (speeds != null) {
+                        for (LongSamplingCounter speed : speeds) {
+                            if (speed != null) {
+                                speed.reset(false);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (mCpuFreqTimeMs != null) {
+                mCpuFreqTimeMs.reset(false);
+            }
+            if (mScreenOffCpuFreqTimeMs != null) {
+                mScreenOffCpuFreqTimeMs.reset(false);
+            }
+
+            resetLongCounterIfNotNull(mMobileRadioApWakeupCount, false);
+            resetLongCounterIfNotNull(mWifiRadioApWakeupCount, false);
+
+            final ArrayMap<String, Wakelock> wakeStats = mWakelockStats.getMap();
+            for (int iw=wakeStats.size()-1; iw>=0; iw--) {
+                Wakelock wl = wakeStats.valueAt(iw);
+                if (wl.reset()) {
+                    wakeStats.removeAt(iw);
+                } else {
+                    active = true;
+                }
+            }
+            mWakelockStats.cleanup();
+            final ArrayMap<String, DualTimer> syncStats = mSyncStats.getMap();
+            for (int is=syncStats.size()-1; is>=0; is--) {
+                DualTimer timer = syncStats.valueAt(is);
+                if (timer.reset(false)) {
+                    syncStats.removeAt(is);
+                    timer.detach();
+                } else {
+                    active = true;
+                }
+            }
+            mSyncStats.cleanup();
+            final ArrayMap<String, DualTimer> jobStats = mJobStats.getMap();
+            for (int ij=jobStats.size()-1; ij>=0; ij--) {
+                DualTimer timer = jobStats.valueAt(ij);
+                if (timer.reset(false)) {
+                    jobStats.removeAt(ij);
+                    timer.detach();
+                } else {
+                    active = true;
+                }
+            }
+            mJobStats.cleanup();
+            mJobCompletions.clear();
+            for (int ise=mSensorStats.size()-1; ise>=0; ise--) {
+                Sensor s = mSensorStats.valueAt(ise);
+                if (s.reset()) {
+                    mSensorStats.removeAt(ise);
+                } else {
+                    active = true;
+                }
+            }
+            for (int ip=mProcessStats.size()-1; ip>=0; ip--) {
+                Proc proc = mProcessStats.valueAt(ip);
+                proc.detach();
+            }
+            mProcessStats.clear();
+            if (mPids.size() > 0) {
+                for (int i=mPids.size()-1; i>=0; i--) {
+                    Pid pid = mPids.valueAt(i);
+                    if (pid.mWakeNesting > 0) {
+                        active = true;
+                    } else {
+                        mPids.removeAt(i);
+                    }
+                }
+            }
+            if (mPackageStats.size() > 0) {
+                Iterator<Map.Entry<String, Pkg>> it = mPackageStats.entrySet().iterator();
+                while (it.hasNext()) {
+                    Map.Entry<String, Pkg> pkgEntry = it.next();
+                    Pkg p = pkgEntry.getValue();
+                    p.detach();
+                    if (p.mServiceStats.size() > 0) {
+                        Iterator<Map.Entry<String, Pkg.Serv>> it2
+                                = p.mServiceStats.entrySet().iterator();
+                        while (it2.hasNext()) {
+                            Map.Entry<String, Pkg.Serv> servEntry = it2.next();
+                            servEntry.getValue().detach();
+                        }
+                    }
+                }
+                mPackageStats.clear();
+            }
+
+            mLastStepUserTime = mLastStepSystemTime = 0;
+            mCurStepUserTime = mCurStepSystemTime = 0;
+
+            if (!active) {
+                if (mWifiRunningTimer != null) {
+                    mWifiRunningTimer.detach();
+                }
+                if (mFullWifiLockTimer != null) {
+                    mFullWifiLockTimer.detach();
+                }
+                if (mWifiScanTimer != null) {
+                    mWifiScanTimer.detach();
+                }
+                for (int i = 0; i < NUM_WIFI_BATCHED_SCAN_BINS; i++) {
+                    if (mWifiBatchedScanTimer[i] != null) {
+                        mWifiBatchedScanTimer[i].detach();
+                    }
+                }
+                if (mWifiMulticastTimer != null) {
+                    mWifiMulticastTimer.detach();
+                }
+                if (mAudioTurnedOnTimer != null) {
+                    mAudioTurnedOnTimer.detach();
+                    mAudioTurnedOnTimer = null;
+                }
+                if (mVideoTurnedOnTimer != null) {
+                    mVideoTurnedOnTimer.detach();
+                    mVideoTurnedOnTimer = null;
+                }
+                if (mFlashlightTurnedOnTimer != null) {
+                    mFlashlightTurnedOnTimer.detach();
+                    mFlashlightTurnedOnTimer = null;
+                }
+                if (mCameraTurnedOnTimer != null) {
+                    mCameraTurnedOnTimer.detach();
+                    mCameraTurnedOnTimer = null;
+                }
+                if (mForegroundActivityTimer != null) {
+                    mForegroundActivityTimer.detach();
+                    mForegroundActivityTimer = null;
+                }
+                if (mForegroundServiceTimer != null) {
+                    mForegroundServiceTimer.detach();
+                    mForegroundServiceTimer = null;
+                }
+                if (mAggregatedPartialWakelockTimer != null) {
+                    mAggregatedPartialWakelockTimer.detach();
+                    mAggregatedPartialWakelockTimer = null;
+                }
+                if (mBluetoothScanTimer != null) {
+                    mBluetoothScanTimer.detach();
+                    mBluetoothScanTimer = null;
+                }
+                if (mBluetoothUnoptimizedScanTimer != null) {
+                    mBluetoothUnoptimizedScanTimer.detach();
+                    mBluetoothUnoptimizedScanTimer = null;
+                }
+                if (mBluetoothScanResultCounter != null) {
+                    mBluetoothScanResultCounter.detach();
+                    mBluetoothScanResultCounter = null;
+                }
+                if (mBluetoothScanResultBgCounter != null) {
+                    mBluetoothScanResultBgCounter.detach();
+                    mBluetoothScanResultBgCounter = null;
+                }
+                if (mUserActivityCounters != null) {
+                    for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
+                        mUserActivityCounters[i].detach();
+                    }
+                }
+                if (mNetworkByteActivityCounters != null) {
+                    for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                        mNetworkByteActivityCounters[i].detach();
+                        mNetworkPacketActivityCounters[i].detach();
+                    }
+                }
+
+                if (mWifiControllerActivity != null) {
+                    mWifiControllerActivity.detach();
+                }
+
+                if (mBluetoothControllerActivity != null) {
+                    mBluetoothControllerActivity.detach();
+                }
+
+                if (mModemControllerActivity != null) {
+                    mModemControllerActivity.detach();
+                }
+
+                mPids.clear();
+
+                mUserCpuTime.detach();
+                mSystemCpuTime.detach();
+
+                if (mCpuClusterSpeedTimesUs != null) {
+                    for (LongSamplingCounter[] cpuSpeeds : mCpuClusterSpeedTimesUs) {
+                        if (cpuSpeeds != null) {
+                            for (LongSamplingCounter c : cpuSpeeds) {
+                                if (c != null) {
+                                    c.detach();
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (mCpuFreqTimeMs != null) {
+                    mCpuFreqTimeMs.detach();
+                }
+                if (mScreenOffCpuFreqTimeMs != null) {
+                    mScreenOffCpuFreqTimeMs.detach();
+                }
+
+                detachLongCounterIfNotNull(mMobileRadioApWakeupCount);
+                detachLongCounterIfNotNull(mWifiRadioApWakeupCount);
+            }
+
+            return !active;
+        }
+
+        void writeJobCompletionsToParcelLocked(Parcel out) {
+            int NJC = mJobCompletions.size();
+            out.writeInt(NJC);
+            for (int ijc=0; ijc<NJC; ijc++) {
+                out.writeString(mJobCompletions.keyAt(ijc));
+                SparseIntArray types = mJobCompletions.valueAt(ijc);
+                int NT = types.size();
+                out.writeInt(NT);
+                for (int it=0; it<NT; it++) {
+                    out.writeInt(types.keyAt(it));
+                    out.writeInt(types.valueAt(it));
+                }
+            }
+        }
+
+        void writeToParcelLocked(Parcel out, long uptimeUs, long elapsedRealtimeUs) {
+            mOnBatteryBackgroundTimeBase.writeToParcel(out, uptimeUs, elapsedRealtimeUs);
+            mOnBatteryScreenOffBackgroundTimeBase.writeToParcel(out, uptimeUs, elapsedRealtimeUs);
+
+            final ArrayMap<String, Wakelock> wakeStats = mWakelockStats.getMap();
+            int NW = wakeStats.size();
+            out.writeInt(NW);
+            for (int iw=0; iw<NW; iw++) {
+                out.writeString(wakeStats.keyAt(iw));
+                Uid.Wakelock wakelock = wakeStats.valueAt(iw);
+                wakelock.writeToParcelLocked(out, elapsedRealtimeUs);
+            }
+
+            final ArrayMap<String, DualTimer> syncStats = mSyncStats.getMap();
+            int NS = syncStats.size();
+            out.writeInt(NS);
+            for (int is=0; is<NS; is++) {
+                out.writeString(syncStats.keyAt(is));
+                DualTimer timer = syncStats.valueAt(is);
+                Timer.writeTimerToParcel(out, timer, elapsedRealtimeUs);
+            }
+
+            final ArrayMap<String, DualTimer> jobStats = mJobStats.getMap();
+            int NJ = jobStats.size();
+            out.writeInt(NJ);
+            for (int ij=0; ij<NJ; ij++) {
+                out.writeString(jobStats.keyAt(ij));
+                DualTimer timer = jobStats.valueAt(ij);
+                Timer.writeTimerToParcel(out, timer, elapsedRealtimeUs);
+            }
+
+            writeJobCompletionsToParcelLocked(out);
+
+            int NSE = mSensorStats.size();
+            out.writeInt(NSE);
+            for (int ise=0; ise<NSE; ise++) {
+                out.writeInt(mSensorStats.keyAt(ise));
+                Uid.Sensor sensor = mSensorStats.valueAt(ise);
+                sensor.writeToParcelLocked(out, elapsedRealtimeUs);
+            }
+
+            int NP = mProcessStats.size();
+            out.writeInt(NP);
+            for (int ip=0; ip<NP; ip++) {
+                out.writeString(mProcessStats.keyAt(ip));
+                Uid.Proc proc = mProcessStats.valueAt(ip);
+                proc.writeToParcelLocked(out);
+            }
+
+            out.writeInt(mPackageStats.size());
+            for (Map.Entry<String, Uid.Pkg> pkgEntry : mPackageStats.entrySet()) {
+                out.writeString(pkgEntry.getKey());
+                Uid.Pkg pkg = pkgEntry.getValue();
+                pkg.writeToParcelLocked(out);
+            }
+
+            if (mWifiRunningTimer != null) {
+                out.writeInt(1);
+                mWifiRunningTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mFullWifiLockTimer != null) {
+                out.writeInt(1);
+                mFullWifiLockTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mWifiScanTimer != null) {
+                out.writeInt(1);
+                mWifiScanTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            for (int i = 0; i < NUM_WIFI_BATCHED_SCAN_BINS; i++) {
+                if (mWifiBatchedScanTimer[i] != null) {
+                    out.writeInt(1);
+                    mWifiBatchedScanTimer[i].writeToParcel(out, elapsedRealtimeUs);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+            if (mWifiMulticastTimer != null) {
+                out.writeInt(1);
+                mWifiMulticastTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (mAudioTurnedOnTimer != null) {
+                out.writeInt(1);
+                mAudioTurnedOnTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mVideoTurnedOnTimer != null) {
+                out.writeInt(1);
+                mVideoTurnedOnTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mFlashlightTurnedOnTimer != null) {
+                out.writeInt(1);
+                mFlashlightTurnedOnTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mCameraTurnedOnTimer != null) {
+                out.writeInt(1);
+                mCameraTurnedOnTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mForegroundActivityTimer != null) {
+                out.writeInt(1);
+                mForegroundActivityTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mForegroundServiceTimer != null) {
+                out.writeInt(1);
+                mForegroundServiceTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mAggregatedPartialWakelockTimer != null) {
+                out.writeInt(1);
+                mAggregatedPartialWakelockTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mBluetoothScanTimer != null) {
+                out.writeInt(1);
+                mBluetoothScanTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mBluetoothUnoptimizedScanTimer != null) {
+                out.writeInt(1);
+                mBluetoothUnoptimizedScanTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mBluetoothScanResultCounter != null) {
+                out.writeInt(1);
+                mBluetoothScanResultCounter.writeToParcel(out);
+            } else {
+                out.writeInt(0);
+            }
+            if (mBluetoothScanResultBgCounter != null) {
+                out.writeInt(1);
+                mBluetoothScanResultBgCounter.writeToParcel(out);
+            } else {
+                out.writeInt(0);
+            }
+            for (int i = 0; i < NUM_PROCESS_STATE; i++) {
+                if (mProcessStateTimer[i] != null) {
+                    out.writeInt(1);
+                    mProcessStateTimer[i].writeToParcel(out, elapsedRealtimeUs);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+            if (mVibratorOnTimer != null) {
+                out.writeInt(1);
+                mVibratorOnTimer.writeToParcel(out, elapsedRealtimeUs);
+            } else {
+                out.writeInt(0);
+            }
+            if (mUserActivityCounters != null) {
+                out.writeInt(1);
+                for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
+                    mUserActivityCounters[i].writeToParcel(out);
+                }
+            } else {
+                out.writeInt(0);
+            }
+            if (mNetworkByteActivityCounters != null) {
+                out.writeInt(1);
+                for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                    mNetworkByteActivityCounters[i].writeToParcel(out);
+                    mNetworkPacketActivityCounters[i].writeToParcel(out);
+                }
+                mMobileRadioActiveTime.writeToParcel(out);
+                mMobileRadioActiveCount.writeToParcel(out);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (mWifiControllerActivity != null) {
+                out.writeInt(1);
+                mWifiControllerActivity.writeToParcel(out, 0);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (mBluetoothControllerActivity != null) {
+                out.writeInt(1);
+                mBluetoothControllerActivity.writeToParcel(out, 0);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (mModemControllerActivity != null) {
+                out.writeInt(1);
+                mModemControllerActivity.writeToParcel(out, 0);
+            } else {
+                out.writeInt(0);
+            }
+
+            mUserCpuTime.writeToParcel(out);
+            mSystemCpuTime.writeToParcel(out);
+
+            if (mCpuClusterSpeedTimesUs != null) {
+                out.writeInt(1);
+                out.writeInt(mCpuClusterSpeedTimesUs.length);
+                for (LongSamplingCounter[] cpuSpeeds : mCpuClusterSpeedTimesUs) {
+                    if (cpuSpeeds != null) {
+                        out.writeInt(1);
+                        out.writeInt(cpuSpeeds.length);
+                        for (LongSamplingCounter c : cpuSpeeds) {
+                            if (c != null) {
+                                out.writeInt(1);
+                                c.writeToParcel(out);
+                            } else {
+                                out.writeInt(0);
+                            }
+                        }
+                    } else {
+                        out.writeInt(0);
+                    }
+                }
+            } else {
+                out.writeInt(0);
+            }
+
+            LongSamplingCounterArray.writeToParcel(out, mCpuFreqTimeMs);
+            LongSamplingCounterArray.writeToParcel(out, mScreenOffCpuFreqTimeMs);
+
+            if (mMobileRadioApWakeupCount != null) {
+                out.writeInt(1);
+                mMobileRadioApWakeupCount.writeToParcel(out);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (mWifiRadioApWakeupCount != null) {
+                out.writeInt(1);
+                mWifiRadioApWakeupCount.writeToParcel(out);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        void readJobCompletionsFromParcelLocked(Parcel in) {
+            int numJobCompletions = in.readInt();
+            mJobCompletions.clear();
+            for (int j = 0; j < numJobCompletions; j++) {
+                String jobName = in.readString();
+                int numTypes = in.readInt();
+                if (numTypes > 0) {
+                    SparseIntArray types = new SparseIntArray();
+                    for (int k = 0; k < numTypes; k++) {
+                        int type = in.readInt();
+                        int count = in.readInt();
+                        types.put(type, count);
+                    }
+                    mJobCompletions.put(jobName, types);
+                }
+            }
+        }
+
+        void readFromParcelLocked(TimeBase timeBase, TimeBase screenOffTimeBase, Parcel in) {
+            mOnBatteryBackgroundTimeBase.readFromParcel(in);
+            mOnBatteryScreenOffBackgroundTimeBase.readFromParcel(in);
+
+            int numWakelocks = in.readInt();
+            mWakelockStats.clear();
+            for (int j = 0; j < numWakelocks; j++) {
+                String wakelockName = in.readString();
+                Uid.Wakelock wakelock = new Wakelock(mBsi, this);
+                wakelock.readFromParcelLocked(
+                        timeBase, screenOffTimeBase, mOnBatteryScreenOffBackgroundTimeBase, in);
+                mWakelockStats.add(wakelockName, wakelock);
+            }
+
+            int numSyncs = in.readInt();
+            mSyncStats.clear();
+            for (int j = 0; j < numSyncs; j++) {
+                String syncName = in.readString();
+                if (in.readInt() != 0) {
+                    mSyncStats.add(syncName, new DualTimer(mBsi.mClocks, Uid.this, SYNC, null,
+                            mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase, in));
+                }
+            }
+
+            int numJobs = in.readInt();
+            mJobStats.clear();
+            for (int j = 0; j < numJobs; j++) {
+                String jobName = in.readString();
+                if (in.readInt() != 0) {
+                    mJobStats.add(jobName, new DualTimer(mBsi.mClocks, Uid.this, JOB, null,
+                            mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase, in));
+                }
+            }
+
+            readJobCompletionsFromParcelLocked(in);
+
+            int numSensors = in.readInt();
+            mSensorStats.clear();
+            for (int k = 0; k < numSensors; k++) {
+                int sensorNumber = in.readInt();
+                Uid.Sensor sensor = new Sensor(mBsi, this, sensorNumber);
+                sensor.readFromParcelLocked(mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase,
+                        in);
+                mSensorStats.put(sensorNumber, sensor);
+            }
+
+            int numProcs = in.readInt();
+            mProcessStats.clear();
+            for (int k = 0; k < numProcs; k++) {
+                String processName = in.readString();
+                Uid.Proc proc = new Proc(mBsi, processName);
+                proc.readFromParcelLocked(in);
+                mProcessStats.put(processName, proc);
+            }
+
+            int numPkgs = in.readInt();
+            mPackageStats.clear();
+            for (int l = 0; l < numPkgs; l++) {
+                String packageName = in.readString();
+                Uid.Pkg pkg = new Pkg(mBsi);
+                pkg.readFromParcelLocked(in);
+                mPackageStats.put(packageName, pkg);
+            }
+
+            mWifiRunning = false;
+            if (in.readInt() != 0) {
+                mWifiRunningTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, WIFI_RUNNING,
+                        mBsi.mWifiRunningTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mWifiRunningTimer = null;
+            }
+            mFullWifiLockOut = false;
+            if (in.readInt() != 0) {
+                mFullWifiLockTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, FULL_WIFI_LOCK,
+                        mBsi.mFullWifiLockTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mFullWifiLockTimer = null;
+            }
+            mWifiScanStarted = false;
+            if (in.readInt() != 0) {
+                mWifiScanTimer = new DualTimer(mBsi.mClocks, Uid.this, WIFI_SCAN,
+                        mBsi.mWifiScanTimers, mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase,
+                        in);
+            } else {
+                mWifiScanTimer = null;
+            }
+            mWifiBatchedScanBinStarted = NO_BATCHED_SCAN_STARTED;
+            for (int i = 0; i < NUM_WIFI_BATCHED_SCAN_BINS; i++) {
+                if (in.readInt() != 0) {
+                    makeWifiBatchedScanBin(i, in);
+                } else {
+                    mWifiBatchedScanTimer[i] = null;
+                }
+            }
+            mWifiMulticastEnabled = false;
+            if (in.readInt() != 0) {
+                mWifiMulticastTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, WIFI_MULTICAST_ENABLED,
+                        mBsi.mWifiMulticastTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mWifiMulticastTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mAudioTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, AUDIO_TURNED_ON,
+                        mBsi.mAudioTurnedOnTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mAudioTurnedOnTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mVideoTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, VIDEO_TURNED_ON,
+                        mBsi.mVideoTurnedOnTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mVideoTurnedOnTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mFlashlightTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                        FLASHLIGHT_TURNED_ON, mBsi.mFlashlightTurnedOnTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mFlashlightTurnedOnTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mCameraTurnedOnTimer = new StopwatchTimer(mBsi.mClocks, Uid.this, CAMERA_TURNED_ON,
+                        mBsi.mCameraTurnedOnTimers, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mCameraTurnedOnTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mForegroundActivityTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                        FOREGROUND_ACTIVITY, null, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mForegroundActivityTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mForegroundServiceTimer = new StopwatchTimer(mBsi.mClocks, Uid.this,
+                        FOREGROUND_SERVICE, null, mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mForegroundServiceTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mAggregatedPartialWakelockTimer = new DualTimer(mBsi.mClocks, this,
+                        AGGREGATED_WAKE_TYPE_PARTIAL, null,
+                        mBsi.mOnBatteryScreenOffTimeBase, mOnBatteryScreenOffBackgroundTimeBase,
+                        in);
+            } else {
+                mAggregatedPartialWakelockTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mBluetoothScanTimer = new DualTimer(mBsi.mClocks, Uid.this, BLUETOOTH_SCAN_ON,
+                        mBsi.mBluetoothScanOnTimers, mBsi.mOnBatteryTimeBase,
+                        mOnBatteryBackgroundTimeBase, in);
+            } else {
+                mBluetoothScanTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mBluetoothUnoptimizedScanTimer = new DualTimer(mBsi.mClocks, Uid.this,
+                        BLUETOOTH_UNOPTIMIZED_SCAN_ON, null,
+                        mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase, in);
+            } else {
+                mBluetoothUnoptimizedScanTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mBluetoothScanResultCounter = new Counter(mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mBluetoothScanResultCounter = null;
+            }
+            if (in.readInt() != 0) {
+                mBluetoothScanResultBgCounter = new Counter(mOnBatteryBackgroundTimeBase, in);
+            } else {
+                mBluetoothScanResultBgCounter = null;
+            }
+            mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
+            for (int i = 0; i < NUM_PROCESS_STATE; i++) {
+                if (in.readInt() != 0) {
+                    makeProcessState(i, in);
+                } else {
+                    mProcessStateTimer[i] = null;
+                }
+            }
+            if (in.readInt() != 0) {
+                mVibratorOnTimer = new BatchTimer(mBsi.mClocks, Uid.this, VIBRATOR_ON,
+                        mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mVibratorOnTimer = null;
+            }
+            if (in.readInt() != 0) {
+                mUserActivityCounters = new Counter[NUM_USER_ACTIVITY_TYPES];
+                for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
+                    mUserActivityCounters[i] = new Counter(mBsi.mOnBatteryTimeBase, in);
+                }
+            } else {
+                mUserActivityCounters = null;
+            }
+            if (in.readInt() != 0) {
+                mNetworkByteActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
+                mNetworkPacketActivityCounters
+                        = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
+                for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                    mNetworkByteActivityCounters[i]
+                            = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+                    mNetworkPacketActivityCounters[i]
+                            = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+                }
+                mMobileRadioActiveTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+                mMobileRadioActiveCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mNetworkByteActivityCounters = null;
+                mNetworkPacketActivityCounters = null;
+            }
+
+            if (in.readInt() != 0) {
+                mWifiControllerActivity = new ControllerActivityCounterImpl(mBsi.mOnBatteryTimeBase,
+                        NUM_WIFI_TX_LEVELS, in);
+            } else {
+                mWifiControllerActivity = null;
+            }
+
+            if (in.readInt() != 0) {
+                mBluetoothControllerActivity = new ControllerActivityCounterImpl(mBsi.mOnBatteryTimeBase,
+                        NUM_BT_TX_LEVELS, in);
+            } else {
+                mBluetoothControllerActivity = null;
+            }
+
+            if (in.readInt() != 0) {
+                mModemControllerActivity = new ControllerActivityCounterImpl(mBsi.mOnBatteryTimeBase,
+                        ModemActivityInfo.TX_POWER_LEVELS, in);
+            } else {
+                mModemControllerActivity = null;
+            }
+
+            mUserCpuTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+            mSystemCpuTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+
+            if (in.readInt() != 0) {
+                int numCpuClusters = in.readInt();
+                if (mBsi.mPowerProfile != null && mBsi.mPowerProfile.getNumCpuClusters() != numCpuClusters) {
+                    throw new ParcelFormatException("Incompatible number of cpu clusters");
+                }
+
+                mCpuClusterSpeedTimesUs = new LongSamplingCounter[numCpuClusters][];
+                for (int cluster = 0; cluster < numCpuClusters; cluster++) {
+                    if (in.readInt() != 0) {
+                        int numSpeeds = in.readInt();
+                        if (mBsi.mPowerProfile != null &&
+                                mBsi.mPowerProfile.getNumSpeedStepsInCpuCluster(cluster) != numSpeeds) {
+                            throw new ParcelFormatException("Incompatible number of cpu speeds");
+                        }
+
+                        final LongSamplingCounter[] cpuSpeeds = new LongSamplingCounter[numSpeeds];
+                        mCpuClusterSpeedTimesUs[cluster] = cpuSpeeds;
+                        for (int speed = 0; speed < numSpeeds; speed++) {
+                            if (in.readInt() != 0) {
+                                cpuSpeeds[speed] = new LongSamplingCounter(
+                                        mBsi.mOnBatteryTimeBase, in);
+                            }
+                        }
+                    } else {
+                        mCpuClusterSpeedTimesUs[cluster] = null;
+                    }
+                }
+            } else {
+                mCpuClusterSpeedTimesUs = null;
+            }
+
+            mCpuFreqTimeMs = LongSamplingCounterArray.readFromParcel(in, mBsi.mOnBatteryTimeBase);
+            mScreenOffCpuFreqTimeMs = LongSamplingCounterArray.readFromParcel(
+                    in, mBsi.mOnBatteryScreenOffTimeBase);
+
+            if (in.readInt() != 0) {
+                mMobileRadioApWakeupCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mMobileRadioApWakeupCount = null;
+            }
+
+            if (in.readInt() != 0) {
+                mWifiRadioApWakeupCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+            } else {
+                mWifiRadioApWakeupCount = null;
+            }
+        }
+
+        /**
+         * The statistics associated with a particular wake lock.
+         */
+        public static class Wakelock extends BatteryStats.Uid.Wakelock {
+            /**
+             * BatteryStatsImpl that we are associated with.
+             */
+            protected BatteryStatsImpl mBsi;
+
+            /**
+             * BatteryStatsImpl that we are associated with.
+             */
+            protected Uid mUid;
+
+            /**
+             * How long (in ms) this uid has been keeping the device partially awake.
+             * Tracks both the total time and the time while the app was in the background.
+             */
+            DualTimer mTimerPartial;
+
+            /**
+             * How long (in ms) this uid has been keeping the device fully awake.
+             */
+            StopwatchTimer mTimerFull;
+
+            /**
+             * How long (in ms) this uid has had a window keeping the device awake.
+             */
+            StopwatchTimer mTimerWindow;
+
+            /**
+             * How long (in ms) this uid has had a draw wake lock.
+             */
+            StopwatchTimer mTimerDraw;
+
+            public Wakelock(BatteryStatsImpl bsi, Uid uid) {
+                mBsi = bsi;
+                mUid = uid;
+            }
+
+            /**
+             * Reads a possibly null Timer from a Parcel.  The timer is associated with the
+             * proper timer pool from the given BatteryStatsImpl object.
+             *
+             * @param in the Parcel to be read from.
+             * return a new Timer, or null.
+             */
+            private StopwatchTimer readStopwatchTimerFromParcel(int type,
+                    ArrayList<StopwatchTimer> pool, TimeBase timeBase, Parcel in) {
+                if (in.readInt() == 0) {
+                    return null;
+                }
+
+                return new StopwatchTimer(mBsi.mClocks, mUid, type, pool, timeBase, in);
+            }
+
+            /**
+             * Reads a possibly null Timer from a Parcel.  The timer is associated with the
+             * proper timer pool from the given BatteryStatsImpl object.
+             *
+             * @param in the Parcel to be read from.
+             * return a new Timer, or null.
+             */
+            private DualTimer readDualTimerFromParcel(int type, ArrayList<StopwatchTimer> pool,
+                    TimeBase timeBase, TimeBase bgTimeBase, Parcel in) {
+                if (in.readInt() == 0) {
+                    return null;
+                }
+
+                return new DualTimer(mBsi.mClocks, mUid, type, pool, timeBase, bgTimeBase, in);
+            }
+
+            boolean reset() {
+                boolean wlactive = false;
+                if (mTimerFull != null) {
+                    wlactive |= !mTimerFull.reset(false);
+                }
+                if (mTimerPartial != null) {
+                    wlactive |= !mTimerPartial.reset(false);
+                }
+                if (mTimerWindow != null) {
+                    wlactive |= !mTimerWindow.reset(false);
+                }
+                if (mTimerDraw != null) {
+                    wlactive |= !mTimerDraw.reset(false);
+                }
+                if (!wlactive) {
+                    if (mTimerFull != null) {
+                        mTimerFull.detach();
+                        mTimerFull = null;
+                    }
+                    if (mTimerPartial != null) {
+                        mTimerPartial.detach();
+                        mTimerPartial = null;
+                    }
+                    if (mTimerWindow != null) {
+                        mTimerWindow.detach();
+                        mTimerWindow = null;
+                    }
+                    if (mTimerDraw != null) {
+                        mTimerDraw.detach();
+                        mTimerDraw = null;
+                    }
+                }
+                return !wlactive;
+            }
+
+            void readFromParcelLocked(TimeBase timeBase, TimeBase screenOffTimeBase,
+                    TimeBase screenOffBgTimeBase, Parcel in) {
+                mTimerPartial = readDualTimerFromParcel(WAKE_TYPE_PARTIAL,
+                        mBsi.mPartialTimers, screenOffTimeBase, screenOffBgTimeBase, in);
+                mTimerFull = readStopwatchTimerFromParcel(WAKE_TYPE_FULL,
+                        mBsi.mFullTimers, timeBase, in);
+                mTimerWindow = readStopwatchTimerFromParcel(WAKE_TYPE_WINDOW,
+                        mBsi.mWindowTimers, timeBase, in);
+                mTimerDraw = readStopwatchTimerFromParcel(WAKE_TYPE_DRAW,
+                        mBsi.mDrawTimers, timeBase, in);
+            }
+
+            void writeToParcelLocked(Parcel out, long elapsedRealtimeUs) {
+                Timer.writeTimerToParcel(out, mTimerPartial, elapsedRealtimeUs);
+                Timer.writeTimerToParcel(out, mTimerFull, elapsedRealtimeUs);
+                Timer.writeTimerToParcel(out, mTimerWindow, elapsedRealtimeUs);
+                Timer.writeTimerToParcel(out, mTimerDraw, elapsedRealtimeUs);
+            }
+
+            @Override
+            public Timer getWakeTime(int type) {
+                switch (type) {
+                case WAKE_TYPE_FULL: return mTimerFull;
+                case WAKE_TYPE_PARTIAL: return mTimerPartial;
+                case WAKE_TYPE_WINDOW: return mTimerWindow;
+                case WAKE_TYPE_DRAW: return mTimerDraw;
+                default: throw new IllegalArgumentException("type = " + type);
+                }
+            }
+        }
+
+        public static class Sensor extends BatteryStats.Uid.Sensor {
+            /**
+             * BatteryStatsImpl that we are associated with.
+             */
+            protected BatteryStatsImpl mBsi;
+
+            /**
+             * Uid that we are associated with.
+             */
+            protected Uid mUid;
+
+            final int mHandle;
+            DualTimer mTimer;
+
+            public Sensor(BatteryStatsImpl bsi, Uid uid, int handle) {
+                mBsi = bsi;
+                mUid = uid;
+                mHandle = handle;
+            }
+
+            private DualTimer readTimersFromParcel(
+                    TimeBase timeBase, TimeBase bgTimeBase, Parcel in) {
+                if (in.readInt() == 0) {
+                    return null;
+                }
+
+                ArrayList<StopwatchTimer> pool = mBsi.mSensorTimers.get(mHandle);
+                if (pool == null) {
+                    pool = new ArrayList<StopwatchTimer>();
+                    mBsi.mSensorTimers.put(mHandle, pool);
+                }
+                return new DualTimer(mBsi.mClocks, mUid, 0, pool, timeBase, bgTimeBase, in);
+            }
+
+            boolean reset() {
+                if (mTimer.reset(true)) {
+                    mTimer = null;
+                    return true;
+                }
+                return false;
+            }
+
+            void readFromParcelLocked(TimeBase timeBase, TimeBase bgTimeBase, Parcel in) {
+                mTimer = readTimersFromParcel(timeBase, bgTimeBase, in);
+            }
+
+            void writeToParcelLocked(Parcel out, long elapsedRealtimeUs) {
+                Timer.writeTimerToParcel(out, mTimer, elapsedRealtimeUs);
+            }
+
+            @Override
+            public Timer getSensorTime() {
+                return mTimer;
+            }
+
+            @Override
+            public Timer getSensorBackgroundTime() {
+                if (mTimer == null) {
+                    return null;
+                }
+                return mTimer.getSubTimer();
+            }
+
+            @Override
+            public int getHandle() {
+                return mHandle;
+            }
+        }
+
+        /**
+         * The statistics associated with a particular process.
+         */
+        public static class Proc extends BatteryStats.Uid.Proc implements TimeBaseObs {
+            /**
+             * BatteryStatsImpl that we are associated with.
+             */
+            protected BatteryStatsImpl mBsi;
+
+            /**
+             * The name of this process.
+             */
+            final String mName;
+
+            /**
+             * Remains true until removed from the stats.
+             */
+            boolean mActive = true;
+
+            /**
+             * Total time (in ms) spent executing in user code.
+             */
+            long mUserTime;
+
+            /**
+             * Total time (in ms) spent executing in kernel code.
+             */
+            long mSystemTime;
+
+            /**
+             * Amount of time (in ms) the process was running in the foreground.
+             */
+            long mForegroundTime;
+
+            /**
+             * Number of times the process has been started.
+             */
+            int mStarts;
+
+            /**
+             * Number of times the process has crashed.
+             */
+            int mNumCrashes;
+
+            /**
+             * Number of times the process has had an ANR.
+             */
+            int mNumAnrs;
+
+            /**
+             * The amount of user time loaded from a previous save.
+             */
+            long mLoadedUserTime;
+
+            /**
+             * The amount of system time loaded from a previous save.
+             */
+            long mLoadedSystemTime;
+
+            /**
+             * The amount of foreground time loaded from a previous save.
+             */
+            long mLoadedForegroundTime;
+
+            /**
+             * The number of times the process has started from a previous save.
+             */
+            int mLoadedStarts;
+
+            /**
+             * Number of times the process has crashed from a previous save.
+             */
+            int mLoadedNumCrashes;
+
+            /**
+             * Number of times the process has had an ANR from a previous save.
+             */
+            int mLoadedNumAnrs;
+
+            /**
+             * The amount of user time when last unplugged.
+             */
+            long mUnpluggedUserTime;
+
+            /**
+             * The amount of system time when last unplugged.
+             */
+            long mUnpluggedSystemTime;
+
+            /**
+             * The amount of foreground time since unplugged.
+             */
+            long mUnpluggedForegroundTime;
+
+            /**
+             * The number of times the process has started before unplugged.
+             */
+            int mUnpluggedStarts;
+
+            /**
+             * Number of times the process has crashed before unplugged.
+             */
+            int mUnpluggedNumCrashes;
+
+            /**
+             * Number of times the process has had an ANR before unplugged.
+             */
+            int mUnpluggedNumAnrs;
+
+            ArrayList<ExcessivePower> mExcessivePower;
+
+            public Proc(BatteryStatsImpl bsi, String name) {
+                mBsi = bsi;
+                mName = name;
+                mBsi.mOnBatteryTimeBase.add(this);
+            }
+
+            public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) {
+                mUnpluggedUserTime = mUserTime;
+                mUnpluggedSystemTime = mSystemTime;
+                mUnpluggedForegroundTime = mForegroundTime;
+                mUnpluggedStarts = mStarts;
+                mUnpluggedNumCrashes = mNumCrashes;
+                mUnpluggedNumAnrs = mNumAnrs;
+            }
+
+            public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            }
+
+            void detach() {
+                mActive = false;
+                mBsi.mOnBatteryTimeBase.remove(this);
+            }
+
+            public int countExcessivePowers() {
+                return mExcessivePower != null ? mExcessivePower.size() : 0;
+            }
+
+            public ExcessivePower getExcessivePower(int i) {
+                if (mExcessivePower != null) {
+                    return mExcessivePower.get(i);
+                }
+                return null;
+            }
+
+            public void addExcessiveCpu(long overTime, long usedTime) {
+                if (mExcessivePower == null) {
+                    mExcessivePower = new ArrayList<ExcessivePower>();
+                }
+                ExcessivePower ew = new ExcessivePower();
+                ew.type = ExcessivePower.TYPE_CPU;
+                ew.overTime = overTime;
+                ew.usedTime = usedTime;
+                mExcessivePower.add(ew);
+            }
+
+            void writeExcessivePowerToParcelLocked(Parcel out) {
+                if (mExcessivePower == null) {
+                    out.writeInt(0);
+                    return;
+                }
+
+                final int N = mExcessivePower.size();
+                out.writeInt(N);
+                for (int i=0; i<N; i++) {
+                    ExcessivePower ew = mExcessivePower.get(i);
+                    out.writeInt(ew.type);
+                    out.writeLong(ew.overTime);
+                    out.writeLong(ew.usedTime);
+                }
+            }
+
+            void readExcessivePowerFromParcelLocked(Parcel in) {
+                final int N = in.readInt();
+                if (N == 0) {
+                    mExcessivePower = null;
+                    return;
+                }
+
+                if (N > 10000) {
+                    throw new ParcelFormatException(
+                            "File corrupt: too many excessive power entries " + N);
+                }
+
+                mExcessivePower = new ArrayList<>();
+                for (int i=0; i<N; i++) {
+                    ExcessivePower ew = new ExcessivePower();
+                    ew.type = in.readInt();
+                    ew.overTime = in.readLong();
+                    ew.usedTime = in.readLong();
+                    mExcessivePower.add(ew);
+                }
+            }
+
+            void writeToParcelLocked(Parcel out) {
+                out.writeLong(mUserTime);
+                out.writeLong(mSystemTime);
+                out.writeLong(mForegroundTime);
+                out.writeInt(mStarts);
+                out.writeInt(mNumCrashes);
+                out.writeInt(mNumAnrs);
+                out.writeLong(mLoadedUserTime);
+                out.writeLong(mLoadedSystemTime);
+                out.writeLong(mLoadedForegroundTime);
+                out.writeInt(mLoadedStarts);
+                out.writeInt(mLoadedNumCrashes);
+                out.writeInt(mLoadedNumAnrs);
+                out.writeLong(mUnpluggedUserTime);
+                out.writeLong(mUnpluggedSystemTime);
+                out.writeLong(mUnpluggedForegroundTime);
+                out.writeInt(mUnpluggedStarts);
+                out.writeInt(mUnpluggedNumCrashes);
+                out.writeInt(mUnpluggedNumAnrs);
+                writeExcessivePowerToParcelLocked(out);
+            }
+
+            void readFromParcelLocked(Parcel in) {
+                mUserTime = in.readLong();
+                mSystemTime = in.readLong();
+                mForegroundTime = in.readLong();
+                mStarts = in.readInt();
+                mNumCrashes = in.readInt();
+                mNumAnrs = in.readInt();
+                mLoadedUserTime = in.readLong();
+                mLoadedSystemTime = in.readLong();
+                mLoadedForegroundTime = in.readLong();
+                mLoadedStarts = in.readInt();
+                mLoadedNumCrashes = in.readInt();
+                mLoadedNumAnrs = in.readInt();
+                mUnpluggedUserTime = in.readLong();
+                mUnpluggedSystemTime = in.readLong();
+                mUnpluggedForegroundTime = in.readLong();
+                mUnpluggedStarts = in.readInt();
+                mUnpluggedNumCrashes = in.readInt();
+                mUnpluggedNumAnrs = in.readInt();
+                readExcessivePowerFromParcelLocked(in);
+            }
+
+            public void addCpuTimeLocked(int utime, int stime) {
+                mUserTime += utime;
+                mSystemTime += stime;
+            }
+
+            public void addForegroundTimeLocked(long ttime) {
+                mForegroundTime += ttime;
+            }
+
+            public void incStartsLocked() {
+                mStarts++;
+            }
+
+            public void incNumCrashesLocked() {
+                mNumCrashes++;
+            }
+
+            public void incNumAnrsLocked() {
+                mNumAnrs++;
+            }
+
+            @Override
+            public boolean isActive() {
+                return mActive;
+            }
+
+            @Override
+            public long getUserTime(int which) {
+                long val = mUserTime;
+                if (which == STATS_CURRENT) {
+                    val -= mLoadedUserTime;
+                } else if (which == STATS_SINCE_UNPLUGGED) {
+                    val -= mUnpluggedUserTime;
+                }
+                return val;
+            }
+
+            @Override
+            public long getSystemTime(int which) {
+                long val = mSystemTime;
+                if (which == STATS_CURRENT) {
+                    val -= mLoadedSystemTime;
+                } else if (which == STATS_SINCE_UNPLUGGED) {
+                    val -= mUnpluggedSystemTime;
+                }
+                return val;
+            }
+
+            @Override
+            public long getForegroundTime(int which) {
+                long val = mForegroundTime;
+                if (which == STATS_CURRENT) {
+                    val -= mLoadedForegroundTime;
+                } else if (which == STATS_SINCE_UNPLUGGED) {
+                    val -= mUnpluggedForegroundTime;
+                }
+                return val;
+            }
+
+            @Override
+            public int getStarts(int which) {
+                int val = mStarts;
+                if (which == STATS_CURRENT) {
+                    val -= mLoadedStarts;
+                } else if (which == STATS_SINCE_UNPLUGGED) {
+                    val -= mUnpluggedStarts;
+                }
+                return val;
+            }
+
+            @Override
+            public int getNumCrashes(int which) {
+                int val = mNumCrashes;
+                if (which == STATS_CURRENT) {
+                    val -= mLoadedNumCrashes;
+                } else if (which == STATS_SINCE_UNPLUGGED) {
+                    val -= mUnpluggedNumCrashes;
+                }
+                return val;
+            }
+
+            @Override
+            public int getNumAnrs(int which) {
+                int val = mNumAnrs;
+                if (which == STATS_CURRENT) {
+                    val -= mLoadedNumAnrs;
+                } else if (which == STATS_SINCE_UNPLUGGED) {
+                    val -= mUnpluggedNumAnrs;
+                }
+                return val;
+            }
+        }
+
+        /**
+         * The statistics associated with a particular package.
+         */
+        public static class Pkg extends BatteryStats.Uid.Pkg implements TimeBaseObs {
+            /**
+             * BatteryStatsImpl that we are associated with.
+             */
+            protected BatteryStatsImpl mBsi;
+
+            /**
+             * Number of times wakeup alarms have occurred for this app.
+             * On screen-off timebase starting in report v25.
+             */
+            ArrayMap<String, Counter> mWakeupAlarms = new ArrayMap<>();
+
+            /**
+             * The statics we have collected for this package's services.
+             */
+            final ArrayMap<String, Serv> mServiceStats = new ArrayMap<>();
+
+            public Pkg(BatteryStatsImpl bsi) {
+                mBsi = bsi;
+                mBsi.mOnBatteryScreenOffTimeBase.add(this);
+            }
+
+            public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            }
+
+            public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
+            }
+
+            void detach() {
+                mBsi.mOnBatteryScreenOffTimeBase.remove(this);
+            }
+
+            void readFromParcelLocked(Parcel in) {
+                int numWA = in.readInt();
+                mWakeupAlarms.clear();
+                for (int i=0; i<numWA; i++) {
+                    String tag = in.readString();
+                    mWakeupAlarms.put(tag, new Counter(mBsi.mOnBatteryScreenOffTimeBase, in));
+                }
+
+                int numServs = in.readInt();
+                mServiceStats.clear();
+                for (int m = 0; m < numServs; m++) {
+                    String serviceName = in.readString();
+                    Uid.Pkg.Serv serv = new Serv(mBsi);
+                    mServiceStats.put(serviceName, serv);
+
+                    serv.readFromParcelLocked(in);
+                }
+            }
+
+            void writeToParcelLocked(Parcel out) {
+                int numWA = mWakeupAlarms.size();
+                out.writeInt(numWA);
+                for (int i=0; i<numWA; i++) {
+                    out.writeString(mWakeupAlarms.keyAt(i));
+                    mWakeupAlarms.valueAt(i).writeToParcel(out);
+                }
+
+                final int NS = mServiceStats.size();
+                out.writeInt(NS);
+                for (int i=0; i<NS; i++) {
+                    out.writeString(mServiceStats.keyAt(i));
+                    Uid.Pkg.Serv serv = mServiceStats.valueAt(i);
+                    serv.writeToParcelLocked(out);
+                }
+            }
+
+            @Override
+            public ArrayMap<String, ? extends BatteryStats.Counter> getWakeupAlarmStats() {
+                return mWakeupAlarms;
+            }
+
+            public void noteWakeupAlarmLocked(String tag) {
+                Counter c = mWakeupAlarms.get(tag);
+                if (c == null) {
+                    c = new Counter(mBsi.mOnBatteryScreenOffTimeBase);
+                    mWakeupAlarms.put(tag, c);
+                }
+                c.stepAtomic();
+            }
+
+            @Override
+            public ArrayMap<String, ? extends BatteryStats.Uid.Pkg.Serv> getServiceStats() {
+                return mServiceStats;
+            }
+
+            /**
+             * The statistics associated with a particular service.
+             */
+            public static class Serv extends BatteryStats.Uid.Pkg.Serv implements TimeBaseObs {
+                /**
+                 * BatteryStatsImpl that we are associated with.
+                 */
+                protected BatteryStatsImpl mBsi;
+
+                /**
+                 * The android package in which this service resides.
+                 */
+                protected Pkg mPkg;
+
+                /**
+                 * Total time (ms in battery uptime) the service has been left started.
+                 */
+                protected long mStartTime;
+
+                /**
+                 * If service has been started and not yet stopped, this is
+                 * when it was started.
+                 */
+                protected long mRunningSince;
+
+                /**
+                 * True if we are currently running.
+                 */
+                protected boolean mRunning;
+
+                /**
+                 * Total number of times startService() has been called.
+                 */
+                protected int mStarts;
+
+                /**
+                 * Total time (ms in battery uptime) the service has been left launched.
+                 */
+                protected long mLaunchedTime;
+
+                /**
+                 * If service has been launched and not yet exited, this is
+                 * when it was launched (ms in battery uptime).
+                 */
+                protected long mLaunchedSince;
+
+                /**
+                 * True if we are currently launched.
+                 */
+                protected boolean mLaunched;
+
+                /**
+                 * Total number times the service has been launched.
+                 */
+                protected int mLaunches;
+
+                /**
+                 * The amount of time spent started loaded from a previous save
+                 * (ms in battery uptime).
+                 */
+                protected long mLoadedStartTime;
+
+                /**
+                 * The number of starts loaded from a previous save.
+                 */
+                protected int mLoadedStarts;
+
+                /**
+                 * The number of launches loaded from a previous save.
+                 */
+                protected int mLoadedLaunches;
+
+                /**
+                 * The amount of time spent started as of the last run (ms
+                 * in battery uptime).
+                 */
+                protected long mLastStartTime;
+
+                /**
+                 * The number of starts as of the last run.
+                 */
+                protected int mLastStarts;
+
+                /**
+                 * The number of launches as of the last run.
+                 */
+                protected int mLastLaunches;
+
+                /**
+                 * The amount of time spent started when last unplugged (ms
+                 * in battery uptime).
+                 */
+                protected long mUnpluggedStartTime;
+
+                /**
+                 * The number of starts when last unplugged.
+                 */
+                protected int mUnpluggedStarts;
+
+                /**
+                 * The number of launches when last unplugged.
+                 */
+                protected int mUnpluggedLaunches;
+
+                /**
+                 * Construct a Serv. Also adds it to the on-battery time base as a listener.
+                 */
+                public Serv(BatteryStatsImpl bsi) {
+                    mBsi = bsi;
+                    mBsi.mOnBatteryTimeBase.add(this);
+                }
+
+                public void onTimeStarted(long elapsedRealtime, long baseUptime,
+                        long baseRealtime) {
+                    mUnpluggedStartTime = getStartTimeToNowLocked(baseUptime);
+                    mUnpluggedStarts = mStarts;
+                    mUnpluggedLaunches = mLaunches;
+                }
+
+                public void onTimeStopped(long elapsedRealtime, long baseUptime,
+                        long baseRealtime) {
+                }
+
+                /**
+                 * Remove this Serv as a listener from the time base.
+                 */
+                public void detach() {
+                    mBsi.mOnBatteryTimeBase.remove(this);
+                }
+
+                public void readFromParcelLocked(Parcel in) {
+                    mStartTime = in.readLong();
+                    mRunningSince = in.readLong();
+                    mRunning = in.readInt() != 0;
+                    mStarts = in.readInt();
+                    mLaunchedTime = in.readLong();
+                    mLaunchedSince = in.readLong();
+                    mLaunched = in.readInt() != 0;
+                    mLaunches = in.readInt();
+                    mLoadedStartTime = in.readLong();
+                    mLoadedStarts = in.readInt();
+                    mLoadedLaunches = in.readInt();
+                    mLastStartTime = 0;
+                    mLastStarts = 0;
+                    mLastLaunches = 0;
+                    mUnpluggedStartTime = in.readLong();
+                    mUnpluggedStarts = in.readInt();
+                    mUnpluggedLaunches = in.readInt();
+                }
+
+                public void writeToParcelLocked(Parcel out) {
+                    out.writeLong(mStartTime);
+                    out.writeLong(mRunningSince);
+                    out.writeInt(mRunning ? 1 : 0);
+                    out.writeInt(mStarts);
+                    out.writeLong(mLaunchedTime);
+                    out.writeLong(mLaunchedSince);
+                    out.writeInt(mLaunched ? 1 : 0);
+                    out.writeInt(mLaunches);
+                    out.writeLong(mLoadedStartTime);
+                    out.writeInt(mLoadedStarts);
+                    out.writeInt(mLoadedLaunches);
+                    out.writeLong(mUnpluggedStartTime);
+                    out.writeInt(mUnpluggedStarts);
+                    out.writeInt(mUnpluggedLaunches);
+                }
+
+                public long getLaunchTimeToNowLocked(long batteryUptime) {
+                    if (!mLaunched) return mLaunchedTime;
+                    return mLaunchedTime + batteryUptime - mLaunchedSince;
+                }
+
+                public long getStartTimeToNowLocked(long batteryUptime) {
+                    if (!mRunning) return mStartTime;
+                    return mStartTime + batteryUptime - mRunningSince;
+                }
+
+                public void startLaunchedLocked() {
+                    if (!mLaunched) {
+                        mLaunches++;
+                        mLaunchedSince = mBsi.getBatteryUptimeLocked();
+                        mLaunched = true;
+                    }
+                }
+
+                public void stopLaunchedLocked() {
+                    if (mLaunched) {
+                        long time = mBsi.getBatteryUptimeLocked() - mLaunchedSince;
+                        if (time > 0) {
+                            mLaunchedTime += time;
+                        } else {
+                            mLaunches--;
+                        }
+                        mLaunched = false;
+                    }
+                }
+
+                public void startRunningLocked() {
+                    if (!mRunning) {
+                        mStarts++;
+                        mRunningSince = mBsi.getBatteryUptimeLocked();
+                        mRunning = true;
+                    }
+                }
+
+                public void stopRunningLocked() {
+                    if (mRunning) {
+                        long time = mBsi.getBatteryUptimeLocked() - mRunningSince;
+                        if (time > 0) {
+                            mStartTime += time;
+                        } else {
+                            mStarts--;
+                        }
+                        mRunning = false;
+                    }
+                }
+
+                public BatteryStatsImpl getBatteryStats() {
+                    return mBsi;
+                }
+
+                @Override
+                public int getLaunches(int which) {
+                    int val = mLaunches;
+                    if (which == STATS_CURRENT) {
+                        val -= mLoadedLaunches;
+                    } else if (which == STATS_SINCE_UNPLUGGED) {
+                        val -= mUnpluggedLaunches;
+                    }
+                    return val;
+                }
+
+                @Override
+                public long getStartTime(long now, int which) {
+                    long val = getStartTimeToNowLocked(now);
+                    if (which == STATS_CURRENT) {
+                        val -= mLoadedStartTime;
+                    } else if (which == STATS_SINCE_UNPLUGGED) {
+                        val -= mUnpluggedStartTime;
+                    }
+                    return val;
+                }
+
+                @Override
+                public int getStarts(int which) {
+                    int val = mStarts;
+                    if (which == STATS_CURRENT) {
+                        val -= mLoadedStarts;
+                    } else if (which == STATS_SINCE_UNPLUGGED) {
+                        val -= mUnpluggedStarts;
+                    }
+
+                    return val;
+                }
+            }
+
+            final Serv newServiceStatsLocked() {
+                return new Serv(mBsi);
+            }
+        }
+
+        /**
+         * Retrieve the statistics object for a particular process, creating
+         * if needed.
+         */
+        public Proc getProcessStatsLocked(String name) {
+            Proc ps = mProcessStats.get(name);
+            if (ps == null) {
+                ps = new Proc(mBsi, name);
+                mProcessStats.put(name, ps);
+            }
+
+            return ps;
+        }
+
+        public void updateUidProcessStateLocked(int procState) {
+            int uidRunningState;
+            // Make special note of Foreground Services
+            final boolean userAwareService =
+                    (procState == ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+            if (procState == ActivityManager.PROCESS_STATE_NONEXISTENT) {
+                uidRunningState = ActivityManager.PROCESS_STATE_NONEXISTENT;
+            } else if (procState == ActivityManager.PROCESS_STATE_TOP) {
+                uidRunningState = PROCESS_STATE_TOP;
+            } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+                // Persistent and other foreground states go here.
+                uidRunningState = PROCESS_STATE_FOREGROUND_SERVICE;
+            } else if (procState <= ActivityManager.PROCESS_STATE_TOP_SLEEPING) {
+                uidRunningState = PROCESS_STATE_TOP_SLEEPING;
+            } else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
+                // Persistent and other foreground states go here.
+                uidRunningState = PROCESS_STATE_FOREGROUND;
+            } else if (procState <= ActivityManager.PROCESS_STATE_RECEIVER) {
+                uidRunningState = PROCESS_STATE_BACKGROUND;
+            } else {
+                uidRunningState = PROCESS_STATE_CACHED;
+            }
+
+            if (mProcessState == uidRunningState && userAwareService == mInForegroundService) {
+                return;
+            }
+
+            final long elapsedRealtimeMs = mBsi.mClocks.elapsedRealtime();
+            if (mProcessState != uidRunningState) {
+                final long uptimeMs = mBsi.mClocks.uptimeMillis();
+
+                if (mProcessState != ActivityManager.PROCESS_STATE_NONEXISTENT) {
+                    mProcessStateTimer[mProcessState].stopRunningLocked(elapsedRealtimeMs);
+                }
+                mProcessState = uidRunningState;
+                if (uidRunningState != ActivityManager.PROCESS_STATE_NONEXISTENT) {
+                    if (mProcessStateTimer[uidRunningState] == null) {
+                        makeProcessState(uidRunningState, null);
+                    }
+                    mProcessStateTimer[uidRunningState].startRunningLocked(elapsedRealtimeMs);
+                }
+
+                updateOnBatteryBgTimeBase(uptimeMs * 1000, elapsedRealtimeMs * 1000);
+                updateOnBatteryScreenOffBgTimeBase(uptimeMs * 1000, elapsedRealtimeMs * 1000);
+            }
+
+            if (userAwareService != mInForegroundService) {
+                if (userAwareService) {
+                    noteForegroundServiceResumedLocked(elapsedRealtimeMs);
+                } else {
+                    noteForegroundServicePausedLocked(elapsedRealtimeMs);
+                }
+                mInForegroundService = userAwareService;
+            }
+        }
+
+        /** Whether to consider Uid to be in the background for background timebase purposes. */
+        public boolean isInBackground() {
+            // Note that PROCESS_STATE_CACHED and ActivityManager.PROCESS_STATE_NONEXISTENT is
+            // also considered to be 'background' for our purposes, because it's not foreground.
+            return mProcessState >= PROCESS_STATE_BACKGROUND;
+        }
+
+        public boolean updateOnBatteryBgTimeBase(long uptimeUs, long realtimeUs) {
+            boolean on = mBsi.mOnBatteryTimeBase.isRunning() && isInBackground();
+            return mOnBatteryBackgroundTimeBase.setRunning(on, uptimeUs, realtimeUs);
+        }
+
+        public boolean updateOnBatteryScreenOffBgTimeBase(long uptimeUs, long realtimeUs) {
+            boolean on = mBsi.mOnBatteryScreenOffTimeBase.isRunning() && isInBackground();
+            return mOnBatteryScreenOffBackgroundTimeBase.setRunning(on, uptimeUs, realtimeUs);
+        }
+
+        public SparseArray<? extends Pid> getPidStats() {
+            return mPids;
+        }
+
+        public Pid getPidStatsLocked(int pid) {
+            Pid p = mPids.get(pid);
+            if (p == null) {
+                p = new Pid();
+                mPids.put(pid, p);
+            }
+            return p;
+        }
+
+        /**
+         * Retrieve the statistics object for a particular service, creating
+         * if needed.
+         */
+        public Pkg getPackageStatsLocked(String name) {
+            Pkg ps = mPackageStats.get(name);
+            if (ps == null) {
+                ps = new Pkg(mBsi);
+                mPackageStats.put(name, ps);
+            }
+
+            return ps;
+        }
+
+        /**
+         * Retrieve the statistics object for a particular service, creating
+         * if needed.
+         */
+        public Pkg.Serv getServiceStatsLocked(String pkg, String serv) {
+            Pkg ps = getPackageStatsLocked(pkg);
+            Pkg.Serv ss = ps.mServiceStats.get(serv);
+            if (ss == null) {
+                ss = ps.newServiceStatsLocked();
+                ps.mServiceStats.put(serv, ss);
+            }
+
+            return ss;
+        }
+
+        public void readSyncSummaryFromParcelLocked(String name, Parcel in) {
+            DualTimer timer = mSyncStats.instantiateObject();
+            timer.readSummaryFromParcelLocked(in);
+            mSyncStats.add(name, timer);
+        }
+
+        public void readJobSummaryFromParcelLocked(String name, Parcel in) {
+            DualTimer timer = mJobStats.instantiateObject();
+            timer.readSummaryFromParcelLocked(in);
+            mJobStats.add(name, timer);
+        }
+
+        public void readWakeSummaryFromParcelLocked(String wlName, Parcel in) {
+            Wakelock wl = new Wakelock(mBsi, this);
+            mWakelockStats.add(wlName, wl);
+            if (in.readInt() != 0) {
+                getWakelockTimerLocked(wl, WAKE_TYPE_FULL).readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                getWakelockTimerLocked(wl, WAKE_TYPE_PARTIAL).readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                getWakelockTimerLocked(wl, WAKE_TYPE_WINDOW).readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                getWakelockTimerLocked(wl, WAKE_TYPE_DRAW).readSummaryFromParcelLocked(in);
+            }
+        }
+
+        public DualTimer getSensorTimerLocked(int sensor, boolean create) {
+            Sensor se = mSensorStats.get(sensor);
+            if (se == null) {
+                if (!create) {
+                    return null;
+                }
+                se = new Sensor(mBsi, this, sensor);
+                mSensorStats.put(sensor, se);
+            }
+            DualTimer t = se.mTimer;
+            if (t != null) {
+                return t;
+            }
+            ArrayList<StopwatchTimer> timers = mBsi.mSensorTimers.get(sensor);
+            if (timers == null) {
+                timers = new ArrayList<StopwatchTimer>();
+                mBsi.mSensorTimers.put(sensor, timers);
+            }
+            t = new DualTimer(mBsi.mClocks, this, BatteryStats.SENSOR, timers,
+                    mBsi.mOnBatteryTimeBase, mOnBatteryBackgroundTimeBase);
+            se.mTimer = t;
+            return t;
+        }
+
+        public void noteStartSyncLocked(String name, long elapsedRealtimeMs) {
+            DualTimer t = mSyncStats.startObject(name);
+            if (t != null) {
+                t.startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteStopSyncLocked(String name, long elapsedRealtimeMs) {
+            DualTimer t = mSyncStats.stopObject(name);
+            if (t != null) {
+                t.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteStartJobLocked(String name, long elapsedRealtimeMs) {
+            DualTimer t = mJobStats.startObject(name);
+            if (t != null) {
+                t.startRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteStopJobLocked(String name, long elapsedRealtimeMs, int stopReason) {
+            DualTimer t = mJobStats.stopObject(name);
+            if (t != null) {
+                t.stopRunningLocked(elapsedRealtimeMs);
+            }
+            if (mBsi.mOnBatteryTimeBase.isRunning()) {
+                SparseIntArray types = mJobCompletions.get(name);
+                if (types == null) {
+                    types = new SparseIntArray();
+                    mJobCompletions.put(name, types);
+                }
+                int last = types.get(stopReason, 0);
+                types.put(stopReason, last + 1);
+            }
+        }
+
+        public StopwatchTimer getWakelockTimerLocked(Wakelock wl, int type) {
+            if (wl == null) {
+                return null;
+            }
+            switch (type) {
+                case WAKE_TYPE_PARTIAL: {
+                    DualTimer t = wl.mTimerPartial;
+                    if (t == null) {
+                        t = new DualTimer(mBsi.mClocks, this, WAKE_TYPE_PARTIAL,
+                                mBsi.mPartialTimers, mBsi.mOnBatteryScreenOffTimeBase,
+                                mOnBatteryScreenOffBackgroundTimeBase);
+                        wl.mTimerPartial = t;
+                    }
+                    return t;
+                }
+                case WAKE_TYPE_FULL: {
+                    StopwatchTimer t = wl.mTimerFull;
+                    if (t == null) {
+                        t = new StopwatchTimer(mBsi.mClocks, this, WAKE_TYPE_FULL,
+                                mBsi.mFullTimers, mBsi.mOnBatteryTimeBase);
+                        wl.mTimerFull = t;
+                    }
+                    return t;
+                }
+                case WAKE_TYPE_WINDOW: {
+                    StopwatchTimer t = wl.mTimerWindow;
+                    if (t == null) {
+                        t = new StopwatchTimer(mBsi.mClocks, this, WAKE_TYPE_WINDOW,
+                                mBsi.mWindowTimers, mBsi.mOnBatteryTimeBase);
+                        wl.mTimerWindow = t;
+                    }
+                    return t;
+                }
+                case WAKE_TYPE_DRAW: {
+                    StopwatchTimer t = wl.mTimerDraw;
+                    if (t == null) {
+                        t = new StopwatchTimer(mBsi.mClocks, this, WAKE_TYPE_DRAW,
+                                mBsi.mDrawTimers, mBsi.mOnBatteryTimeBase);
+                        wl.mTimerDraw = t;
+                    }
+                    return t;
+                }
+                default:
+                    throw new IllegalArgumentException("type=" + type);
+            }
+        }
+
+        public void noteStartWakeLocked(int pid, String name, int type, long elapsedRealtimeMs) {
+            Wakelock wl = mWakelockStats.startObject(name);
+            if (wl != null) {
+                getWakelockTimerLocked(wl, type).startRunningLocked(elapsedRealtimeMs);
+            }
+            if (type == WAKE_TYPE_PARTIAL) {
+                createAggregatedPartialWakelockTimerLocked().startRunningLocked(elapsedRealtimeMs);
+                if (pid >= 0) {
+                    Pid p = getPidStatsLocked(pid);
+                    if (p.mWakeNesting++ == 0) {
+                        p.mWakeStartMs = elapsedRealtimeMs;
+                    }
+                }
+            }
+        }
+
+        public void noteStopWakeLocked(int pid, String name, int type, long elapsedRealtimeMs) {
+            Wakelock wl = mWakelockStats.stopObject(name);
+            if (wl != null) {
+                getWakelockTimerLocked(wl, type).stopRunningLocked(elapsedRealtimeMs);
+            }
+            if (type == WAKE_TYPE_PARTIAL) {
+                if (mAggregatedPartialWakelockTimer != null) {
+                    mAggregatedPartialWakelockTimer.stopRunningLocked(elapsedRealtimeMs);
+                }
+                if (pid >= 0) {
+                    Pid p = mPids.get(pid);
+                    if (p != null && p.mWakeNesting > 0) {
+                        if (p.mWakeNesting-- == 1) {
+                            p.mWakeSumMs += elapsedRealtimeMs - p.mWakeStartMs;
+                            p.mWakeStartMs = 0;
+                        }
+                    }
+                }
+            }
+        }
+
+        public void reportExcessiveCpuLocked(String proc, long overTime, long usedTime) {
+            Proc p = getProcessStatsLocked(proc);
+            if (p != null) {
+                p.addExcessiveCpu(overTime, usedTime);
+            }
+        }
+
+        public void noteStartSensor(int sensor, long elapsedRealtimeMs) {
+            DualTimer t = getSensorTimerLocked(sensor, /* create= */ true);
+            t.startRunningLocked(elapsedRealtimeMs);
+        }
+
+        public void noteStopSensor(int sensor, long elapsedRealtimeMs) {
+            // Don't create a timer if one doesn't already exist
+            DualTimer t = getSensorTimerLocked(sensor, false);
+            if (t != null) {
+                t.stopRunningLocked(elapsedRealtimeMs);
+            }
+        }
+
+        public void noteStartGps(long elapsedRealtimeMs) {
+            noteStartSensor(Sensor.GPS, elapsedRealtimeMs);
+        }
+
+        public void noteStopGps(long elapsedRealtimeMs) {
+            noteStopSensor(Sensor.GPS, elapsedRealtimeMs);
+        }
+
+        public BatteryStatsImpl getBatteryStats() {
+            return mBsi;
+        }
+    }
+
+    public long[] getCpuFreqs() {
+        return mCpuFreqs;
+    }
+
+    public BatteryStatsImpl(File systemDir, Handler handler, PlatformIdleStateCallback cb,
+            UserInfoProvider userInfoProvider) {
+        this(new SystemClocks(), systemDir, handler, cb, userInfoProvider);
+    }
+
+    private BatteryStatsImpl(Clocks clocks, File systemDir, Handler handler,
+            PlatformIdleStateCallback cb,
+            UserInfoProvider userInfoProvider) {
+        init(clocks);
+
+        if (systemDir != null) {
+            mFile = new JournaledFile(new File(systemDir, "batterystats.bin"),
+                    new File(systemDir, "batterystats.bin.tmp"));
+        } else {
+            mFile = null;
+        }
+        mCheckinFile = new AtomicFile(new File(systemDir, "batterystats-checkin.bin"));
+        mDailyFile = new AtomicFile(new File(systemDir, "batterystats-daily.xml"));
+        mHandler = new MyHandler(handler.getLooper());
+        mStartCount++;
+        mScreenOnTimer = new StopwatchTimer(mClocks, null, -1, null, mOnBatteryTimeBase);
+        for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            mScreenBrightnessTimer[i] = new StopwatchTimer(mClocks, null, -100-i, null,
+                    mOnBatteryTimeBase);
+        }
+        mInteractiveTimer = new StopwatchTimer(mClocks, null, -10, null, mOnBatteryTimeBase);
+        mPowerSaveModeEnabledTimer = new StopwatchTimer(mClocks, null, -2, null,
+                mOnBatteryTimeBase);
+        mDeviceIdleModeLightTimer = new StopwatchTimer(mClocks, null, -11, null,
+                mOnBatteryTimeBase);
+        mDeviceIdleModeFullTimer = new StopwatchTimer(mClocks, null, -14, null, mOnBatteryTimeBase);
+        mDeviceLightIdlingTimer = new StopwatchTimer(mClocks, null, -15, null, mOnBatteryTimeBase);
+        mDeviceIdlingTimer = new StopwatchTimer(mClocks, null, -12, null, mOnBatteryTimeBase);
+        mPhoneOnTimer = new StopwatchTimer(mClocks, null, -3, null, mOnBatteryTimeBase);
+        for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(mClocks, null, -200-i, null,
+                    mOnBatteryTimeBase);
+        }
+        mPhoneSignalScanningTimer = new StopwatchTimer(mClocks, null, -200+1, null,
+                mOnBatteryTimeBase);
+        for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+            mPhoneDataConnectionsTimer[i] = new StopwatchTimer(mClocks, null, -300-i, null,
+                    mOnBatteryTimeBase);
+        }
+        for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+            mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase);
+            mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase);
+        }
+        mWifiActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase, NUM_WIFI_TX_LEVELS);
+        mBluetoothActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
+                NUM_BT_TX_LEVELS);
+        mModemActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
+                ModemActivityInfo.TX_POWER_LEVELS);
+        mMobileRadioActiveTimer = new StopwatchTimer(mClocks, null, -400, null, mOnBatteryTimeBase);
+        mMobileRadioActivePerAppTimer = new StopwatchTimer(mClocks, null, -401, null,
+                mOnBatteryTimeBase);
+        mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase);
+        mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase);
+        mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase);
+        mWifiOnTimer = new StopwatchTimer(mClocks, null, -4, null, mOnBatteryTimeBase);
+        mGlobalWifiRunningTimer = new StopwatchTimer(mClocks, null, -5, null, mOnBatteryTimeBase);
+        for (int i=0; i<NUM_WIFI_STATES; i++) {
+            mWifiStateTimer[i] = new StopwatchTimer(mClocks, null, -600-i, null,
+                    mOnBatteryTimeBase);
+        }
+        for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+            mWifiSupplStateTimer[i] = new StopwatchTimer(mClocks, null, -700-i, null,
+                    mOnBatteryTimeBase);
+        }
+        for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            mWifiSignalStrengthsTimer[i] = new StopwatchTimer(mClocks, null, -800-i, null,
+                    mOnBatteryTimeBase);
+        }
+        mAudioOnTimer = new StopwatchTimer(mClocks, null, -7, null, mOnBatteryTimeBase);
+        mVideoOnTimer = new StopwatchTimer(mClocks, null, -8, null, mOnBatteryTimeBase);
+        mFlashlightOnTimer = new StopwatchTimer(mClocks, null, -9, null, mOnBatteryTimeBase);
+        mCameraOnTimer = new StopwatchTimer(mClocks, null, -13, null, mOnBatteryTimeBase);
+        mBluetoothScanTimer = new StopwatchTimer(mClocks, null, -14, null, mOnBatteryTimeBase);
+        mDischargeScreenOffCounter = new LongSamplingCounter(mOnBatteryScreenOffTimeBase);
+        mDischargeCounter = new LongSamplingCounter(mOnBatteryTimeBase);
+        mOnBattery = mOnBatteryInternal = false;
+        long uptime = mClocks.uptimeMillis() * 1000;
+        long realtime = mClocks.elapsedRealtime() * 1000;
+        initTimes(uptime, realtime);
+        mStartPlatformVersion = mEndPlatformVersion = Build.ID;
+        mDischargeStartLevel = 0;
+        mDischargeUnplugLevel = 0;
+        mDischargePlugLevel = -1;
+        mDischargeCurrentLevel = 0;
+        mCurrentBatteryLevel = 0;
+        initDischarge();
+        clearHistoryLocked();
+        updateDailyDeadlineLocked();
+        mPlatformIdleStateCallback = cb;
+        mUserInfoProvider = userInfoProvider;
+    }
+
+    public BatteryStatsImpl(Parcel p) {
+        this(new SystemClocks(), p);
+    }
+
+    public BatteryStatsImpl(Clocks clocks, Parcel p) {
+        init(clocks);
+        mFile = null;
+        mCheckinFile = null;
+        mDailyFile = null;
+        mHandler = null;
+        mExternalSync = null;
+        clearHistoryLocked();
+        readFromParcel(p);
+        mPlatformIdleStateCallback = null;
+    }
+
+    public void setPowerProfileLocked(PowerProfile profile) {
+        mPowerProfile = profile;
+
+        // We need to initialize the KernelCpuSpeedReaders to read from
+        // the first cpu of each core. Once we have the PowerProfile, we have access to this
+        // information.
+        final int numClusters = mPowerProfile.getNumCpuClusters();
+        mKernelCpuSpeedReaders = new KernelCpuSpeedReader[numClusters];
+        int firstCpuOfCluster = 0;
+        for (int i = 0; i < numClusters; i++) {
+            final int numSpeedSteps = mPowerProfile.getNumSpeedStepsInCpuCluster(i);
+            mKernelCpuSpeedReaders[i] = new KernelCpuSpeedReader(firstCpuOfCluster,
+                    numSpeedSteps);
+            firstCpuOfCluster += mPowerProfile.getNumCoresInCpuCluster(i);
+        }
+
+        if (mEstimatedBatteryCapacity == -1) {
+            // Initialize the estimated battery capacity to a known preset one.
+            mEstimatedBatteryCapacity = (int) mPowerProfile.getBatteryCapacity();
+        }
+    }
+
+    public void setCallback(BatteryCallback cb) {
+        mCallback = cb;
+    }
+
+    public void setRadioScanningTimeoutLocked(long timeout) {
+        if (mPhoneSignalScanningTimer != null) {
+            mPhoneSignalScanningTimer.setTimeout(timeout);
+        }
+    }
+
+    public void setExternalStatsSyncLocked(ExternalStatsSync sync) {
+        mExternalSync = sync;
+    }
+
+    public void updateDailyDeadlineLocked() {
+        // Get the current time.
+        long currentTime = mDailyStartTime = System.currentTimeMillis();
+        Calendar calDeadline = Calendar.getInstance();
+        calDeadline.setTimeInMillis(currentTime);
+
+        // Move time up to the next day, ranging from 1am to 3pm.
+        calDeadline.set(Calendar.DAY_OF_YEAR, calDeadline.get(Calendar.DAY_OF_YEAR) + 1);
+        calDeadline.set(Calendar.MILLISECOND, 0);
+        calDeadline.set(Calendar.SECOND, 0);
+        calDeadline.set(Calendar.MINUTE, 0);
+        calDeadline.set(Calendar.HOUR_OF_DAY, 1);
+        mNextMinDailyDeadline = calDeadline.getTimeInMillis();
+        calDeadline.set(Calendar.HOUR_OF_DAY, 3);
+        mNextMaxDailyDeadline = calDeadline.getTimeInMillis();
+    }
+
+    public void recordDailyStatsIfNeededLocked(boolean settled) {
+        long currentTime = System.currentTimeMillis();
+        if (currentTime >= mNextMaxDailyDeadline) {
+            recordDailyStatsLocked();
+        } else if (settled && currentTime >= mNextMinDailyDeadline) {
+            recordDailyStatsLocked();
+        } else if (currentTime < (mDailyStartTime-(1000*60*60*24))) {
+            recordDailyStatsLocked();
+        }
+    }
+
+    public void recordDailyStatsLocked() {
+        DailyItem item = new DailyItem();
+        item.mStartTime = mDailyStartTime;
+        item.mEndTime = System.currentTimeMillis();
+        boolean hasData = false;
+        if (mDailyDischargeStepTracker.mNumStepDurations > 0) {
+            hasData = true;
+            item.mDischargeSteps = new LevelStepTracker(
+                    mDailyDischargeStepTracker.mNumStepDurations,
+                    mDailyDischargeStepTracker.mStepDurations);
+        }
+        if (mDailyChargeStepTracker.mNumStepDurations > 0) {
+            hasData = true;
+            item.mChargeSteps = new LevelStepTracker(
+                    mDailyChargeStepTracker.mNumStepDurations,
+                    mDailyChargeStepTracker.mStepDurations);
+        }
+        if (mDailyPackageChanges != null) {
+            hasData = true;
+            item.mPackageChanges = mDailyPackageChanges;
+            mDailyPackageChanges = null;
+        }
+        mDailyDischargeStepTracker.init();
+        mDailyChargeStepTracker.init();
+        updateDailyDeadlineLocked();
+
+        if (hasData) {
+            mDailyItems.add(item);
+            while (mDailyItems.size() > MAX_DAILY_ITEMS) {
+                mDailyItems.remove(0);
+            }
+            final ByteArrayOutputStream memStream = new ByteArrayOutputStream();
+            try {
+                XmlSerializer out = new FastXmlSerializer();
+                out.setOutput(memStream, StandardCharsets.UTF_8.name());
+                writeDailyItemsLocked(out);
+                BackgroundThread.getHandler().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        synchronized (mCheckinFile) {
+                            FileOutputStream stream = null;
+                            try {
+                                stream = mDailyFile.startWrite();
+                                memStream.writeTo(stream);
+                                stream.flush();
+                                FileUtils.sync(stream);
+                                stream.close();
+                                mDailyFile.finishWrite(stream);
+                            } catch (IOException e) {
+                                Slog.w("BatteryStats",
+                                        "Error writing battery daily items", e);
+                                mDailyFile.failWrite(stream);
+                            }
+                        }
+                    }
+                });
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    private void writeDailyItemsLocked(XmlSerializer out) throws IOException {
+        StringBuilder sb = new StringBuilder(64);
+        out.startDocument(null, true);
+        out.startTag(null, "daily-items");
+        for (int i=0; i<mDailyItems.size(); i++) {
+            final DailyItem dit = mDailyItems.get(i);
+            out.startTag(null, "item");
+            out.attribute(null, "start", Long.toString(dit.mStartTime));
+            out.attribute(null, "end", Long.toString(dit.mEndTime));
+            writeDailyLevelSteps(out, "dis", dit.mDischargeSteps, sb);
+            writeDailyLevelSteps(out, "chg", dit.mChargeSteps, sb);
+            if (dit.mPackageChanges != null) {
+                for (int j=0; j<dit.mPackageChanges.size(); j++) {
+                    PackageChange pc = dit.mPackageChanges.get(j);
+                    if (pc.mUpdate) {
+                        out.startTag(null, "upd");
+                        out.attribute(null, "pkg", pc.mPackageName);
+                        out.attribute(null, "ver", Integer.toString(pc.mVersionCode));
+                        out.endTag(null, "upd");
+                    } else {
+                        out.startTag(null, "rem");
+                        out.attribute(null, "pkg", pc.mPackageName);
+                        out.endTag(null, "rem");
+                    }
+                }
+            }
+            out.endTag(null, "item");
+        }
+        out.endTag(null, "daily-items");
+        out.endDocument();
+    }
+
+    private void writeDailyLevelSteps(XmlSerializer out, String tag, LevelStepTracker steps,
+            StringBuilder tmpBuilder) throws IOException {
+        if (steps != null) {
+            out.startTag(null, tag);
+            out.attribute(null, "n", Integer.toString(steps.mNumStepDurations));
+            for (int i=0; i<steps.mNumStepDurations; i++) {
+                out.startTag(null, "s");
+                tmpBuilder.setLength(0);
+                steps.encodeEntryAt(i, tmpBuilder);
+                out.attribute(null, "v", tmpBuilder.toString());
+                out.endTag(null, "s");
+            }
+            out.endTag(null, tag);
+        }
+    }
+
+    public void readDailyStatsLocked() {
+        Slog.d(TAG, "Reading daily items from " + mDailyFile.getBaseFile());
+        mDailyItems.clear();
+        FileInputStream stream;
+        try {
+            stream = mDailyFile.openRead();
+        } catch (FileNotFoundException e) {
+            return;
+        }
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(stream, StandardCharsets.UTF_8.name());
+            readDailyItemsLocked(parser);
+        } catch (XmlPullParserException e) {
+        } finally {
+            try {
+                stream.close();
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    private void readDailyItemsLocked(XmlPullParser parser) {
+        try {
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                ;
+            }
+
+            if (type != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("no start tag found");
+            }
+
+            int outerDepth = parser.getDepth();
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+
+                String tagName = parser.getName();
+                if (tagName.equals("item")) {
+                    readDailyItemTagLocked(parser);
+                } else {
+                    Slog.w(TAG, "Unknown element under <daily-items>: "
+                            + parser.getName());
+                    XmlUtils.skipCurrentTag(parser);
+                }
+            }
+
+        } catch (IllegalStateException e) {
+            Slog.w(TAG, "Failed parsing daily " + e);
+        } catch (NullPointerException e) {
+            Slog.w(TAG, "Failed parsing daily " + e);
+        } catch (NumberFormatException e) {
+            Slog.w(TAG, "Failed parsing daily " + e);
+        } catch (XmlPullParserException e) {
+            Slog.w(TAG, "Failed parsing daily " + e);
+        } catch (IOException e) {
+            Slog.w(TAG, "Failed parsing daily " + e);
+        } catch (IndexOutOfBoundsException e) {
+            Slog.w(TAG, "Failed parsing daily " + e);
+        }
+    }
+
+    void readDailyItemTagLocked(XmlPullParser parser) throws NumberFormatException,
+            XmlPullParserException, IOException {
+        DailyItem dit = new DailyItem();
+        String attr = parser.getAttributeValue(null, "start");
+        if (attr != null) {
+            dit.mStartTime = Long.parseLong(attr);
+        }
+        attr = parser.getAttributeValue(null, "end");
+        if (attr != null) {
+            dit.mEndTime = Long.parseLong(attr);
+        }
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals("dis")) {
+                readDailyItemTagDetailsLocked(parser, dit, false, "dis");
+            } else if (tagName.equals("chg")) {
+                readDailyItemTagDetailsLocked(parser, dit, true, "chg");
+            } else if (tagName.equals("upd")) {
+                if (dit.mPackageChanges == null) {
+                    dit.mPackageChanges = new ArrayList<>();
+                }
+                PackageChange pc = new PackageChange();
+                pc.mUpdate = true;
+                pc.mPackageName = parser.getAttributeValue(null, "pkg");
+                String verStr = parser.getAttributeValue(null, "ver");
+                pc.mVersionCode = verStr != null ? Integer.parseInt(verStr) : 0;
+                dit.mPackageChanges.add(pc);
+                XmlUtils.skipCurrentTag(parser);
+            } else if (tagName.equals("rem")) {
+                if (dit.mPackageChanges == null) {
+                    dit.mPackageChanges = new ArrayList<>();
+                }
+                PackageChange pc = new PackageChange();
+                pc.mUpdate = false;
+                pc.mPackageName = parser.getAttributeValue(null, "pkg");
+                dit.mPackageChanges.add(pc);
+                XmlUtils.skipCurrentTag(parser);
+            } else {
+                Slog.w(TAG, "Unknown element under <item>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        mDailyItems.add(dit);
+    }
+
+    void readDailyItemTagDetailsLocked(XmlPullParser parser, DailyItem dit, boolean isCharge,
+            String tag)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        final String numAttr = parser.getAttributeValue(null, "n");
+        if (numAttr == null) {
+            Slog.w(TAG, "Missing 'n' attribute at " + parser.getPositionDescription());
+            XmlUtils.skipCurrentTag(parser);
+            return;
+        }
+        final int num = Integer.parseInt(numAttr);
+        LevelStepTracker steps = new LevelStepTracker(num);
+        if (isCharge) {
+            dit.mChargeSteps = steps;
+        } else {
+            dit.mDischargeSteps = steps;
+        }
+        int i = 0;
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if ("s".equals(tagName)) {
+                if (i < num) {
+                    String valueAttr = parser.getAttributeValue(null, "v");
+                    if (valueAttr != null) {
+                        steps.decodeEntryAt(i, valueAttr);
+                        i++;
+                    }
+                }
+            } else {
+                Slog.w(TAG, "Unknown element under <" + tag + ">: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        steps.mNumStepDurations = i;
+    }
+
+    @Override
+    public DailyItem getDailyItemLocked(int daysAgo) {
+        int index = mDailyItems.size()-1-daysAgo;
+        return index >= 0 ? mDailyItems.get(index) : null;
+    }
+
+    @Override
+    public long getCurrentDailyStartTime() {
+        return mDailyStartTime;
+    }
+
+    @Override
+    public long getNextMinDailyDeadline() {
+        return mNextMinDailyDeadline;
+    }
+
+    @Override
+    public long getNextMaxDailyDeadline() {
+        return mNextMaxDailyDeadline;
+    }
+
+    @Override
+    public boolean startIteratingOldHistoryLocked() {
+        if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize()
+                + " pos=" + mHistoryBuffer.dataPosition());
+        if ((mHistoryIterator = mHistory) == null) {
+            return false;
+        }
+        mHistoryBuffer.setDataPosition(0);
+        mHistoryReadTmp.clear();
+        mReadOverflow = false;
+        mIteratingHistory = true;
+        return true;
+    }
+
+    @Override
+    public boolean getNextOldHistoryLocked(HistoryItem out) {
+        boolean end = mHistoryBuffer.dataPosition() >= mHistoryBuffer.dataSize();
+        if (!end) {
+            readHistoryDelta(mHistoryBuffer, mHistoryReadTmp);
+            mReadOverflow |= mHistoryReadTmp.cmd == HistoryItem.CMD_OVERFLOW;
+        }
+        HistoryItem cur = mHistoryIterator;
+        if (cur == null) {
+            if (!mReadOverflow && !end) {
+                Slog.w(TAG, "Old history ends before new history!");
+            }
+            return false;
+        }
+        out.setTo(cur);
+        mHistoryIterator = cur.next;
+        if (!mReadOverflow) {
+            if (end) {
+                Slog.w(TAG, "New history ends before old history!");
+            } else if (!out.same(mHistoryReadTmp)) {
+                PrintWriter pw = new FastPrintWriter(new LogWriter(android.util.Log.WARN, TAG));
+                pw.println("Histories differ!");
+                pw.println("Old history:");
+                (new HistoryPrinter()).printNextItem(pw, out, 0, false, true);
+                pw.println("New history:");
+                (new HistoryPrinter()).printNextItem(pw, mHistoryReadTmp, 0, false,
+                        true);
+                pw.flush();
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void finishIteratingOldHistoryLocked() {
+        mIteratingHistory = false;
+        mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
+        mHistoryIterator = null;
+    }
+
+    public int getHistoryTotalSize() {
+        return MAX_HISTORY_BUFFER;
+    }
+
+    public int getHistoryUsedSize() {
+        return mHistoryBuffer.dataSize();
+    }
+
+    @Override
+    public boolean startIteratingHistoryLocked() {
+        if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize()
+                + " pos=" + mHistoryBuffer.dataPosition());
+        if (mHistoryBuffer.dataSize() <= 0) {
+            return false;
+        }
+        mHistoryBuffer.setDataPosition(0);
+        mReadOverflow = false;
+        mIteratingHistory = true;
+        mReadHistoryStrings = new String[mHistoryTagPool.size()];
+        mReadHistoryUids = new int[mHistoryTagPool.size()];
+        mReadHistoryChars = 0;
+        for (HashMap.Entry<HistoryTag, Integer> ent : mHistoryTagPool.entrySet()) {
+            final HistoryTag tag = ent.getKey();
+            final int idx = ent.getValue();
+            mReadHistoryStrings[idx] = tag.string;
+            mReadHistoryUids[idx] = tag.uid;
+            mReadHistoryChars += tag.string.length() + 1;
+        }
+        return true;
+    }
+
+    @Override
+    public int getHistoryStringPoolSize() {
+        return mReadHistoryStrings.length;
+    }
+
+    @Override
+    public int getHistoryStringPoolBytes() {
+        // Each entry is a fixed 12 bytes: 4 for index, 4 for uid, 4 for string size
+        // Each string character is 2 bytes.
+        return (mReadHistoryStrings.length * 12) + (mReadHistoryChars * 2);
+    }
+
+    @Override
+    public String getHistoryTagPoolString(int index) {
+        return mReadHistoryStrings[index];
+    }
+
+    @Override
+    public int getHistoryTagPoolUid(int index) {
+        return mReadHistoryUids[index];
+    }
+
+    @Override
+    public boolean getNextHistoryLocked(HistoryItem out) {
+        final int pos = mHistoryBuffer.dataPosition();
+        if (pos == 0) {
+            out.clear();
+        }
+        boolean end = pos >= mHistoryBuffer.dataSize();
+        if (end) {
+            return false;
+        }
+
+        final long lastRealtime = out.time;
+        final long lastWalltime = out.currentTime;
+        readHistoryDelta(mHistoryBuffer, out);
+        if (out.cmd != HistoryItem.CMD_CURRENT_TIME
+                && out.cmd != HistoryItem.CMD_RESET && lastWalltime != 0) {
+            out.currentTime = lastWalltime + (out.time - lastRealtime);
+        }
+        return true;
+    }
+
+    @Override
+    public void finishIteratingHistoryLocked() {
+        mIteratingHistory = false;
+        mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
+        mReadHistoryStrings = null;
+    }
+
+    @Override
+    public long getHistoryBaseTime() {
+        return mHistoryBaseTime;
+    }
+
+    @Override
+    public int getStartCount() {
+        return mStartCount;
+    }
+
+    public boolean isOnBattery() {
+        return mOnBattery;
+    }
+
+    public boolean isCharging() {
+        return mCharging;
+    }
+
+    public boolean isScreenOn() {
+        return mScreenState == Display.STATE_ON;
+    }
+
+    void initTimes(long uptime, long realtime) {
+        mStartClockTime = System.currentTimeMillis();
+        mOnBatteryTimeBase.init(uptime, realtime);
+        mOnBatteryScreenOffTimeBase.init(uptime, realtime);
+        mRealtime = 0;
+        mUptime = 0;
+        mRealtimeStart = realtime;
+        mUptimeStart = uptime;
+    }
+
+    void initDischarge() {
+        mLowDischargeAmountSinceCharge = 0;
+        mHighDischargeAmountSinceCharge = 0;
+        mDischargeAmountScreenOn = 0;
+        mDischargeAmountScreenOnSinceCharge = 0;
+        mDischargeAmountScreenOff = 0;
+        mDischargeAmountScreenOffSinceCharge = 0;
+        mDischargeStepTracker.init();
+        mChargeStepTracker.init();
+        mDischargeScreenOffCounter.reset(false);
+        mDischargeCounter.reset(false);
+    }
+
+    public void resetAllStatsCmdLocked() {
+        resetAllStatsLocked();
+        final long mSecUptime = mClocks.uptimeMillis();
+        long uptime = mSecUptime * 1000;
+        long mSecRealtime = mClocks.elapsedRealtime();
+        long realtime = mSecRealtime * 1000;
+        mDischargeStartLevel = mHistoryCur.batteryLevel;
+        pullPendingStateUpdatesLocked();
+        addHistoryRecordLocked(mSecRealtime, mSecUptime);
+        mDischargeCurrentLevel = mDischargeUnplugLevel = mDischargePlugLevel
+                = mCurrentBatteryLevel = mHistoryCur.batteryLevel;
+        mOnBatteryTimeBase.reset(uptime, realtime);
+        mOnBatteryScreenOffTimeBase.reset(uptime, realtime);
+        if ((mHistoryCur.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) == 0) {
+            if (mScreenState == Display.STATE_ON) {
+                mDischargeScreenOnUnplugLevel = mHistoryCur.batteryLevel;
+                mDischargeScreenOffUnplugLevel = 0;
+            } else {
+                mDischargeScreenOnUnplugLevel = 0;
+                mDischargeScreenOffUnplugLevel = mHistoryCur.batteryLevel;
+            }
+            mDischargeAmountScreenOn = 0;
+            mDischargeAmountScreenOff = 0;
+        }
+        initActiveHistoryEventsLocked(mSecRealtime, mSecUptime);
+    }
+
+    private void resetAllStatsLocked() {
+        final long uptimeMillis = mClocks.uptimeMillis();
+        final long elapsedRealtimeMillis = mClocks.elapsedRealtime();
+        mStartCount = 0;
+        initTimes(uptimeMillis * 1000, elapsedRealtimeMillis * 1000);
+        mScreenOnTimer.reset(false);
+        for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            mScreenBrightnessTimer[i].reset(false);
+        }
+
+        if (mPowerProfile != null) {
+            mEstimatedBatteryCapacity = (int) mPowerProfile.getBatteryCapacity();
+        } else {
+            mEstimatedBatteryCapacity = -1;
+        }
+        mMinLearnedBatteryCapacity = -1;
+        mMaxLearnedBatteryCapacity = -1;
+        mInteractiveTimer.reset(false);
+        mPowerSaveModeEnabledTimer.reset(false);
+        mLastIdleTimeStart = elapsedRealtimeMillis;
+        mLongestLightIdleTime = 0;
+        mLongestFullIdleTime = 0;
+        mDeviceIdleModeLightTimer.reset(false);
+        mDeviceIdleModeFullTimer.reset(false);
+        mDeviceLightIdlingTimer.reset(false);
+        mDeviceIdlingTimer.reset(false);
+        mPhoneOnTimer.reset(false);
+        mAudioOnTimer.reset(false);
+        mVideoOnTimer.reset(false);
+        mFlashlightOnTimer.reset(false);
+        mCameraOnTimer.reset(false);
+        mBluetoothScanTimer.reset(false);
+        for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            mPhoneSignalStrengthsTimer[i].reset(false);
+        }
+        mPhoneSignalScanningTimer.reset(false);
+        for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+            mPhoneDataConnectionsTimer[i].reset(false);
+        }
+        for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+            mNetworkByteActivityCounters[i].reset(false);
+            mNetworkPacketActivityCounters[i].reset(false);
+        }
+        mMobileRadioActiveTimer.reset(false);
+        mMobileRadioActivePerAppTimer.reset(false);
+        mMobileRadioActiveAdjustedTime.reset(false);
+        mMobileRadioActiveUnknownTime.reset(false);
+        mMobileRadioActiveUnknownCount.reset(false);
+        mWifiOnTimer.reset(false);
+        mGlobalWifiRunningTimer.reset(false);
+        for (int i=0; i<NUM_WIFI_STATES; i++) {
+            mWifiStateTimer[i].reset(false);
+        }
+        for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+            mWifiSupplStateTimer[i].reset(false);
+        }
+        for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            mWifiSignalStrengthsTimer[i].reset(false);
+        }
+        mWifiActivity.reset(false);
+        mBluetoothActivity.reset(false);
+        mModemActivity.reset(false);
+        mNumConnectivityChange = mLoadedNumConnectivityChange = mUnpluggedNumConnectivityChange = 0;
+
+        for (int i=0; i<mUidStats.size(); i++) {
+            if (mUidStats.valueAt(i).reset(uptimeMillis * 1000, elapsedRealtimeMillis * 1000)) {
+                mUidStats.remove(mUidStats.keyAt(i));
+                i--;
+            }
+        }
+
+        if (mKernelWakelockStats.size() > 0) {
+            for (SamplingTimer timer : mKernelWakelockStats.values()) {
+                mOnBatteryScreenOffTimeBase.remove(timer);
+            }
+            mKernelWakelockStats.clear();
+        }
+
+        if (mKernelMemoryStats.size() > 0) {
+            for (int i = 0; i < mKernelMemoryStats.size(); i++) {
+                mOnBatteryTimeBase.remove(mKernelMemoryStats.valueAt(i));
+            }
+            mKernelMemoryStats.clear();
+        }
+
+        if (mWakeupReasonStats.size() > 0) {
+            for (SamplingTimer timer : mWakeupReasonStats.values()) {
+                mOnBatteryTimeBase.remove(timer);
+            }
+            mWakeupReasonStats.clear();
+        }
+
+        mLastHistoryStepDetails = null;
+        mLastStepCpuUserTime = mLastStepCpuSystemTime = 0;
+        mCurStepCpuUserTime = mCurStepCpuSystemTime = 0;
+        mLastStepCpuUserTime = mCurStepCpuUserTime = 0;
+        mLastStepCpuSystemTime = mCurStepCpuSystemTime = 0;
+        mLastStepStatUserTime = mCurStepStatUserTime = 0;
+        mLastStepStatSystemTime = mCurStepStatSystemTime = 0;
+        mLastStepStatIOWaitTime = mCurStepStatIOWaitTime = 0;
+        mLastStepStatIrqTime = mCurStepStatIrqTime = 0;
+        mLastStepStatSoftIrqTime = mCurStepStatSoftIrqTime = 0;
+        mLastStepStatIdleTime = mCurStepStatIdleTime = 0;
+
+        initDischarge();
+
+        clearHistoryLocked();
+    }
+
+    private void initActiveHistoryEventsLocked(long elapsedRealtimeMs, long uptimeMs) {
+        for (int i=0; i<HistoryItem.EVENT_COUNT; i++) {
+            if (!mRecordAllHistory && i == HistoryItem.EVENT_PROC) {
+                // Not recording process starts/stops.
+                continue;
+            }
+            HashMap<String, SparseIntArray> active = mActiveEvents.getStateForEvent(i);
+            if (active == null) {
+                continue;
+            }
+            for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) {
+                SparseIntArray uids = ent.getValue();
+                for (int j=0; j<uids.size(); j++) {
+                    addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, i, ent.getKey(),
+                            uids.keyAt(j));
+                }
+            }
+        }
+    }
+
+    void updateDischargeScreenLevelsLocked(boolean oldScreenOn, boolean newScreenOn) {
+        if (oldScreenOn) {
+            int diff = mDischargeScreenOnUnplugLevel - mDischargeCurrentLevel;
+            if (diff > 0) {
+                mDischargeAmountScreenOn += diff;
+                mDischargeAmountScreenOnSinceCharge += diff;
+            }
+        } else {
+            int diff = mDischargeScreenOffUnplugLevel - mDischargeCurrentLevel;
+            if (diff > 0) {
+                mDischargeAmountScreenOff += diff;
+                mDischargeAmountScreenOffSinceCharge += diff;
+            }
+        }
+        if (newScreenOn) {
+            mDischargeScreenOnUnplugLevel = mDischargeCurrentLevel;
+            mDischargeScreenOffUnplugLevel = 0;
+        } else {
+            mDischargeScreenOnUnplugLevel = 0;
+            mDischargeScreenOffUnplugLevel = mDischargeCurrentLevel;
+        }
+    }
+
+    public void pullPendingStateUpdatesLocked() {
+        if (mOnBatteryInternal) {
+            final boolean screenOn = mScreenState == Display.STATE_ON;
+            updateDischargeScreenLevelsLocked(screenOn, screenOn);
+        }
+    }
+
+    private final NetworkStatsFactory mNetworkStatsFactory = new NetworkStatsFactory();
+    private final Pools.Pool<NetworkStats> mNetworkStatsPool = new Pools.SynchronizedPool<>(6);
+
+    private final Object mWifiNetworkLock = new Object();
+
+    @GuardedBy("mWifiNetworkLock")
+    private String[] mWifiIfaces = EmptyArray.STRING;
+
+    @GuardedBy("mWifiNetworkLock")
+    private NetworkStats mLastWifiNetworkStats = new NetworkStats(0, -1);
+
+    private final Object mModemNetworkLock = new Object();
+
+    @GuardedBy("mModemNetworkLock")
+    private String[] mModemIfaces = EmptyArray.STRING;
+
+    @GuardedBy("mModemNetworkLock")
+    private NetworkStats mLastModemNetworkStats = new NetworkStats(0, -1);
+
+    private NetworkStats readNetworkStatsLocked(String[] ifaces) {
+        try {
+            if (!ArrayUtils.isEmpty(ifaces)) {
+                return mNetworkStatsFactory.readNetworkStatsDetail(NetworkStats.UID_ALL, ifaces,
+                        NetworkStats.TAG_NONE, mNetworkStatsPool.acquire());
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "failed to read network stats for ifaces: " + Arrays.toString(ifaces));
+        }
+        return null;
+    }
+
+    /**
+     * Distribute WiFi energy info and network traffic to apps.
+     * @param info The energy information from the WiFi controller.
+     */
+    public void updateWifiState(@Nullable final WifiActivityEnergyInfo info) {
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Updating wifi stats: " + Arrays.toString(mWifiIfaces));
+        }
+
+        // Grab a separate lock to acquire the network stats, which may do I/O.
+        NetworkStats delta = null;
+        synchronized (mWifiNetworkLock) {
+            final NetworkStats latestStats = readNetworkStatsLocked(mWifiIfaces);
+            if (latestStats != null) {
+                delta = NetworkStats.subtract(latestStats, mLastWifiNetworkStats, null, null,
+                        mNetworkStatsPool.acquire());
+                mNetworkStatsPool.release(mLastWifiNetworkStats);
+                mLastWifiNetworkStats = latestStats;
+            }
+        }
+
+        synchronized (this) {
+            if (!mOnBatteryInternal) {
+                if (delta != null) {
+                    mNetworkStatsPool.release(delta);
+                }
+                return;
+            }
+
+            final long elapsedRealtimeMs = mClocks.elapsedRealtime();
+            SparseLongArray rxPackets = new SparseLongArray();
+            SparseLongArray txPackets = new SparseLongArray();
+            long totalTxPackets = 0;
+            long totalRxPackets = 0;
+            if (delta != null) {
+                NetworkStats.Entry entry = new NetworkStats.Entry();
+                final int size = delta.size();
+                for (int i = 0; i < size; i++) {
+                    entry = delta.getValues(i, entry);
+
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "Wifi uid " + entry.uid + ": delta rx=" + entry.rxBytes
+                                + " tx=" + entry.txBytes + " rxPackets=" + entry.rxPackets
+                                + " txPackets=" + entry.txPackets);
+                    }
+
+                    if (entry.rxBytes == 0 && entry.txBytes == 0) {
+                        // Skip the lookup below since there is no work to do.
+                        continue;
+                    }
+
+                    final Uid u = getUidStatsLocked(mapUid(entry.uid));
+                    if (entry.rxBytes != 0) {
+                        u.noteNetworkActivityLocked(NETWORK_WIFI_RX_DATA, entry.rxBytes,
+                                entry.rxPackets);
+                        if (entry.set == NetworkStats.SET_DEFAULT) { // Background transfers
+                            u.noteNetworkActivityLocked(NETWORK_WIFI_BG_RX_DATA, entry.rxBytes,
+                                    entry.rxPackets);
+                        }
+                        mNetworkByteActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked(
+                                entry.rxBytes);
+                        mNetworkPacketActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked(
+                                entry.rxPackets);
+
+                        rxPackets.put(u.getUid(), entry.rxPackets);
+
+                        // Sum the total number of packets so that the Rx Power can
+                        // be evenly distributed amongst the apps.
+                        totalRxPackets += entry.rxPackets;
+                    }
+
+                    if (entry.txBytes != 0) {
+                        u.noteNetworkActivityLocked(NETWORK_WIFI_TX_DATA, entry.txBytes,
+                                entry.txPackets);
+                        if (entry.set == NetworkStats.SET_DEFAULT) { // Background transfers
+                            u.noteNetworkActivityLocked(NETWORK_WIFI_BG_TX_DATA, entry.txBytes,
+                                    entry.txPackets);
+                        }
+                        mNetworkByteActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked(
+                                entry.txBytes);
+                        mNetworkPacketActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked(
+                                entry.txPackets);
+
+                        txPackets.put(u.getUid(), entry.txPackets);
+
+                        // Sum the total number of packets so that the Tx Power can
+                        // be evenly distributed amongst the apps.
+                        totalTxPackets += entry.txPackets;
+                    }
+                }
+                mNetworkStatsPool.release(delta);
+                delta = null;
+            }
+
+            if (info != null) {
+                mHasWifiReporting = true;
+
+                // Measured in mAms
+                final long txTimeMs = info.getControllerTxTimeMillis();
+                final long rxTimeMs = info.getControllerRxTimeMillis();
+                final long idleTimeMs = info.getControllerIdleTimeMillis();
+                final long totalTimeMs = txTimeMs + rxTimeMs + idleTimeMs;
+
+                long leftOverRxTimeMs = rxTimeMs;
+                long leftOverTxTimeMs = txTimeMs;
+
+                if (DEBUG_ENERGY) {
+                    Slog.d(TAG, "------ BEGIN WiFi power blaming ------");
+                    Slog.d(TAG, "  Tx Time:    " + txTimeMs + " ms");
+                    Slog.d(TAG, "  Rx Time:    " + rxTimeMs + " ms");
+                    Slog.d(TAG, "  Idle Time:  " + idleTimeMs + " ms");
+                    Slog.d(TAG, "  Total Time: " + totalTimeMs + " ms");
+                }
+
+                long totalWifiLockTimeMs = 0;
+                long totalScanTimeMs = 0;
+
+                // On the first pass, collect some totals so that we can normalize power
+                // calculations if we need to.
+                final int uidStatsSize = mUidStats.size();
+                for (int i = 0; i < uidStatsSize; i++) {
+                    final Uid uid = mUidStats.valueAt(i);
+
+                    // Sum the total scan power for all apps.
+                    totalScanTimeMs += uid.mWifiScanTimer.getTimeSinceMarkLocked(
+                            elapsedRealtimeMs * 1000) / 1000;
+
+                    // Sum the total time holding wifi lock for all apps.
+                    totalWifiLockTimeMs += uid.mFullWifiLockTimer.getTimeSinceMarkLocked(
+                            elapsedRealtimeMs * 1000) / 1000;
+                }
+
+                if (DEBUG_ENERGY && totalScanTimeMs > rxTimeMs) {
+                    Slog.d(TAG,
+                            "  !Estimated scan time > Actual rx time (" + totalScanTimeMs + " ms > "
+                                    + rxTimeMs + " ms). Normalizing scan time.");
+                }
+                if (DEBUG_ENERGY && totalScanTimeMs > txTimeMs) {
+                    Slog.d(TAG,
+                            "  !Estimated scan time > Actual tx time (" + totalScanTimeMs + " ms > "
+                                    + txTimeMs + " ms). Normalizing scan time.");
+                }
+
+                // Actually assign and distribute power usage to apps.
+                for (int i = 0; i < uidStatsSize; i++) {
+                    final Uid uid = mUidStats.valueAt(i);
+
+                    long scanTimeSinceMarkMs = uid.mWifiScanTimer.getTimeSinceMarkLocked(
+                            elapsedRealtimeMs * 1000) / 1000;
+                    if (scanTimeSinceMarkMs > 0) {
+                        // Set the new mark so that next time we get new data since this point.
+                        uid.mWifiScanTimer.setMark(elapsedRealtimeMs);
+
+                        long scanRxTimeSinceMarkMs = scanTimeSinceMarkMs;
+                        long scanTxTimeSinceMarkMs = scanTimeSinceMarkMs;
+
+                        // Our total scan time is more than the reported Tx/Rx time.
+                        // This is possible because the cost of a scan is approximate.
+                        // Let's normalize the result so that we evenly blame each app
+                        // scanning.
+                        //
+                        // This means that we may have apps that transmitted/received packets not be
+                        // blamed for this, but this is fine as scans are relatively more expensive.
+                        if (totalScanTimeMs > rxTimeMs) {
+                            scanRxTimeSinceMarkMs = (rxTimeMs * scanRxTimeSinceMarkMs) /
+                                    totalScanTimeMs;
+                        }
+                        if (totalScanTimeMs > txTimeMs) {
+                            scanTxTimeSinceMarkMs = (txTimeMs * scanTxTimeSinceMarkMs) /
+                                    totalScanTimeMs;
+                        }
+
+                        if (DEBUG_ENERGY) {
+                            Slog.d(TAG, "  ScanTime for UID " + uid.getUid() + ": Rx:"
+                                    + scanRxTimeSinceMarkMs + " ms  Tx:"
+                                    + scanTxTimeSinceMarkMs + " ms)");
+                        }
+
+                        ControllerActivityCounterImpl activityCounter =
+                                uid.getOrCreateWifiControllerActivityLocked();
+                        activityCounter.getRxTimeCounter().addCountLocked(scanRxTimeSinceMarkMs);
+                        activityCounter.getTxTimeCounters()[0].addCountLocked(
+                                scanTxTimeSinceMarkMs);
+                        leftOverRxTimeMs -= scanRxTimeSinceMarkMs;
+                        leftOverTxTimeMs -= scanTxTimeSinceMarkMs;
+                    }
+
+                    // Distribute evenly the power consumed while Idle to each app holding a WiFi
+                    // lock.
+                    final long wifiLockTimeSinceMarkMs =
+                            uid.mFullWifiLockTimer.getTimeSinceMarkLocked(
+                                    elapsedRealtimeMs * 1000) / 1000;
+                    if (wifiLockTimeSinceMarkMs > 0) {
+                        // Set the new mark so that next time we get new data since this point.
+                        uid.mFullWifiLockTimer.setMark(elapsedRealtimeMs);
+
+                        final long myIdleTimeMs = (wifiLockTimeSinceMarkMs * idleTimeMs)
+                                / totalWifiLockTimeMs;
+                        if (DEBUG_ENERGY) {
+                            Slog.d(TAG, "  IdleTime for UID " + uid.getUid() + ": "
+                                    + myIdleTimeMs + " ms");
+                        }
+                        uid.getOrCreateWifiControllerActivityLocked().getIdleTimeCounter()
+                                .addCountLocked(myIdleTimeMs);
+                    }
+                }
+
+                if (DEBUG_ENERGY) {
+                    Slog.d(TAG, "  New RxPower: " + leftOverRxTimeMs + " ms");
+                    Slog.d(TAG, "  New TxPower: " + leftOverTxTimeMs + " ms");
+                }
+
+                // Distribute the remaining Tx power appropriately between all apps that transmitted
+                // packets.
+                for (int i = 0; i < txPackets.size(); i++) {
+                    final Uid uid = getUidStatsLocked(txPackets.keyAt(i));
+                    final long myTxTimeMs = (txPackets.valueAt(i) * leftOverTxTimeMs)
+                            / totalTxPackets;
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "  TxTime for UID " + uid.getUid() + ": " + myTxTimeMs + " ms");
+                    }
+                    uid.getOrCreateWifiControllerActivityLocked().getTxTimeCounters()[0]
+                            .addCountLocked(myTxTimeMs);
+                }
+
+                // Distribute the remaining Rx power appropriately between all apps that received
+                // packets.
+                for (int i = 0; i < rxPackets.size(); i++) {
+                    final Uid uid = getUidStatsLocked(rxPackets.keyAt(i));
+                    final long myRxTimeMs = (rxPackets.valueAt(i) * leftOverRxTimeMs)
+                            / totalRxPackets;
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "  RxTime for UID " + uid.getUid() + ": " + myRxTimeMs + " ms");
+                    }
+                    uid.getOrCreateWifiControllerActivityLocked().getRxTimeCounter()
+                            .addCountLocked(myRxTimeMs);
+                }
+
+                // Any left over power use will be picked up by the WiFi category in BatteryStatsHelper.
+
+
+                // Update WiFi controller stats.
+                mWifiActivity.getRxTimeCounter().addCountLocked(info.getControllerRxTimeMillis());
+                mWifiActivity.getTxTimeCounters()[0].addCountLocked(
+                        info.getControllerTxTimeMillis());
+                mWifiActivity.getIdleTimeCounter().addCountLocked(
+                        info.getControllerIdleTimeMillis());
+
+                // POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
+                final double opVolt = mPowerProfile.getAveragePower(
+                        PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+                if (opVolt != 0) {
+                    // We store the power drain as mAms.
+                    mWifiActivity.getPowerCounter().addCountLocked(
+                            (long) (info.getControllerEnergyUsed() / opVolt));
+                }
+            }
+        }
+    }
+
+    /**
+     * Distribute Cell radio energy info and network traffic to apps.
+     */
+    public void updateMobileRadioState(@Nullable final ModemActivityInfo activityInfo) {
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Updating mobile radio stats with " + activityInfo);
+        }
+
+        // Grab a separate lock to acquire the network stats, which may do I/O.
+        NetworkStats delta = null;
+        synchronized (mModemNetworkLock) {
+            final NetworkStats latestStats = readNetworkStatsLocked(mModemIfaces);
+            if (latestStats != null) {
+                delta = NetworkStats.subtract(latestStats, mLastModemNetworkStats, null, null,
+                        mNetworkStatsPool.acquire());
+                mNetworkStatsPool.release(mLastModemNetworkStats);
+                mLastModemNetworkStats = latestStats;
+            }
+        }
+
+        synchronized (this) {
+            if (!mOnBatteryInternal) {
+                if (delta != null) {
+                    mNetworkStatsPool.release(delta);
+                }
+                return;
+            }
+
+            final long elapsedRealtimeMs = mClocks.elapsedRealtime();
+            long radioTime = mMobileRadioActivePerAppTimer.getTimeSinceMarkLocked(
+                    elapsedRealtimeMs * 1000);
+            mMobileRadioActivePerAppTimer.setMark(elapsedRealtimeMs);
+
+            long totalRxPackets = 0;
+            long totalTxPackets = 0;
+            if (delta != null) {
+                NetworkStats.Entry entry = new NetworkStats.Entry();
+                final int size = delta.size();
+                for (int i = 0; i < size; i++) {
+                    entry = delta.getValues(i, entry);
+                    if (entry.rxPackets == 0 && entry.txPackets == 0) {
+                        continue;
+                    }
+
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "Mobile uid " + entry.uid + ": delta rx=" + entry.rxBytes
+                                + " tx=" + entry.txBytes + " rxPackets=" + entry.rxPackets
+                                + " txPackets=" + entry.txPackets);
+                    }
+
+                    totalRxPackets += entry.rxPackets;
+                    totalTxPackets += entry.txPackets;
+
+                    final Uid u = getUidStatsLocked(mapUid(entry.uid));
+                    u.noteNetworkActivityLocked(NETWORK_MOBILE_RX_DATA, entry.rxBytes,
+                            entry.rxPackets);
+                    u.noteNetworkActivityLocked(NETWORK_MOBILE_TX_DATA, entry.txBytes,
+                            entry.txPackets);
+                    if (entry.set == NetworkStats.SET_DEFAULT) { // Background transfers
+                        u.noteNetworkActivityLocked(NETWORK_MOBILE_BG_RX_DATA,
+                                entry.rxBytes, entry.rxPackets);
+                        u.noteNetworkActivityLocked(NETWORK_MOBILE_BG_TX_DATA,
+                                entry.txBytes, entry.txPackets);
+                    }
+
+                    mNetworkByteActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked(
+                            entry.rxBytes);
+                    mNetworkByteActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked(
+                            entry.txBytes);
+                    mNetworkPacketActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked(
+                            entry.rxPackets);
+                    mNetworkPacketActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked(
+                            entry.txPackets);
+                }
+
+                // Now distribute proportional blame to the apps that did networking.
+                long totalPackets = totalRxPackets + totalTxPackets;
+                if (totalPackets > 0) {
+                    for (int i = 0; i < size; i++) {
+                        entry = delta.getValues(i, entry);
+                        if (entry.rxPackets == 0 && entry.txPackets == 0) {
+                            continue;
+                        }
+
+                        final Uid u = getUidStatsLocked(mapUid(entry.uid));
+
+                        // Distribute total radio active time in to this app.
+                        final long appPackets = entry.rxPackets + entry.txPackets;
+                        final long appRadioTime = (radioTime * appPackets) / totalPackets;
+                        u.noteMobileRadioActiveTimeLocked(appRadioTime);
+
+                        // Remove this app from the totals, so that we don't lose any time
+                        // due to rounding.
+                        radioTime -= appRadioTime;
+                        totalPackets -= appPackets;
+
+                        if (activityInfo != null) {
+                            ControllerActivityCounterImpl activityCounter =
+                                    u.getOrCreateModemControllerActivityLocked();
+                            if (totalRxPackets > 0 && entry.rxPackets > 0) {
+                                final long rxMs = (entry.rxPackets * activityInfo.getRxTimeMillis())
+                                        / totalRxPackets;
+                                activityCounter.getRxTimeCounter().addCountLocked(rxMs);
+                            }
+
+                            if (totalTxPackets > 0 && entry.txPackets > 0) {
+                                for (int lvl = 0; lvl < ModemActivityInfo.TX_POWER_LEVELS; lvl++) {
+                                    long txMs =
+                                            entry.txPackets * activityInfo.getTxTimeMillis()[lvl];
+                                    txMs /= totalTxPackets;
+                                    activityCounter.getTxTimeCounters()[lvl].addCountLocked(txMs);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (radioTime > 0) {
+                    // Whoops, there is some radio time we can't blame on an app!
+                    mMobileRadioActiveUnknownTime.addCountLocked(radioTime);
+                    mMobileRadioActiveUnknownCount.addCountLocked(1);
+                }
+
+                mNetworkStatsPool.release(delta);
+                delta = null;
+            }
+
+            if (activityInfo != null) {
+                mHasModemReporting = true;
+                mModemActivity.getIdleTimeCounter().addCountLocked(
+                        activityInfo.getIdleTimeMillis());
+                mModemActivity.getRxTimeCounter().addCountLocked(activityInfo.getRxTimeMillis());
+                for (int lvl = 0; lvl < ModemActivityInfo.TX_POWER_LEVELS; lvl++) {
+                    mModemActivity.getTxTimeCounters()[lvl]
+                            .addCountLocked(activityInfo.getTxTimeMillis()[lvl]);
+                }
+
+                // POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
+                final double opVolt = mPowerProfile.getAveragePower(
+                        PowerProfile.POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+                if (opVolt != 0) {
+                    // We store the power drain as mAms.
+                    mModemActivity.getPowerCounter().addCountLocked(
+                            (long) (activityInfo.getEnergyUsed() / opVolt));
+                }
+            }
+        }
+    }
+
+    /**
+     * Distribute Bluetooth energy info and network traffic to apps.
+     * @param info The energy information from the bluetooth controller.
+     */
+    public void updateBluetoothStateLocked(@Nullable final BluetoothActivityEnergyInfo info) {
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Updating bluetooth stats: " + info);
+        }
+
+        if (info == null || !mOnBatteryInternal) {
+            return;
+        }
+
+        mHasBluetoothReporting = true;
+
+        final long elapsedRealtimeMs = mClocks.elapsedRealtime();
+        final long rxTimeMs = info.getControllerRxTimeMillis();
+        final long txTimeMs = info.getControllerTxTimeMillis();
+
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "------ BEGIN BLE power blaming ------");
+            Slog.d(TAG, "  Tx Time:    " + txTimeMs + " ms");
+            Slog.d(TAG, "  Rx Time:    " + rxTimeMs + " ms");
+            Slog.d(TAG, "  Idle Time:  " + info.getControllerIdleTimeMillis() + " ms");
+        }
+
+        long totalScanTimeMs = 0;
+
+        final int uidCount = mUidStats.size();
+        for (int i = 0; i < uidCount; i++) {
+            final Uid u = mUidStats.valueAt(i);
+            if (u.mBluetoothScanTimer == null) {
+                continue;
+            }
+
+            totalScanTimeMs += u.mBluetoothScanTimer.getTimeSinceMarkLocked(
+                    elapsedRealtimeMs * 1000) / 1000;
+        }
+
+        final boolean normalizeScanRxTime = (totalScanTimeMs > rxTimeMs);
+        final boolean normalizeScanTxTime = (totalScanTimeMs > txTimeMs);
+
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Normalizing scan power for RX=" + normalizeScanRxTime
+                    + " TX=" + normalizeScanTxTime);
+        }
+
+        long leftOverRxTimeMs = rxTimeMs;
+        long leftOverTxTimeMs = txTimeMs;
+
+        for (int i = 0; i < uidCount; i++) {
+            final Uid u = mUidStats.valueAt(i);
+            if (u.mBluetoothScanTimer == null) {
+                continue;
+            }
+
+            long scanTimeSinceMarkMs = u.mBluetoothScanTimer.getTimeSinceMarkLocked(
+                    elapsedRealtimeMs * 1000) / 1000;
+            if (scanTimeSinceMarkMs > 0) {
+                // Set the new mark so that next time we get new data since this point.
+                u.mBluetoothScanTimer.setMark(elapsedRealtimeMs);
+
+                long scanTimeRxSinceMarkMs = scanTimeSinceMarkMs;
+                long scanTimeTxSinceMarkMs = scanTimeSinceMarkMs;
+
+                if (normalizeScanRxTime) {
+                    // Scan time is longer than the total rx time in the controller,
+                    // so distribute the scan time proportionately. This means regular traffic
+                    // will not blamed, but scans are more expensive anyways.
+                    scanTimeRxSinceMarkMs = (rxTimeMs * scanTimeRxSinceMarkMs) / totalScanTimeMs;
+                }
+
+                if (normalizeScanTxTime) {
+                    // Scan time is longer than the total tx time in the controller,
+                    // so distribute the scan time proportionately. This means regular traffic
+                    // will not blamed, but scans are more expensive anyways.
+                    scanTimeTxSinceMarkMs = (txTimeMs * scanTimeTxSinceMarkMs) / totalScanTimeMs;
+                }
+
+                final ControllerActivityCounterImpl counter =
+                        u.getOrCreateBluetoothControllerActivityLocked();
+                counter.getRxTimeCounter().addCountLocked(scanTimeRxSinceMarkMs);
+                counter.getTxTimeCounters()[0].addCountLocked(scanTimeTxSinceMarkMs);
+
+                leftOverRxTimeMs -= scanTimeRxSinceMarkMs;
+                leftOverTxTimeMs -= scanTimeTxSinceMarkMs;
+            }
+        }
+
+        if (DEBUG_ENERGY) {
+            Slog.d(TAG, "Left over time for traffic RX=" + leftOverRxTimeMs
+                    + " TX=" + leftOverTxTimeMs);
+        }
+
+        //
+        // Now distribute blame to apps that did bluetooth traffic.
+        //
+
+        long totalTxBytes = 0;
+        long totalRxBytes = 0;
+
+        final UidTraffic[] uidTraffic = info.getUidTraffic();
+        final int numUids = uidTraffic != null ? uidTraffic.length : 0;
+        for (int i = 0; i < numUids; i++) {
+            final UidTraffic traffic = uidTraffic[i];
+
+            // Add to the global counters.
+            mNetworkByteActivityCounters[NETWORK_BT_RX_DATA].addCountLocked(
+                    traffic.getRxBytes());
+            mNetworkByteActivityCounters[NETWORK_BT_TX_DATA].addCountLocked(
+                    traffic.getTxBytes());
+
+            // Add to the UID counters.
+            final Uid u = getUidStatsLocked(mapUid(traffic.getUid()));
+            u.noteNetworkActivityLocked(NETWORK_BT_RX_DATA, traffic.getRxBytes(), 0);
+            u.noteNetworkActivityLocked(NETWORK_BT_TX_DATA, traffic.getTxBytes(), 0);
+
+            // Calculate the total traffic.
+            totalTxBytes += traffic.getTxBytes();
+            totalRxBytes += traffic.getRxBytes();
+        }
+
+        if ((totalTxBytes != 0 || totalRxBytes != 0) &&
+                (leftOverRxTimeMs != 0 || leftOverTxTimeMs != 0)) {
+            for (int i = 0; i < numUids; i++) {
+                final UidTraffic traffic = uidTraffic[i];
+
+                final Uid u = getUidStatsLocked(mapUid(traffic.getUid()));
+                final ControllerActivityCounterImpl counter =
+                        u.getOrCreateBluetoothControllerActivityLocked();
+
+                if (totalRxBytes > 0 && traffic.getRxBytes() > 0) {
+                    final long timeRxMs = (leftOverRxTimeMs * traffic.getRxBytes()) / totalRxBytes;
+
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "UID=" + traffic.getUid() + " rx_bytes=" + traffic.getRxBytes()
+                                + " rx_time=" + timeRxMs);
+                    }
+                    counter.getRxTimeCounter().addCountLocked(timeRxMs);
+                    leftOverRxTimeMs -= timeRxMs;
+                }
+
+                if (totalTxBytes > 0 && traffic.getTxBytes() > 0) {
+                    final long timeTxMs = (leftOverTxTimeMs * traffic.getTxBytes()) / totalTxBytes;
+
+                    if (DEBUG_ENERGY) {
+                        Slog.d(TAG, "UID=" + traffic.getUid() + " tx_bytes=" + traffic.getTxBytes()
+                                + " tx_time=" + timeTxMs);
+                    }
+
+                    counter.getTxTimeCounters()[0].addCountLocked(timeTxMs);
+                    leftOverTxTimeMs -= timeTxMs;
+                }
+            }
+        }
+
+        mBluetoothActivity.getRxTimeCounter().addCountLocked(
+                info.getControllerRxTimeMillis());
+        mBluetoothActivity.getTxTimeCounters()[0].addCountLocked(
+                info.getControllerTxTimeMillis());
+        mBluetoothActivity.getIdleTimeCounter().addCountLocked(
+                info.getControllerIdleTimeMillis());
+
+        // POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
+        final double opVolt = mPowerProfile.getAveragePower(
+                PowerProfile.POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+        if (opVolt != 0) {
+            // We store the power drain as mAms.
+            mBluetoothActivity.getPowerCounter().addCountLocked(
+                    (long) (info.getControllerEnergyUsed() / opVolt));
+        }
+    }
+
+    /**
+     * Read and distribute kernel wake lock use across apps.
+     */
+    public void updateKernelWakelocksLocked() {
+        final KernelWakelockStats wakelockStats = mKernelWakelockReader.readKernelWakelockStats(
+                mTmpWakelockStats);
+        if (wakelockStats == null) {
+            // Not crashing might make board bringup easier.
+            Slog.w(TAG, "Couldn't get kernel wake lock stats");
+            return;
+        }
+
+        for (Map.Entry<String, KernelWakelockStats.Entry> ent : wakelockStats.entrySet()) {
+            String name = ent.getKey();
+            KernelWakelockStats.Entry kws = ent.getValue();
+
+            SamplingTimer kwlt = mKernelWakelockStats.get(name);
+            if (kwlt == null) {
+                kwlt = new SamplingTimer(mClocks, mOnBatteryScreenOffTimeBase);
+                mKernelWakelockStats.put(name, kwlt);
+            }
+
+            kwlt.update(kws.mTotalTime, kws.mCount);
+            kwlt.setUpdateVersion(kws.mVersion);
+        }
+
+        int numWakelocksSetStale = 0;
+        // Set timers to stale if they didn't appear in /d/wakeup_sources (or /proc/wakelocks)
+        // this time.
+        for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) {
+            SamplingTimer st = ent.getValue();
+            if (st.getUpdateVersion() != wakelockStats.kernelWakelockVersion) {
+                st.endSample();
+                numWakelocksSetStale++;
+            }
+        }
+
+        // Record whether we've seen a non-zero time (for debugging b/22716723).
+        if (wakelockStats.isEmpty()) {
+            Slog.wtf(TAG, "All kernel wakelocks had time of zero");
+        }
+
+        if (numWakelocksSetStale == mKernelWakelockStats.size()) {
+            Slog.wtf(TAG, "All kernel wakelocks were set stale. new version=" +
+                    wakelockStats.kernelWakelockVersion);
+        }
+    }
+
+    // We use an anonymous class to access these variables,
+    // so they can't live on the stack or they'd have to be
+    // final MutableLong objects (more allocations).
+    // Used in updateCpuTimeLocked().
+    long mTempTotalCpuUserTimeUs;
+    long mTempTotalCpuSystemTimeUs;
+    long[][] mWakeLockAllocationsUs;
+
+    /**
+     * Reads the newest memory stats from the kernel.
+     */
+    public void updateKernelMemoryBandwidthLocked() {
+        mKernelMemoryBandwidthStats.updateStats();
+        LongSparseLongArray bandwidthEntries = mKernelMemoryBandwidthStats.getBandwidthEntries();
+        final int bandwidthEntryCount = bandwidthEntries.size();
+        int index;
+        for (int i = 0; i < bandwidthEntryCount; i++) {
+            SamplingTimer timer;
+            if ((index = mKernelMemoryStats.indexOfKey(bandwidthEntries.keyAt(i))) >= 0) {
+                timer = mKernelMemoryStats.valueAt(index);
+            } else {
+                timer = new SamplingTimer(mClocks, mOnBatteryTimeBase);
+                mKernelMemoryStats.put(bandwidthEntries.keyAt(i), timer);
+            }
+            timer.update(bandwidthEntries.valueAt(i), 1);
+            if (DEBUG_MEMORY) {
+                Slog.d(TAG, String.format("Added entry %d and updated timer to: "
+                        + "mUnpluggedReportedTotalTime %d size %d", bandwidthEntries.keyAt(i),
+                        mKernelMemoryStats.get(
+                                bandwidthEntries.keyAt(i)).mUnpluggedReportedTotalTime,
+                        mKernelMemoryStats.size()));
+            }
+        }
+    }
+
+    /**
+     * Read and distribute CPU usage across apps. If their are partial wakelocks being held
+     * and we are on battery with screen off, we give more of the cpu time to those apps holding
+     * wakelocks. If the screen is on, we just assign the actual cpu time an app used.
+     */
+    public void updateCpuTimeLocked() {
+        if (mPowerProfile == null) {
+            return;
+        }
+
+        if (DEBUG_ENERGY_CPU) {
+            Slog.d(TAG, "!Cpu updating!");
+        }
+
+        if (mCpuFreqs == null) {
+            mCpuFreqs = mKernelUidCpuFreqTimeReader.readFreqs(mPowerProfile);
+        }
+
+        // Calculate the wakelocks we have to distribute amongst. The system is excluded as it is
+        // usually holding the wakelock on behalf of an app.
+        // And Only distribute cpu power to wakelocks if the screen is off and we're on battery.
+        ArrayList<StopwatchTimer> partialTimersToConsider = null;
+        if (mOnBatteryScreenOffTimeBase.isRunning()) {
+            partialTimersToConsider = new ArrayList<>();
+            for (int i = mPartialTimers.size() - 1; i >= 0; --i) {
+                final StopwatchTimer timer = mPartialTimers.get(i);
+                // Since the collection and blaming of wakelocks can be scheduled to run after
+                // some delay, the mPartialTimers list may have new entries. We can't blame
+                // the newly added timer for past cpu time, so we only consider timers that
+                // were present for one round of collection. Once a timer has gone through
+                // a round of collection, its mInList field is set to true.
+                if (timer.mInList && timer.mUid != null && timer.mUid.mUid != Process.SYSTEM_UID) {
+                    partialTimersToConsider.add(timer);
+                }
+            }
+        }
+        markPartialTimersAsEligible();
+
+        // When the battery is not on, we don't attribute the cpu times to any timers but we still
+        // need to take the snapshots.
+        if (!mOnBatteryInternal) {
+            mKernelUidCpuTimeReader.readDelta(null);
+            mKernelUidCpuFreqTimeReader.readDelta(null);
+            for (int cluster = mKernelCpuSpeedReaders.length - 1; cluster >= 0; --cluster) {
+                mKernelCpuSpeedReaders[cluster].readDelta();
+            }
+            return;
+        }
+
+        mUserInfoProvider.refreshUserIds();
+        final SparseLongArray updatedUids = mKernelUidCpuFreqTimeReader.perClusterTimesAvailable()
+                ? null : new SparseLongArray();
+        readKernelUidCpuTimesLocked(partialTimersToConsider, updatedUids);
+        // updatedUids=null means /proc/uid_time_in_state provides snapshots of per-cluster cpu
+        // freqs, so no need to approximate these values.
+        if (updatedUids != null) {
+            updateClusterSpeedTimes(updatedUids);
+        }
+        readKernelUidCpuFreqTimesLocked(partialTimersToConsider);
+    }
+
+    /**
+     * Mark the current partial timers as gone through a collection so that they will be
+     * considered in the next cpu times distribution to wakelock holders.
+     */
+    @VisibleForTesting
+    public void markPartialTimersAsEligible() {
+        if (ArrayUtils.referenceEquals(mPartialTimers, mLastPartialTimers)) {
+            // No difference, so each timer is now considered for the next collection.
+            for (int i = mPartialTimers.size() - 1; i >= 0; --i) {
+                mPartialTimers.get(i).mInList = true;
+            }
+        } else {
+            // The lists are different, meaning we added (or removed a timer) since the last
+            // collection.
+            for (int i = mLastPartialTimers.size() - 1; i >= 0; --i) {
+                mLastPartialTimers.get(i).mInList = false;
+            }
+            mLastPartialTimers.clear();
+
+            // Mark the current timers as gone through a collection.
+            final int numPartialTimers = mPartialTimers.size();
+            for (int i = 0; i < numPartialTimers; ++i) {
+                final StopwatchTimer timer = mPartialTimers.get(i);
+                timer.mInList = true;
+                mLastPartialTimers.add(timer);
+            }
+        }
+    }
+
+    /**
+     * Take snapshot of cpu times (aggregated over all uids) at different frequencies and
+     * calculate cpu times spent by each uid at different frequencies.
+     *
+     * @param updatedUids The uids for which times spent at different frequencies are calculated.
+     */
+    @VisibleForTesting
+    public void updateClusterSpeedTimes(@NonNull SparseLongArray updatedUids) {
+        long totalCpuClustersTimeMs = 0;
+        // Read the time spent for each cluster at various cpu frequencies.
+        final long[][] clusterSpeedTimesMs = new long[mKernelCpuSpeedReaders.length][];
+        for (int cluster = 0; cluster < mKernelCpuSpeedReaders.length; cluster++) {
+            clusterSpeedTimesMs[cluster] = mKernelCpuSpeedReaders[cluster].readDelta();
+            if (clusterSpeedTimesMs[cluster] != null) {
+                for (int speed = clusterSpeedTimesMs[cluster].length - 1; speed >= 0; --speed) {
+                    totalCpuClustersTimeMs += clusterSpeedTimesMs[cluster][speed];
+                }
+            }
+        }
+        if (totalCpuClustersTimeMs != 0) {
+            // We have cpu times per freq aggregated over all uids but we need the times per uid.
+            // So, we distribute total time spent by an uid to different cpu freqs based on the
+            // amount of time cpu was running at that freq.
+            final int updatedUidsCount = updatedUids.size();
+            for (int i = 0; i < updatedUidsCount; ++i) {
+                final Uid u = getUidStatsLocked(updatedUids.keyAt(i));
+                final long appCpuTimeUs = updatedUids.valueAt(i);
+                // Add the cpu speeds to this UID.
+                final int numClusters = mPowerProfile.getNumCpuClusters();
+                if (u.mCpuClusterSpeedTimesUs == null ||
+                        u.mCpuClusterSpeedTimesUs.length != numClusters) {
+                    u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][];
+                }
+
+                for (int cluster = 0; cluster < clusterSpeedTimesMs.length; cluster++) {
+                    final int speedsInCluster = clusterSpeedTimesMs[cluster].length;
+                    if (u.mCpuClusterSpeedTimesUs[cluster] == null || speedsInCluster !=
+                            u.mCpuClusterSpeedTimesUs[cluster].length) {
+                        u.mCpuClusterSpeedTimesUs[cluster]
+                                = new LongSamplingCounter[speedsInCluster];
+                    }
+
+                    final LongSamplingCounter[] cpuSpeeds = u.mCpuClusterSpeedTimesUs[cluster];
+                    for (int speed = 0; speed < speedsInCluster; speed++) {
+                        if (cpuSpeeds[speed] == null) {
+                            cpuSpeeds[speed] = new LongSamplingCounter(mOnBatteryTimeBase);
+                        }
+                        cpuSpeeds[speed].addCountLocked(appCpuTimeUs
+                                * clusterSpeedTimesMs[cluster][speed]
+                                / totalCpuClustersTimeMs);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Take a snapshot of the cpu times spent by each uid and update the corresponding counters.
+     * If {@param partialTimers} is not null and empty, then we assign a portion of cpu times to
+     * wakelock holders.
+     *
+     * @param partialTimers The wakelock holders among which the cpu times will be distributed.
+     * @param updatedUids If not null, then the uids found in the snapshot will be added to this.
+     */
+    @VisibleForTesting
+    public void readKernelUidCpuTimesLocked(@Nullable ArrayList<StopwatchTimer> partialTimers,
+            @Nullable SparseLongArray updatedUids) {
+        mTempTotalCpuUserTimeUs = mTempTotalCpuSystemTimeUs = 0;
+        final int numWakelocks = partialTimers == null ? 0 : partialTimers.size();
+        final long startTimeMs = mClocks.uptimeMillis();
+
+        mKernelUidCpuTimeReader.readDelta((uid, userTimeUs, systemTimeUs) -> {
+            uid = mapUid(uid);
+            if (Process.isIsolated(uid)) {
+                // This could happen if the isolated uid mapping was removed before that process
+                // was actually killed.
+                mKernelUidCpuTimeReader.removeUid(uid);
+                Slog.d(TAG, "Got readings for an isolated uid with no mapping: " + uid);
+                return;
+            }
+            if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) {
+                Slog.d(TAG, "Got readings for an invalid user's uid " + uid);
+                mKernelUidCpuTimeReader.removeUid(uid);
+                return;
+            }
+            final Uid u = getUidStatsLocked(uid);
+
+            // Accumulate the total system and user time.
+            mTempTotalCpuUserTimeUs += userTimeUs;
+            mTempTotalCpuSystemTimeUs += systemTimeUs;
+
+            StringBuilder sb = null;
+            if (DEBUG_ENERGY_CPU) {
+                sb = new StringBuilder();
+                sb.append("  got time for uid=").append(u.mUid).append(": u=");
+                TimeUtils.formatDuration(userTimeUs / 1000, sb);
+                sb.append(" s=");
+                TimeUtils.formatDuration(systemTimeUs / 1000, sb);
+                sb.append("\n");
+            }
+
+            if (numWakelocks > 0) {
+                // We have wakelocks being held, so only give a portion of the
+                // time to the process. The rest will be distributed among wakelock
+                // holders.
+                userTimeUs = (userTimeUs * WAKE_LOCK_WEIGHT) / 100;
+                systemTimeUs = (systemTimeUs * WAKE_LOCK_WEIGHT) / 100;
+            }
+
+            if (sb != null) {
+                sb.append("  adding to uid=").append(u.mUid).append(": u=");
+                TimeUtils.formatDuration(userTimeUs / 1000, sb);
+                sb.append(" s=");
+                TimeUtils.formatDuration(systemTimeUs / 1000, sb);
+                Slog.d(TAG, sb.toString());
+            }
+
+            u.mUserCpuTime.addCountLocked(userTimeUs);
+            u.mSystemCpuTime.addCountLocked(systemTimeUs);
+            if (updatedUids != null) {
+                updatedUids.put(u.getUid(), userTimeUs + systemTimeUs);
+            }
+        });
+
+        final long elapsedTimeMs = mClocks.uptimeMillis() - startTimeMs;
+        if (DEBUG_ENERGY_CPU || elapsedTimeMs >= 100) {
+            Slog.d(TAG, "Reading cpu stats took " + elapsedTimeMs + "ms");
+        }
+
+        if (numWakelocks > 0) {
+            // Distribute a portion of the total cpu time to wakelock holders.
+            mTempTotalCpuUserTimeUs = (mTempTotalCpuUserTimeUs * (100 - WAKE_LOCK_WEIGHT)) / 100;
+            mTempTotalCpuSystemTimeUs =
+                    (mTempTotalCpuSystemTimeUs * (100 - WAKE_LOCK_WEIGHT)) / 100;
+
+            for (int i = 0; i < numWakelocks; ++i) {
+                final StopwatchTimer timer = partialTimers.get(i);
+                final int userTimeUs = (int) (mTempTotalCpuUserTimeUs / (numWakelocks - i));
+                final int systemTimeUs = (int) (mTempTotalCpuSystemTimeUs / (numWakelocks - i));
+
+                if (DEBUG_ENERGY_CPU) {
+                    final StringBuilder sb = new StringBuilder();
+                    sb.append("  Distributing wakelock uid=").append(timer.mUid.mUid)
+                            .append(": u=");
+                    TimeUtils.formatDuration(userTimeUs / 1000, sb);
+                    sb.append(" s=");
+                    TimeUtils.formatDuration(systemTimeUs / 1000, sb);
+                    Slog.d(TAG, sb.toString());
+                }
+
+                timer.mUid.mUserCpuTime.addCountLocked(userTimeUs);
+                timer.mUid.mSystemCpuTime.addCountLocked(systemTimeUs);
+                if (updatedUids != null) {
+                    final int uid = timer.mUid.getUid();
+                    updatedUids.put(uid, updatedUids.get(uid, 0) + userTimeUs + systemTimeUs);
+                }
+
+                final Uid.Proc proc = timer.mUid.getProcessStatsLocked("*wakelock*");
+                proc.addCpuTimeLocked(userTimeUs / 1000, systemTimeUs / 1000);
+
+                mTempTotalCpuUserTimeUs -= userTimeUs;
+                mTempTotalCpuSystemTimeUs -= systemTimeUs;
+            }
+        }
+    }
+
+    /**
+     * Take a snapshot of the cpu times spent by each uid in each freq and update the
+     * corresponding counters.
+     *
+     * @param partialTimers The wakelock holders among which the cpu freq times will be distributed.
+     */
+    @VisibleForTesting
+    public void readKernelUidCpuFreqTimesLocked(@Nullable ArrayList<StopwatchTimer> partialTimers) {
+        final boolean perClusterTimesAvailable =
+                mKernelUidCpuFreqTimeReader.perClusterTimesAvailable();
+        final int numWakelocks = partialTimers == null ? 0 : partialTimers.size();
+        final int numClusters = mPowerProfile.getNumCpuClusters();
+        mWakeLockAllocationsUs = null;
+        mKernelUidCpuFreqTimeReader.readDelta((uid, cpuFreqTimeMs) -> {
+            uid = mapUid(uid);
+            if (Process.isIsolated(uid)) {
+                mKernelUidCpuFreqTimeReader.removeUid(uid);
+                Slog.d(TAG, "Got freq readings for an isolated uid with no mapping: " + uid);
+                return;
+            }
+            if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) {
+                Slog.d(TAG, "Got freq readings for an invalid user's uid " + uid);
+                mKernelUidCpuFreqTimeReader.removeUid(uid);
+                return;
+            }
+            final Uid u = getUidStatsLocked(uid);
+            if (u.mCpuFreqTimeMs == null || u.mCpuFreqTimeMs.getSize() != cpuFreqTimeMs.length) {
+                u.mCpuFreqTimeMs = new LongSamplingCounterArray(mOnBatteryTimeBase);
+            }
+            u.mCpuFreqTimeMs.addCountLocked(cpuFreqTimeMs);
+            if (u.mScreenOffCpuFreqTimeMs == null ||
+                    u.mScreenOffCpuFreqTimeMs.getSize() != cpuFreqTimeMs.length) {
+                u.mScreenOffCpuFreqTimeMs = new LongSamplingCounterArray(
+                        mOnBatteryScreenOffTimeBase);
+            }
+            u.mScreenOffCpuFreqTimeMs.addCountLocked(cpuFreqTimeMs);
+
+            if (perClusterTimesAvailable) {
+                if (u.mCpuClusterSpeedTimesUs == null ||
+                        u.mCpuClusterSpeedTimesUs.length != numClusters) {
+                    u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][];
+                }
+                if (numWakelocks > 0 && mWakeLockAllocationsUs == null) {
+                    mWakeLockAllocationsUs = new long[numClusters][];
+                }
+
+                int freqIndex = 0;
+                for (int cluster = 0; cluster < numClusters; ++cluster) {
+                    final int speedsInCluster = mPowerProfile.getNumSpeedStepsInCpuCluster(cluster);
+                    if (u.mCpuClusterSpeedTimesUs[cluster] == null ||
+                            u.mCpuClusterSpeedTimesUs[cluster].length != speedsInCluster) {
+                        u.mCpuClusterSpeedTimesUs[cluster]
+                                = new LongSamplingCounter[speedsInCluster];
+                    }
+                    if (numWakelocks > 0 && mWakeLockAllocationsUs[cluster] == null) {
+                        mWakeLockAllocationsUs[cluster] = new long[speedsInCluster];
+                    }
+                    final LongSamplingCounter[] cpuTimesUs = u.mCpuClusterSpeedTimesUs[cluster];
+                    for (int speed = 0; speed < speedsInCluster; ++speed) {
+                        if (cpuTimesUs[speed] == null) {
+                            cpuTimesUs[speed] = new LongSamplingCounter(mOnBatteryTimeBase);
+                        }
+                        final long appAllocationUs;
+                        if (mWakeLockAllocationsUs != null) {
+                            appAllocationUs =
+                                    (cpuFreqTimeMs[freqIndex] * 1000 * WAKE_LOCK_WEIGHT) / 100;
+                            mWakeLockAllocationsUs[cluster][speed] +=
+                                    (cpuFreqTimeMs[freqIndex] * 1000 - appAllocationUs);
+                        } else {
+                            appAllocationUs = cpuFreqTimeMs[freqIndex] * 1000;
+                        }
+                        cpuTimesUs[speed].addCountLocked(appAllocationUs);
+                        freqIndex++;
+                    }
+                }
+            }
+        });
+
+        if (mWakeLockAllocationsUs != null) {
+            for (int i = 0; i < numWakelocks; ++i) {
+                final Uid u = partialTimers.get(i).mUid;
+                if (u.mCpuClusterSpeedTimesUs == null ||
+                        u.mCpuClusterSpeedTimesUs.length != numClusters) {
+                    u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][];
+                }
+
+                for (int cluster = 0; cluster < numClusters; ++cluster) {
+                    final int speedsInCluster = mPowerProfile.getNumSpeedStepsInCpuCluster(cluster);
+                    if (u.mCpuClusterSpeedTimesUs[cluster] == null ||
+                            u.mCpuClusterSpeedTimesUs[cluster].length != speedsInCluster) {
+                        u.mCpuClusterSpeedTimesUs[cluster]
+                                = new LongSamplingCounter[speedsInCluster];
+                    }
+                    final LongSamplingCounter[] cpuTimeUs = u.mCpuClusterSpeedTimesUs[cluster];
+                    for (int speed = 0; speed < speedsInCluster; ++speed) {
+                        if (cpuTimeUs[speed] == null) {
+                            cpuTimeUs[speed] = new LongSamplingCounter(mOnBatteryTimeBase);
+                        }
+                        final long allocationUs =
+                                mWakeLockAllocationsUs[cluster][speed] / (numWakelocks - i);
+                        cpuTimeUs[speed].addCountLocked(allocationUs);
+                        mWakeLockAllocationsUs[cluster][speed] -= allocationUs;
+                    }
+                }
+            }
+        }
+    }
+
+    boolean setChargingLocked(boolean charging) {
+        if (mCharging != charging) {
+            mCharging = charging;
+            if (charging) {
+                mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
+            } else {
+                mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
+            }
+            mHandler.sendEmptyMessage(MSG_REPORT_CHARGING);
+            return true;
+        }
+        return false;
+    }
+
+    void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime, final boolean onBattery,
+            final int oldStatus, final int level, final int chargeUAh) {
+        boolean doWrite = false;
+        Message m = mHandler.obtainMessage(MSG_REPORT_POWER_CHANGE);
+        m.arg1 = onBattery ? 1 : 0;
+        mHandler.sendMessage(m);
+
+        final long uptime = mSecUptime * 1000;
+        final long realtime = mSecRealtime * 1000;
+        final boolean screenOn = mScreenState == Display.STATE_ON;
+        if (onBattery) {
+            // We will reset our status if we are unplugging after the
+            // battery was last full, or the level is at 100, or
+            // we have gone through a significant charge (from a very low
+            // level to a now very high level).
+            boolean reset = false;
+            if (!mNoAutoReset && (oldStatus == BatteryManager.BATTERY_STATUS_FULL
+                    || level >= 90
+                    || (mDischargeCurrentLevel < 20 && level >= 80)
+                    || (getHighDischargeAmountSinceCharge() >= 200
+                            && mHistoryBuffer.dataSize() >= MAX_HISTORY_BUFFER))) {
+                Slog.i(TAG, "Resetting battery stats: level=" + level + " status=" + oldStatus
+                        + " dischargeLevel=" + mDischargeCurrentLevel
+                        + " lowAmount=" + getLowDischargeAmountSinceCharge()
+                        + " highAmount=" + getHighDischargeAmountSinceCharge());
+                // Before we write, collect a snapshot of the final aggregated
+                // stats to be reported in the next checkin.  Only do this if we have
+                // a sufficient amount of data to make it interesting.
+                if (getLowDischargeAmountSinceCharge() >= 20) {
+                    final Parcel parcel = Parcel.obtain();
+                    writeSummaryToParcel(parcel, true);
+                    BackgroundThread.getHandler().post(new Runnable() {
+                        @Override public void run() {
+                            synchronized (mCheckinFile) {
+                                FileOutputStream stream = null;
+                                try {
+                                    stream = mCheckinFile.startWrite();
+                                    stream.write(parcel.marshall());
+                                    stream.flush();
+                                    FileUtils.sync(stream);
+                                    stream.close();
+                                    mCheckinFile.finishWrite(stream);
+                                } catch (IOException e) {
+                                    Slog.w("BatteryStats",
+                                            "Error writing checkin battery statistics", e);
+                                    mCheckinFile.failWrite(stream);
+                                } finally {
+                                    parcel.recycle();
+                                }
+                            }
+                        }
+                    });
+                }
+                doWrite = true;
+                resetAllStatsLocked();
+                if (chargeUAh > 0 && level > 0) {
+                    // Only use the reported coulomb charge value if it is supported and reported.
+                    mEstimatedBatteryCapacity = (int) ((chargeUAh / 1000) / (level / 100.0));
+                }
+                mDischargeStartLevel = level;
+                reset = true;
+                mDischargeStepTracker.init();
+            }
+            if (mCharging) {
+                setChargingLocked(false);
+            }
+            mLastChargingStateLevel = level;
+            mOnBattery = mOnBatteryInternal = true;
+            mLastDischargeStepLevel = level;
+            mMinDischargeStepLevel = level;
+            mDischargeStepTracker.clearTime();
+            mDailyDischargeStepTracker.clearTime();
+            mInitStepMode = mCurStepMode;
+            mModStepMode = 0;
+            pullPendingStateUpdatesLocked();
+            mHistoryCur.batteryLevel = (byte)level;
+            mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Battery unplugged to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            if (reset) {
+                mRecordingHistory = true;
+                startRecordingHistory(mSecRealtime, mSecUptime, reset);
+            }
+            addHistoryRecordLocked(mSecRealtime, mSecUptime);
+            mDischargeCurrentLevel = mDischargeUnplugLevel = level;
+            if (screenOn) {
+                mDischargeScreenOnUnplugLevel = level;
+                mDischargeScreenOffUnplugLevel = 0;
+            } else {
+                mDischargeScreenOnUnplugLevel = 0;
+                mDischargeScreenOffUnplugLevel = level;
+            }
+            mDischargeAmountScreenOn = 0;
+            mDischargeAmountScreenOff = 0;
+            updateTimeBasesLocked(true, !screenOn, uptime, realtime);
+        } else {
+            mLastChargingStateLevel = level;
+            mOnBattery = mOnBatteryInternal = false;
+            pullPendingStateUpdatesLocked();
+            mHistoryCur.batteryLevel = (byte)level;
+            mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+            if (DEBUG_HISTORY) Slog.v(TAG, "Battery plugged to: "
+                    + Integer.toHexString(mHistoryCur.states));
+            addHistoryRecordLocked(mSecRealtime, mSecUptime);
+            mDischargeCurrentLevel = mDischargePlugLevel = level;
+            if (level < mDischargeUnplugLevel) {
+                mLowDischargeAmountSinceCharge += mDischargeUnplugLevel-level-1;
+                mHighDischargeAmountSinceCharge += mDischargeUnplugLevel-level;
+            }
+            updateDischargeScreenLevelsLocked(screenOn, screenOn);
+            updateTimeBasesLocked(false, !screenOn, uptime, realtime);
+            mChargeStepTracker.init();
+            mLastChargeStepLevel = level;
+            mMaxChargeStepLevel = level;
+            mInitStepMode = mCurStepMode;
+            mModStepMode = 0;
+        }
+        if (doWrite || (mLastWriteTime + (60 * 1000)) < mSecRealtime) {
+            if (mFile != null) {
+                writeAsyncLocked();
+            }
+        }
+    }
+
+    private void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs,
+            boolean reset) {
+        mRecordingHistory = true;
+        mHistoryCur.currentTime = System.currentTimeMillis();
+        addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs,
+                reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME,
+                mHistoryCur);
+        mHistoryCur.currentTime = 0;
+        if (reset) {
+            initActiveHistoryEventsLocked(elapsedRealtimeMs, uptimeMs);
+        }
+    }
+
+    private void recordCurrentTimeChangeLocked(final long currentTime, final long elapsedRealtimeMs,
+            final long uptimeMs) {
+        if (mRecordingHistory) {
+            mHistoryCur.currentTime = currentTime;
+            addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_CURRENT_TIME,
+                    mHistoryCur);
+            mHistoryCur.currentTime = 0;
+        }
+    }
+
+    private void recordShutdownLocked(final long elapsedRealtimeMs, final long uptimeMs) {
+        if (mRecordingHistory) {
+            mHistoryCur.currentTime = System.currentTimeMillis();
+            addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_SHUTDOWN,
+                    mHistoryCur);
+            mHistoryCur.currentTime = 0;
+        }
+    }
+
+    private void scheduleSyncExternalStatsLocked(String reason, int updateFlags) {
+        if (mExternalSync != null) {
+            mExternalSync.scheduleSync(reason, updateFlags);
+        }
+    }
+
+    // This should probably be exposed in the API, though it's not critical
+    public static final int BATTERY_PLUGGED_NONE = 0;
+
+    public void setBatteryStateLocked(int status, int health, int plugType, int level,
+            int temp, int volt, int chargeUAh, int chargeFullUAh) {
+        // Temperature is encoded without the signed bit, so clamp any negative temperatures to 0.
+        temp = Math.max(0, temp);
+
+        final boolean onBattery = plugType == BATTERY_PLUGGED_NONE;
+        final long uptime = mClocks.uptimeMillis();
+        final long elapsedRealtime = mClocks.elapsedRealtime();
+        if (!mHaveBatteryLevel) {
+            mHaveBatteryLevel = true;
+            // We start out assuming that the device is plugged in (not
+            // on battery).  If our first report is now that we are indeed
+            // plugged in, then twiddle our state to correctly reflect that
+            // since we won't be going through the full setOnBattery().
+            if (onBattery == mOnBattery) {
+                if (onBattery) {
+                    mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+                } else {
+                    mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+                }
+            }
+            // Always start out assuming charging, that will be updated later.
+            mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
+            mHistoryCur.batteryStatus = (byte)status;
+            mHistoryCur.batteryLevel = (byte)level;
+            mHistoryCur.batteryChargeUAh = chargeUAh;
+            mMaxChargeStepLevel = mMinDischargeStepLevel =
+                    mLastChargeStepLevel = mLastDischargeStepLevel = level;
+            mLastChargingStateLevel = level;
+        } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) {
+            recordDailyStatsIfNeededLocked(level >= 100 && onBattery);
+        }
+        int oldStatus = mHistoryCur.batteryStatus;
+        if (onBattery) {
+            mDischargeCurrentLevel = level;
+            if (!mRecordingHistory) {
+                mRecordingHistory = true;
+                startRecordingHistory(elapsedRealtime, uptime, true);
+            }
+        } else if (level < 96) {
+            if (!mRecordingHistory) {
+                mRecordingHistory = true;
+                startRecordingHistory(elapsedRealtime, uptime, true);
+            }
+        }
+        mCurrentBatteryLevel = level;
+        if (mDischargePlugLevel < 0) {
+            mDischargePlugLevel = level;
+        }
+
+        if (onBattery != mOnBattery) {
+            mHistoryCur.batteryLevel = (byte)level;
+            mHistoryCur.batteryStatus = (byte)status;
+            mHistoryCur.batteryHealth = (byte)health;
+            mHistoryCur.batteryPlugType = (byte)plugType;
+            mHistoryCur.batteryTemperature = (short)temp;
+            mHistoryCur.batteryVoltage = (char)volt;
+            if (chargeUAh < mHistoryCur.batteryChargeUAh) {
+                // Only record discharges
+                final long chargeDiff = mHistoryCur.batteryChargeUAh - chargeUAh;
+                mDischargeCounter.addCountLocked(chargeDiff);
+                mDischargeScreenOffCounter.addCountLocked(chargeDiff);
+            }
+            mHistoryCur.batteryChargeUAh = chargeUAh;
+            setOnBatteryLocked(elapsedRealtime, uptime, onBattery, oldStatus, level, chargeUAh);
+        } else {
+            boolean changed = false;
+            if (mHistoryCur.batteryLevel != level) {
+                mHistoryCur.batteryLevel = (byte)level;
+                changed = true;
+
+                // TODO(adamlesinski): Schedule the creation of a HistoryStepDetails record
+                // which will pull external stats.
+                scheduleSyncExternalStatsLocked("battery-level", ExternalStatsSync.UPDATE_ALL);
+            }
+            if (mHistoryCur.batteryStatus != status) {
+                mHistoryCur.batteryStatus = (byte)status;
+                changed = true;
+            }
+            if (mHistoryCur.batteryHealth != health) {
+                mHistoryCur.batteryHealth = (byte)health;
+                changed = true;
+            }
+            if (mHistoryCur.batteryPlugType != plugType) {
+                mHistoryCur.batteryPlugType = (byte)plugType;
+                changed = true;
+            }
+            if (temp >= (mHistoryCur.batteryTemperature+10)
+                    || temp <= (mHistoryCur.batteryTemperature-10)) {
+                mHistoryCur.batteryTemperature = (short)temp;
+                changed = true;
+            }
+            if (volt > (mHistoryCur.batteryVoltage+20)
+                    || volt < (mHistoryCur.batteryVoltage-20)) {
+                mHistoryCur.batteryVoltage = (char)volt;
+                changed = true;
+            }
+            if (chargeUAh >= (mHistoryCur.batteryChargeUAh+10)
+                    || chargeUAh <= (mHistoryCur.batteryChargeUAh-10)) {
+                if (chargeUAh < mHistoryCur.batteryChargeUAh) {
+                    // Only record discharges
+                    final long chargeDiff = mHistoryCur.batteryChargeUAh - chargeUAh;
+                    mDischargeCounter.addCountLocked(chargeDiff);
+                    mDischargeScreenOffCounter.addCountLocked(chargeDiff);
+                }
+                mHistoryCur.batteryChargeUAh = chargeUAh;
+                changed = true;
+            }
+            long modeBits = (((long)mInitStepMode) << STEP_LEVEL_INITIAL_MODE_SHIFT)
+                    | (((long)mModStepMode) << STEP_LEVEL_MODIFIED_MODE_SHIFT)
+                    | (((long)(level&0xff)) << STEP_LEVEL_LEVEL_SHIFT);
+            if (onBattery) {
+                changed |= setChargingLocked(false);
+                if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) {
+                    mDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level,
+                            modeBits, elapsedRealtime);
+                    mDailyDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level,
+                            modeBits, elapsedRealtime);
+                    mLastDischargeStepLevel = level;
+                    mMinDischargeStepLevel = level;
+                    mInitStepMode = mCurStepMode;
+                    mModStepMode = 0;
+                }
+            } else {
+                if (level >= 90) {
+                    // If the battery level is at least 90%, always consider the device to be
+                    // charging even if it happens to go down a level.
+                    changed |= setChargingLocked(true);
+                    mLastChargeStepLevel = level;
+                } if (!mCharging) {
+                    if (mLastChargeStepLevel < level) {
+                        // We have not reporting that we are charging, but the level has now
+                        // gone up, so consider the state to be charging.
+                        changed |= setChargingLocked(true);
+                        mLastChargeStepLevel = level;
+                    }
+                } else {
+                    if (mLastChargeStepLevel > level) {
+                        // We had reported that the device was charging, but here we are with
+                        // power connected and the level going down.  Looks like the current
+                        // power supplied isn't enough, so consider the device to now be
+                        // discharging.
+                        changed |= setChargingLocked(false);
+                        mLastChargeStepLevel = level;
+                    }
+                }
+                if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) {
+                    mChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel,
+                            modeBits, elapsedRealtime);
+                    mDailyChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel,
+                            modeBits, elapsedRealtime);
+                    mLastChargeStepLevel = level;
+                    mMaxChargeStepLevel = level;
+                    mInitStepMode = mCurStepMode;
+                    mModStepMode = 0;
+                }
+            }
+            if (changed) {
+                addHistoryRecordLocked(elapsedRealtime, uptime);
+            }
+        }
+        if (!onBattery && status == BatteryManager.BATTERY_STATUS_FULL) {
+            // We don't record history while we are plugged in and fully charged.
+            // The next time we are unplugged, history will be cleared.
+            mRecordingHistory = DEBUG;
+        }
+
+        if (mMinLearnedBatteryCapacity == -1) {
+            mMinLearnedBatteryCapacity = chargeFullUAh;
+        } else {
+            Math.min(mMinLearnedBatteryCapacity, chargeFullUAh);
+        }
+        mMaxLearnedBatteryCapacity = Math.max(mMaxLearnedBatteryCapacity, chargeFullUAh);
+    }
+
+    public long getAwakeTimeBattery() {
+        return computeBatteryUptime(getBatteryUptimeLocked(), STATS_CURRENT);
+    }
+
+    public long getAwakeTimePlugged() {
+        return (mClocks.uptimeMillis() * 1000) - getAwakeTimeBattery();
+    }
+
+    @Override
+    public long computeUptime(long curTime, int which) {
+        switch (which) {
+            case STATS_SINCE_CHARGED: return mUptime + (curTime-mUptimeStart);
+            case STATS_CURRENT: return (curTime-mUptimeStart);
+            case STATS_SINCE_UNPLUGGED: return (curTime-mOnBatteryTimeBase.getUptimeStart());
+        }
+        return 0;
+    }
+
+    @Override
+    public long computeRealtime(long curTime, int which) {
+        switch (which) {
+            case STATS_SINCE_CHARGED: return mRealtime + (curTime-mRealtimeStart);
+            case STATS_CURRENT: return (curTime-mRealtimeStart);
+            case STATS_SINCE_UNPLUGGED: return (curTime-mOnBatteryTimeBase.getRealtimeStart());
+        }
+        return 0;
+    }
+
+    @Override
+    public long computeBatteryUptime(long curTime, int which) {
+        return mOnBatteryTimeBase.computeUptime(curTime, which);
+    }
+
+    @Override
+    public long computeBatteryRealtime(long curTime, int which) {
+        return mOnBatteryTimeBase.computeRealtime(curTime, which);
+    }
+
+    @Override
+    public long computeBatteryScreenOffUptime(long curTime, int which) {
+        return mOnBatteryScreenOffTimeBase.computeUptime(curTime, which);
+    }
+
+    @Override
+    public long computeBatteryScreenOffRealtime(long curTime, int which) {
+        return mOnBatteryScreenOffTimeBase.computeRealtime(curTime, which);
+    }
+
+    private long computeTimePerLevel(long[] steps, int numSteps) {
+        // For now we'll do a simple average across all steps.
+        if (numSteps <= 0) {
+            return -1;
+        }
+        long total = 0;
+        for (int i=0; i<numSteps; i++) {
+            total += steps[i] & STEP_LEVEL_TIME_MASK;
+        }
+        return total / numSteps;
+        /*
+        long[] buckets = new long[numSteps];
+        int numBuckets = 0;
+        int numToAverage = 4;
+        int i = 0;
+        while (i < numSteps) {
+            long totalTime = 0;
+            int num = 0;
+            for (int j=0; j<numToAverage && (i+j)<numSteps; j++) {
+                totalTime += steps[i+j] & STEP_LEVEL_TIME_MASK;
+                num++;
+            }
+            buckets[numBuckets] = totalTime / num;
+            numBuckets++;
+            numToAverage *= 2;
+            i += num;
+        }
+        if (numBuckets < 1) {
+            return -1;
+        }
+        long averageTime = buckets[numBuckets-1];
+        for (i=numBuckets-2; i>=0; i--) {
+            averageTime = (averageTime + buckets[i]) / 2;
+        }
+        return averageTime;
+        */
+    }
+
+    @Override
+    public long computeBatteryTimeRemaining(long curTime) {
+        if (!mOnBattery) {
+            return -1;
+        }
+        /* Simple implementation just looks at the average discharge per level across the
+           entire sample period.
+        int discharge = (getLowDischargeAmountSinceCharge()+getHighDischargeAmountSinceCharge())/2;
+        if (discharge < 2) {
+            return -1;
+        }
+        long duration = computeBatteryRealtime(curTime, STATS_SINCE_CHARGED);
+        if (duration < 1000*1000) {
+            return -1;
+        }
+        long usPerLevel = duration/discharge;
+        return usPerLevel * mCurrentBatteryLevel;
+        */
+        if (mDischargeStepTracker.mNumStepDurations < 1) {
+            return -1;
+        }
+        long msPerLevel = mDischargeStepTracker.computeTimePerLevel();
+        if (msPerLevel <= 0) {
+            return -1;
+        }
+        return (msPerLevel * mCurrentBatteryLevel) * 1000;
+    }
+
+    @Override
+    public LevelStepTracker getDischargeLevelStepTracker() {
+        return mDischargeStepTracker;
+    }
+
+    @Override
+    public LevelStepTracker getDailyDischargeLevelStepTracker() {
+        return mDailyDischargeStepTracker;
+    }
+
+    @Override
+    public long computeChargeTimeRemaining(long curTime) {
+        if (mOnBattery) {
+            // Not yet working.
+            return -1;
+        }
+        /* Broken
+        int curLevel = mCurrentBatteryLevel;
+        int plugLevel = mDischargePlugLevel;
+        if (plugLevel < 0 || curLevel < (plugLevel+1)) {
+            return -1;
+        }
+        long duration = computeBatteryRealtime(curTime, STATS_SINCE_UNPLUGGED);
+        if (duration < 1000*1000) {
+            return -1;
+        }
+        long usPerLevel = duration/(curLevel-plugLevel);
+        return usPerLevel * (100-curLevel);
+        */
+        if (mChargeStepTracker.mNumStepDurations < 1) {
+            return -1;
+        }
+        long msPerLevel = mChargeStepTracker.computeTimePerLevel();
+        if (msPerLevel <= 0) {
+            return -1;
+        }
+        return (msPerLevel * (100-mCurrentBatteryLevel)) * 1000;
+    }
+
+    @Override
+    public LevelStepTracker getChargeLevelStepTracker() {
+        return mChargeStepTracker;
+    }
+
+    @Override
+    public LevelStepTracker getDailyChargeLevelStepTracker() {
+        return mDailyChargeStepTracker;
+    }
+
+    @Override
+    public ArrayList<PackageChange> getDailyPackageChanges() {
+        return mDailyPackageChanges;
+    }
+
+    protected long getBatteryUptimeLocked() {
+        return mOnBatteryTimeBase.getUptime(mClocks.uptimeMillis() * 1000);
+    }
+
+    @Override
+    public long getBatteryUptime(long curTime) {
+        return mOnBatteryTimeBase.getUptime(curTime);
+    }
+
+    @Override
+    public long getBatteryRealtime(long curTime) {
+        return mOnBatteryTimeBase.getRealtime(curTime);
+    }
+
+    @Override
+    public int getDischargeStartLevel() {
+        synchronized(this) {
+            return getDischargeStartLevelLocked();
+        }
+    }
+
+    public int getDischargeStartLevelLocked() {
+            return mDischargeUnplugLevel;
+    }
+
+    @Override
+    public int getDischargeCurrentLevel() {
+        synchronized(this) {
+            return getDischargeCurrentLevelLocked();
+        }
+    }
+
+    public int getDischargeCurrentLevelLocked() {
+        return mDischargeCurrentLevel;
+    }
+
+    @Override
+    public int getLowDischargeAmountSinceCharge() {
+        synchronized(this) {
+            int val = mLowDischargeAmountSinceCharge;
+            if (mOnBattery && mDischargeCurrentLevel < mDischargeUnplugLevel) {
+                val += mDischargeUnplugLevel-mDischargeCurrentLevel-1;
+            }
+            return val;
+        }
+    }
+
+    @Override
+    public int getHighDischargeAmountSinceCharge() {
+        synchronized(this) {
+            int val = mHighDischargeAmountSinceCharge;
+            if (mOnBattery && mDischargeCurrentLevel < mDischargeUnplugLevel) {
+                val += mDischargeUnplugLevel-mDischargeCurrentLevel;
+            }
+            return val;
+        }
+    }
+
+    @Override
+    public int getDischargeAmount(int which) {
+        int dischargeAmount = which == STATS_SINCE_CHARGED
+                ? getHighDischargeAmountSinceCharge()
+                : (getDischargeStartLevel() - getDischargeCurrentLevel());
+        if (dischargeAmount < 0) {
+            dischargeAmount = 0;
+        }
+        return dischargeAmount;
+    }
+
+    public int getDischargeAmountScreenOn() {
+        synchronized(this) {
+            int val = mDischargeAmountScreenOn;
+            if (mOnBattery && mScreenState == Display.STATE_ON
+                    && mDischargeCurrentLevel < mDischargeScreenOnUnplugLevel) {
+                val += mDischargeScreenOnUnplugLevel-mDischargeCurrentLevel;
+            }
+            return val;
+        }
+    }
+
+    public int getDischargeAmountScreenOnSinceCharge() {
+        synchronized(this) {
+            int val = mDischargeAmountScreenOnSinceCharge;
+            if (mOnBattery && mScreenState == Display.STATE_ON
+                    && mDischargeCurrentLevel < mDischargeScreenOnUnplugLevel) {
+                val += mDischargeScreenOnUnplugLevel-mDischargeCurrentLevel;
+            }
+            return val;
+        }
+    }
+
+    public int getDischargeAmountScreenOff() {
+        synchronized(this) {
+            int val = mDischargeAmountScreenOff;
+            if (mOnBattery && mScreenState != Display.STATE_ON
+                    && mDischargeCurrentLevel < mDischargeScreenOffUnplugLevel) {
+                val += mDischargeScreenOffUnplugLevel-mDischargeCurrentLevel;
+            }
+            return val;
+        }
+    }
+
+    public int getDischargeAmountScreenOffSinceCharge() {
+        synchronized(this) {
+            int val = mDischargeAmountScreenOffSinceCharge;
+            if (mOnBattery && mScreenState != Display.STATE_ON
+                    && mDischargeCurrentLevel < mDischargeScreenOffUnplugLevel) {
+                val += mDischargeScreenOffUnplugLevel-mDischargeCurrentLevel;
+            }
+            return val;
+        }
+    }
+
+    /**
+     * Retrieve the statistics object for a particular uid, creating if needed.
+     */
+    public Uid getUidStatsLocked(int uid) {
+        Uid u = mUidStats.get(uid);
+        if (u == null) {
+            u = new Uid(this, uid);
+            mUidStats.put(uid, u);
+        }
+        return u;
+    }
+
+    public void onCleanupUserLocked(int userId) {
+        final int firstUidForUser = UserHandle.getUid(userId, 0);
+        final int lastUidForUser = UserHandle.getUid(userId, UserHandle.PER_USER_RANGE - 1);
+        mKernelUidCpuFreqTimeReader.removeUidsInRange(firstUidForUser, lastUidForUser);
+        mKernelUidCpuTimeReader.removeUidsInRange(firstUidForUser, lastUidForUser);
+    }
+
+    public void onUserRemovedLocked(int userId) {
+        final int firstUidForUser = UserHandle.getUid(userId, 0);
+        final int lastUidForUser = UserHandle.getUid(userId, UserHandle.PER_USER_RANGE - 1);
+        mUidStats.put(firstUidForUser, null);
+        mUidStats.put(lastUidForUser, null);
+        final int firstIndex = mUidStats.indexOfKey(firstUidForUser);
+        final int lastIndex = mUidStats.indexOfKey(lastUidForUser);
+        mUidStats.removeAtRange(firstIndex, lastIndex - firstIndex + 1);
+    }
+
+    /**
+     * Remove the statistics object for a particular uid.
+     */
+    public void removeUidStatsLocked(int uid) {
+        mKernelUidCpuTimeReader.removeUid(uid);
+        mKernelUidCpuFreqTimeReader.removeUid(uid);
+        mUidStats.remove(uid);
+    }
+
+    /**
+     * Retrieve the statistics object for a particular process, creating
+     * if needed.
+     */
+    public Uid.Proc getProcessStatsLocked(int uid, String name) {
+        uid = mapUid(uid);
+        Uid u = getUidStatsLocked(uid);
+        return u.getProcessStatsLocked(name);
+    }
+
+    /**
+     * Retrieve the statistics object for a particular process, creating
+     * if needed.
+     */
+    public Uid.Pkg getPackageStatsLocked(int uid, String pkg) {
+        uid = mapUid(uid);
+        Uid u = getUidStatsLocked(uid);
+        return u.getPackageStatsLocked(pkg);
+    }
+
+    /**
+     * Retrieve the statistics object for a particular service, creating
+     * if needed.
+     */
+    public Uid.Pkg.Serv getServiceStatsLocked(int uid, String pkg, String name) {
+        uid = mapUid(uid);
+        Uid u = getUidStatsLocked(uid);
+        return u.getServiceStatsLocked(pkg, name);
+    }
+
+    public void shutdownLocked() {
+        recordShutdownLocked(mClocks.elapsedRealtime(), mClocks.uptimeMillis());
+        writeSyncLocked();
+        mShuttingDown = true;
+    }
+
+    Parcel mPendingWrite = null;
+    final ReentrantLock mWriteLock = new ReentrantLock();
+
+    public void writeAsyncLocked() {
+        writeLocked(false);
+    }
+
+    public void writeSyncLocked() {
+        writeLocked(true);
+    }
+
+    void writeLocked(boolean sync) {
+        if (mFile == null) {
+            Slog.w("BatteryStats", "writeLocked: no file associated with this instance");
+            return;
+        }
+
+        if (mShuttingDown) {
+            return;
+        }
+
+        Parcel out = Parcel.obtain();
+        writeSummaryToParcel(out, true);
+        mLastWriteTime = mClocks.elapsedRealtime();
+
+        if (mPendingWrite != null) {
+            mPendingWrite.recycle();
+        }
+        mPendingWrite = out;
+
+        if (sync) {
+            commitPendingDataToDisk();
+        } else {
+            BackgroundThread.getHandler().post(new Runnable() {
+                @Override public void run() {
+                    commitPendingDataToDisk();
+                }
+            });
+        }
+    }
+
+    public void commitPendingDataToDisk() {
+        final Parcel next;
+        synchronized (this) {
+            next = mPendingWrite;
+            mPendingWrite = null;
+            if (next == null) {
+                return;
+            }
+        }
+
+        mWriteLock.lock();
+        try {
+            FileOutputStream stream = new FileOutputStream(mFile.chooseForWrite());
+            stream.write(next.marshall());
+            stream.flush();
+            FileUtils.sync(stream);
+            stream.close();
+            mFile.commit();
+        } catch (IOException e) {
+            Slog.w("BatteryStats", "Error writing battery statistics", e);
+            mFile.rollback();
+        } finally {
+            next.recycle();
+            mWriteLock.unlock();
+        }
+    }
+
+    public void readLocked() {
+        if (mDailyFile != null) {
+            readDailyStatsLocked();
+        }
+
+        if (mFile == null) {
+            Slog.w("BatteryStats", "readLocked: no file associated with this instance");
+            return;
+        }
+
+        mUidStats.clear();
+
+        try {
+            File file = mFile.chooseForRead();
+            if (!file.exists()) {
+                return;
+            }
+            FileInputStream stream = new FileInputStream(file);
+
+            byte[] raw = BatteryStatsHelper.readFully(stream);
+            Parcel in = Parcel.obtain();
+            in.unmarshall(raw, 0, raw.length);
+            in.setDataPosition(0);
+            stream.close();
+
+            readSummaryFromParcel(in);
+        } catch(Exception e) {
+            Slog.e("BatteryStats", "Error reading battery statistics", e);
+            resetAllStatsLocked();
+        }
+
+        mEndPlatformVersion = Build.ID;
+
+        if (mHistoryBuffer.dataPosition() > 0) {
+            mRecordingHistory = true;
+            final long elapsedRealtime = mClocks.elapsedRealtime();
+            final long uptime = mClocks.uptimeMillis();
+            if (USE_OLD_HISTORY) {
+                addHistoryRecordLocked(elapsedRealtime, uptime, HistoryItem.CMD_START, mHistoryCur);
+            }
+            addHistoryBufferLocked(elapsedRealtime, uptime, HistoryItem.CMD_START, mHistoryCur);
+            startRecordingHistory(elapsedRealtime, uptime, false);
+        }
+
+        recordDailyStatsIfNeededLocked(false);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    void readHistory(Parcel in, boolean andOldHistory) throws ParcelFormatException {
+        final long historyBaseTime = in.readLong();
+
+        mHistoryBuffer.setDataSize(0);
+        mHistoryBuffer.setDataPosition(0);
+        mHistoryTagPool.clear();
+        mNextHistoryTagIdx = 0;
+        mNumHistoryTagChars = 0;
+
+        int numTags = in.readInt();
+        for (int i=0; i<numTags; i++) {
+            int idx = in.readInt();
+            String str = in.readString();
+            if (str == null) {
+                throw new ParcelFormatException("null history tag string");
+            }
+            int uid = in.readInt();
+            HistoryTag tag = new HistoryTag();
+            tag.string = str;
+            tag.uid = uid;
+            tag.poolIdx = idx;
+            mHistoryTagPool.put(tag, idx);
+            if (idx >= mNextHistoryTagIdx) {
+                mNextHistoryTagIdx = idx+1;
+            }
+            mNumHistoryTagChars += tag.string.length() + 1;
+        }
+
+        int bufSize = in.readInt();
+        int curPos = in.dataPosition();
+        if (bufSize >= (MAX_MAX_HISTORY_BUFFER*3)) {
+            throw new ParcelFormatException("File corrupt: history data buffer too large " +
+                    bufSize);
+        } else if ((bufSize&~3) != bufSize) {
+            throw new ParcelFormatException("File corrupt: history data buffer not aligned " +
+                    bufSize);
+        } else {
+            if (DEBUG_HISTORY) Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
+                    + " bytes at " + curPos);
+            mHistoryBuffer.appendFrom(in, curPos, bufSize);
+            in.setDataPosition(curPos + bufSize);
+        }
+
+        if (andOldHistory) {
+            readOldHistory(in);
+        }
+
+        if (DEBUG_HISTORY) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("****************** OLD mHistoryBaseTime: ");
+            TimeUtils.formatDuration(mHistoryBaseTime, sb);
+            Slog.i(TAG, sb.toString());
+        }
+        mHistoryBaseTime = historyBaseTime;
+        if (DEBUG_HISTORY) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("****************** NEW mHistoryBaseTime: ");
+            TimeUtils.formatDuration(mHistoryBaseTime, sb);
+            Slog.i(TAG, sb.toString());
+        }
+
+        // We are just arbitrarily going to insert 1 minute from the sample of
+        // the last run until samples in this run.
+        if (mHistoryBaseTime > 0) {
+            long oldnow = mClocks.elapsedRealtime();
+            mHistoryBaseTime = mHistoryBaseTime - oldnow + 1;
+            if (DEBUG_HISTORY) {
+                StringBuilder sb = new StringBuilder(128);
+                sb.append("****************** ADJUSTED mHistoryBaseTime: ");
+                TimeUtils.formatDuration(mHistoryBaseTime, sb);
+                Slog.i(TAG, sb.toString());
+            }
+        }
+    }
+
+    void readOldHistory(Parcel in) {
+        if (!USE_OLD_HISTORY) {
+            return;
+        }
+        mHistory = mHistoryEnd = mHistoryCache = null;
+        long time;
+        while (in.dataAvail() > 0 && (time=in.readLong()) >= 0) {
+            HistoryItem rec = new HistoryItem(time, in);
+            addHistoryRecordLocked(rec);
+        }
+    }
+
+    void writeHistory(Parcel out, boolean inclData, boolean andOldHistory) {
+        if (DEBUG_HISTORY) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("****************** WRITING mHistoryBaseTime: ");
+            TimeUtils.formatDuration(mHistoryBaseTime, sb);
+            sb.append(" mLastHistoryElapsedRealtime: ");
+            TimeUtils.formatDuration(mLastHistoryElapsedRealtime, sb);
+            Slog.i(TAG, sb.toString());
+        }
+        out.writeLong(mHistoryBaseTime + mLastHistoryElapsedRealtime);
+        if (!inclData) {
+            out.writeInt(0);
+            out.writeInt(0);
+            return;
+        }
+        out.writeInt(mHistoryTagPool.size());
+        for (HashMap.Entry<HistoryTag, Integer> ent : mHistoryTagPool.entrySet()) {
+            HistoryTag tag = ent.getKey();
+            out.writeInt(ent.getValue());
+            out.writeString(tag.string);
+            out.writeInt(tag.uid);
+        }
+        out.writeInt(mHistoryBuffer.dataSize());
+        if (DEBUG_HISTORY) Slog.i(TAG, "***************** WRITING HISTORY: "
+                + mHistoryBuffer.dataSize() + " bytes at " + out.dataPosition());
+        out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+
+        if (andOldHistory) {
+            writeOldHistory(out);
+        }
+    }
+
+    void writeOldHistory(Parcel out) {
+        if (!USE_OLD_HISTORY) {
+            return;
+        }
+        HistoryItem rec = mHistory;
+        while (rec != null) {
+            if (rec.time >= 0) rec.writeToParcel(out, 0);
+            rec = rec.next;
+        }
+        out.writeLong(-1);
+    }
+
+    public void readSummaryFromParcel(Parcel in) throws ParcelFormatException {
+        final int version = in.readInt();
+        if (version != VERSION) {
+            Slog.w("BatteryStats", "readFromParcel: version got " + version
+                + ", expected " + VERSION + "; erasing old stats");
+            return;
+        }
+
+        readHistory(in, true);
+
+        mStartCount = in.readInt();
+        mUptime = in.readLong();
+        mRealtime = in.readLong();
+        mStartClockTime = in.readLong();
+        mStartPlatformVersion = in.readString();
+        mEndPlatformVersion = in.readString();
+        mOnBatteryTimeBase.readSummaryFromParcel(in);
+        mOnBatteryScreenOffTimeBase.readSummaryFromParcel(in);
+        mDischargeUnplugLevel = in.readInt();
+        mDischargePlugLevel = in.readInt();
+        mDischargeCurrentLevel = in.readInt();
+        mCurrentBatteryLevel = in.readInt();
+        mEstimatedBatteryCapacity = in.readInt();
+        mMinLearnedBatteryCapacity = in.readInt();
+        mMaxLearnedBatteryCapacity = in.readInt();
+        mLowDischargeAmountSinceCharge = in.readInt();
+        mHighDischargeAmountSinceCharge = in.readInt();
+        mDischargeAmountScreenOnSinceCharge = in.readInt();
+        mDischargeAmountScreenOffSinceCharge = in.readInt();
+        mDischargeStepTracker.readFromParcel(in);
+        mChargeStepTracker.readFromParcel(in);
+        mDailyDischargeStepTracker.readFromParcel(in);
+        mDailyChargeStepTracker.readFromParcel(in);
+        mDischargeCounter.readSummaryFromParcelLocked(in);
+        mDischargeScreenOffCounter.readSummaryFromParcelLocked(in);
+        int NPKG = in.readInt();
+        if (NPKG > 0) {
+            mDailyPackageChanges = new ArrayList<>(NPKG);
+            while (NPKG > 0) {
+                NPKG--;
+                PackageChange pc = new PackageChange();
+                pc.mPackageName = in.readString();
+                pc.mUpdate = in.readInt() != 0;
+                pc.mVersionCode = in.readInt();
+                mDailyPackageChanges.add(pc);
+            }
+        } else {
+            mDailyPackageChanges = null;
+        }
+        mDailyStartTime = in.readLong();
+        mNextMinDailyDeadline = in.readLong();
+        mNextMaxDailyDeadline = in.readLong();
+
+        mStartCount++;
+
+        mScreenState = Display.STATE_UNKNOWN;
+        mScreenOnTimer.readSummaryFromParcelLocked(in);
+        for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            mScreenBrightnessTimer[i].readSummaryFromParcelLocked(in);
+        }
+        mInteractive = false;
+        mInteractiveTimer.readSummaryFromParcelLocked(in);
+        mPhoneOn = false;
+        mPowerSaveModeEnabledTimer.readSummaryFromParcelLocked(in);
+        mLongestLightIdleTime = in.readLong();
+        mLongestFullIdleTime = in.readLong();
+        mDeviceIdleModeLightTimer.readSummaryFromParcelLocked(in);
+        mDeviceIdleModeFullTimer.readSummaryFromParcelLocked(in);
+        mDeviceLightIdlingTimer.readSummaryFromParcelLocked(in);
+        mDeviceIdlingTimer.readSummaryFromParcelLocked(in);
+        mPhoneOnTimer.readSummaryFromParcelLocked(in);
+        for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            mPhoneSignalStrengthsTimer[i].readSummaryFromParcelLocked(in);
+        }
+        mPhoneSignalScanningTimer.readSummaryFromParcelLocked(in);
+        for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+            mPhoneDataConnectionsTimer[i].readSummaryFromParcelLocked(in);
+        }
+        for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+            mNetworkByteActivityCounters[i].readSummaryFromParcelLocked(in);
+            mNetworkPacketActivityCounters[i].readSummaryFromParcelLocked(in);
+        }
+        mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+        mMobileRadioActiveTimer.readSummaryFromParcelLocked(in);
+        mMobileRadioActivePerAppTimer.readSummaryFromParcelLocked(in);
+        mMobileRadioActiveAdjustedTime.readSummaryFromParcelLocked(in);
+        mMobileRadioActiveUnknownTime.readSummaryFromParcelLocked(in);
+        mMobileRadioActiveUnknownCount.readSummaryFromParcelLocked(in);
+        mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+        mWifiOn = false;
+        mWifiOnTimer.readSummaryFromParcelLocked(in);
+        mGlobalWifiRunning = false;
+        mGlobalWifiRunningTimer.readSummaryFromParcelLocked(in);
+        for (int i=0; i<NUM_WIFI_STATES; i++) {
+            mWifiStateTimer[i].readSummaryFromParcelLocked(in);
+        }
+        for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+            mWifiSupplStateTimer[i].readSummaryFromParcelLocked(in);
+        }
+        for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            mWifiSignalStrengthsTimer[i].readSummaryFromParcelLocked(in);
+        }
+        mWifiActivity.readSummaryFromParcel(in);
+        mBluetoothActivity.readSummaryFromParcel(in);
+        mModemActivity.readSummaryFromParcel(in);
+        mHasWifiReporting = in.readInt() != 0;
+        mHasBluetoothReporting = in.readInt() != 0;
+        mHasModemReporting = in.readInt() != 0;
+
+        mNumConnectivityChange = mLoadedNumConnectivityChange = in.readInt();
+        mFlashlightOnNesting = 0;
+        mFlashlightOnTimer.readSummaryFromParcelLocked(in);
+        mCameraOnNesting = 0;
+        mCameraOnTimer.readSummaryFromParcelLocked(in);
+        mBluetoothScanNesting = 0;
+        mBluetoothScanTimer.readSummaryFromParcelLocked(in);
+
+        int NKW = in.readInt();
+        if (NKW > 10000) {
+            throw new ParcelFormatException("File corrupt: too many kernel wake locks " + NKW);
+        }
+        for (int ikw = 0; ikw < NKW; ikw++) {
+            if (in.readInt() != 0) {
+                String kwltName = in.readString();
+                getKernelWakelockTimerLocked(kwltName).readSummaryFromParcelLocked(in);
+            }
+        }
+
+        int NWR = in.readInt();
+        if (NWR > 10000) {
+            throw new ParcelFormatException("File corrupt: too many wakeup reasons " + NWR);
+        }
+        for (int iwr = 0; iwr < NWR; iwr++) {
+            if (in.readInt() != 0) {
+                String reasonName = in.readString();
+                getWakeupReasonTimerLocked(reasonName).readSummaryFromParcelLocked(in);
+            }
+        }
+
+        int NMS = in.readInt();
+        for (int ims = 0; ims < NMS; ims++) {
+            if (in.readInt() != 0) {
+                long kmstName = in.readLong();
+                getKernelMemoryTimerLocked(kmstName).readSummaryFromParcelLocked(in);
+            }
+        }
+
+        final int NU = in.readInt();
+        if (NU > 10000) {
+            throw new ParcelFormatException("File corrupt: too many uids " + NU);
+        }
+        for (int iu = 0; iu < NU; iu++) {
+            int uid = in.readInt();
+            Uid u = new Uid(this, uid);
+            mUidStats.put(uid, u);
+
+            u.mOnBatteryBackgroundTimeBase.readSummaryFromParcel(in);
+            u.mOnBatteryScreenOffBackgroundTimeBase.readSummaryFromParcel(in);
+
+            u.mWifiRunning = false;
+            if (in.readInt() != 0) {
+                u.mWifiRunningTimer.readSummaryFromParcelLocked(in);
+            }
+            u.mFullWifiLockOut = false;
+            if (in.readInt() != 0) {
+                u.mFullWifiLockTimer.readSummaryFromParcelLocked(in);
+            }
+            u.mWifiScanStarted = false;
+            if (in.readInt() != 0) {
+                u.mWifiScanTimer.readSummaryFromParcelLocked(in);
+            }
+            u.mWifiBatchedScanBinStarted = Uid.NO_BATCHED_SCAN_STARTED;
+            for (int i = 0; i < Uid.NUM_WIFI_BATCHED_SCAN_BINS; i++) {
+                if (in.readInt() != 0) {
+                    u.makeWifiBatchedScanBin(i, null);
+                    u.mWifiBatchedScanTimer[i].readSummaryFromParcelLocked(in);
+                }
+            }
+            u.mWifiMulticastEnabled = false;
+            if (in.readInt() != 0) {
+                u.mWifiMulticastTimer.readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createAudioTurnedOnTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createVideoTurnedOnTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createFlashlightTurnedOnTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createCameraTurnedOnTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createForegroundActivityTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createForegroundServiceTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createAggregatedPartialWakelockTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createBluetoothScanTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createBluetoothUnoptimizedScanTimerLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createBluetoothScanResultCounterLocked().readSummaryFromParcelLocked(in);
+            }
+            if (in.readInt() != 0) {
+                u.createBluetoothScanResultBgCounterLocked().readSummaryFromParcelLocked(in);
+            }
+            u.mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
+            for (int i = 0; i < Uid.NUM_PROCESS_STATE; i++) {
+                if (in.readInt() != 0) {
+                    u.makeProcessState(i, null);
+                    u.mProcessStateTimer[i].readSummaryFromParcelLocked(in);
+                }
+            }
+            if (in.readInt() != 0) {
+                u.createVibratorOnTimerLocked().readSummaryFromParcelLocked(in);
+            }
+
+            if (in.readInt() != 0) {
+                if (u.mUserActivityCounters == null) {
+                    u.initUserActivityLocked();
+                }
+                for (int i=0; i<Uid.NUM_USER_ACTIVITY_TYPES; i++) {
+                    u.mUserActivityCounters[i].readSummaryFromParcelLocked(in);
+                }
+            }
+
+            if (in.readInt() != 0) {
+                if (u.mNetworkByteActivityCounters == null) {
+                    u.initNetworkActivityLocked();
+                }
+                for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                    u.mNetworkByteActivityCounters[i].readSummaryFromParcelLocked(in);
+                    u.mNetworkPacketActivityCounters[i].readSummaryFromParcelLocked(in);
+                }
+                u.mMobileRadioActiveTime.readSummaryFromParcelLocked(in);
+                u.mMobileRadioActiveCount.readSummaryFromParcelLocked(in);
+            }
+
+            u.mUserCpuTime.readSummaryFromParcelLocked(in);
+            u.mSystemCpuTime.readSummaryFromParcelLocked(in);
+
+            if (in.readInt() != 0) {
+                final int numClusters = in.readInt();
+                if (mPowerProfile != null && mPowerProfile.getNumCpuClusters() != numClusters) {
+                    throw new ParcelFormatException("Incompatible cpu cluster arrangement");
+                }
+
+                u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][];
+                for (int cluster = 0; cluster < numClusters; cluster++) {
+                    if (in.readInt() != 0) {
+                        final int NSB = in.readInt();
+                        if (mPowerProfile != null &&
+                                mPowerProfile.getNumSpeedStepsInCpuCluster(cluster) != NSB) {
+                            throw new ParcelFormatException("File corrupt: too many speed bins " +
+                                    NSB);
+                        }
+
+                        u.mCpuClusterSpeedTimesUs[cluster] = new LongSamplingCounter[NSB];
+                        for (int speed = 0; speed < NSB; speed++) {
+                            if (in.readInt() != 0) {
+                                u.mCpuClusterSpeedTimesUs[cluster][speed] = new LongSamplingCounter(
+                                        mOnBatteryTimeBase);
+                                u.mCpuClusterSpeedTimesUs[cluster][speed].readSummaryFromParcelLocked(in);
+                            }
+                        }
+                    } else {
+                        u.mCpuClusterSpeedTimesUs[cluster] = null;
+                    }
+                }
+            } else {
+                u.mCpuClusterSpeedTimesUs = null;
+            }
+
+            u.mCpuFreqTimeMs = LongSamplingCounterArray.readSummaryFromParcelLocked(
+                    in, mOnBatteryTimeBase);
+            u.mScreenOffCpuFreqTimeMs = LongSamplingCounterArray.readSummaryFromParcelLocked(
+                    in, mOnBatteryScreenOffTimeBase);
+
+            if (in.readInt() != 0) {
+                u.mMobileRadioApWakeupCount = new LongSamplingCounter(mOnBatteryTimeBase);
+                u.mMobileRadioApWakeupCount.readSummaryFromParcelLocked(in);
+            } else {
+                u.mMobileRadioApWakeupCount = null;
+            }
+
+            if (in.readInt() != 0) {
+                u.mWifiRadioApWakeupCount = new LongSamplingCounter(mOnBatteryTimeBase);
+                u.mWifiRadioApWakeupCount.readSummaryFromParcelLocked(in);
+            } else {
+                u.mWifiRadioApWakeupCount = null;
+            }
+
+            int NW = in.readInt();
+            if (NW > (MAX_WAKELOCKS_PER_UID+1)) {
+                throw new ParcelFormatException("File corrupt: too many wake locks " + NW);
+            }
+            for (int iw = 0; iw < NW; iw++) {
+                String wlName = in.readString();
+                u.readWakeSummaryFromParcelLocked(wlName, in);
+            }
+
+            int NS = in.readInt();
+            if (NS > (MAX_WAKELOCKS_PER_UID+1)) {
+                throw new ParcelFormatException("File corrupt: too many syncs " + NS);
+            }
+            for (int is = 0; is < NS; is++) {
+                String name = in.readString();
+                u.readSyncSummaryFromParcelLocked(name, in);
+            }
+
+            int NJ = in.readInt();
+            if (NJ > (MAX_WAKELOCKS_PER_UID+1)) {
+                throw new ParcelFormatException("File corrupt: too many job timers " + NJ);
+            }
+            for (int ij = 0; ij < NJ; ij++) {
+                String name = in.readString();
+                u.readJobSummaryFromParcelLocked(name, in);
+            }
+
+            u.readJobCompletionsFromParcelLocked(in);
+
+            int NP = in.readInt();
+            if (NP > 1000) {
+                throw new ParcelFormatException("File corrupt: too many sensors " + NP);
+            }
+            for (int is = 0; is < NP; is++) {
+                int seNumber = in.readInt();
+                if (in.readInt() != 0) {
+                    u.getSensorTimerLocked(seNumber, true).readSummaryFromParcelLocked(in);
+                }
+            }
+
+            NP = in.readInt();
+            if (NP > 1000) {
+                throw new ParcelFormatException("File corrupt: too many processes " + NP);
+            }
+            for (int ip = 0; ip < NP; ip++) {
+                String procName = in.readString();
+                Uid.Proc p = u.getProcessStatsLocked(procName);
+                p.mUserTime = p.mLoadedUserTime = in.readLong();
+                p.mSystemTime = p.mLoadedSystemTime = in.readLong();
+                p.mForegroundTime = p.mLoadedForegroundTime = in.readLong();
+                p.mStarts = p.mLoadedStarts = in.readInt();
+                p.mNumCrashes = p.mLoadedNumCrashes = in.readInt();
+                p.mNumAnrs = p.mLoadedNumAnrs = in.readInt();
+                p.readExcessivePowerFromParcelLocked(in);
+            }
+
+            NP = in.readInt();
+            if (NP > 10000) {
+                throw new ParcelFormatException("File corrupt: too many packages " + NP);
+            }
+            for (int ip = 0; ip < NP; ip++) {
+                String pkgName = in.readString();
+                Uid.Pkg p = u.getPackageStatsLocked(pkgName);
+                final int NWA = in.readInt();
+                if (NWA > 1000) {
+                    throw new ParcelFormatException("File corrupt: too many wakeup alarms " + NWA);
+                }
+                p.mWakeupAlarms.clear();
+                for (int iwa=0; iwa<NWA; iwa++) {
+                    String tag = in.readString();
+                    Counter c = new Counter(mOnBatteryScreenOffTimeBase);
+                    c.readSummaryFromParcelLocked(in);
+                    p.mWakeupAlarms.put(tag, c);
+                }
+                NS = in.readInt();
+                if (NS > 1000) {
+                    throw new ParcelFormatException("File corrupt: too many services " + NS);
+                }
+                for (int is = 0; is < NS; is++) {
+                    String servName = in.readString();
+                    Uid.Pkg.Serv s = u.getServiceStatsLocked(pkgName, servName);
+                    s.mStartTime = s.mLoadedStartTime = in.readLong();
+                    s.mStarts = s.mLoadedStarts = in.readInt();
+                    s.mLaunches = s.mLoadedLaunches = in.readInt();
+                }
+            }
+        }
+    }
+
+    /**
+     * Writes a summary of the statistics to a Parcel, in a format suitable to be written to
+     * disk.  This format does not allow a lossless round-trip.
+     *
+     * @param out the Parcel to be written to.
+     */
+    public void writeSummaryToParcel(Parcel out, boolean inclHistory) {
+        pullPendingStateUpdatesLocked();
+
+        // Pull the clock time.  This may update the time and make a new history entry
+        // if we had originally pulled a time before the RTC was set.
+        long startClockTime = getStartClockTime();
+
+        final long NOW_SYS = mClocks.uptimeMillis() * 1000;
+        final long NOWREAL_SYS = mClocks.elapsedRealtime() * 1000;
+
+        out.writeInt(VERSION);
+
+        writeHistory(out, inclHistory, true);
+
+        out.writeInt(mStartCount);
+        out.writeLong(computeUptime(NOW_SYS, STATS_SINCE_CHARGED));
+        out.writeLong(computeRealtime(NOWREAL_SYS, STATS_SINCE_CHARGED));
+        out.writeLong(startClockTime);
+        out.writeString(mStartPlatformVersion);
+        out.writeString(mEndPlatformVersion);
+        mOnBatteryTimeBase.writeSummaryToParcel(out, NOW_SYS, NOWREAL_SYS);
+        mOnBatteryScreenOffTimeBase.writeSummaryToParcel(out, NOW_SYS, NOWREAL_SYS);
+        out.writeInt(mDischargeUnplugLevel);
+        out.writeInt(mDischargePlugLevel);
+        out.writeInt(mDischargeCurrentLevel);
+        out.writeInt(mCurrentBatteryLevel);
+        out.writeInt(mEstimatedBatteryCapacity);
+        out.writeInt(mMinLearnedBatteryCapacity);
+        out.writeInt(mMaxLearnedBatteryCapacity);
+        out.writeInt(getLowDischargeAmountSinceCharge());
+        out.writeInt(getHighDischargeAmountSinceCharge());
+        out.writeInt(getDischargeAmountScreenOnSinceCharge());
+        out.writeInt(getDischargeAmountScreenOffSinceCharge());
+        mDischargeStepTracker.writeToParcel(out);
+        mChargeStepTracker.writeToParcel(out);
+        mDailyDischargeStepTracker.writeToParcel(out);
+        mDailyChargeStepTracker.writeToParcel(out);
+        mDischargeCounter.writeSummaryFromParcelLocked(out);
+        mDischargeScreenOffCounter.writeSummaryFromParcelLocked(out);
+        if (mDailyPackageChanges != null) {
+            final int NPKG = mDailyPackageChanges.size();
+            out.writeInt(NPKG);
+            for (int i=0; i<NPKG; i++) {
+                PackageChange pc = mDailyPackageChanges.get(i);
+                out.writeString(pc.mPackageName);
+                out.writeInt(pc.mUpdate ? 1 : 0);
+                out.writeInt(pc.mVersionCode);
+            }
+        } else {
+            out.writeInt(0);
+        }
+        out.writeLong(mDailyStartTime);
+        out.writeLong(mNextMinDailyDeadline);
+        out.writeLong(mNextMaxDailyDeadline);
+
+        mScreenOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            mScreenBrightnessTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        }
+        mInteractiveTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mPowerSaveModeEnabledTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        out.writeLong(mLongestLightIdleTime);
+        out.writeLong(mLongestFullIdleTime);
+        mDeviceIdleModeLightTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mDeviceIdleModeFullTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mDeviceLightIdlingTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mDeviceIdlingTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mPhoneOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            mPhoneSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        }
+        mPhoneSignalScanningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+            mPhoneDataConnectionsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        }
+        for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+            mNetworkByteActivityCounters[i].writeSummaryFromParcelLocked(out);
+            mNetworkPacketActivityCounters[i].writeSummaryFromParcelLocked(out);
+        }
+        mMobileRadioActiveTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mMobileRadioActivePerAppTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mMobileRadioActiveAdjustedTime.writeSummaryFromParcelLocked(out);
+        mMobileRadioActiveUnknownTime.writeSummaryFromParcelLocked(out);
+        mMobileRadioActiveUnknownCount.writeSummaryFromParcelLocked(out);
+        mWifiOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mGlobalWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        for (int i=0; i<NUM_WIFI_STATES; i++) {
+            mWifiStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        }
+        for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+            mWifiSupplStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        }
+        for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            mWifiSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        }
+        mWifiActivity.writeSummaryToParcel(out);
+        mBluetoothActivity.writeSummaryToParcel(out);
+        mModemActivity.writeSummaryToParcel(out);
+        out.writeInt(mHasWifiReporting ? 1 : 0);
+        out.writeInt(mHasBluetoothReporting ? 1 : 0);
+        out.writeInt(mHasModemReporting ? 1 : 0);
+
+        out.writeInt(mNumConnectivityChange);
+        mFlashlightOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mCameraOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+        mBluetoothScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+
+        out.writeInt(mKernelWakelockStats.size());
+        for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) {
+            Timer kwlt = ent.getValue();
+            if (kwlt != null) {
+                out.writeInt(1);
+                out.writeString(ent.getKey());
+                kwlt.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        out.writeInt(mWakeupReasonStats.size());
+        for (Map.Entry<String, SamplingTimer> ent : mWakeupReasonStats.entrySet()) {
+            SamplingTimer timer = ent.getValue();
+            if (timer != null) {
+                out.writeInt(1);
+                out.writeString(ent.getKey());
+                timer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        out.writeInt(mKernelMemoryStats.size());
+        for (int i = 0; i < mKernelMemoryStats.size(); i++) {
+            Timer kmt = mKernelMemoryStats.valueAt(i);
+            if (kmt != null) {
+                out.writeInt(1);
+                out.writeLong(mKernelMemoryStats.keyAt(i));
+                kmt.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        final int NU = mUidStats.size();
+        out.writeInt(NU);
+        for (int iu = 0; iu < NU; iu++) {
+            out.writeInt(mUidStats.keyAt(iu));
+            Uid u = mUidStats.valueAt(iu);
+
+            u.mOnBatteryBackgroundTimeBase.writeSummaryToParcel(out, NOW_SYS, NOWREAL_SYS);
+            u.mOnBatteryScreenOffBackgroundTimeBase.writeSummaryToParcel(out, NOW_SYS, NOWREAL_SYS);
+
+            if (u.mWifiRunningTimer != null) {
+                out.writeInt(1);
+                u.mWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mFullWifiLockTimer != null) {
+                out.writeInt(1);
+                u.mFullWifiLockTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mWifiScanTimer != null) {
+                out.writeInt(1);
+                u.mWifiScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            for (int i = 0; i < Uid.NUM_WIFI_BATCHED_SCAN_BINS; i++) {
+                if (u.mWifiBatchedScanTimer[i] != null) {
+                    out.writeInt(1);
+                    u.mWifiBatchedScanTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+            if (u.mWifiMulticastTimer != null) {
+                out.writeInt(1);
+                u.mWifiMulticastTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mAudioTurnedOnTimer != null) {
+                out.writeInt(1);
+                u.mAudioTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mVideoTurnedOnTimer != null) {
+                out.writeInt(1);
+                u.mVideoTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mFlashlightTurnedOnTimer != null) {
+                out.writeInt(1);
+                u.mFlashlightTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mCameraTurnedOnTimer != null) {
+                out.writeInt(1);
+                u.mCameraTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mForegroundActivityTimer != null) {
+                out.writeInt(1);
+                u.mForegroundActivityTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mForegroundServiceTimer != null) {
+                out.writeInt(1);
+                u.mForegroundServiceTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mAggregatedPartialWakelockTimer != null) {
+                out.writeInt(1);
+                u.mAggregatedPartialWakelockTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mBluetoothScanTimer != null) {
+                out.writeInt(1);
+                u.mBluetoothScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mBluetoothUnoptimizedScanTimer != null) {
+                out.writeInt(1);
+                u.mBluetoothUnoptimizedScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mBluetoothScanResultCounter != null) {
+                out.writeInt(1);
+                u.mBluetoothScanResultCounter.writeSummaryFromParcelLocked(out);
+            } else {
+                out.writeInt(0);
+            }
+            if (u.mBluetoothScanResultBgCounter != null) {
+                out.writeInt(1);
+                u.mBluetoothScanResultBgCounter.writeSummaryFromParcelLocked(out);
+            } else {
+                out.writeInt(0);
+            }
+            for (int i = 0; i < Uid.NUM_PROCESS_STATE; i++) {
+                if (u.mProcessStateTimer[i] != null) {
+                    out.writeInt(1);
+                    u.mProcessStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+            if (u.mVibratorOnTimer != null) {
+                out.writeInt(1);
+                u.mVibratorOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (u.mUserActivityCounters == null) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                for (int i=0; i<Uid.NUM_USER_ACTIVITY_TYPES; i++) {
+                    u.mUserActivityCounters[i].writeSummaryFromParcelLocked(out);
+                }
+            }
+
+            if (u.mNetworkByteActivityCounters == null) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+                    u.mNetworkByteActivityCounters[i].writeSummaryFromParcelLocked(out);
+                    u.mNetworkPacketActivityCounters[i].writeSummaryFromParcelLocked(out);
+                }
+                u.mMobileRadioActiveTime.writeSummaryFromParcelLocked(out);
+                u.mMobileRadioActiveCount.writeSummaryFromParcelLocked(out);
+            }
+
+            u.mUserCpuTime.writeSummaryFromParcelLocked(out);
+            u.mSystemCpuTime.writeSummaryFromParcelLocked(out);
+
+            if (u.mCpuClusterSpeedTimesUs != null) {
+                out.writeInt(1);
+                out.writeInt(u.mCpuClusterSpeedTimesUs.length);
+                for (LongSamplingCounter[] cpuSpeeds : u.mCpuClusterSpeedTimesUs) {
+                    if (cpuSpeeds != null) {
+                        out.writeInt(1);
+                        out.writeInt(cpuSpeeds.length);
+                        for (LongSamplingCounter c : cpuSpeeds) {
+                            if (c != null) {
+                                out.writeInt(1);
+                                c.writeSummaryFromParcelLocked(out);
+                            } else {
+                                out.writeInt(0);
+                            }
+                        }
+                    } else {
+                        out.writeInt(0);
+                    }
+                }
+            } else {
+                out.writeInt(0);
+            }
+
+            LongSamplingCounterArray.writeSummaryToParcelLocked(out, u.mCpuFreqTimeMs);
+            LongSamplingCounterArray.writeSummaryToParcelLocked(out, u.mScreenOffCpuFreqTimeMs);
+
+            if (u.mMobileRadioApWakeupCount != null) {
+                out.writeInt(1);
+                u.mMobileRadioApWakeupCount.writeSummaryFromParcelLocked(out);
+            } else {
+                out.writeInt(0);
+            }
+
+            if (u.mWifiRadioApWakeupCount != null) {
+                out.writeInt(1);
+                u.mWifiRadioApWakeupCount.writeSummaryFromParcelLocked(out);
+            } else {
+                out.writeInt(0);
+            }
+
+            final ArrayMap<String, Uid.Wakelock> wakeStats = u.mWakelockStats.getMap();
+            int NW = wakeStats.size();
+            out.writeInt(NW);
+            for (int iw=0; iw<NW; iw++) {
+                out.writeString(wakeStats.keyAt(iw));
+                Uid.Wakelock wl = wakeStats.valueAt(iw);
+                if (wl.mTimerFull != null) {
+                    out.writeInt(1);
+                    wl.mTimerFull.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+                if (wl.mTimerPartial != null) {
+                    out.writeInt(1);
+                    wl.mTimerPartial.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+                if (wl.mTimerWindow != null) {
+                    out.writeInt(1);
+                    wl.mTimerWindow.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+                if (wl.mTimerDraw != null) {
+                    out.writeInt(1);
+                    wl.mTimerDraw.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+
+            final ArrayMap<String, DualTimer> syncStats = u.mSyncStats.getMap();
+            int NS = syncStats.size();
+            out.writeInt(NS);
+            for (int is=0; is<NS; is++) {
+                out.writeString(syncStats.keyAt(is));
+                syncStats.valueAt(is).writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            }
+
+            final ArrayMap<String, DualTimer> jobStats = u.mJobStats.getMap();
+            int NJ = jobStats.size();
+            out.writeInt(NJ);
+            for (int ij=0; ij<NJ; ij++) {
+                out.writeString(jobStats.keyAt(ij));
+                jobStats.valueAt(ij).writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+            }
+
+            u.writeJobCompletionsToParcelLocked(out);
+
+            int NSE = u.mSensorStats.size();
+            out.writeInt(NSE);
+            for (int ise=0; ise<NSE; ise++) {
+                out.writeInt(u.mSensorStats.keyAt(ise));
+                Uid.Sensor se = u.mSensorStats.valueAt(ise);
+                if (se.mTimer != null) {
+                    out.writeInt(1);
+                    se.mTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+
+            int NP = u.mProcessStats.size();
+            out.writeInt(NP);
+            for (int ip=0; ip<NP; ip++) {
+                out.writeString(u.mProcessStats.keyAt(ip));
+                Uid.Proc ps = u.mProcessStats.valueAt(ip);
+                out.writeLong(ps.mUserTime);
+                out.writeLong(ps.mSystemTime);
+                out.writeLong(ps.mForegroundTime);
+                out.writeInt(ps.mStarts);
+                out.writeInt(ps.mNumCrashes);
+                out.writeInt(ps.mNumAnrs);
+                ps.writeExcessivePowerToParcelLocked(out);
+            }
+
+            NP = u.mPackageStats.size();
+            out.writeInt(NP);
+            if (NP > 0) {
+                for (Map.Entry<String, BatteryStatsImpl.Uid.Pkg> ent
+                    : u.mPackageStats.entrySet()) {
+                    out.writeString(ent.getKey());
+                    Uid.Pkg ps = ent.getValue();
+                    final int NWA = ps.mWakeupAlarms.size();
+                    out.writeInt(NWA);
+                    for (int iwa=0; iwa<NWA; iwa++) {
+                        out.writeString(ps.mWakeupAlarms.keyAt(iwa));
+                        ps.mWakeupAlarms.valueAt(iwa).writeSummaryFromParcelLocked(out);
+                    }
+                    NS = ps.mServiceStats.size();
+                    out.writeInt(NS);
+                    for (int is=0; is<NS; is++) {
+                        out.writeString(ps.mServiceStats.keyAt(is));
+                        BatteryStatsImpl.Uid.Pkg.Serv ss = ps.mServiceStats.valueAt(is);
+                        long time = ss.getStartTimeToNowLocked(
+                                mOnBatteryTimeBase.getUptime(NOW_SYS));
+                        out.writeLong(time);
+                        out.writeInt(ss.mStarts);
+                        out.writeInt(ss.mLaunches);
+                    }
+                }
+            }
+        }
+    }
+
+    public void readFromParcel(Parcel in) {
+        readFromParcelLocked(in);
+    }
+
+    void readFromParcelLocked(Parcel in) {
+        int magic = in.readInt();
+        if (magic != MAGIC) {
+            throw new ParcelFormatException("Bad magic number: #" + Integer.toHexString(magic));
+        }
+
+        readHistory(in, false);
+
+        mStartCount = in.readInt();
+        mStartClockTime = in.readLong();
+        mStartPlatformVersion = in.readString();
+        mEndPlatformVersion = in.readString();
+        mUptime = in.readLong();
+        mUptimeStart = in.readLong();
+        mRealtime = in.readLong();
+        mRealtimeStart = in.readLong();
+        mOnBattery = in.readInt() != 0;
+        mEstimatedBatteryCapacity = in.readInt();
+        mMinLearnedBatteryCapacity = in.readInt();
+        mMaxLearnedBatteryCapacity = in.readInt();
+        mOnBatteryInternal = false; // we are no longer really running.
+        mOnBatteryTimeBase.readFromParcel(in);
+        mOnBatteryScreenOffTimeBase.readFromParcel(in);
+
+        mScreenState = Display.STATE_UNKNOWN;
+        mScreenOnTimer = new StopwatchTimer(mClocks, null, -1, null, mOnBatteryTimeBase, in);
+        for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            mScreenBrightnessTimer[i] = new StopwatchTimer(mClocks, null, -100-i, null,
+                    mOnBatteryTimeBase, in);
+        }
+        mInteractive = false;
+        mInteractiveTimer = new StopwatchTimer(mClocks, null, -10, null, mOnBatteryTimeBase, in);
+        mPhoneOn = false;
+        mPowerSaveModeEnabledTimer = new StopwatchTimer(mClocks, null, -2, null,
+                mOnBatteryTimeBase, in);
+        mLongestLightIdleTime = in.readLong();
+        mLongestFullIdleTime = in.readLong();
+        mDeviceIdleModeLightTimer = new StopwatchTimer(mClocks, null, -14, null,
+                mOnBatteryTimeBase, in);
+        mDeviceIdleModeFullTimer = new StopwatchTimer(mClocks, null, -11, null,
+                mOnBatteryTimeBase, in);
+        mDeviceLightIdlingTimer = new StopwatchTimer(mClocks, null, -15, null,
+                mOnBatteryTimeBase, in);
+        mDeviceIdlingTimer = new StopwatchTimer(mClocks, null, -12, null, mOnBatteryTimeBase, in);
+        mPhoneOnTimer = new StopwatchTimer(mClocks, null, -3, null, mOnBatteryTimeBase, in);
+        for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(mClocks, null, -200-i,
+                    null, mOnBatteryTimeBase, in);
+        }
+        mPhoneSignalScanningTimer = new StopwatchTimer(mClocks, null, -200+1, null,
+                mOnBatteryTimeBase, in);
+        for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+            mPhoneDataConnectionsTimer[i] = new StopwatchTimer(mClocks, null, -300-i,
+                    null, mOnBatteryTimeBase, in);
+        }
+        for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+            mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in);
+            mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in);
+        }
+        mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+        mMobileRadioActiveTimer = new StopwatchTimer(mClocks, null, -400, null,
+                mOnBatteryTimeBase, in);
+        mMobileRadioActivePerAppTimer = new StopwatchTimer(mClocks, null, -401, null, 
+                mOnBatteryTimeBase, in);
+        mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase, in);
+        mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase, in);
+        mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase, in);
+        mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+        mWifiOn = false;
+        mWifiOnTimer = new StopwatchTimer(mClocks, null, -4, null, mOnBatteryTimeBase, in);
+        mGlobalWifiRunning = false;
+        mGlobalWifiRunningTimer = new StopwatchTimer(mClocks, null, -5, null,
+                mOnBatteryTimeBase, in);
+        for (int i=0; i<NUM_WIFI_STATES; i++) {
+            mWifiStateTimer[i] = new StopwatchTimer(mClocks, null, -600-i,
+                    null, mOnBatteryTimeBase, in);
+        }
+        for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+            mWifiSupplStateTimer[i] = new StopwatchTimer(mClocks, null, -700-i,
+                    null, mOnBatteryTimeBase, in);
+        }
+        for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            mWifiSignalStrengthsTimer[i] = new StopwatchTimer(mClocks, null, -800-i,
+                    null, mOnBatteryTimeBase, in);
+        }
+
+        mWifiActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
+                NUM_WIFI_TX_LEVELS, in);
+        mBluetoothActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
+                NUM_BT_TX_LEVELS, in);
+        mModemActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
+                ModemActivityInfo.TX_POWER_LEVELS, in);
+        mHasWifiReporting = in.readInt() != 0;
+        mHasBluetoothReporting = in.readInt() != 0;
+        mHasModemReporting = in.readInt() != 0;
+
+        mNumConnectivityChange = in.readInt();
+        mLoadedNumConnectivityChange = in.readInt();
+        mUnpluggedNumConnectivityChange = in.readInt();
+        mAudioOnNesting = 0;
+        mAudioOnTimer = new StopwatchTimer(mClocks, null, -7, null, mOnBatteryTimeBase);
+        mVideoOnNesting = 0;
+        mVideoOnTimer = new StopwatchTimer(mClocks, null, -8, null, mOnBatteryTimeBase);
+        mFlashlightOnNesting = 0;
+        mFlashlightOnTimer = new StopwatchTimer(mClocks, null, -9, null, mOnBatteryTimeBase, in);
+        mCameraOnNesting = 0;
+        mCameraOnTimer = new StopwatchTimer(mClocks, null, -13, null, mOnBatteryTimeBase, in);
+        mBluetoothScanNesting = 0;
+        mBluetoothScanTimer = new StopwatchTimer(mClocks, null, -14, null, mOnBatteryTimeBase, in);
+        mDischargeUnplugLevel = in.readInt();
+        mDischargePlugLevel = in.readInt();
+        mDischargeCurrentLevel = in.readInt();
+        mCurrentBatteryLevel = in.readInt();
+        mLowDischargeAmountSinceCharge = in.readInt();
+        mHighDischargeAmountSinceCharge = in.readInt();
+        mDischargeAmountScreenOn = in.readInt();
+        mDischargeAmountScreenOnSinceCharge = in.readInt();
+        mDischargeAmountScreenOff = in.readInt();
+        mDischargeAmountScreenOffSinceCharge = in.readInt();
+        mDischargeStepTracker.readFromParcel(in);
+        mChargeStepTracker.readFromParcel(in);
+        mDischargeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in);
+        mDischargeScreenOffCounter = new LongSamplingCounter(mOnBatteryTimeBase, in);
+        mLastWriteTime = in.readLong();
+
+        mKernelWakelockStats.clear();
+        int NKW = in.readInt();
+        for (int ikw = 0; ikw < NKW; ikw++) {
+            if (in.readInt() != 0) {
+                String wakelockName = in.readString();
+                SamplingTimer kwlt = new SamplingTimer(mClocks, mOnBatteryScreenOffTimeBase, in);
+                mKernelWakelockStats.put(wakelockName, kwlt);
+            }
+        }
+
+        mWakeupReasonStats.clear();
+        int NWR = in.readInt();
+        for (int iwr = 0; iwr < NWR; iwr++) {
+            if (in.readInt() != 0) {
+                String reasonName = in.readString();
+                SamplingTimer timer = new SamplingTimer(mClocks, mOnBatteryTimeBase, in);
+                mWakeupReasonStats.put(reasonName, timer);
+            }
+        }
+
+        mKernelMemoryStats.clear();
+        int nmt = in.readInt();
+        for (int imt = 0; imt < nmt; imt++) {
+            if (in.readInt() != 0) {
+                Long bucket = in.readLong();
+                SamplingTimer kmt = new SamplingTimer(mClocks, mOnBatteryTimeBase, in);
+                mKernelMemoryStats.put(bucket, kmt);
+            }
+        }
+
+        mPartialTimers.clear();
+        mFullTimers.clear();
+        mWindowTimers.clear();
+        mWifiRunningTimers.clear();
+        mFullWifiLockTimers.clear();
+        mWifiScanTimers.clear();
+        mWifiBatchedScanTimers.clear();
+        mWifiMulticastTimers.clear();
+        mAudioTurnedOnTimers.clear();
+        mVideoTurnedOnTimers.clear();
+        mFlashlightTurnedOnTimers.clear();
+        mCameraTurnedOnTimers.clear();
+
+        int numUids = in.readInt();
+        mUidStats.clear();
+        for (int i = 0; i < numUids; i++) {
+            int uid = in.readInt();
+            Uid u = new Uid(this, uid);
+            u.readFromParcelLocked(mOnBatteryTimeBase, mOnBatteryScreenOffTimeBase, in);
+            mUidStats.append(uid, u);
+        }
+    }
+
+    public void writeToParcel(Parcel out, int flags) {
+        writeToParcelLocked(out, true, flags);
+    }
+
+    public void writeToParcelWithoutUids(Parcel out, int flags) {
+        writeToParcelLocked(out, false, flags);
+    }
+
+    @SuppressWarnings("unused")
+    void writeToParcelLocked(Parcel out, boolean inclUids, int flags) {
+        // Need to update with current kernel wake lock counts.
+        pullPendingStateUpdatesLocked();
+
+        // Pull the clock time.  This may update the time and make a new history entry
+        // if we had originally pulled a time before the RTC was set.
+        long startClockTime = getStartClockTime();
+
+        final long uSecUptime = mClocks.uptimeMillis() * 1000;
+        final long uSecRealtime = mClocks.elapsedRealtime() * 1000;
+        final long batteryRealtime = mOnBatteryTimeBase.getRealtime(uSecRealtime);
+        final long batteryScreenOffRealtime = mOnBatteryScreenOffTimeBase.getRealtime(uSecRealtime);
+
+        out.writeInt(MAGIC);
+
+        writeHistory(out, true, false);
+
+        out.writeInt(mStartCount);
+        out.writeLong(startClockTime);
+        out.writeString(mStartPlatformVersion);
+        out.writeString(mEndPlatformVersion);
+        out.writeLong(mUptime);
+        out.writeLong(mUptimeStart);
+        out.writeLong(mRealtime);
+        out.writeLong(mRealtimeStart);
+        out.writeInt(mOnBattery ? 1 : 0);
+        out.writeInt(mEstimatedBatteryCapacity);
+        out.writeInt(mMinLearnedBatteryCapacity);
+        out.writeInt(mMaxLearnedBatteryCapacity);
+        mOnBatteryTimeBase.writeToParcel(out, uSecUptime, uSecRealtime);
+        mOnBatteryScreenOffTimeBase.writeToParcel(out, uSecUptime, uSecRealtime);
+
+        mScreenOnTimer.writeToParcel(out, uSecRealtime);
+        for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+            mScreenBrightnessTimer[i].writeToParcel(out, uSecRealtime);
+        }
+        mInteractiveTimer.writeToParcel(out, uSecRealtime);
+        mPowerSaveModeEnabledTimer.writeToParcel(out, uSecRealtime);
+        out.writeLong(mLongestLightIdleTime);
+        out.writeLong(mLongestFullIdleTime);
+        mDeviceIdleModeLightTimer.writeToParcel(out, uSecRealtime);
+        mDeviceIdleModeFullTimer.writeToParcel(out, uSecRealtime);
+        mDeviceLightIdlingTimer.writeToParcel(out, uSecRealtime);
+        mDeviceIdlingTimer.writeToParcel(out, uSecRealtime);
+        mPhoneOnTimer.writeToParcel(out, uSecRealtime);
+        for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+            mPhoneSignalStrengthsTimer[i].writeToParcel(out, uSecRealtime);
+        }
+        mPhoneSignalScanningTimer.writeToParcel(out, uSecRealtime);
+        for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+            mPhoneDataConnectionsTimer[i].writeToParcel(out, uSecRealtime);
+        }
+        for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) {
+            mNetworkByteActivityCounters[i].writeToParcel(out);
+            mNetworkPacketActivityCounters[i].writeToParcel(out);
+        }
+        mMobileRadioActiveTimer.writeToParcel(out, uSecRealtime);
+        mMobileRadioActivePerAppTimer.writeToParcel(out, uSecRealtime);
+        mMobileRadioActiveAdjustedTime.writeToParcel(out);
+        mMobileRadioActiveUnknownTime.writeToParcel(out);
+        mMobileRadioActiveUnknownCount.writeToParcel(out);
+        mWifiOnTimer.writeToParcel(out, uSecRealtime);
+        mGlobalWifiRunningTimer.writeToParcel(out, uSecRealtime);
+        for (int i=0; i<NUM_WIFI_STATES; i++) {
+            mWifiStateTimer[i].writeToParcel(out, uSecRealtime);
+        }
+        for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+            mWifiSupplStateTimer[i].writeToParcel(out, uSecRealtime);
+        }
+        for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+            mWifiSignalStrengthsTimer[i].writeToParcel(out, uSecRealtime);
+        }
+        mWifiActivity.writeToParcel(out, 0);
+        mBluetoothActivity.writeToParcel(out, 0);
+        mModemActivity.writeToParcel(out, 0);
+        out.writeInt(mHasWifiReporting ? 1 : 0);
+        out.writeInt(mHasBluetoothReporting ? 1 : 0);
+        out.writeInt(mHasModemReporting ? 1 : 0);
+
+        out.writeInt(mNumConnectivityChange);
+        out.writeInt(mLoadedNumConnectivityChange);
+        out.writeInt(mUnpluggedNumConnectivityChange);
+        mFlashlightOnTimer.writeToParcel(out, uSecRealtime);
+        mCameraOnTimer.writeToParcel(out, uSecRealtime);
+        mBluetoothScanTimer.writeToParcel(out, uSecRealtime);
+        out.writeInt(mDischargeUnplugLevel);
+        out.writeInt(mDischargePlugLevel);
+        out.writeInt(mDischargeCurrentLevel);
+        out.writeInt(mCurrentBatteryLevel);
+        out.writeInt(mLowDischargeAmountSinceCharge);
+        out.writeInt(mHighDischargeAmountSinceCharge);
+        out.writeInt(mDischargeAmountScreenOn);
+        out.writeInt(mDischargeAmountScreenOnSinceCharge);
+        out.writeInt(mDischargeAmountScreenOff);
+        out.writeInt(mDischargeAmountScreenOffSinceCharge);
+        mDischargeStepTracker.writeToParcel(out);
+        mChargeStepTracker.writeToParcel(out);
+        mDischargeCounter.writeToParcel(out);
+        mDischargeScreenOffCounter.writeToParcel(out);
+        out.writeLong(mLastWriteTime);
+
+        if (inclUids) {
+            out.writeInt(mKernelWakelockStats.size());
+            for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) {
+                SamplingTimer kwlt = ent.getValue();
+                if (kwlt != null) {
+                    out.writeInt(1);
+                    out.writeString(ent.getKey());
+                    kwlt.writeToParcel(out, uSecRealtime);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+            out.writeInt(mWakeupReasonStats.size());
+            for (Map.Entry<String, SamplingTimer> ent : mWakeupReasonStats.entrySet()) {
+                SamplingTimer timer = ent.getValue();
+                if (timer != null) {
+                    out.writeInt(1);
+                    out.writeString(ent.getKey());
+                    timer.writeToParcel(out, uSecRealtime);
+                } else {
+                    out.writeInt(0);
+                }
+            }
+        } else {
+            out.writeInt(0);
+        }
+
+        out.writeInt(mKernelMemoryStats.size());
+        for (int i = 0; i < mKernelMemoryStats.size(); i++) {
+            SamplingTimer kmt = mKernelMemoryStats.valueAt(i);
+            if (kmt != null) {
+                out.writeInt(1);
+                out.writeLong(mKernelMemoryStats.keyAt(i));
+                kmt.writeToParcel(out, uSecRealtime);
+            } else {
+                out.writeInt(0);
+            }
+        }
+
+        if (inclUids) {
+            int size = mUidStats.size();
+            out.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                out.writeInt(mUidStats.keyAt(i));
+                Uid uid = mUidStats.valueAt(i);
+
+                uid.writeToParcelLocked(out, uSecUptime, uSecRealtime);
+            }
+        } else {
+            out.writeInt(0);
+        }
+    }
+
+    public static final Parcelable.Creator<BatteryStatsImpl> CREATOR =
+        new Parcelable.Creator<BatteryStatsImpl>() {
+        public BatteryStatsImpl createFromParcel(Parcel in) {
+            return new BatteryStatsImpl(in);
+        }
+
+        public BatteryStatsImpl[] newArray(int size) {
+            return new BatteryStatsImpl[size];
+        }
+    };
+
+    public void prepareForDumpLocked() {
+        // Need to retrieve current kernel wake lock stats before printing.
+        pullPendingStateUpdatesLocked();
+
+        // Pull the clock time.  This may update the time and make a new history entry
+        // if we had originally pulled a time before the RTC was set.
+        getStartClockTime();
+    }
+
+    public void dumpLocked(Context context, PrintWriter pw, int flags, int reqUid, long histStart) {
+        if (DEBUG) {
+            pw.println("mOnBatteryTimeBase:");
+            mOnBatteryTimeBase.dump(pw, "  ");
+            pw.println("mOnBatteryScreenOffTimeBase:");
+            mOnBatteryScreenOffTimeBase.dump(pw, "  ");
+            Printer pr = new PrintWriterPrinter(pw);
+            pr.println("*** Screen timer:");
+            mScreenOnTimer.logState(pr, "  ");
+            for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+                pr.println("*** Screen brightness #" + i + ":");
+                mScreenBrightnessTimer[i].logState(pr, "  ");
+            }
+            pr.println("*** Interactive timer:");
+            mInteractiveTimer.logState(pr, "  ");
+            pr.println("*** Power save mode timer:");
+            mPowerSaveModeEnabledTimer.logState(pr, "  ");
+            pr.println("*** Device idle mode light timer:");
+            mDeviceIdleModeLightTimer.logState(pr, "  ");
+            pr.println("*** Device idle mode full timer:");
+            mDeviceIdleModeFullTimer.logState(pr, "  ");
+            pr.println("*** Device light idling timer:");
+            mDeviceLightIdlingTimer.logState(pr, "  ");
+            pr.println("*** Device idling timer:");
+            mDeviceIdlingTimer.logState(pr, "  ");
+            pr.println("*** Phone timer:");
+            mPhoneOnTimer.logState(pr, "  ");
+            for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
+                pr.println("*** Phone signal strength #" + i + ":");
+                mPhoneSignalStrengthsTimer[i].logState(pr, "  ");
+            }
+            pr.println("*** Signal scanning :");
+            mPhoneSignalScanningTimer.logState(pr, "  ");
+            for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) {
+                pr.println("*** Data connection type #" + i + ":");
+                mPhoneDataConnectionsTimer[i].logState(pr, "  ");
+            }
+            pr.println("*** mMobileRadioPowerState=" + mMobileRadioPowerState);
+            pr.println("*** Mobile network active timer:");
+            mMobileRadioActiveTimer.logState(pr, "  ");
+            pr.println("*** Mobile network active adjusted timer:");
+            mMobileRadioActiveAdjustedTime.logState(pr, "  ");
+            pr.println("*** mWifiRadioPowerState=" + mWifiRadioPowerState);
+            pr.println("*** Wifi timer:");
+            mWifiOnTimer.logState(pr, "  ");
+            pr.println("*** WifiRunning timer:");
+            mGlobalWifiRunningTimer.logState(pr, "  ");
+            for (int i=0; i<NUM_WIFI_STATES; i++) {
+                pr.println("*** Wifi state #" + i + ":");
+                mWifiStateTimer[i].logState(pr, "  ");
+            }
+            for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+                pr.println("*** Wifi suppl state #" + i + ":");
+                mWifiSupplStateTimer[i].logState(pr, "  ");
+            }
+            for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+                pr.println("*** Wifi signal strength #" + i + ":");
+                mWifiSignalStrengthsTimer[i].logState(pr, "  ");
+            }
+            pr.println("*** Flashlight timer:");
+            mFlashlightOnTimer.logState(pr, "  ");
+            pr.println("*** Camera timer:");
+            mCameraOnTimer.logState(pr, "  ");
+        }
+        super.dumpLocked(context, pw, flags, reqUid, histStart);
+    }
+}
diff --git a/com/android/internal/os/BinderInternal.java b/com/android/internal/os/BinderInternal.java
new file mode 100644
index 0000000..ea4575a
--- /dev/null
+++ b/com/android/internal/os/BinderInternal.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2008 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.internal.os;
+
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.EventLog;
+
+import dalvik.system.VMRuntime;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Private and debugging Binder APIs.
+ * 
+ * @see IBinder
+ */
+public class BinderInternal {
+    static WeakReference<GcWatcher> sGcWatcher
+            = new WeakReference<GcWatcher>(new GcWatcher());
+    static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
+    static Runnable[] sTmpWatchers = new Runnable[1];
+    static long sLastGcTime;
+
+    static final class GcWatcher {
+        @Override
+        protected void finalize() throws Throwable {
+            handleGc();
+            sLastGcTime = SystemClock.uptimeMillis();
+            synchronized (sGcWatchers) {
+                sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
+            }
+            for (int i=0; i<sTmpWatchers.length; i++) {
+                if (sTmpWatchers[i] != null) {
+                    sTmpWatchers[i].run();
+                }
+            }
+            sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
+        }
+    }
+
+    public static void addGcWatcher(Runnable watcher) {
+        synchronized (sGcWatchers) {
+            sGcWatchers.add(watcher);
+        }
+    }
+
+    /**
+     * Add the calling thread to the IPC thread pool.  This function does
+     * not return until the current process is exiting.
+     */
+    public static final native void joinThreadPool();
+    
+    /**
+     * Return the system time (as reported by {@link SystemClock#uptimeMillis
+     * SystemClock.uptimeMillis()}) that the last garbage collection occurred
+     * in this process.  This is not for general application use, and the
+     * meaning of "when a garbage collection occurred" will change as the
+     * garbage collector evolves.
+     * 
+     * @return Returns the time as per {@link SystemClock#uptimeMillis
+     * SystemClock.uptimeMillis()} of the last garbage collection.
+     */
+    public static long getLastGcTime() {
+        return sLastGcTime;
+    }
+
+    /**
+     * Return the global "context object" of the system.  This is usually
+     * an implementation of IServiceManager, which you can use to find
+     * other services.
+     */
+    public static final native IBinder getContextObject();
+    
+    /**
+     * Special for system process to not allow incoming calls to run at
+     * background scheduling priority.
+     * @hide
+     */
+    public static final native void disableBackgroundScheduling(boolean disable);
+
+    public static final native void setMaxThreads(int numThreads);
+    
+    static native final void handleGc();
+    
+    public static void forceGc(String reason) {
+        EventLog.writeEvent(2741, reason);
+        VMRuntime.getRuntime().requestConcurrentGC();
+    }
+    
+    static void forceBinderGc() {
+        forceGc("Binder");
+    }
+}
diff --git a/com/android/internal/os/BluetoothPowerCalculator.java b/com/android/internal/os/BluetoothPowerCalculator.java
new file mode 100644
index 0000000..2f383ea
--- /dev/null
+++ b/com/android/internal/os/BluetoothPowerCalculator.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+import android.util.Log;
+
+public class BluetoothPowerCalculator extends PowerCalculator {
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private static final String TAG = "BluetoothPowerCalculator";
+    private final double mIdleMa;
+    private final double mRxMa;
+    private final double mTxMa;
+    private double mAppTotalPowerMah = 0;
+    private long mAppTotalTimeMs = 0;
+
+    public BluetoothPowerCalculator(PowerProfile profile) {
+        mIdleMa = profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE);
+        mRxMa = profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX);
+        mTxMa = profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_TX);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+
+        final BatteryStats.ControllerActivityCounter counter = u.getBluetoothControllerActivity();
+        if (counter == null) {
+            return;
+        }
+
+        final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
+        final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
+        final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
+        final long totalTimeMs = idleTimeMs + txTimeMs + rxTimeMs;
+        double powerMah = counter.getPowerCounter().getCountLocked(statsType)
+                / (double)(1000*60*60);
+
+        if (powerMah == 0) {
+            powerMah = ((idleTimeMs * mIdleMa) + (rxTimeMs * mRxMa) + (txTimeMs * mTxMa))
+                    / (1000*60*60);
+        }
+
+        app.bluetoothPowerMah = powerMah;
+        app.bluetoothRunningTimeMs = totalTimeMs;
+        app.btRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_BT_RX_DATA, statsType);
+        app.btTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_BT_TX_DATA, statsType);
+
+        mAppTotalPowerMah += powerMah;
+        mAppTotalTimeMs += totalTimeMs;
+    }
+
+    @Override
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+                                   long rawUptimeUs, int statsType) {
+        final BatteryStats.ControllerActivityCounter counter =
+                stats.getBluetoothControllerActivity();
+
+        final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
+        final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
+        final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
+        final long totalTimeMs = idleTimeMs + txTimeMs + rxTimeMs;
+        double powerMah = counter.getPowerCounter().getCountLocked(statsType)
+                 / (double)(1000*60*60);
+
+        if (powerMah == 0) {
+            // Some devices do not report the power, so calculate it.
+            powerMah = ((idleTimeMs * mIdleMa) + (rxTimeMs * mRxMa) + (txTimeMs * mTxMa))
+                    / (1000*60*60);
+        }
+
+        // Subtract what the apps used, but clamp to 0.
+        powerMah = Math.max(0, powerMah - mAppTotalPowerMah);
+
+        if (DEBUG && powerMah != 0) {
+            Log.d(TAG, "Bluetooth active: time=" + (totalTimeMs)
+                    + " power=" + BatteryStatsHelper.makemAh(powerMah));
+        }
+
+        app.bluetoothPowerMah = powerMah;
+        app.bluetoothRunningTimeMs = Math.max(0, totalTimeMs - mAppTotalTimeMs);
+    }
+
+    @Override
+    public void reset() {
+        mAppTotalPowerMah = 0;
+        mAppTotalTimeMs = 0;
+    }
+}
diff --git a/com/android/internal/os/CameraPowerCalculator.java b/com/android/internal/os/CameraPowerCalculator.java
new file mode 100644
index 0000000..3273080
--- /dev/null
+++ b/com/android/internal/os/CameraPowerCalculator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+
+/**
+ * Power calculator for the camera subsystem, excluding the flashlight.
+ *
+ * Note: Power draw for the flash unit should be included in the FlashlightPowerCalculator.
+ */
+public class CameraPowerCalculator extends PowerCalculator {
+    private final double mCameraPowerOnAvg;
+
+    public CameraPowerCalculator(PowerProfile profile) {
+        mCameraPowerOnAvg = profile.getAveragePower(PowerProfile.POWER_CAMERA);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+
+        // Calculate camera power usage.  Right now, this is a (very) rough estimate based on the
+        // average power usage for a typical camera application.
+        final BatteryStats.Timer timer = u.getCameraTurnedOnTimer();
+        if (timer != null) {
+            final long totalTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000;
+            app.cameraTimeMs = totalTime;
+            app.cameraPowerMah = (totalTime * mCameraPowerOnAvg) / (1000*60*60);
+        } else {
+            app.cameraTimeMs = 0;
+            app.cameraPowerMah = 0;
+        }
+    }
+}
diff --git a/com/android/internal/os/ClassLoaderFactory.java b/com/android/internal/os/ClassLoaderFactory.java
new file mode 100644
index 0000000..b2b769e
--- /dev/null
+++ b/com/android/internal/os/ClassLoaderFactory.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 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.internal.os;
+
+import android.os.Trace;
+
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.DexClassLoader;
+import dalvik.system.PathClassLoader;
+
+/**
+ * Creates class loaders.
+ *
+ * @hide
+ */
+public class ClassLoaderFactory {
+    // Unconstructable
+    private ClassLoaderFactory() {}
+
+    private static final String PATH_CLASS_LOADER_NAME = PathClassLoader.class.getName();
+    private static final String DEX_CLASS_LOADER_NAME = DexClassLoader.class.getName();
+    private static final String DELEGATE_LAST_CLASS_LOADER_NAME =
+            DelegateLastClassLoader.class.getName();
+
+    /**
+     * Returns true if {@code name} is a supported classloader. {@code name} must be a
+     * binary name of a class, as defined by {@code Class.getName}.
+     */
+    public static boolean isValidClassLoaderName(String name) {
+        // This method is used to parse package data and does not accept null names.
+        return name != null && (isPathClassLoaderName(name) || isDelegateLastClassLoaderName(name));
+    }
+
+    /**
+     * Returns true if {@code name} is the encoding for either PathClassLoader or DexClassLoader.
+     * The two class loaders are grouped together because they have the same behaviour.
+     */
+    public static boolean isPathClassLoaderName(String name) {
+        // For null values we default to PathClassLoader. This cover the case when packages
+        // don't specify any value for their class loaders.
+        return name == null || PATH_CLASS_LOADER_NAME.equals(name) ||
+                DEX_CLASS_LOADER_NAME.equals(name);
+    }
+
+    /**
+     * Returns true if {@code name} is the encoding for the DelegateLastClassLoader.
+     */
+    public static boolean isDelegateLastClassLoaderName(String name) {
+        return DELEGATE_LAST_CLASS_LOADER_NAME.equals(name);
+    }
+
+    /**
+     * Same as {@code createClassLoader} below, except that no associated namespace
+     * is created.
+     */
+    public static ClassLoader createClassLoader(String dexPath,
+            String librarySearchPath, ClassLoader parent, String classloaderName) {
+        if (isPathClassLoaderName(classloaderName)) {
+            return new PathClassLoader(dexPath, librarySearchPath, parent);
+        } else if (isDelegateLastClassLoaderName(classloaderName)) {
+            return new DelegateLastClassLoader(dexPath, librarySearchPath, parent);
+        }
+
+        throw new AssertionError("Invalid classLoaderName: " + classloaderName);
+    }
+
+    /**
+     * Create a ClassLoader and initialize a linker-namespace for it.
+     */
+    public static ClassLoader createClassLoader(String dexPath,
+            String librarySearchPath, String libraryPermittedPath, ClassLoader parent,
+            int targetSdkVersion, boolean isNamespaceShared, String classloaderName) {
+
+        final ClassLoader classLoader = createClassLoader(dexPath, librarySearchPath, parent,
+                classloaderName);
+
+        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "createClassloaderNamespace");
+        String errorMessage = createClassloaderNamespace(classLoader,
+                                                         targetSdkVersion,
+                                                         librarySearchPath,
+                                                         libraryPermittedPath,
+                                                         isNamespaceShared);
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+        if (errorMessage != null) {
+            throw new UnsatisfiedLinkError("Unable to create namespace for the classloader " +
+                                           classLoader + ": " + errorMessage);
+        }
+
+        return classLoader;
+    }
+
+    private static native String createClassloaderNamespace(ClassLoader classLoader,
+                                                            int targetSdkVersion,
+                                                            String librarySearchPath,
+                                                            String libraryPermittedPath,
+                                                            boolean isNamespaceShared);
+}
diff --git a/com/android/internal/os/CpuPowerCalculator.java b/com/android/internal/os/CpuPowerCalculator.java
new file mode 100644
index 0000000..bb743c1
--- /dev/null
+++ b/com/android/internal/os/CpuPowerCalculator.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+import android.util.ArrayMap;
+import android.util.Log;
+
+public class CpuPowerCalculator extends PowerCalculator {
+    private static final String TAG = "CpuPowerCalculator";
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private static final long MICROSEC_IN_HR = (long) 60 * 60 * 1000 * 1000;
+    private final PowerProfile mProfile;
+
+    public CpuPowerCalculator(PowerProfile profile) {
+        mProfile = profile;
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+
+        app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000;
+        final int numClusters = mProfile.getNumCpuClusters();
+
+        double cpuPowerMaUs = 0;
+        for (int cluster = 0; cluster < numClusters; cluster++) {
+            final int speedsForCluster = mProfile.getNumSpeedStepsInCpuCluster(cluster);
+            for (int speed = 0; speed < speedsForCluster; speed++) {
+                final long timeUs = u.getTimeAtCpuSpeed(cluster, speed, statsType);
+                final double cpuSpeedStepPower = timeUs *
+                        mProfile.getAveragePowerForCpu(cluster, speed);
+                if (DEBUG) {
+                    Log.d(TAG, "UID " + u.getUid() + ": CPU cluster #" + cluster + " step #"
+                            + speed + " timeUs=" + timeUs + " power="
+                            + BatteryStatsHelper.makemAh(cpuSpeedStepPower / MICROSEC_IN_HR));
+                }
+                cpuPowerMaUs += cpuSpeedStepPower;
+            }
+        }
+        app.cpuPowerMah = cpuPowerMaUs / MICROSEC_IN_HR;
+
+        if (DEBUG && (app.cpuTimeMs != 0 || app.cpuPowerMah != 0)) {
+            Log.d(TAG, "UID " + u.getUid() + ": CPU time=" + app.cpuTimeMs + " ms power="
+                    + BatteryStatsHelper.makemAh(app.cpuPowerMah));
+        }
+
+        // Keep track of the package with highest drain.
+        double highestDrain = 0;
+
+        app.cpuFgTimeMs = 0;
+        final ArrayMap<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats();
+        final int processStatsCount = processStats.size();
+        for (int i = 0; i < processStatsCount; i++) {
+            final BatteryStats.Uid.Proc ps = processStats.valueAt(i);
+            final String processName = processStats.keyAt(i);
+            app.cpuFgTimeMs += ps.getForegroundTime(statsType);
+
+            final long costValue = ps.getUserTime(statsType) + ps.getSystemTime(statsType)
+                    + ps.getForegroundTime(statsType);
+
+            // Each App can have multiple packages and with multiple running processes.
+            // Keep track of the package who's process has the highest drain.
+            if (app.packageWithHighestDrain == null ||
+                    app.packageWithHighestDrain.startsWith("*")) {
+                highestDrain = costValue;
+                app.packageWithHighestDrain = processName;
+            } else if (highestDrain < costValue && !processName.startsWith("*")) {
+                highestDrain = costValue;
+                app.packageWithHighestDrain = processName;
+            }
+        }
+
+        // Ensure that the CPU times make sense.
+        if (app.cpuFgTimeMs > app.cpuTimeMs) {
+            if (DEBUG && app.cpuFgTimeMs > app.cpuTimeMs + 10000) {
+                Log.d(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time");
+            }
+
+            // Statistics may not have been gathered yet.
+            app.cpuTimeMs = app.cpuFgTimeMs;
+        }
+    }
+}
diff --git a/com/android/internal/os/FlashlightPowerCalculator.java b/com/android/internal/os/FlashlightPowerCalculator.java
new file mode 100644
index 0000000..fef66ff
--- /dev/null
+++ b/com/android/internal/os/FlashlightPowerCalculator.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+
+/**
+ * Power calculator for the flashlight.
+ */
+public class FlashlightPowerCalculator extends PowerCalculator {
+    private final double mFlashlightPowerOnAvg;
+
+    public FlashlightPowerCalculator(PowerProfile profile) {
+        mFlashlightPowerOnAvg = profile.getAveragePower(PowerProfile.POWER_FLASHLIGHT);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+
+        // Calculate flashlight power usage.  Right now, this is based on the average power draw
+        // of the flash unit when kept on over a short period of time.
+        final BatteryStats.Timer timer = u.getFlashlightTurnedOnTimer();
+        if (timer != null) {
+            final long totalTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000;
+            app.flashlightTimeMs = totalTime;
+            app.flashlightPowerMah = (totalTime * mFlashlightPowerOnAvg) / (1000*60*60);
+        } else {
+            app.flashlightTimeMs = 0;
+            app.flashlightPowerMah = 0;
+        }
+    }
+}
diff --git a/com/android/internal/os/FuseAppLoop.java b/com/android/internal/os/FuseAppLoop.java
new file mode 100644
index 0000000..088e726
--- /dev/null
+++ b/com/android/internal/os/FuseAppLoop.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2016 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.internal.os;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ProxyFileDescriptorCallback;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.concurrent.ThreadFactory;
+
+public class FuseAppLoop implements Handler.Callback {
+    private static final String TAG = "FuseAppLoop";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    public static final int ROOT_INODE = 1;
+    private static final int MIN_INODE = 2;
+    private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() {
+        @Override
+        public Thread newThread(Runnable r) {
+            return new Thread(r, TAG);
+        }
+    };
+    private static final int FUSE_OK = 0;
+    private static final int ARGS_POOL_SIZE = 50;
+
+    private final Object mLock = new Object();
+    private final int mMountPointId;
+    private final Thread mThread;
+
+    @GuardedBy("mLock")
+    private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
+
+    @GuardedBy("mLock")
+    private final BytesMap mBytesMap = new BytesMap();
+
+    @GuardedBy("mLock")
+    private final LinkedList<Args> mArgsPool = new LinkedList<>();
+
+    /**
+     * Sequential number can be used as file name and inode in AppFuse.
+     * 0 is regarded as an error, 1 is mount point. So we start the number from 2.
+     */
+    @GuardedBy("mLock")
+    private int mNextInode = MIN_INODE;
+
+    @GuardedBy("mLock")
+    private long mInstance;
+
+    public FuseAppLoop(
+            int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) {
+        mMountPointId = mountPointId;
+        if (factory == null) {
+            factory = sDefaultThreadFactory;
+        }
+        mInstance = native_new(fd.detachFd());
+        mThread = factory.newThread(() -> {
+            native_start(mInstance);
+            synchronized (mLock) {
+                native_delete(mInstance);
+                mInstance = 0;
+                mBytesMap.clear();
+            }
+        });
+        mThread.start();
+    }
+
+    public int registerCallback(@NonNull ProxyFileDescriptorCallback callback,
+            @NonNull Handler handler) throws FuseUnavailableMountException {
+        synchronized (mLock) {
+            Preconditions.checkNotNull(callback);
+            Preconditions.checkNotNull(handler);
+            Preconditions.checkState(
+                    mCallbackMap.size() < Integer.MAX_VALUE - MIN_INODE, "Too many opened files.");
+            Preconditions.checkArgument(
+                    Thread.currentThread().getId() != handler.getLooper().getThread().getId(),
+                    "Handler must be different from the current thread");
+            if (mInstance == 0) {
+                throw new FuseUnavailableMountException(mMountPointId);
+            }
+            int id;
+            while (true) {
+                id = mNextInode;
+                mNextInode++;
+                if (mNextInode < 0) {
+                    mNextInode = MIN_INODE;
+                }
+                if (mCallbackMap.get(id) == null) {
+                    break;
+                }
+            }
+            mCallbackMap.put(id, new CallbackEntry(
+                    callback, new Handler(handler.getLooper(), this)));
+            return id;
+        }
+    }
+
+    public void unregisterCallback(int id) {
+        synchronized (mLock) {
+            mCallbackMap.remove(id);
+        }
+    }
+
+    public int getMountPointId() {
+        return mMountPointId;
+    }
+
+    // Defined in fuse.h
+    private static final int FUSE_LOOKUP = 1;
+    private static final int FUSE_GETATTR = 3;
+    private static final int FUSE_OPEN = 14;
+    private static final int FUSE_READ = 15;
+    private static final int FUSE_WRITE = 16;
+    private static final int FUSE_RELEASE = 18;
+    private static final int FUSE_FSYNC = 20;
+
+    // Defined in FuseBuffer.h
+    private static final int FUSE_MAX_WRITE = 256 * 1024;
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        final Args args = (Args) msg.obj;
+        final CallbackEntry entry = args.entry;
+        final long inode = args.inode;
+        final long unique = args.unique;
+        final int size = args.size;
+        final long offset = args.offset;
+        final byte[] data = args.data;
+
+        try {
+            switch (msg.what) {
+                case FUSE_LOOKUP: {
+                    final long fileSize = entry.callback.onGetSize();
+                    synchronized (mLock) {
+                        if (mInstance != 0) {
+                            native_replyLookup(mInstance, unique, inode, fileSize);
+                        }
+                        recycleLocked(args);
+                    }
+                    break;
+                }
+                case FUSE_GETATTR: {
+                    final long fileSize = entry.callback.onGetSize();
+                    synchronized (mLock) {
+                        if (mInstance != 0) {
+                            native_replyGetAttr(mInstance, unique, inode, fileSize);
+                        }
+                        recycleLocked(args);
+                    }
+                    break;
+                }
+                case FUSE_READ:
+                    final int readSize = entry.callback.onRead(
+                            offset, size, data);
+                    synchronized (mLock) {
+                        if (mInstance != 0) {
+                            native_replyRead(mInstance, unique, readSize, data);
+                        }
+                        recycleLocked(args);
+                    }
+                    break;
+                case FUSE_WRITE:
+                    final int writeSize = entry.callback.onWrite(offset, size, data);
+                    synchronized (mLock) {
+                        if (mInstance != 0) {
+                            native_replyWrite(mInstance, unique, writeSize);
+                        }
+                        recycleLocked(args);
+                    }
+                    break;
+                case FUSE_FSYNC:
+                    entry.callback.onFsync();
+                    synchronized (mLock) {
+                        if (mInstance != 0) {
+                            native_replySimple(mInstance, unique, FUSE_OK);
+                        }
+                        recycleLocked(args);
+                    }
+                    break;
+                case FUSE_RELEASE:
+                    entry.callback.onRelease();
+                    synchronized (mLock) {
+                        if (mInstance != 0) {
+                            native_replySimple(mInstance, unique, FUSE_OK);
+                        }
+                        mBytesMap.stopUsing(entry.getThreadId());
+                        recycleLocked(args);
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown FUSE command: " + msg.what);
+            }
+        } catch (Exception error) {
+            synchronized (mLock) {
+                Log.e(TAG, "", error);
+                replySimpleLocked(unique, getError(error));
+                recycleLocked(args);
+            }
+        }
+
+        return true;
+    }
+
+    // Called by JNI.
+    @SuppressWarnings("unused")
+    private void onCommand(int command, long unique, long inode, long offset, int size,
+            byte[] data) {
+        synchronized (mLock) {
+            try {
+                final Args args;
+                if (mArgsPool.size() == 0) {
+                    args = new Args();
+                } else {
+                    args = mArgsPool.pop();
+                }
+                args.unique = unique;
+                args.inode = inode;
+                args.offset = offset;
+                args.size = size;
+                args.data = data;
+                args.entry = getCallbackEntryOrThrowLocked(inode);
+                if (!args.entry.handler.sendMessage(
+                        Message.obtain(args.entry.handler, command, 0, 0, args))) {
+                    throw new ErrnoException("onCommand", OsConstants.EBADF);
+                }
+            } catch (Exception error) {
+                replySimpleLocked(unique, getError(error));
+            }
+        }
+    }
+
+    // Called by JNI.
+    @SuppressWarnings("unused")
+    private byte[] onOpen(long unique, long inode) {
+        synchronized (mLock) {
+            try {
+                final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
+                if (entry.opened) {
+                    throw new ErrnoException("onOpen", OsConstants.EMFILE);
+                }
+                if (mInstance != 0) {
+                    native_replyOpen(mInstance, unique, /* fh */ inode);
+                    entry.opened = true;
+                    return mBytesMap.startUsing(entry.getThreadId());
+                }
+            } catch (ErrnoException error) {
+                replySimpleLocked(unique, getError(error));
+            }
+            return null;
+        }
+    }
+
+    private static int getError(@NonNull Exception error) {
+        if (error instanceof ErrnoException) {
+            final int errno = ((ErrnoException) error).errno;
+            if (errno != OsConstants.ENOSYS) {
+                return -errno;
+            }
+        }
+        return -OsConstants.EBADF;
+    }
+
+    private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
+        final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
+        if (entry == null) {
+            throw new ErrnoException("getCallbackEntryOrThrowLocked", OsConstants.ENOENT);
+        }
+        return entry;
+    }
+
+    private void recycleLocked(Args args) {
+        if (mArgsPool.size() < ARGS_POOL_SIZE) {
+            mArgsPool.add(args);
+        }
+    }
+
+    private void replySimpleLocked(long unique, int result) {
+        if (mInstance != 0) {
+            native_replySimple(mInstance, unique, result);
+        }
+    }
+
+    native long native_new(int fd);
+    native void native_delete(long ptr);
+    native void native_start(long ptr);
+
+    native void native_replySimple(long ptr, long unique, int result);
+    native void native_replyOpen(long ptr, long unique, long fh);
+    native void native_replyLookup(long ptr, long unique, long inode, long size);
+    native void native_replyGetAttr(long ptr, long unique, long inode, long size);
+    native void native_replyWrite(long ptr, long unique, int size);
+    native void native_replyRead(long ptr, long unique, int size, byte[] bytes);
+
+    private static int checkInode(long inode) {
+        Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
+        return (int) inode;
+    }
+
+    public static class UnmountedException extends Exception {}
+
+    private static class CallbackEntry {
+        final ProxyFileDescriptorCallback callback;
+        final Handler handler;
+        boolean opened;
+
+        CallbackEntry(ProxyFileDescriptorCallback callback, Handler handler) {
+            this.callback = Preconditions.checkNotNull(callback);
+            this.handler = Preconditions.checkNotNull(handler);
+        }
+
+        long getThreadId() {
+            return handler.getLooper().getThread().getId();
+        }
+    }
+
+    /**
+     * Entry for bytes map.
+     */
+    private static class BytesMapEntry {
+        int counter = 0;
+        byte[] bytes = new byte[FUSE_MAX_WRITE];
+    }
+
+    /**
+     * Map between Thread ID and byte buffer.
+     */
+    private static class BytesMap {
+        final Map<Long, BytesMapEntry> mEntries = new HashMap<>();
+
+        byte[] startUsing(long threadId) {
+            BytesMapEntry entry = mEntries.get(threadId);
+            if (entry == null) {
+                entry = new BytesMapEntry();
+                mEntries.put(threadId, entry);
+            }
+            entry.counter++;
+            return entry.bytes;
+        }
+
+        void stopUsing(long threadId) {
+            final BytesMapEntry entry = mEntries.get(threadId);
+            Preconditions.checkNotNull(entry);
+            entry.counter--;
+            if (entry.counter <= 0) {
+                mEntries.remove(threadId);
+            }
+        }
+
+        void clear() {
+            mEntries.clear();
+        }
+    }
+
+    private static class Args {
+        long unique;
+        long inode;
+        long offset;
+        int size;
+        byte[] data;
+        CallbackEntry entry;
+    }
+}
diff --git a/com/android/internal/os/FuseUnavailableMountException.java b/com/android/internal/os/FuseUnavailableMountException.java
new file mode 100644
index 0000000..ca3cfb9
--- /dev/null
+++ b/com/android/internal/os/FuseUnavailableMountException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 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.internal.os;
+
+/**
+ * Exception occurred when the mount point has already been unavailable.
+ */
+public class FuseUnavailableMountException extends Exception {
+    public FuseUnavailableMountException(int mountId) {
+        super("AppFuse mount point " + mountId + " is unavailable");
+    }
+}
diff --git a/com/android/internal/os/HandlerCaller.java b/com/android/internal/os/HandlerCaller.java
new file mode 100644
index 0000000..ae7c5f2
--- /dev/null
+++ b/com/android/internal/os/HandlerCaller.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2008 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.internal.os;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+public class HandlerCaller {
+    final Looper mMainLooper;
+    final Handler mH;
+
+    final Callback mCallback;
+
+    class MyHandler extends Handler {
+        MyHandler(Looper looper, boolean async) {
+            super(looper, null, async);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            mCallback.executeMessage(msg);
+        }
+    }
+
+    public interface Callback {
+        public void executeMessage(Message msg);
+    }
+
+    public HandlerCaller(Context context, Looper looper, Callback callback,
+            boolean asyncHandler) {
+        mMainLooper = looper != null ? looper : context.getMainLooper();
+        mH = new MyHandler(mMainLooper, asyncHandler);
+        mCallback = callback;
+    }
+
+    public Handler getHandler() {
+        return mH;
+    }
+
+    public void executeOrSendMessage(Message msg) {
+        // If we are calling this from the main thread, then we can call
+        // right through.  Otherwise, we need to send the message to the
+        // main thread.
+        if (Looper.myLooper() == mMainLooper) {
+            mCallback.executeMessage(msg);
+            msg.recycle();
+            return;
+        }
+        
+        mH.sendMessage(msg);
+    }
+
+    public void sendMessageDelayed(Message msg, long delayMillis) {
+        mH.sendMessageDelayed(msg, delayMillis);
+    }
+
+    public boolean hasMessages(int what) {
+        return mH.hasMessages(what);
+    }
+    
+    public void removeMessages(int what) {
+        mH.removeMessages(what);
+    }
+    
+    public void removeMessages(int what, Object obj) {
+        mH.removeMessages(what, obj);
+    }
+    
+    public void sendMessage(Message msg) {
+        mH.sendMessage(msg);
+    }
+
+    public SomeArgs sendMessageAndWait(Message msg) {
+        if (Looper.myLooper() == mH.getLooper()) {
+            throw new IllegalStateException("Can't wait on same thread as looper");
+        }
+        SomeArgs args = (SomeArgs)msg.obj;
+        args.mWaitState = SomeArgs.WAIT_WAITING;
+        mH.sendMessage(msg);
+        synchronized (args) {
+            while (args.mWaitState == SomeArgs.WAIT_WAITING) {
+                try {
+                    args.wait();
+                } catch (InterruptedException e) {
+                    return null;
+                }
+            }
+        }
+        args.mWaitState = SomeArgs.WAIT_NONE;
+        return args;
+    }
+
+    public Message obtainMessage(int what) {
+        return mH.obtainMessage(what);
+    }
+    
+    public Message obtainMessageBO(int what, boolean arg1, Object arg2) {
+        return mH.obtainMessage(what, arg1 ? 1 : 0, 0, arg2);
+    }
+    
+    public Message obtainMessageBOO(int what, boolean arg1, Object arg2, Object arg3) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg2;
+        args.arg2 = arg3;
+        return mH.obtainMessage(what, arg1 ? 1 : 0, 0, args);
+    }
+    
+    public Message obtainMessageO(int what, Object arg1) {
+        return mH.obtainMessage(what, 0, 0, arg1);
+    }
+    
+    public Message obtainMessageI(int what, int arg1) {
+        return mH.obtainMessage(what, arg1, 0);
+    }
+    
+    public Message obtainMessageII(int what, int arg1, int arg2) {
+        return mH.obtainMessage(what, arg1, arg2);
+    }
+    
+    public Message obtainMessageIO(int what, int arg1, Object arg2) {
+        return mH.obtainMessage(what, arg1, 0, arg2);
+    }
+    
+    public Message obtainMessageIIO(int what, int arg1, int arg2, Object arg3) {
+        return mH.obtainMessage(what, arg1, arg2, arg3);
+    }
+    
+    public Message obtainMessageIIOO(int what, int arg1, int arg2,
+            Object arg3, Object arg4) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg3;
+        args.arg2 = arg4;
+        return mH.obtainMessage(what, arg1, arg2, args);
+    }
+    
+    public Message obtainMessageIOO(int what, int arg1, Object arg2, Object arg3) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg2;
+        args.arg2 = arg3;
+        return mH.obtainMessage(what, arg1, 0, args);
+    }
+    
+    public Message obtainMessageIOOO(int what, int arg1, Object arg2, Object arg3, Object arg4) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg2;
+        args.arg2 = arg3;
+        args.arg3 = arg4;
+        return mH.obtainMessage(what, arg1, 0, args);
+    }
+
+    public Message obtainMessageIIOOO(int what, int arg1, int arg2, Object arg3, Object arg4,
+            Object arg5) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg3;
+        args.arg2 = arg4;
+        args.arg3 = arg5;
+        return mH.obtainMessage(what, arg1, arg2, args);
+    }
+
+    public Message obtainMessageIIOOOO(int what, int arg1, int arg2, Object arg3, Object arg4,
+            Object arg5, Object arg6) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg3;
+        args.arg2 = arg4;
+        args.arg3 = arg5;
+        args.arg4 = arg6;
+        return mH.obtainMessage(what, arg1, arg2, args);
+    }
+
+    public Message obtainMessageOO(int what, Object arg1, Object arg2) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+    
+    public Message obtainMessageOOO(int what, Object arg1, Object arg2, Object arg3) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        args.arg3 = arg3;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+    
+    public Message obtainMessageOOOO(int what, Object arg1, Object arg2,
+            Object arg3, Object arg4) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        args.arg3 = arg3;
+        args.arg4 = arg4;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+    
+    public Message obtainMessageOOOOO(int what, Object arg1, Object arg2,
+            Object arg3, Object arg4, Object arg5) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        args.arg3 = arg3;
+        args.arg4 = arg4;
+        args.arg5 = arg5;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+
+    public Message obtainMessageOOOOII(int what, Object arg1, Object arg2,
+            Object arg3, Object arg4, int arg5, int arg6) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        args.arg3 = arg3;
+        args.arg4 = arg4;
+        args.argi5 = arg5;
+        args.argi6 = arg6;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+
+    public Message obtainMessageIIII(int what, int arg1, int arg2,
+            int arg3, int arg4) {
+        SomeArgs args = SomeArgs.obtain();
+        args.argi1 = arg1;
+        args.argi2 = arg2;
+        args.argi3 = arg3;
+        args.argi4 = arg4;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+
+    public Message obtainMessageIIIIII(int what, int arg1, int arg2,
+            int arg3, int arg4, int arg5, int arg6) {
+        SomeArgs args = SomeArgs.obtain();
+        args.argi1 = arg1;
+        args.argi2 = arg2;
+        args.argi3 = arg3;
+        args.argi4 = arg4;
+        args.argi5 = arg5;
+        args.argi6 = arg6;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+
+    public Message obtainMessageIIIIO(int what, int arg1, int arg2,
+            int arg3, int arg4, Object arg5) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg5;
+        args.argi1 = arg1;
+        args.argi2 = arg2;
+        args.argi3 = arg3;
+        args.argi4 = arg4;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+}
diff --git a/com/android/internal/os/KernelCpuSpeedReader.java b/com/android/internal/os/KernelCpuSpeedReader.java
new file mode 100644
index 0000000..757a112
--- /dev/null
+++ b/com/android/internal/os/KernelCpuSpeedReader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.text.TextUtils;
+import android.os.StrictMode;
+import android.system.OsConstants;
+import android.util.Slog;
+
+import libcore.io.Libcore;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Reads CPU time of a specific core spent at various frequencies and provides a delta from the
+ * last call to {@link #readDelta}. Each line in the proc file has the format:
+ *
+ * freq time
+ *
+ * where time is measured in jiffies.
+ */
+public class KernelCpuSpeedReader {
+    private static final String TAG = "KernelCpuSpeedReader";
+
+    private final String mProcFile;
+    private final long[] mLastSpeedTimesMs;
+    private final long[] mDeltaSpeedTimesMs;
+
+    // How long a CPU jiffy is in milliseconds.
+    private final long mJiffyMillis;
+
+    /**
+     * @param cpuNumber The cpu (cpu0, cpu1, etc) whose state to read.
+     */
+    public KernelCpuSpeedReader(int cpuNumber, int numSpeedSteps) {
+        mProcFile = String.format("/sys/devices/system/cpu/cpu%d/cpufreq/stats/time_in_state",
+                cpuNumber);
+        mLastSpeedTimesMs = new long[numSpeedSteps];
+        mDeltaSpeedTimesMs = new long[numSpeedSteps];
+        long jiffyHz = Libcore.os.sysconf(OsConstants._SC_CLK_TCK);
+        mJiffyMillis = 1000/jiffyHz;
+    }
+
+    /**
+     * The returned array is modified in subsequent calls to {@link #readDelta}.
+     * @return The time (in milliseconds) spent at different cpu speeds since the last call to
+     * {@link #readDelta}.
+     */
+    public long[] readDelta() {
+        StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+        try (BufferedReader reader = new BufferedReader(new FileReader(mProcFile))) {
+            TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' ');
+            String line;
+            int speedIndex = 0;
+            while (speedIndex < mLastSpeedTimesMs.length && (line = reader.readLine()) != null) {
+                splitter.setString(line);
+                splitter.next();
+
+                long time = Long.parseLong(splitter.next()) * mJiffyMillis;
+                if (time < mLastSpeedTimesMs[speedIndex]) {
+                    // The stats reset when the cpu hotplugged. That means that the time
+                    // we read is offset from 0, so the time is the delta.
+                    mDeltaSpeedTimesMs[speedIndex] = time;
+                } else {
+                    mDeltaSpeedTimesMs[speedIndex] = time - mLastSpeedTimesMs[speedIndex];
+                }
+                mLastSpeedTimesMs[speedIndex] = time;
+                speedIndex++;
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to read cpu-freq: " + e.getMessage());
+            Arrays.fill(mDeltaSpeedTimesMs, 0);
+        } finally {
+            StrictMode.setThreadPolicy(policy);
+        }
+        return mDeltaSpeedTimesMs;
+    }
+}
diff --git a/com/android/internal/os/KernelMemoryBandwidthStats.java b/com/android/internal/os/KernelMemoryBandwidthStats.java
new file mode 100644
index 0000000..15a5e3e
--- /dev/null
+++ b/com/android/internal/os/KernelMemoryBandwidthStats.java
@@ -0,0 +1,92 @@
+package com.android.internal.os;
+
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.LongSparseLongArray;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Reads DDR time spent at various frequencies and stores the data.  Supports diff comparison with
+ * other KernelMemoryBandwidthStats objects. The sysfs file has the format:
+ *
+ * freq time_in_bucket ... time_in_bucket
+ *      ...
+ * freq time_in_bucket ... time_in_bucket
+ *
+ * where time is measured in nanoseconds.
+ */
+public class KernelMemoryBandwidthStats {
+    private static final String TAG = "KernelMemoryBandwidthStats";
+
+    private static final String mSysfsFile = "/sys/kernel/memory_state_time/show_stat";
+    private static final boolean DEBUG = false;
+
+    protected final LongSparseLongArray mBandwidthEntries = new LongSparseLongArray();
+    private boolean mStatsDoNotExist = false;
+
+    public void updateStats() {
+        if (mStatsDoNotExist) {
+            // Skip reading.
+            return;
+        }
+
+        final long startTime = SystemClock.uptimeMillis();
+
+        StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+        try (BufferedReader reader = new BufferedReader(new FileReader(mSysfsFile))) {
+            parseStats(reader);
+        } catch (FileNotFoundException e) {
+            Slog.w(TAG, "No kernel memory bandwidth stats available");
+            mBandwidthEntries.clear();
+            mStatsDoNotExist = true;
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to read memory bandwidth: " + e.getMessage());
+            mBandwidthEntries.clear();
+        } finally {
+            StrictMode.setThreadPolicy(policy);
+        }
+
+        final long readTime = SystemClock.uptimeMillis() - startTime;
+        if (DEBUG || readTime > 100) {
+            Slog.w(TAG, "Reading memory bandwidth file took " + readTime + "ms");
+        }
+    }
+
+    @VisibleForTesting
+    public void parseStats(BufferedReader reader) throws IOException {
+        String line;
+        TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' ');
+        mBandwidthEntries.clear();
+        while ((line = reader.readLine()) != null) {
+            splitter.setString(line);
+            splitter.next();
+            int bandwidth = 0;
+            int index;
+            do {
+                if ((index = mBandwidthEntries.indexOfKey(bandwidth)) >= 0) {
+                    mBandwidthEntries.put(bandwidth, mBandwidthEntries.valueAt(index)
+                            + Long.parseLong(splitter.next()) / 1000000);
+                } else {
+                    mBandwidthEntries.put(bandwidth, Long.parseLong(splitter.next()) / 1000000);
+                }
+                if (DEBUG) {
+                    Slog.d(TAG, String.format("bandwidth: %s time: %s", bandwidth,
+                            mBandwidthEntries.get(bandwidth)));
+                }
+                bandwidth++;
+            } while(splitter.hasNext());
+        }
+    }
+
+    public LongSparseLongArray getBandwidthEntries() {
+        return mBandwidthEntries;
+    }
+}
diff --git a/com/android/internal/os/KernelUidCpuFreqTimeReader.java b/com/android/internal/os/KernelUidCpuFreqTimeReader.java
new file mode 100644
index 0000000..8884d24
--- /dev/null
+++ b/com/android/internal/os/KernelUidCpuFreqTimeReader.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2017 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.internal.os;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.util.IntArray;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Reads /proc/uid_time_in_state which has the format:
+ *
+ * uid: [freq1] [freq2] [freq3] ...
+ * [uid1]: [time in freq1] [time in freq2] [time in freq3] ...
+ * [uid2]: [time in freq1] [time in freq2] [time in freq3] ...
+ * ...
+ *
+ * This provides the times a UID's processes spent executing at each different cpu frequency.
+ * The file contains a monotonically increasing count of time for a single boot. This class
+ * maintains the previous results of a call to {@link #readDelta} in order to provide a proper
+ * delta.
+ */
+public class KernelUidCpuFreqTimeReader {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "KernelUidCpuFreqTimeReader";
+    private static final String UID_TIMES_PROC_FILE = "/proc/uid_time_in_state";
+
+    public interface Callback {
+        void onUidCpuFreqTime(int uid, long[] cpuFreqTimeMs);
+    }
+
+    private long[] mCpuFreqs;
+    private int mCpuFreqsCount;
+    private long mLastTimeReadMs;
+    private long mNowTimeMs;
+
+    private SparseArray<long[]> mLastUidCpuFreqTimeMs = new SparseArray<>();
+
+    // We check the existence of proc file a few times (just in case it is not ready yet when we
+    // start reading) and if it is not available, we simply ignore further read requests.
+    private static final int TOTAL_READ_ERROR_COUNT = 5;
+    private int mReadErrorCounter;
+    private boolean mProcFileAvailable;
+    private boolean mPerClusterTimesAvailable;
+
+    public boolean perClusterTimesAvailable() {
+        return mPerClusterTimesAvailable;
+    }
+
+    public long[] readFreqs(@NonNull PowerProfile powerProfile) {
+        checkNotNull(powerProfile);
+
+        if (mCpuFreqs != null) {
+            // No need to read cpu freqs more than once.
+            return mCpuFreqs;
+        }
+        if (!mProcFileAvailable && mReadErrorCounter >= TOTAL_READ_ERROR_COUNT) {
+            return null;
+        }
+        try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) {
+            mProcFileAvailable = true;
+            return readFreqs(reader, powerProfile);
+        } catch (IOException e) {
+            mReadErrorCounter++;
+            Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e);
+            return null;
+        }
+    }
+
+    @VisibleForTesting
+    public long[] readFreqs(BufferedReader reader, PowerProfile powerProfile)
+            throws IOException {
+        final String line = reader.readLine();
+        if (line == null) {
+            return null;
+        }
+        return readCpuFreqs(line, powerProfile);
+    }
+
+    public void readDelta(@Nullable Callback callback) {
+        if (!mProcFileAvailable) {
+            return;
+        }
+        try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) {
+            mNowTimeMs = SystemClock.elapsedRealtime();
+            readDelta(reader, callback);
+            mLastTimeReadMs = mNowTimeMs;
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e);
+        }
+    }
+
+    public void removeUid(int uid) {
+        mLastUidCpuFreqTimeMs.delete(uid);
+    }
+
+    public void removeUidsInRange(int startUid, int endUid) {
+        if (endUid < startUid) {
+            return;
+        }
+        mLastUidCpuFreqTimeMs.put(startUid, null);
+        mLastUidCpuFreqTimeMs.put(endUid, null);
+        final int firstIndex = mLastUidCpuFreqTimeMs.indexOfKey(startUid);
+        final int lastIndex = mLastUidCpuFreqTimeMs.indexOfKey(endUid);
+        mLastUidCpuFreqTimeMs.removeAtRange(firstIndex, lastIndex - firstIndex + 1);
+    }
+
+    @VisibleForTesting
+    public void readDelta(BufferedReader reader, @Nullable Callback callback) throws IOException {
+        String line = reader.readLine();
+        if (line == null) {
+            return;
+        }
+        while ((line = reader.readLine()) != null) {
+            final int index = line.indexOf(' ');
+            final int uid = Integer.parseInt(line.substring(0, index - 1), 10);
+            readTimesForUid(uid, line.substring(index + 1, line.length()), callback);
+        }
+    }
+
+    private void readTimesForUid(int uid, String line, Callback callback) {
+        long[] uidTimeMs = mLastUidCpuFreqTimeMs.get(uid);
+        if (uidTimeMs == null) {
+            uidTimeMs = new long[mCpuFreqsCount];
+            mLastUidCpuFreqTimeMs.put(uid, uidTimeMs);
+        }
+        final String[] timesStr = line.split(" ");
+        final int size = timesStr.length;
+        if (size != uidTimeMs.length) {
+            Slog.e(TAG, "No. of readings don't match cpu freqs, readings: " + size
+                    + " cpuFreqsCount: " + uidTimeMs.length);
+            return;
+        }
+        final long[] deltaUidTimeMs = new long[size];
+        final long[] curUidTimeMs = new long[size];
+        boolean notify = false;
+        for (int i = 0; i < size; ++i) {
+            // Times read will be in units of 10ms
+            final long totalTimeMs = Long.parseLong(timesStr[i], 10) * 10;
+            deltaUidTimeMs[i] = totalTimeMs - uidTimeMs[i];
+            // If there is malformed data for any uid, then we just log about it and ignore
+            // the data for that uid.
+            if (deltaUidTimeMs[i] < 0 || totalTimeMs < 0) {
+                if (DEBUG) {
+                    final StringBuilder sb = new StringBuilder("Malformed cpu freq data for UID=")
+                            .append(uid).append("\n");
+                    sb.append("data=").append("(").append(uidTimeMs[i]).append(",")
+                            .append(totalTimeMs).append(")").append("\n");
+                    sb.append("times=").append("(");
+                    TimeUtils.formatDuration(mLastTimeReadMs, sb);
+                    sb.append(",");
+                    TimeUtils.formatDuration(mNowTimeMs, sb);
+                    sb.append(")");
+                    Slog.e(TAG, sb.toString());
+                }
+                return;
+            }
+            curUidTimeMs[i] = totalTimeMs;
+            notify = notify || (deltaUidTimeMs[i] > 0);
+        }
+        if (notify) {
+            System.arraycopy(curUidTimeMs, 0, uidTimeMs, 0, size);
+            if (callback != null) {
+                callback.onUidCpuFreqTime(uid, deltaUidTimeMs);
+            }
+        }
+    }
+
+    private long[] readCpuFreqs(String line, PowerProfile powerProfile) {
+        final String[] freqStr = line.split(" ");
+        // First item would be "uid: " which needs to be ignored.
+        mCpuFreqsCount = freqStr.length - 1;
+        mCpuFreqs = new long[mCpuFreqsCount];
+        for (int i = 0; i < mCpuFreqsCount; ++i) {
+            mCpuFreqs[i] = Long.parseLong(freqStr[i + 1], 10);
+        }
+
+        // Check if the freqs in the proc file correspond to per-cluster freqs.
+        final IntArray numClusterFreqs = extractClusterInfoFromProcFileFreqs();
+        final int numClusters = powerProfile.getNumCpuClusters();
+        if (numClusterFreqs.size() == numClusters) {
+            mPerClusterTimesAvailable = true;
+            for (int i = 0; i < numClusters; ++i) {
+                if (numClusterFreqs.get(i) != powerProfile.getNumSpeedStepsInCpuCluster(i)) {
+                    mPerClusterTimesAvailable = false;
+                    break;
+                }
+            }
+        } else {
+            mPerClusterTimesAvailable = false;
+        }
+        Slog.i(TAG, "mPerClusterTimesAvailable=" + mPerClusterTimesAvailable);
+
+        return mCpuFreqs;
+    }
+
+    /**
+     * Extracts no. of cpu clusters and no. of freqs in each of these clusters from the freqs
+     * read from the proc file.
+     *
+     * We need to assume that freqs in each cluster are strictly increasing.
+     * For e.g. if the freqs read from proc file are: 12, 34, 15, 45, 12, 15, 52. Then it means
+     * there are 3 clusters: (12, 34), (15, 45), (12, 15, 52)
+     *
+     * @return an IntArray filled with no. of freqs in each cluster.
+     */
+    private IntArray extractClusterInfoFromProcFileFreqs() {
+        final IntArray numClusterFreqs = new IntArray();
+        int freqsFound = 0;
+        for (int i = 0; i < mCpuFreqsCount; ++i) {
+            freqsFound++;
+            if (i + 1 == mCpuFreqsCount || mCpuFreqs[i + 1] <= mCpuFreqs[i]) {
+                numClusterFreqs.add(freqsFound);
+                freqsFound = 0;
+            }
+        }
+        return numClusterFreqs;
+    }
+}
diff --git a/com/android/internal/os/KernelUidCpuTimeReader.java b/com/android/internal/os/KernelUidCpuTimeReader.java
new file mode 100644
index 0000000..37d9d1d
--- /dev/null
+++ b/com/android/internal/os/KernelUidCpuTimeReader.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseLongArray;
+import android.util.TimeUtils;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Reads /proc/uid_cputime/show_uid_stat which has the line format:
+ *
+ * uid: user_time_micro_seconds system_time_micro_seconds power_in_milli-amp-micro_seconds
+ *
+ * This provides the time a UID's processes spent executing in user-space and kernel-space.
+ * The file contains a monotonically increasing count of time for a single boot. This class
+ * maintains the previous results of a call to {@link #readDelta} in order to provide a proper
+ * delta.
+ */
+public class KernelUidCpuTimeReader {
+    private static final String TAG = "KernelUidCpuTimeReader";
+    private static final String sProcFile = "/proc/uid_cputime/show_uid_stat";
+    private static final String sRemoveUidProcFile = "/proc/uid_cputime/remove_uid_range";
+
+    /**
+     * Callback interface for processing each line of the proc file.
+     */
+    public interface Callback {
+        /**
+         * @param uid UID of the app
+         * @param userTimeUs time spent executing in user space in microseconds
+         * @param systemTimeUs time spent executing in kernel space in microseconds
+         */
+        void onUidCpuTime(int uid, long userTimeUs, long systemTimeUs);
+    }
+
+    private SparseLongArray mLastUserTimeUs = new SparseLongArray();
+    private SparseLongArray mLastSystemTimeUs = new SparseLongArray();
+    private long mLastTimeReadUs = 0;
+
+    /**
+     * Reads the proc file, calling into the callback with a delta of time for each UID.
+     * @param callback The callback to invoke for each line of the proc file. If null,
+     *                 the data is consumed and subsequent calls to readDelta will provide
+     *                 a fresh delta.
+     */
+    public void readDelta(@Nullable Callback callback) {
+        long nowUs = SystemClock.elapsedRealtime() * 1000;
+        try (BufferedReader reader = new BufferedReader(new FileReader(sProcFile))) {
+            TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' ');
+            String line;
+            while ((line = reader.readLine()) != null) {
+                splitter.setString(line);
+                final String uidStr = splitter.next();
+                final int uid = Integer.parseInt(uidStr.substring(0, uidStr.length() - 1), 10);
+                final long userTimeUs = Long.parseLong(splitter.next(), 10);
+                final long systemTimeUs = Long.parseLong(splitter.next(), 10);
+
+                // Only report if there is a callback and if this is not the first read.
+                if (callback != null && mLastTimeReadUs != 0) {
+                    long userTimeDeltaUs = userTimeUs;
+                    long systemTimeDeltaUs = systemTimeUs;
+                    int index = mLastUserTimeUs.indexOfKey(uid);
+                    if (index >= 0) {
+                        userTimeDeltaUs -= mLastUserTimeUs.valueAt(index);
+                        systemTimeDeltaUs -= mLastSystemTimeUs.valueAt(index);
+
+                        final long timeDiffUs = nowUs - mLastTimeReadUs;
+                        if (userTimeDeltaUs < 0 || systemTimeDeltaUs < 0) {
+                            StringBuilder sb = new StringBuilder("Malformed cpu data for UID=");
+                            sb.append(uid).append("!\n");
+                            sb.append("Time between reads: ");
+                            TimeUtils.formatDuration(timeDiffUs / 1000, sb);
+                            sb.append("\n");
+                            sb.append("Previous times: u=");
+                            TimeUtils.formatDuration(mLastUserTimeUs.valueAt(index) / 1000, sb);
+                            sb.append(" s=");
+                            TimeUtils.formatDuration(mLastSystemTimeUs.valueAt(index) / 1000, sb);
+
+                            sb.append("\nCurrent times: u=");
+                            TimeUtils.formatDuration(userTimeUs / 1000, sb);
+                            sb.append(" s=");
+                            TimeUtils.formatDuration(systemTimeUs / 1000, sb);
+                            sb.append("\nDelta: u=");
+                            TimeUtils.formatDuration(userTimeDeltaUs / 1000, sb);
+                            sb.append(" s=");
+                            TimeUtils.formatDuration(systemTimeDeltaUs / 1000, sb);
+                            Slog.e(TAG, sb.toString());
+
+                            userTimeDeltaUs = 0;
+                            systemTimeDeltaUs = 0;
+                        }
+                    }
+
+                    if (userTimeDeltaUs != 0 || systemTimeDeltaUs != 0) {
+                        callback.onUidCpuTime(uid, userTimeDeltaUs, systemTimeDeltaUs);
+                    }
+                }
+                mLastUserTimeUs.put(uid, userTimeUs);
+                mLastSystemTimeUs.put(uid, systemTimeUs);
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to read uid_cputime: " + e.getMessage());
+        }
+        mLastTimeReadUs = nowUs;
+    }
+
+    /**
+     * Removes the UID from the kernel module and from internal accounting data.
+     * @param uid The UID to remove.
+     */
+    public void removeUid(int uid) {
+        final int index = mLastSystemTimeUs.indexOfKey(uid);
+        if (index >= 0) {
+            mLastSystemTimeUs.removeAt(index);
+            mLastUserTimeUs.removeAt(index);
+        }
+        removeUidsFromKernelModule(uid, uid);
+    }
+
+    /**
+     * Removes UIDs in a given range from the kernel module and internal accounting data.
+     * @param startUid the first uid to remove
+     * @param endUid the last uid to remove
+     */
+    public void removeUidsInRange(int startUid, int endUid) {
+        if (endUid < startUid) {
+            return;
+        }
+        mLastSystemTimeUs.put(startUid, 0);
+        mLastUserTimeUs.put(startUid, 0);
+        mLastSystemTimeUs.put(endUid, 0);
+        mLastUserTimeUs.put(endUid, 0);
+        final int startIndex = mLastSystemTimeUs.indexOfKey(startUid);
+        final int endIndex = mLastSystemTimeUs.indexOfKey(endUid);
+        mLastSystemTimeUs.removeAtRange(startIndex, endIndex - startIndex + 1);
+        mLastUserTimeUs.removeAtRange(startIndex, endIndex - startIndex + 1);
+        removeUidsFromKernelModule(startUid, endUid);
+    }
+
+    private void removeUidsFromKernelModule(int startUid, int endUid) {
+        Slog.d(TAG, "Removing uids " + startUid + "-" + endUid);
+        try (FileWriter writer = new FileWriter(sRemoveUidProcFile)) {
+            writer.write(startUid + "-" + endUid);
+            writer.flush();
+        } catch (IOException e) {
+            Slog.e(TAG, "failed to remove uids " + startUid + " - " + endUid
+                    + " from uid_cputime module", e);
+        }
+    }
+}
diff --git a/com/android/internal/os/KernelWakelockReader.java b/com/android/internal/os/KernelWakelockReader.java
new file mode 100644
index 0000000..7178ec7
--- /dev/null
+++ b/com/android/internal/os/KernelWakelockReader.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileInputStream;
+import java.util.Iterator;
+
+/**
+ * Reads and parses wakelock stats from the kernel (/proc/wakelocks).
+ */
+public class KernelWakelockReader {
+    private static final String TAG = "KernelWakelockReader";
+    private static int sKernelWakelockUpdateVersion = 0;
+    private static final String sWakelockFile = "/proc/wakelocks";
+    private static final String sWakeupSourceFile = "/d/wakeup_sources";
+
+    private static final int[] PROC_WAKELOCKS_FORMAT = new int[] {
+        Process.PROC_TAB_TERM|Process.PROC_OUT_STRING|                // 0: name
+                              Process.PROC_QUOTES,
+        Process.PROC_TAB_TERM|Process.PROC_OUT_LONG,                  // 1: count
+        Process.PROC_TAB_TERM,
+        Process.PROC_TAB_TERM,
+        Process.PROC_TAB_TERM,
+        Process.PROC_TAB_TERM|Process.PROC_OUT_LONG,                  // 5: totalTime
+    };
+
+    private static final int[] WAKEUP_SOURCES_FORMAT = new int[] {
+        Process.PROC_TAB_TERM|Process.PROC_OUT_STRING,                // 0: name
+        Process.PROC_TAB_TERM|Process.PROC_COMBINE|
+                              Process.PROC_OUT_LONG,                  // 1: count
+        Process.PROC_TAB_TERM|Process.PROC_COMBINE,
+        Process.PROC_TAB_TERM|Process.PROC_COMBINE,
+        Process.PROC_TAB_TERM|Process.PROC_COMBINE,
+        Process.PROC_TAB_TERM|Process.PROC_COMBINE,
+        Process.PROC_TAB_TERM|Process.PROC_COMBINE
+                             |Process.PROC_OUT_LONG,                  // 6: totalTime
+    };
+
+    private final String[] mProcWakelocksName = new String[3];
+    private final long[] mProcWakelocksData = new long[3];
+
+    /**
+     * Reads kernel wakelock stats and updates the staleStats with the new information.
+     * @param staleStats Existing object to update.
+     * @return the updated data.
+     */
+    public final KernelWakelockStats readKernelWakelockStats(KernelWakelockStats staleStats) {
+        byte[] buffer = new byte[32*1024];
+        int len;
+        boolean wakeup_sources;
+        final long startTime = SystemClock.uptimeMillis();
+
+        try {
+            FileInputStream is;
+            try {
+                is = new FileInputStream(sWakelockFile);
+                wakeup_sources = false;
+            } catch (java.io.FileNotFoundException e) {
+                try {
+                    is = new FileInputStream(sWakeupSourceFile);
+                    wakeup_sources = true;
+                } catch (java.io.FileNotFoundException e2) {
+                    Slog.wtf(TAG, "neither " + sWakelockFile + " nor " +
+                            sWakeupSourceFile + " exists");
+                    return null;
+                }
+            }
+
+            len = is.read(buffer);
+            is.close();
+        } catch (java.io.IOException e) {
+            Slog.wtf(TAG, "failed to read kernel wakelocks", e);
+            return null;
+        }
+
+        final long readTime = SystemClock.uptimeMillis() - startTime;
+        if (readTime > 100) {
+            Slog.w(TAG, "Reading wakelock stats took " + readTime + "ms");
+        }
+
+        if (len > 0) {
+            if (len >= buffer.length) {
+                Slog.wtf(TAG, "Kernel wake locks exceeded buffer size " + buffer.length);
+            }
+            int i;
+            for (i=0; i<len; i++) {
+                if (buffer[i] == '\0') {
+                    len = i;
+                    break;
+                }
+            }
+        }
+        return parseProcWakelocks(buffer, len, wakeup_sources, staleStats);
+    }
+
+    /**
+     * Reads the wakelocks and updates the staleStats with the new information.
+     */
+    @VisibleForTesting
+    public KernelWakelockStats parseProcWakelocks(byte[] wlBuffer, int len, boolean wakeup_sources,
+                                                  final KernelWakelockStats staleStats) {
+        String name;
+        int count;
+        long totalTime;
+        int startIndex;
+        int endIndex;
+
+        // Advance past the first line.
+        int i;
+        for (i = 0; i < len && wlBuffer[i] != '\n' && wlBuffer[i] != '\0'; i++);
+        startIndex = endIndex = i + 1;
+
+        synchronized(this) {
+            sKernelWakelockUpdateVersion++;
+            while (endIndex < len) {
+                for (endIndex=startIndex;
+                        endIndex < len && wlBuffer[endIndex] != '\n' && wlBuffer[endIndex] != '\0';
+                        endIndex++);
+                // Don't go over the end of the buffer, Process.parseProcLine might
+                // write to wlBuffer[endIndex]
+                if (endIndex > (len - 1) ) {
+                    break;
+                }
+
+                String[] nameStringArray = mProcWakelocksName;
+                long[] wlData = mProcWakelocksData;
+                // Stomp out any bad characters since this is from a circular buffer
+                // A corruption is seen sometimes that results in the vm crashing
+                // This should prevent crashes and the line will probably fail to parse
+                for (int j = startIndex; j < endIndex; j++) {
+                    if ((wlBuffer[j] & 0x80) != 0) wlBuffer[j] = (byte) '?';
+                }
+                boolean parsed = Process.parseProcLine(wlBuffer, startIndex, endIndex,
+                        wakeup_sources ? WAKEUP_SOURCES_FORMAT :
+                                         PROC_WAKELOCKS_FORMAT,
+                        nameStringArray, wlData, null);
+
+                name = nameStringArray[0];
+                count = (int) wlData[1];
+
+                if (wakeup_sources) {
+                        // convert milliseconds to microseconds
+                        totalTime = wlData[2] * 1000;
+                } else {
+                        // convert nanoseconds to microseconds with rounding.
+                        totalTime = (wlData[2] + 500) / 1000;
+                }
+
+                if (parsed && name.length() > 0) {
+                    if (!staleStats.containsKey(name)) {
+                        staleStats.put(name, new KernelWakelockStats.Entry(count, totalTime,
+                                sKernelWakelockUpdateVersion));
+                    } else {
+                        KernelWakelockStats.Entry kwlStats = staleStats.get(name);
+                        if (kwlStats.mVersion == sKernelWakelockUpdateVersion) {
+                            kwlStats.mCount += count;
+                            kwlStats.mTotalTime += totalTime;
+                        } else {
+                            kwlStats.mCount = count;
+                            kwlStats.mTotalTime = totalTime;
+                            kwlStats.mVersion = sKernelWakelockUpdateVersion;
+                        }
+                    }
+                } else if (!parsed) {
+                    try {
+                        Slog.wtf(TAG, "Failed to parse proc line: " +
+                                new String(wlBuffer, startIndex, endIndex - startIndex));
+                    } catch (Exception e) {
+                        Slog.wtf(TAG, "Failed to parse proc line!");
+                    }
+                }
+                startIndex = endIndex + 1;
+            }
+
+            // Don't report old data.
+            Iterator<KernelWakelockStats.Entry> itr = staleStats.values().iterator();
+            while (itr.hasNext()) {
+                if (itr.next().mVersion != sKernelWakelockUpdateVersion) {
+                    itr.remove();
+                }
+            }
+
+            staleStats.kernelWakelockVersion = sKernelWakelockUpdateVersion;
+            return staleStats;
+        }
+    }
+}
diff --git a/com/android/internal/os/KernelWakelockStats.java b/com/android/internal/os/KernelWakelockStats.java
new file mode 100644
index 0000000..144ea00
--- /dev/null
+++ b/com/android/internal/os/KernelWakelockStats.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import java.util.HashMap;
+
+/**
+ * Kernel wakelock stats object.
+ */
+public class KernelWakelockStats extends HashMap<String, KernelWakelockStats.Entry> {
+    public static class Entry {
+        public int mCount;
+        public long mTotalTime;
+        public int mVersion;
+
+        Entry(int count, long totalTime, int version) {
+            mCount = count;
+            mTotalTime = totalTime;
+            mVersion = version;
+        }
+    }
+
+    int kernelWakelockVersion;
+}
diff --git a/com/android/internal/os/LoggingPrintStream.java b/com/android/internal/os/LoggingPrintStream.java
new file mode 100644
index 0000000..f14394a
--- /dev/null
+++ b/com/android/internal/os/LoggingPrintStream.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2008 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.internal.os;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * A print stream which logs output line by line.
+ *
+ * {@hide}
+ */
+abstract class LoggingPrintStream extends PrintStream {
+
+    private final StringBuilder builder = new StringBuilder();
+
+    /**
+     * A buffer that is initialized when raw bytes are first written to this
+     * stream. It may contain the leading bytes of multi-byte characters.
+     * Between writes this buffer is always ready to receive data; ie. the
+     * position is at the first unassigned byte and the limit is the capacity.
+     */
+    private ByteBuffer encodedBytes;
+
+    /**
+     * A buffer that is initialized when raw bytes are first written to this
+     * stream. Between writes this buffer is always clear; ie. the position is
+     * zero and the limit is the capacity.
+     */
+    private CharBuffer decodedChars;
+
+    /**
+     * Decodes bytes to characters using the system default charset. Initialized
+     * when raw bytes are first written to this stream.
+     */
+    private CharsetDecoder decoder;
+
+    protected LoggingPrintStream() {
+        super(new OutputStream() {
+            public void write(int oneByte) throws IOException {
+                throw new AssertionError();
+            }
+        });
+    }
+
+    /**
+     * Logs the given line.
+     */
+    protected abstract void log(String line);
+
+    @Override
+    public synchronized void flush() {
+        flush(true);
+    }
+
+    /**
+     * Searches buffer for line breaks and logs a message for each one.
+     *
+     * @param completely true if the ending chars should be treated as a line
+     *  even though they don't end in a line break
+     */
+    private void flush(boolean completely) {
+        int length = builder.length();
+
+        int start = 0;
+        int nextBreak;
+
+        // Log one line for each line break.
+        while (start < length
+                && (nextBreak = builder.indexOf("\n", start)) != -1) {
+            log(builder.substring(start, nextBreak));
+            start = nextBreak + 1;
+        }
+
+        if (completely) {
+            // Log the remainder of the buffer.
+            if (start < length) {
+                log(builder.substring(start));
+            }
+            builder.setLength(0);
+        } else {
+            // Delete characters leading up to the next starting point.
+            builder.delete(0, start);
+        }
+    }
+
+    public void write(int oneByte) {
+        write(new byte[] { (byte) oneByte }, 0, 1);
+    }
+
+    @Override
+    public void write(byte[] buffer) {
+        write(buffer, 0, buffer.length);
+    }
+
+    @Override
+    public synchronized void write(byte bytes[], int start, int count) {
+        if (decoder == null) {
+            encodedBytes = ByteBuffer.allocate(80);
+            decodedChars = CharBuffer.allocate(80);
+            decoder = Charset.defaultCharset().newDecoder()
+                    .onMalformedInput(CodingErrorAction.REPLACE)
+                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
+        }
+
+        int end = start + count;
+        while (start < end) {
+            // copy some bytes from the array to the long-lived buffer. This
+            // way, if we end with a partial character we don't lose it.
+            int numBytes = Math.min(encodedBytes.remaining(), end - start);
+            encodedBytes.put(bytes, start, numBytes);
+            start += numBytes;
+
+            encodedBytes.flip();
+            CoderResult coderResult;
+            do {
+                // decode bytes from the byte buffer into the char buffer
+                coderResult = decoder.decode(encodedBytes, decodedChars, false);
+
+                // copy chars from the char buffer into our string builder
+                decodedChars.flip();
+                builder.append(decodedChars);
+                decodedChars.clear();
+            } while (coderResult.isOverflow());
+            encodedBytes.compact();
+        }
+        flush(false);
+    }
+
+    /** Always returns false. */
+    @Override
+    public boolean checkError() {
+        return false;
+    }
+
+    /** Ignored. */
+    @Override
+    protected void setError() { /* ignored */ }
+
+    /** Ignored. */
+    @Override
+    public void close() { /* ignored */ }
+
+    @Override
+    public PrintStream format(String format, Object... args) {
+        return format(Locale.getDefault(), format, args);
+    }
+
+    @Override
+    public PrintStream printf(String format, Object... args) {
+        return format(format, args);
+    }
+
+    @Override
+    public PrintStream printf(Locale l, String format, Object... args) {
+        return format(l, format, args);
+    }
+
+    private final Formatter formatter = new Formatter(builder, null);
+
+    @Override
+    public synchronized PrintStream format(
+            Locale l, String format, Object... args) {
+        if (format == null) {
+            throw new NullPointerException("format");
+        }
+
+        formatter.format(l, format, args);
+        flush(false);
+        return this;
+    }
+
+    @Override
+    public synchronized void print(char[] charArray) {
+        builder.append(charArray);
+        flush(false);
+    }
+
+    @Override
+    public synchronized void print(char ch) {
+        builder.append(ch);
+        if (ch == '\n') {
+            flush(false);
+        }
+    }
+
+    @Override
+    public synchronized void print(double dnum) {
+        builder.append(dnum);
+    }
+
+    @Override
+    public synchronized void print(float fnum) {
+        builder.append(fnum);
+    }
+
+    @Override
+    public synchronized void print(int inum) {
+        builder.append(inum);
+    }
+
+    @Override
+    public synchronized void print(long lnum) {
+        builder.append(lnum);
+    }
+
+    @Override
+    public synchronized void print(Object obj) {
+        builder.append(obj);
+        flush(false);
+    }
+
+    @Override
+    public synchronized void print(String str) {
+        builder.append(str);
+        flush(false);
+    }
+
+    @Override
+    public synchronized void print(boolean bool) {
+        builder.append(bool);
+    }
+
+    @Override
+    public synchronized void println() {
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(char[] charArray) {
+        builder.append(charArray);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(char ch) {
+        builder.append(ch);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(double dnum) {
+        builder.append(dnum);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(float fnum) {
+        builder.append(fnum);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(int inum) {
+        builder.append(inum);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(long lnum) {
+        builder.append(lnum);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(Object obj) {
+        builder.append(obj);
+        flush(true);
+    }
+
+    @Override
+    public synchronized void println(String s) {
+        if (builder.length() == 0 && s != null) {
+            // Optimization for a simple println.
+            int length = s.length();
+
+            int start = 0;
+            int nextBreak;
+
+            // Log one line for each line break.
+            while (start < length
+                    && (nextBreak = s.indexOf('\n', start)) != -1) {
+                log(s.substring(start, nextBreak));
+                start = nextBreak + 1;
+            }
+
+            if (start < length) {
+                log(s.substring(start));
+            }
+        } else {
+            builder.append(s);
+            flush(true);
+        }
+    }
+
+    @Override
+    public synchronized void println(boolean bool) {
+        builder.append(bool);
+        flush(true);
+    }
+
+    @Override
+    public synchronized PrintStream append(char c) {
+        print(c);
+        return this;
+    }
+
+    @Override
+    public synchronized PrintStream append(CharSequence csq) {
+        builder.append(csq);
+        flush(false);
+        return this;
+    }
+
+    @Override
+    public synchronized PrintStream append(
+            CharSequence csq, int start, int end) {
+        builder.append(csq, start, end);
+        flush(false);
+        return this;
+    }
+}
diff --git a/com/android/internal/os/MemoryPowerCalculator.java b/com/android/internal/os/MemoryPowerCalculator.java
new file mode 100644
index 0000000..efd3ab5
--- /dev/null
+++ b/com/android/internal/os/MemoryPowerCalculator.java
@@ -0,0 +1,54 @@
+package com.android.internal.os;
+
+import android.os.BatteryStats;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+public class MemoryPowerCalculator extends PowerCalculator {
+
+    public static final String TAG = "MemoryPowerCalculator";
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private final double[] powerAverages;
+
+    public MemoryPowerCalculator(PowerProfile profile) {
+        int numBuckets = profile.getNumElements(PowerProfile.POWER_MEMORY);
+        powerAverages = new double[numBuckets];
+        for (int i = 0; i < numBuckets; i++) {
+            powerAverages[i] = profile.getAveragePower(PowerProfile.POWER_MEMORY, i);
+            if (powerAverages[i] == 0 && DEBUG) {
+                Log.d(TAG, "Problem with PowerProfile. Received 0 value in MemoryPowerCalculator");
+            }
+        }
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+            long rawUptimeUs, int statsType) {}
+
+    @Override
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+            long rawUptimeUs, int statsType) {
+        double totalMah = 0;
+        long totalTimeMs = 0;
+        LongSparseArray<? extends BatteryStats.Timer> timers = stats.getKernelMemoryStats();
+        for (int i = 0; i < timers.size() && i < powerAverages.length; i++) {
+            double mAatRail = powerAverages[(int) timers.keyAt(i)];
+            long timeMs = timers.valueAt(i).getTotalTimeLocked(rawRealtimeUs, statsType);
+            double mAm = (mAatRail * timeMs) / (1000*60);
+            if(DEBUG) {
+                Log.d(TAG, "Calculating mAh for bucket " + timers.keyAt(i) + " while unplugged");
+                Log.d(TAG, "Converted power profile number from "
+                        + powerAverages[(int) timers.keyAt(i)] + " into " + mAatRail);
+                Log.d(TAG, "Calculated mAm " + mAm);
+            }
+            totalMah += mAm/60;
+            totalTimeMs += timeMs;
+        }
+        app.usagePowerMah = totalMah;
+        app.usageTimeMs = totalTimeMs;
+        if (DEBUG) {
+            Log.d(TAG, String.format("Calculated total mAh for memory %f while unplugged %d ",
+                    totalMah, totalTimeMs));
+        }
+    }
+}
diff --git a/com/android/internal/os/MobileRadioPowerCalculator.java b/com/android/internal/os/MobileRadioPowerCalculator.java
new file mode 100644
index 0000000..8586d76
--- /dev/null
+++ b/com/android/internal/os/MobileRadioPowerCalculator.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+import android.telephony.SignalStrength;
+import android.util.Log;
+
+public class MobileRadioPowerCalculator extends PowerCalculator {
+    private static final String TAG = "MobileRadioPowerController";
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private final double mPowerRadioOn;
+    private final double[] mPowerBins = new double[SignalStrength.NUM_SIGNAL_STRENGTH_BINS];
+    private final double mPowerScan;
+    private BatteryStats mStats;
+    private long mTotalAppMobileActiveMs = 0;
+
+    /**
+     * Return estimated power (in mAs) of sending or receiving a packet with the mobile radio.
+     */
+    private double getMobilePowerPerPacket(long rawRealtimeUs, int statsType) {
+        final long MOBILE_BPS = 200000; // TODO: Extract average bit rates from system
+        final double MOBILE_POWER = mPowerRadioOn / 3600;
+
+        final long mobileRx = mStats.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_RX_DATA,
+                statsType);
+        final long mobileTx = mStats.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_TX_DATA,
+                statsType);
+        final long mobileData = mobileRx + mobileTx;
+
+        final long radioDataUptimeMs =
+                mStats.getMobileRadioActiveTime(rawRealtimeUs, statsType) / 1000;
+        final double mobilePps = (mobileData != 0 && radioDataUptimeMs != 0)
+                ? (mobileData / (double)radioDataUptimeMs)
+                : (((double)MOBILE_BPS) / 8 / 2048);
+        return (MOBILE_POWER / mobilePps) / (60*60);
+    }
+
+    public MobileRadioPowerCalculator(PowerProfile profile, BatteryStats stats) {
+        mPowerRadioOn = profile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE);
+        for (int i = 0; i < mPowerBins.length; i++) {
+            mPowerBins[i] = profile.getAveragePower(PowerProfile.POWER_RADIO_ON, i);
+        }
+        mPowerScan = profile.getAveragePower(PowerProfile.POWER_RADIO_SCANNING);
+        mStats = stats;
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+        // Add cost of mobile traffic.
+        app.mobileRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_RX_DATA,
+                statsType);
+        app.mobileTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_TX_DATA,
+                statsType);
+        app.mobileActive = u.getMobileRadioActiveTime(statsType) / 1000;
+        app.mobileActiveCount = u.getMobileRadioActiveCount(statsType);
+        app.mobileRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_MOBILE_RX_DATA,
+                statsType);
+        app.mobileTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_MOBILE_TX_DATA,
+                statsType);
+
+        if (app.mobileActive > 0) {
+            // We are tracking when the radio is up, so can use the active time to
+            // determine power use.
+            mTotalAppMobileActiveMs += app.mobileActive;
+            app.mobileRadioPowerMah = (app.mobileActive * mPowerRadioOn) / (1000*60*60);
+        } else {
+            // We are not tracking when the radio is up, so must approximate power use
+            // based on the number of packets.
+            app.mobileRadioPowerMah = (app.mobileRxPackets + app.mobileTxPackets)
+                    * getMobilePowerPerPacket(rawRealtimeUs, statsType);
+        }
+        if (DEBUG && app.mobileRadioPowerMah != 0) {
+            Log.d(TAG, "UID " + u.getUid() + ": mobile packets "
+                    + (app.mobileRxPackets + app.mobileTxPackets)
+                    + " active time " + app.mobileActive
+                    + " power=" + BatteryStatsHelper.makemAh(app.mobileRadioPowerMah));
+        }
+    }
+
+    @Override
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+                                   long rawUptimeUs, int statsType) {
+        double power = 0;
+        long signalTimeMs = 0;
+        long noCoverageTimeMs = 0;
+        for (int i = 0; i < mPowerBins.length; i++) {
+            long strengthTimeMs = stats.getPhoneSignalStrengthTime(i, rawRealtimeUs, statsType)
+                    / 1000;
+            final double p = (strengthTimeMs * mPowerBins[i]) / (60*60*1000);
+            if (DEBUG && p != 0) {
+                Log.d(TAG, "Cell strength #" + i + ": time=" + strengthTimeMs + " power="
+                        + BatteryStatsHelper.makemAh(p));
+            }
+            power += p;
+            signalTimeMs += strengthTimeMs;
+            if (i == 0) {
+                noCoverageTimeMs = strengthTimeMs;
+            }
+        }
+
+        final long scanningTimeMs = stats.getPhoneSignalScanningTime(rawRealtimeUs, statsType)
+                / 1000;
+        final double p = (scanningTimeMs * mPowerScan) / (60*60*1000);
+        if (DEBUG && p != 0) {
+            Log.d(TAG, "Cell radio scanning: time=" + scanningTimeMs
+                    + " power=" + BatteryStatsHelper.makemAh(p));
+        }
+        power += p;
+        long radioActiveTimeMs = mStats.getMobileRadioActiveTime(rawRealtimeUs, statsType) / 1000;
+        long remainingActiveTimeMs = radioActiveTimeMs - mTotalAppMobileActiveMs;
+        if (remainingActiveTimeMs > 0) {
+            power += (mPowerRadioOn * remainingActiveTimeMs) / (1000*60*60);
+        }
+
+        if (power != 0) {
+            if (signalTimeMs != 0) {
+                app.noCoveragePercent = noCoverageTimeMs * 100.0 / signalTimeMs;
+            }
+            app.mobileActive = remainingActiveTimeMs;
+            app.mobileActiveCount = stats.getMobileRadioActiveUnknownCount(statsType);
+            app.mobileRadioPowerMah = power;
+        }
+    }
+
+    @Override
+    public void reset() {
+        mTotalAppMobileActiveMs = 0;
+    }
+
+    public void reset(BatteryStats stats) {
+        reset();
+        mStats = stats;
+    }
+}
diff --git a/com/android/internal/os/PowerCalculator.java b/com/android/internal/os/PowerCalculator.java
new file mode 100644
index 0000000..cd69d68
--- /dev/null
+++ b/com/android/internal/os/PowerCalculator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+
+/**
+ * Calculates power use of a device subsystem for an app.
+ */
+public abstract class PowerCalculator {
+    /**
+     * Calculate the amount of power an app used for this subsystem.
+     * @param app The BatterySipper that represents the power use of an app.
+     * @param u The recorded stats for the app.
+     * @param rawRealtimeUs The raw system realtime in microseconds.
+     * @param rawUptimeUs The raw system uptime in microseconds.
+     * @param statsType The type of stats. Can be {@link BatteryStats#STATS_CURRENT},
+     *                  {@link BatteryStats#STATS_SINCE_CHARGED}, or
+     *                  {@link BatteryStats#STATS_SINCE_UNPLUGGED}.
+     */
+    public abstract void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                                      long rawUptimeUs, int statsType);
+
+    /**
+     * Calculate the remaining power that can not be attributed to an app.
+     * @param app The BatterySipper that will represent this remaining power.
+     * @param stats The BatteryStats object from which to retrieve data.
+     * @param rawRealtimeUs The raw system realtime in microseconds.
+     * @param rawUptimeUs The raw system uptime in microseconds.
+     * @param statsType The type of stats. Can be {@link BatteryStats#STATS_CURRENT},
+     *                  {@link BatteryStats#STATS_SINCE_CHARGED}, or
+     *                  {@link BatteryStats#STATS_SINCE_UNPLUGGED}.
+     */
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+                                   long rawUptimeUs, int statsType) {
+    }
+
+    /**
+     * Reset any state maintained in this calculator.
+     */
+    public void reset() {
+    }
+}
diff --git a/com/android/internal/os/PowerProfile.java b/com/android/internal/os/PowerProfile.java
new file mode 100644
index 0000000..872b465
--- /dev/null
+++ b/com/android/internal/os/PowerProfile.java
@@ -0,0 +1,459 @@
+/*
+ * 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.internal.os;
+
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Reports power consumption values for various device activities. Reads values from an XML file.
+ * Customize the XML file for different devices.
+ * [hidden]
+ */
+public class PowerProfile {
+
+    /**
+     * No power consumption, or accounted for elsewhere.
+     */
+    public static final String POWER_NONE = "none";
+
+    /**
+     * Power consumption when CPU is in power collapse mode.
+     */
+    public static final String POWER_CPU_IDLE = "cpu.idle";
+
+    /**
+     * Power consumption when CPU is awake (when a wake lock is held).  This
+     * should be 0 on devices that can go into full CPU power collapse even
+     * when a wake lock is held.  Otherwise, this is the power consumption in
+     * addition to POWER_CPU_IDLE due to a wake lock being held but with no
+     * CPU activity.
+     */
+    public static final String POWER_CPU_AWAKE = "cpu.awake";
+
+    /**
+     * Power consumption when CPU is in power collapse mode.
+     */
+    @Deprecated
+    public static final String POWER_CPU_ACTIVE = "cpu.active";
+
+    /**
+     * Power consumption when WiFi driver is scanning for networks.
+     */
+    public static final String POWER_WIFI_SCAN = "wifi.scan";
+
+    /**
+     * Power consumption when WiFi driver is on.
+     */
+    public static final String POWER_WIFI_ON = "wifi.on";
+
+    /**
+     * Power consumption when WiFi driver is transmitting/receiving.
+     */
+    public static final String POWER_WIFI_ACTIVE = "wifi.active";
+
+    //
+    // Updated power constants. These are not estimated, they are real world
+    // currents and voltages for the underlying bluetooth and wifi controllers.
+    //
+
+    public static final String POWER_WIFI_CONTROLLER_IDLE = "wifi.controller.idle";
+    public static final String POWER_WIFI_CONTROLLER_RX = "wifi.controller.rx";
+    public static final String POWER_WIFI_CONTROLLER_TX = "wifi.controller.tx";
+    public static final String POWER_WIFI_CONTROLLER_TX_LEVELS = "wifi.controller.tx_levels";
+    public static final String POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE = "wifi.controller.voltage";
+
+    public static final String POWER_BLUETOOTH_CONTROLLER_IDLE = "bluetooth.controller.idle";
+    public static final String POWER_BLUETOOTH_CONTROLLER_RX = "bluetooth.controller.rx";
+    public static final String POWER_BLUETOOTH_CONTROLLER_TX = "bluetooth.controller.tx";
+    public static final String POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE =
+            "bluetooth.controller.voltage";
+
+    public static final String POWER_MODEM_CONTROLLER_IDLE = "modem.controller.idle";
+    public static final String POWER_MODEM_CONTROLLER_RX = "modem.controller.rx";
+    public static final String POWER_MODEM_CONTROLLER_TX = "modem.controller.tx";
+    public static final String POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE =
+            "modem.controller.voltage";
+
+    /**
+     * Power consumption when GPS is on.
+     */
+    public static final String POWER_GPS_ON = "gps.on";
+
+    /**
+     * Power consumption when Bluetooth driver is on.
+     * @deprecated
+     */
+    @Deprecated
+    public static final String POWER_BLUETOOTH_ON = "bluetooth.on";
+
+    /**
+     * Power consumption when Bluetooth driver is transmitting/receiving.
+     * @deprecated
+     */
+    @Deprecated
+    public static final String POWER_BLUETOOTH_ACTIVE = "bluetooth.active";
+
+    /**
+     * Power consumption when Bluetooth driver gets an AT command.
+     * @deprecated
+     */
+    @Deprecated
+    public static final String POWER_BLUETOOTH_AT_CMD = "bluetooth.at";
+
+
+    /**
+     * Power consumption when screen is on, not including the backlight power.
+     */
+    public static final String POWER_SCREEN_ON = "screen.on";
+
+    /**
+     * Power consumption when cell radio is on but not on a call.
+     */
+    public static final String POWER_RADIO_ON = "radio.on";
+
+    /**
+     * Power consumption when cell radio is hunting for a signal.
+     */
+    public static final String POWER_RADIO_SCANNING = "radio.scanning";
+
+    /**
+     * Power consumption when talking on the phone.
+     */
+    public static final String POWER_RADIO_ACTIVE = "radio.active";
+
+    /**
+     * Power consumption at full backlight brightness. If the backlight is at
+     * 50% brightness, then this should be multiplied by 0.5
+     */
+    public static final String POWER_SCREEN_FULL = "screen.full";
+
+    /**
+     * Power consumed by the audio hardware when playing back audio content. This is in addition
+     * to the CPU power, probably due to a DSP and / or amplifier.
+     */
+    public static final String POWER_AUDIO = "dsp.audio";
+
+    /**
+     * Power consumed by any media hardware when playing back video content. This is in addition
+     * to the CPU power, probably due to a DSP.
+     */
+    public static final String POWER_VIDEO = "dsp.video";
+
+    /**
+     * Average power consumption when camera flashlight is on.
+     */
+    public static final String POWER_FLASHLIGHT = "camera.flashlight";
+
+    /**
+     * Power consumption when DDR is being used.
+     */
+    public static final String POWER_MEMORY = "memory.bandwidths";
+
+    /**
+     * Average power consumption when the camera is on over all standard use cases.
+     *
+     * TODO: Add more fine-grained camera power metrics.
+     */
+    public static final String POWER_CAMERA = "camera.avg";
+
+    @Deprecated
+    public static final String POWER_CPU_SPEEDS = "cpu.speeds";
+
+    /**
+     * Power consumed by wif batched scaning.  Broken down into bins by
+     * Channels Scanned per Hour.  May do 1-720 scans per hour of 1-100 channels
+     * for a range of 1-72,000.  Going logrithmic (1-8, 9-64, 65-512, 513-4096, 4097-)!
+     */
+    public static final String POWER_WIFI_BATCHED_SCAN = "wifi.batchedscan";
+
+    /**
+     * Battery capacity in milliAmpHour (mAh).
+     */
+    public static final String POWER_BATTERY_CAPACITY = "battery.capacity";
+
+    static final HashMap<String, Object> sPowerMap = new HashMap<>();
+
+    private static final String TAG_DEVICE = "device";
+    private static final String TAG_ITEM = "item";
+    private static final String TAG_ARRAY = "array";
+    private static final String TAG_ARRAYITEM = "value";
+    private static final String ATTR_NAME = "name";
+
+    private static final Object sLock = new Object();
+
+    public PowerProfile(Context context) {
+        // Read the XML file for the given profile (normally only one per
+        // device)
+        synchronized (sLock) {
+            if (sPowerMap.size() == 0) {
+                readPowerValuesFromXml(context);
+            }
+            initCpuClusters();
+        }
+    }
+
+    private void readPowerValuesFromXml(Context context) {
+        int id = com.android.internal.R.xml.power_profile;
+        final Resources resources = context.getResources();
+        XmlResourceParser parser = resources.getXml(id);
+        boolean parsingArray = false;
+        ArrayList<Double> array = new ArrayList<Double>();
+        String arrayName = null;
+
+        try {
+            XmlUtils.beginDocument(parser, TAG_DEVICE);
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+
+                String element = parser.getName();
+                if (element == null) break;
+
+                if (parsingArray && !element.equals(TAG_ARRAYITEM)) {
+                    // Finish array
+                    sPowerMap.put(arrayName, array.toArray(new Double[array.size()]));
+                    parsingArray = false;
+                }
+                if (element.equals(TAG_ARRAY)) {
+                    parsingArray = true;
+                    array.clear();
+                    arrayName = parser.getAttributeValue(null, ATTR_NAME);
+                } else if (element.equals(TAG_ITEM) || element.equals(TAG_ARRAYITEM)) {
+                    String name = null;
+                    if (!parsingArray) name = parser.getAttributeValue(null, ATTR_NAME);
+                    if (parser.next() == XmlPullParser.TEXT) {
+                        String power = parser.getText();
+                        double value = 0;
+                        try {
+                            value = Double.valueOf(power);
+                        } catch (NumberFormatException nfe) {
+                        }
+                        if (element.equals(TAG_ITEM)) {
+                            sPowerMap.put(name, value);
+                        } else if (parsingArray) {
+                            array.add(value);
+                        }
+                    }
+                }
+            }
+            if (parsingArray) {
+                sPowerMap.put(arrayName, array.toArray(new Double[array.size()]));
+            }
+        } catch (XmlPullParserException e) {
+            throw new RuntimeException(e);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            parser.close();
+        }
+
+        // Now collect other config variables.
+        int[] configResIds = new int[]{
+                com.android.internal.R.integer.config_bluetooth_idle_cur_ma,
+                com.android.internal.R.integer.config_bluetooth_rx_cur_ma,
+                com.android.internal.R.integer.config_bluetooth_tx_cur_ma,
+                com.android.internal.R.integer.config_bluetooth_operating_voltage_mv,
+                com.android.internal.R.integer.config_wifi_idle_receive_cur_ma,
+                com.android.internal.R.integer.config_wifi_active_rx_cur_ma,
+                com.android.internal.R.integer.config_wifi_tx_cur_ma,
+                com.android.internal.R.integer.config_wifi_operating_voltage_mv,
+        };
+
+        String[] configResIdKeys = new String[]{
+                POWER_BLUETOOTH_CONTROLLER_IDLE,
+                POWER_BLUETOOTH_CONTROLLER_RX,
+                POWER_BLUETOOTH_CONTROLLER_TX,
+                POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE,
+                POWER_WIFI_CONTROLLER_IDLE,
+                POWER_WIFI_CONTROLLER_RX,
+                POWER_WIFI_CONTROLLER_TX,
+                POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE,
+        };
+
+        for (int i = 0; i < configResIds.length; i++) {
+            String key = configResIdKeys[i];
+            // if we already have some of these parameters in power_profile.xml, ignore the
+            // value in config.xml
+            if ((sPowerMap.containsKey(key) && (Double) sPowerMap.get(key) > 0)) {
+                continue;
+            }
+            int value = resources.getInteger(configResIds[i]);
+            if (value > 0) {
+                sPowerMap.put(key, (double) value);
+            }
+        }
+    }
+
+    private CpuClusterKey[] mCpuClusters;
+
+    private static final String POWER_CPU_CLUSTER_CORE_COUNT = "cpu.clusters.cores";
+    private static final String POWER_CPU_CLUSTER_SPEED_PREFIX = "cpu.speeds.cluster";
+    private static final String POWER_CPU_CLUSTER_ACTIVE_PREFIX = "cpu.active.cluster";
+
+    @SuppressWarnings("deprecation")
+    private void initCpuClusters() {
+        // Figure out how many CPU clusters we're dealing with
+        final Object obj = sPowerMap.get(POWER_CPU_CLUSTER_CORE_COUNT);
+        if (obj == null || !(obj instanceof Double[])) {
+            // Default to single.
+            mCpuClusters = new CpuClusterKey[1];
+            mCpuClusters[0] = new CpuClusterKey(POWER_CPU_SPEEDS, POWER_CPU_ACTIVE, 1);
+
+        } else {
+            final Double[] array = (Double[]) obj;
+            mCpuClusters = new CpuClusterKey[array.length];
+            for (int cluster = 0; cluster < array.length; cluster++) {
+                int numCpusInCluster = (int) Math.round(array[cluster]);
+                mCpuClusters[cluster] = new CpuClusterKey(
+                        POWER_CPU_CLUSTER_SPEED_PREFIX + cluster,
+                        POWER_CPU_CLUSTER_ACTIVE_PREFIX + cluster,
+                        numCpusInCluster);
+            }
+        }
+    }
+
+    public static class CpuClusterKey {
+        private final String timeKey;
+        private final String powerKey;
+        private final int numCpus;
+
+        private CpuClusterKey(String timeKey, String powerKey, int numCpus) {
+            this.timeKey = timeKey;
+            this.powerKey = powerKey;
+            this.numCpus = numCpus;
+        }
+    }
+
+    public int getNumCpuClusters() {
+        return mCpuClusters.length;
+    }
+
+    public int getNumCoresInCpuCluster(int index) {
+        return mCpuClusters[index].numCpus;
+    }
+
+    public int getNumSpeedStepsInCpuCluster(int index) {
+        Object value = sPowerMap.get(mCpuClusters[index].timeKey);
+        if (value != null && value instanceof Double[]) {
+            return ((Double[])value).length;
+        }
+        return 1; // Only one speed
+    }
+
+    public double getAveragePowerForCpu(int cluster, int step) {
+        if (cluster >= 0 && cluster < mCpuClusters.length) {
+            return getAveragePower(mCpuClusters[cluster].powerKey, step);
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the number of memory bandwidth buckets defined in power_profile.xml, or a
+     * default value if the subsystem has no recorded value.
+     * @return the number of memory bandwidth buckets.
+     */
+    public int getNumElements(String key) {
+        if (sPowerMap.containsKey(key)) {
+            Object data = sPowerMap.get(key);
+            if (data instanceof Double[]) {
+                final Double[] values = (Double[]) data;
+                return values.length;
+            } else {
+                return 1;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the average current in mA consumed by the subsystem, or the given
+     * default value if the subsystem has no recorded value.
+     * @param type the subsystem type
+     * @param defaultValue the value to return if the subsystem has no recorded value.
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePowerOrDefault(String type, double defaultValue) {
+        if (sPowerMap.containsKey(type)) {
+            Object data = sPowerMap.get(type);
+            if (data instanceof Double[]) {
+                return ((Double[])data)[0];
+            } else {
+                return (Double) sPowerMap.get(type);
+            }
+        } else {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Returns the average current in mA consumed by the subsystem
+     * @param type the subsystem type
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePower(String type) {
+        return getAveragePowerOrDefault(type, 0);
+    }
+    
+    /**
+     * Returns the average current in mA consumed by the subsystem for the given level.
+     * @param type the subsystem type
+     * @param level the level of power at which the subsystem is running. For instance, the
+     *  signal strength of the cell network between 0 and 4 (if there are 4 bars max.)
+     *  If there is no data for multiple levels, the level is ignored.
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePower(String type, int level) {
+        if (sPowerMap.containsKey(type)) {
+            Object data = sPowerMap.get(type);
+            if (data instanceof Double[]) {
+                final Double[] values = (Double[]) data;
+                if (values.length > level && level >= 0) {
+                    return values[level];
+                } else if (level < 0 || values.length == 0) {
+                    return 0;
+                } else {
+                    return values[values.length - 1];
+                }
+            } else {
+                return (Double) data;
+            }
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Returns the battery capacity, if available, in milli Amp Hours. If not available,
+     * it returns zero.
+     * @return the battery capacity in mAh
+     */
+    public double getBatteryCapacity() {
+        return getAveragePower(POWER_BATTERY_CAPACITY);
+    }
+}
diff --git a/com/android/internal/os/ProcessCpuTracker.java b/com/android/internal/os/ProcessCpuTracker.java
new file mode 100644
index 0000000..e46dfc4
--- /dev/null
+++ b/com/android/internal/os/ProcessCpuTracker.java
@@ -0,0 +1,916 @@
+/*
+ * Copyright (C) 2007 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.internal.os;
+
+import static android.os.Process.*;
+
+import android.os.FileUtils;
+import android.os.Process;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.system.OsConstants;
+import android.util.Slog;
+
+import com.android.internal.util.FastPrintWriter;
+
+import libcore.io.IoUtils;
+import libcore.io.Libcore;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+
+public class ProcessCpuTracker {
+    private static final String TAG = "ProcessCpuTracker";
+    private static final boolean DEBUG = false;
+    private static final boolean localLOGV = DEBUG || false;
+
+    private static final int[] PROCESS_STATS_FORMAT = new int[] {
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_PARENS,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 10: minor faults
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 12: major faults
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 14: utime
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 15: stime
+    };
+
+    static final int PROCESS_STAT_MINOR_FAULTS = 0;
+    static final int PROCESS_STAT_MAJOR_FAULTS = 1;
+    static final int PROCESS_STAT_UTIME = 2;
+    static final int PROCESS_STAT_STIME = 3;
+
+    /** Stores user time and system time in jiffies. */
+    private final long[] mProcessStatsData = new long[4];
+
+    /** Stores user time and system time in jiffies.  Used for
+     * public API to retrieve CPU use for a process.  Must lock while in use. */
+    private final long[] mSinglePidStatsData = new long[4];
+
+    private static final int[] PROCESS_FULL_STATS_FORMAT = new int[] {
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_PARENS|PROC_OUT_STRING,    // 2: name
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 10: minor faults
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 12: major faults
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 14: utime
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 15: stime
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 23: vsize
+    };
+
+    static final int PROCESS_FULL_STAT_MINOR_FAULTS = 1;
+    static final int PROCESS_FULL_STAT_MAJOR_FAULTS = 2;
+    static final int PROCESS_FULL_STAT_UTIME = 3;
+    static final int PROCESS_FULL_STAT_STIME = 4;
+    static final int PROCESS_FULL_STAT_VSIZE = 5;
+
+    private final String[] mProcessFullStatsStringData = new String[6];
+    private final long[] mProcessFullStatsData = new long[6];
+
+    private static final int[] SYSTEM_CPU_FORMAT = new int[] {
+        PROC_SPACE_TERM|PROC_COMBINE,
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 1: user time
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 2: nice time
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 3: sys time
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 4: idle time
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 5: iowait time
+        PROC_SPACE_TERM|PROC_OUT_LONG,                  // 6: irq time
+        PROC_SPACE_TERM|PROC_OUT_LONG                   // 7: softirq time
+    };
+
+    private final long[] mSystemCpuData = new long[7];
+
+    private static final int[] LOAD_AVERAGE_FORMAT = new int[] {
+        PROC_SPACE_TERM|PROC_OUT_FLOAT,                 // 0: 1 min
+        PROC_SPACE_TERM|PROC_OUT_FLOAT,                 // 1: 5 mins
+        PROC_SPACE_TERM|PROC_OUT_FLOAT                  // 2: 15 mins
+    };
+
+    private final float[] mLoadAverageData = new float[3];
+
+    private final boolean mIncludeThreads;
+
+    // How long a CPU jiffy is in milliseconds.
+    private final long mJiffyMillis;
+
+    private float mLoad1 = 0;
+    private float mLoad5 = 0;
+    private float mLoad15 = 0;
+
+    // All times are in milliseconds. They are converted from jiffies to milliseconds
+    // when extracted from the kernel.
+    private long mCurrentSampleTime;
+    private long mLastSampleTime;
+
+    private long mCurrentSampleRealTime;
+    private long mLastSampleRealTime;
+
+    private long mCurrentSampleWallTime;
+    private long mLastSampleWallTime;
+
+    private long mBaseUserTime;
+    private long mBaseSystemTime;
+    private long mBaseIoWaitTime;
+    private long mBaseIrqTime;
+    private long mBaseSoftIrqTime;
+    private long mBaseIdleTime;
+    private int mRelUserTime;
+    private int mRelSystemTime;
+    private int mRelIoWaitTime;
+    private int mRelIrqTime;
+    private int mRelSoftIrqTime;
+    private int mRelIdleTime;
+    private boolean mRelStatsAreGood;
+
+    private int[] mCurPids;
+    private int[] mCurThreadPids;
+
+    private final ArrayList<Stats> mProcStats = new ArrayList<Stats>();
+    private final ArrayList<Stats> mWorkingProcs = new ArrayList<Stats>();
+    private boolean mWorkingProcsSorted;
+
+    private boolean mFirst = true;
+
+    private byte[] mBuffer = new byte[4096];
+
+    public interface FilterStats {
+        /** Which stats to pick when filtering */
+        boolean needed(Stats stats);
+    }
+
+    public static class Stats {
+        public final int pid;
+        public final int uid;
+        final String statFile;
+        final String cmdlineFile;
+        final String threadsDir;
+        final ArrayList<Stats> threadStats;
+        final ArrayList<Stats> workingThreads;
+
+        public BatteryStatsImpl.Uid.Proc batteryStats;
+
+        public boolean interesting;
+
+        public String baseName;
+        public String name;
+        public int nameWidth;
+
+        // vsize capture when process first detected; can be used to
+        // filter out kernel processes.
+        public long vsize;
+
+        /**
+         * Time in milliseconds.
+         */
+        public long base_uptime;
+
+        /**
+         * Time in milliseconds.
+         */
+        public long rel_uptime;
+
+        /**
+         * Time in milliseconds.
+         */
+        public long base_utime;
+
+        /**
+         * Time in milliseconds.
+         */
+        public long base_stime;
+
+        /**
+         * Time in milliseconds.
+         */
+        public int rel_utime;
+
+        /**
+         * Time in milliseconds.
+         */
+        public int rel_stime;
+
+        public long base_minfaults;
+        public long base_majfaults;
+        public int rel_minfaults;
+        public int rel_majfaults;
+
+        public boolean active;
+        public boolean working;
+        public boolean added;
+        public boolean removed;
+
+        Stats(int _pid, int parentPid, boolean includeThreads) {
+            pid = _pid;
+            if (parentPid < 0) {
+                final File procDir = new File("/proc", Integer.toString(pid));
+                statFile = new File(procDir, "stat").toString();
+                cmdlineFile = new File(procDir, "cmdline").toString();
+                threadsDir = (new File(procDir, "task")).toString();
+                if (includeThreads) {
+                    threadStats = new ArrayList<Stats>();
+                    workingThreads = new ArrayList<Stats>();
+                } else {
+                    threadStats = null;
+                    workingThreads = null;
+                }
+            } else {
+                final File procDir = new File("/proc", Integer.toString(
+                        parentPid));
+                final File taskDir = new File(
+                        new File(procDir, "task"), Integer.toString(pid));
+                statFile = new File(taskDir, "stat").toString();
+                cmdlineFile = null;
+                threadsDir = null;
+                threadStats = null;
+                workingThreads = null;
+            }
+            uid = FileUtils.getUid(statFile.toString());
+        }
+    }
+
+    private final static Comparator<Stats> sLoadComparator = new Comparator<Stats>() {
+        public final int
+        compare(Stats sta, Stats stb) {
+            int ta = sta.rel_utime + sta.rel_stime;
+            int tb = stb.rel_utime + stb.rel_stime;
+            if (ta != tb) {
+                return ta > tb ? -1 : 1;
+            }
+            if (sta.added != stb.added) {
+                return sta.added ? -1 : 1;
+            }
+            if (sta.removed != stb.removed) {
+                return sta.added ? -1 : 1;
+            }
+            return 0;
+        }
+    };
+
+
+    public ProcessCpuTracker(boolean includeThreads) {
+        mIncludeThreads = includeThreads;
+        long jiffyHz = Libcore.os.sysconf(OsConstants._SC_CLK_TCK);
+        mJiffyMillis = 1000/jiffyHz;
+    }
+
+    public void onLoadChanged(float load1, float load5, float load15) {
+    }
+
+    public int onMeasureProcessName(String name) {
+        return 0;
+    }
+
+    public void init() {
+        if (DEBUG) Slog.v(TAG, "Init: " + this);
+        mFirst = true;
+        update();
+    }
+
+    public void update() {
+        if (DEBUG) Slog.v(TAG, "Update: " + this);
+
+        final long nowUptime = SystemClock.uptimeMillis();
+        final long nowRealtime = SystemClock.elapsedRealtime();
+        final long nowWallTime = System.currentTimeMillis();
+
+        final long[] sysCpu = mSystemCpuData;
+        if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT,
+                null, sysCpu, null)) {
+            // Total user time is user + nice time.
+            final long usertime = (sysCpu[0]+sysCpu[1]) * mJiffyMillis;
+            // Total system time is simply system time.
+            final long systemtime = sysCpu[2] * mJiffyMillis;
+            // Total idle time is simply idle time.
+            final long idletime = sysCpu[3] * mJiffyMillis;
+            // Total irq time is iowait + irq + softirq time.
+            final long iowaittime = sysCpu[4] * mJiffyMillis;
+            final long irqtime = sysCpu[5] * mJiffyMillis;
+            final long softirqtime = sysCpu[6] * mJiffyMillis;
+
+            // This code is trying to avoid issues with idle time going backwards,
+            // but currently it gets into situations where it triggers most of the time. :(
+            if (true || (usertime >= mBaseUserTime && systemtime >= mBaseSystemTime
+                    && iowaittime >= mBaseIoWaitTime && irqtime >= mBaseIrqTime
+                    && softirqtime >= mBaseSoftIrqTime && idletime >= mBaseIdleTime)) {
+                mRelUserTime = (int)(usertime - mBaseUserTime);
+                mRelSystemTime = (int)(systemtime - mBaseSystemTime);
+                mRelIoWaitTime = (int)(iowaittime - mBaseIoWaitTime);
+                mRelIrqTime = (int)(irqtime - mBaseIrqTime);
+                mRelSoftIrqTime = (int)(softirqtime - mBaseSoftIrqTime);
+                mRelIdleTime = (int)(idletime - mBaseIdleTime);
+                mRelStatsAreGood = true;
+
+                if (DEBUG) {
+                    Slog.i("Load", "Total U:" + (sysCpu[0]*mJiffyMillis)
+                          + " N:" + (sysCpu[1]*mJiffyMillis)
+                          + " S:" + (sysCpu[2]*mJiffyMillis) + " I:" + (sysCpu[3]*mJiffyMillis)
+                          + " W:" + (sysCpu[4]*mJiffyMillis) + " Q:" + (sysCpu[5]*mJiffyMillis)
+                          + " O:" + (sysCpu[6]*mJiffyMillis));
+                    Slog.i("Load", "Rel U:" + mRelUserTime + " S:" + mRelSystemTime
+                          + " I:" + mRelIdleTime + " Q:" + mRelIrqTime);
+                }
+
+                mBaseUserTime = usertime;
+                mBaseSystemTime = systemtime;
+                mBaseIoWaitTime = iowaittime;
+                mBaseIrqTime = irqtime;
+                mBaseSoftIrqTime = softirqtime;
+                mBaseIdleTime = idletime;
+
+            } else {
+                mRelUserTime = 0;
+                mRelSystemTime = 0;
+                mRelIoWaitTime = 0;
+                mRelIrqTime = 0;
+                mRelSoftIrqTime = 0;
+                mRelIdleTime = 0;
+                mRelStatsAreGood = false;
+                Slog.w(TAG, "/proc/stats has gone backwards; skipping CPU update");
+                return;
+            }
+        }
+
+        mLastSampleTime = mCurrentSampleTime;
+        mCurrentSampleTime = nowUptime;
+        mLastSampleRealTime = mCurrentSampleRealTime;
+        mCurrentSampleRealTime = nowRealtime;
+        mLastSampleWallTime = mCurrentSampleWallTime;
+        mCurrentSampleWallTime = nowWallTime;
+
+        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+        try {
+            mCurPids = collectStats("/proc", -1, mFirst, mCurPids, mProcStats);
+        } finally {
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+
+        final float[] loadAverages = mLoadAverageData;
+        if (Process.readProcFile("/proc/loadavg", LOAD_AVERAGE_FORMAT,
+                null, null, loadAverages)) {
+            float load1 = loadAverages[0];
+            float load5 = loadAverages[1];
+            float load15 = loadAverages[2];
+            if (load1 != mLoad1 || load5 != mLoad5 || load15 != mLoad15) {
+                mLoad1 = load1;
+                mLoad5 = load5;
+                mLoad15 = load15;
+                onLoadChanged(load1, load5, load15);
+            }
+        }
+
+        if (DEBUG) Slog.i(TAG, "*** TIME TO COLLECT STATS: "
+                + (SystemClock.uptimeMillis()-mCurrentSampleTime));
+
+        mWorkingProcsSorted = false;
+        mFirst = false;
+    }
+
+    private int[] collectStats(String statsFile, int parentPid, boolean first,
+            int[] curPids, ArrayList<Stats> allProcs) {
+
+        int[] pids = Process.getPids(statsFile, curPids);
+        int NP = (pids == null) ? 0 : pids.length;
+        int NS = allProcs.size();
+        int curStatsIndex = 0;
+        for (int i=0; i<NP; i++) {
+            int pid = pids[i];
+            if (pid < 0) {
+                NP = pid;
+                break;
+            }
+            Stats st = curStatsIndex < NS ? allProcs.get(curStatsIndex) : null;
+
+            if (st != null && st.pid == pid) {
+                // Update an existing process...
+                st.added = false;
+                st.working = false;
+                curStatsIndex++;
+                if (DEBUG) Slog.v(TAG, "Existing "
+                        + (parentPid < 0 ? "process" : "thread")
+                        + " pid " + pid + ": " + st);
+
+                if (st.interesting) {
+                    final long uptime = SystemClock.uptimeMillis();
+
+                    final long[] procStats = mProcessStatsData;
+                    if (!Process.readProcFile(st.statFile.toString(),
+                            PROCESS_STATS_FORMAT, null, procStats, null)) {
+                        continue;
+                    }
+
+                    final long minfaults = procStats[PROCESS_STAT_MINOR_FAULTS];
+                    final long majfaults = procStats[PROCESS_STAT_MAJOR_FAULTS];
+                    final long utime = procStats[PROCESS_STAT_UTIME] * mJiffyMillis;
+                    final long stime = procStats[PROCESS_STAT_STIME] * mJiffyMillis;
+
+                    if (utime == st.base_utime && stime == st.base_stime) {
+                        st.rel_utime = 0;
+                        st.rel_stime = 0;
+                        st.rel_minfaults = 0;
+                        st.rel_majfaults = 0;
+                        if (st.active) {
+                            st.active = false;
+                        }
+                        continue;
+                    }
+
+                    if (!st.active) {
+                        st.active = true;
+                    }
+
+                    if (parentPid < 0) {
+                        getName(st, st.cmdlineFile);
+                        if (st.threadStats != null) {
+                            mCurThreadPids = collectStats(st.threadsDir, pid, false,
+                                    mCurThreadPids, st.threadStats);
+                        }
+                    }
+
+                    if (DEBUG) Slog.v("Load", "Stats changed " + st.name + " pid=" + st.pid
+                            + " utime=" + utime + "-" + st.base_utime
+                            + " stime=" + stime + "-" + st.base_stime
+                            + " minfaults=" + minfaults + "-" + st.base_minfaults
+                            + " majfaults=" + majfaults + "-" + st.base_majfaults);
+
+                    st.rel_uptime = uptime - st.base_uptime;
+                    st.base_uptime = uptime;
+                    st.rel_utime = (int)(utime - st.base_utime);
+                    st.rel_stime = (int)(stime - st.base_stime);
+                    st.base_utime = utime;
+                    st.base_stime = stime;
+                    st.rel_minfaults = (int)(minfaults - st.base_minfaults);
+                    st.rel_majfaults = (int)(majfaults - st.base_majfaults);
+                    st.base_minfaults = minfaults;
+                    st.base_majfaults = majfaults;
+                    st.working = true;
+                }
+
+                continue;
+            }
+
+            if (st == null || st.pid > pid) {
+                // We have a new process!
+                st = new Stats(pid, parentPid, mIncludeThreads);
+                allProcs.add(curStatsIndex, st);
+                curStatsIndex++;
+                NS++;
+                if (DEBUG) Slog.v(TAG, "New "
+                        + (parentPid < 0 ? "process" : "thread")
+                        + " pid " + pid + ": " + st);
+
+                final String[] procStatsString = mProcessFullStatsStringData;
+                final long[] procStats = mProcessFullStatsData;
+                st.base_uptime = SystemClock.uptimeMillis();
+                String path = st.statFile.toString();
+                //Slog.d(TAG, "Reading proc file: " + path);
+                if (Process.readProcFile(path, PROCESS_FULL_STATS_FORMAT, procStatsString,
+                        procStats, null)) {
+                    // This is a possible way to filter out processes that
+                    // are actually kernel threads...  do we want to?  Some
+                    // of them do use CPU, but there can be a *lot* that are
+                    // not doing anything.
+                    st.vsize = procStats[PROCESS_FULL_STAT_VSIZE];
+                    if (true || procStats[PROCESS_FULL_STAT_VSIZE] != 0) {
+                        st.interesting = true;
+                        st.baseName = procStatsString[0];
+                        st.base_minfaults = procStats[PROCESS_FULL_STAT_MINOR_FAULTS];
+                        st.base_majfaults = procStats[PROCESS_FULL_STAT_MAJOR_FAULTS];
+                        st.base_utime = procStats[PROCESS_FULL_STAT_UTIME] * mJiffyMillis;
+                        st.base_stime = procStats[PROCESS_FULL_STAT_STIME] * mJiffyMillis;
+                    } else {
+                        Slog.i(TAG, "Skipping kernel process pid " + pid
+                                + " name " + procStatsString[0]);
+                        st.baseName = procStatsString[0];
+                    }
+                } else {
+                    Slog.w(TAG, "Skipping unknown process pid " + pid);
+                    st.baseName = "<unknown>";
+                    st.base_utime = st.base_stime = 0;
+                    st.base_minfaults = st.base_majfaults = 0;
+                }
+
+                if (parentPid < 0) {
+                    getName(st, st.cmdlineFile);
+                    if (st.threadStats != null) {
+                        mCurThreadPids = collectStats(st.threadsDir, pid, true,
+                                mCurThreadPids, st.threadStats);
+                    }
+                } else if (st.interesting) {
+                    st.name = st.baseName;
+                    st.nameWidth = onMeasureProcessName(st.name);
+                }
+
+                if (DEBUG) Slog.v("Load", "Stats added " + st.name + " pid=" + st.pid
+                        + " utime=" + st.base_utime + " stime=" + st.base_stime
+                        + " minfaults=" + st.base_minfaults + " majfaults=" + st.base_majfaults);
+
+                st.rel_utime = 0;
+                st.rel_stime = 0;
+                st.rel_minfaults = 0;
+                st.rel_majfaults = 0;
+                st.added = true;
+                if (!first && st.interesting) {
+                    st.working = true;
+                }
+                continue;
+            }
+
+            // This process has gone away!
+            st.rel_utime = 0;
+            st.rel_stime = 0;
+            st.rel_minfaults = 0;
+            st.rel_majfaults = 0;
+            st.removed = true;
+            st.working = true;
+            allProcs.remove(curStatsIndex);
+            NS--;
+            if (DEBUG) Slog.v(TAG, "Removed "
+                    + (parentPid < 0 ? "process" : "thread")
+                    + " pid " + pid + ": " + st);
+            // Decrement the loop counter so that we process the current pid
+            // again the next time through the loop.
+            i--;
+            continue;
+        }
+
+        while (curStatsIndex < NS) {
+            // This process has gone away!
+            final Stats st = allProcs.get(curStatsIndex);
+            st.rel_utime = 0;
+            st.rel_stime = 0;
+            st.rel_minfaults = 0;
+            st.rel_majfaults = 0;
+            st.removed = true;
+            st.working = true;
+            allProcs.remove(curStatsIndex);
+            NS--;
+            if (localLOGV) Slog.v(TAG, "Removed pid " + st.pid + ": " + st);
+        }
+
+        return pids;
+    }
+
+    /**
+     * Returns the total time (in milliseconds) spent executing in
+     * both user and system code.  Safe to call without lock held.
+     */
+    public long getCpuTimeForPid(int pid) {
+        synchronized (mSinglePidStatsData) {
+            final String statFile = "/proc/" + pid + "/stat";
+            final long[] statsData = mSinglePidStatsData;
+            if (Process.readProcFile(statFile, PROCESS_STATS_FORMAT,
+                    null, statsData, null)) {
+                long time = statsData[PROCESS_STAT_UTIME]
+                        + statsData[PROCESS_STAT_STIME];
+                return time * mJiffyMillis;
+            }
+            return 0;
+        }
+    }
+
+    /**
+     * @return time in milliseconds.
+     */
+    final public int getLastUserTime() {
+        return mRelUserTime;
+    }
+
+    /**
+     * @return time in milliseconds.
+     */
+    final public int getLastSystemTime() {
+        return mRelSystemTime;
+    }
+
+    /**
+     * @return time in milliseconds.
+     */
+    final public int getLastIoWaitTime() {
+        return mRelIoWaitTime;
+    }
+
+    /**
+     * @return time in milliseconds.
+     */
+    final public int getLastIrqTime() {
+        return mRelIrqTime;
+    }
+
+    /**
+     * @return time in milliseconds.
+     */
+    final public int getLastSoftIrqTime() {
+        return mRelSoftIrqTime;
+    }
+
+    /**
+     * @return time in milliseconds.
+     */
+    final public int getLastIdleTime() {
+        return mRelIdleTime;
+    }
+
+    final public boolean hasGoodLastStats() {
+        return mRelStatsAreGood;
+    }
+
+    final public float getTotalCpuPercent() {
+        int denom = mRelUserTime+mRelSystemTime+mRelIrqTime+mRelIdleTime;
+        if (denom <= 0) {
+            return 0;
+        }
+        return ((float)(mRelUserTime+mRelSystemTime+mRelIrqTime)*100) / denom;
+    }
+
+    final void buildWorkingProcs() {
+        if (!mWorkingProcsSorted) {
+            mWorkingProcs.clear();
+            final int N = mProcStats.size();
+            for (int i=0; i<N; i++) {
+                Stats stats = mProcStats.get(i);
+                if (stats.working) {
+                    mWorkingProcs.add(stats);
+                    if (stats.threadStats != null && stats.threadStats.size() > 1) {
+                        stats.workingThreads.clear();
+                        final int M = stats.threadStats.size();
+                        for (int j=0; j<M; j++) {
+                            Stats tstats = stats.threadStats.get(j);
+                            if (tstats.working) {
+                                stats.workingThreads.add(tstats);
+                            }
+                        }
+                        Collections.sort(stats.workingThreads, sLoadComparator);
+                    }
+                }
+            }
+            Collections.sort(mWorkingProcs, sLoadComparator);
+            mWorkingProcsSorted = true;
+        }
+    }
+
+    final public int countStats() {
+        return mProcStats.size();
+    }
+
+    final public Stats getStats(int index) {
+        return mProcStats.get(index);
+    }
+
+    final public List<Stats> getStats(FilterStats filter) {
+        final ArrayList<Stats> statses = new ArrayList<>(mProcStats.size());
+        final int N = mProcStats.size();
+        for (int p = 0; p < N; p++) {
+            Stats stats = mProcStats.get(p);
+            if (filter.needed(stats)) {
+                statses.add(stats);
+            }
+        }
+        return statses;
+    }
+
+    final public int countWorkingStats() {
+        buildWorkingProcs();
+        return mWorkingProcs.size();
+    }
+
+    final public Stats getWorkingStats(int index) {
+        return mWorkingProcs.get(index);
+    }
+
+    final public String printCurrentLoad() {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new FastPrintWriter(sw, false, 128);
+        pw.print("Load: ");
+        pw.print(mLoad1);
+        pw.print(" / ");
+        pw.print(mLoad5);
+        pw.print(" / ");
+        pw.println(mLoad15);
+        pw.flush();
+        return sw.toString();
+    }
+
+    final public String printCurrentState(long now) {
+        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+        buildWorkingProcs();
+
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new FastPrintWriter(sw, false, 1024);
+
+        pw.print("CPU usage from ");
+        if (now > mLastSampleTime) {
+            pw.print(now-mLastSampleTime);
+            pw.print("ms to ");
+            pw.print(now-mCurrentSampleTime);
+            pw.print("ms ago");
+        } else {
+            pw.print(mLastSampleTime-now);
+            pw.print("ms to ");
+            pw.print(mCurrentSampleTime-now);
+            pw.print("ms later");
+        }
+        pw.print(" (");
+        pw.print(sdf.format(new Date(mLastSampleWallTime)));
+        pw.print(" to ");
+        pw.print(sdf.format(new Date(mCurrentSampleWallTime)));
+        pw.print(")");
+
+        long sampleTime = mCurrentSampleTime - mLastSampleTime;
+        long sampleRealTime = mCurrentSampleRealTime - mLastSampleRealTime;
+        long percAwake = sampleRealTime > 0 ? ((sampleTime*100) / sampleRealTime) : 0;
+        if (percAwake != 100) {
+            pw.print(" with ");
+            pw.print(percAwake);
+            pw.print("% awake");
+        }
+        pw.println(":");
+
+        final int totalTime = mRelUserTime + mRelSystemTime + mRelIoWaitTime
+                + mRelIrqTime + mRelSoftIrqTime + mRelIdleTime;
+
+        if (DEBUG) Slog.i(TAG, "totalTime " + totalTime + " over sample time "
+                + (mCurrentSampleTime-mLastSampleTime));
+
+        int N = mWorkingProcs.size();
+        for (int i=0; i<N; i++) {
+            Stats st = mWorkingProcs.get(i);
+            printProcessCPU(pw, st.added ? " +" : (st.removed ? " -": "  "),
+                    st.pid, st.name, (int)st.rel_uptime,
+                    st.rel_utime, st.rel_stime, 0, 0, 0, st.rel_minfaults, st.rel_majfaults);
+            if (!st.removed && st.workingThreads != null) {
+                int M = st.workingThreads.size();
+                for (int j=0; j<M; j++) {
+                    Stats tst = st.workingThreads.get(j);
+                    printProcessCPU(pw,
+                            tst.added ? "   +" : (tst.removed ? "   -": "    "),
+                            tst.pid, tst.name, (int)st.rel_uptime,
+                            tst.rel_utime, tst.rel_stime, 0, 0, 0, 0, 0);
+                }
+            }
+        }
+
+        printProcessCPU(pw, "", -1, "TOTAL", totalTime, mRelUserTime, mRelSystemTime,
+                mRelIoWaitTime, mRelIrqTime, mRelSoftIrqTime, 0, 0);
+
+        pw.flush();
+        return sw.toString();
+    }
+
+    private void printRatio(PrintWriter pw, long numerator, long denominator) {
+        long thousands = (numerator*1000)/denominator;
+        long hundreds = thousands/10;
+        pw.print(hundreds);
+        if (hundreds < 10) {
+            long remainder = thousands - (hundreds*10);
+            if (remainder != 0) {
+                pw.print('.');
+                pw.print(remainder);
+            }
+        }
+    }
+
+    private void printProcessCPU(PrintWriter pw, String prefix, int pid, String label,
+            int totalTime, int user, int system, int iowait, int irq, int softIrq,
+            int minFaults, int majFaults) {
+        pw.print(prefix);
+        if (totalTime == 0) totalTime = 1;
+        printRatio(pw, user+system+iowait+irq+softIrq, totalTime);
+        pw.print("% ");
+        if (pid >= 0) {
+            pw.print(pid);
+            pw.print("/");
+        }
+        pw.print(label);
+        pw.print(": ");
+        printRatio(pw, user, totalTime);
+        pw.print("% user + ");
+        printRatio(pw, system, totalTime);
+        pw.print("% kernel");
+        if (iowait > 0) {
+            pw.print(" + ");
+            printRatio(pw, iowait, totalTime);
+            pw.print("% iowait");
+        }
+        if (irq > 0) {
+            pw.print(" + ");
+            printRatio(pw, irq, totalTime);
+            pw.print("% irq");
+        }
+        if (softIrq > 0) {
+            pw.print(" + ");
+            printRatio(pw, softIrq, totalTime);
+            pw.print("% softirq");
+        }
+        if (minFaults > 0 || majFaults > 0) {
+            pw.print(" / faults:");
+            if (minFaults > 0) {
+                pw.print(" ");
+                pw.print(minFaults);
+                pw.print(" minor");
+            }
+            if (majFaults > 0) {
+                pw.print(" ");
+                pw.print(majFaults);
+                pw.print(" major");
+            }
+        }
+        pw.println();
+    }
+
+    private String readFile(String file, char endChar) {
+        // Permit disk reads here, as /proc/meminfo isn't really "on
+        // disk" and should be fast.  TODO: make BlockGuard ignore
+        // /proc/ and /sys/ files perhaps?
+        StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+        FileInputStream is = null;
+        try {
+            is = new FileInputStream(file);
+            int len = is.read(mBuffer);
+            is.close();
+
+            if (len > 0) {
+                int i;
+                for (i=0; i<len; i++) {
+                    if (mBuffer[i] == endChar) {
+                        break;
+                    }
+                }
+                return new String(mBuffer, 0, i);
+            }
+        } catch (java.io.FileNotFoundException e) {
+        } catch (java.io.IOException e) {
+        } finally {
+            IoUtils.closeQuietly(is);
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+        return null;
+    }
+
+    private void getName(Stats st, String cmdlineFile) {
+        String newName = st.name;
+        if (st.name == null || st.name.equals("app_process")
+                || st.name.equals("<pre-initialized>")) {
+            String cmdName = readFile(cmdlineFile, '\0');
+            if (cmdName != null && cmdName.length() > 1) {
+                newName = cmdName;
+                int i = newName.lastIndexOf("/");
+                if (i > 0 && i < newName.length()-1) {
+                    newName = newName.substring(i+1);
+                }
+            }
+            if (newName == null) {
+                newName = st.baseName;
+            }
+        }
+        if (st.name == null || !newName.equals(st.name)) {
+            st.name = newName;
+            st.nameWidth = onMeasureProcessName(st.name);
+        }
+    }
+}
diff --git a/com/android/internal/os/RoSystemProperties.java b/com/android/internal/os/RoSystemProperties.java
new file mode 100644
index 0000000..89a4e17
--- /dev/null
+++ b/com/android/internal/os/RoSystemProperties.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.internal.os;
+
+import android.os.SystemProperties;
+
+/**
+ * This is a cache of various ro.* properties so that they can be read just once
+ * at class init time.
+ */
+public class RoSystemProperties {
+    public static final boolean DEBUGGABLE =
+            SystemProperties.getInt("ro.debuggable", 0) == 1;
+    public static final int FACTORYTEST =
+            SystemProperties.getInt("ro.factorytest", 0);
+    public static final String CONTROL_PRIVAPP_PERMISSIONS =
+            SystemProperties.get("ro.control_privapp_permissions");
+
+    // ------ ro.config.* -------- //
+    public static final boolean CONFIG_LOW_RAM =
+            SystemProperties.getBoolean("ro.config.low_ram", false);
+    public static final boolean CONFIG_SMALL_BATTERY =
+            SystemProperties.getBoolean("ro.config.small_battery", false);
+
+    // ------ ro.fw.* ------------ //
+    public static final boolean FW_SYSTEM_USER_SPLIT =
+            SystemProperties.getBoolean("ro.fw.system_user_split", false);
+
+    // ------ ro.crypto.* -------- //
+    public static final String CRYPTO_STATE = SystemProperties.get("ro.crypto.state");
+    public static final String CRYPTO_TYPE = SystemProperties.get("ro.crypto.type");
+    // These are pseudo-properties
+    public static final boolean CRYPTO_ENCRYPTABLE =
+            !CRYPTO_STATE.isEmpty() && !"unsupported".equals(CRYPTO_STATE);
+    public static final boolean CRYPTO_ENCRYPTED =
+            "encrypted".equalsIgnoreCase(CRYPTO_STATE);
+    public static final boolean CRYPTO_FILE_ENCRYPTED =
+            "file".equalsIgnoreCase(CRYPTO_TYPE);
+    public static final boolean CRYPTO_BLOCK_ENCRYPTED =
+            "block".equalsIgnoreCase(CRYPTO_TYPE);
+
+    public static final boolean CONTROL_PRIVAPP_PERMISSIONS_LOG =
+            "log".equalsIgnoreCase(CONTROL_PRIVAPP_PERMISSIONS);
+    public static final boolean CONTROL_PRIVAPP_PERMISSIONS_ENFORCE =
+            "enforce".equalsIgnoreCase(CONTROL_PRIVAPP_PERMISSIONS);
+    public static final boolean CONTROL_PRIVAPP_PERMISSIONS_DISABLE =
+            !CONTROL_PRIVAPP_PERMISSIONS_LOG && !CONTROL_PRIVAPP_PERMISSIONS_ENFORCE;
+
+}
diff --git a/com/android/internal/os/RuntimeInit.java b/com/android/internal/os/RuntimeInit.java
new file mode 100644
index 0000000..66475e4
--- /dev/null
+++ b/com/android/internal/os/RuntimeInit.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2006 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.internal.os;
+
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.ApplicationErrorReport;
+import android.os.Build;
+import android.os.DeadObjectException;
+import android.os.Debug;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Slog;
+import com.android.internal.logging.AndroidConfig;
+import com.android.server.NetworkManagementSocketTagger;
+import dalvik.system.VMRuntime;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.TimeZone;
+import java.util.logging.LogManager;
+import org.apache.harmony.luni.internal.util.TimezoneGetter;
+
+/**
+ * Main entry point for runtime initialization.  Not for
+ * public consumption.
+ * @hide
+ */
+public class RuntimeInit {
+    final static String TAG = "AndroidRuntime";
+    final static boolean DEBUG = false;
+
+    /** true if commonInit() has been called */
+    private static boolean initialized;
+
+    private static IBinder mApplicationObject;
+
+    private static volatile boolean mCrashing = false;
+
+    private static final native void nativeFinishInit();
+    private static final native void nativeSetExitWithoutCleanup(boolean exitWithoutCleanup);
+
+    private static int Clog_e(String tag, String msg, Throwable tr) {
+        return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
+    }
+
+    /**
+     * Logs a message when a thread encounters an uncaught exception. By
+     * default, {@link KillApplicationHandler} will terminate this process later,
+     * but apps can override that behavior.
+     */
+    private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
+        @Override
+        public void uncaughtException(Thread t, Throwable e) {
+            // Don't re-enter if KillApplicationHandler has already run
+            if (mCrashing) return;
+            if (mApplicationObject == null) {
+                // The "FATAL EXCEPTION" string is still used on Android even though
+                // apps can set a custom UncaughtExceptionHandler that renders uncaught
+                // exceptions non-fatal.
+                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
+            } else {
+                StringBuilder message = new StringBuilder();
+                // The "FATAL EXCEPTION" string is still used on Android even though
+                // apps can set a custom UncaughtExceptionHandler that renders uncaught
+                // exceptions non-fatal.
+                message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
+                final String processName = ActivityThread.currentProcessName();
+                if (processName != null) {
+                    message.append("Process: ").append(processName).append(", ");
+                }
+                message.append("PID: ").append(Process.myPid());
+                Clog_e(TAG, message.toString(), e);
+            }
+        }
+    }
+
+    /**
+     * Handle application death from an uncaught exception.  The framework
+     * catches these for the main threads, so this should only matter for
+     * threads created by applications.  Before this method runs,
+     * {@link LoggingHandler} will already have logged details.
+     */
+    private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
+        public void uncaughtException(Thread t, Throwable e) {
+            try {
+                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
+                if (mCrashing) return;
+                mCrashing = true;
+
+                // Try to end profiling. If a profiler is running at this point, and we kill the
+                // process (below), the in-memory buffer will be lost. So try to stop, which will
+                // flush the buffer. (This makes method trace profiling useful to debug crashes.)
+                if (ActivityThread.currentActivityThread() != null) {
+                    ActivityThread.currentActivityThread().stopProfiling();
+                }
+
+                // Bring up crash dialog, wait for it to be dismissed
+                ActivityManager.getService().handleApplicationCrash(
+                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
+            } catch (Throwable t2) {
+                if (t2 instanceof DeadObjectException) {
+                    // System process is dead; ignore
+                } else {
+                    try {
+                        Clog_e(TAG, "Error reporting crash", t2);
+                    } catch (Throwable t3) {
+                        // Even Clog_e() fails!  Oh well.
+                    }
+                }
+            } finally {
+                // Try everything to make sure this process goes away.
+                Process.killProcess(Process.myPid());
+                System.exit(10);
+            }
+        }
+    }
+
+    protected static final void commonInit() {
+        if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
+
+        /*
+         * set handlers; these apply to all threads in the VM. Apps can replace
+         * the default handler, but not the pre handler.
+         */
+        Thread.setUncaughtExceptionPreHandler(new LoggingHandler());
+        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler());
+
+        /*
+         * Install a TimezoneGetter subclass for ZoneInfo.db
+         */
+        TimezoneGetter.setInstance(new TimezoneGetter() {
+            @Override
+            public String getId() {
+                return SystemProperties.get("persist.sys.timezone");
+            }
+        });
+        TimeZone.setDefault(null);
+
+        /*
+         * Sets handler for java.util.logging to use Android log facilities.
+         * The odd "new instance-and-then-throw-away" is a mirror of how
+         * the "java.util.logging.config.class" system property works. We
+         * can't use the system property here since the logger has almost
+         * certainly already been initialized.
+         */
+        LogManager.getLogManager().reset();
+        new AndroidConfig();
+
+        /*
+         * Sets the default HTTP User-Agent used by HttpURLConnection.
+         */
+        String userAgent = getDefaultUserAgent();
+        System.setProperty("http.agent", userAgent);
+
+        /*
+         * Wire socket tagging to traffic stats.
+         */
+        NetworkManagementSocketTagger.install();
+
+        /*
+         * If we're running in an emulator launched with "-trace", put the
+         * VM into emulator trace profiling mode so that the user can hit
+         * F9/F10 at any time to capture traces.  This has performance
+         * consequences, so it's not something you want to do always.
+         */
+        String trace = SystemProperties.get("ro.kernel.android.tracing");
+        if (trace.equals("1")) {
+            Slog.i(TAG, "NOTE: emulator trace profiling enabled");
+            Debug.enableEmulatorTraceOutput();
+        }
+
+        initialized = true;
+    }
+
+    /**
+     * Returns an HTTP user agent of the form
+     * "Dalvik/1.1.0 (Linux; U; Android Eclair Build/MASTER)".
+     */
+    private static String getDefaultUserAgent() {
+        StringBuilder result = new StringBuilder(64);
+        result.append("Dalvik/");
+        result.append(System.getProperty("java.vm.version")); // such as 1.1.0
+        result.append(" (Linux; U; Android ");
+
+        String version = Build.VERSION.RELEASE; // "1.0" or "3.4b5"
+        result.append(version.length() > 0 ? version : "1.0");
+
+        // add the model for the release build
+        if ("REL".equals(Build.VERSION.CODENAME)) {
+            String model = Build.MODEL;
+            if (model.length() > 0) {
+                result.append("; ");
+                result.append(model);
+            }
+        }
+        String id = Build.ID; // "MASTER" or "M4-rc20"
+        if (id.length() > 0) {
+            result.append(" Build/");
+            result.append(id);
+        }
+        result.append(")");
+        return result.toString();
+    }
+
+    /**
+     * Invokes a static "main(argv[]) method on class "className".
+     * Converts various failing exceptions into RuntimeExceptions, with
+     * the assumption that they will then cause the VM instance to exit.
+     *
+     * @param className Fully-qualified class name
+     * @param argv Argument vector for main()
+     * @param classLoader the classLoader to load {@className} with
+     */
+    private static Runnable findStaticMain(String className, String[] argv,
+            ClassLoader classLoader) {
+        Class<?> cl;
+
+        try {
+            cl = Class.forName(className, true, classLoader);
+        } catch (ClassNotFoundException ex) {
+            throw new RuntimeException(
+                    "Missing class when invoking static main " + className,
+                    ex);
+        }
+
+        Method m;
+        try {
+            m = cl.getMethod("main", new Class[] { String[].class });
+        } catch (NoSuchMethodException ex) {
+            throw new RuntimeException(
+                    "Missing static main on " + className, ex);
+        } catch (SecurityException ex) {
+            throw new RuntimeException(
+                    "Problem getting static main on " + className, ex);
+        }
+
+        int modifiers = m.getModifiers();
+        if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
+            throw new RuntimeException(
+                    "Main method is not public and static on " + className);
+        }
+
+        /*
+         * This throw gets caught in ZygoteInit.main(), which responds
+         * by invoking the exception's run() method. This arrangement
+         * clears up all the stack frames that were required in setting
+         * up the process.
+         */
+        return new MethodAndArgsCaller(m, argv);
+    }
+
+    public static final void main(String[] argv) {
+        enableDdms();
+        if (argv.length == 2 && argv[1].equals("application")) {
+            if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");
+            redirectLogStreams();
+        } else {
+            if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool");
+        }
+
+        commonInit();
+
+        /*
+         * Now that we're running in interpreted code, call back into native code
+         * to run the system.
+         */
+        nativeFinishInit();
+
+        if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!");
+    }
+
+    protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
+            ClassLoader classLoader) {
+        // If the application calls System.exit(), terminate the process
+        // immediately without running any shutdown hooks.  It is not possible to
+        // shutdown an Android application gracefully.  Among other things, the
+        // Android runtime shutdown hooks close the Binder driver, which can cause
+        // leftover running threads to crash before the process actually exits.
+        nativeSetExitWithoutCleanup(true);
+
+        // We want to be fairly aggressive about heap utilization, to avoid
+        // holding on to a lot of memory that isn't needed.
+        VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
+        VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);
+
+        final Arguments args = new Arguments(argv);
+
+        // The end of of the RuntimeInit event (see #zygoteInit).
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+        // Remaining arguments are passed to the start class's static main
+        return findStaticMain(args.startClass, args.startArgs, classLoader);
+    }
+
+    /**
+     * Redirect System.out and System.err to the Android log.
+     */
+    public static void redirectLogStreams() {
+        System.out.close();
+        System.setOut(new AndroidPrintStream(Log.INFO, "System.out"));
+        System.err.close();
+        System.setErr(new AndroidPrintStream(Log.WARN, "System.err"));
+    }
+
+    /**
+     * Report a serious error in the current process.  May or may not cause
+     * the process to terminate (depends on system settings).
+     *
+     * @param tag to record with the error
+     * @param t exception describing the error site and conditions
+     */
+    public static void wtf(String tag, Throwable t, boolean system) {
+        try {
+            if (ActivityManager.getService().handleApplicationWtf(
+                    mApplicationObject, tag, system,
+                    new ApplicationErrorReport.ParcelableCrashInfo(t))) {
+                // The Activity Manager has already written us off -- now exit.
+                Process.killProcess(Process.myPid());
+                System.exit(10);
+            }
+        } catch (Throwable t2) {
+            if (t2 instanceof DeadObjectException) {
+                // System process is dead; ignore
+            } else {
+                Slog.e(TAG, "Error reporting WTF", t2);
+                Slog.e(TAG, "Original WTF:", t);
+            }
+        }
+    }
+
+    /**
+     * Set the object identifying this application/process, for reporting VM
+     * errors.
+     */
+    public static final void setApplicationObject(IBinder app) {
+        mApplicationObject = app;
+    }
+
+    public static final IBinder getApplicationObject() {
+        return mApplicationObject;
+    }
+
+    /**
+     * Enable DDMS.
+     */
+    static final void enableDdms() {
+        // Register handlers for DDM messages.
+        android.ddm.DdmRegister.registerHandlers();
+    }
+
+    /**
+     * Handles argument parsing for args related to the runtime.
+     *
+     * Current recognized args:
+     * <ul>
+     *   <li> <code> [--] &lt;start class name&gt;  &lt;args&gt;
+     * </ul>
+     */
+    static class Arguments {
+        /** first non-option argument */
+        String startClass;
+
+        /** all following arguments */
+        String[] startArgs;
+
+        /**
+         * Constructs instance and parses args
+         * @param args runtime command-line args
+         * @throws IllegalArgumentException
+         */
+        Arguments(String args[]) throws IllegalArgumentException {
+            parseArgs(args);
+        }
+
+        /**
+         * Parses the commandline arguments intended for the Runtime.
+         */
+        private void parseArgs(String args[])
+                throws IllegalArgumentException {
+            int curArg = 0;
+            for (; curArg < args.length; curArg++) {
+                String arg = args[curArg];
+
+                if (arg.equals("--")) {
+                    curArg++;
+                    break;
+                } else if (!arg.startsWith("--")) {
+                    break;
+                }
+            }
+
+            if (curArg == args.length) {
+                throw new IllegalArgumentException("Missing classname argument to RuntimeInit!");
+            }
+
+            startClass = args[curArg++];
+            startArgs = new String[args.length - curArg];
+            System.arraycopy(args, curArg, startArgs, 0, startArgs.length);
+        }
+    }
+
+    /**
+     * Helper class which holds a method and arguments and can call them. This is used as part of
+     * a trampoline to get rid of the initial process setup stack frames.
+     */
+    static class MethodAndArgsCaller implements Runnable {
+        /** method to call */
+        private final Method mMethod;
+
+        /** argument array */
+        private final String[] mArgs;
+
+        public MethodAndArgsCaller(Method method, String[] args) {
+            mMethod = method;
+            mArgs = args;
+        }
+
+        public void run() {
+            try {
+                mMethod.invoke(null, new Object[] { mArgs });
+            } catch (IllegalAccessException ex) {
+                throw new RuntimeException(ex);
+            } catch (InvocationTargetException ex) {
+                Throwable cause = ex.getCause();
+                if (cause instanceof RuntimeException) {
+                    throw (RuntimeException) cause;
+                } else if (cause instanceof Error) {
+                    throw (Error) cause;
+                }
+                throw new RuntimeException(ex);
+            }
+        }
+    }
+}
diff --git a/com/android/internal/os/SensorPowerCalculator.java b/com/android/internal/os/SensorPowerCalculator.java
new file mode 100644
index 0000000..c98639b
--- /dev/null
+++ b/com/android/internal/os/SensorPowerCalculator.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.os.BatteryStats;
+import android.util.SparseArray;
+
+import java.util.List;
+
+public class SensorPowerCalculator extends PowerCalculator {
+    private final List<Sensor> mSensors;
+    private final double mGpsPowerOn;
+
+    public SensorPowerCalculator(PowerProfile profile, SensorManager sensorManager) {
+        mSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
+        mGpsPowerOn = profile.getAveragePower(PowerProfile.POWER_GPS_ON);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+        // Process Sensor usage
+        final SparseArray<? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats();
+        final int NSE = sensorStats.size();
+        for (int ise = 0; ise < NSE; ise++) {
+            final BatteryStats.Uid.Sensor sensor = sensorStats.valueAt(ise);
+            final int sensorHandle = sensorStats.keyAt(ise);
+            final BatteryStats.Timer timer = sensor.getSensorTime();
+            final long sensorTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000;
+            switch (sensorHandle) {
+                case BatteryStats.Uid.Sensor.GPS:
+                    app.gpsTimeMs = sensorTime;
+                    app.gpsPowerMah = (app.gpsTimeMs * mGpsPowerOn) / (1000*60*60);
+                    break;
+                default:
+                    final int sensorsCount = mSensors.size();
+                    for (int i = 0; i < sensorsCount; i++) {
+                        final Sensor s = mSensors.get(i);
+                        if (s.getHandle() == sensorHandle) {
+                            app.sensorPowerMah += (sensorTime * s.getPower()) / (1000*60*60);
+                            break;
+                        }
+                    }
+                    break;
+            }
+        }
+    }
+}
diff --git a/com/android/internal/os/SomeArgs.java b/com/android/internal/os/SomeArgs.java
new file mode 100644
index 0000000..8fb56d4
--- /dev/null
+++ b/com/android/internal/os/SomeArgs.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 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.internal.os;
+
+/**
+ * Helper class for passing more arguments though a message
+ * and avoiding allocation of a custom class for wrapping the
+ * arguments. This class maintains a pool of instances and
+ * it is responsibility of the client to recycle and instance
+ * once it is no longer used.
+ */
+public final class SomeArgs {
+
+    private static final int MAX_POOL_SIZE = 10;
+
+    private static SomeArgs sPool;
+    private static int sPoolSize;
+    private static Object sPoolLock = new Object();
+
+    private SomeArgs mNext;
+
+    private boolean mInPool;
+
+    static final int WAIT_NONE = 0;
+    static final int WAIT_WAITING = 1;
+    static final int WAIT_FINISHED = 2;
+    int mWaitState = WAIT_NONE;
+
+    public Object arg1;
+    public Object arg2;
+    public Object arg3;
+    public Object arg4;
+    public Object arg5;
+    public Object arg6;
+    public Object arg7;
+    public Object arg8;
+    public int argi1;
+    public int argi2;
+    public int argi3;
+    public int argi4;
+    public int argi5;
+    public int argi6;
+
+    private SomeArgs() {
+        /* do nothing - reduce visibility */
+    }
+
+    public static SomeArgs obtain() {
+        synchronized (sPoolLock) {
+            if (sPoolSize > 0) {
+                SomeArgs args = sPool;
+                sPool = sPool.mNext;
+                args.mNext = null;
+                args.mInPool = false;
+                sPoolSize--;
+                return args;
+            } else {
+                return new SomeArgs();
+            }
+        }
+    }
+
+    public void complete() {
+        synchronized (this) {
+            if (mWaitState != WAIT_WAITING) {
+                throw new IllegalStateException("Not waiting");
+            }
+            mWaitState = WAIT_FINISHED;
+            notifyAll();
+        }
+    }
+
+    public void recycle() {
+        if (mInPool) {
+            throw new IllegalStateException("Already recycled.");
+        }
+        if (mWaitState != WAIT_NONE) {
+            return;
+        }
+        synchronized (sPoolLock) {
+            clear();
+            if (sPoolSize < MAX_POOL_SIZE) {
+                mNext = sPool;
+                mInPool = true;
+                sPool = this;
+                sPoolSize++;
+            }
+        }
+    }
+
+    private void clear() {
+        arg1 = null;
+        arg2 = null;
+        arg3 = null;
+        arg4 = null;
+        arg5 = null;
+        arg6 = null;
+        arg7 = null;
+        argi1 = 0;
+        argi2 = 0;
+        argi3 = 0;
+        argi4 = 0;
+        argi5 = 0;
+        argi6 = 0;
+    }
+}
diff --git a/com/android/internal/os/TransferPipe.java b/com/android/internal/os/TransferPipe.java
new file mode 100644
index 0000000..738ecc0
--- /dev/null
+++ b/com/android/internal/os/TransferPipe.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2011 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.internal.os;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Slog;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Helper for transferring data through a pipe from a client app.
+ */
+public final class TransferPipe implements Runnable, Closeable {
+    static final String TAG = "TransferPipe";
+    static final boolean DEBUG = false;
+
+    static final long DEFAULT_TIMEOUT = 5000;  // 5 seconds
+
+    final Thread mThread;
+    final ParcelFileDescriptor[] mFds;
+
+    FileDescriptor mOutFd;
+    long mEndTime;
+    String mFailure;
+    boolean mComplete;
+
+    String mBufferPrefix;
+
+    interface Caller {
+        void go(IInterface iface, FileDescriptor fd, String prefix,
+                String[] args) throws RemoteException;
+    }
+
+    public TransferPipe() throws IOException {
+        this(null);
+    }
+
+    public TransferPipe(String bufferPrefix) throws IOException {
+        mThread = new Thread(this, "TransferPipe");
+        mFds = ParcelFileDescriptor.createPipe();
+        mBufferPrefix = bufferPrefix;
+    }
+
+    ParcelFileDescriptor getReadFd() {
+        return mFds[0];
+    }
+
+    public ParcelFileDescriptor getWriteFd() {
+        return mFds[1];
+    }
+
+    public void setBufferPrefix(String prefix) {
+        mBufferPrefix = prefix;
+    }
+
+    public static void dumpAsync(IBinder binder, FileDescriptor out, String[] args)
+            throws IOException, RemoteException {
+        goDump(binder, out, args);
+    }
+
+    /**
+     * Read raw bytes from a service's dump function.
+     *
+     * <p>This can be used for dumping {@link android.util.proto.ProtoOutputStream protos}.
+     *
+     * @param binder The service providing the data
+     * @param args The arguments passed to the dump function of the service
+     */
+    public static byte[] dumpAsync(@NonNull IBinder binder, @Nullable String... args)
+            throws IOException, RemoteException {
+        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+        try {
+            TransferPipe.dumpAsync(binder, pipe[1].getFileDescriptor(), args);
+
+            // Data is written completely when dumpAsync is done
+            pipe[1].close();
+            pipe[1] = null;
+
+            byte[] buffer = new byte[4096];
+            try (ByteArrayOutputStream combinedBuffer = new ByteArrayOutputStream()) {
+                try (FileInputStream is = new FileInputStream(pipe[0].getFileDescriptor())) {
+                    while (true) {
+                        int numRead = is.read(buffer);
+                        if (numRead == -1) {
+                            break;
+                        }
+
+                        combinedBuffer.write(buffer, 0, numRead);
+                    }
+                }
+
+                return combinedBuffer.toByteArray();
+            }
+        } finally {
+            pipe[0].close();
+            IoUtils.closeQuietly(pipe[1]);
+        }
+    }
+
+    static void go(Caller caller, IInterface iface, FileDescriptor out,
+            String prefix, String[] args) throws IOException, RemoteException {
+        go(caller, iface, out, prefix, args, DEFAULT_TIMEOUT);
+    }
+
+    static void go(Caller caller, IInterface iface, FileDescriptor out,
+            String prefix, String[] args, long timeout) throws IOException, RemoteException {
+        if ((iface.asBinder()) instanceof Binder) {
+            // This is a local object...  just call it directly.
+            try {
+                caller.go(iface, out, prefix, args);
+            } catch (RemoteException e) {
+            }
+            return;
+        }
+
+        try (TransferPipe tp = new TransferPipe()) {
+            caller.go(iface, tp.getWriteFd().getFileDescriptor(), prefix, args);
+            tp.go(out, timeout);
+        }
+    }
+
+    static void goDump(IBinder binder, FileDescriptor out,
+            String[] args) throws IOException, RemoteException {
+        goDump(binder, out, args, DEFAULT_TIMEOUT);
+    }
+
+    static void goDump(IBinder binder, FileDescriptor out,
+            String[] args, long timeout) throws IOException, RemoteException {
+        if (binder instanceof Binder) {
+            // This is a local object...  just call it directly.
+            try {
+                binder.dump(out, args);
+            } catch (RemoteException e) {
+            }
+            return;
+        }
+
+        try (TransferPipe tp = new TransferPipe()) {
+            binder.dumpAsync(tp.getWriteFd().getFileDescriptor(), args);
+            tp.go(out, timeout);
+        }
+    }
+
+    public void go(FileDescriptor out) throws IOException {
+        go(out, DEFAULT_TIMEOUT);
+    }
+
+    public void go(FileDescriptor out, long timeout) throws IOException {
+        try {
+            synchronized (this) {
+                mOutFd = out;
+                mEndTime = SystemClock.uptimeMillis() + timeout;
+
+                if (DEBUG) Slog.i(TAG, "read=" + getReadFd() + " write=" + getWriteFd()
+                        + " out=" + out);
+
+                // Close the write fd, so we know when the other side is done.
+                closeFd(1);
+
+                mThread.start();
+
+                while (mFailure == null && !mComplete) {
+                    long waitTime = mEndTime - SystemClock.uptimeMillis();
+                    if (waitTime <= 0) {
+                        if (DEBUG) Slog.i(TAG, "TIMEOUT!");
+                        mThread.interrupt();
+                        throw new IOException("Timeout");
+                    }
+
+                    try {
+                        wait(waitTime);
+                    } catch (InterruptedException e) {
+                    }
+                }
+
+                if (DEBUG) Slog.i(TAG, "Finished: " + mFailure);
+                if (mFailure != null) {
+                    throw new IOException(mFailure);
+                }
+            }
+        } finally {
+            kill();
+        }
+    }
+
+    void closeFd(int num) {
+        if (mFds[num] != null) {
+            if (DEBUG) Slog.i(TAG, "Closing: " + mFds[num]);
+            try {
+                mFds[num].close();
+            } catch (IOException e) {
+            }
+            mFds[num] = null;
+        }
+    }
+
+    @Override
+    public void close() {
+        kill();
+    }
+
+    public void kill() {
+        synchronized (this) {
+            closeFd(0);
+            closeFd(1);
+        }
+    }
+
+    @Override
+    public void run() {
+        final byte[] buffer = new byte[1024];
+        final FileInputStream fis;
+        final FileOutputStream fos;
+
+        synchronized (this) {
+            ParcelFileDescriptor readFd = getReadFd();
+            if (readFd == null) {
+                Slog.w(TAG, "Pipe has been closed...");
+                return;
+            }
+            fis = new FileInputStream(readFd.getFileDescriptor());
+            fos = new FileOutputStream(mOutFd);
+        }
+
+        if (DEBUG) Slog.i(TAG, "Ready to read pipe...");
+        byte[] bufferPrefix = null;
+        boolean needPrefix = true;
+        if (mBufferPrefix != null) {
+            bufferPrefix = mBufferPrefix.getBytes();
+        }
+
+        int size;
+        try {
+            while ((size=fis.read(buffer)) > 0) {
+                if (DEBUG) Slog.i(TAG, "Got " + size + " bytes");
+                if (bufferPrefix == null) {
+                    fos.write(buffer, 0, size);
+                } else {
+                    int start = 0;
+                    for (int i=0; i<size; i++) {
+                        if (buffer[i] != '\n') {
+                            if (i > start) {
+                                fos.write(buffer, start, i-start);
+                            }
+                            start = i;
+                            if (needPrefix) {
+                                fos.write(bufferPrefix);
+                                needPrefix = false;
+                            }
+                            do {
+                                i++;
+                            } while (i<size && buffer[i] != '\n');
+                            if (i < size) {
+                                needPrefix = true;
+                            }
+                        }
+                    }
+                    if (size > start) {
+                        fos.write(buffer, start, size-start);
+                    }
+                }
+            }
+            if (DEBUG) Slog.i(TAG, "End of pipe: size=" + size);
+            if (mThread.isInterrupted()) {
+                if (DEBUG) Slog.i(TAG, "Interrupted!");
+            }
+        } catch (IOException e) {
+            synchronized (this) {
+                mFailure = e.toString();
+                notifyAll();
+                return;
+            }
+        }
+
+        synchronized (this) {
+            mComplete = true;
+            notifyAll();
+        }
+    }
+}
diff --git a/com/android/internal/os/WakelockPowerCalculator.java b/com/android/internal/os/WakelockPowerCalculator.java
new file mode 100644
index 0000000..c7897b2
--- /dev/null
+++ b/com/android/internal/os/WakelockPowerCalculator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+import android.util.ArrayMap;
+import android.util.Log;
+
+public class WakelockPowerCalculator extends PowerCalculator {
+    private static final String TAG = "WakelockPowerCalculator";
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private final double mPowerWakelock;
+    private long mTotalAppWakelockTimeMs = 0;
+
+    public WakelockPowerCalculator(PowerProfile profile) {
+        mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+        long wakeLockTimeUs = 0;
+        final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats =
+                u.getWakelockStats();
+        final int wakelockStatsCount = wakelockStats.size();
+        for (int i = 0; i < wakelockStatsCount; i++) {
+            final BatteryStats.Uid.Wakelock wakelock = wakelockStats.valueAt(i);
+
+            // Only care about partial wake locks since full wake locks
+            // are canceled when the user turns the screen off.
+            BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL);
+            if (timer != null) {
+                wakeLockTimeUs += timer.getTotalTimeLocked(rawRealtimeUs, statsType);
+            }
+        }
+        app.wakeLockTimeMs = wakeLockTimeUs / 1000; // convert to millis
+        mTotalAppWakelockTimeMs += app.wakeLockTimeMs;
+
+        // Add cost of holding a wake lock.
+        app.wakeLockPowerMah = (app.wakeLockTimeMs * mPowerWakelock) / (1000*60*60);
+        if (DEBUG && app.wakeLockPowerMah != 0) {
+            Log.d(TAG, "UID " + u.getUid() + ": wake " + app.wakeLockTimeMs
+                    + " power=" + BatteryStatsHelper.makemAh(app.wakeLockPowerMah));
+        }
+    }
+
+    @Override
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+                                   long rawUptimeUs, int statsType) {
+        long wakeTimeMillis = stats.getBatteryUptime(rawUptimeUs) / 1000;
+        wakeTimeMillis -= mTotalAppWakelockTimeMs
+                + (stats.getScreenOnTime(rawRealtimeUs, statsType) / 1000);
+        if (wakeTimeMillis > 0) {
+            final double power = (wakeTimeMillis * mPowerWakelock) / (1000*60*60);
+            if (DEBUG) {
+                Log.d(TAG, "OS wakeLockTime " + wakeTimeMillis + " power "
+                        + BatteryStatsHelper.makemAh(power));
+            }
+            app.wakeLockTimeMs += wakeTimeMillis;
+            app.wakeLockPowerMah += power;
+        }
+    }
+
+    @Override
+    public void reset() {
+        mTotalAppWakelockTimeMs = 0;
+    }
+}
diff --git a/com/android/internal/os/WebViewZygoteInit.java b/com/android/internal/os/WebViewZygoteInit.java
new file mode 100644
index 0000000..cadb66a
--- /dev/null
+++ b/com/android/internal/os/WebViewZygoteInit.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 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.internal.os;
+
+import android.app.ApplicationLoaders;
+import android.net.LocalSocket;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebViewFactory;
+import android.webkit.WebViewFactoryProvider;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+
+/**
+ * Startup class for the WebView zygote process.
+ *
+ * See {@link ZygoteInit} for generic zygote startup documentation.
+ *
+ * @hide
+ */
+class WebViewZygoteInit {
+    public static final String TAG = "WebViewZygoteInit";
+
+    private static ZygoteServer sServer;
+
+    private static class WebViewZygoteServer extends ZygoteServer {
+        @Override
+        protected ZygoteConnection createNewConnection(LocalSocket socket, String abiList)
+                throws IOException {
+            return new WebViewZygoteConnection(socket, abiList);
+        }
+    }
+
+    private static class WebViewZygoteConnection extends ZygoteConnection {
+        WebViewZygoteConnection(LocalSocket socket, String abiList) throws IOException {
+            super(socket, abiList);
+        }
+
+        @Override
+        protected void preload() {
+            // Nothing to preload by default.
+        }
+
+        @Override
+        protected boolean isPreloadComplete() {
+            // Webview zygotes don't preload any classes or resources or defaults, all of their
+            // preloading is package specific.
+            return true;
+        }
+
+        @Override
+        protected void handlePreloadPackage(String packagePath, String libsPath, String cacheKey) {
+            Log.i(TAG, "Beginning package preload");
+            // Ask ApplicationLoaders to create and cache a classloader for the WebView APK so that
+            // our children will reuse the same classloader instead of creating their own.
+            // This enables us to preload Java and native code in the webview zygote process and
+            // have the preloaded versions actually be used post-fork.
+            ClassLoader loader = ApplicationLoaders.getDefault().createAndCacheWebViewClassLoader(
+                    packagePath, libsPath, cacheKey);
+
+            // Add the APK to the Zygote's list of allowed files for children.
+            String[] packageList = TextUtils.split(packagePath, File.pathSeparator);
+            for (String packageEntry : packageList) {
+                Zygote.nativeAllowFileAcrossFork(packageEntry);
+            }
+
+            // Once we have the classloader, look up the WebViewFactoryProvider implementation and
+            // call preloadInZygote() on it to give it the opportunity to preload the native library
+            // and perform any other initialisation work that should be shared among the children.
+            boolean preloadSucceeded = false;
+            try {
+                Class<WebViewFactoryProvider> providerClass =
+                        WebViewFactory.getWebViewProviderClass(loader);
+                Method preloadInZygote = providerClass.getMethod("preloadInZygote");
+                preloadInZygote.setAccessible(true);
+                if (preloadInZygote.getReturnType() != Boolean.TYPE) {
+                    Log.e(TAG, "Unexpected return type: preloadInZygote must return boolean");
+                } else {
+                    preloadSucceeded = (boolean) providerClass.getMethod("preloadInZygote")
+                            .invoke(null);
+                    if (!preloadSucceeded) {
+                        Log.e(TAG, "preloadInZygote returned false");
+                    }
+                }
+            } catch (ReflectiveOperationException e) {
+                Log.e(TAG, "Exception while preloading package", e);
+            }
+
+            try {
+                DataOutputStream socketOut = getSocketOutputStream();
+                socketOut.writeInt(preloadSucceeded ? 1 : 0);
+            } catch (IOException ioe) {
+                throw new IllegalStateException("Error writing to command socket", ioe);
+            }
+
+            Log.i(TAG, "Package preload done");
+        }
+    }
+
+    public static void main(String argv[]) {
+        sServer = new WebViewZygoteServer();
+
+        // Zygote goes into its own process group.
+        try {
+            Os.setpgid(0, 0);
+        } catch (ErrnoException ex) {
+            throw new RuntimeException("Failed to setpgid(0,0)", ex);
+        }
+
+        final Runnable caller;
+        try {
+            sServer.registerServerSocket("webview_zygote");
+            // The select loop returns early in the child process after a fork and
+            // loops forever in the zygote.
+            caller = sServer.runSelectLoop(TextUtils.join(",", Build.SUPPORTED_ABIS));
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Fatal exception:", e);
+            throw e;
+        } finally {
+            sServer.closeServerSocket();
+        }
+
+        // We're in the child process and have exited the select loop. Proceed to execute the
+        // command.
+        if (caller != null) {
+            caller.run();
+        }
+    }
+}
diff --git a/com/android/internal/os/WifiPowerCalculator.java b/com/android/internal/os/WifiPowerCalculator.java
new file mode 100644
index 0000000..b447039
--- /dev/null
+++ b/com/android/internal/os/WifiPowerCalculator.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+import android.util.Log;
+
+/**
+ * WiFi power calculator for when BatteryStats supports energy reporting
+ * from the WiFi controller.
+ */
+public class WifiPowerCalculator extends PowerCalculator {
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private static final String TAG = "WifiPowerCalculator";
+    private final double mIdleCurrentMa;
+    private final double mTxCurrentMa;
+    private final double mRxCurrentMa;
+    private double mTotalAppPowerDrain = 0;
+    private long mTotalAppRunningTime = 0;
+
+    public WifiPowerCalculator(PowerProfile profile) {
+        mIdleCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
+        mTxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX);
+        mRxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+        final BatteryStats.ControllerActivityCounter counter = u.getWifiControllerActivity();
+        if (counter == null) {
+            return;
+        }
+
+        final long idleTime = counter.getIdleTimeCounter().getCountLocked(statsType);
+        final long txTime = counter.getTxTimeCounters()[0].getCountLocked(statsType);
+        final long rxTime = counter.getRxTimeCounter().getCountLocked(statsType);
+        app.wifiRunningTimeMs = idleTime + rxTime + txTime;
+        mTotalAppRunningTime += app.wifiRunningTimeMs;
+
+        app.wifiPowerMah =
+                ((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
+                / (1000*60*60);
+        mTotalAppPowerDrain += app.wifiPowerMah;
+
+        app.wifiRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_RX_DATA,
+                statsType);
+        app.wifiTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_TX_DATA,
+                statsType);
+        app.wifiRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_RX_DATA,
+                statsType);
+        app.wifiTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_TX_DATA,
+                statsType);
+
+        if (DEBUG && app.wifiPowerMah != 0) {
+            Log.d(TAG, "UID " + u.getUid() + ": idle=" + idleTime + "ms rx=" + rxTime + "ms tx=" +
+                    txTime + "ms power=" + BatteryStatsHelper.makemAh(app.wifiPowerMah));
+        }
+    }
+
+    @Override
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+                                   long rawUptimeUs, int statsType) {
+        final BatteryStats.ControllerActivityCounter counter = stats.getWifiControllerActivity();
+
+        final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
+        final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
+        final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
+
+        app.wifiRunningTimeMs = Math.max(0,
+                (idleTimeMs + rxTimeMs + txTimeMs) - mTotalAppRunningTime);
+
+        double powerDrainMah = counter.getPowerCounter().getCountLocked(statsType)
+                / (double)(1000*60*60);
+        if (powerDrainMah == 0) {
+            // Some controllers do not report power drain, so we can calculate it here.
+            powerDrainMah = ((idleTimeMs * mIdleCurrentMa) + (txTimeMs * mTxCurrentMa)
+                    + (rxTimeMs * mRxCurrentMa)) / (1000*60*60);
+        }
+        app.wifiPowerMah = Math.max(0, powerDrainMah - mTotalAppPowerDrain);
+
+        if (DEBUG) {
+            Log.d(TAG, "left over WiFi power: " + BatteryStatsHelper.makemAh(app.wifiPowerMah));
+        }
+    }
+
+    @Override
+    public void reset() {
+        mTotalAppPowerDrain = 0;
+        mTotalAppRunningTime = 0;
+    }
+}
diff --git a/com/android/internal/os/WifiPowerEstimator.java b/com/android/internal/os/WifiPowerEstimator.java
new file mode 100644
index 0000000..d175202
--- /dev/null
+++ b/com/android/internal/os/WifiPowerEstimator.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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.internal.os;
+
+import android.os.BatteryStats;
+import android.util.Log;
+
+/**
+ * Estimates WiFi power usage based on timers in BatteryStats.
+ */
+public class WifiPowerEstimator extends PowerCalculator {
+    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
+    private static final String TAG = "WifiPowerEstimator";
+    private final double mWifiPowerPerPacket;
+    private final double mWifiPowerOn;
+    private final double mWifiPowerScan;
+    private final double mWifiPowerBatchScan;
+    private long mTotalAppWifiRunningTimeMs = 0;
+
+    public WifiPowerEstimator(PowerProfile profile) {
+        mWifiPowerPerPacket = getWifiPowerPerPacket(profile);
+        mWifiPowerOn = profile.getAveragePower(PowerProfile.POWER_WIFI_ON);
+        mWifiPowerScan = profile.getAveragePower(PowerProfile.POWER_WIFI_SCAN);
+        mWifiPowerBatchScan = profile.getAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN);
+    }
+
+    /**
+     * Return estimated power per Wi-Fi packet in mAh/packet where 1 packet = 2 KB.
+     */
+    private static double getWifiPowerPerPacket(PowerProfile profile) {
+        final long WIFI_BPS = 1000000; // TODO: Extract average bit rates from system
+        final double WIFI_POWER = profile.getAveragePower(PowerProfile.POWER_WIFI_ACTIVE)
+                / 3600;
+        return WIFI_POWER / (((double)WIFI_BPS) / 8 / 2048);
+    }
+
+    @Override
+    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
+                             long rawUptimeUs, int statsType) {
+        app.wifiRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_RX_DATA,
+                statsType);
+        app.wifiTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_TX_DATA,
+                statsType);
+        app.wifiRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_RX_DATA,
+                statsType);
+        app.wifiTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_TX_DATA,
+                statsType);
+
+        final double wifiPacketPower = (app.wifiRxPackets + app.wifiTxPackets)
+                * mWifiPowerPerPacket;
+
+        app.wifiRunningTimeMs = u.getWifiRunningTime(rawRealtimeUs, statsType) / 1000;
+        mTotalAppWifiRunningTimeMs += app.wifiRunningTimeMs;
+        final double wifiLockPower = (app.wifiRunningTimeMs * mWifiPowerOn) / (1000*60*60);
+
+        final long wifiScanTimeMs = u.getWifiScanTime(rawRealtimeUs, statsType) / 1000;
+        final double wifiScanPower = (wifiScanTimeMs * mWifiPowerScan) / (1000*60*60);
+
+        double wifiBatchScanPower = 0;
+        for (int bin = 0; bin < BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; bin++) {
+            final long batchScanTimeMs =
+                    u.getWifiBatchedScanTime(bin, rawRealtimeUs, statsType) / 1000;
+            final double batchScanPower = (batchScanTimeMs * mWifiPowerBatchScan) / (1000*60*60);
+            wifiBatchScanPower += batchScanPower;
+        }
+
+        app.wifiPowerMah = wifiPacketPower + wifiLockPower + wifiScanPower + wifiBatchScanPower;
+        if (DEBUG && app.wifiPowerMah != 0) {
+            Log.d(TAG, "UID " + u.getUid() + ": power=" +
+                    BatteryStatsHelper.makemAh(app.wifiPowerMah));
+        }
+    }
+
+    @Override
+    public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
+                                   long rawUptimeUs, int statsType) {
+        final long totalRunningTimeMs = stats.getGlobalWifiRunningTime(rawRealtimeUs, statsType)
+                / 1000;
+        final double powerDrain = ((totalRunningTimeMs - mTotalAppWifiRunningTimeMs) * mWifiPowerOn)
+                / (1000*60*60);
+        app.wifiRunningTimeMs = totalRunningTimeMs;
+        app.wifiPowerMah = Math.max(0, powerDrain);
+    }
+
+    @Override
+    public void reset() {
+        mTotalAppWifiRunningTimeMs = 0;
+    }
+}
diff --git a/com/android/internal/os/WrapperInit.java b/com/android/internal/os/WrapperInit.java
new file mode 100644
index 0000000..4901080
--- /dev/null
+++ b/com/android/internal/os/WrapperInit.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2011 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.internal.os;
+
+import android.os.Process;
+import android.os.Trace;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructCapUserData;
+import android.system.StructCapUserHeader;
+import android.util.TimingsTraceLog;
+import android.util.Slog;
+import dalvik.system.VMRuntime;
+import java.io.DataOutputStream;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import libcore.io.IoUtils;
+
+/**
+ * Startup class for the wrapper process.
+ * @hide
+ */
+public class WrapperInit {
+    private final static String TAG = "AndroidRuntime";
+
+    /**
+     * Class not instantiable.
+     */
+    private WrapperInit() {
+    }
+
+    /**
+     * The main function called when starting a runtime application through a
+     * wrapper process instead of by forking Zygote.
+     *
+     * The first argument specifies the file descriptor for a pipe that should receive
+     * the pid of this process, or 0 if none.
+     *
+     * The second argument is the target SDK version for the app.
+     *
+     * The remaining arguments are passed to the runtime.
+     *
+     * @param args The command-line arguments.
+     */
+    public static void main(String[] args) {
+        // Parse our mandatory arguments.
+        int fdNum = Integer.parseInt(args[0], 10);
+        int targetSdkVersion = Integer.parseInt(args[1], 10);
+
+        // Tell the Zygote what our actual PID is (since it only knows about the
+        // wrapper that it directly forked).
+        if (fdNum != 0) {
+            try {
+                FileDescriptor fd = new FileDescriptor();
+                fd.setInt$(fdNum);
+                DataOutputStream os = new DataOutputStream(new FileOutputStream(fd));
+                os.writeInt(Process.myPid());
+                os.close();
+                IoUtils.closeQuietly(fd);
+            } catch (IOException ex) {
+                Slog.d(TAG, "Could not write pid of wrapped process to Zygote pipe.", ex);
+            }
+        }
+
+        // Mimic system Zygote preloading.
+        ZygoteInit.preload(new TimingsTraceLog("WrapperInitTiming",
+                Trace.TRACE_TAG_DALVIK));
+
+        // Launch the application.
+        String[] runtimeArgs = new String[args.length - 2];
+        System.arraycopy(args, 2, runtimeArgs, 0, runtimeArgs.length);
+        Runnable r = wrapperInit(targetSdkVersion, runtimeArgs);
+
+        r.run();
+    }
+
+    /**
+     * Executes a runtime application with a wrapper command.
+     * This method never returns.
+     *
+     * @param invokeWith The wrapper command.
+     * @param niceName The nice name for the application, or null if none.
+     * @param targetSdkVersion The target SDK version for the app.
+     * @param pipeFd The pipe to which the application's pid should be written, or null if none.
+     * @param args Arguments for {@link RuntimeInit#main}.
+     */
+    public static void execApplication(String invokeWith, String niceName,
+            int targetSdkVersion, String instructionSet, FileDescriptor pipeFd,
+            String[] args) {
+        StringBuilder command = new StringBuilder(invokeWith);
+
+        final String appProcess;
+        if (VMRuntime.is64BitInstructionSet(instructionSet)) {
+            appProcess = "/system/bin/app_process64";
+        } else {
+            appProcess = "/system/bin/app_process32";
+        }
+        command.append(' ');
+        command.append(appProcess);
+
+        command.append(" /system/bin --application");
+        if (niceName != null) {
+            command.append(" '--nice-name=").append(niceName).append("'");
+        }
+        command.append(" com.android.internal.os.WrapperInit ");
+        command.append(pipeFd != null ? pipeFd.getInt$() : 0);
+        command.append(' ');
+        command.append(targetSdkVersion);
+        Zygote.appendQuotedShellArgs(command, args);
+        preserveCapabilities();
+        Zygote.execShell(command.toString());
+    }
+
+    /**
+     * The main function called when an application is started through a
+     * wrapper process.
+     *
+     * When the wrapper starts, the runtime starts {@link RuntimeInit#main}
+     * which calls {@link main} which then calls this method.
+     * So we don't need to call commonInit() here.
+     *
+     * @param targetSdkVersion target SDK version
+     * @param argv arg strings
+     */
+    private static Runnable wrapperInit(int targetSdkVersion, String[] argv) {
+        if (RuntimeInit.DEBUG) {
+            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from wrapper");
+        }
+
+        // Check whether the first argument is a "-cp" in argv, and assume the next argument is the
+        // classpath. If found, create a PathClassLoader and use it for applicationInit.
+        ClassLoader classLoader = null;
+        if (argv != null && argv.length > 2 && argv[0].equals("-cp")) {
+            classLoader = ZygoteInit.createPathClassLoader(argv[1], targetSdkVersion);
+
+            // Install this classloader as the context classloader, too.
+            Thread.currentThread().setContextClassLoader(classLoader);
+
+            // Remove the classpath from the arguments.
+            String removedArgs[] = new String[argv.length - 2];
+            System.arraycopy(argv, 2, removedArgs, 0, argv.length - 2);
+            argv = removedArgs;
+        }
+
+        // Perform the same initialization that would happen after the Zygote forks.
+        Zygote.nativePreApplicationInit();
+        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
+    }
+
+    /**
+     * Copy current capabilities to ambient capabilities. This is required for apps using
+     * capabilities, as execv will re-evaluate the capability set, and the set of sh is
+     * empty. Ambient capabilities have to be set to inherit them effectively.
+     *
+     * Note: This is BEST EFFORT ONLY. In case capabilities can't be raised, this function
+     *       will silently return. In THIS CASE ONLY, as this is a development feature, it
+     *       is better to return and try to run anyways, instead of blocking the wrapped app.
+     *       This is acceptable here as failure will leave the wrapped app with strictly less
+     *       capabilities, which may make it crash, but not exceed its allowances.
+     */
+    private static void preserveCapabilities() {
+        StructCapUserHeader header = new StructCapUserHeader(
+                OsConstants._LINUX_CAPABILITY_VERSION_3, 0);
+        StructCapUserData[] data;
+        try {
+            data = Os.capget(header);
+        } catch (ErrnoException e) {
+            Slog.e(RuntimeInit.TAG, "RuntimeInit: Failed capget", e);
+            return;
+        }
+
+        if (data[0].permitted != data[0].inheritable ||
+                data[1].permitted != data[1].inheritable) {
+            data[0] = new StructCapUserData(data[0].effective, data[0].permitted,
+                    data[0].permitted);
+            data[1] = new StructCapUserData(data[1].effective, data[1].permitted,
+                    data[1].permitted);
+            try {
+                Os.capset(header, data);
+            } catch (ErrnoException e) {
+                Slog.e(RuntimeInit.TAG, "RuntimeInit: Failed capset", e);
+                return;
+            }
+        }
+
+        for (int i = 0; i < 64; i++) {
+            int dataIndex = OsConstants.CAP_TO_INDEX(i);
+            int capMask = OsConstants.CAP_TO_MASK(i);
+            if ((data[dataIndex].inheritable & capMask) != 0) {
+                try {
+                    Os.prctl(OsConstants.PR_CAP_AMBIENT, OsConstants.PR_CAP_AMBIENT_RAISE, i, 0,
+                            0);
+                } catch (ErrnoException ex) {
+                    // Only log here. Try to run the wrapped application even without this
+                    // ambient capability. It may crash after fork, but at least we'll try.
+                    Slog.e(RuntimeInit.TAG, "RuntimeInit: Failed to raise ambient capability "
+                            + i, ex);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/os/Zygote.java b/com/android/internal/os/Zygote.java
new file mode 100644
index 0000000..4e4b5b8
--- /dev/null
+++ b/com/android/internal/os/Zygote.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2014 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.internal.os;
+
+import android.os.IVold;
+import android.os.Trace;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import dalvik.system.ZygoteHooks;
+
+/** @hide */
+public final class Zygote {
+    /*
+    * Bit values for "debugFlags" argument.  The definitions are duplicated
+    * in the native code.
+    */
+
+    /** enable debugging over JDWP */
+    public static final int DEBUG_ENABLE_JDWP   = 1;
+    /** enable JNI checks */
+    public static final int DEBUG_ENABLE_CHECKJNI   = 1 << 1;
+    /** enable Java programming language "assert" statements */
+    public static final int DEBUG_ENABLE_ASSERT     = 1 << 2;
+    /** disable the AOT compiler and JIT */
+    public static final int DEBUG_ENABLE_SAFEMODE   = 1 << 3;
+    /** Enable logging of third-party JNI activity. */
+    public static final int DEBUG_ENABLE_JNI_LOGGING = 1 << 4;
+    /** Force generation of native debugging information. */
+    public static final int DEBUG_GENERATE_DEBUG_INFO = 1 << 5;
+    /** Always use JIT-ed code. */
+    public static final int DEBUG_ALWAYS_JIT = 1 << 6;
+    /** Make the code native debuggable by turning off some optimizations. */
+    public static final int DEBUG_NATIVE_DEBUGGABLE = 1 << 7;
+    /** Make the code Java debuggable by turning off some optimizations. */
+    public static final int DEBUG_JAVA_DEBUGGABLE = 1 << 8;
+
+    /** No external storage should be mounted. */
+    public static final int MOUNT_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE;
+    /** Default external storage should be mounted. */
+    public static final int MOUNT_EXTERNAL_DEFAULT = IVold.REMOUNT_MODE_DEFAULT;
+    /** Read-only external storage should be mounted. */
+    public static final int MOUNT_EXTERNAL_READ = IVold.REMOUNT_MODE_READ;
+    /** Read-write external storage should be mounted. */
+    public static final int MOUNT_EXTERNAL_WRITE = IVold.REMOUNT_MODE_WRITE;
+
+    private static final ZygoteHooks VM_HOOKS = new ZygoteHooks();
+
+    private Zygote() {}
+
+    /**
+     * Forks a new VM instance.  The current VM must have been started
+     * with the -Xzygote flag. <b>NOTE: new instance keeps all
+     * root capabilities. The new process is expected to call capset()</b>.
+     *
+     * @param uid the UNIX uid that the new process should setuid() to after
+     * fork()ing and and before spawning any threads.
+     * @param gid the UNIX gid that the new process should setgid() to after
+     * fork()ing and and before spawning any threads.
+     * @param gids null-ok; a list of UNIX gids that the new process should
+     * setgroups() to after fork and before spawning any threads.
+     * @param debugFlags bit flags that enable debugging features.
+     * @param rlimits null-ok an array of rlimit tuples, with the second
+     * dimension having a length of 3 and representing
+     * (resource, rlim_cur, rlim_max). These are set via the posix
+     * setrlimit(2) call.
+     * @param seInfo null-ok a string specifying SELinux information for
+     * the new process.
+     * @param niceName null-ok a string specifying the process name.
+     * @param fdsToClose an array of ints, holding one or more POSIX
+     * file descriptor numbers that are to be closed by the child
+     * (and replaced by /dev/null) after forking.  An integer value
+     * of -1 in any entry in the array means "ignore this one".
+     * @param fdsToIgnore null-ok an array of ints, either null or holding
+     * one or more POSIX file descriptor numbers that are to be ignored
+     * in the file descriptor table check.
+     * @param instructionSet null-ok the instruction set to use.
+     * @param appDataDir null-ok the data directory of the app.
+     *
+     * @return 0 if this is the child, pid of the child
+     * if this is the parent, or -1 on error.
+     */
+    public static int forkAndSpecialize(int uid, int gid, int[] gids, int debugFlags,
+          int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
+          int[] fdsToIgnore, String instructionSet, String appDataDir) {
+        VM_HOOKS.preFork();
+        // Resets nice priority for zygote process.
+        resetNicePriority();
+        int pid = nativeForkAndSpecialize(
+                  uid, gid, gids, debugFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
+                  fdsToIgnore, instructionSet, appDataDir);
+        // Enable tracing as soon as possible for the child process.
+        if (pid == 0) {
+            Trace.setTracingEnabled(true, debugFlags);
+
+            // Note that this event ends at the end of handleChildProc,
+            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork");
+        }
+        VM_HOOKS.postForkCommon();
+        return pid;
+    }
+
+    native private static int nativeForkAndSpecialize(int uid, int gid, int[] gids,int debugFlags,
+          int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
+          int[] fdsToIgnore, String instructionSet, String appDataDir);
+
+    /**
+     * Called to do any initialization before starting an application.
+     */
+    native static void nativePreApplicationInit();
+
+    /**
+     * Special method to start the system server process. In addition to the
+     * common actions performed in forkAndSpecialize, the pid of the child
+     * process is recorded such that the death of the child process will cause
+     * zygote to exit.
+     *
+     * @param uid the UNIX uid that the new process should setuid() to after
+     * fork()ing and and before spawning any threads.
+     * @param gid the UNIX gid that the new process should setgid() to after
+     * fork()ing and and before spawning any threads.
+     * @param gids null-ok; a list of UNIX gids that the new process should
+     * setgroups() to after fork and before spawning any threads.
+     * @param debugFlags bit flags that enable debugging features.
+     * @param rlimits null-ok an array of rlimit tuples, with the second
+     * dimension having a length of 3 and representing
+     * (resource, rlim_cur, rlim_max). These are set via the posix
+     * setrlimit(2) call.
+     * @param permittedCapabilities argument for setcap()
+     * @param effectiveCapabilities argument for setcap()
+     *
+     * @return 0 if this is the child, pid of the child
+     * if this is the parent, or -1 on error.
+     */
+    public static int forkSystemServer(int uid, int gid, int[] gids, int debugFlags,
+            int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {
+        VM_HOOKS.preFork();
+        // Resets nice priority for zygote process.
+        resetNicePriority();
+        int pid = nativeForkSystemServer(
+                uid, gid, gids, debugFlags, rlimits, permittedCapabilities, effectiveCapabilities);
+        // Enable tracing as soon as we enter the system_server.
+        if (pid == 0) {
+            Trace.setTracingEnabled(true, debugFlags);
+        }
+        VM_HOOKS.postForkCommon();
+        return pid;
+    }
+
+    native private static int nativeForkSystemServer(int uid, int gid, int[] gids, int debugFlags,
+            int[][] rlimits, long permittedCapabilities, long effectiveCapabilities);
+
+    /**
+     * Lets children of the zygote inherit open file descriptors to this path.
+     */
+    native protected static void nativeAllowFileAcrossFork(String path);
+
+    /**
+     * Zygote unmount storage space on initializing.
+     * This method is called once.
+     */
+    native protected static void nativeUnmountStorageOnInit();
+
+    private static void callPostForkChildHooks(int debugFlags, boolean isSystemServer,
+            String instructionSet) {
+        VM_HOOKS.postForkChild(debugFlags, isSystemServer, instructionSet);
+    }
+
+    /**
+     * Resets the calling thread priority to the default value (Thread.NORM_PRIORITY
+     * or nice value 0). This updates both the priority value in java.lang.Thread and
+     * the nice value (setpriority).
+     */
+    static void resetNicePriority() {
+        Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
+    }
+
+    /**
+     * Executes "/system/bin/sh -c &lt;command&gt;" using the exec() system call.
+     * This method throws a runtime exception if exec() failed, otherwise, this
+     * method never returns.
+     *
+     * @param command The shell command to execute.
+     */
+    public static void execShell(String command) {
+        String[] args = { "/system/bin/sh", "-c", command };
+        try {
+            Os.execv(args[0], args);
+        } catch (ErrnoException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Appends quotes shell arguments to the specified string builder.
+     * The arguments are quoted using single-quotes, escaped if necessary,
+     * prefixed with a space, and appended to the command.
+     *
+     * @param command A string builder for the shell command being constructed.
+     * @param args An array of argument strings to be quoted and appended to the command.
+     * @see #execShell(String)
+     */
+    public static void appendQuotedShellArgs(StringBuilder command, String[] args) {
+        for (String arg : args) {
+            command.append(" '").append(arg.replace("'", "'\\''")).append("'");
+        }
+    }
+}
diff --git a/com/android/internal/os/ZygoteConnection.java b/com/android/internal/os/ZygoteConnection.java
new file mode 100644
index 0000000..9fa3239
--- /dev/null
+++ b/com/android/internal/os/ZygoteConnection.java
@@ -0,0 +1,921 @@
+/*
+ * Copyright (C) 2007 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.internal.os;
+
+import static android.system.OsConstants.F_SETFD;
+import static android.system.OsConstants.O_CLOEXEC;
+import static android.system.OsConstants.POLLIN;
+import static android.system.OsConstants.STDERR_FILENO;
+import static android.system.OsConstants.STDIN_FILENO;
+import static android.system.OsConstants.STDOUT_FILENO;
+import static com.android.internal.os.ZygoteConnectionConstants.CONNECTION_TIMEOUT_MILLIS;
+import static com.android.internal.os.ZygoteConnectionConstants.MAX_ZYGOTE_ARGC;
+import static com.android.internal.os.ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS;
+
+import android.net.Credentials;
+import android.net.LocalSocket;
+import android.os.FactoryTest;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructPollfd;
+import android.util.Log;
+import dalvik.system.VMRuntime;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import libcore.io.IoUtils;
+
+/**
+ * A connection that can make spawn requests.
+ */
+class ZygoteConnection {
+    private static final String TAG = "Zygote";
+
+    /** a prototype instance for a future List.toArray() */
+    private static final int[][] intArray2d = new int[0][0];
+
+    /**
+     * The command socket.
+     *
+     * mSocket is retained in the child process in "peer wait" mode, so
+     * that it closes when the child process terminates. In other cases,
+     * it is closed in the peer.
+     */
+    private final LocalSocket mSocket;
+    private final DataOutputStream mSocketOutStream;
+    private final BufferedReader mSocketReader;
+    private final Credentials peer;
+    private final String abiList;
+    private boolean isEof;
+
+    /**
+     * Constructs instance from connected socket.
+     *
+     * @param socket non-null; connected socket
+     * @param abiList non-null; a list of ABIs this zygote supports.
+     * @throws IOException
+     */
+    ZygoteConnection(LocalSocket socket, String abiList) throws IOException {
+        mSocket = socket;
+        this.abiList = abiList;
+
+        mSocketOutStream
+                = new DataOutputStream(socket.getOutputStream());
+
+        mSocketReader = new BufferedReader(
+                new InputStreamReader(socket.getInputStream()), 256);
+
+        mSocket.setSoTimeout(CONNECTION_TIMEOUT_MILLIS);
+
+        try {
+            peer = mSocket.getPeerCredentials();
+        } catch (IOException ex) {
+            Log.e(TAG, "Cannot read peer credentials", ex);
+            throw ex;
+        }
+
+        isEof = false;
+    }
+
+    /**
+     * Returns the file descriptor of the associated socket.
+     *
+     * @return null-ok; file descriptor
+     */
+    FileDescriptor getFileDesciptor() {
+        return mSocket.getFileDescriptor();
+    }
+
+    /**
+     * Reads one start command from the command socket. If successful, a child is forked and a
+     * {@code Runnable} that calls the childs main method (or equivalent) is returned in the child
+     * process. {@code null} is always returned in the parent process (the zygote).
+     *
+     * If the client closes the socket, an {@code EOF} condition is set, which callers can test
+     * for by calling {@code ZygoteConnection.isClosedByPeer}.
+     */
+    Runnable processOneCommand(ZygoteServer zygoteServer) {
+        String args[];
+        Arguments parsedArgs = null;
+        FileDescriptor[] descriptors;
+
+        try {
+            args = readArgumentList();
+            descriptors = mSocket.getAncillaryFileDescriptors();
+        } catch (IOException ex) {
+            throw new IllegalStateException("IOException on command socket", ex);
+        }
+
+        // readArgumentList returns null only when it has reached EOF with no available
+        // data to read. This will only happen when the remote socket has disconnected.
+        if (args == null) {
+            isEof = true;
+            return null;
+        }
+
+        int pid = -1;
+        FileDescriptor childPipeFd = null;
+        FileDescriptor serverPipeFd = null;
+
+        parsedArgs = new Arguments(args);
+
+        if (parsedArgs.abiListQuery) {
+            handleAbiListQuery();
+            return null;
+        }
+
+        if (parsedArgs.preloadDefault) {
+            handlePreload();
+            return null;
+        }
+
+        if (parsedArgs.preloadPackage != null) {
+            handlePreloadPackage(parsedArgs.preloadPackage, parsedArgs.preloadPackageLibs,
+                    parsedArgs.preloadPackageCacheKey);
+            return null;
+        }
+
+        if (parsedArgs.permittedCapabilities != 0 || parsedArgs.effectiveCapabilities != 0) {
+            throw new ZygoteSecurityException("Client may not specify capabilities: " +
+                    "permitted=0x" + Long.toHexString(parsedArgs.permittedCapabilities) +
+                    ", effective=0x" + Long.toHexString(parsedArgs.effectiveCapabilities));
+        }
+
+        applyUidSecurityPolicy(parsedArgs, peer);
+        applyInvokeWithSecurityPolicy(parsedArgs, peer);
+
+        applyDebuggerSystemProperty(parsedArgs);
+        applyInvokeWithSystemProperty(parsedArgs);
+
+        int[][] rlimits = null;
+
+        if (parsedArgs.rlimits != null) {
+            rlimits = parsedArgs.rlimits.toArray(intArray2d);
+        }
+
+        int[] fdsToIgnore = null;
+
+        if (parsedArgs.invokeWith != null) {
+            try {
+                FileDescriptor[] pipeFds = Os.pipe2(O_CLOEXEC);
+                childPipeFd = pipeFds[1];
+                serverPipeFd = pipeFds[0];
+                Os.fcntlInt(childPipeFd, F_SETFD, 0);
+                fdsToIgnore = new int[]{childPipeFd.getInt$(), serverPipeFd.getInt$()};
+            } catch (ErrnoException errnoEx) {
+                throw new IllegalStateException("Unable to set up pipe for invoke-with", errnoEx);
+            }
+        }
+
+        /**
+         * In order to avoid leaking descriptors to the Zygote child,
+         * the native code must close the two Zygote socket descriptors
+         * in the child process before it switches from Zygote-root to
+         * the UID and privileges of the application being launched.
+         *
+         * In order to avoid "bad file descriptor" errors when the
+         * two LocalSocket objects are closed, the Posix file
+         * descriptors are released via a dup2() call which closes
+         * the socket and substitutes an open descriptor to /dev/null.
+         */
+
+        int [] fdsToClose = { -1, -1 };
+
+        FileDescriptor fd = mSocket.getFileDescriptor();
+
+        if (fd != null) {
+            fdsToClose[0] = fd.getInt$();
+        }
+
+        fd = zygoteServer.getServerSocketFileDescriptor();
+
+        if (fd != null) {
+            fdsToClose[1] = fd.getInt$();
+        }
+
+        fd = null;
+
+        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
+                parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
+                parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.instructionSet,
+                parsedArgs.appDataDir);
+
+        try {
+            if (pid == 0) {
+                // in child
+                zygoteServer.setForkChild();
+
+                zygoteServer.closeServerSocket();
+                IoUtils.closeQuietly(serverPipeFd);
+                serverPipeFd = null;
+
+                return handleChildProc(parsedArgs, descriptors, childPipeFd);
+            } else {
+                // In the parent. A pid < 0 indicates a failure and will be handled in
+                // handleParentProc.
+                IoUtils.closeQuietly(childPipeFd);
+                childPipeFd = null;
+                handleParentProc(pid, descriptors, serverPipeFd);
+                return null;
+            }
+        } finally {
+            IoUtils.closeQuietly(childPipeFd);
+            IoUtils.closeQuietly(serverPipeFd);
+        }
+    }
+
+    private void handleAbiListQuery() {
+        try {
+            final byte[] abiListBytes = abiList.getBytes(StandardCharsets.US_ASCII);
+            mSocketOutStream.writeInt(abiListBytes.length);
+            mSocketOutStream.write(abiListBytes);
+        } catch (IOException ioe) {
+            throw new IllegalStateException("Error writing to command socket", ioe);
+        }
+    }
+
+    /**
+     * Preloads resources if the zygote is in lazily preload mode. Writes the result of the
+     * preload operation; {@code 0} when a preload was initiated due to this request and {@code 1}
+     * if no preload was initiated. The latter implies that the zygote is not configured to load
+     * resources lazy or that the zygote has already handled a previous request to handlePreload.
+     */
+    private void handlePreload() {
+        try {
+            if (isPreloadComplete()) {
+                mSocketOutStream.writeInt(1);
+            } else {
+                preload();
+                mSocketOutStream.writeInt(0);
+            }
+        } catch (IOException ioe) {
+            throw new IllegalStateException("Error writing to command socket", ioe);
+        }
+    }
+
+    protected void preload() {
+        ZygoteInit.lazyPreload();
+    }
+
+    protected boolean isPreloadComplete() {
+        return ZygoteInit.isPreloadComplete();
+    }
+
+    protected DataOutputStream getSocketOutputStream() {
+        return mSocketOutStream;
+    }
+
+    protected void handlePreloadPackage(String packagePath, String libsPath, String cacheKey) {
+        throw new RuntimeException("Zyogte does not support package preloading");
+    }
+
+    /**
+     * Closes socket associated with this connection.
+     */
+    void closeSocket() {
+        try {
+            mSocket.close();
+        } catch (IOException ex) {
+            Log.e(TAG, "Exception while closing command "
+                    + "socket in parent", ex);
+        }
+    }
+
+    boolean isClosedByPeer() {
+        return isEof;
+    }
+
+    /**
+     * Handles argument parsing for args related to the zygote spawner.
+     *
+     * Current recognized args:
+     * <ul>
+     *   <li> --setuid=<i>uid of child process, defaults to 0</i>
+     *   <li> --setgid=<i>gid of child process, defaults to 0</i>
+     *   <li> --setgroups=<i>comma-separated list of supplimentary gid's</i>
+     *   <li> --capabilities=<i>a pair of comma-separated integer strings
+     * indicating Linux capabilities(2) set for child. The first string
+     * represents the <code>permitted</code> set, and the second the
+     * <code>effective</code> set. Precede each with 0 or
+     * 0x for octal or hexidecimal value. If unspecified, both default to 0.
+     * This parameter is only applied if the uid of the new process will
+     * be non-0. </i>
+     *   <li> --rlimit=r,c,m<i>tuple of values for setrlimit() call.
+     *    <code>r</code> is the resource, <code>c</code> and <code>m</code>
+     *    are the settings for current and max value.</i>
+     *   <li> --instruction-set=<i>instruction-set-string</i> which instruction set to use/emulate.
+     *   <li> --nice-name=<i>nice name to appear in ps</i>
+     *   <li> --runtime-args indicates that the remaining arg list should
+     * be handed off to com.android.internal.os.RuntimeInit, rather than
+     * processed directly.
+     * Android runtime startup (eg, Binder initialization) is also eschewed.
+     *   <li> [--] &lt;args for RuntimeInit &gt;
+     * </ul>
+     */
+    static class Arguments {
+        /** from --setuid */
+        int uid = 0;
+        boolean uidSpecified;
+
+        /** from --setgid */
+        int gid = 0;
+        boolean gidSpecified;
+
+        /** from --setgroups */
+        int[] gids;
+
+        /**
+         * From --enable-jdwp, --enable-checkjni, --enable-assert,
+         * --enable-safemode, --generate-debug-info, --enable-jni-logging,
+         * --java-debuggable, and --native-debuggable.
+         */
+        int debugFlags;
+
+        /** From --mount-external */
+        int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
+
+        /** from --target-sdk-version. */
+        int targetSdkVersion;
+        boolean targetSdkVersionSpecified;
+
+        /** from --nice-name */
+        String niceName;
+
+        /** from --capabilities */
+        boolean capabilitiesSpecified;
+        long permittedCapabilities;
+        long effectiveCapabilities;
+
+        /** from --seinfo */
+        boolean seInfoSpecified;
+        String seInfo;
+
+        /** from all --rlimit=r,c,m */
+        ArrayList<int[]> rlimits;
+
+        /** from --invoke-with */
+        String invokeWith;
+
+        /**
+         * Any args after and including the first non-option arg
+         * (or after a '--')
+         */
+        String remainingArgs[];
+
+        /**
+         * Whether the current arguments constitute an ABI list query.
+         */
+        boolean abiListQuery;
+
+        /**
+         * The instruction set to use, or null when not important.
+         */
+        String instructionSet;
+
+        /**
+         * The app data directory. May be null, e.g., for the system server. Note that this might
+         * not be reliable in the case of process-sharing apps.
+         */
+        String appDataDir;
+
+        /**
+         * Whether to preload a package, with the package path in the remainingArgs.
+         */
+        String preloadPackage;
+        String preloadPackageLibs;
+        String preloadPackageCacheKey;
+
+        /**
+         * Whether this is a request to start preloading the default resources and classes.
+         * This argument only makes sense when the zygote is in lazy preload mode (i.e, when
+         * it's started with --enable-lazy-preload).
+         */
+        boolean preloadDefault;
+
+        /**
+         * Constructs instance and parses args
+         * @param args zygote command-line args
+         * @throws IllegalArgumentException
+         */
+        Arguments(String args[]) throws IllegalArgumentException {
+            parseArgs(args);
+        }
+
+        /**
+         * Parses the commandline arguments intended for the Zygote spawner
+         * (such as "--setuid=" and "--setgid=") and creates an array
+         * containing the remaining args.
+         *
+         * Per security review bug #1112214, duplicate args are disallowed in
+         * critical cases to make injection harder.
+         */
+        private void parseArgs(String args[])
+                throws IllegalArgumentException {
+            int curArg = 0;
+
+            boolean seenRuntimeArgs = false;
+
+            for ( /* curArg */ ; curArg < args.length; curArg++) {
+                String arg = args[curArg];
+
+                if (arg.equals("--")) {
+                    curArg++;
+                    break;
+                } else if (arg.startsWith("--setuid=")) {
+                    if (uidSpecified) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+                    uidSpecified = true;
+                    uid = Integer.parseInt(
+                            arg.substring(arg.indexOf('=') + 1));
+                } else if (arg.startsWith("--setgid=")) {
+                    if (gidSpecified) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+                    gidSpecified = true;
+                    gid = Integer.parseInt(
+                            arg.substring(arg.indexOf('=') + 1));
+                } else if (arg.startsWith("--target-sdk-version=")) {
+                    if (targetSdkVersionSpecified) {
+                        throw new IllegalArgumentException(
+                                "Duplicate target-sdk-version specified");
+                    }
+                    targetSdkVersionSpecified = true;
+                    targetSdkVersion = Integer.parseInt(
+                            arg.substring(arg.indexOf('=') + 1));
+                } else if (arg.equals("--enable-jdwp")) {
+                    debugFlags |= Zygote.DEBUG_ENABLE_JDWP;
+                } else if (arg.equals("--enable-safemode")) {
+                    debugFlags |= Zygote.DEBUG_ENABLE_SAFEMODE;
+                } else if (arg.equals("--enable-checkjni")) {
+                    debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
+                } else if (arg.equals("--generate-debug-info")) {
+                    debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO;
+                } else if (arg.equals("--always-jit")) {
+                    debugFlags |= Zygote.DEBUG_ALWAYS_JIT;
+                } else if (arg.equals("--native-debuggable")) {
+                    debugFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;
+                } else if (arg.equals("--java-debuggable")) {
+                    debugFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;
+                } else if (arg.equals("--enable-jni-logging")) {
+                    debugFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING;
+                } else if (arg.equals("--enable-assert")) {
+                    debugFlags |= Zygote.DEBUG_ENABLE_ASSERT;
+                } else if (arg.equals("--runtime-args")) {
+                    seenRuntimeArgs = true;
+                } else if (arg.startsWith("--seinfo=")) {
+                    if (seInfoSpecified) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+                    seInfoSpecified = true;
+                    seInfo = arg.substring(arg.indexOf('=') + 1);
+                } else if (arg.startsWith("--capabilities=")) {
+                    if (capabilitiesSpecified) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+                    capabilitiesSpecified = true;
+                    String capString = arg.substring(arg.indexOf('=')+1);
+
+                    String[] capStrings = capString.split(",", 2);
+
+                    if (capStrings.length == 1) {
+                        effectiveCapabilities = Long.decode(capStrings[0]);
+                        permittedCapabilities = effectiveCapabilities;
+                    } else {
+                        permittedCapabilities = Long.decode(capStrings[0]);
+                        effectiveCapabilities = Long.decode(capStrings[1]);
+                    }
+                } else if (arg.startsWith("--rlimit=")) {
+                    // Duplicate --rlimit arguments are specifically allowed.
+                    String[] limitStrings
+                            = arg.substring(arg.indexOf('=')+1).split(",");
+
+                    if (limitStrings.length != 3) {
+                        throw new IllegalArgumentException(
+                                "--rlimit= should have 3 comma-delimited ints");
+                    }
+                    int[] rlimitTuple = new int[limitStrings.length];
+
+                    for(int i=0; i < limitStrings.length; i++) {
+                        rlimitTuple[i] = Integer.parseInt(limitStrings[i]);
+                    }
+
+                    if (rlimits == null) {
+                        rlimits = new ArrayList();
+                    }
+
+                    rlimits.add(rlimitTuple);
+                } else if (arg.startsWith("--setgroups=")) {
+                    if (gids != null) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+
+                    String[] params
+                            = arg.substring(arg.indexOf('=') + 1).split(",");
+
+                    gids = new int[params.length];
+
+                    for (int i = params.length - 1; i >= 0 ; i--) {
+                        gids[i] = Integer.parseInt(params[i]);
+                    }
+                } else if (arg.equals("--invoke-with")) {
+                    if (invokeWith != null) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+                    try {
+                        invokeWith = args[++curArg];
+                    } catch (IndexOutOfBoundsException ex) {
+                        throw new IllegalArgumentException(
+                                "--invoke-with requires argument");
+                    }
+                } else if (arg.startsWith("--nice-name=")) {
+                    if (niceName != null) {
+                        throw new IllegalArgumentException(
+                                "Duplicate arg specified");
+                    }
+                    niceName = arg.substring(arg.indexOf('=') + 1);
+                } else if (arg.equals("--mount-external-default")) {
+                    mountExternal = Zygote.MOUNT_EXTERNAL_DEFAULT;
+                } else if (arg.equals("--mount-external-read")) {
+                    mountExternal = Zygote.MOUNT_EXTERNAL_READ;
+                } else if (arg.equals("--mount-external-write")) {
+                    mountExternal = Zygote.MOUNT_EXTERNAL_WRITE;
+                } else if (arg.equals("--query-abi-list")) {
+                    abiListQuery = true;
+                } else if (arg.startsWith("--instruction-set=")) {
+                    instructionSet = arg.substring(arg.indexOf('=') + 1);
+                } else if (arg.startsWith("--app-data-dir=")) {
+                    appDataDir = arg.substring(arg.indexOf('=') + 1);
+                } else if (arg.equals("--preload-package")) {
+                    preloadPackage = args[++curArg];
+                    preloadPackageLibs = args[++curArg];
+                    preloadPackageCacheKey = args[++curArg];
+                } else if (arg.equals("--preload-default")) {
+                    preloadDefault = true;
+                } else {
+                    break;
+                }
+            }
+
+            if (abiListQuery) {
+                if (args.length - curArg > 0) {
+                    throw new IllegalArgumentException("Unexpected arguments after --query-abi-list.");
+                }
+            } else if (preloadPackage != null) {
+                if (args.length - curArg > 0) {
+                    throw new IllegalArgumentException(
+                            "Unexpected arguments after --preload-package.");
+                }
+            } else if (!preloadDefault) {
+                if (!seenRuntimeArgs) {
+                    throw new IllegalArgumentException("Unexpected argument : " + args[curArg]);
+                }
+
+                remainingArgs = new String[args.length - curArg];
+                System.arraycopy(args, curArg, remainingArgs, 0, remainingArgs.length);
+            }
+        }
+    }
+
+    /**
+     * Reads an argument list from the command socket/
+     * @return Argument list or null if EOF is reached
+     * @throws IOException passed straight through
+     */
+    private String[] readArgumentList()
+            throws IOException {
+
+        /**
+         * See android.os.Process.zygoteSendArgsAndGetPid()
+         * Presently the wire format to the zygote process is:
+         * a) a count of arguments (argc, in essence)
+         * b) a number of newline-separated argument strings equal to count
+         *
+         * After the zygote process reads these it will write the pid of
+         * the child or -1 on failure.
+         */
+
+        int argc;
+
+        try {
+            String s = mSocketReader.readLine();
+
+            if (s == null) {
+                // EOF reached.
+                return null;
+            }
+            argc = Integer.parseInt(s);
+        } catch (NumberFormatException ex) {
+            Log.e(TAG, "invalid Zygote wire format: non-int at argc");
+            throw new IOException("invalid wire format");
+        }
+
+        // See bug 1092107: large argc can be used for a DOS attack
+        if (argc > MAX_ZYGOTE_ARGC) {
+            throw new IOException("max arg count exceeded");
+        }
+
+        String[] result = new String[argc];
+        for (int i = 0; i < argc; i++) {
+            result[i] = mSocketReader.readLine();
+            if (result[i] == null) {
+                // We got an unexpected EOF.
+                throw new IOException("truncated request");
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * uid 1000 (Process.SYSTEM_UID) may specify any uid &gt; 1000 in normal
+     * operation. It may also specify any gid and setgroups() list it chooses.
+     * In factory test mode, it may specify any UID.
+     *
+     * @param args non-null; zygote spawner arguments
+     * @param peer non-null; peer credentials
+     * @throws ZygoteSecurityException
+     */
+    private static void applyUidSecurityPolicy(Arguments args, Credentials peer)
+            throws ZygoteSecurityException {
+
+        if (peer.getUid() == Process.SYSTEM_UID) {
+            /* In normal operation, SYSTEM_UID can only specify a restricted
+             * set of UIDs. In factory test mode, SYSTEM_UID may specify any uid.
+             */
+            boolean uidRestricted = FactoryTest.getMode() == FactoryTest.FACTORY_TEST_OFF;
+
+            if (uidRestricted && args.uidSpecified && (args.uid < Process.SYSTEM_UID)) {
+                throw new ZygoteSecurityException(
+                        "System UID may not launch process with UID < "
+                        + Process.SYSTEM_UID);
+            }
+        }
+
+        // If not otherwise specified, uid and gid are inherited from peer
+        if (!args.uidSpecified) {
+            args.uid = peer.getUid();
+            args.uidSpecified = true;
+        }
+        if (!args.gidSpecified) {
+            args.gid = peer.getGid();
+            args.gidSpecified = true;
+        }
+    }
+
+    /**
+     * Applies debugger system properties to the zygote arguments.
+     *
+     * If "ro.debuggable" is "1", all apps are debuggable. Otherwise,
+     * the debugger state is specified via the "--enable-jdwp" flag
+     * in the spawn request.
+     *
+     * @param args non-null; zygote spawner args
+     */
+    public static void applyDebuggerSystemProperty(Arguments args) {
+        if (RoSystemProperties.DEBUGGABLE) {
+            args.debugFlags |= Zygote.DEBUG_ENABLE_JDWP;
+        }
+    }
+
+    /**
+     * Applies zygote security policy.
+     * Based on the credentials of the process issuing a zygote command:
+     * <ol>
+     * <li> uid 0 (root) may specify --invoke-with to launch Zygote with a
+     * wrapper command.
+     * <li> Any other uid may not specify any invoke-with argument.
+     * </ul>
+     *
+     * @param args non-null; zygote spawner arguments
+     * @param peer non-null; peer credentials
+     * @throws ZygoteSecurityException
+     */
+    private static void applyInvokeWithSecurityPolicy(Arguments args, Credentials peer)
+            throws ZygoteSecurityException {
+        int peerUid = peer.getUid();
+
+        if (args.invokeWith != null && peerUid != 0 &&
+            (args.debugFlags & Zygote.DEBUG_ENABLE_JDWP) == 0) {
+            throw new ZygoteSecurityException("Peer is permitted to specify an"
+                    + "explicit invoke-with wrapper command only for debuggable"
+                    + "applications.");
+        }
+    }
+
+    /**
+     * Applies invoke-with system properties to the zygote arguments.
+     *
+     * @param args non-null; zygote args
+     */
+    public static void applyInvokeWithSystemProperty(Arguments args) {
+        if (args.invokeWith == null && args.niceName != null) {
+            String property = "wrap." + args.niceName;
+            args.invokeWith = SystemProperties.get(property);
+            if (args.invokeWith != null && args.invokeWith.length() == 0) {
+                args.invokeWith = null;
+            }
+        }
+    }
+
+    /**
+     * Handles post-fork setup of child proc, closing sockets as appropriate,
+     * reopen stdio as appropriate, and ultimately throwing MethodAndArgsCaller
+     * if successful or returning if failed.
+     *
+     * @param parsedArgs non-null; zygote args
+     * @param descriptors null-ok; new file descriptors for stdio if available.
+     * @param pipeFd null-ok; pipe for communication back to Zygote.
+     */
+    private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,
+            FileDescriptor pipeFd) {
+        /**
+         * By the time we get here, the native code has closed the two actual Zygote
+         * socket connections, and substituted /dev/null in their place.  The LocalSocket
+         * objects still need to be closed properly.
+         */
+
+        closeSocket();
+        if (descriptors != null) {
+            try {
+                Os.dup2(descriptors[0], STDIN_FILENO);
+                Os.dup2(descriptors[1], STDOUT_FILENO);
+                Os.dup2(descriptors[2], STDERR_FILENO);
+
+                for (FileDescriptor fd: descriptors) {
+                    IoUtils.closeQuietly(fd);
+                }
+            } catch (ErrnoException ex) {
+                Log.e(TAG, "Error reopening stdio", ex);
+            }
+        }
+
+        if (parsedArgs.niceName != null) {
+            Process.setArgV0(parsedArgs.niceName);
+        }
+
+        // End of the postFork event.
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+        if (parsedArgs.invokeWith != null) {
+            WrapperInit.execApplication(parsedArgs.invokeWith,
+                    parsedArgs.niceName, parsedArgs.targetSdkVersion,
+                    VMRuntime.getCurrentInstructionSet(),
+                    pipeFd, parsedArgs.remainingArgs);
+
+            // Should not get here.
+            throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
+        } else {
+            return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,
+                    null /* classLoader */);
+        }
+    }
+
+    /**
+     * Handles post-fork cleanup of parent proc
+     *
+     * @param pid != 0; pid of child if &gt; 0 or indication of failed fork
+     * if &lt; 0;
+     * @param descriptors null-ok; file descriptors for child's new stdio if
+     * specified.
+     * @param pipeFd null-ok; pipe for communication with child.
+     */
+    private void handleParentProc(int pid, FileDescriptor[] descriptors, FileDescriptor pipeFd) {
+        if (pid > 0) {
+            setChildPgid(pid);
+        }
+
+        if (descriptors != null) {
+            for (FileDescriptor fd: descriptors) {
+                IoUtils.closeQuietly(fd);
+            }
+        }
+
+        boolean usingWrapper = false;
+        if (pipeFd != null && pid > 0) {
+            int innerPid = -1;
+            try {
+                // Do a busy loop here. We can't guarantee that a failure (and thus an exception
+                // bail) happens in a timely manner.
+                final int BYTES_REQUIRED = 4;  // Bytes in an int.
+
+                StructPollfd fds[] = new StructPollfd[] {
+                        new StructPollfd()
+                };
+
+                byte data[] = new byte[BYTES_REQUIRED];
+
+                int remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS;
+                int dataIndex = 0;
+                long startTime = System.nanoTime();
+
+                while (dataIndex < data.length && remainingSleepTime > 0) {
+                    fds[0].fd = pipeFd;
+                    fds[0].events = (short) POLLIN;
+                    fds[0].revents = 0;
+                    fds[0].userData = null;
+
+                    int res = android.system.Os.poll(fds, remainingSleepTime);
+                    long endTime = System.nanoTime();
+                    int elapsedTimeMs = (int)((endTime - startTime) / 1000000l);
+                    remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS - elapsedTimeMs;
+
+                    if (res > 0) {
+                        if ((fds[0].revents & POLLIN) != 0) {
+                            // Only read one byte, so as not to block.
+                            int readBytes = android.system.Os.read(pipeFd, data, dataIndex, 1);
+                            if (readBytes < 0) {
+                                throw new RuntimeException("Some error");
+                            }
+                            dataIndex += readBytes;
+                        } else {
+                            // Error case. revents should contain one of the error bits.
+                            break;
+                        }
+                    } else if (res == 0) {
+                        Log.w(TAG, "Timed out waiting for child.");
+                    }
+                }
+
+                if (dataIndex == data.length) {
+                    DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
+                    innerPid = is.readInt();
+                }
+
+                if (innerPid == -1) {
+                    Log.w(TAG, "Error reading pid from wrapped process, child may have died");
+                }
+            } catch (Exception ex) {
+                Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex);
+            }
+
+            // Ensure that the pid reported by the wrapped process is either the
+            // child process that we forked, or a descendant of it.
+            if (innerPid > 0) {
+                int parentPid = innerPid;
+                while (parentPid > 0 && parentPid != pid) {
+                    parentPid = Process.getParentPid(parentPid);
+                }
+                if (parentPid > 0) {
+                    Log.i(TAG, "Wrapped process has pid " + innerPid);
+                    pid = innerPid;
+                    usingWrapper = true;
+                } else {
+                    Log.w(TAG, "Wrapped process reported a pid that is not a child of "
+                            + "the process that we forked: childPid=" + pid
+                            + " innerPid=" + innerPid);
+                }
+            }
+        }
+
+        try {
+            mSocketOutStream.writeInt(pid);
+            mSocketOutStream.writeBoolean(usingWrapper);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Error writing to command socket", ex);
+        }
+    }
+
+    private void setChildPgid(int pid) {
+        // Try to move the new child into the peer's process group.
+        try {
+            Os.setpgid(pid, Os.getpgid(peer.getPid()));
+        } catch (ErrnoException ex) {
+            // This exception is expected in the case where
+            // the peer is not in our session
+            // TODO get rid of this log message in the case where
+            // getsid(0) != getsid(peer.getPid())
+            Log.i(TAG, "Zygote: setpgid failed. This is "
+                + "normal if peer is not in our session");
+        }
+    }
+}
diff --git a/com/android/internal/os/ZygoteConnectionConstants.java b/com/android/internal/os/ZygoteConnectionConstants.java
new file mode 100644
index 0000000..506e39f
--- /dev/null
+++ b/com/android/internal/os/ZygoteConnectionConstants.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 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.internal.os;
+
+/**
+ * Sharable zygote constants.
+ *
+ * @hide
+ */
+public class ZygoteConnectionConstants {
+    /**
+     * {@link android.net.LocalSocket#setSoTimeout} value for connections.
+     * Effectively, the amount of time a requestor has between the start of
+     * the request and the completed request. The select-loop mode Zygote
+     * doesn't have the logic to return to the select loop in the middle of
+     * a request, so we need to time out here to avoid being denial-of-serviced.
+     */
+    public static final int CONNECTION_TIMEOUT_MILLIS = 1000;
+
+    /** max number of arguments that a connection can specify */
+    public static final int MAX_ZYGOTE_ARGC = 1024;
+
+    /**
+     * Wait time for a wrapped app to report back its pid.
+     *
+     * We'll wait up to thirty seconds. This should give enough time for the fork
+     * to go through, but not to trigger the watchdog in the system server (by default
+     * sixty seconds).
+     *
+     * WARNING: This may trigger the watchdog in debug mode. However, to support
+     *          wrapping on lower-end devices we do not have much choice.
+     */
+    public static final int WRAPPED_PID_TIMEOUT_MILLIS = 30000;
+}
diff --git a/com/android/internal/os/ZygoteInit.java b/com/android/internal/os/ZygoteInit.java
new file mode 100644
index 0000000..7058193
--- /dev/null
+++ b/com/android/internal/os/ZygoteInit.java
@@ -0,0 +1,874 @@
+/*
+ * Copyright (C) 2007 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.internal.os;
+
+import static android.system.OsConstants.S_IRWXG;
+import static android.system.OsConstants.S_IRWXO;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.icu.impl.CacheValue;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.util.ULocale;
+import android.opengl.EGL14;
+import android.os.Build;
+import android.os.IInstalld;
+import android.os.Environment;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.Seccomp;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.ZygoteProcess;
+import android.os.storage.StorageManager;
+import android.security.keystore.AndroidKeyStoreProvider;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructCapUserData;
+import android.system.StructCapUserHeader;
+import android.text.Hyphenator;
+import android.util.TimingsTraceLog;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Slog;
+import android.webkit.WebViewFactory;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+
+import com.android.internal.util.Preconditions;
+import dalvik.system.DexFile;
+import dalvik.system.VMRuntime;
+import dalvik.system.ZygoteHooks;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.Security;
+import java.security.Provider;
+
+/**
+ * Startup class for the zygote process.
+ *
+ * Pre-initializes some classes, and then waits for commands on a UNIX domain
+ * socket. Based on these commands, forks off child processes that inherit
+ * the initial state of the VM.
+ *
+ * Please see {@link ZygoteConnection.Arguments} for documentation on the
+ * client protocol.
+ *
+ * @hide
+ */
+public class ZygoteInit {
+    private static final String TAG = "Zygote";
+
+    private static final String PROPERTY_DISABLE_OPENGL_PRELOADING = "ro.zygote.disable_gl_preload";
+    private static final String PROPERTY_GFX_DRIVER = "ro.gfx.driver.0";
+
+    private static final int LOG_BOOT_PROGRESS_PRELOAD_START = 3020;
+    private static final int LOG_BOOT_PROGRESS_PRELOAD_END = 3030;
+
+    /** when preloading, GC after allocating this many bytes */
+    private static final int PRELOAD_GC_THRESHOLD = 50000;
+
+    private static final String ABI_LIST_ARG = "--abi-list=";
+
+    private static final String SOCKET_NAME_ARG = "--socket-name=";
+
+    /**
+     * Used to pre-load resources.
+     */
+    private static Resources mResources;
+
+    /**
+     * The path of a file that contains classes to preload.
+     */
+    private static final String PRELOADED_CLASSES = "/system/etc/preloaded-classes";
+
+    /** Controls whether we should preload resources during zygote init. */
+    public static final boolean PRELOAD_RESOURCES = true;
+
+    private static final int UNPRIVILEGED_UID = 9999;
+    private static final int UNPRIVILEGED_GID = 9999;
+
+    private static final int ROOT_UID = 0;
+    private static final int ROOT_GID = 0;
+
+    private static boolean sPreloadComplete;
+
+    static void preload(TimingsTraceLog bootTimingsTraceLog) {
+        Log.d(TAG, "begin preload");
+        bootTimingsTraceLog.traceBegin("BeginIcuCachePinning");
+        beginIcuCachePinning();
+        bootTimingsTraceLog.traceEnd(); // BeginIcuCachePinning
+        bootTimingsTraceLog.traceBegin("PreloadClasses");
+        preloadClasses();
+        bootTimingsTraceLog.traceEnd(); // PreloadClasses
+        bootTimingsTraceLog.traceBegin("PreloadResources");
+        preloadResources();
+        bootTimingsTraceLog.traceEnd(); // PreloadResources
+        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadAppProcessHALs");
+        nativePreloadAppProcessHALs();
+        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
+        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadOpenGL");
+        preloadOpenGL();
+        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
+        preloadSharedLibraries();
+        preloadTextResources();
+        // Ask the WebViewFactory to do any initialization that must run in the zygote process,
+        // for memory sharing purposes.
+        WebViewFactory.prepareWebViewInZygote();
+        endIcuCachePinning();
+        warmUpJcaProviders();
+        Log.d(TAG, "end preload");
+
+        sPreloadComplete = true;
+    }
+
+    public static void lazyPreload() {
+        Preconditions.checkState(!sPreloadComplete);
+        Log.i(TAG, "Lazily preloading resources.");
+
+        preload(new TimingsTraceLog("ZygoteInitTiming_lazy", Trace.TRACE_TAG_DALVIK));
+    }
+
+    private static void beginIcuCachePinning() {
+        // Pin ICU data in memory from this point that would normally be held by soft references.
+        // Without this, any references created immediately below or during class preloading
+        // would be collected when the Zygote GC runs in gcAndFinalize().
+        Log.i(TAG, "Installing ICU cache reference pinning...");
+
+        CacheValue.setStrength(CacheValue.Strength.STRONG);
+
+        Log.i(TAG, "Preloading ICU data...");
+        // Explicitly exercise code to cache data apps are likely to need.
+        ULocale[] localesToPin = { ULocale.ROOT, ULocale.US, ULocale.getDefault() };
+        for (ULocale uLocale : localesToPin) {
+            new DecimalFormatSymbols(uLocale);
+        }
+    }
+
+    private static void endIcuCachePinning() {
+        // All cache references created by ICU from this point will be soft.
+        CacheValue.setStrength(CacheValue.Strength.SOFT);
+
+        Log.i(TAG, "Uninstalled ICU cache reference pinning...");
+    }
+
+    private static void preloadSharedLibraries() {
+        Log.i(TAG, "Preloading shared libraries...");
+        System.loadLibrary("android");
+        System.loadLibrary("compiler_rt");
+        System.loadLibrary("jnigraphics");
+    }
+
+    native private static void nativePreloadAppProcessHALs();
+
+    private static void preloadOpenGL() {
+        String driverPackageName = SystemProperties.get(PROPERTY_GFX_DRIVER);
+        if (!SystemProperties.getBoolean(PROPERTY_DISABLE_OPENGL_PRELOADING, false) &&
+                (driverPackageName == null || driverPackageName.isEmpty())) {
+            EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+        }
+    }
+
+    private static void preloadTextResources() {
+        Hyphenator.init();
+        TextView.preloadFontCache();
+    }
+
+    /**
+     * Register AndroidKeyStoreProvider and warm up the providers that are already registered.
+     *
+     * By doing it here we avoid that each app does it when requesting a service from the
+     * provider for the first time.
+     */
+    private static void warmUpJcaProviders() {
+        long startTime = SystemClock.uptimeMillis();
+        Trace.traceBegin(
+                Trace.TRACE_TAG_DALVIK, "Starting installation of AndroidKeyStoreProvider");
+        // AndroidKeyStoreProvider.install() manipulates the list of JCA providers to insert
+        // preferred providers. Note this is not done via security.properties as the JCA providers
+        // are not on the classpath in the case of, for example, raw dalvikvm runtimes.
+        AndroidKeyStoreProvider.install();
+        Log.i(TAG, "Installed AndroidKeyStoreProvider in "
+                + (SystemClock.uptimeMillis() - startTime) + "ms.");
+        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
+
+        startTime = SystemClock.uptimeMillis();
+        Trace.traceBegin(
+                Trace.TRACE_TAG_DALVIK, "Starting warm up of JCA providers");
+        for (Provider p : Security.getProviders()) {
+            p.warmUpServiceProvision();
+        }
+        Log.i(TAG, "Warmed up JCA providers in "
+                + (SystemClock.uptimeMillis() - startTime) + "ms.");
+        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
+    }
+
+    /**
+     * Performs Zygote process initialization. Loads and initializes
+     * commonly used classes.
+     *
+     * Most classes only cause a few hundred bytes to be allocated, but
+     * a few will allocate a dozen Kbytes (in one case, 500+K).
+     */
+    private static void preloadClasses() {
+        final VMRuntime runtime = VMRuntime.getRuntime();
+
+        InputStream is;
+        try {
+            is = new FileInputStream(PRELOADED_CLASSES);
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
+            return;
+        }
+
+        Log.i(TAG, "Preloading classes...");
+        long startTime = SystemClock.uptimeMillis();
+
+        // Drop root perms while running static initializers.
+        final int reuid = Os.getuid();
+        final int regid = Os.getgid();
+
+        // We need to drop root perms only if we're already root. In the case of "wrapped"
+        // processes (see WrapperInit), this function is called from an unprivileged uid
+        // and gid.
+        boolean droppedPriviliges = false;
+        if (reuid == ROOT_UID && regid == ROOT_GID) {
+            try {
+                Os.setregid(ROOT_GID, UNPRIVILEGED_GID);
+                Os.setreuid(ROOT_UID, UNPRIVILEGED_UID);
+            } catch (ErrnoException ex) {
+                throw new RuntimeException("Failed to drop root", ex);
+            }
+
+            droppedPriviliges = true;
+        }
+
+        // Alter the target heap utilization.  With explicit GCs this
+        // is not likely to have any effect.
+        float defaultUtilization = runtime.getTargetHeapUtilization();
+        runtime.setTargetHeapUtilization(0.8f);
+
+        try {
+            BufferedReader br
+                = new BufferedReader(new InputStreamReader(is), 256);
+
+            int count = 0;
+            String line;
+            while ((line = br.readLine()) != null) {
+                // Skip comments and blank lines.
+                line = line.trim();
+                if (line.startsWith("#") || line.equals("")) {
+                    continue;
+                }
+
+                Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
+                try {
+                    if (false) {
+                        Log.v(TAG, "Preloading " + line + "...");
+                    }
+                    // Load and explicitly initialize the given class. Use
+                    // Class.forName(String, boolean, ClassLoader) to avoid repeated stack lookups
+                    // (to derive the caller's class-loader). Use true to force initialization, and
+                    // null for the boot classpath class-loader (could as well cache the
+                    // class-loader of this class in a variable).
+                    Class.forName(line, true, null);
+                    count++;
+                } catch (ClassNotFoundException e) {
+                    Log.w(TAG, "Class not found for preloading: " + line);
+                } catch (UnsatisfiedLinkError e) {
+                    Log.w(TAG, "Problem preloading " + line + ": " + e);
+                } catch (Throwable t) {
+                    Log.e(TAG, "Error preloading " + line + ".", t);
+                    if (t instanceof Error) {
+                        throw (Error) t;
+                    }
+                    if (t instanceof RuntimeException) {
+                        throw (RuntimeException) t;
+                    }
+                    throw new RuntimeException(t);
+                }
+                Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
+            }
+
+            Log.i(TAG, "...preloaded " + count + " classes in "
+                    + (SystemClock.uptimeMillis()-startTime) + "ms.");
+        } catch (IOException e) {
+            Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
+        } finally {
+            IoUtils.closeQuietly(is);
+            // Restore default.
+            runtime.setTargetHeapUtilization(defaultUtilization);
+
+            // Fill in dex caches with classes, fields, and methods brought in by preloading.
+            Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadDexCaches");
+            runtime.preloadDexCaches();
+            Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
+
+            // Bring back root. We'll need it later if we're in the zygote.
+            if (droppedPriviliges) {
+                try {
+                    Os.setreuid(ROOT_UID, ROOT_UID);
+                    Os.setregid(ROOT_GID, ROOT_GID);
+                } catch (ErrnoException ex) {
+                    throw new RuntimeException("Failed to restore root", ex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Load in commonly used resources, so they can be shared across
+     * processes.
+     *
+     * These tend to be a few Kbytes, but are frequently in the 20-40K
+     * range, and occasionally even larger.
+     */
+    private static void preloadResources() {
+        final VMRuntime runtime = VMRuntime.getRuntime();
+
+        try {
+            mResources = Resources.getSystem();
+            mResources.startPreloading();
+            if (PRELOAD_RESOURCES) {
+                Log.i(TAG, "Preloading resources...");
+
+                long startTime = SystemClock.uptimeMillis();
+                TypedArray ar = mResources.obtainTypedArray(
+                        com.android.internal.R.array.preloaded_drawables);
+                int N = preloadDrawables(ar);
+                ar.recycle();
+                Log.i(TAG, "...preloaded " + N + " resources in "
+                        + (SystemClock.uptimeMillis()-startTime) + "ms.");
+
+                startTime = SystemClock.uptimeMillis();
+                ar = mResources.obtainTypedArray(
+                        com.android.internal.R.array.preloaded_color_state_lists);
+                N = preloadColorStateLists(ar);
+                ar.recycle();
+                Log.i(TAG, "...preloaded " + N + " resources in "
+                        + (SystemClock.uptimeMillis()-startTime) + "ms.");
+
+                if (mResources.getBoolean(
+                        com.android.internal.R.bool.config_freeformWindowManagement)) {
+                    startTime = SystemClock.uptimeMillis();
+                    ar = mResources.obtainTypedArray(
+                            com.android.internal.R.array.preloaded_freeform_multi_window_drawables);
+                    N = preloadDrawables(ar);
+                    ar.recycle();
+                    Log.i(TAG, "...preloaded " + N + " resource in "
+                            + (SystemClock.uptimeMillis() - startTime) + "ms.");
+                }
+            }
+            mResources.finishPreloading();
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Failure preloading resources", e);
+        }
+    }
+
+    private static int preloadColorStateLists(TypedArray ar) {
+        int N = ar.length();
+        for (int i=0; i<N; i++) {
+            int id = ar.getResourceId(i, 0);
+            if (false) {
+                Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
+            }
+            if (id != 0) {
+                if (mResources.getColorStateList(id, null) == null) {
+                    throw new IllegalArgumentException(
+                            "Unable to find preloaded color resource #0x"
+                            + Integer.toHexString(id)
+                            + " (" + ar.getString(i) + ")");
+                }
+            }
+        }
+        return N;
+    }
+
+
+    private static int preloadDrawables(TypedArray ar) {
+        int N = ar.length();
+        for (int i=0; i<N; i++) {
+            int id = ar.getResourceId(i, 0);
+            if (false) {
+                Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
+            }
+            if (id != 0) {
+                if (mResources.getDrawable(id, null) == null) {
+                    throw new IllegalArgumentException(
+                            "Unable to find preloaded drawable resource #0x"
+                            + Integer.toHexString(id)
+                            + " (" + ar.getString(i) + ")");
+                }
+            }
+        }
+        return N;
+    }
+
+    /**
+     * Runs several special GCs to try to clean up a few generations of
+     * softly- and final-reachable objects, along with any other garbage.
+     * This is only useful just before a fork().
+     */
+    /*package*/ static void gcAndFinalize() {
+        final VMRuntime runtime = VMRuntime.getRuntime();
+
+        /* runFinalizationSync() lets finalizers be called in Zygote,
+         * which doesn't have a HeapWorker thread.
+         */
+        System.gc();
+        runtime.runFinalizationSync();
+        System.gc();
+    }
+
+    /**
+     * Finish remaining work for the newly forked system server process.
+     */
+    private static Runnable handleSystemServerProcess(ZygoteConnection.Arguments parsedArgs) {
+        // set umask to 0077 so new files and directories will default to owner-only permissions.
+        Os.umask(S_IRWXG | S_IRWXO);
+
+        if (parsedArgs.niceName != null) {
+            Process.setArgV0(parsedArgs.niceName);
+        }
+
+        final String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH");
+        if (systemServerClasspath != null) {
+            performSystemServerDexOpt(systemServerClasspath);
+            // Capturing profiles is only supported for debug or eng builds since selinux normally
+            // prevents it.
+            boolean profileSystemServer = SystemProperties.getBoolean(
+                    "dalvik.vm.profilesystemserver", false);
+            if (profileSystemServer && (Build.IS_USERDEBUG || Build.IS_ENG)) {
+                try {
+                    File profileDir = Environment.getDataProfilesDePackageDirectory(
+                            Process.SYSTEM_UID, "system_server");
+                    File profile = new File(profileDir, "primary.prof");
+                    profile.getParentFile().mkdirs();
+                    profile.createNewFile();
+                    String[] codePaths = systemServerClasspath.split(":");
+                    VMRuntime.registerAppInfo(profile.getPath(), codePaths);
+                } catch (Exception e) {
+                    Log.wtf(TAG, "Failed to set up system server profile", e);
+                }
+            }
+        }
+
+        if (parsedArgs.invokeWith != null) {
+            String[] args = parsedArgs.remainingArgs;
+            // If we have a non-null system server class path, we'll have to duplicate the
+            // existing arguments and append the classpath to it. ART will handle the classpath
+            // correctly when we exec a new process.
+            if (systemServerClasspath != null) {
+                String[] amendedArgs = new String[args.length + 2];
+                amendedArgs[0] = "-cp";
+                amendedArgs[1] = systemServerClasspath;
+                System.arraycopy(args, 0, amendedArgs, 2, args.length);
+                args = amendedArgs;
+            }
+
+            WrapperInit.execApplication(parsedArgs.invokeWith,
+                    parsedArgs.niceName, parsedArgs.targetSdkVersion,
+                    VMRuntime.getCurrentInstructionSet(), null, args);
+
+            throw new IllegalStateException("Unexpected return from WrapperInit.execApplication");
+        } else {
+            ClassLoader cl = null;
+            if (systemServerClasspath != null) {
+                cl = createPathClassLoader(systemServerClasspath, parsedArgs.targetSdkVersion);
+
+                Thread.currentThread().setContextClassLoader(cl);
+            }
+
+            /*
+             * Pass the remaining arguments to SystemServer.
+             */
+            return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
+        }
+
+        /* should never reach here */
+    }
+
+    /**
+     * Creates a PathClassLoader for the given class path that is associated with a shared
+     * namespace, i.e., this classloader can access platform-private native libraries. The
+     * classloader will use java.library.path as the native library path.
+     */
+    static ClassLoader createPathClassLoader(String classPath, int targetSdkVersion) {
+        String libraryPath = System.getProperty("java.library.path");
+
+        return ClassLoaderFactory.createClassLoader(classPath, libraryPath, libraryPath,
+                ClassLoader.getSystemClassLoader(), targetSdkVersion, true /* isNamespaceShared */,
+                null /* classLoaderName */);
+    }
+
+    /**
+     * Performs dex-opt on the elements of {@code classPath}, if needed. We
+     * choose the instruction set of the current runtime.
+     */
+    private static void performSystemServerDexOpt(String classPath) {
+        final String[] classPathElements = classPath.split(":");
+        final IInstalld installd = IInstalld.Stub
+                .asInterface(ServiceManager.getService("installd"));
+        final String instructionSet = VMRuntime.getRuntime().vmInstructionSet();
+
+        String classPathForElement = "";
+        for (String classPathElement : classPathElements) {
+            // System server is fully AOTed and never profiled
+            // for profile guided compilation.
+            String systemServerFilter = SystemProperties.get(
+                    "dalvik.vm.systemservercompilerfilter", "speed");
+
+            int dexoptNeeded;
+            try {
+                dexoptNeeded = DexFile.getDexOptNeeded(
+                    classPathElement, instructionSet, systemServerFilter,
+                    false /* newProfile */, false /* downgrade */);
+            } catch (FileNotFoundException ignored) {
+                // Do not add to the classpath.
+                Log.w(TAG, "Missing classpath element for system server: " + classPathElement);
+                continue;
+            } catch (IOException e) {
+                // Not fully clear what to do here as we don't know the cause of the
+                // IO exception. Add to the classpath to be conservative, but don't
+                // attempt to compile it.
+                Log.w(TAG, "Error checking classpath element for system server: "
+                        + classPathElement, e);
+                dexoptNeeded = DexFile.NO_DEXOPT_NEEDED;
+            }
+
+            if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) {
+                final String packageName = "*";
+                final String outputPath = null;
+                final int dexFlags = 0;
+                final String compilerFilter = systemServerFilter;
+                final String uuid = StorageManager.UUID_PRIVATE_INTERNAL;
+                final String seInfo = null;
+                final String classLoaderContext =
+                        getSystemServerClassLoaderContext(classPathForElement);
+                try {
+                    installd.dexopt(classPathElement, Process.SYSTEM_UID, packageName,
+                            instructionSet, dexoptNeeded, outputPath, dexFlags, compilerFilter,
+                            uuid, classLoaderContext, seInfo, false /* downgrade */);
+                } catch (RemoteException | ServiceSpecificException e) {
+                    // Ignore (but log), we need this on the classpath for fallback mode.
+                    Log.w(TAG, "Failed compiling classpath element for system server: "
+                            + classPathElement, e);
+                }
+            }
+
+            classPathForElement = encodeSystemServerClassPath(
+                    classPathForElement, classPathElement);
+        }
+    }
+
+    /**
+     * Encodes the system server class loader context in a format that is accepted by dexopt.
+     * This assumes the system server is always loaded with a {@link dalvik.system.PathClassLoader}.
+     *
+     * Note that ideally we would use the {@code DexoptUtils} to compute this. However we have no
+     * dependency here on the server so we hard code the logic again.
+     */
+    private static String getSystemServerClassLoaderContext(String classPath) {
+        return classPath == null ? "PCL[]" : "PCL[" + classPath + "]";
+    }
+
+    /**
+     * Encodes the class path in a format accepted by dexopt.
+     * @param classPath the old class path (may be empty).
+     * @param newElement the new class path elements
+     * @return the class path encoding resulted from appending {@code newElement} to
+     * {@code classPath}.
+     */
+    private static String encodeSystemServerClassPath(String classPath, String newElement) {
+        return (classPath == null || classPath.isEmpty())
+                ? newElement
+                : classPath + ":" + newElement;
+    }
+
+    /**
+     * Prepare the arguments and forks for the system server process.
+     *
+     * Returns an {@code Runnable} that provides an entrypoint into system_server code in the
+     * child process, and {@code null} in the parent.
+     */
+    private static Runnable forkSystemServer(String abiList, String socketName,
+            ZygoteServer zygoteServer) {
+        long capabilities = posixCapabilitiesAsBits(
+            OsConstants.CAP_IPC_LOCK,
+            OsConstants.CAP_KILL,
+            OsConstants.CAP_NET_ADMIN,
+            OsConstants.CAP_NET_BIND_SERVICE,
+            OsConstants.CAP_NET_BROADCAST,
+            OsConstants.CAP_NET_RAW,
+            OsConstants.CAP_SYS_MODULE,
+            OsConstants.CAP_SYS_NICE,
+            OsConstants.CAP_SYS_PTRACE,
+            OsConstants.CAP_SYS_TIME,
+            OsConstants.CAP_SYS_TTY_CONFIG,
+            OsConstants.CAP_WAKE_ALARM,
+            OsConstants.CAP_BLOCK_SUSPEND
+        );
+        /* Containers run without some capabilities, so drop any caps that are not available. */
+        StructCapUserHeader header = new StructCapUserHeader(
+                OsConstants._LINUX_CAPABILITY_VERSION_3, 0);
+        StructCapUserData[] data;
+        try {
+            data = Os.capget(header);
+        } catch (ErrnoException ex) {
+            throw new RuntimeException("Failed to capget()", ex);
+        }
+        capabilities &= ((long) data[0].effective) | (((long) data[1].effective) << 32);
+
+        /* Hardcoded command line to start the system server */
+        String args[] = {
+            "--setuid=1000",
+            "--setgid=1000",
+            "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1032,3001,3002,3003,3006,3007,3009,3010",
+            "--capabilities=" + capabilities + "," + capabilities,
+            "--nice-name=system_server",
+            "--runtime-args",
+            "com.android.server.SystemServer",
+        };
+        ZygoteConnection.Arguments parsedArgs = null;
+
+        int pid;
+
+        try {
+            parsedArgs = new ZygoteConnection.Arguments(args);
+            ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
+            ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);
+
+            /* Request to fork the system server process */
+            pid = Zygote.forkSystemServer(
+                    parsedArgs.uid, parsedArgs.gid,
+                    parsedArgs.gids,
+                    parsedArgs.debugFlags,
+                    null,
+                    parsedArgs.permittedCapabilities,
+                    parsedArgs.effectiveCapabilities);
+        } catch (IllegalArgumentException ex) {
+            throw new RuntimeException(ex);
+        }
+
+        /* For child process */
+        if (pid == 0) {
+            if (hasSecondZygote(abiList)) {
+                waitForSecondaryZygote(socketName);
+            }
+
+            zygoteServer.closeServerSocket();
+            return handleSystemServerProcess(parsedArgs);
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the bit array representation of the provided list of POSIX capabilities.
+     */
+    private static long posixCapabilitiesAsBits(int... capabilities) {
+        long result = 0;
+        for (int capability : capabilities) {
+            if ((capability < 0) || (capability > OsConstants.CAP_LAST_CAP)) {
+                throw new IllegalArgumentException(String.valueOf(capability));
+            }
+            result |= (1L << capability);
+        }
+        return result;
+    }
+
+    public static void main(String argv[]) {
+        ZygoteServer zygoteServer = new ZygoteServer();
+
+        // Mark zygote start. This ensures that thread creation will throw
+        // an error.
+        ZygoteHooks.startZygoteNoThreadCreation();
+
+        // Zygote goes into its own process group.
+        try {
+            Os.setpgid(0, 0);
+        } catch (ErrnoException ex) {
+            throw new RuntimeException("Failed to setpgid(0,0)", ex);
+        }
+
+        final Runnable caller;
+        try {
+            // Report Zygote start time to tron unless it is a runtime restart
+            if (!"1".equals(SystemProperties.get("sys.boot_completed"))) {
+                MetricsLogger.histogram(null, "boot_zygote_init",
+                        (int) SystemClock.elapsedRealtime());
+            }
+
+            String bootTimeTag = Process.is64Bit() ? "Zygote64Timing" : "Zygote32Timing";
+            TimingsTraceLog bootTimingsTraceLog = new TimingsTraceLog(bootTimeTag,
+                    Trace.TRACE_TAG_DALVIK);
+            bootTimingsTraceLog.traceBegin("ZygoteInit");
+            RuntimeInit.enableDdms();
+
+            boolean startSystemServer = false;
+            String socketName = "zygote";
+            String abiList = null;
+            boolean enableLazyPreload = false;
+            for (int i = 1; i < argv.length; i++) {
+                if ("start-system-server".equals(argv[i])) {
+                    startSystemServer = true;
+                } else if ("--enable-lazy-preload".equals(argv[i])) {
+                    enableLazyPreload = true;
+                } else if (argv[i].startsWith(ABI_LIST_ARG)) {
+                    abiList = argv[i].substring(ABI_LIST_ARG.length());
+                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
+                    socketName = argv[i].substring(SOCKET_NAME_ARG.length());
+                } else {
+                    throw new RuntimeException("Unknown command line argument: " + argv[i]);
+                }
+            }
+
+            if (abiList == null) {
+                throw new RuntimeException("No ABI list supplied.");
+            }
+
+            zygoteServer.registerServerSocket(socketName);
+            // In some configurations, we avoid preloading resources and classes eagerly.
+            // In such cases, we will preload things prior to our first fork.
+            if (!enableLazyPreload) {
+                bootTimingsTraceLog.traceBegin("ZygotePreload");
+                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
+                    SystemClock.uptimeMillis());
+                preload(bootTimingsTraceLog);
+                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
+                    SystemClock.uptimeMillis());
+                bootTimingsTraceLog.traceEnd(); // ZygotePreload
+            } else {
+                Zygote.resetNicePriority();
+            }
+
+            // Do an initial gc to clean up after startup
+            bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
+            gcAndFinalize();
+            bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC
+
+            bootTimingsTraceLog.traceEnd(); // ZygoteInit
+            // Disable tracing so that forked processes do not inherit stale tracing tags from
+            // Zygote.
+            Trace.setTracingEnabled(false, 0);
+
+            // Zygote process unmounts root storage spaces.
+            Zygote.nativeUnmountStorageOnInit();
+
+            // Set seccomp policy
+            Seccomp.setPolicy();
+
+            ZygoteHooks.stopZygoteNoThreadCreation();
+
+            if (startSystemServer) {
+                Runnable r = forkSystemServer(abiList, socketName, zygoteServer);
+
+                // {@code r == null} in the parent (zygote) process, and {@code r != null} in the
+                // child (system_server) process.
+                if (r != null) {
+                    r.run();
+                    return;
+                }
+            }
+
+            Log.i(TAG, "Accepting command socket connections");
+
+            // The select loop returns early in the child process after a fork and
+            // loops forever in the zygote.
+            caller = zygoteServer.runSelectLoop(abiList);
+        } catch (Throwable ex) {
+            Log.e(TAG, "System zygote died with exception", ex);
+            throw ex;
+        } finally {
+            zygoteServer.closeServerSocket();
+        }
+
+        // We're in the child process and have exited the select loop. Proceed to execute the
+        // command.
+        if (caller != null) {
+            caller.run();
+        }
+    }
+
+    /**
+     * Return {@code true} if this device configuration has another zygote.
+     *
+     * We determine this by comparing the device ABI list with this zygotes
+     * list. If this zygote supports all ABIs this device supports, there won't
+     * be another zygote.
+     */
+    private static boolean hasSecondZygote(String abiList) {
+        return !SystemProperties.get("ro.product.cpu.abilist").equals(abiList);
+    }
+
+    private static void waitForSecondaryZygote(String socketName) {
+        String otherZygoteName = Process.ZYGOTE_SOCKET.equals(socketName) ?
+                Process.SECONDARY_ZYGOTE_SOCKET : Process.ZYGOTE_SOCKET;
+        ZygoteProcess.waitForConnectionToZygote(otherZygoteName);
+    }
+
+    static boolean isPreloadComplete() {
+        return sPreloadComplete;
+    }
+
+    /**
+     * Class not instantiable.
+     */
+    private ZygoteInit() {
+    }
+
+    /**
+     * The main function called when started through the zygote process. This
+     * could be unified with main(), if the native code in nativeFinishInit()
+     * were rationalized with Zygote startup.<p>
+     *
+     * Current recognized args:
+     * <ul>
+     *   <li> <code> [--] &lt;start class name&gt;  &lt;args&gt;
+     * </ul>
+     *
+     * @param targetSdkVersion target SDK version
+     * @param argv arg strings
+     */
+    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
+        if (RuntimeInit.DEBUG) {
+            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
+        }
+
+        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
+        RuntimeInit.redirectLogStreams();
+
+        RuntimeInit.commonInit();
+        ZygoteInit.nativeZygoteInit();
+        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
+    }
+
+    private static final native void nativeZygoteInit();
+}
diff --git a/com/android/internal/os/ZygoteSecurityException.java b/com/android/internal/os/ZygoteSecurityException.java
new file mode 100644
index 0000000..13b4759
--- /dev/null
+++ b/com/android/internal/os/ZygoteSecurityException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2007 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.internal.os;
+
+/**
+ * Exception thrown when a security policy is violated.
+ */
+class ZygoteSecurityException extends RuntimeException {
+    ZygoteSecurityException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/internal/os/ZygoteServer.java b/com/android/internal/os/ZygoteServer.java
new file mode 100644
index 0000000..8baa15a
--- /dev/null
+++ b/com/android/internal/os/ZygoteServer.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2007 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.internal.os;
+
+import static android.system.OsConstants.POLLIN;
+
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.system.Os;
+import android.system.ErrnoException;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import android.util.Slog;
+import java.io.IOException;
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+
+/**
+ * Server socket class for zygote processes.
+ *
+ * Provides functions to wait for commands on a UNIX domain socket, and fork
+ * off child processes that inherit the initial state of the VM.%
+ *
+ * Please see {@link ZygoteConnection.Arguments} for documentation on the
+ * client protocol.
+ */
+class ZygoteServer {
+    public static final String TAG = "ZygoteServer";
+
+    private static final String ANDROID_SOCKET_PREFIX = "ANDROID_SOCKET_";
+
+    private LocalServerSocket mServerSocket;
+
+    /**
+     * Set by the child process, immediately after a call to {@code Zygote.forkAndSpecialize}.
+     */
+    private boolean mIsForkChild;
+
+    ZygoteServer() {
+    }
+
+    void setForkChild() {
+        mIsForkChild = true;
+    }
+
+    /**
+     * Registers a server socket for zygote command connections
+     *
+     * @throws RuntimeException when open fails
+     */
+    void registerServerSocket(String socketName) {
+        if (mServerSocket == null) {
+            int fileDesc;
+            final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
+            try {
+                String env = System.getenv(fullSocketName);
+                fileDesc = Integer.parseInt(env);
+            } catch (RuntimeException ex) {
+                throw new RuntimeException(fullSocketName + " unset or invalid", ex);
+            }
+
+            try {
+                FileDescriptor fd = new FileDescriptor();
+                fd.setInt$(fileDesc);
+                mServerSocket = new LocalServerSocket(fd);
+            } catch (IOException ex) {
+                throw new RuntimeException(
+                        "Error binding to local socket '" + fileDesc + "'", ex);
+            }
+        }
+    }
+
+    /**
+     * Waits for and accepts a single command connection. Throws
+     * RuntimeException on failure.
+     */
+    private ZygoteConnection acceptCommandPeer(String abiList) {
+        try {
+            return createNewConnection(mServerSocket.accept(), abiList);
+        } catch (IOException ex) {
+            throw new RuntimeException(
+                    "IOException during accept()", ex);
+        }
+    }
+
+    protected ZygoteConnection createNewConnection(LocalSocket socket, String abiList)
+            throws IOException {
+        return new ZygoteConnection(socket, abiList);
+    }
+
+    /**
+     * Close and clean up zygote sockets. Called on shutdown and on the
+     * child's exit path.
+     */
+    void closeServerSocket() {
+        try {
+            if (mServerSocket != null) {
+                FileDescriptor fd = mServerSocket.getFileDescriptor();
+                mServerSocket.close();
+                if (fd != null) {
+                    Os.close(fd);
+                }
+            }
+        } catch (IOException ex) {
+            Log.e(TAG, "Zygote:  error closing sockets", ex);
+        } catch (ErrnoException ex) {
+            Log.e(TAG, "Zygote:  error closing descriptor", ex);
+        }
+
+        mServerSocket = null;
+    }
+
+    /**
+     * Return the server socket's underlying file descriptor, so that
+     * ZygoteConnection can pass it to the native code for proper
+     * closure after a child process is forked off.
+     */
+
+    FileDescriptor getServerSocketFileDescriptor() {
+        return mServerSocket.getFileDescriptor();
+    }
+
+    /**
+     * Runs the zygote process's select loop. Accepts new connections as
+     * they happen, and reads commands from connections one spawn-request's
+     * worth at a time.
+     */
+    Runnable runSelectLoop(String abiList) {
+        ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
+        ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
+
+        fds.add(mServerSocket.getFileDescriptor());
+        peers.add(null);
+
+        while (true) {
+            StructPollfd[] pollFds = new StructPollfd[fds.size()];
+            for (int i = 0; i < pollFds.length; ++i) {
+                pollFds[i] = new StructPollfd();
+                pollFds[i].fd = fds.get(i);
+                pollFds[i].events = (short) POLLIN;
+            }
+            try {
+                Os.poll(pollFds, -1);
+            } catch (ErrnoException ex) {
+                throw new RuntimeException("poll failed", ex);
+            }
+            for (int i = pollFds.length - 1; i >= 0; --i) {
+                if ((pollFds[i].revents & POLLIN) == 0) {
+                    continue;
+                }
+
+                if (i == 0) {
+                    ZygoteConnection newPeer = acceptCommandPeer(abiList);
+                    peers.add(newPeer);
+                    fds.add(newPeer.getFileDesciptor());
+                } else {
+                    try {
+                        ZygoteConnection connection = peers.get(i);
+                        final Runnable command = connection.processOneCommand(this);
+
+                        if (mIsForkChild) {
+                            // We're in the child. We should always have a command to run at this
+                            // stage if processOneCommand hasn't called "exec".
+                            if (command == null) {
+                                throw new IllegalStateException("command == null");
+                            }
+
+                            return command;
+                        } else {
+                            // We're in the server - we should never have any commands to run.
+                            if (command != null) {
+                                throw new IllegalStateException("command != null");
+                            }
+
+                            // We don't know whether the remote side of the socket was closed or
+                            // not until we attempt to read from it from processOneCommand. This shows up as
+                            // a regular POLLIN event in our regular processing loop.
+                            if (connection.isClosedByPeer()) {
+                                connection.closeSocket();
+                                peers.remove(i);
+                                fds.remove(i);
+                            }
+                        }
+                    } catch (Exception e) {
+                        if (!mIsForkChild) {
+                            // We're in the server so any exception here is one that has taken place
+                            // pre-fork while processing commands or reading / writing from the
+                            // control socket. Make a loud noise about any such exceptions so that
+                            // we know exactly what failed and why.
+
+                            Slog.e(TAG, "Exception executing zygote command: ", e);
+
+                            // Make sure the socket is closed so that the other end knows immediately
+                            // that something has gone wrong and doesn't time out waiting for a
+                            // response.
+                            ZygoteConnection conn = peers.remove(i);
+                            conn.closeSocket();
+
+                            fds.remove(i);
+                        } else {
+                            // We're in the child so any exception caught here has happened post
+                            // fork and before we execute ActivityThread.main (or any other main()
+                            // method). Log the details of the exception and bring down the process.
+                            Log.e(TAG, "Caught post-fork exception in child process.", e);
+                            throw e;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/policy/BackdropFrameRenderer.java b/com/android/internal/policy/BackdropFrameRenderer.java
new file mode 100644
index 0000000..a70209c
--- /dev/null
+++ b/com/android/internal/policy/BackdropFrameRenderer.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2015 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.internal.policy;
+
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Looper;
+import android.view.Choreographer;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+import android.view.ThreadedRenderer;
+
+/**
+ * The thread which draws a fill in background while the app is resizing in areas where the app
+ * content draw is lagging behind the resize operation.
+ * It starts with the creation and it ends once someone calls destroy().
+ * Any size changes can be passed by a call to setTargetRect will passed to the thread and
+ * executed via the Choreographer.
+ * @hide
+ */
+public class BackdropFrameRenderer extends Thread implements Choreographer.FrameCallback {
+
+    private DecorView mDecorView;
+
+    // This is containing the last requested size by a resize command. Note that this size might
+    // or might not have been applied to the output already.
+    private final Rect mTargetRect = new Rect();
+
+    // The render nodes for the multi threaded renderer.
+    private ThreadedRenderer mRenderer;
+    private RenderNode mFrameAndBackdropNode;
+    private RenderNode mSystemBarBackgroundNode;
+
+    private final Rect mOldTargetRect = new Rect();
+    private final Rect mNewTargetRect = new Rect();
+
+    private Choreographer mChoreographer;
+
+    // Cached size values from the last render for the case that the view hierarchy is gone
+    // during a configuration change.
+    private int mLastContentWidth;
+    private int mLastContentHeight;
+    private int mLastCaptionHeight;
+    private int mLastXOffset;
+    private int mLastYOffset;
+
+    // Whether to report when next frame is drawn or not.
+    private boolean mReportNextDraw;
+
+    private Drawable mCaptionBackgroundDrawable;
+    private Drawable mUserCaptionBackgroundDrawable;
+    private Drawable mResizingBackgroundDrawable;
+    private ColorDrawable mStatusBarColor;
+    private ColorDrawable mNavigationBarColor;
+    private boolean mOldFullscreen;
+    private boolean mFullscreen;
+    private final int mResizeMode;
+    private final Rect mOldSystemInsets = new Rect();
+    private final Rect mOldStableInsets = new Rect();
+    private final Rect mSystemInsets = new Rect();
+    private final Rect mStableInsets = new Rect();
+    private final Rect mTmpRect = new Rect();
+
+    public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
+            Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
+            Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
+            boolean fullscreen, Rect systemInsets, Rect stableInsets, int resizeMode) {
+        setName("ResizeFrame");
+
+        mRenderer = renderer;
+        onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
+                userCaptionBackgroundDrawable, statusBarColor, navigationBarColor);
+
+        // Create a render node for the content and frame backdrop
+        // which can be resized independently from the content.
+        mFrameAndBackdropNode = RenderNode.create("FrameAndBackdropNode", null);
+
+        mRenderer.addRenderNode(mFrameAndBackdropNode, true);
+
+        // Set the initial bounds and draw once so that we do not get a broken frame.
+        mTargetRect.set(initialBounds);
+        mFullscreen = fullscreen;
+        mOldFullscreen = fullscreen;
+        mSystemInsets.set(systemInsets);
+        mStableInsets.set(stableInsets);
+        mOldSystemInsets.set(systemInsets);
+        mOldStableInsets.set(stableInsets);
+        mResizeMode = resizeMode;
+
+        // Kick off our draw thread.
+        start();
+    }
+
+    void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
+            Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
+            int statusBarColor, int navigationBarColor) {
+        mDecorView = decorView;
+        mResizingBackgroundDrawable = resizingBackgroundDrawable != null
+                        && resizingBackgroundDrawable.getConstantState() != null
+                ? resizingBackgroundDrawable.getConstantState().newDrawable()
+                : null;
+        mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable != null
+                        && captionBackgroundDrawableDrawable.getConstantState() != null
+                ? captionBackgroundDrawableDrawable.getConstantState().newDrawable()
+                : null;
+        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable != null
+                        && userCaptionBackgroundDrawable.getConstantState() != null
+                ? userCaptionBackgroundDrawable.getConstantState().newDrawable()
+                : null;
+        if (mCaptionBackgroundDrawable == null) {
+            mCaptionBackgroundDrawable = mResizingBackgroundDrawable;
+        }
+        if (statusBarColor != 0) {
+            mStatusBarColor = new ColorDrawable(statusBarColor);
+            addSystemBarNodeIfNeeded();
+        } else {
+            mStatusBarColor = null;
+        }
+        if (navigationBarColor != 0) {
+            mNavigationBarColor = new ColorDrawable(navigationBarColor);
+            addSystemBarNodeIfNeeded();
+        } else {
+            mNavigationBarColor = null;
+        }
+    }
+
+    private void addSystemBarNodeIfNeeded() {
+        if (mSystemBarBackgroundNode != null) {
+            return;
+        }
+        mSystemBarBackgroundNode = RenderNode.create("SystemBarBackgroundNode", null);
+        mRenderer.addRenderNode(mSystemBarBackgroundNode, false);
+    }
+
+    /**
+     * Call this function asynchronously when the window size has been changed or when the insets
+     * have changed or whether window switched between a fullscreen or non-fullscreen layout.
+     * The change will be picked up once per frame and the frame will be re-rendered accordingly.
+     *
+     * @param newTargetBounds The new target bounds.
+     * @param fullscreen Whether the window is currently drawing in fullscreen.
+     * @param systemInsets The current visible system insets for the window.
+     * @param stableInsets The stable insets for the window.
+     */
+    public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
+        synchronized (this) {
+            mFullscreen = fullscreen;
+            mTargetRect.set(newTargetBounds);
+            mSystemInsets.set(systemInsets);
+            mStableInsets.set(stableInsets);
+            // Notify of a bounds change.
+            pingRenderLocked(false /* drawImmediate */);
+        }
+    }
+
+    /**
+     * The window got replaced due to a configuration change.
+     */
+    public void onConfigurationChange() {
+        synchronized (this) {
+            if (mRenderer != null) {
+                // Enforce a window redraw.
+                mOldTargetRect.set(0, 0, 0, 0);
+                pingRenderLocked(false /* drawImmediate */);
+            }
+        }
+    }
+
+    /**
+     * All resources of the renderer will be released. This function can be called from the
+     * the UI thread as well as the renderer thread.
+     */
+    public void releaseRenderer() {
+        synchronized (this) {
+            if (mRenderer != null) {
+                // Invalidate the current content bounds.
+                mRenderer.setContentDrawBounds(0, 0, 0, 0);
+
+                // Remove the render node again
+                // (see comment above - better to do that only once).
+                mRenderer.removeRenderNode(mFrameAndBackdropNode);
+                if (mSystemBarBackgroundNode != null) {
+                    mRenderer.removeRenderNode(mSystemBarBackgroundNode);
+                }
+
+                mRenderer = null;
+
+                // Exit the renderer loop.
+                pingRenderLocked(false /* drawImmediate */);
+            }
+        }
+    }
+
+    @Override
+    public void run() {
+        try {
+            Looper.prepare();
+            synchronized (this) {
+                mChoreographer = Choreographer.getInstance();
+            }
+            Looper.loop();
+        } finally {
+            releaseRenderer();
+        }
+        synchronized (this) {
+            // Make sure no more messages are being sent.
+            mChoreographer = null;
+            Choreographer.releaseInstance();
+        }
+    }
+
+    /**
+     * The implementation of the FrameCallback.
+     * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
+     * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
+     */
+    @Override
+    public void doFrame(long frameTimeNanos) {
+        synchronized (this) {
+            if (mRenderer == null) {
+                reportDrawIfNeeded();
+                // Tell the looper to stop. We are done.
+                Looper.myLooper().quit();
+                return;
+            }
+            doFrameUncheckedLocked();
+        }
+    }
+
+    private void doFrameUncheckedLocked() {
+        mNewTargetRect.set(mTargetRect);
+        if (!mNewTargetRect.equals(mOldTargetRect)
+                || mOldFullscreen != mFullscreen
+                || !mStableInsets.equals(mOldStableInsets)
+                || !mSystemInsets.equals(mOldSystemInsets)
+                || mReportNextDraw) {
+            mOldFullscreen = mFullscreen;
+            mOldTargetRect.set(mNewTargetRect);
+            mOldSystemInsets.set(mSystemInsets);
+            mOldStableInsets.set(mStableInsets);
+            redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
+        }
+    }
+
+    /**
+     * The content is about to be drawn and we got the location of where it will be shown.
+     * If a "redrawLocked" call has already been processed, we will re-issue the call
+     * if the previous call was ignored since the size was unknown.
+     * @param xOffset The x offset where the content is drawn to.
+     * @param yOffset The y offset where the content is drawn to.
+     * @param xSize The width size of the content. This should not be 0.
+     * @param ySize The height of the content.
+     * @return true if a frame should be requested after the content is drawn; false otherwise.
+     */
+    public boolean onContentDrawn(int xOffset, int yOffset, int xSize, int ySize) {
+        synchronized (this) {
+            final boolean firstCall = mLastContentWidth == 0;
+            // The current content buffer is drawn here.
+            mLastContentWidth = xSize;
+            mLastContentHeight = ySize - mLastCaptionHeight;
+            mLastXOffset = xOffset;
+            mLastYOffset = yOffset;
+
+            // Inform the renderer of the content's new bounds
+            mRenderer.setContentDrawBounds(
+                    mLastXOffset,
+                    mLastYOffset,
+                    mLastXOffset + mLastContentWidth,
+                    mLastYOffset + mLastCaptionHeight + mLastContentHeight);
+
+            // If this was the first call and redrawLocked got already called prior
+            // to us, we should re-issue a redrawLocked now.
+            return firstCall
+                    && (mLastCaptionHeight != 0 || !mDecorView.isShowingCaption());
+        }
+    }
+
+    public void onRequestDraw(boolean reportNextDraw) {
+        synchronized (this) {
+            mReportNextDraw = reportNextDraw;
+            mOldTargetRect.set(0, 0, 0, 0);
+            pingRenderLocked(true /* drawImmediate */);
+        }
+    }
+
+    /**
+     * Redraws the background, the caption and the system inset backgrounds if something changed.
+     *
+     * @param newBounds The window bounds which needs to be drawn.
+     * @param fullscreen Whether the window is currently drawing in fullscreen.
+     * @param systemInsets The current visible system insets for the window.
+     * @param stableInsets The stable insets for the window.
+     */
+    private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
+
+        // While a configuration change is taking place the view hierarchy might become
+        // inaccessible. For that case we remember the previous metrics to avoid flashes.
+        // Note that even when there is no visible caption, the caption child will exist.
+        final int captionHeight = mDecorView.getCaptionHeight();
+
+        // The caption height will probably never dynamically change while we are resizing.
+        // Once set to something other then 0 it should be kept that way.
+        if (captionHeight != 0) {
+            // Remember the height of the caption.
+            mLastCaptionHeight = captionHeight;
+        }
+
+        // Make sure that the other thread has already prepared the render draw calls for the
+        // content. If any size is 0, we have to wait for it to be drawn first.
+        if ((mLastCaptionHeight == 0 && mDecorView.isShowingCaption()) ||
+                mLastContentWidth == 0 || mLastContentHeight == 0) {
+            return;
+        }
+
+        // Since the surface is spanning the entire screen, we have to add the start offset of
+        // the bounds to get to the surface location.
+        final int left = mLastXOffset + newBounds.left;
+        final int top = mLastYOffset + newBounds.top;
+        final int width = newBounds.width();
+        final int height = newBounds.height();
+
+        mFrameAndBackdropNode.setLeftTopRightBottom(left, top, left + width, top + height);
+
+        // Draw the caption and content backdrops in to our render node.
+        DisplayListCanvas canvas = mFrameAndBackdropNode.start(width, height);
+        final Drawable drawable = mUserCaptionBackgroundDrawable != null
+                ? mUserCaptionBackgroundDrawable : mCaptionBackgroundDrawable;
+
+        if (drawable != null) {
+            drawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
+            drawable.draw(canvas);
+        }
+
+        // The backdrop: clear everything with the background. Clipping is done elsewhere.
+        if (mResizingBackgroundDrawable != null) {
+            mResizingBackgroundDrawable.setBounds(0, mLastCaptionHeight, left + width, top + height);
+            mResizingBackgroundDrawable.draw(canvas);
+        }
+        mFrameAndBackdropNode.end(canvas);
+
+        drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
+
+        // We need to render the node explicitly
+        mRenderer.drawRenderNode(mFrameAndBackdropNode);
+
+        reportDrawIfNeeded();
+    }
+
+    private void drawColorViews(int left, int top, int width, int height,
+            boolean fullscreen, Rect systemInsets, Rect stableInsets) {
+        if (mSystemBarBackgroundNode == null) {
+            return;
+        }
+        DisplayListCanvas canvas = mSystemBarBackgroundNode.start(width, height);
+        mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
+        final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
+        if (mStatusBarColor != null) {
+            mStatusBarColor.setBounds(0, 0, left + width, topInset);
+            mStatusBarColor.draw(canvas);
+        }
+
+        // We only want to draw the navigation bar if our window is currently fullscreen because we
+        // don't want the navigation bar background be moving around when resizing in docked mode.
+        // However, we need it for the transitions into/out of docked mode.
+        if (mNavigationBarColor != null && fullscreen) {
+            DecorView.getNavigationBarRect(width, height, stableInsets, systemInsets, mTmpRect);
+            mNavigationBarColor.setBounds(mTmpRect);
+            mNavigationBarColor.draw(canvas);
+        }
+        mSystemBarBackgroundNode.end(canvas);
+        mRenderer.drawRenderNode(mSystemBarBackgroundNode);
+    }
+
+    /** Notify view root that a frame has been drawn by us, if it has requested so. */
+    private void reportDrawIfNeeded() {
+        if (mReportNextDraw) {
+            if (mDecorView.isAttachedToWindow()) {
+                mDecorView.getViewRootImpl().reportDrawFinish();
+            }
+            mReportNextDraw = false;
+        }
+    }
+
+    /**
+     * Sends a message to the renderer to wake up and perform the next action which can be
+     * either the next rendering or the self destruction if mRenderer is null.
+     * Note: This call must be synchronized.
+     *
+     * @param drawImmediate if we should draw immediately instead of scheduling a frame
+     */
+    private void pingRenderLocked(boolean drawImmediate) {
+        if (mChoreographer != null && !drawImmediate) {
+            mChoreographer.postFrameCallback(this);
+        } else {
+            doFrameUncheckedLocked();
+        }
+    }
+
+    void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
+        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
+    }
+}
diff --git a/com/android/internal/policy/DecorContext.java b/com/android/internal/policy/DecorContext.java
new file mode 100644
index 0000000..eac9f64
--- /dev/null
+++ b/com/android/internal/policy/DecorContext.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 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.internal.policy;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.view.ContextThemeWrapper;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+
+/**
+ * Context for decor views which can be seeded with pure application context and not depend on the
+ * activity, but still provide some of the facilities that Activity has,
+ * e.g. themes, activity-based resources, etc.
+ *
+ * @hide
+ */
+class DecorContext extends ContextThemeWrapper {
+    private PhoneWindow mPhoneWindow;
+    private WindowManager mWindowManager;
+    private Resources mActivityResources;
+
+    public DecorContext(Context context, Resources activityResources) {
+        super(context, null);
+        mActivityResources = activityResources;
+    }
+
+    void setPhoneWindow(PhoneWindow phoneWindow) {
+        mPhoneWindow = phoneWindow;
+        mWindowManager = null;
+    }
+
+    @Override
+    public Object getSystemService(String name) {
+        if (Context.WINDOW_SERVICE.equals(name)) {
+            if (mWindowManager == null) {
+                WindowManagerImpl wm =
+                        (WindowManagerImpl) super.getSystemService(Context.WINDOW_SERVICE);
+                mWindowManager = wm.createLocalWindowManager(mPhoneWindow);
+            }
+            return mWindowManager;
+        }
+        return super.getSystemService(name);
+    }
+
+    @Override
+    public Resources getResources() {
+        return mActivityResources;
+    }
+
+    @Override
+    public AssetManager getAssets() {
+        return mActivityResources.getAssets();
+    }
+}
diff --git a/com/android/internal/policy/DecorView.java b/com/android/internal/policy/DecorView.java
new file mode 100644
index 0000000..85251d4
--- /dev/null
+++ b/com/android/internal/policy/DecorView.java
@@ -0,0 +1,2530 @@
+/*
+ * Copyright (C) 2015 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.internal.policy;
+
+import android.app.WindowConfiguration;
+import android.graphics.Outline;
+import android.graphics.drawable.InsetDrawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.Pair;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
+import com.android.internal.R;
+import com.android.internal.policy.PhoneWindow.PanelFeatureState;
+import com.android.internal.policy.PhoneWindow.PhoneWindowMenuCallback;
+import com.android.internal.view.FloatingActionMode;
+import com.android.internal.view.RootViewSurfaceTaker;
+import com.android.internal.view.StandaloneActionMode;
+import com.android.internal.view.menu.ContextMenuBuilder;
+import com.android.internal.view.menu.MenuHelper;
+import com.android.internal.widget.ActionBarContextView;
+import com.android.internal.widget.BackgroundFallback;
+import com.android.internal.widget.DecorCaptionView;
+import com.android.internal.widget.FloatingToolbar;
+
+import java.util.List;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Shader;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.ContextThemeWrapper;
+import android.view.DisplayListCanvas;
+import android.view.Gravity;
+import android.view.InputQueue;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.ThreadedRenderer;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowCallbacks;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.PopupWindow;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N;
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.getMode;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.Window.DECOR_CAPTION_SHADE_DARK;
+import static android.view.Window.DECOR_CAPTION_SHADE_LIGHT;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
+import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION;
+import static com.android.internal.policy.PhoneWindow.FEATURE_OPTIONS_PANEL;
+
+/** @hide */
+public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
+    private static final String TAG = "DecorView";
+
+    private static final boolean DEBUG_MEASURE = false;
+
+    private static final boolean SWEEP_OPEN_MENU = false;
+
+    // The height of a window which has focus in DIP.
+    private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
+    // The height of a window which has not in DIP.
+    private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
+
+    public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES =
+            new ColorViewAttributes(SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
+                    Gravity.TOP, Gravity.LEFT, Gravity.RIGHT,
+                    Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME,
+                    com.android.internal.R.id.statusBarBackground,
+                    FLAG_FULLSCREEN);
+
+    public static final ColorViewAttributes NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES =
+            new ColorViewAttributes(
+                    SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION,
+                    Gravity.BOTTOM, Gravity.RIGHT, Gravity.LEFT,
+                    Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME,
+                    com.android.internal.R.id.navigationBarBackground,
+                    0 /* hideWindowFlag */);
+
+    // This is used to workaround an issue where the PiP shadow can be transparent if the window
+    // background is transparent
+    private static final ViewOutlineProvider PIP_OUTLINE_PROVIDER = new ViewOutlineProvider() {
+        @Override
+        public void getOutline(View view, Outline outline) {
+            outline.setRect(0, 0, view.getWidth(), view.getHeight());
+            outline.setAlpha(1f);
+        }
+    };
+
+    // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
+    // size calculation takes the shadow size into account. We set the elevation currently
+    // to max until the first layout command has been executed.
+    private boolean mAllowUpdateElevation = false;
+
+    private boolean mElevationAdjustedForStack = false;
+
+    // Keeps track of the picture-in-picture mode for the view shadow
+    private boolean mIsInPictureInPictureMode;
+
+    // Stores the previous outline provider prior to applying PIP_OUTLINE_PROVIDER
+    private ViewOutlineProvider mLastOutlineProvider;
+
+    int mDefaultOpacity = PixelFormat.OPAQUE;
+
+    /** The feature ID of the panel, or -1 if this is the application's DecorView */
+    private final int mFeatureId;
+
+    private final Rect mDrawingBounds = new Rect();
+
+    private final Rect mBackgroundPadding = new Rect();
+
+    private final Rect mFramePadding = new Rect();
+
+    private final Rect mFrameOffsets = new Rect();
+
+    private boolean mHasCaption = false;
+
+    private boolean mChanging;
+
+    private Drawable mMenuBackground;
+    private boolean mWatchingForMenu;
+    private int mDownY;
+
+    ActionMode mPrimaryActionMode;
+    private ActionMode mFloatingActionMode;
+    private ActionBarContextView mPrimaryActionModeView;
+    private PopupWindow mPrimaryActionModePopup;
+    private Runnable mShowPrimaryActionModePopup;
+    private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener;
+    private View mFloatingActionModeOriginatingView;
+    private FloatingToolbar mFloatingToolbar;
+    private ObjectAnimator mFadeAnim;
+
+    // View added at runtime to draw under the status bar area
+    private View mStatusGuard;
+    // View added at runtime to draw under the navigation bar area
+    private View mNavigationGuard;
+
+    private final ColorViewState mStatusColorViewState =
+            new ColorViewState(STATUS_BAR_COLOR_VIEW_ATTRIBUTES);
+    private final ColorViewState mNavigationColorViewState =
+            new ColorViewState(NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES);
+
+    private final Interpolator mShowInterpolator;
+    private final Interpolator mHideInterpolator;
+    private final int mBarEnterExitDuration;
+    final boolean mForceWindowDrawsStatusBarBackground;
+    private final int mSemiTransparentStatusBarColor;
+
+    private final BackgroundFallback mBackgroundFallback = new BackgroundFallback();
+
+    private int mLastTopInset = 0;
+    private int mLastBottomInset = 0;
+    private int mLastRightInset = 0;
+    private int mLastLeftInset = 0;
+    private boolean mLastHasTopStableInset = false;
+    private boolean mLastHasBottomStableInset = false;
+    private boolean mLastHasRightStableInset = false;
+    private boolean mLastHasLeftStableInset = false;
+    private int mLastWindowFlags = 0;
+    private boolean mLastShouldAlwaysConsumeNavBar = false;
+
+    private int mRootScrollY = 0;
+
+    private PhoneWindow mWindow;
+
+    ViewGroup mContentRoot;
+
+    private Rect mTempRect;
+    private Rect mOutsets = new Rect();
+
+    // This is the caption view for the window, containing the caption and window control
+    // buttons. The visibility of this decor depends on the workspace and the window type.
+    // If the window type does not require such a view, this member might be null.
+    DecorCaptionView mDecorCaptionView;
+
+    private boolean mWindowResizeCallbacksAdded = false;
+    private Drawable.Callback mLastBackgroundDrawableCb = null;
+    private BackdropFrameRenderer mBackdropFrameRenderer = null;
+    private Drawable mResizingBackgroundDrawable;
+    private Drawable mCaptionBackgroundDrawable;
+    private Drawable mUserCaptionBackgroundDrawable;
+
+    private float mAvailableWidth;
+
+    String mLogTag = TAG;
+    private final Rect mFloatingInsets = new Rect();
+    private boolean mApplyFloatingVerticalInsets = false;
+    private boolean mApplyFloatingHorizontalInsets = false;
+
+    private int mResizeMode = RESIZE_MODE_INVALID;
+    private final int mResizeShadowSize;
+    private final Paint mVerticalResizeShadowPaint = new Paint();
+    private final Paint mHorizontalResizeShadowPaint = new Paint();
+
+    DecorView(Context context, int featureId, PhoneWindow window,
+            WindowManager.LayoutParams params) {
+        super(context);
+        mFeatureId = featureId;
+
+        mShowInterpolator = AnimationUtils.loadInterpolator(context,
+                android.R.interpolator.linear_out_slow_in);
+        mHideInterpolator = AnimationUtils.loadInterpolator(context,
+                android.R.interpolator.fast_out_linear_in);
+
+        mBarEnterExitDuration = context.getResources().getInteger(
+                R.integer.dock_enter_exit_duration);
+        mForceWindowDrawsStatusBarBackground = context.getResources().getBoolean(
+                R.bool.config_forceWindowDrawsStatusBarBackground)
+                && context.getApplicationInfo().targetSdkVersion >= N;
+        mSemiTransparentStatusBarColor = context.getResources().getColor(
+                R.color.system_bar_background_semi_transparent, null /* theme */);
+
+        updateAvailableWidth();
+
+        setWindow(window);
+
+        updateLogTag(params);
+
+        mResizeShadowSize = context.getResources().getDimensionPixelSize(
+                R.dimen.resize_shadow_size);
+        initResizingPaints();
+    }
+
+    void setBackgroundFallback(int resId) {
+        mBackgroundFallback.setDrawable(resId != 0 ? getContext().getDrawable(resId) : null);
+        setWillNotDraw(getBackground() == null && !mBackgroundFallback.hasFallback());
+    }
+
+    @Override
+    public boolean gatherTransparentRegion(Region region) {
+        boolean statusOpaque = gatherTransparentRegion(mStatusColorViewState, region);
+        boolean navOpaque = gatherTransparentRegion(mNavigationColorViewState, region);
+        boolean decorOpaque = super.gatherTransparentRegion(region);
+
+        // combine bools after computation, so each method above always executes
+        return statusOpaque || navOpaque || decorOpaque;
+    }
+
+    boolean gatherTransparentRegion(ColorViewState colorViewState, Region region) {
+        if (colorViewState.view != null && colorViewState.visible && isResizing()) {
+            // If a visible ColorViewState is in a resizing host DecorView, forcibly register its
+            // opaque area, since it's drawn by a different root RenderNode. It would otherwise be
+            // rejected by ViewGroup#gatherTransparentRegion() for the view not being VISIBLE.
+            return colorViewState.view.gatherTransparentRegion(region);
+        }
+        return false; // no opaque area added
+    }
+
+    @Override
+    public void onDraw(Canvas c) {
+        super.onDraw(c);
+
+        // When we are resizing, we need the fallback background to cover the area where we have our
+        // system bar background views as the navigation bar will be hidden during resizing.
+        mBackgroundFallback.draw(isResizing() ? this : mContentRoot, mContentRoot, c,
+                mWindow.mContentParent);
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        final int keyCode = event.getKeyCode();
+        final int action = event.getAction();
+        final boolean isDown = action == KeyEvent.ACTION_DOWN;
+
+        if (isDown && (event.getRepeatCount() == 0)) {
+            // First handle chording of panel key: if a panel key is held
+            // but not released, try to execute a shortcut in it.
+            if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
+                boolean handled = dispatchKeyShortcutEvent(event);
+                if (handled) {
+                    return true;
+                }
+            }
+
+            // If a panel is open, perform a shortcut on it without the
+            // chorded panel key
+            if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {
+                if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {
+                    return true;
+                }
+            }
+        }
+
+        if (!mWindow.isDestroyed()) {
+            final Window.Callback cb = mWindow.getCallback();
+            final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
+                    : super.dispatchKeyEvent(event);
+            if (handled) {
+                return true;
+            }
+        }
+
+        return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
+                : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
+    }
+
+    @Override
+    public boolean dispatchKeyShortcutEvent(KeyEvent ev) {
+        // If the panel is already prepared, then perform the shortcut using it.
+        boolean handled;
+        if (mWindow.mPreparedPanel != null) {
+            handled = mWindow.performPanelShortcut(mWindow.mPreparedPanel, ev.getKeyCode(), ev,
+                    Menu.FLAG_PERFORM_NO_CLOSE);
+            if (handled) {
+                if (mWindow.mPreparedPanel != null) {
+                    mWindow.mPreparedPanel.isHandled = true;
+                }
+                return true;
+            }
+        }
+
+        // Shortcut not handled by the panel.  Dispatch to the view hierarchy.
+        final Window.Callback cb = mWindow.getCallback();
+        handled = cb != null && !mWindow.isDestroyed() && mFeatureId < 0
+                ? cb.dispatchKeyShortcutEvent(ev) : super.dispatchKeyShortcutEvent(ev);
+        if (handled) {
+            return true;
+        }
+
+        // If the panel is not prepared, then we may be trying to handle a shortcut key
+        // combination such as Control+C.  Temporarily prepare the panel then mark it
+        // unprepared again when finished to ensure that the panel will again be prepared
+        // the next time it is shown for real.
+        PhoneWindow.PanelFeatureState st =
+                mWindow.getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
+        if (st != null && mWindow.mPreparedPanel == null) {
+            mWindow.preparePanel(st, ev);
+            handled = mWindow.performPanelShortcut(st, ev.getKeyCode(), ev,
+                    Menu.FLAG_PERFORM_NO_CLOSE);
+            st.isPrepared = false;
+            if (handled) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        final Window.Callback cb = mWindow.getCallback();
+        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
+                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchTrackballEvent(MotionEvent ev) {
+        final Window.Callback cb = mWindow.getCallback();
+        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
+                ? cb.dispatchTrackballEvent(ev) : super.dispatchTrackballEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+        final Window.Callback cb = mWindow.getCallback();
+        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
+                ? cb.dispatchGenericMotionEvent(ev) : super.dispatchGenericMotionEvent(ev);
+    }
+
+    public boolean superDispatchKeyEvent(KeyEvent event) {
+        // Give priority to closing action modes if applicable.
+        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+            final int action = event.getAction();
+            // Back cancels action modes first.
+            if (mPrimaryActionMode != null) {
+                if (action == KeyEvent.ACTION_UP) {
+                    mPrimaryActionMode.finish();
+                }
+                return true;
+            }
+        }
+
+        return super.dispatchKeyEvent(event);
+    }
+
+    public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
+        return super.dispatchKeyShortcutEvent(event);
+    }
+
+    public boolean superDispatchTouchEvent(MotionEvent event) {
+        return super.dispatchTouchEvent(event);
+    }
+
+    public boolean superDispatchTrackballEvent(MotionEvent event) {
+        return super.dispatchTrackballEvent(event);
+    }
+
+    public boolean superDispatchGenericMotionEvent(MotionEvent event) {
+        return super.dispatchGenericMotionEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return onInterceptTouchEvent(event);
+    }
+
+    private boolean isOutOfInnerBounds(int x, int y) {
+        return x < 0 || y < 0 || x > getWidth() || y > getHeight();
+    }
+
+    private boolean isOutOfBounds(int x, int y) {
+        return x < -5 || y < -5 || x > (getWidth() + 5)
+                || y > (getHeight() + 5);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        int action = event.getAction();
+        if (mHasCaption && isShowingCaption()) {
+            // Don't dispatch ACTION_DOWN to the captionr if the window is resizable and the event
+            // was (starting) outside the window. Window resizing events should be handled by
+            // WindowManager.
+            // TODO: Investigate how to handle the outside touch in window manager
+            //       without generating these events.
+            //       Currently we receive these because we need to enlarge the window's
+            //       touch region so that the monitor channel receives the events
+            //       in the outside touch area.
+            if (action == MotionEvent.ACTION_DOWN) {
+                final int x = (int) event.getX();
+                final int y = (int) event.getY();
+                if (isOutOfInnerBounds(x, y)) {
+                    return true;
+                }
+            }
+        }
+
+        if (mFeatureId >= 0) {
+            if (action == MotionEvent.ACTION_DOWN) {
+                int x = (int)event.getX();
+                int y = (int)event.getY();
+                if (isOutOfBounds(x, y)) {
+                    mWindow.closePanel(mFeatureId);
+                    return true;
+                }
+            }
+        }
+
+        if (!SWEEP_OPEN_MENU) {
+            return false;
+        }
+
+        if (mFeatureId >= 0) {
+            if (action == MotionEvent.ACTION_DOWN) {
+                Log.i(mLogTag, "Watchiing!");
+                mWatchingForMenu = true;
+                mDownY = (int) event.getY();
+                return false;
+            }
+
+            if (!mWatchingForMenu) {
+                return false;
+            }
+
+            int y = (int)event.getY();
+            if (action == MotionEvent.ACTION_MOVE) {
+                if (y > (mDownY+30)) {
+                    Log.i(mLogTag, "Closing!");
+                    mWindow.closePanel(mFeatureId);
+                    mWatchingForMenu = false;
+                    return true;
+                }
+            } else if (action == MotionEvent.ACTION_UP) {
+                mWatchingForMenu = false;
+            }
+
+            return false;
+        }
+
+        //Log.i(mLogTag, "Intercept: action=" + action + " y=" + event.getY()
+        //        + " (in " + getHeight() + ")");
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            int y = (int)event.getY();
+            if (y >= (getHeight()-5) && !mWindow.hasChildren()) {
+                Log.i(mLogTag, "Watching!");
+                mWatchingForMenu = true;
+            }
+            return false;
+        }
+
+        if (!mWatchingForMenu) {
+            return false;
+        }
+
+        int y = (int)event.getY();
+        if (action == MotionEvent.ACTION_MOVE) {
+            if (y < (getHeight()-30)) {
+                Log.i(mLogTag, "Opening!");
+                mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, new KeyEvent(
+                        KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU));
+                mWatchingForMenu = false;
+                return true;
+            }
+        } else if (action == MotionEvent.ACTION_UP) {
+            mWatchingForMenu = false;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void sendAccessibilityEvent(int eventType) {
+        if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+            return;
+        }
+
+        // if we are showing a feature that should be announced and one child
+        // make this child the event source since this is the feature itself
+        // otherwise the callback will take over and announce its client
+        if ((mFeatureId == Window.FEATURE_OPTIONS_PANEL ||
+                mFeatureId == Window.FEATURE_CONTEXT_MENU ||
+                mFeatureId == Window.FEATURE_PROGRESS ||
+                mFeatureId == Window.FEATURE_INDETERMINATE_PROGRESS)
+                && getChildCount() == 1) {
+            getChildAt(0).sendAccessibilityEvent(eventType);
+        } else {
+            super.sendAccessibilityEvent(eventType);
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        final Window.Callback cb = mWindow.getCallback();
+        if (cb != null && !mWindow.isDestroyed()) {
+            if (cb.dispatchPopulateAccessibilityEvent(event)) {
+                return true;
+            }
+        }
+        return super.dispatchPopulateAccessibilityEventInternal(event);
+    }
+
+    @Override
+    protected boolean setFrame(int l, int t, int r, int b) {
+        boolean changed = super.setFrame(l, t, r, b);
+        if (changed) {
+            final Rect drawingBounds = mDrawingBounds;
+            getDrawingRect(drawingBounds);
+
+            Drawable fg = getForeground();
+            if (fg != null) {
+                final Rect frameOffsets = mFrameOffsets;
+                drawingBounds.left += frameOffsets.left;
+                drawingBounds.top += frameOffsets.top;
+                drawingBounds.right -= frameOffsets.right;
+                drawingBounds.bottom -= frameOffsets.bottom;
+                fg.setBounds(drawingBounds);
+                final Rect framePadding = mFramePadding;
+                drawingBounds.left += framePadding.left - frameOffsets.left;
+                drawingBounds.top += framePadding.top - frameOffsets.top;
+                drawingBounds.right -= framePadding.right - frameOffsets.right;
+                drawingBounds.bottom -= framePadding.bottom - frameOffsets.bottom;
+            }
+
+            Drawable bg = getBackground();
+            if (bg != null) {
+                bg.setBounds(drawingBounds);
+            }
+
+            if (SWEEP_OPEN_MENU) {
+                if (mMenuBackground == null && mFeatureId < 0
+                        && mWindow.getAttributes().height
+                        == WindowManager.LayoutParams.MATCH_PARENT) {
+                    mMenuBackground = getContext().getDrawable(
+                            R.drawable.menu_background);
+                }
+                if (mMenuBackground != null) {
+                    mMenuBackground.setBounds(drawingBounds.left,
+                            drawingBounds.bottom-6, drawingBounds.right,
+                            drawingBounds.bottom+20);
+                }
+            }
+        }
+        return changed;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+        final boolean isPortrait =
+                getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
+
+        final int widthMode = getMode(widthMeasureSpec);
+        final int heightMode = getMode(heightMeasureSpec);
+
+        boolean fixedWidth = false;
+        mApplyFloatingHorizontalInsets = false;
+        if (widthMode == AT_MOST) {
+            final TypedValue tvw = isPortrait ? mWindow.mFixedWidthMinor : mWindow.mFixedWidthMajor;
+            if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {
+                final int w;
+                if (tvw.type == TypedValue.TYPE_DIMENSION) {
+                    w = (int) tvw.getDimension(metrics);
+                } else if (tvw.type == TypedValue.TYPE_FRACTION) {
+                    w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);
+                } else {
+                    w = 0;
+                }
+                if (DEBUG_MEASURE) Log.d(mLogTag, "Fixed width: " + w);
+                final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+                if (w > 0) {
+                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            Math.min(w, widthSize), EXACTLY);
+                    fixedWidth = true;
+                } else {
+                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            widthSize - mFloatingInsets.left - mFloatingInsets.right,
+                            AT_MOST);
+                    mApplyFloatingHorizontalInsets = true;
+                }
+            }
+        }
+
+        mApplyFloatingVerticalInsets = false;
+        if (heightMode == AT_MOST) {
+            final TypedValue tvh = isPortrait ? mWindow.mFixedHeightMajor
+                    : mWindow.mFixedHeightMinor;
+            if (tvh != null && tvh.type != TypedValue.TYPE_NULL) {
+                final int h;
+                if (tvh.type == TypedValue.TYPE_DIMENSION) {
+                    h = (int) tvh.getDimension(metrics);
+                } else if (tvh.type == TypedValue.TYPE_FRACTION) {
+                    h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels);
+                } else {
+                    h = 0;
+                }
+                if (DEBUG_MEASURE) Log.d(mLogTag, "Fixed height: " + h);
+                final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+                if (h > 0) {
+                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            Math.min(h, heightSize), EXACTLY);
+                } else if ((mWindow.getAttributes().flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
+                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            heightSize - mFloatingInsets.top - mFloatingInsets.bottom, AT_MOST);
+                    mApplyFloatingVerticalInsets = true;
+                }
+            }
+        }
+
+        getOutsets(mOutsets);
+        if (mOutsets.top > 0 || mOutsets.bottom > 0) {
+            int mode = MeasureSpec.getMode(heightMeasureSpec);
+            if (mode != MeasureSpec.UNSPECIFIED) {
+                int height = MeasureSpec.getSize(heightMeasureSpec);
+                heightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        height + mOutsets.top + mOutsets.bottom, mode);
+            }
+        }
+        if (mOutsets.left > 0 || mOutsets.right > 0) {
+            int mode = MeasureSpec.getMode(widthMeasureSpec);
+            if (mode != MeasureSpec.UNSPECIFIED) {
+                int width = MeasureSpec.getSize(widthMeasureSpec);
+                widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        width + mOutsets.left + mOutsets.right, mode);
+            }
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int width = getMeasuredWidth();
+        boolean measure = false;
+
+        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
+
+        if (!fixedWidth && widthMode == AT_MOST) {
+            final TypedValue tv = isPortrait ? mWindow.mMinWidthMinor : mWindow.mMinWidthMajor;
+            if (tv.type != TypedValue.TYPE_NULL) {
+                final int min;
+                if (tv.type == TypedValue.TYPE_DIMENSION) {
+                    min = (int)tv.getDimension(metrics);
+                } else if (tv.type == TypedValue.TYPE_FRACTION) {
+                    min = (int)tv.getFraction(mAvailableWidth, mAvailableWidth);
+                } else {
+                    min = 0;
+                }
+                if (DEBUG_MEASURE) Log.d(mLogTag, "Adjust for min width: " + min + ", value::"
+                        + tv.coerceToString() + ", mAvailableWidth=" + mAvailableWidth);
+
+                if (width < min) {
+                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
+                    measure = true;
+                }
+            }
+        }
+
+        // TODO: Support height?
+
+        if (measure) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        getOutsets(mOutsets);
+        if (mOutsets.left > 0) {
+            offsetLeftAndRight(-mOutsets.left);
+        }
+        if (mOutsets.top > 0) {
+            offsetTopAndBottom(-mOutsets.top);
+        }
+        if (mApplyFloatingVerticalInsets) {
+            offsetTopAndBottom(mFloatingInsets.top);
+        }
+        if (mApplyFloatingHorizontalInsets) {
+            offsetLeftAndRight(mFloatingInsets.left);
+        }
+
+        // If the application changed its SystemUI metrics, we might also have to adapt
+        // our shadow elevation.
+        updateElevation();
+        mAllowUpdateElevation = true;
+
+        if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) {
+            getViewRootImpl().requestInvalidateRootRenderNode();
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        if (mMenuBackground != null) {
+            mMenuBackground.draw(canvas);
+        }
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView) {
+        return showContextMenuForChildInternal(originalView, Float.NaN, Float.NaN);
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView, float x, float y) {
+        return showContextMenuForChildInternal(originalView, x, y);
+    }
+
+    private boolean showContextMenuForChildInternal(View originalView,
+            float x, float y) {
+        // Only allow one context menu at a time.
+        if (mWindow.mContextMenuHelper != null) {
+            mWindow.mContextMenuHelper.dismiss();
+            mWindow.mContextMenuHelper = null;
+        }
+
+        // Reuse the context menu builder.
+        final PhoneWindowMenuCallback callback = mWindow.mContextMenuCallback;
+        if (mWindow.mContextMenu == null) {
+            mWindow.mContextMenu = new ContextMenuBuilder(getContext());
+            mWindow.mContextMenu.setCallback(callback);
+        } else {
+            mWindow.mContextMenu.clearAll();
+        }
+
+        final MenuHelper helper;
+        final boolean isPopup = !Float.isNaN(x) && !Float.isNaN(y);
+        if (isPopup) {
+            helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y);
+        } else {
+            helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken());
+        }
+
+        if (helper != null) {
+            // If it's a dialog, the callback needs to handle showing
+            // sub-menus. Either way, the callback is required for propagating
+            // selection to Context.onContextMenuItemSelected().
+            callback.setShowDialogForSubmenu(!isPopup);
+            helper.setPresenterCallback(callback);
+        }
+
+        mWindow.mContextMenuHelper = helper;
+        return helper != null;
+    }
+
+    @Override
+    public ActionMode startActionModeForChild(View originalView,
+            ActionMode.Callback callback) {
+        return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY);
+    }
+
+    @Override
+    public ActionMode startActionModeForChild(
+            View child, ActionMode.Callback callback, int type) {
+        return startActionMode(child, callback, type);
+    }
+
+    @Override
+    public ActionMode startActionMode(ActionMode.Callback callback) {
+        return startActionMode(callback, ActionMode.TYPE_PRIMARY);
+    }
+
+    @Override
+    public ActionMode startActionMode(ActionMode.Callback callback, int type) {
+        return startActionMode(this, callback, type);
+    }
+
+    private ActionMode startActionMode(
+            View originatingView, ActionMode.Callback callback, int type) {
+        ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
+        ActionMode mode = null;
+        if (mWindow.getCallback() != null && !mWindow.isDestroyed()) {
+            try {
+                mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);
+            } catch (AbstractMethodError ame) {
+                // Older apps might not implement the typed version of this method.
+                if (type == ActionMode.TYPE_PRIMARY) {
+                    try {
+                        mode = mWindow.getCallback().onWindowStartingActionMode(
+                                wrappedCallback);
+                    } catch (AbstractMethodError ame2) {
+                        // Older apps might not implement this callback method at all.
+                    }
+                }
+            }
+        }
+        if (mode != null) {
+            if (mode.getType() == ActionMode.TYPE_PRIMARY) {
+                cleanupPrimaryActionMode();
+                mPrimaryActionMode = mode;
+            } else if (mode.getType() == ActionMode.TYPE_FLOATING) {
+                if (mFloatingActionMode != null) {
+                    mFloatingActionMode.finish();
+                }
+                mFloatingActionMode = mode;
+            }
+        } else {
+            mode = createActionMode(type, wrappedCallback, originatingView);
+            if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
+                setHandledActionMode(mode);
+            } else {
+                mode = null;
+            }
+        }
+        if (mode != null && mWindow.getCallback() != null && !mWindow.isDestroyed()) {
+            try {
+                mWindow.getCallback().onActionModeStarted(mode);
+            } catch (AbstractMethodError ame) {
+                // Older apps might not implement this callback method.
+            }
+        }
+        return mode;
+    }
+
+    private void cleanupPrimaryActionMode() {
+        if (mPrimaryActionMode != null) {
+            mPrimaryActionMode.finish();
+            mPrimaryActionMode = null;
+        }
+        if (mPrimaryActionModeView != null) {
+            mPrimaryActionModeView.killMode();
+        }
+    }
+
+    private void cleanupFloatingActionModeViews() {
+        if (mFloatingToolbar != null) {
+            mFloatingToolbar.dismiss();
+            mFloatingToolbar = null;
+        }
+        if (mFloatingActionModeOriginatingView != null) {
+            if (mFloatingToolbarPreDrawListener != null) {
+                mFloatingActionModeOriginatingView.getViewTreeObserver()
+                    .removeOnPreDrawListener(mFloatingToolbarPreDrawListener);
+                mFloatingToolbarPreDrawListener = null;
+            }
+            mFloatingActionModeOriginatingView = null;
+        }
+    }
+
+    void startChanging() {
+        mChanging = true;
+    }
+
+    void finishChanging() {
+        mChanging = false;
+        drawableChanged();
+    }
+
+    public void setWindowBackground(Drawable drawable) {
+        if (getBackground() != drawable) {
+            setBackgroundDrawable(drawable);
+            if (drawable != null) {
+                mResizingBackgroundDrawable = enforceNonTranslucentBackground(drawable,
+                        mWindow.isTranslucent() || mWindow.isShowingWallpaper());
+            } else {
+                mResizingBackgroundDrawable = getResizingBackgroundDrawable(
+                        getContext(), 0, mWindow.mBackgroundFallbackResource,
+                        mWindow.isTranslucent() || mWindow.isShowingWallpaper());
+            }
+            if (mResizingBackgroundDrawable != null) {
+                mResizingBackgroundDrawable.getPadding(mBackgroundPadding);
+            } else {
+                mBackgroundPadding.setEmpty();
+            }
+            drawableChanged();
+        }
+    }
+
+    public void setWindowFrame(Drawable drawable) {
+        if (getForeground() != drawable) {
+            setForeground(drawable);
+            if (drawable != null) {
+                drawable.getPadding(mFramePadding);
+            } else {
+                mFramePadding.setEmpty();
+            }
+            drawableChanged();
+        }
+    }
+
+    @Override
+    public void onWindowSystemUiVisibilityChanged(int visible) {
+        updateColorViews(null /* insets */, true /* animate */);
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        final WindowManager.LayoutParams attrs = mWindow.getAttributes();
+        mFloatingInsets.setEmpty();
+        if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
+            // For dialog windows we want to make sure they don't go over the status bar or nav bar.
+            // We consume the system insets and we will reuse them later during the measure phase.
+            // We allow the app to ignore this and handle insets itself by using
+            // FLAG_LAYOUT_IN_SCREEN.
+            if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
+                mFloatingInsets.top = insets.getSystemWindowInsetTop();
+                mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
+                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0,
+                        insets.getSystemWindowInsetRight(), 0);
+            }
+            if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
+                mFloatingInsets.left = insets.getSystemWindowInsetTop();
+                mFloatingInsets.right = insets.getSystemWindowInsetBottom();
+                insets = insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(),
+                        0, insets.getSystemWindowInsetBottom());
+            }
+        }
+        mFrameOffsets.set(insets.getSystemWindowInsets());
+        insets = updateColorViews(insets, true /* animate */);
+        insets = updateStatusGuard(insets);
+        insets = updateNavigationGuard(insets);
+        if (getForeground() != null) {
+            drawableChanged();
+        }
+        return insets;
+    }
+
+    @Override
+    public boolean isTransitionGroup() {
+        return false;
+    }
+
+    public static int getColorViewTopInset(int stableTop, int systemTop) {
+        return Math.min(stableTop, systemTop);
+    }
+
+    public static int getColorViewBottomInset(int stableBottom, int systemBottom) {
+        return Math.min(stableBottom, systemBottom);
+    }
+
+    public static int getColorViewRightInset(int stableRight, int systemRight) {
+        return Math.min(stableRight, systemRight);
+    }
+
+    public static int getColorViewLeftInset(int stableLeft, int systemLeft) {
+        return Math.min(stableLeft, systemLeft);
+    }
+
+    public static boolean isNavBarToRightEdge(int bottomInset, int rightInset) {
+        return bottomInset == 0 && rightInset > 0;
+    }
+
+    public static boolean isNavBarToLeftEdge(int bottomInset, int leftInset) {
+        return bottomInset == 0 && leftInset > 0;
+    }
+
+    public static int getNavBarSize(int bottomInset, int rightInset, int leftInset) {
+        return isNavBarToRightEdge(bottomInset, rightInset) ? rightInset
+                : isNavBarToLeftEdge(bottomInset, leftInset) ? leftInset : bottomInset;
+    }
+
+    public static void getNavigationBarRect(int canvasWidth, int canvasHeight, Rect stableInsets,
+            Rect contentInsets, Rect outRect) {
+        final int bottomInset = getColorViewBottomInset(stableInsets.bottom, contentInsets.bottom);
+        final int leftInset = getColorViewLeftInset(stableInsets.left, contentInsets.left);
+        final int rightInset = getColorViewLeftInset(stableInsets.right, contentInsets.right);
+        final int size = getNavBarSize(bottomInset, rightInset, leftInset);
+        if (isNavBarToRightEdge(bottomInset, rightInset)) {
+            outRect.set(canvasWidth - size, 0, canvasWidth, canvasHeight);
+        } else if (isNavBarToLeftEdge(bottomInset, leftInset)) {
+            outRect.set(0, 0, size, canvasHeight);
+        } else {
+            outRect.set(0, canvasHeight - size, canvasWidth, canvasHeight);
+        }
+    }
+
+    WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
+        WindowManager.LayoutParams attrs = mWindow.getAttributes();
+        int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
+
+        if (!mWindow.mIsFloating) {
+            boolean disallowAnimate = !isLaidOut();
+            disallowAnimate |= ((mLastWindowFlags ^ attrs.flags)
+                    & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
+            mLastWindowFlags = attrs.flags;
+
+            if (insets != null) {
+                mLastTopInset = getColorViewTopInset(insets.getStableInsetTop(),
+                        insets.getSystemWindowInsetTop());
+                mLastBottomInset = getColorViewBottomInset(insets.getStableInsetBottom(),
+                        insets.getSystemWindowInsetBottom());
+                mLastRightInset = getColorViewRightInset(insets.getStableInsetRight(),
+                        insets.getSystemWindowInsetRight());
+                mLastLeftInset = getColorViewRightInset(insets.getStableInsetLeft(),
+                        insets.getSystemWindowInsetLeft());
+
+                // Don't animate if the presence of stable insets has changed, because that
+                // indicates that the window was either just added and received them for the
+                // first time, or the window size or position has changed.
+                boolean hasTopStableInset = insets.getStableInsetTop() != 0;
+                disallowAnimate |= (hasTopStableInset != mLastHasTopStableInset);
+                mLastHasTopStableInset = hasTopStableInset;
+
+                boolean hasBottomStableInset = insets.getStableInsetBottom() != 0;
+                disallowAnimate |= (hasBottomStableInset != mLastHasBottomStableInset);
+                mLastHasBottomStableInset = hasBottomStableInset;
+
+                boolean hasRightStableInset = insets.getStableInsetRight() != 0;
+                disallowAnimate |= (hasRightStableInset != mLastHasRightStableInset);
+                mLastHasRightStableInset = hasRightStableInset;
+
+                boolean hasLeftStableInset = insets.getStableInsetLeft() != 0;
+                disallowAnimate |= (hasLeftStableInset != mLastHasLeftStableInset);
+                mLastHasLeftStableInset = hasLeftStableInset;
+
+                mLastShouldAlwaysConsumeNavBar = insets.shouldAlwaysConsumeNavBar();
+            }
+
+            boolean navBarToRightEdge = isNavBarToRightEdge(mLastBottomInset, mLastRightInset);
+            boolean navBarToLeftEdge = isNavBarToLeftEdge(mLastBottomInset, mLastLeftInset);
+            int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset);
+            updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
+                    mWindow.mNavigationBarColor, mWindow.mNavigationBarDividerColor, navBarSize,
+                    navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge,
+                    0 /* sideInset */, animate && !disallowAnimate, false /* force */);
+
+            boolean statusBarNeedsRightInset = navBarToRightEdge
+                    && mNavigationColorViewState.present;
+            boolean statusBarNeedsLeftInset = navBarToLeftEdge
+                    && mNavigationColorViewState.present;
+            int statusBarSideInset = statusBarNeedsRightInset ? mLastRightInset
+                    : statusBarNeedsLeftInset ? mLastLeftInset : 0;
+            updateColorViewInt(mStatusColorViewState, sysUiVisibility,
+                    calculateStatusBarColor(), 0, mLastTopInset,
+                    false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset,
+                    animate && !disallowAnimate,
+                    mForceWindowDrawsStatusBarBackground);
+        }
+
+        // When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, we still need
+        // to ensure that the rest of the view hierarchy doesn't notice it, unless they've
+        // explicitly asked for it.
+        boolean consumingNavBar =
+                (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
+                        && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
+                        && (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0
+                || mLastShouldAlwaysConsumeNavBar;
+
+        // If we didn't request fullscreen layout, but we still got it because of the
+        // mForceWindowDrawsStatusBarBackground flag, also consume top inset.
+        boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
+                && (sysUiVisibility & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
+                && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
+                && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
+                && mForceWindowDrawsStatusBarBackground
+                && mLastTopInset != 0;
+
+        int consumedTop = consumingStatusBar ? mLastTopInset : 0;
+        int consumedRight = consumingNavBar ? mLastRightInset : 0;
+        int consumedBottom = consumingNavBar ? mLastBottomInset : 0;
+        int consumedLeft = consumingNavBar ? mLastLeftInset : 0;
+
+        if (mContentRoot != null
+                && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
+            MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
+            if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
+                    || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
+                lp.topMargin = consumedTop;
+                lp.rightMargin = consumedRight;
+                lp.bottomMargin = consumedBottom;
+                lp.leftMargin = consumedLeft;
+                mContentRoot.setLayoutParams(lp);
+
+                if (insets == null) {
+                    // The insets have changed, but we're not currently in the process
+                    // of dispatching them.
+                    requestApplyInsets();
+                }
+            }
+            if (insets != null) {
+                insets = insets.replaceSystemWindowInsets(
+                        insets.getSystemWindowInsetLeft() - consumedLeft,
+                        insets.getSystemWindowInsetTop() - consumedTop,
+                        insets.getSystemWindowInsetRight() - consumedRight,
+                        insets.getSystemWindowInsetBottom() - consumedBottom);
+            }
+        }
+
+        if (insets != null) {
+            insets = insets.consumeStableInsets();
+        }
+        return insets;
+    }
+
+    private int calculateStatusBarColor() {
+        return calculateStatusBarColor(mWindow.getAttributes().flags,
+                mSemiTransparentStatusBarColor, mWindow.mStatusBarColor);
+    }
+
+    public static int calculateStatusBarColor(int flags, int semiTransparentStatusBarColor,
+            int statusBarColor) {
+        return (flags & FLAG_TRANSLUCENT_STATUS) != 0 ? semiTransparentStatusBarColor
+                : (flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 ? statusBarColor
+                : Color.BLACK;
+    }
+
+    private int getCurrentColor(ColorViewState state) {
+        if (state.visible) {
+            return state.color;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Update a color view
+     *
+     * @param state the color view to update.
+     * @param sysUiVis the current systemUiVisibility to apply.
+     * @param color the current color to apply.
+     * @param dividerColor the current divider color to apply.
+     * @param size the current size in the non-parent-matching dimension.
+     * @param verticalBar if true the view is attached to a vertical edge, otherwise to a
+     *                    horizontal edge,
+     * @param sideMargin sideMargin for the color view.
+     * @param animate if true, the change will be animated.
+     */
+    private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
+            int dividerColor, int size, boolean verticalBar, boolean seascape, int sideMargin,
+            boolean animate, boolean force) {
+        state.present = state.attributes.isPresent(sysUiVis, mWindow.getAttributes().flags, force);
+        boolean show = state.attributes.isVisible(state.present, color,
+                mWindow.getAttributes().flags, force);
+        boolean showView = show && !isResizing() && size > 0;
+
+        boolean visibilityChanged = false;
+        View view = state.view;
+
+        int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size;
+        int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT;
+        int resolvedGravity = verticalBar
+                ? (seascape ? state.attributes.seascapeGravity : state.attributes.horizontalGravity)
+                : state.attributes.verticalGravity;
+
+        if (view == null) {
+            if (showView) {
+                state.view = view = new View(mContext);
+                setColor(view, color, dividerColor, verticalBar, seascape);
+                view.setTransitionName(state.attributes.transitionName);
+                view.setId(state.attributes.id);
+                visibilityChanged = true;
+                view.setVisibility(INVISIBLE);
+                state.targetVisibility = VISIBLE;
+
+                LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
+                        resolvedGravity);
+                if (seascape) {
+                    lp.leftMargin = sideMargin;
+                } else {
+                    lp.rightMargin = sideMargin;
+                }
+                addView(view, lp);
+                updateColorViewTranslations();
+            }
+        } else {
+            int vis = showView ? VISIBLE : INVISIBLE;
+            visibilityChanged = state.targetVisibility != vis;
+            state.targetVisibility = vis;
+            LayoutParams lp = (LayoutParams) view.getLayoutParams();
+            int rightMargin = seascape ? 0 : sideMargin;
+            int leftMargin = seascape ? sideMargin : 0;
+            if (lp.height != resolvedHeight || lp.width != resolvedWidth
+                    || lp.gravity != resolvedGravity || lp.rightMargin != rightMargin
+                    || lp.leftMargin != leftMargin) {
+                lp.height = resolvedHeight;
+                lp.width = resolvedWidth;
+                lp.gravity = resolvedGravity;
+                lp.rightMargin = rightMargin;
+                lp.leftMargin = leftMargin;
+                view.setLayoutParams(lp);
+            }
+            if (showView) {
+                setColor(view, color, dividerColor, verticalBar, seascape);
+            }
+        }
+        if (visibilityChanged) {
+            view.animate().cancel();
+            if (animate && !isResizing()) {
+                if (showView) {
+                    if (view.getVisibility() != VISIBLE) {
+                        view.setVisibility(VISIBLE);
+                        view.setAlpha(0.0f);
+                    }
+                    view.animate().alpha(1.0f).setInterpolator(mShowInterpolator).
+                            setDuration(mBarEnterExitDuration);
+                } else {
+                    view.animate().alpha(0.0f).setInterpolator(mHideInterpolator)
+                            .setDuration(mBarEnterExitDuration)
+                            .withEndAction(new Runnable() {
+                                @Override
+                                public void run() {
+                                    state.view.setAlpha(1.0f);
+                                    state.view.setVisibility(INVISIBLE);
+                                }
+                            });
+                }
+            } else {
+                view.setAlpha(1.0f);
+                view.setVisibility(showView ? VISIBLE : INVISIBLE);
+            }
+        }
+        state.visible = show;
+        state.color = color;
+    }
+
+    private static void setColor(View v, int color, int dividerColor, boolean verticalBar,
+            boolean seascape) {
+        if (dividerColor != 0) {
+            final Pair<Boolean, Boolean> dir = (Pair<Boolean, Boolean>) v.getTag();
+            if (dir == null || dir.first != verticalBar || dir.second != seascape) {
+                final int size = Math.round(
+                        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
+                                v.getContext().getResources().getDisplayMetrics()));
+                // Use an inset to make the divider line on the side that faces the app.
+                final InsetDrawable d = new InsetDrawable(new ColorDrawable(color),
+                        verticalBar && !seascape ? size : 0,
+                        !verticalBar ? size : 0,
+                        verticalBar && seascape ? size : 0, 0);
+                v.setBackground(new LayerDrawable(new Drawable[] {
+                        new ColorDrawable(dividerColor), d }));
+                v.setTag(new Pair<>(verticalBar, seascape));
+            } else {
+                final LayerDrawable d = (LayerDrawable) v.getBackground();
+                final InsetDrawable inset = ((InsetDrawable) d.getDrawable(1));
+                ((ColorDrawable) inset.getDrawable()).setColor(color);
+                ((ColorDrawable) d.getDrawable(0)).setColor(dividerColor);
+            }
+        } else {
+            v.setTag(null);
+            v.setBackgroundColor(color);
+        }
+    }
+
+    private void updateColorViewTranslations() {
+        // Put the color views back in place when they get moved off the screen
+        // due to the the ViewRootImpl panning.
+        int rootScrollY = mRootScrollY;
+        if (mStatusColorViewState.view != null) {
+            mStatusColorViewState.view.setTranslationY(rootScrollY > 0 ? rootScrollY : 0);
+        }
+        if (mNavigationColorViewState.view != null) {
+            mNavigationColorViewState.view.setTranslationY(rootScrollY < 0 ? rootScrollY : 0);
+        }
+    }
+
+    private WindowInsets updateStatusGuard(WindowInsets insets) {
+        boolean showStatusGuard = false;
+        // Show the status guard when the non-overlay contextual action bar is showing
+        if (mPrimaryActionModeView != null) {
+            if (mPrimaryActionModeView.getLayoutParams() instanceof MarginLayoutParams) {
+                // Insets are magic!
+                final MarginLayoutParams mlp = (MarginLayoutParams)
+                        mPrimaryActionModeView.getLayoutParams();
+                boolean mlpChanged = false;
+                if (mPrimaryActionModeView.isShown()) {
+                    if (mTempRect == null) {
+                        mTempRect = new Rect();
+                    }
+                    final Rect rect = mTempRect;
+
+                    // If the parent doesn't consume the insets, manually
+                    // apply the default system window insets.
+                    mWindow.mContentParent.computeSystemWindowInsets(insets, rect);
+                    final int newMargin = rect.top == 0 ? insets.getSystemWindowInsetTop() : 0;
+                    if (mlp.topMargin != newMargin) {
+                        mlpChanged = true;
+                        mlp.topMargin = insets.getSystemWindowInsetTop();
+
+                        if (mStatusGuard == null) {
+                            mStatusGuard = new View(mContext);
+                            mStatusGuard.setBackgroundColor(mContext.getColor(
+                                    R.color.input_method_navigation_guard));
+                            addView(mStatusGuard, indexOfChild(mStatusColorViewState.view),
+                                    new LayoutParams(LayoutParams.MATCH_PARENT,
+                                            mlp.topMargin, Gravity.START | Gravity.TOP));
+                        } else {
+                            final LayoutParams lp = (LayoutParams)
+                                    mStatusGuard.getLayoutParams();
+                            if (lp.height != mlp.topMargin) {
+                                lp.height = mlp.topMargin;
+                                mStatusGuard.setLayoutParams(lp);
+                            }
+                        }
+                    }
+
+                    // The action mode's theme may differ from the app, so
+                    // always show the status guard above it if we have one.
+                    showStatusGuard = mStatusGuard != null;
+
+                    // We only need to consume the insets if the action
+                    // mode is overlaid on the app content (e.g. it's
+                    // sitting in a FrameLayout, see
+                    // screen_simple_overlay_action_mode.xml).
+                    final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate()
+                            & (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0;
+                    insets = insets.consumeSystemWindowInsets(
+                            false, nonOverlay && showStatusGuard /* top */, false, false);
+                } else {
+                    // reset top margin
+                    if (mlp.topMargin != 0) {
+                        mlpChanged = true;
+                        mlp.topMargin = 0;
+                    }
+                }
+                if (mlpChanged) {
+                    mPrimaryActionModeView.setLayoutParams(mlp);
+                }
+            }
+        }
+        if (mStatusGuard != null) {
+            mStatusGuard.setVisibility(showStatusGuard ? View.VISIBLE : View.GONE);
+        }
+        return insets;
+    }
+
+    private WindowInsets updateNavigationGuard(WindowInsets insets) {
+        // IME windows lay out below the nav bar, but the content view must not (for back compat)
+        // Only make this adjustment if the window is not requesting layout in overscan
+        if (mWindow.getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
+                && (mWindow.getAttributes().flags & FLAG_LAYOUT_IN_OVERSCAN) == 0) {
+            // prevent the content view from including the nav bar height
+            if (mWindow.mContentParent != null) {
+                if (mWindow.mContentParent.getLayoutParams() instanceof MarginLayoutParams) {
+                    MarginLayoutParams mlp =
+                            (MarginLayoutParams) mWindow.mContentParent.getLayoutParams();
+                    mlp.bottomMargin = insets.getSystemWindowInsetBottom();
+                    mWindow.mContentParent.setLayoutParams(mlp);
+                }
+            }
+            // position the navigation guard view, creating it if necessary
+            if (mNavigationGuard == null) {
+                mNavigationGuard = new View(mContext);
+                mNavigationGuard.setBackgroundColor(mContext.getColor(
+                        R.color.input_method_navigation_guard));
+                addView(mNavigationGuard, indexOfChild(mNavigationColorViewState.view),
+                        new LayoutParams(LayoutParams.MATCH_PARENT,
+                                insets.getSystemWindowInsetBottom(),
+                                Gravity.START | Gravity.BOTTOM));
+            } else {
+                LayoutParams lp = (LayoutParams) mNavigationGuard.getLayoutParams();
+                lp.height = insets.getSystemWindowInsetBottom();
+                mNavigationGuard.setLayoutParams(lp);
+            }
+            updateNavigationGuardColor();
+            insets = insets.consumeSystemWindowInsets(
+                    false, false, false, true /* bottom */);
+        }
+        return insets;
+    }
+
+    void updateNavigationGuardColor() {
+        if (mNavigationGuard != null) {
+            // Make navigation bar guard invisible if the transparent color is specified.
+            // Only TRANSPARENT is sufficient for hiding the navigation bar if the no software
+            // keyboard is shown by IMS.
+            mNavigationGuard.setVisibility(mWindow.getNavigationBarColor() == Color.TRANSPARENT ?
+                    View.INVISIBLE : View.VISIBLE);
+        }
+    }
+
+    /**
+     * Overrides the view outline when the activity enters picture-in-picture to ensure that it has
+     * an opaque shadow even if the window background is completely transparent. This only applies
+     * to activities that are currently the task root.
+     */
+    public void updatePictureInPictureOutlineProvider(boolean isInPictureInPictureMode) {
+        if (mIsInPictureInPictureMode == isInPictureInPictureMode) {
+            return;
+        }
+
+        if (isInPictureInPictureMode) {
+            final Window.WindowControllerCallback callback =
+                    mWindow.getWindowControllerCallback();
+            if (callback != null && callback.isTaskRoot()) {
+                // Call super implementation directly as we don't want to save the PIP outline
+                // provider to be restored
+                super.setOutlineProvider(PIP_OUTLINE_PROVIDER);
+            }
+        } else {
+            // Restore the previous outline provider
+            if (getOutlineProvider() != mLastOutlineProvider) {
+                setOutlineProvider(mLastOutlineProvider);
+            }
+        }
+        mIsInPictureInPictureMode = isInPictureInPictureMode;
+    }
+
+    @Override
+    public void setOutlineProvider(ViewOutlineProvider provider) {
+        super.setOutlineProvider(provider);
+
+        // Save the outline provider set to ensure that we can restore when the activity leaves PiP
+        mLastOutlineProvider = provider;
+    }
+
+    private void drawableChanged() {
+        if (mChanging) {
+            return;
+        }
+
+        setPadding(mFramePadding.left + mBackgroundPadding.left,
+                mFramePadding.top + mBackgroundPadding.top,
+                mFramePadding.right + mBackgroundPadding.right,
+                mFramePadding.bottom + mBackgroundPadding.bottom);
+        requestLayout();
+        invalidate();
+
+        int opacity = PixelFormat.OPAQUE;
+        final WindowConfiguration winConfig = getResources().getConfiguration().windowConfiguration;
+        if (winConfig.hasWindowShadow()) {
+            // If the window has a shadow, it must be translucent.
+            opacity = PixelFormat.TRANSLUCENT;
+        } else{
+            // Note: If there is no background, we will assume opaque. The
+            // common case seems to be that an application sets there to be
+            // no background so it can draw everything itself. For that,
+            // we would like to assume OPAQUE and let the app force it to
+            // the slower TRANSLUCENT mode if that is really what it wants.
+            Drawable bg = getBackground();
+            Drawable fg = getForeground();
+            if (bg != null) {
+                if (fg == null) {
+                    opacity = bg.getOpacity();
+                } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0
+                        && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) {
+                    // If the frame padding is zero, then we can be opaque
+                    // if either the frame -or- the background is opaque.
+                    int fop = fg.getOpacity();
+                    int bop = bg.getOpacity();
+                    if (false)
+                        Log.v(mLogTag, "Background opacity: " + bop + ", Frame opacity: " + fop);
+                    if (fop == PixelFormat.OPAQUE || bop == PixelFormat.OPAQUE) {
+                        opacity = PixelFormat.OPAQUE;
+                    } else if (fop == PixelFormat.UNKNOWN) {
+                        opacity = bop;
+                    } else if (bop == PixelFormat.UNKNOWN) {
+                        opacity = fop;
+                    } else {
+                        opacity = Drawable.resolveOpacity(fop, bop);
+                    }
+                } else {
+                    // For now we have to assume translucent if there is a
+                    // frame with padding... there is no way to tell if the
+                    // frame and background together will draw all pixels.
+                    if (false)
+                        Log.v(mLogTag, "Padding: " + mFramePadding);
+                    opacity = PixelFormat.TRANSLUCENT;
+                }
+            }
+            if (false)
+                Log.v(mLogTag, "Background: " + bg + ", Frame: " + fg);
+        }
+
+        if (false)
+            Log.v(mLogTag, "Selected default opacity: " + opacity);
+
+        mDefaultOpacity = opacity;
+        if (mFeatureId < 0) {
+            mWindow.setDefaultWindowFormat(opacity);
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+
+        // If the user is chording a menu shortcut, release the chord since
+        // this window lost focus
+        if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL) && !hasWindowFocus
+                && mWindow.mPanelChordingKey != 0) {
+            mWindow.closePanel(Window.FEATURE_OPTIONS_PANEL);
+        }
+
+        final Window.Callback cb = mWindow.getCallback();
+        if (cb != null && !mWindow.isDestroyed() && mFeatureId < 0) {
+            cb.onWindowFocusChanged(hasWindowFocus);
+        }
+
+        if (mPrimaryActionMode != null) {
+            mPrimaryActionMode.onWindowFocusChanged(hasWindowFocus);
+        }
+        if (mFloatingActionMode != null) {
+            mFloatingActionMode.onWindowFocusChanged(hasWindowFocus);
+        }
+
+        updateElevation();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        final Window.Callback cb = mWindow.getCallback();
+        if (cb != null && !mWindow.isDestroyed() && mFeatureId < 0) {
+            cb.onAttachedToWindow();
+        }
+
+        if (mFeatureId == -1) {
+            /*
+             * The main window has been attached, try to restore any panels
+             * that may have been open before. This is called in cases where
+             * an activity is being killed for configuration change and the
+             * menu was open. When the activity is recreated, the menu
+             * should be shown again.
+             */
+            mWindow.openPanelsAfterRestore();
+        }
+
+        if (!mWindowResizeCallbacksAdded) {
+            // If there is no window callback installed there was no window set before. Set it now.
+            // Note that our ViewRootImpl object will not change.
+            getViewRootImpl().addWindowCallbacks(this);
+            mWindowResizeCallbacksAdded = true;
+        } else if (mBackdropFrameRenderer != null) {
+            // We are resizing and this call happened due to a configuration change. Tell the
+            // renderer about it.
+            mBackdropFrameRenderer.onConfigurationChange();
+        }
+        mWindow.onViewRootImplSet(getViewRootImpl());
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        final Window.Callback cb = mWindow.getCallback();
+        if (cb != null && mFeatureId < 0) {
+            cb.onDetachedFromWindow();
+        }
+
+        if (mWindow.mDecorContentParent != null) {
+            mWindow.mDecorContentParent.dismissPopups();
+        }
+
+        if (mPrimaryActionModePopup != null) {
+            removeCallbacks(mShowPrimaryActionModePopup);
+            if (mPrimaryActionModePopup.isShowing()) {
+                mPrimaryActionModePopup.dismiss();
+            }
+            mPrimaryActionModePopup = null;
+        }
+        if (mFloatingToolbar != null) {
+            mFloatingToolbar.dismiss();
+            mFloatingToolbar = null;
+        }
+
+        PhoneWindow.PanelFeatureState st = mWindow.getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
+        if (st != null && st.menu != null && mFeatureId < 0) {
+            st.menu.close();
+        }
+
+        releaseThreadedRenderer();
+
+        if (mWindowResizeCallbacksAdded) {
+            getViewRootImpl().removeWindowCallbacks(this);
+            mWindowResizeCallbacksAdded = false;
+        }
+    }
+
+    @Override
+    public void onCloseSystemDialogs(String reason) {
+        if (mFeatureId >= 0) {
+            mWindow.closeAllPanels();
+        }
+    }
+
+    public android.view.SurfaceHolder.Callback2 willYouTakeTheSurface() {
+        return mFeatureId < 0 ? mWindow.mTakeSurfaceCallback : null;
+    }
+
+    public InputQueue.Callback willYouTakeTheInputQueue() {
+        return mFeatureId < 0 ? mWindow.mTakeInputQueueCallback : null;
+    }
+
+    public void setSurfaceType(int type) {
+        mWindow.setType(type);
+    }
+
+    public void setSurfaceFormat(int format) {
+        mWindow.setFormat(format);
+    }
+
+    public void setSurfaceKeepScreenOn(boolean keepOn) {
+        if (keepOn) mWindow.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        else mWindow.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    @Override
+    public void onRootViewScrollYChanged(int rootScrollY) {
+        mRootScrollY = rootScrollY;
+        updateColorViewTranslations();
+    }
+
+    private ActionMode createActionMode(
+            int type, ActionMode.Callback2 callback, View originatingView) {
+        switch (type) {
+            case ActionMode.TYPE_PRIMARY:
+            default:
+                return createStandaloneActionMode(callback);
+            case ActionMode.TYPE_FLOATING:
+                return createFloatingActionMode(originatingView, callback);
+        }
+    }
+
+    private void setHandledActionMode(ActionMode mode) {
+        if (mode.getType() == ActionMode.TYPE_PRIMARY) {
+            setHandledPrimaryActionMode(mode);
+        } else if (mode.getType() == ActionMode.TYPE_FLOATING) {
+            setHandledFloatingActionMode(mode);
+        }
+    }
+
+    private ActionMode createStandaloneActionMode(ActionMode.Callback callback) {
+        endOnGoingFadeAnimation();
+        cleanupPrimaryActionMode();
+        // We want to create new mPrimaryActionModeView in two cases: if there is no existing
+        // instance at all, or if there is one, but it is detached from window. The latter case
+        // might happen when app is resized in multi-window mode and decor view is preserved
+        // along with the main app window. Keeping mPrimaryActionModeView reference doesn't cause
+        // app memory leaks because killMode() is called when the dismiss animation ends and from
+        // cleanupPrimaryActionMode() invocation above.
+        if (mPrimaryActionModeView == null || !mPrimaryActionModeView.isAttachedToWindow()) {
+            if (mWindow.isFloating()) {
+                // Use the action bar theme.
+                final TypedValue outValue = new TypedValue();
+                final Resources.Theme baseTheme = mContext.getTheme();
+                baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true);
+
+                final Context actionBarContext;
+                if (outValue.resourceId != 0) {
+                    final Resources.Theme actionBarTheme = mContext.getResources().newTheme();
+                    actionBarTheme.setTo(baseTheme);
+                    actionBarTheme.applyStyle(outValue.resourceId, true);
+
+                    actionBarContext = new ContextThemeWrapper(mContext, 0);
+                    actionBarContext.getTheme().setTo(actionBarTheme);
+                } else {
+                    actionBarContext = mContext;
+                }
+
+                mPrimaryActionModeView = new ActionBarContextView(actionBarContext);
+                mPrimaryActionModePopup = new PopupWindow(actionBarContext, null,
+                        R.attr.actionModePopupWindowStyle);
+                mPrimaryActionModePopup.setWindowLayoutType(
+                        WindowManager.LayoutParams.TYPE_APPLICATION);
+                mPrimaryActionModePopup.setContentView(mPrimaryActionModeView);
+                mPrimaryActionModePopup.setWidth(MATCH_PARENT);
+
+                actionBarContext.getTheme().resolveAttribute(
+                        R.attr.actionBarSize, outValue, true);
+                final int height = TypedValue.complexToDimensionPixelSize(outValue.data,
+                        actionBarContext.getResources().getDisplayMetrics());
+                mPrimaryActionModeView.setContentHeight(height);
+                mPrimaryActionModePopup.setHeight(WRAP_CONTENT);
+                mShowPrimaryActionModePopup = new Runnable() {
+                    public void run() {
+                        mPrimaryActionModePopup.showAtLocation(
+                                mPrimaryActionModeView.getApplicationWindowToken(),
+                                Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0);
+                        endOnGoingFadeAnimation();
+
+                        if (shouldAnimatePrimaryActionModeView()) {
+                            mFadeAnim = ObjectAnimator.ofFloat(mPrimaryActionModeView, View.ALPHA,
+                                    0f, 1f);
+                            mFadeAnim.addListener(new AnimatorListenerAdapter() {
+                                @Override
+                                public void onAnimationStart(Animator animation) {
+                                    mPrimaryActionModeView.setVisibility(VISIBLE);
+                                }
+
+                                @Override
+                                public void onAnimationEnd(Animator animation) {
+                                    mPrimaryActionModeView.setAlpha(1f);
+                                    mFadeAnim = null;
+                                }
+                            });
+                            mFadeAnim.start();
+                        } else {
+                            mPrimaryActionModeView.setAlpha(1f);
+                            mPrimaryActionModeView.setVisibility(VISIBLE);
+                        }
+                    }
+                };
+            } else {
+                ViewStub stub = findViewById(R.id.action_mode_bar_stub);
+                if (stub != null) {
+                    mPrimaryActionModeView = (ActionBarContextView) stub.inflate();
+                    mPrimaryActionModePopup = null;
+                }
+            }
+        }
+        if (mPrimaryActionModeView != null) {
+            mPrimaryActionModeView.killMode();
+            ActionMode mode = new StandaloneActionMode(
+                    mPrimaryActionModeView.getContext(), mPrimaryActionModeView,
+                    callback, mPrimaryActionModePopup == null);
+            return mode;
+        }
+        return null;
+    }
+
+    private void endOnGoingFadeAnimation() {
+        if (mFadeAnim != null) {
+            mFadeAnim.end();
+        }
+    }
+
+    private void setHandledPrimaryActionMode(ActionMode mode) {
+        endOnGoingFadeAnimation();
+        mPrimaryActionMode = mode;
+        mPrimaryActionMode.invalidate();
+        mPrimaryActionModeView.initForMode(mPrimaryActionMode);
+        if (mPrimaryActionModePopup != null) {
+            post(mShowPrimaryActionModePopup);
+        } else {
+            if (shouldAnimatePrimaryActionModeView()) {
+                mFadeAnim = ObjectAnimator.ofFloat(mPrimaryActionModeView, View.ALPHA, 0f, 1f);
+                mFadeAnim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animation) {
+                        mPrimaryActionModeView.setVisibility(View.VISIBLE);
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mPrimaryActionModeView.setAlpha(1f);
+                        mFadeAnim = null;
+                    }
+                });
+                mFadeAnim.start();
+            } else {
+                mPrimaryActionModeView.setAlpha(1f);
+                mPrimaryActionModeView.setVisibility(View.VISIBLE);
+            }
+        }
+        mPrimaryActionModeView.sendAccessibilityEvent(
+                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+    }
+
+    boolean shouldAnimatePrimaryActionModeView() {
+        // We only to animate the action mode in if the decor has already been laid out.
+        // If it hasn't been laid out, it hasn't been drawn to screen yet.
+        return isLaidOut();
+    }
+
+    private ActionMode createFloatingActionMode(
+            View originatingView, ActionMode.Callback2 callback) {
+        if (mFloatingActionMode != null) {
+            mFloatingActionMode.finish();
+        }
+        cleanupFloatingActionModeViews();
+        mFloatingToolbar = new FloatingToolbar(mWindow);
+        final FloatingActionMode mode =
+                new FloatingActionMode(mContext, callback, originatingView, mFloatingToolbar);
+        mFloatingActionModeOriginatingView = originatingView;
+        mFloatingToolbarPreDrawListener =
+            new ViewTreeObserver.OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                    mode.updateViewLocationInWindow();
+                    return true;
+                }
+            };
+        return mode;
+    }
+
+    private void setHandledFloatingActionMode(ActionMode mode) {
+        mFloatingActionMode = mode;
+        mFloatingActionMode.invalidate();  // Will show the floating toolbar if necessary.
+        mFloatingActionModeOriginatingView.getViewTreeObserver()
+            .addOnPreDrawListener(mFloatingToolbarPreDrawListener);
+    }
+
+    /**
+     * Informs the decor if the caption is attached and visible.
+     * @param attachedAndVisible true when the decor is visible.
+     * Note that this will even be called if there is no caption.
+     **/
+    void enableCaption(boolean attachedAndVisible) {
+        if (mHasCaption != attachedAndVisible) {
+            mHasCaption = attachedAndVisible;
+            if (getForeground() != null) {
+                drawableChanged();
+            }
+        }
+    }
+
+    void setWindow(PhoneWindow phoneWindow) {
+        mWindow = phoneWindow;
+        Context context = getContext();
+        if (context instanceof DecorContext) {
+            DecorContext decorContext = (DecorContext) context;
+            decorContext.setPhoneWindow(mWindow);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        final boolean displayWindowDecor =
+                newConfig.windowConfiguration.hasWindowDecorCaption();
+        if (mDecorCaptionView == null && displayWindowDecor) {
+            // Configuration now requires a caption.
+            final LayoutInflater inflater = mWindow.getLayoutInflater();
+            mDecorCaptionView = createDecorCaptionView(inflater);
+            if (mDecorCaptionView != null) {
+                if (mDecorCaptionView.getParent() == null) {
+                    addView(mDecorCaptionView, 0,
+                            new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+                }
+                removeView(mContentRoot);
+                mDecorCaptionView.addView(mContentRoot,
+                        new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
+            }
+        } else if (mDecorCaptionView != null) {
+            // We might have to change the kind of surface before we do anything else.
+            mDecorCaptionView.onConfigurationChanged(displayWindowDecor);
+            enableCaption(displayWindowDecor);
+        }
+
+        updateAvailableWidth();
+        initializeElevation();
+    }
+
+    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
+        if (mBackdropFrameRenderer != null) {
+            loadBackgroundDrawablesIfNeeded();
+            mBackdropFrameRenderer.onResourcesLoaded(
+                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
+                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
+                    getCurrentColor(mNavigationColorViewState));
+        }
+
+        mDecorCaptionView = createDecorCaptionView(inflater);
+        final View root = inflater.inflate(layoutResource, null);
+        if (mDecorCaptionView != null) {
+            if (mDecorCaptionView.getParent() == null) {
+                addView(mDecorCaptionView,
+                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+            }
+            mDecorCaptionView.addView(root,
+                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
+        } else {
+
+            // Put it below the color views.
+            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+        }
+        mContentRoot = (ViewGroup) root;
+        initializeElevation();
+    }
+
+    private void loadBackgroundDrawablesIfNeeded() {
+        if (mResizingBackgroundDrawable == null) {
+            mResizingBackgroundDrawable = getResizingBackgroundDrawable(getContext(),
+                    mWindow.mBackgroundResource, mWindow.mBackgroundFallbackResource,
+                    mWindow.isTranslucent() || mWindow.isShowingWallpaper());
+            if (mResizingBackgroundDrawable == null) {
+                // We shouldn't really get here as the background fallback should be always
+                // available since it is defaulted by the system.
+                Log.w(mLogTag, "Failed to find background drawable for PhoneWindow=" + mWindow);
+            }
+        }
+        if (mCaptionBackgroundDrawable == null) {
+            mCaptionBackgroundDrawable = getContext().getDrawable(
+                    R.drawable.decor_caption_title_focused);
+        }
+        if (mResizingBackgroundDrawable != null) {
+            mLastBackgroundDrawableCb = mResizingBackgroundDrawable.getCallback();
+            mResizingBackgroundDrawable.setCallback(null);
+        }
+    }
+
+    // Free floating overlapping windows require a caption.
+    private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {
+        DecorCaptionView decorCaptionView = null;
+        for (int i = getChildCount() - 1; i >= 0 && decorCaptionView == null; i--) {
+            View view = getChildAt(i);
+            if (view instanceof DecorCaptionView) {
+                // The decor was most likely saved from a relaunch - so reuse it.
+                decorCaptionView = (DecorCaptionView) view;
+                removeViewAt(i);
+            }
+        }
+        final WindowManager.LayoutParams attrs = mWindow.getAttributes();
+        final boolean isApplication = attrs.type == TYPE_BASE_APPLICATION ||
+                attrs.type == TYPE_APPLICATION || attrs.type == TYPE_DRAWN_APPLICATION;
+        final WindowConfiguration winConfig = getResources().getConfiguration().windowConfiguration;
+        // Only a non floating application window on one of the allowed workspaces can get a caption
+        if (!mWindow.isFloating() && isApplication && winConfig.hasWindowDecorCaption()) {
+            // Dependent on the brightness of the used title we either use the
+            // dark or the light button frame.
+            if (decorCaptionView == null) {
+                decorCaptionView = inflateDecorCaptionView(inflater);
+            }
+            decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
+        } else {
+            decorCaptionView = null;
+        }
+
+        // Tell the decor if it has a visible caption.
+        enableCaption(decorCaptionView != null);
+        return decorCaptionView;
+    }
+
+    private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) {
+        final Context context = getContext();
+        // We make a copy of the inflater, so it has the right context associated with it.
+        inflater = inflater.from(context);
+        final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,
+                null);
+        setDecorCaptionShade(context, view);
+        return view;
+    }
+
+    private void setDecorCaptionShade(Context context, DecorCaptionView view) {
+        final int shade = mWindow.getDecorCaptionShade();
+        switch (shade) {
+            case DECOR_CAPTION_SHADE_LIGHT:
+                setLightDecorCaptionShade(view);
+                break;
+            case DECOR_CAPTION_SHADE_DARK:
+                setDarkDecorCaptionShade(view);
+                break;
+            default: {
+                TypedValue value = new TypedValue();
+                context.getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
+                // We invert the shade depending on brightness of the theme. Dark shade for light
+                // theme and vice versa. Thanks to this the buttons should be visible on the
+                // background.
+                if (Color.luminance(value.data) < 0.5) {
+                    setLightDecorCaptionShade(view);
+                } else {
+                    setDarkDecorCaptionShade(view);
+                }
+                break;
+            }
+        }
+    }
+
+    void updateDecorCaptionShade() {
+        if (mDecorCaptionView != null) {
+            setDecorCaptionShade(getContext(), mDecorCaptionView);
+        }
+    }
+
+    private void setLightDecorCaptionShade(DecorCaptionView view) {
+        view.findViewById(R.id.maximize_window).setBackgroundResource(
+                R.drawable.decor_maximize_button_light);
+        view.findViewById(R.id.close_window).setBackgroundResource(
+                R.drawable.decor_close_button_light);
+    }
+
+    private void setDarkDecorCaptionShade(DecorCaptionView view) {
+        view.findViewById(R.id.maximize_window).setBackgroundResource(
+                R.drawable.decor_maximize_button_dark);
+        view.findViewById(R.id.close_window).setBackgroundResource(
+                R.drawable.decor_close_button_dark);
+    }
+
+    /**
+     * Returns the color used to fill areas the app has not rendered content to yet when the
+     * user is resizing the window of an activity in multi-window mode.
+     */
+    public static Drawable getResizingBackgroundDrawable(Context context, int backgroundRes,
+            int backgroundFallbackRes, boolean windowTranslucent) {
+        if (backgroundRes != 0) {
+            final Drawable drawable = context.getDrawable(backgroundRes);
+            if (drawable != null) {
+                return enforceNonTranslucentBackground(drawable, windowTranslucent);
+            }
+        }
+
+        if (backgroundFallbackRes != 0) {
+            final Drawable fallbackDrawable = context.getDrawable(backgroundFallbackRes);
+            if (fallbackDrawable != null) {
+                return enforceNonTranslucentBackground(fallbackDrawable, windowTranslucent);
+            }
+        }
+        return new ColorDrawable(Color.BLACK);
+    }
+
+    /**
+     * Enforces a drawable to be non-translucent to act as a background if needed, i.e. if the
+     * window is not translucent.
+     */
+    private static Drawable enforceNonTranslucentBackground(Drawable drawable,
+            boolean windowTranslucent) {
+        if (!windowTranslucent && drawable instanceof ColorDrawable) {
+            ColorDrawable colorDrawable = (ColorDrawable) drawable;
+            int color = colorDrawable.getColor();
+            if (Color.alpha(color) != 255) {
+                ColorDrawable copy = (ColorDrawable) colorDrawable.getConstantState().newDrawable()
+                        .mutate();
+                copy.setColor(
+                        Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)));
+                return copy;
+            }
+        }
+        return drawable;
+    }
+
+    void clearContentView() {
+        if (mDecorCaptionView != null) {
+            mDecorCaptionView.removeContentView();
+        } else {
+            // This window doesn't have caption, so we need to remove everything except our views
+            // we might have added.
+            for (int i = getChildCount() - 1; i >= 0; i--) {
+                View v = getChildAt(i);
+                if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view
+                        && v != mStatusGuard && v != mNavigationGuard) {
+                    removeViewAt(i);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets) {
+        if (mBackdropFrameRenderer != null) {
+            mBackdropFrameRenderer.setTargetRect(newBounds, fullscreen, systemInsets, stableInsets);
+        }
+    }
+
+    @Override
+    public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+            Rect stableInsets, int resizeMode) {
+        if (mWindow.isDestroyed()) {
+            // If the owner's window is gone, we should not be able to come here anymore.
+            releaseThreadedRenderer();
+            return;
+        }
+        if (mBackdropFrameRenderer != null) {
+            return;
+        }
+        final ThreadedRenderer renderer = getThreadedRenderer();
+        if (renderer != null) {
+            loadBackgroundDrawablesIfNeeded();
+            mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer,
+                    initialBounds, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
+                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
+                    getCurrentColor(mNavigationColorViewState), fullscreen, systemInsets,
+                    stableInsets, resizeMode);
+
+            // Get rid of the shadow while we are resizing. Shadow drawing takes considerable time.
+            // If we want to get the shadow shown while resizing, we would need to elevate a new
+            // element which owns the caption and has the elevation.
+            updateElevation();
+
+            updateColorViews(null /* insets */, false);
+        }
+        mResizeMode = resizeMode;
+        getViewRootImpl().requestInvalidateRootRenderNode();
+    }
+
+    @Override
+    public void onWindowDragResizeEnd() {
+        releaseThreadedRenderer();
+        updateColorViews(null /* insets */, false);
+        mResizeMode = RESIZE_MODE_INVALID;
+        getViewRootImpl().requestInvalidateRootRenderNode();
+    }
+
+    @Override
+    public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
+        if (mBackdropFrameRenderer == null) {
+            return false;
+        }
+        return mBackdropFrameRenderer.onContentDrawn(offsetX, offsetY, sizeX, sizeY);
+    }
+
+    @Override
+    public void onRequestDraw(boolean reportNextDraw) {
+        if (mBackdropFrameRenderer != null) {
+            mBackdropFrameRenderer.onRequestDraw(reportNextDraw);
+        } else if (reportNextDraw) {
+            // If render thread is gone, just report immediately.
+            if (isAttachedToWindow()) {
+                getViewRootImpl().reportDrawFinish();
+            }
+        }
+    }
+
+    @Override
+    public void onPostDraw(DisplayListCanvas canvas) {
+        drawResizingShadowIfNeeded(canvas);
+    }
+
+    private void initResizingPaints() {
+        final int startColor = mContext.getResources().getColor(
+                R.color.resize_shadow_start_color, null);
+        final int endColor = mContext.getResources().getColor(
+                R.color.resize_shadow_end_color, null);
+        final int middleColor = (startColor + endColor) / 2;
+        mHorizontalResizeShadowPaint.setShader(new LinearGradient(
+                0, 0, 0, mResizeShadowSize, new int[] { startColor, middleColor, endColor },
+                new float[] { 0f, 0.3f, 1f }, Shader.TileMode.CLAMP));
+        mVerticalResizeShadowPaint.setShader(new LinearGradient(
+                0, 0, mResizeShadowSize, 0, new int[] { startColor, middleColor, endColor },
+                new float[] { 0f, 0.3f, 1f }, Shader.TileMode.CLAMP));
+    }
+
+    private void drawResizingShadowIfNeeded(DisplayListCanvas canvas) {
+        if (mResizeMode != RESIZE_MODE_DOCKED_DIVIDER || mWindow.mIsFloating
+                || mWindow.isTranslucent()
+                || mWindow.isShowingWallpaper()) {
+            return;
+        }
+        canvas.save();
+        canvas.translate(0, getHeight() - mFrameOffsets.bottom);
+        canvas.drawRect(0, 0, getWidth(), mResizeShadowSize, mHorizontalResizeShadowPaint);
+        canvas.restore();
+        canvas.save();
+        canvas.translate(getWidth() - mFrameOffsets.right, 0);
+        canvas.drawRect(0, 0, mResizeShadowSize, getHeight(), mVerticalResizeShadowPaint);
+        canvas.restore();
+    }
+
+    /** Release the renderer thread which is usually done when the user stops resizing. */
+    private void releaseThreadedRenderer() {
+        if (mResizingBackgroundDrawable != null && mLastBackgroundDrawableCb != null) {
+            mResizingBackgroundDrawable.setCallback(mLastBackgroundDrawableCb);
+            mLastBackgroundDrawableCb = null;
+        }
+
+        if (mBackdropFrameRenderer != null) {
+            mBackdropFrameRenderer.releaseRenderer();
+            mBackdropFrameRenderer = null;
+            // Bring the shadow back.
+            updateElevation();
+        }
+    }
+
+    private boolean isResizing() {
+        return mBackdropFrameRenderer != null;
+    }
+
+    /**
+     * The elevation gets set for the first time and the framework needs to be informed that
+     * the surface layer gets created with the shadow size in mind.
+     */
+    private void initializeElevation() {
+        // TODO(skuhne): Call setMaxElevation here accordingly after b/22668382 got fixed.
+        mAllowUpdateElevation = false;
+        updateElevation();
+    }
+
+    private void updateElevation() {
+        float elevation = 0;
+        final boolean wasAdjustedForStack = mElevationAdjustedForStack;
+        // Do not use a shadow when we are in resizing mode (mBackdropFrameRenderer not null)
+        // since the shadow is bound to the content size and not the target size.
+        final int windowingMode =
+                getResources().getConfiguration().windowConfiguration.getWindowingMode();
+        if ((windowingMode == WINDOWING_MODE_FREEFORM) && !isResizing()) {
+            elevation = hasWindowFocus() ?
+                    DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP;
+            // Add a maximum shadow height value to the top level view.
+            // Note that pinned stack doesn't have focus
+            // so maximum shadow height adjustment isn't needed.
+            // TODO(skuhne): Remove this if clause once b/22668382 got fixed.
+            if (!mAllowUpdateElevation) {
+                elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
+            }
+            // Convert the DP elevation into physical pixels.
+            elevation = dipToPx(elevation);
+            mElevationAdjustedForStack = true;
+        } else if (windowingMode == WINDOWING_MODE_PINNED) {
+            elevation = dipToPx(DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
+            mElevationAdjustedForStack = true;
+        } else {
+            mElevationAdjustedForStack = false;
+        }
+
+        // Don't change the elevation if we didn't previously adjust it for the stack it was in
+        // or it didn't change.
+        if ((wasAdjustedForStack || mElevationAdjustedForStack)
+                && getElevation() != elevation) {
+            mWindow.setElevation(elevation);
+        }
+    }
+
+    boolean isShowingCaption() {
+        return mDecorCaptionView != null && mDecorCaptionView.isCaptionShowing();
+    }
+
+    int getCaptionHeight() {
+        return isShowingCaption() ? mDecorCaptionView.getCaptionHeight() : 0;
+    }
+
+    /**
+     * Converts a DIP measure into physical pixels.
+     * @param dip The dip value.
+     * @return Returns the number of pixels.
+     */
+    private float dipToPx(float dip) {
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
+                getResources().getDisplayMetrics());
+    }
+
+    /**
+     * Provide an override of the caption background drawable.
+     */
+    void setUserCaptionBackgroundDrawable(Drawable drawable) {
+        mUserCaptionBackgroundDrawable = drawable;
+        if (mBackdropFrameRenderer != null) {
+            mBackdropFrameRenderer.setUserCaptionBackgroundDrawable(drawable);
+        }
+    }
+
+    private static String getTitleSuffix(WindowManager.LayoutParams params) {
+        if (params == null) {
+            return "";
+        }
+        final String[] split = params.getTitle().toString().split("\\.");
+        if (split.length > 0) {
+            return split[split.length - 1];
+        } else {
+            return "";
+        }
+    }
+
+    void updateLogTag(WindowManager.LayoutParams params) {
+        mLogTag = TAG + "[" + getTitleSuffix(params) + "]";
+    }
+
+    private void updateAvailableWidth() {
+        Resources res = getResources();
+        mAvailableWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                res.getConfiguration().screenWidthDp, res.getDisplayMetrics());
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void requestKeyboardShortcuts(List<KeyboardShortcutGroup> list, int deviceId) {
+        final PanelFeatureState st = mWindow.getPanelState(FEATURE_OPTIONS_PANEL, false);
+        final Menu menu = st != null ? st.menu : null;
+        if (!mWindow.isDestroyed() && mWindow.getCallback() != null) {
+            mWindow.getCallback().onProvideKeyboardShortcuts(list, menu, deviceId);
+        }
+    }
+
+    @Override
+    public void dispatchPointerCaptureChanged(boolean hasCapture) {
+        super.dispatchPointerCaptureChanged(hasCapture);
+        if (!mWindow.isDestroyed() && mWindow.getCallback() != null) {
+            mWindow.getCallback().onPointerCaptureChanged(hasCapture);
+        }
+    }
+
+    @Override
+    public int getAccessibilityViewId() {
+        return AccessibilityNodeInfo.ROOT_ITEM_ID;
+    }
+
+    @Override
+    public String toString() {
+        return "DecorView@" + Integer.toHexString(this.hashCode()) + "["
+                + getTitleSuffix(mWindow.getAttributes()) + "]";
+    }
+
+    private static class ColorViewState {
+        View view = null;
+        int targetVisibility = View.INVISIBLE;
+        boolean present = false;
+        boolean visible;
+        int color;
+
+        final ColorViewAttributes attributes;
+
+        ColorViewState(ColorViewAttributes attributes) {
+            this.attributes = attributes;
+        }
+    }
+
+    public static class ColorViewAttributes {
+
+        final int id;
+        final int systemUiHideFlag;
+        final int translucentFlag;
+        final int verticalGravity;
+        final int horizontalGravity;
+        final int seascapeGravity;
+        final String transitionName;
+        final int hideWindowFlag;
+
+        private ColorViewAttributes(int systemUiHideFlag, int translucentFlag, int verticalGravity,
+                int horizontalGravity, int seascapeGravity, String transitionName, int id,
+                int hideWindowFlag) {
+            this.id = id;
+            this.systemUiHideFlag = systemUiHideFlag;
+            this.translucentFlag = translucentFlag;
+            this.verticalGravity = verticalGravity;
+            this.horizontalGravity = horizontalGravity;
+            this.seascapeGravity = seascapeGravity;
+            this.transitionName = transitionName;
+            this.hideWindowFlag = hideWindowFlag;
+        }
+
+        public boolean isPresent(int sysUiVis, int windowFlags, boolean force) {
+            return (sysUiVis & systemUiHideFlag) == 0
+                    && (windowFlags & hideWindowFlag) == 0
+                    && ((windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
+                    || force);
+        }
+
+        public boolean isVisible(boolean present, int color, int windowFlags, boolean force) {
+            return present
+                    && (color & Color.BLACK) != 0
+                    && ((windowFlags & translucentFlag) == 0  || force);
+        }
+
+        public boolean isVisible(int sysUiVis, int color, int windowFlags, boolean force) {
+            final boolean present = isPresent(sysUiVis, windowFlags, force);
+            return isVisible(present, color, windowFlags, force);
+        }
+    }
+
+    /**
+     * Clears out internal references when the action mode is destroyed.
+     */
+    private class ActionModeCallback2Wrapper extends ActionMode.Callback2 {
+        private final ActionMode.Callback mWrapped;
+
+        public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) {
+            mWrapped = wrapped;
+        }
+
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            return mWrapped.onCreateActionMode(mode, menu);
+        }
+
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            requestFitSystemWindows();
+            return mWrapped.onPrepareActionMode(mode, menu);
+        }
+
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            return mWrapped.onActionItemClicked(mode, item);
+        }
+
+        public void onDestroyActionMode(ActionMode mode) {
+            mWrapped.onDestroyActionMode(mode);
+            final boolean isMncApp = mContext.getApplicationInfo().targetSdkVersion
+                    >= M;
+            final boolean isPrimary;
+            final boolean isFloating;
+            if (isMncApp) {
+                isPrimary = mode == mPrimaryActionMode;
+                isFloating = mode == mFloatingActionMode;
+                if (!isPrimary && mode.getType() == ActionMode.TYPE_PRIMARY) {
+                    Log.e(mLogTag, "Destroying unexpected ActionMode instance of TYPE_PRIMARY; "
+                            + mode + " was not the current primary action mode! Expected "
+                            + mPrimaryActionMode);
+                }
+                if (!isFloating && mode.getType() == ActionMode.TYPE_FLOATING) {
+                    Log.e(mLogTag, "Destroying unexpected ActionMode instance of TYPE_FLOATING; "
+                            + mode + " was not the current floating action mode! Expected "
+                            + mFloatingActionMode);
+                }
+            } else {
+                isPrimary = mode.getType() == ActionMode.TYPE_PRIMARY;
+                isFloating = mode.getType() == ActionMode.TYPE_FLOATING;
+            }
+            if (isPrimary) {
+                if (mPrimaryActionModePopup != null) {
+                    removeCallbacks(mShowPrimaryActionModePopup);
+                }
+                if (mPrimaryActionModeView != null) {
+                    endOnGoingFadeAnimation();
+                    // Store action mode view reference, so we can access it safely when animation
+                    // ends. mPrimaryActionModePopup is set together with mPrimaryActionModeView,
+                    // so no need to store reference to it in separate variable.
+                    final ActionBarContextView lastActionModeView = mPrimaryActionModeView;
+                    mFadeAnim = ObjectAnimator.ofFloat(mPrimaryActionModeView, View.ALPHA,
+                            1f, 0f);
+                    mFadeAnim.addListener(new Animator.AnimatorListener() {
+
+                                @Override
+                                public void onAnimationStart(Animator animation) {
+
+                                }
+
+                                @Override
+                                public void onAnimationEnd(Animator animation) {
+                                    // If mPrimaryActionModeView has changed - it means that we've
+                                    // cleared the content while preserving decor view. We don't
+                                    // want to change the state of new instances accidentally here.
+                                    if (lastActionModeView == mPrimaryActionModeView) {
+                                        lastActionModeView.setVisibility(GONE);
+                                        if (mPrimaryActionModePopup != null) {
+                                            mPrimaryActionModePopup.dismiss();
+                                        }
+                                        lastActionModeView.killMode();
+                                        mFadeAnim = null;
+                                    }
+                                }
+
+                                @Override
+                                public void onAnimationCancel(Animator animation) {
+
+                                }
+
+                                @Override
+                                public void onAnimationRepeat(Animator animation) {
+
+                                }
+                            });
+                    mFadeAnim.start();
+                }
+
+                mPrimaryActionMode = null;
+            } else if (isFloating) {
+                cleanupFloatingActionModeViews();
+                mFloatingActionMode = null;
+            }
+            if (mWindow.getCallback() != null && !mWindow.isDestroyed()) {
+                try {
+                    mWindow.getCallback().onActionModeFinished(mode);
+                } catch (AbstractMethodError ame) {
+                    // Older apps might not implement this callback method.
+                }
+            }
+            requestFitSystemWindows();
+        }
+
+        @Override
+        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+            if (mWrapped instanceof ActionMode.Callback2) {
+                ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect);
+            } else {
+                super.onGetContentRect(mode, view, outRect);
+            }
+        }
+    }
+}
diff --git a/com/android/internal/policy/DividerSnapAlgorithm.java b/com/android/internal/policy/DividerSnapAlgorithm.java
new file mode 100644
index 0000000..fb6b8b0
--- /dev/null
+++ b/com/android/internal/policy/DividerSnapAlgorithm.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2015 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.internal.policy;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.util.Log;
+import android.view.Display;
+import android.view.DisplayInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Calculates the snap targets and the snap position given a position and a velocity. All positions
+ * here are to be interpreted as the left/top edge of the divider rectangle.
+ *
+ * @hide
+ */
+public class DividerSnapAlgorithm {
+
+    private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
+    private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
+
+    /**
+     * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
+     */
+    private static final int SNAP_MODE_16_9 = 0;
+
+    /**
+     * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
+     */
+    private static final int SNAP_FIXED_RATIO = 1;
+
+    /**
+     * 1 snap target: 1:1
+     */
+    private static final int SNAP_ONLY_1_1 = 2;
+
+    /**
+     * 1 snap target: minimized height, (1 - minimized height)
+     */
+    private static final int SNAP_MODE_MINIMIZED = 3;
+
+    private final float mMinFlingVelocityPxPerSecond;
+    private final float mMinDismissVelocityPxPerSecond;
+    private final int mDisplayWidth;
+    private final int mDisplayHeight;
+    private final int mDividerSize;
+    private final ArrayList<SnapTarget> mTargets = new ArrayList<>();
+    private final Rect mInsets = new Rect();
+    private final int mSnapMode;
+    private final int mMinimalSizeResizableTask;
+    private final int mTaskHeightInMinimizedMode;
+    private final float mFixedRatio;
+    private boolean mIsHorizontalDivision;
+
+    /** The first target which is still splitting the screen */
+    private final SnapTarget mFirstSplitTarget;
+
+    /** The last target which is still splitting the screen */
+    private final SnapTarget mLastSplitTarget;
+
+    private final SnapTarget mDismissStartTarget;
+    private final SnapTarget mDismissEndTarget;
+    private final SnapTarget mMiddleTarget;
+
+    public static DividerSnapAlgorithm create(Context ctx, Rect insets) {
+        DisplayInfo displayInfo = new DisplayInfo();
+        ctx.getSystemService(DisplayManager.class).getDisplay(
+                Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo);
+        int dividerWindowWidth = ctx.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.docked_stack_divider_thickness);
+        int dividerInsets = ctx.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.docked_stack_divider_insets);
+        return new DividerSnapAlgorithm(ctx.getResources(),
+                displayInfo.logicalWidth, displayInfo.logicalHeight,
+                dividerWindowWidth - 2 * dividerInsets,
+                ctx.getApplicationContext().getResources().getConfiguration().orientation
+                        == Configuration.ORIENTATION_PORTRAIT,
+                insets);
+    }
+
+    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
+            boolean isHorizontalDivision, Rect insets) {
+        this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets, false);
+    }
+
+    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
+            boolean isHorizontalDivision, Rect insets, boolean isMinimizedMode) {
+        mMinFlingVelocityPxPerSecond =
+                MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
+        mMinDismissVelocityPxPerSecond =
+                MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
+        mDividerSize = dividerSize;
+        mDisplayWidth = displayWidth;
+        mDisplayHeight = displayHeight;
+        mIsHorizontalDivision = isHorizontalDivision;
+        mInsets.set(insets);
+        mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED :
+                res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode);
+        mFixedRatio = res.getFraction(
+                com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
+        mMinimalSizeResizableTask = res.getDimensionPixelSize(
+                com.android.internal.R.dimen.default_minimal_size_resizable_task);
+        mTaskHeightInMinimizedMode = res.getDimensionPixelSize(
+                com.android.internal.R.dimen.task_height_of_minimized_mode);
+        calculateTargets(isHorizontalDivision);
+        mFirstSplitTarget = mTargets.get(1);
+        mLastSplitTarget = mTargets.get(mTargets.size() - 2);
+        mDismissStartTarget = mTargets.get(0);
+        mDismissEndTarget = mTargets.get(mTargets.size() - 1);
+        mMiddleTarget = mTargets.get(mTargets.size() / 2);
+    }
+
+    /**
+     * @return whether it's feasible to enable split screen in the current configuration, i.e. when
+     *         snapping in the middle both tasks are larger than the minimal task size.
+     */
+    public boolean isSplitScreenFeasible() {
+        int statusBarSize = mInsets.top;
+        int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
+        int size = mIsHorizontalDivision
+                ? mDisplayHeight
+                : mDisplayWidth;
+        int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
+        return availableSpace / 2 >= mMinimalSizeResizableTask;
+    }
+
+    public SnapTarget calculateSnapTarget(int position, float velocity) {
+        return calculateSnapTarget(position, velocity, true /* hardDismiss */);
+    }
+
+    /**
+     * @param position the top/left position of the divider
+     * @param velocity current dragging velocity
+     * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
+     */
+    public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
+        if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
+            return mDismissStartTarget;
+        }
+        if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
+            return mDismissEndTarget;
+        }
+        if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
+            return snap(position, hardDismiss);
+        }
+        if (velocity < 0) {
+            return mFirstSplitTarget;
+        } else {
+            return mLastSplitTarget;
+        }
+    }
+
+    public SnapTarget calculateNonDismissingSnapTarget(int position) {
+        SnapTarget target = snap(position, false /* hardDismiss */);
+        if (target == mDismissStartTarget) {
+            return mFirstSplitTarget;
+        } else if (target == mDismissEndTarget) {
+            return mLastSplitTarget;
+        } else {
+            return target;
+        }
+    }
+
+    public float calculateDismissingFraction(int position) {
+        if (position < mFirstSplitTarget.position) {
+            return 1f - (float) (position - getStartInset())
+                    / (mFirstSplitTarget.position - getStartInset());
+        } else if (position > mLastSplitTarget.position) {
+            return (float) (position - mLastSplitTarget.position)
+                    / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
+        }
+        return 0f;
+    }
+
+    public SnapTarget getClosestDismissTarget(int position) {
+        if (position < mFirstSplitTarget.position) {
+            return mDismissStartTarget;
+        } else if (position > mLastSplitTarget.position) {
+            return mDismissEndTarget;
+        } else if (position - mDismissStartTarget.position
+                < mDismissEndTarget.position - position) {
+            return mDismissStartTarget;
+        } else {
+            return mDismissEndTarget;
+        }
+    }
+
+    public SnapTarget getFirstSplitTarget() {
+        return mFirstSplitTarget;
+    }
+
+    public SnapTarget getLastSplitTarget() {
+        return mLastSplitTarget;
+    }
+
+    public SnapTarget getDismissStartTarget() {
+        return mDismissStartTarget;
+    }
+
+    public SnapTarget getDismissEndTarget() {
+        return mDismissEndTarget;
+    }
+
+    private int getStartInset() {
+        if (mIsHorizontalDivision) {
+            return mInsets.top;
+        } else {
+            return mInsets.left;
+        }
+    }
+
+    private int getEndInset() {
+        if (mIsHorizontalDivision) {
+            return mInsets.bottom;
+        } else {
+            return mInsets.right;
+        }
+    }
+
+    private SnapTarget snap(int position, boolean hardDismiss) {
+        int minIndex = -1;
+        float minDistance = Float.MAX_VALUE;
+        int size = mTargets.size();
+        for (int i = 0; i < size; i++) {
+            SnapTarget target = mTargets.get(i);
+            float distance = Math.abs(position - target.position);
+            if (hardDismiss) {
+                distance /= target.distanceMultiplier;
+            }
+            if (distance < minDistance) {
+                minIndex = i;
+                minDistance = distance;
+            }
+        }
+        return mTargets.get(minIndex);
+    }
+
+    private void calculateTargets(boolean isHorizontalDivision) {
+        mTargets.clear();
+        int dividerMax = isHorizontalDivision
+                ? mDisplayHeight
+                : mDisplayWidth;
+        int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
+        mTargets.add(new SnapTarget(-mDividerSize, -mDividerSize, SnapTarget.FLAG_DISMISS_START,
+                0.35f));
+        switch (mSnapMode) {
+            case SNAP_MODE_16_9:
+                addRatio16_9Targets(isHorizontalDivision, dividerMax);
+                break;
+            case SNAP_FIXED_RATIO:
+                addFixedDivisionTargets(isHorizontalDivision, dividerMax);
+                break;
+            case SNAP_ONLY_1_1:
+                addMiddleTarget(isHorizontalDivision);
+                break;
+            case SNAP_MODE_MINIMIZED:
+                addMinimizedTarget(isHorizontalDivision);
+                break;
+        }
+        mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
+                SnapTarget.FLAG_DISMISS_END, 0.35f));
+    }
+
+    private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
+            int bottomPosition, int dividerMax) {
+        maybeAddTarget(topPosition, topPosition - mInsets.top);
+        addMiddleTarget(isHorizontalDivision);
+        maybeAddTarget(bottomPosition, dividerMax - mInsets.bottom
+                - (bottomPosition + mDividerSize));
+    }
+
+    private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
+        int start = isHorizontalDivision ? mInsets.top : mInsets.left;
+        int end = isHorizontalDivision
+                ? mDisplayHeight - mInsets.bottom
+                : mDisplayWidth - mInsets.right;
+        int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
+        int topPosition = start + size;
+        int bottomPosition = end - size - mDividerSize;
+        addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
+    }
+
+    private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) {
+        int start = isHorizontalDivision ? mInsets.top : mInsets.left;
+        int end = isHorizontalDivision
+                ? mDisplayHeight - mInsets.bottom
+                : mDisplayWidth - mInsets.right;
+        int startOther = isHorizontalDivision ? mInsets.left : mInsets.top;
+        int endOther = isHorizontalDivision
+                ? mDisplayWidth - mInsets.right
+                : mDisplayHeight - mInsets.bottom;
+        float size = 9.0f / 16.0f * (endOther - startOther);
+        int sizeInt = (int) Math.floor(size);
+        int topPosition = start + sizeInt;
+        int bottomPosition = end - sizeInt - mDividerSize;
+        addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
+    }
+
+    /**
+     * Adds a target at {@param position} but only if the area with size of {@param smallerSize}
+     * meets the minimal size requirement.
+     */
+    private void maybeAddTarget(int position, int smallerSize) {
+        if (smallerSize >= mMinimalSizeResizableTask) {
+            mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
+        }
+    }
+
+    private void addMiddleTarget(boolean isHorizontalDivision) {
+        int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
+                mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
+        mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
+    }
+
+    private void addMinimizedTarget(boolean isHorizontalDivision) {
+        // In portrait offset the position by the statusbar height, in landscape add the statusbar
+        // height as well to match portrait offset
+        int position = mTaskHeightInMinimizedMode + mInsets.top;
+        if (!isHorizontalDivision) {
+            position += mInsets.left;
+        }
+        mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
+    }
+
+    public SnapTarget getMiddleTarget() {
+        return mMiddleTarget;
+    }
+
+    public SnapTarget getNextTarget(SnapTarget snapTarget) {
+        int index = mTargets.indexOf(snapTarget);
+        if (index != -1 && index < mTargets.size() - 1) {
+            return mTargets.get(index + 1);
+        }
+        return snapTarget;
+    }
+
+    public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
+        int index = mTargets.indexOf(snapTarget);
+        if (index != -1 && index > 0) {
+            return mTargets.get(index - 1);
+        }
+        return snapTarget;
+    }
+
+    public boolean isFirstSplitTargetAvailable() {
+        return mFirstSplitTarget != mMiddleTarget;
+    }
+
+    public boolean isLastSplitTargetAvailable() {
+        return mLastSplitTarget != mMiddleTarget;
+    }
+
+    /**
+     * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
+     * if {@param increment} is negative and moves right otherwise.
+     */
+    public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
+        int index = mTargets.indexOf(snapTarget);
+        if (index != -1) {
+            SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
+                    % mTargets.size());
+            if (newTarget == mDismissStartTarget) {
+                return mLastSplitTarget;
+            } else if (newTarget == mDismissEndTarget) {
+                return mFirstSplitTarget;
+            } else {
+                return newTarget;
+            }
+        }
+        return snapTarget;
+    }
+
+    /**
+     * Represents a snap target for the divider.
+     */
+    public static class SnapTarget {
+        public static final int FLAG_NONE = 0;
+
+        /** If the divider reaches this value, the left/top task should be dismissed. */
+        public static final int FLAG_DISMISS_START = 1;
+
+        /** If the divider reaches this value, the right/bottom task should be dismissed */
+        public static final int FLAG_DISMISS_END = 2;
+
+        /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
+        public final int position;
+
+        /**
+         * Like {@link #position}, but used to calculate the task bounds which might be different
+         * from the stack bounds.
+         */
+        public final int taskPosition;
+
+        public final int flag;
+
+        /**
+         * Multiplier used to calculate distance to snap position. The lower this value, the harder
+         * it's to snap on this target
+         */
+        private final float distanceMultiplier;
+
+        public SnapTarget(int position, int taskPosition, int flag) {
+            this(position, taskPosition, flag, 1f);
+        }
+
+        public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
+            this.position = position;
+            this.taskPosition = taskPosition;
+            this.flag = flag;
+            this.distanceMultiplier = distanceMultiplier;
+        }
+    }
+}
diff --git a/com/android/internal/policy/DockedDividerUtils.java b/com/android/internal/policy/DockedDividerUtils.java
new file mode 100644
index 0000000..c68e506
--- /dev/null
+++ b/com/android/internal/policy/DockedDividerUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 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.internal.policy;
+
+import android.graphics.Rect;
+
+import static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+/**
+ * Utility functions for docked stack divider used by both window manager and System UI.
+ *
+ * @hide
+ */
+public class DockedDividerUtils {
+
+    public static void calculateBoundsForPosition(int position, int dockSide, Rect outRect,
+            int displayWidth, int displayHeight, int dividerSize) {
+        outRect.set(0, 0, displayWidth, displayHeight);
+        switch (dockSide) {
+            case DOCKED_LEFT:
+                outRect.right = position;
+                break;
+            case DOCKED_TOP:
+                outRect.bottom = position;
+                break;
+            case DOCKED_RIGHT:
+                outRect.left = position + dividerSize;
+                break;
+            case DOCKED_BOTTOM:
+                outRect.top = position + dividerSize;
+                break;
+        }
+        sanitizeStackBounds(outRect, dockSide == DOCKED_LEFT || dockSide == DOCKED_TOP);
+    }
+
+    /**
+     * Makes sure that the bounds are always valid, i. e. they are at least one pixel high and wide.
+     *
+     * @param bounds The bounds to sanitize.
+     * @param topLeft Pass true if the bounds are at the top/left of the screen, false if they are
+     *                at the bottom/right. This is used to determine in which direction to extend
+     *                the bounds.
+     */
+    public static void sanitizeStackBounds(Rect bounds, boolean topLeft) {
+
+        // If the bounds are either on the top or left of the screen, rather move it further to the
+        // left/top to make it more offscreen. If they are on the bottom or right, push them off the
+        // screen by moving it even more to the bottom/right.
+        if (topLeft) {
+            if (bounds.left >= bounds.right) {
+                bounds.left = bounds.right - 1;
+            }
+            if (bounds.top >= bounds.bottom) {
+                bounds.top = bounds.bottom - 1;
+            }
+        } else {
+            if (bounds.right <= bounds.left) {
+                bounds.right = bounds.left + 1;
+            }
+            if (bounds.bottom <= bounds.top) {
+                bounds.bottom = bounds.top + 1;
+            }
+        }
+    }
+
+    public static int calculatePositionForBounds(Rect bounds, int dockSide, int dividerSize) {
+        switch (dockSide) {
+            case DOCKED_LEFT:
+                return bounds.right;
+            case DOCKED_TOP:
+                return bounds.bottom;
+            case DOCKED_RIGHT:
+                return bounds.left - dividerSize;
+            case DOCKED_BOTTOM:
+                return bounds.top - dividerSize;
+            default:
+                return 0;
+        }
+    }
+
+    public static int calculateMiddlePosition(boolean isHorizontalDivision, Rect insets,
+            int displayWidth, int displayHeight, int dividerSize) {
+        int start = isHorizontalDivision ? insets.top : insets.left;
+        int end = isHorizontalDivision
+                ? displayHeight - insets.bottom
+                : displayWidth - insets.right;
+        return start + (end - start) / 2 - dividerSize / 2;
+    }
+
+    public static int getDockSideFromCreatedMode(boolean dockOnTopOrLeft,
+            boolean isHorizontalDivision) {
+        if (dockOnTopOrLeft) {
+            if (isHorizontalDivision) {
+                return DOCKED_TOP;
+            } else {
+                return DOCKED_LEFT;
+            }
+        } else {
+            if (isHorizontalDivision) {
+                return DOCKED_BOTTOM;
+            } else {
+                return DOCKED_RIGHT;
+            }
+        }
+    }
+
+    public static int invertDockSide(int dockSide) {
+        switch (dockSide) {
+            case DOCKED_LEFT:
+                return DOCKED_RIGHT;
+            case DOCKED_TOP:
+                return DOCKED_BOTTOM;
+            case DOCKED_RIGHT:
+                return DOCKED_LEFT;
+            case DOCKED_BOTTOM:
+                return DOCKED_TOP;
+            default:
+                return DOCKED_INVALID;
+        }
+    }
+}
diff --git a/com/android/internal/policy/PhoneFallbackEventHandler.java b/com/android/internal/policy/PhoneFallbackEventHandler.java
new file mode 100644
index 0000000..ebc2c71
--- /dev/null
+++ b/com/android/internal/policy/PhoneFallbackEventHandler.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2008 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.internal.policy;
+
+import android.app.KeyguardManager;
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.media.session.MediaSessionLegacyHelper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.FallbackEventHandler;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.View;
+import com.android.internal.policy.PhoneWindow;
+
+/**
+ * @hide
+ */
+public class PhoneFallbackEventHandler implements FallbackEventHandler {
+    private static String TAG = "PhoneFallbackEventHandler";
+    private static final boolean DEBUG = false;
+
+    Context mContext;
+    View mView;
+
+    AudioManager mAudioManager;
+    KeyguardManager mKeyguardManager;
+    SearchManager mSearchManager;
+    TelephonyManager mTelephonyManager;
+
+    public PhoneFallbackEventHandler(Context context) {
+        mContext = context;
+    }
+
+    public void setView(View v) {
+        mView = v;
+    }
+
+    public void preDispatchKeyEvent(KeyEvent event) {
+        getAudioManager().preDispatchKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE);
+    }
+
+    public boolean dispatchKeyEvent(KeyEvent event) {
+
+        final int action = event.getAction();
+        final int keyCode = event.getKeyCode();
+
+        if (action == KeyEvent.ACTION_DOWN) {
+            return onKeyDown(keyCode, event);
+        } else {
+            return onKeyUp(keyCode, event);
+        }
+    }
+
+    boolean onKeyDown(int keyCode, KeyEvent event) {
+        /* ****************************************************************************
+         * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES.
+         * See the comment in PhoneWindow.onKeyDown
+         * ****************************************************************************/
+        final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState();
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+            case KeyEvent.KEYCODE_VOLUME_MUTE: {
+                MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(
+                        event, AudioManager.USE_DEFAULT_STREAM_TYPE, false);
+                return true;
+            }
+
+
+            case KeyEvent.KEYCODE_MEDIA_PLAY:
+            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                /* Suppress PLAY/PAUSE toggle when phone is ringing or in-call
+                 * to avoid music playback */
+                if (getTelephonyManager().getCallState() != TelephonyManager.CALL_STATE_IDLE) {
+                    return true;  // suppress key event
+                }
+            case KeyEvent.KEYCODE_MUTE:
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            case KeyEvent.KEYCODE_MEDIA_STOP:
+            case KeyEvent.KEYCODE_MEDIA_NEXT:
+            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+            case KeyEvent.KEYCODE_MEDIA_REWIND:
+            case KeyEvent.KEYCODE_MEDIA_RECORD:
+            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+            case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
+                handleMediaKeyEvent(event);
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_CALL: {
+                if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) {
+                    break;
+                }
+                if (event.getRepeatCount() == 0) {
+                    dispatcher.startTracking(event, this);
+                } else if (event.isLongPress() && dispatcher.isTracking(event)) {
+                    dispatcher.performedLongPress(event);
+                    if (isUserSetupComplete()) {
+                        mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                        // launch the VoiceDialer
+                        Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
+                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                        try {
+                            sendCloseSystemWindows();
+                            mContext.startActivity(intent);
+                        } catch (ActivityNotFoundException e) {
+                            startCallActivity();
+                        }
+                    } else {
+                        Log.i(TAG, "Not starting call activity because user "
+                                + "setup is in progress.");
+                    }
+                }
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_CAMERA: {
+                if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) {
+                    break;
+                }
+                if (event.getRepeatCount() == 0) {
+                    dispatcher.startTracking(event, this);
+                } else if (event.isLongPress() && dispatcher.isTracking(event)) {
+                    dispatcher.performedLongPress(event);
+                    if (isUserSetupComplete()) {
+                        mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                        sendCloseSystemWindows();
+                        // Broadcast an intent that the Camera button was longpressed
+                        Intent intent = new Intent(Intent.ACTION_CAMERA_BUTTON, null);
+                        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+                        intent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+                        mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT_OR_SELF,
+                                null, null, null, 0, null, null);
+                    } else {
+                        Log.i(TAG, "Not dispatching CAMERA long press because user "
+                                + "setup is in progress.");
+                    }
+                }
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_SEARCH: {
+                if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) {
+                    break;
+                }
+                if (event.getRepeatCount() == 0) {
+                    dispatcher.startTracking(event, this);
+                } else if (event.isLongPress() && dispatcher.isTracking(event)) {
+                    Configuration config = mContext.getResources().getConfiguration();
+                    if (config.keyboard == Configuration.KEYBOARD_NOKEYS
+                            || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
+                        if (isUserSetupComplete()) {
+                            // launch the search activity
+                            Intent intent = new Intent(Intent.ACTION_SEARCH_LONG_PRESS);
+                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                            try {
+                                mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                                sendCloseSystemWindows();
+                                getSearchManager().stopSearch();
+                                mContext.startActivity(intent);
+                                // Only clear this if we successfully start the
+                                // activity; otherwise we will allow the normal short
+                                // press action to be performed.
+                                dispatcher.performedLongPress(event);
+                                return true;
+                            } catch (ActivityNotFoundException e) {
+                                // Ignore
+                            }
+                        } else {
+                            Log.i(TAG, "Not dispatching SEARCH long press because user "
+                                    + "setup is in progress.");
+                        }
+                    }
+                }
+                break;
+            }
+        }
+        return false;
+    }
+
+    boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (DEBUG) {
+            Log.d(TAG, "up " + keyCode);
+        }
+        final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState();
+        if (dispatcher != null) {
+            dispatcher.handleUpEvent(event);
+        }
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+            case KeyEvent.KEYCODE_VOLUME_MUTE: {
+                if (!event.isCanceled()) {
+                    MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(
+                            event, AudioManager.USE_DEFAULT_STREAM_TYPE, false);
+                }
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            case KeyEvent.KEYCODE_MUTE:
+            case KeyEvent.KEYCODE_MEDIA_PLAY:
+            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+            case KeyEvent.KEYCODE_MEDIA_STOP:
+            case KeyEvent.KEYCODE_MEDIA_NEXT:
+            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+            case KeyEvent.KEYCODE_MEDIA_REWIND:
+            case KeyEvent.KEYCODE_MEDIA_RECORD:
+            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+            case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
+                handleMediaKeyEvent(event);
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_CAMERA: {
+                if (getKeyguardManager().inKeyguardRestrictedInputMode()) {
+                    break;
+                }
+                if (event.isTracking() && !event.isCanceled()) {
+                    // Add short press behavior here if desired
+                }
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_CALL: {
+                if (getKeyguardManager().inKeyguardRestrictedInputMode()) {
+                    break;
+                }
+                if (event.isTracking() && !event.isCanceled()) {
+                    if (isUserSetupComplete()) {
+                        startCallActivity();
+                    } else {
+                        Log.i(TAG, "Not starting call activity because user "
+                                + "setup is in progress.");
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void startCallActivity() {
+        sendCloseSystemWindows();
+        Intent intent = new Intent(Intent.ACTION_CALL_BUTTON);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        try {
+            mContext.startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            Log.w(TAG, "No activity found for android.intent.action.CALL_BUTTON.");
+        }
+    }
+
+    SearchManager getSearchManager() {
+        if (mSearchManager == null) {
+            mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+        }
+        return mSearchManager;
+    }
+
+    TelephonyManager getTelephonyManager() {
+        if (mTelephonyManager == null) {
+            mTelephonyManager = (TelephonyManager)mContext.getSystemService(
+                    Context.TELEPHONY_SERVICE);
+        }
+        return mTelephonyManager;
+    }
+
+    KeyguardManager getKeyguardManager() {
+        if (mKeyguardManager == null) {
+            mKeyguardManager = (KeyguardManager)mContext.getSystemService(Context.KEYGUARD_SERVICE);
+        }
+        return mKeyguardManager;
+    }
+
+    AudioManager getAudioManager() {
+        if (mAudioManager == null) {
+            mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
+        }
+        return mAudioManager;
+    }
+
+    void sendCloseSystemWindows() {
+        PhoneWindow.sendCloseSystemWindows(mContext, null);
+    }
+
+    private void handleMediaKeyEvent(KeyEvent keyEvent) {
+        MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(keyEvent, false);
+    }
+
+    private boolean isUserSetupComplete() {
+        return Settings.Secure.getInt(mContext.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE, 0) != 0;
+    }
+}
+
diff --git a/com/android/internal/policy/PhoneLayoutInflater.java b/com/android/internal/policy/PhoneLayoutInflater.java
new file mode 100644
index 0000000..991b6bb
--- /dev/null
+++ b/com/android/internal/policy/PhoneLayoutInflater.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2006 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.internal.policy;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+/**
+ * @hide
+ */
+public class PhoneLayoutInflater extends LayoutInflater {
+    private static final String[] sClassPrefixList = {
+        "android.widget.",
+        "android.webkit.",
+        "android.app."
+    };
+
+    /**
+     * Instead of instantiating directly, you should retrieve an instance
+     * through {@link Context#getSystemService}
+     *
+     * @param context The Context in which in which to find resources and other
+     *                application-specific things.
+     *
+     * @see Context#getSystemService
+     */
+    public PhoneLayoutInflater(Context context) {
+        super(context);
+    }
+
+    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
+        super(original, newContext);
+    }
+
+    /** Override onCreateView to instantiate names that correspond to the
+        widgets known to the Widget factory. If we don't find a match,
+        call through to our super class.
+    */
+    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
+        for (String prefix : sClassPrefixList) {
+            try {
+                View view = createView(name, prefix, attrs);
+                if (view != null) {
+                    return view;
+                }
+            } catch (ClassNotFoundException e) {
+                // In this case we want to let the base class take a crack
+                // at it.
+            }
+        }
+
+        return super.onCreateView(name, attrs);
+    }
+
+    public LayoutInflater cloneInContext(Context newContext) {
+        return new PhoneLayoutInflater(this, newContext);
+    }
+}
+
diff --git a/com/android/internal/policy/PhoneWindow.java b/com/android/internal/policy/PhoneWindow.java
new file mode 100644
index 0000000..b13560c
--- /dev/null
+++ b/com/android/internal/policy/PhoneWindow.java
@@ -0,0 +1,3853 @@
+/*
+ * Copyright (C) 2006 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.internal.policy;
+
+import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.*;
+
+import android.app.ActivityManager;
+import android.app.SearchManager;
+import android.os.UserHandle;
+
+import android.text.TextUtils;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.IRotationWatcher.Stub;
+import android.view.IWindowManager;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.InputQueue;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SearchEvent;
+import android.view.SurfaceHolder.Callback2;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewManager;
+import android.view.ViewParent;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.ActivityConfigCallback;
+import android.view.Window;
+import android.view.WindowManager;
+import com.android.internal.R;
+import com.android.internal.view.menu.ContextMenuBuilder;
+import com.android.internal.view.menu.IconMenuPresenter;
+import com.android.internal.view.menu.ListMenuPresenter;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuDialogHelper;
+import com.android.internal.view.menu.MenuHelper;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.widget.DecorContentParent;
+import com.android.internal.widget.SwipeDismissLayout;
+
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionLegacyHelper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.Settings;
+import android.transition.Scene;
+import android.transition.Transition;
+import android.transition.TransitionInflater;
+import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.util.AndroidRuntimeException;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Android-specific Window.
+ * <p>
+ * todo: need to pull the generic functionality out into a base class
+ * in android.widget.
+ *
+ * @hide
+ */
+public class PhoneWindow extends Window implements MenuBuilder.Callback {
+
+    private final static String TAG = "PhoneWindow";
+
+    private static final boolean DEBUG = false;
+
+    private final static int DEFAULT_BACKGROUND_FADE_DURATION_MS = 300;
+
+    private static final int CUSTOM_TITLE_COMPATIBLE_FEATURES = DEFAULT_FEATURES |
+            (1 << FEATURE_CUSTOM_TITLE) |
+            (1 << FEATURE_CONTENT_TRANSITIONS) |
+            (1 << FEATURE_ACTIVITY_TRANSITIONS) |
+            (1 << FEATURE_ACTION_MODE_OVERLAY);
+
+    private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet();
+
+    /**
+     * Simple callback used by the context menu and its submenus. The options
+     * menu submenus do not use this (their behavior is more complex).
+     */
+    final PhoneWindowMenuCallback mContextMenuCallback = new PhoneWindowMenuCallback(this);
+
+    final TypedValue mMinWidthMajor = new TypedValue();
+    final TypedValue mMinWidthMinor = new TypedValue();
+    TypedValue mFixedWidthMajor;
+    TypedValue mFixedWidthMinor;
+    TypedValue mFixedHeightMajor;
+    TypedValue mFixedHeightMinor;
+
+    // This is the top-level view of the window, containing the window decor.
+    private DecorView mDecor;
+
+    // When we reuse decor views, we need to recreate the content root. This happens when the decor
+    // view is requested, so we need to force the recreating without introducing an infinite loop.
+    private boolean mForceDecorInstall = false;
+
+    // This is the view in which the window contents are placed. It is either
+    // mDecor itself, or a child of mDecor where the contents go.
+    ViewGroup mContentParent;
+    // Whether the client has explicitly set the content view. If false and mContentParent is not
+    // null, then the content parent was set due to window preservation.
+    private boolean mContentParentExplicitlySet = false;
+
+    Callback2 mTakeSurfaceCallback;
+
+    InputQueue.Callback mTakeInputQueueCallback;
+
+    boolean mIsFloating;
+    private boolean mIsTranslucent;
+
+    private LayoutInflater mLayoutInflater;
+
+    private TextView mTitleView;
+
+    DecorContentParent mDecorContentParent;
+    private ActionMenuPresenterCallback mActionMenuPresenterCallback;
+    private PanelMenuPresenterCallback mPanelMenuPresenterCallback;
+
+    private TransitionManager mTransitionManager;
+    private Scene mContentScene;
+
+    // The icon resource has been explicitly set elsewhere
+    // and should not be overwritten with a default.
+    static final int FLAG_RESOURCE_SET_ICON = 1 << 0;
+
+    // The logo resource has been explicitly set elsewhere
+    // and should not be overwritten with a default.
+    static final int FLAG_RESOURCE_SET_LOGO = 1 << 1;
+
+    // The icon resource is currently configured to use the system fallback
+    // as no default was previously specified. Anything can override this.
+    static final int FLAG_RESOURCE_SET_ICON_FALLBACK = 1 << 2;
+
+    int mResourcesSetFlags;
+    int mIconRes;
+    int mLogoRes;
+
+    private DrawableFeatureState[] mDrawables;
+
+    private PanelFeatureState[] mPanels;
+
+    /**
+     * The panel that is prepared or opened (the most recent one if there are
+     * multiple panels). Shortcuts will go to this panel. It gets set in
+     * {@link #preparePanel} and cleared in {@link #closePanel}.
+     */
+    PanelFeatureState mPreparedPanel;
+
+    /**
+     * The keycode that is currently held down (as a modifier) for chording. If
+     * this is 0, there is no key held down.
+     */
+    int mPanelChordingKey;
+
+    // This stores if the system supports Picture-in-Picture
+    // to see if KEYCODE_WINDOW should be handled here or not.
+    private boolean mSupportsPictureInPicture;
+
+    private ImageView mLeftIconView;
+
+    private ImageView mRightIconView;
+
+    private ProgressBar mCircularProgressBar;
+
+    private ProgressBar mHorizontalProgressBar;
+
+    int mBackgroundResource = 0;
+    int mBackgroundFallbackResource = 0;
+
+    private Drawable mBackgroundDrawable;
+
+    private boolean mLoadElevation = true;
+    private float mElevation;
+
+    /** Whether window content should be clipped to the background outline. */
+    private boolean mClipToOutline;
+
+    private int mFrameResource = 0;
+
+    private int mTextColor = 0;
+    int mStatusBarColor = 0;
+    int mNavigationBarColor = 0;
+    int mNavigationBarDividerColor = 0;
+    private boolean mForcedStatusBarColor = false;
+    private boolean mForcedNavigationBarColor = false;
+
+    private CharSequence mTitle = null;
+
+    private int mTitleColor = 0;
+
+    private boolean mAlwaysReadCloseOnTouchAttr = false;
+
+    ContextMenuBuilder mContextMenu;
+    MenuHelper mContextMenuHelper;
+    private boolean mClosingActionMenu;
+
+    private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
+    private MediaController mMediaController;
+
+    private AudioManager mAudioManager;
+    private KeyguardManager mKeyguardManager;
+
+    private int mUiOptions = 0;
+
+    private boolean mInvalidatePanelMenuPosted;
+    private int mInvalidatePanelMenuFeatures;
+    private final Runnable mInvalidatePanelMenuRunnable = new Runnable() {
+        @Override public void run() {
+            for (int i = 0; i <= FEATURE_MAX; i++) {
+                if ((mInvalidatePanelMenuFeatures & 1 << i) != 0) {
+                    doInvalidatePanelMenu(i);
+                }
+            }
+            mInvalidatePanelMenuPosted = false;
+            mInvalidatePanelMenuFeatures = 0;
+        }
+    };
+
+    private Transition mEnterTransition = null;
+    private Transition mReturnTransition = USE_DEFAULT_TRANSITION;
+    private Transition mExitTransition = null;
+    private Transition mReenterTransition = USE_DEFAULT_TRANSITION;
+    private Transition mSharedElementEnterTransition = null;
+    private Transition mSharedElementReturnTransition = USE_DEFAULT_TRANSITION;
+    private Transition mSharedElementExitTransition = null;
+    private Transition mSharedElementReenterTransition = USE_DEFAULT_TRANSITION;
+    private Boolean mAllowReturnTransitionOverlap;
+    private Boolean mAllowEnterTransitionOverlap;
+    private long mBackgroundFadeDurationMillis = -1;
+    private Boolean mSharedElementsUseOverlay;
+
+    private boolean mIsStartingWindow;
+    private int mTheme = -1;
+
+    private int mDecorCaptionShade = DECOR_CAPTION_SHADE_AUTO;
+
+    private boolean mUseDecorContext = false;
+
+    /** @see ViewRootImpl#mActivityConfigCallback */
+    private ActivityConfigCallback mActivityConfigCallback;
+
+    static class WindowManagerHolder {
+        static final IWindowManager sWindowManager = IWindowManager.Stub.asInterface(
+                ServiceManager.getService("window"));
+    }
+
+    static final RotationWatcher sRotationWatcher = new RotationWatcher();
+
+    public PhoneWindow(Context context) {
+        super(context);
+        mLayoutInflater = LayoutInflater.from(context);
+    }
+
+    /**
+     * Constructor for main window of an activity.
+     */
+    public PhoneWindow(Context context, Window preservedWindow,
+            ActivityConfigCallback activityConfigCallback) {
+        this(context);
+        // Only main activity windows use decor context, all the other windows depend on whatever
+        // context that was given to them.
+        mUseDecorContext = true;
+        if (preservedWindow != null) {
+            mDecor = (DecorView) preservedWindow.getDecorView();
+            mElevation = preservedWindow.getElevation();
+            mLoadElevation = false;
+            mForceDecorInstall = true;
+            // If we're preserving window, carry over the app token from the preserved
+            // window, as we'll be skipping the addView in handleResumeActivity(), and
+            // the token will not be updated as for a new window.
+            getAttributes().token = preservedWindow.getAttributes().token;
+        }
+        // Even though the device doesn't support picture-in-picture mode,
+        // an user can force using it through developer options.
+        boolean forceResizable = Settings.Global.getInt(context.getContentResolver(),
+                DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
+        mSupportsPictureInPicture = forceResizable || context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_PICTURE_IN_PICTURE);
+        mActivityConfigCallback = activityConfigCallback;
+    }
+
+    @Override
+    public final void setContainer(Window container) {
+        super.setContainer(container);
+    }
+
+    @Override
+    public boolean requestFeature(int featureId) {
+        if (mContentParentExplicitlySet) {
+            throw new AndroidRuntimeException("requestFeature() must be called before adding content");
+        }
+        final int features = getFeatures();
+        final int newFeatures = features | (1 << featureId);
+        if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
+                (newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
+            // Another feature is enabled and the user is trying to enable the custom title feature
+            // or custom title feature is enabled and the user is trying to enable another feature
+            throw new AndroidRuntimeException(
+                    "You cannot combine custom titles with other title features");
+        }
+        if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) {
+            return false; // Ignore. No title dominates.
+        }
+        if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) {
+            // Remove the action bar feature if we have no title. No title dominates.
+            removeFeature(FEATURE_ACTION_BAR);
+        }
+
+        if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_SWIPE_TO_DISMISS) {
+            throw new AndroidRuntimeException(
+                    "You cannot combine swipe dismissal and the action bar.");
+        }
+        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0 && featureId == FEATURE_ACTION_BAR) {
+            throw new AndroidRuntimeException(
+                    "You cannot combine swipe dismissal and the action bar.");
+        }
+
+        if (featureId == FEATURE_INDETERMINATE_PROGRESS &&
+                getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+            throw new AndroidRuntimeException("You cannot use indeterminate progress on a watch.");
+        }
+        return super.requestFeature(featureId);
+    }
+
+    @Override
+    public void setUiOptions(int uiOptions) {
+        mUiOptions = uiOptions;
+    }
+
+    @Override
+    public void setUiOptions(int uiOptions, int mask) {
+        mUiOptions = (mUiOptions & ~mask) | (uiOptions & mask);
+    }
+
+    @Override
+    public TransitionManager getTransitionManager() {
+        return mTransitionManager;
+    }
+
+    @Override
+    public void setTransitionManager(TransitionManager tm) {
+        mTransitionManager = tm;
+    }
+
+    @Override
+    public Scene getContentScene() {
+        return mContentScene;
+    }
+
+    @Override
+    public void setContentView(int layoutResID) {
+        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
+        // decor, when theme attributes and the like are crystalized. Do not check the feature
+        // before this happens.
+        if (mContentParent == null) {
+            installDecor();
+        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
+            mContentParent.removeAllViews();
+        }
+
+        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
+            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
+                    getContext());
+            transitionTo(newScene);
+        } else {
+            mLayoutInflater.inflate(layoutResID, mContentParent);
+        }
+        mContentParent.requestApplyInsets();
+        final Callback cb = getCallback();
+        if (cb != null && !isDestroyed()) {
+            cb.onContentChanged();
+        }
+        mContentParentExplicitlySet = true;
+    }
+
+    @Override
+    public void setContentView(View view) {
+        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+    }
+
+    @Override
+    public void setContentView(View view, ViewGroup.LayoutParams params) {
+        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
+        // decor, when theme attributes and the like are crystalized. Do not check the feature
+        // before this happens.
+        if (mContentParent == null) {
+            installDecor();
+        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
+            mContentParent.removeAllViews();
+        }
+
+        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
+            view.setLayoutParams(params);
+            final Scene newScene = new Scene(mContentParent, view);
+            transitionTo(newScene);
+        } else {
+            mContentParent.addView(view, params);
+        }
+        mContentParent.requestApplyInsets();
+        final Callback cb = getCallback();
+        if (cb != null && !isDestroyed()) {
+            cb.onContentChanged();
+        }
+        mContentParentExplicitlySet = true;
+    }
+
+    @Override
+    public void addContentView(View view, ViewGroup.LayoutParams params) {
+        if (mContentParent == null) {
+            installDecor();
+        }
+        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
+            // TODO Augment the scenes/transitions API to support this.
+            Log.v(TAG, "addContentView does not support content transitions");
+        }
+        mContentParent.addView(view, params);
+        mContentParent.requestApplyInsets();
+        final Callback cb = getCallback();
+        if (cb != null && !isDestroyed()) {
+            cb.onContentChanged();
+        }
+    }
+
+    @Override
+    public void clearContentView() {
+        if (mDecor != null) {
+            mDecor.clearContentView();
+        }
+    }
+
+    private void transitionTo(Scene scene) {
+        if (mContentScene == null) {
+            scene.enter();
+        } else {
+            mTransitionManager.transitionTo(scene);
+        }
+        mContentScene = scene;
+    }
+
+    @Override
+    public View getCurrentFocus() {
+        return mDecor != null ? mDecor.findFocus() : null;
+    }
+
+    @Override
+    public void takeSurface(Callback2 callback) {
+        mTakeSurfaceCallback = callback;
+    }
+
+    public void takeInputQueue(InputQueue.Callback callback) {
+        mTakeInputQueueCallback = callback;
+    }
+
+    @Override
+    public boolean isFloating() {
+        return mIsFloating;
+    }
+
+    public boolean isTranslucent() {
+        return mIsTranslucent;
+    }
+
+    /**
+     * @return Whether the window is currently showing the wallpaper.
+     */
+    boolean isShowingWallpaper() {
+        return (getAttributes().flags & FLAG_SHOW_WALLPAPER) != 0;
+    }
+
+    /**
+     * Return a LayoutInflater instance that can be used to inflate XML view layout
+     * resources for use in this Window.
+     *
+     * @return LayoutInflater The shared LayoutInflater.
+     */
+    @Override
+    public LayoutInflater getLayoutInflater() {
+        return mLayoutInflater;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        setTitle(title, true);
+    }
+
+    public void setTitle(CharSequence title, boolean updateAccessibilityTitle) {
+        if (mTitleView != null) {
+            mTitleView.setText(title);
+        } else if (mDecorContentParent != null) {
+            mDecorContentParent.setWindowTitle(title);
+        }
+        mTitle = title;
+        if (updateAccessibilityTitle) {
+            WindowManager.LayoutParams params = getAttributes();
+            if (!TextUtils.equals(title, params.accessibilityTitle)) {
+                params.accessibilityTitle = TextUtils.stringOrSpannedString(title);
+                if (mDecor != null) {
+                    // ViewRootImpl will make sure the change propagates to WindowManagerService
+                    ViewRootImpl vr = mDecor.getViewRootImpl();
+                    if (vr != null) {
+                        vr.onWindowTitleChanged();
+                    }
+                }
+                dispatchWindowAttributesChanged(getAttributes());
+            }
+        }
+    }
+
+    @Override
+    @Deprecated
+    public void setTitleColor(int textColor) {
+        if (mTitleView != null) {
+            mTitleView.setTextColor(textColor);
+        }
+        mTitleColor = textColor;
+    }
+
+    /**
+     * Prepares the panel to either be opened or chorded. This creates the Menu
+     * instance for the panel and populates it via the Activity callbacks.
+     *
+     * @param st The panel state to prepare.
+     * @param event The event that triggered the preparing of the panel.
+     * @return Whether the panel was prepared. If the panel should not be shown,
+     *         returns false.
+     */
+    public final boolean preparePanel(PanelFeatureState st, KeyEvent event) {
+        if (isDestroyed()) {
+            return false;
+        }
+
+        // Already prepared (isPrepared will be reset to false later)
+        if (st.isPrepared) {
+            return true;
+        }
+
+        if ((mPreparedPanel != null) && (mPreparedPanel != st)) {
+            // Another Panel is prepared and possibly open, so close it
+            closePanel(mPreparedPanel, false);
+        }
+
+        final Callback cb = getCallback();
+
+        if (cb != null) {
+            st.createdPanelView = cb.onCreatePanelView(st.featureId);
+        }
+
+        final boolean isActionBarMenu =
+                (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR);
+
+        if (isActionBarMenu && mDecorContentParent != null) {
+            // Enforce ordering guarantees around events so that the action bar never
+            // dispatches menu-related events before the panel is prepared.
+            mDecorContentParent.setMenuPrepared();
+        }
+
+        if (st.createdPanelView == null) {
+            // Init the panel state's menu--return false if init failed
+            if (st.menu == null || st.refreshMenuContent) {
+                if (st.menu == null) {
+                    if (!initializePanelMenu(st) || (st.menu == null)) {
+                        return false;
+                    }
+                }
+
+                if (isActionBarMenu && mDecorContentParent != null) {
+                    if (mActionMenuPresenterCallback == null) {
+                        mActionMenuPresenterCallback = new ActionMenuPresenterCallback();
+                    }
+                    mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback);
+                }
+
+                // Call callback, and return if it doesn't want to display menu.
+
+                // Creating the panel menu will involve a lot of manipulation;
+                // don't dispatch change events to presenters until we're done.
+                st.menu.stopDispatchingItemsChanged();
+                if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) {
+                    // Ditch the menu created above
+                    st.setMenu(null);
+
+                    if (isActionBarMenu && mDecorContentParent != null) {
+                        // Don't show it in the action bar either
+                        mDecorContentParent.setMenu(null, mActionMenuPresenterCallback);
+                    }
+
+                    return false;
+                }
+
+                st.refreshMenuContent = false;
+            }
+
+            // Callback and return if the callback does not want to show the menu
+
+            // Preparing the panel menu can involve a lot of manipulation;
+            // don't dispatch change events to presenters until we're done.
+            st.menu.stopDispatchingItemsChanged();
+
+            // Restore action view state before we prepare. This gives apps
+            // an opportunity to override frozen/restored state in onPrepare.
+            if (st.frozenActionViewState != null) {
+                st.menu.restoreActionViewStates(st.frozenActionViewState);
+                st.frozenActionViewState = null;
+            }
+
+            if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) {
+                if (isActionBarMenu && mDecorContentParent != null) {
+                    // The app didn't want to show the menu for now but it still exists.
+                    // Clear it out of the action bar.
+                    mDecorContentParent.setMenu(null, mActionMenuPresenterCallback);
+                }
+                st.menu.startDispatchingItemsChanged();
+                return false;
+            }
+
+            // Set the proper keymap
+            KeyCharacterMap kmap = KeyCharacterMap.load(
+                    event != null ? event.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD);
+            st.qwertyMode = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC;
+            st.menu.setQwertyMode(st.qwertyMode);
+            st.menu.startDispatchingItemsChanged();
+        }
+
+        // Set other state
+        st.isPrepared = true;
+        st.isHandled = false;
+        mPreparedPanel = st;
+
+        return true;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        // Action bars handle their own menu state
+        if (mDecorContentParent == null) {
+            PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+            if ((st != null) && (st.menu != null)) {
+                if (st.isOpen) {
+                    // Freeze state
+                    final Bundle state = new Bundle();
+                    if (st.iconMenuPresenter != null) {
+                        st.iconMenuPresenter.saveHierarchyState(state);
+                    }
+                    if (st.listMenuPresenter != null) {
+                        st.listMenuPresenter.saveHierarchyState(state);
+                    }
+
+                    // Remove the menu views since they need to be recreated
+                    // according to the new configuration
+                    clearMenuViews(st);
+
+                    // Re-open the same menu
+                    reopenMenu(false);
+
+                    // Restore state
+                    if (st.iconMenuPresenter != null) {
+                        st.iconMenuPresenter.restoreHierarchyState(state);
+                    }
+                    if (st.listMenuPresenter != null) {
+                        st.listMenuPresenter.restoreHierarchyState(state);
+                    }
+
+                } else {
+                    // Clear menu views so on next menu opening, it will use
+                    // the proper layout
+                    clearMenuViews(st);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onMultiWindowModeChanged() {
+        if (mDecor != null) {
+            mDecor.onConfigurationChanged(getContext().getResources().getConfiguration());
+        }
+    }
+
+    @Override
+    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+        if (mDecor != null) {
+            mDecor.updatePictureInPictureOutlineProvider(isInPictureInPictureMode);
+        }
+    }
+
+    @Override
+    public void reportActivityRelaunched() {
+        if (mDecor != null && mDecor.getViewRootImpl() != null) {
+            mDecor.getViewRootImpl().reportActivityRelaunched();
+        }
+    }
+
+    private static void clearMenuViews(PanelFeatureState st) {
+        // This can be called on config changes, so we should make sure
+        // the views will be reconstructed based on the new orientation, etc.
+
+        // Allow the callback to create a new panel view
+        st.createdPanelView = null;
+
+        // Causes the decor view to be recreated
+        st.refreshDecorView = true;
+
+        st.clearMenuPresenters();
+    }
+
+    @Override
+    public final void openPanel(int featureId, KeyEvent event) {
+        if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null &&
+                mDecorContentParent.canShowOverflowMenu() &&
+                !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) {
+            mDecorContentParent.showOverflowMenu();
+        } else {
+            openPanel(getPanelState(featureId, true), event);
+        }
+    }
+
+    private void openPanel(final PanelFeatureState st, KeyEvent event) {
+        // System.out.println("Open panel: isOpen=" + st.isOpen);
+
+        // Already open, return
+        if (st.isOpen || isDestroyed()) {
+            return;
+        }
+
+        // Don't open an options panel for honeycomb apps on xlarge devices.
+        // (The app should be using an action bar for menu items.)
+        if (st.featureId == FEATURE_OPTIONS_PANEL) {
+            Context context = getContext();
+            Configuration config = context.getResources().getConfiguration();
+            boolean isXLarge = (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) ==
+                    Configuration.SCREENLAYOUT_SIZE_XLARGE;
+            boolean isHoneycombApp = context.getApplicationInfo().targetSdkVersion >=
+                    android.os.Build.VERSION_CODES.HONEYCOMB;
+
+            if (isXLarge && isHoneycombApp) {
+                return;
+            }
+        }
+
+        Callback cb = getCallback();
+        if ((cb != null) && (!cb.onMenuOpened(st.featureId, st.menu))) {
+            // Callback doesn't want the menu to open, reset any state
+            closePanel(st, true);
+            return;
+        }
+
+        final WindowManager wm = getWindowManager();
+        if (wm == null) {
+            return;
+        }
+
+        // Prepare panel (should have been done before, but just in case)
+        if (!preparePanel(st, event)) {
+            return;
+        }
+
+        int width = WRAP_CONTENT;
+        if (st.decorView == null || st.refreshDecorView) {
+            if (st.decorView == null) {
+                // Initialize the panel decor, this will populate st.decorView
+                if (!initializePanelDecor(st) || (st.decorView == null))
+                    return;
+            } else if (st.refreshDecorView && (st.decorView.getChildCount() > 0)) {
+                // Decor needs refreshing, so remove its views
+                st.decorView.removeAllViews();
+            }
+
+            // This will populate st.shownPanelView
+            if (!initializePanelContent(st) || !st.hasPanelItems()) {
+                return;
+            }
+
+            ViewGroup.LayoutParams lp = st.shownPanelView.getLayoutParams();
+            if (lp == null) {
+                lp = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+            }
+
+            int backgroundResId;
+            if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
+                // If the contents is fill parent for the width, set the
+                // corresponding background
+                backgroundResId = st.fullBackground;
+                width = MATCH_PARENT;
+            } else {
+                // Otherwise, set the normal panel background
+                backgroundResId = st.background;
+            }
+            st.decorView.setWindowBackground(getContext().getDrawable(
+                    backgroundResId));
+
+            ViewParent shownPanelParent = st.shownPanelView.getParent();
+            if (shownPanelParent != null && shownPanelParent instanceof ViewGroup) {
+                ((ViewGroup) shownPanelParent).removeView(st.shownPanelView);
+            }
+            st.decorView.addView(st.shownPanelView, lp);
+
+            /*
+             * Give focus to the view, if it or one of its children does not
+             * already have it.
+             */
+            if (!st.shownPanelView.hasFocus()) {
+                st.shownPanelView.requestFocus();
+            }
+        } else if (!st.isInListMode()) {
+            width = MATCH_PARENT;
+        } else if (st.createdPanelView != null) {
+            // If we already had a panel view, carry width=MATCH_PARENT through
+            // as we did above when it was created.
+            ViewGroup.LayoutParams lp = st.createdPanelView.getLayoutParams();
+            if (lp != null && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
+                width = MATCH_PARENT;
+            }
+        }
+
+        st.isHandled = false;
+
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                width, WRAP_CONTENT,
+                st.x, st.y, WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG,
+                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                st.decorView.mDefaultOpacity);
+
+        if (st.isCompact) {
+            lp.gravity = getOptionsPanelGravity();
+            sRotationWatcher.addWindow(this);
+        } else {
+            lp.gravity = st.gravity;
+        }
+
+        lp.windowAnimations = st.windowAnimations;
+
+        wm.addView(st.decorView, lp);
+        st.isOpen = true;
+        // Log.v(TAG, "Adding main menu to window manager.");
+    }
+
+    @Override
+    public final void closePanel(int featureId) {
+        if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null &&
+                mDecorContentParent.canShowOverflowMenu() &&
+                !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) {
+            mDecorContentParent.hideOverflowMenu();
+        } else if (featureId == FEATURE_CONTEXT_MENU) {
+            closeContextMenu();
+        } else {
+            closePanel(getPanelState(featureId, true), true);
+        }
+    }
+
+    /**
+     * Closes the given panel.
+     *
+     * @param st The panel to be closed.
+     * @param doCallback Whether to notify the callback that the panel was
+     *            closed. If the panel is in the process of re-opening or
+     *            opening another panel (e.g., menu opening a sub menu), the
+     *            callback should not happen and this variable should be false.
+     *            In addition, this method internally will only perform the
+     *            callback if the panel is open.
+     */
+    public final void closePanel(PanelFeatureState st, boolean doCallback) {
+        // System.out.println("Close panel: isOpen=" + st.isOpen);
+        if (doCallback && st.featureId == FEATURE_OPTIONS_PANEL &&
+                mDecorContentParent != null && mDecorContentParent.isOverflowMenuShowing()) {
+            checkCloseActionMenu(st.menu);
+            return;
+        }
+
+        final ViewManager wm = getWindowManager();
+        if ((wm != null) && st.isOpen) {
+            if (st.decorView != null) {
+                wm.removeView(st.decorView);
+                // Log.v(TAG, "Removing main menu from window manager.");
+                if (st.isCompact) {
+                    sRotationWatcher.removeWindow(this);
+                }
+            }
+
+            if (doCallback) {
+                callOnPanelClosed(st.featureId, st, null);
+            }
+        }
+
+        st.isPrepared = false;
+        st.isHandled = false;
+        st.isOpen = false;
+
+        // This view is no longer shown, so null it out
+        st.shownPanelView = null;
+
+        if (st.isInExpandedMode) {
+            // Next time the menu opens, it should not be in expanded mode, so
+            // force a refresh of the decor
+            st.refreshDecorView = true;
+            st.isInExpandedMode = false;
+        }
+
+        if (mPreparedPanel == st) {
+            mPreparedPanel = null;
+            mPanelChordingKey = 0;
+        }
+    }
+
+    void checkCloseActionMenu(Menu menu) {
+        if (mClosingActionMenu) {
+            return;
+        }
+
+        mClosingActionMenu = true;
+        mDecorContentParent.dismissPopups();
+        Callback cb = getCallback();
+        if (cb != null && !isDestroyed()) {
+            cb.onPanelClosed(FEATURE_ACTION_BAR, menu);
+        }
+        mClosingActionMenu = false;
+    }
+
+    @Override
+    public final void togglePanel(int featureId, KeyEvent event) {
+        PanelFeatureState st = getPanelState(featureId, true);
+        if (st.isOpen) {
+            closePanel(st, true);
+        } else {
+            openPanel(st, event);
+        }
+    }
+
+    @Override
+    public void invalidatePanelMenu(int featureId) {
+        mInvalidatePanelMenuFeatures |= 1 << featureId;
+
+        if (!mInvalidatePanelMenuPosted && mDecor != null) {
+            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
+            mInvalidatePanelMenuPosted = true;
+        }
+    }
+
+    void doPendingInvalidatePanelMenu() {
+        if (mInvalidatePanelMenuPosted) {
+            mDecor.removeCallbacks(mInvalidatePanelMenuRunnable);
+            mInvalidatePanelMenuRunnable.run();
+        }
+    }
+
+    void doInvalidatePanelMenu(int featureId) {
+        PanelFeatureState st = getPanelState(featureId, false);
+        if (st == null) {
+            return;
+        }
+        Bundle savedActionViewStates = null;
+        if (st.menu != null) {
+            savedActionViewStates = new Bundle();
+            st.menu.saveActionViewStates(savedActionViewStates);
+            if (savedActionViewStates.size() > 0) {
+                st.frozenActionViewState = savedActionViewStates;
+            }
+            // This will be started again when the panel is prepared.
+            st.menu.stopDispatchingItemsChanged();
+            st.menu.clear();
+        }
+        st.refreshMenuContent = true;
+        st.refreshDecorView = true;
+
+        // Prepare the options panel if we have an action bar
+        if ((featureId == FEATURE_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL)
+                && mDecorContentParent != null) {
+            st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
+            if (st != null) {
+                st.isPrepared = false;
+                preparePanel(st, null);
+            }
+        }
+    }
+
+    /**
+     * Called when the panel key is pushed down.
+     * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}.
+     * @param event The key event.
+     * @return Whether the key was handled.
+     */
+    public final boolean onKeyDownPanel(int featureId, KeyEvent event) {
+        final int keyCode = event.getKeyCode();
+
+        if (event.getRepeatCount() == 0) {
+            // The panel key was pushed, so set the chording key
+            mPanelChordingKey = keyCode;
+
+            PanelFeatureState st = getPanelState(featureId, false);
+            if (st != null && !st.isOpen) {
+                return preparePanel(st, event);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Called when the panel key is released.
+     * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}.
+     * @param event The key event.
+     */
+    public final void onKeyUpPanel(int featureId, KeyEvent event) {
+        // The panel key was released, so clear the chording key
+        if (mPanelChordingKey != 0) {
+            mPanelChordingKey = 0;
+
+            final PanelFeatureState st = getPanelState(featureId, false);
+
+            if (event.isCanceled() || (mDecor != null && mDecor.mPrimaryActionMode != null) ||
+                    (st == null)) {
+                return;
+            }
+
+            boolean playSoundEffect = false;
+            if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null &&
+                    mDecorContentParent.canShowOverflowMenu() &&
+                    !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) {
+                if (!mDecorContentParent.isOverflowMenuShowing()) {
+                    if (!isDestroyed() && preparePanel(st, event)) {
+                        playSoundEffect = mDecorContentParent.showOverflowMenu();
+                    }
+                } else {
+                    playSoundEffect = mDecorContentParent.hideOverflowMenu();
+                }
+            } else {
+                if (st.isOpen || st.isHandled) {
+
+                    // Play the sound effect if the user closed an open menu (and not if
+                    // they just released a menu shortcut)
+                    playSoundEffect = st.isOpen;
+
+                    // Close menu
+                    closePanel(st, true);
+
+                } else if (st.isPrepared) {
+                    boolean show = true;
+                    if (st.refreshMenuContent) {
+                        // Something may have invalidated the menu since we prepared it.
+                        // Re-prepare it to refresh.
+                        st.isPrepared = false;
+                        show = preparePanel(st, event);
+                    }
+
+                    if (show) {
+                        // Write 'menu opened' to event log
+                        EventLog.writeEvent(50001, 0);
+
+                        // Show menu
+                        openPanel(st, event);
+
+                        playSoundEffect = true;
+                    }
+                }
+            }
+
+            if (playSoundEffect) {
+                AudioManager audioManager = (AudioManager) getContext().getSystemService(
+                        Context.AUDIO_SERVICE);
+                if (audioManager != null) {
+                    audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
+                } else {
+                    Log.w(TAG, "Couldn't get audio manager");
+                }
+            }
+        }
+    }
+
+    @Override
+    public final void closeAllPanels() {
+        final ViewManager wm = getWindowManager();
+        if (wm == null) {
+            return;
+        }
+
+        final PanelFeatureState[] panels = mPanels;
+        final int N = panels != null ? panels.length : 0;
+        for (int i = 0; i < N; i++) {
+            final PanelFeatureState panel = panels[i];
+            if (panel != null) {
+                closePanel(panel, true);
+            }
+        }
+
+        closeContextMenu();
+    }
+
+    /**
+     * Closes the context menu. This notifies the menu logic of the close, along
+     * with dismissing it from the UI.
+     */
+    private synchronized void closeContextMenu() {
+        if (mContextMenu != null) {
+            mContextMenu.close();
+            dismissContextMenu();
+        }
+    }
+
+    /**
+     * Dismisses just the context menu UI. To close the context menu, use
+     * {@link #closeContextMenu()}.
+     */
+    private synchronized void dismissContextMenu() {
+        mContextMenu = null;
+
+        if (mContextMenuHelper != null) {
+            mContextMenuHelper.dismiss();
+            mContextMenuHelper = null;
+        }
+    }
+
+    @Override
+    public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) {
+        return performPanelShortcut(getPanelState(featureId, false), keyCode, event, flags);
+    }
+
+    boolean performPanelShortcut(PanelFeatureState st, int keyCode, KeyEvent event,
+            int flags) {
+        if (event.isSystem() || (st == null)) {
+            return false;
+        }
+
+        boolean handled = false;
+
+        // Only try to perform menu shortcuts if preparePanel returned true (possible false
+        // return value from application not wanting to show the menu).
+        if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) {
+            // The menu is prepared now, perform the shortcut on it
+            handled = st.menu.performShortcut(keyCode, event, flags);
+        }
+
+        if (handled) {
+            // Mark as handled
+            st.isHandled = true;
+
+            // Only close down the menu if we don't have an action bar keeping it open.
+            if ((flags & Menu.FLAG_PERFORM_NO_CLOSE) == 0 && mDecorContentParent == null) {
+                closePanel(st, true);
+            }
+        }
+
+        return handled;
+    }
+
+    @Override
+    public boolean performPanelIdentifierAction(int featureId, int id, int flags) {
+
+        PanelFeatureState st = getPanelState(featureId, true);
+        if (!preparePanel(st, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU))) {
+            return false;
+        }
+        if (st.menu == null) {
+            return false;
+        }
+
+        boolean res = st.menu.performIdentifierAction(id, flags);
+
+        // Only close down the menu if we don't have an action bar keeping it open.
+        if (mDecorContentParent == null) {
+            closePanel(st, true);
+        }
+
+        return res;
+    }
+
+    public PanelFeatureState findMenuPanel(Menu menu) {
+        final PanelFeatureState[] panels = mPanels;
+        final int N = panels != null ? panels.length : 0;
+        for (int i = 0; i < N; i++) {
+            final PanelFeatureState panel = panels[i];
+            if (panel != null && panel.menu == menu) {
+                return panel;
+            }
+        }
+        return null;
+    }
+
+    public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+        final Callback cb = getCallback();
+        if (cb != null && !isDestroyed()) {
+            final PanelFeatureState panel = findMenuPanel(menu.getRootMenu());
+            if (panel != null) {
+                return cb.onMenuItemSelected(panel.featureId, item);
+            }
+        }
+        return false;
+    }
+
+    public void onMenuModeChange(MenuBuilder menu) {
+        reopenMenu(true);
+    }
+
+    private void reopenMenu(boolean toggleMenuMode) {
+        if (mDecorContentParent != null && mDecorContentParent.canShowOverflowMenu() &&
+                (!ViewConfiguration.get(getContext()).hasPermanentMenuKey() ||
+                        mDecorContentParent.isOverflowMenuShowPending())) {
+            final Callback cb = getCallback();
+            if (!mDecorContentParent.isOverflowMenuShowing() || !toggleMenuMode) {
+                if (cb != null && !isDestroyed()) {
+                    // If we have a menu invalidation pending, do it now.
+                    if (mInvalidatePanelMenuPosted &&
+                            (mInvalidatePanelMenuFeatures & (1 << FEATURE_OPTIONS_PANEL)) != 0) {
+                        mDecor.removeCallbacks(mInvalidatePanelMenuRunnable);
+                        mInvalidatePanelMenuRunnable.run();
+                    }
+
+                    final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+
+                    // If we don't have a menu or we're waiting for a full content refresh,
+                    // forget it. This is a lingering event that no longer matters.
+                    if (st != null && st.menu != null && !st.refreshMenuContent &&
+                            cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) {
+                        cb.onMenuOpened(FEATURE_ACTION_BAR, st.menu);
+                        mDecorContentParent.showOverflowMenu();
+                    }
+                }
+            } else {
+                mDecorContentParent.hideOverflowMenu();
+                final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+                if (st != null && cb != null && !isDestroyed()) {
+                    cb.onPanelClosed(FEATURE_ACTION_BAR, st.menu);
+                }
+            }
+            return;
+        }
+
+        PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+
+        if (st == null) {
+            return;
+        }
+
+        // Save the future expanded mode state since closePanel will reset it
+        boolean newExpandedMode = toggleMenuMode ? !st.isInExpandedMode : st.isInExpandedMode;
+
+        st.refreshDecorView = true;
+        closePanel(st, false);
+
+        // Set the expanded mode state
+        st.isInExpandedMode = newExpandedMode;
+
+        openPanel(st, null);
+    }
+
+    /**
+     * Initializes the menu associated with the given panel feature state. You
+     * must at the very least set PanelFeatureState.menu to the Menu to be
+     * associated with the given panel state. The default implementation creates
+     * a new menu for the panel state.
+     *
+     * @param st The panel whose menu is being initialized.
+     * @return Whether the initialization was successful.
+     */
+    protected boolean initializePanelMenu(final PanelFeatureState st) {
+        Context context = getContext();
+
+        // If we have an action bar, initialize the menu with the right theme.
+        if ((st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR) &&
+                mDecorContentParent != null) {
+            final TypedValue outValue = new TypedValue();
+            final Theme baseTheme = context.getTheme();
+            baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true);
+
+            Theme widgetTheme = null;
+            if (outValue.resourceId != 0) {
+                widgetTheme = context.getResources().newTheme();
+                widgetTheme.setTo(baseTheme);
+                widgetTheme.applyStyle(outValue.resourceId, true);
+                widgetTheme.resolveAttribute(
+                        R.attr.actionBarWidgetTheme, outValue, true);
+            } else {
+                baseTheme.resolveAttribute(
+                        R.attr.actionBarWidgetTheme, outValue, true);
+            }
+
+            if (outValue.resourceId != 0) {
+                if (widgetTheme == null) {
+                    widgetTheme = context.getResources().newTheme();
+                    widgetTheme.setTo(baseTheme);
+                }
+                widgetTheme.applyStyle(outValue.resourceId, true);
+            }
+
+            if (widgetTheme != null) {
+                context = new ContextThemeWrapper(context, 0);
+                context.getTheme().setTo(widgetTheme);
+            }
+        }
+
+        final MenuBuilder menu = new MenuBuilder(context);
+        menu.setCallback(this);
+        st.setMenu(menu);
+
+        return true;
+    }
+
+    /**
+     * Perform initial setup of a panel. This should at the very least set the
+     * style information in the PanelFeatureState and must set
+     * PanelFeatureState.decor to the panel's window decor view.
+     *
+     * @param st The panel being initialized.
+     */
+    protected boolean initializePanelDecor(PanelFeatureState st) {
+        st.decorView = generateDecor(st.featureId);
+        st.gravity = Gravity.CENTER | Gravity.BOTTOM;
+        st.setStyle(getContext());
+        TypedArray a = getContext().obtainStyledAttributes(null,
+                R.styleable.Window, 0, st.listPresenterTheme);
+        final float elevation = a.getDimension(R.styleable.Window_windowElevation, 0);
+        if (elevation != 0) {
+            st.decorView.setElevation(elevation);
+        }
+        a.recycle();
+
+        return true;
+    }
+
+    /**
+     * Determine the gravity value for the options panel. This can
+     * differ in compact mode.
+     *
+     * @return gravity value to use for the panel window
+     */
+    private int getOptionsPanelGravity() {
+        try {
+            return WindowManagerHolder.sWindowManager.getPreferredOptionsPanelGravity();
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Couldn't getOptionsPanelGravity; using default", ex);
+            return Gravity.CENTER | Gravity.BOTTOM;
+        }
+    }
+
+    void onOptionsPanelRotationChanged() {
+        final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+        if (st == null) return;
+
+        final WindowManager.LayoutParams lp = st.decorView != null ?
+                (WindowManager.LayoutParams) st.decorView.getLayoutParams() : null;
+        if (lp != null) {
+            lp.gravity = getOptionsPanelGravity();
+            final ViewManager wm = getWindowManager();
+            if (wm != null) {
+                wm.updateViewLayout(st.decorView, lp);
+            }
+        }
+    }
+
+    /**
+     * Initializes the panel associated with the panel feature state. You must
+     * at the very least set PanelFeatureState.panel to the View implementing
+     * its contents. The default implementation gets the panel from the menu.
+     *
+     * @param st The panel state being initialized.
+     * @return Whether the initialization was successful.
+     */
+    protected boolean initializePanelContent(PanelFeatureState st) {
+        if (st.createdPanelView != null) {
+            st.shownPanelView = st.createdPanelView;
+            return true;
+        }
+
+        if (st.menu == null) {
+            return false;
+        }
+
+        if (mPanelMenuPresenterCallback == null) {
+            mPanelMenuPresenterCallback = new PanelMenuPresenterCallback();
+        }
+
+        MenuView menuView = st.isInListMode()
+                ? st.getListMenuView(getContext(), mPanelMenuPresenterCallback)
+                : st.getIconMenuView(getContext(), mPanelMenuPresenterCallback);
+
+        st.shownPanelView = (View) menuView;
+
+        if (st.shownPanelView != null) {
+            // Use the menu View's default animations if it has any
+            final int defaultAnimations = menuView.getWindowAnimations();
+            if (defaultAnimations != 0) {
+                st.windowAnimations = defaultAnimations;
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean performContextMenuIdentifierAction(int id, int flags) {
+        return (mContextMenu != null) ? mContextMenu.performIdentifierAction(id, flags) : false;
+    }
+
+    @Override
+    public final void setElevation(float elevation) {
+        mElevation = elevation;
+        final WindowManager.LayoutParams attrs = getAttributes();
+        if (mDecor != null) {
+            mDecor.setElevation(elevation);
+            attrs.setSurfaceInsets(mDecor, true /*manual*/, false /*preservePrevious*/);
+        }
+        dispatchWindowAttributesChanged(attrs);
+    }
+
+    @Override
+    public float getElevation() {
+        return mElevation;
+    }
+
+    @Override
+    public final void setClipToOutline(boolean clipToOutline) {
+        mClipToOutline = clipToOutline;
+        if (mDecor != null) {
+            mDecor.setClipToOutline(clipToOutline);
+        }
+    }
+
+    @Override
+    public final void setBackgroundDrawable(Drawable drawable) {
+        if (drawable != mBackgroundDrawable || mBackgroundResource != 0) {
+            mBackgroundResource = 0;
+            mBackgroundDrawable = drawable;
+            if (mDecor != null) {
+                mDecor.setWindowBackground(drawable);
+            }
+            if (mBackgroundFallbackResource != 0) {
+                mDecor.setBackgroundFallback(drawable != null ? 0 : mBackgroundFallbackResource);
+            }
+        }
+    }
+
+    @Override
+    public final void setFeatureDrawableResource(int featureId, int resId) {
+        if (resId != 0) {
+            DrawableFeatureState st = getDrawableState(featureId, true);
+            if (st.resid != resId) {
+                st.resid = resId;
+                st.uri = null;
+                st.local = getContext().getDrawable(resId);
+                updateDrawable(featureId, st, false);
+            }
+        } else {
+            setFeatureDrawable(featureId, null);
+        }
+    }
+
+    @Override
+    public final void setFeatureDrawableUri(int featureId, Uri uri) {
+        if (uri != null) {
+            DrawableFeatureState st = getDrawableState(featureId, true);
+            if (st.uri == null || !st.uri.equals(uri)) {
+                st.resid = 0;
+                st.uri = uri;
+                st.local = loadImageURI(uri);
+                updateDrawable(featureId, st, false);
+            }
+        } else {
+            setFeatureDrawable(featureId, null);
+        }
+    }
+
+    @Override
+    public final void setFeatureDrawable(int featureId, Drawable drawable) {
+        DrawableFeatureState st = getDrawableState(featureId, true);
+        st.resid = 0;
+        st.uri = null;
+        if (st.local != drawable) {
+            st.local = drawable;
+            updateDrawable(featureId, st, false);
+        }
+    }
+
+    @Override
+    public void setFeatureDrawableAlpha(int featureId, int alpha) {
+        DrawableFeatureState st = getDrawableState(featureId, true);
+        if (st.alpha != alpha) {
+            st.alpha = alpha;
+            updateDrawable(featureId, st, false);
+        }
+    }
+
+    protected final void setFeatureDefaultDrawable(int featureId, Drawable drawable) {
+        DrawableFeatureState st = getDrawableState(featureId, true);
+        if (st.def != drawable) {
+            st.def = drawable;
+            updateDrawable(featureId, st, false);
+        }
+    }
+
+    @Override
+    public final void setFeatureInt(int featureId, int value) {
+        // XXX Should do more management (as with drawable features) to
+        // deal with interactions between multiple window policies.
+        updateInt(featureId, value, false);
+    }
+
+    /**
+     * Update the state of a drawable feature. This should be called, for every
+     * drawable feature supported, as part of onActive(), to make sure that the
+     * contents of a containing window is properly updated.
+     *
+     * @see #onActive
+     * @param featureId The desired drawable feature to change.
+     * @param fromActive Always true when called from onActive().
+     */
+    protected final void updateDrawable(int featureId, boolean fromActive) {
+        final DrawableFeatureState st = getDrawableState(featureId, false);
+        if (st != null) {
+            updateDrawable(featureId, st, fromActive);
+        }
+    }
+
+    /**
+     * Called when a Drawable feature changes, for the window to update its
+     * graphics.
+     *
+     * @param featureId The feature being changed.
+     * @param drawable The new Drawable to show, or null if none.
+     * @param alpha The new alpha blending of the Drawable.
+     */
+    protected void onDrawableChanged(int featureId, Drawable drawable, int alpha) {
+        ImageView view;
+        if (featureId == FEATURE_LEFT_ICON) {
+            view = getLeftIconView();
+        } else if (featureId == FEATURE_RIGHT_ICON) {
+            view = getRightIconView();
+        } else {
+            return;
+        }
+
+        if (drawable != null) {
+            drawable.setAlpha(alpha);
+            view.setImageDrawable(drawable);
+            view.setVisibility(View.VISIBLE);
+        } else {
+            view.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Called when an int feature changes, for the window to update its
+     * graphics.
+     *
+     * @param featureId The feature being changed.
+     * @param value The new integer value.
+     */
+    protected void onIntChanged(int featureId, int value) {
+        if (featureId == FEATURE_PROGRESS || featureId == FEATURE_INDETERMINATE_PROGRESS) {
+            updateProgressBars(value);
+        } else if (featureId == FEATURE_CUSTOM_TITLE) {
+            FrameLayout titleContainer = findViewById(R.id.title_container);
+            if (titleContainer != null) {
+                mLayoutInflater.inflate(value, titleContainer);
+            }
+        }
+    }
+
+    /**
+     * Updates the progress bars that are shown in the title bar.
+     *
+     * @param value Can be one of {@link Window#PROGRESS_VISIBILITY_ON},
+     *            {@link Window#PROGRESS_VISIBILITY_OFF},
+     *            {@link Window#PROGRESS_INDETERMINATE_ON},
+     *            {@link Window#PROGRESS_INDETERMINATE_OFF}, or a value
+     *            starting at {@link Window#PROGRESS_START} through
+     *            {@link Window#PROGRESS_END} for setting the default
+     *            progress (if {@link Window#PROGRESS_END} is given,
+     *            the progress bar widgets in the title will be hidden after an
+     *            animation), a value between
+     *            {@link Window#PROGRESS_SECONDARY_START} -
+     *            {@link Window#PROGRESS_SECONDARY_END} for the
+     *            secondary progress (if
+     *            {@link Window#PROGRESS_SECONDARY_END} is given, the
+     *            progress bar widgets will still be shown with the secondary
+     *            progress bar will be completely filled in.)
+     */
+    private void updateProgressBars(int value) {
+        ProgressBar circularProgressBar = getCircularProgressBar(true);
+        ProgressBar horizontalProgressBar = getHorizontalProgressBar(true);
+
+        final int features = getLocalFeatures();
+        if (value == PROGRESS_VISIBILITY_ON) {
+            if ((features & (1 << FEATURE_PROGRESS)) != 0) {
+                if (horizontalProgressBar != null) {
+                    int level = horizontalProgressBar.getProgress();
+                    int visibility = (horizontalProgressBar.isIndeterminate() || level < 10000) ?
+                            View.VISIBLE : View.INVISIBLE;
+                    horizontalProgressBar.setVisibility(visibility);
+                } else {
+                    Log.e(TAG, "Horizontal progress bar not located in current window decor");
+                }
+            }
+            if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
+                if (circularProgressBar != null) {
+                    circularProgressBar.setVisibility(View.VISIBLE);
+                } else {
+                    Log.e(TAG, "Circular progress bar not located in current window decor");
+                }
+            }
+        } else if (value == PROGRESS_VISIBILITY_OFF) {
+            if ((features & (1 << FEATURE_PROGRESS)) != 0) {
+                if (horizontalProgressBar != null) {
+                    horizontalProgressBar.setVisibility(View.GONE);
+                } else {
+                    Log.e(TAG, "Horizontal progress bar not located in current window decor");
+                }
+            }
+            if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
+                if (circularProgressBar != null) {
+                    circularProgressBar.setVisibility(View.GONE);
+                } else {
+                    Log.e(TAG, "Circular progress bar not located in current window decor");
+                }
+            }
+        } else if (value == PROGRESS_INDETERMINATE_ON) {
+            if (horizontalProgressBar != null) {
+                horizontalProgressBar.setIndeterminate(true);
+            } else {
+                Log.e(TAG, "Horizontal progress bar not located in current window decor");
+            }
+        } else if (value == PROGRESS_INDETERMINATE_OFF) {
+            if (horizontalProgressBar != null) {
+                horizontalProgressBar.setIndeterminate(false);
+            } else {
+                Log.e(TAG, "Horizontal progress bar not located in current window decor");
+            }
+        } else if (PROGRESS_START <= value && value <= PROGRESS_END) {
+            // We want to set the progress value before testing for visibility
+            // so that when the progress bar becomes visible again, it has the
+            // correct level.
+            if (horizontalProgressBar != null) {
+                horizontalProgressBar.setProgress(value - PROGRESS_START);
+            } else {
+                Log.e(TAG, "Horizontal progress bar not located in current window decor");
+            }
+
+            if (value < PROGRESS_END) {
+                showProgressBars(horizontalProgressBar, circularProgressBar);
+            } else {
+                hideProgressBars(horizontalProgressBar, circularProgressBar);
+            }
+        } else if (PROGRESS_SECONDARY_START <= value && value <= PROGRESS_SECONDARY_END) {
+            if (horizontalProgressBar != null) {
+                horizontalProgressBar.setSecondaryProgress(value - PROGRESS_SECONDARY_START);
+            } else {
+                Log.e(TAG, "Horizontal progress bar not located in current window decor");
+            }
+
+            showProgressBars(horizontalProgressBar, circularProgressBar);
+        }
+
+    }
+
+    private void showProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) {
+        final int features = getLocalFeatures();
+        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 &&
+                spinnyProgressBar != null && spinnyProgressBar.getVisibility() == View.INVISIBLE) {
+            spinnyProgressBar.setVisibility(View.VISIBLE);
+        }
+        // Only show the progress bars if the primary progress is not complete
+        if ((features & (1 << FEATURE_PROGRESS)) != 0 && horizontalProgressBar != null &&
+                horizontalProgressBar.getProgress() < 10000) {
+            horizontalProgressBar.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private void hideProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) {
+        final int features = getLocalFeatures();
+        Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.fade_out);
+        anim.setDuration(1000);
+        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 &&
+                spinnyProgressBar != null &&
+                spinnyProgressBar.getVisibility() == View.VISIBLE) {
+            spinnyProgressBar.startAnimation(anim);
+            spinnyProgressBar.setVisibility(View.INVISIBLE);
+        }
+        if ((features & (1 << FEATURE_PROGRESS)) != 0 && horizontalProgressBar != null &&
+                horizontalProgressBar.getVisibility() == View.VISIBLE) {
+            horizontalProgressBar.startAnimation(anim);
+            horizontalProgressBar.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    @Override
+    public void setIcon(int resId) {
+        mIconRes = resId;
+        mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON;
+        mResourcesSetFlags &= ~FLAG_RESOURCE_SET_ICON_FALLBACK;
+        if (mDecorContentParent != null) {
+            mDecorContentParent.setIcon(resId);
+        }
+    }
+
+    @Override
+    public void setDefaultIcon(int resId) {
+        if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0) {
+            return;
+        }
+        mIconRes = resId;
+        if (mDecorContentParent != null && (!mDecorContentParent.hasIcon() ||
+                (mResourcesSetFlags & FLAG_RESOURCE_SET_ICON_FALLBACK) != 0)) {
+            if (resId != 0) {
+                mDecorContentParent.setIcon(resId);
+                mResourcesSetFlags &= ~FLAG_RESOURCE_SET_ICON_FALLBACK;
+            } else {
+                mDecorContentParent.setIcon(
+                        getContext().getPackageManager().getDefaultActivityIcon());
+                mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
+            }
+        }
+    }
+
+    @Override
+    public void setLogo(int resId) {
+        mLogoRes = resId;
+        mResourcesSetFlags |= FLAG_RESOURCE_SET_LOGO;
+        if (mDecorContentParent != null) {
+            mDecorContentParent.setLogo(resId);
+        }
+    }
+
+    @Override
+    public void setDefaultLogo(int resId) {
+        if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0) {
+            return;
+        }
+        mLogoRes = resId;
+        if (mDecorContentParent != null && !mDecorContentParent.hasLogo()) {
+            mDecorContentParent.setLogo(resId);
+        }
+    }
+
+    @Override
+    public void setLocalFocus(boolean hasFocus, boolean inTouchMode) {
+        getViewRootImpl().windowFocusChanged(hasFocus, inTouchMode);
+
+    }
+
+    @Override
+    public void injectInputEvent(InputEvent event) {
+        getViewRootImpl().dispatchInputEvent(event);
+    }
+
+    private ViewRootImpl getViewRootImpl() {
+        if (mDecor != null) {
+            ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
+            if (viewRootImpl != null) {
+                return viewRootImpl;
+            }
+        }
+        throw new IllegalStateException("view not added");
+    }
+
+    /**
+     * Request that key events come to this activity. Use this if your activity
+     * has no views with focus, but the activity still wants a chance to process
+     * key events.
+     */
+    @Override
+    public void takeKeyEvents(boolean get) {
+        mDecor.setFocusable(get);
+    }
+
+    @Override
+    public boolean superDispatchKeyEvent(KeyEvent event) {
+        return mDecor.superDispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
+        return mDecor.superDispatchKeyShortcutEvent(event);
+    }
+
+    @Override
+    public boolean superDispatchTouchEvent(MotionEvent event) {
+        return mDecor.superDispatchTouchEvent(event);
+    }
+
+    @Override
+    public boolean superDispatchTrackballEvent(MotionEvent event) {
+        return mDecor.superDispatchTrackballEvent(event);
+    }
+
+    @Override
+    public boolean superDispatchGenericMotionEvent(MotionEvent event) {
+        return mDecor.superDispatchGenericMotionEvent(event);
+    }
+
+    /**
+     * A key was pressed down and not handled by anything else in the window.
+     *
+     * @see #onKeyUp
+     * @see android.view.KeyEvent
+     */
+    protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) {
+        /* ****************************************************************************
+         * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES.
+         *
+         * If your key handling must happen before the app gets a crack at the event,
+         * it goes in PhoneWindowManager.
+         *
+         * If your key handling should happen in all windows, and does not depend on
+         * the state of the current application, other than that the current
+         * application can override the behavior by handling the event itself, it
+         * should go in PhoneFallbackEventHandler.
+         *
+         * Only if your handling depends on the window, and the fact that it has
+         * a DecorView, should it go here.
+         * ****************************************************************************/
+
+        final KeyEvent.DispatcherState dispatcher =
+                mDecor != null ? mDecor.getKeyDispatcherState() : null;
+        //Log.i(TAG, "Key down: repeat=" + event.getRepeatCount()
+        //        + " flags=0x" + Integer.toHexString(event.getFlags()));
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+            case KeyEvent.KEYCODE_VOLUME_MUTE: {
+                // If we have a session send it the volume command, otherwise
+                // use the suggested stream.
+                if (mMediaController != null) {
+                    int direction = 0;
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_VOLUME_UP:
+                            direction = AudioManager.ADJUST_RAISE;
+                            break;
+                        case KeyEvent.KEYCODE_VOLUME_DOWN:
+                            direction = AudioManager.ADJUST_LOWER;
+                            break;
+                        case KeyEvent.KEYCODE_VOLUME_MUTE:
+                            direction = AudioManager.ADJUST_TOGGLE_MUTE;
+                            break;
+                    }
+                    mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
+                } else {
+                    MediaSessionLegacyHelper.getHelper(getContext()).sendVolumeKeyEvent(
+                            event, mVolumeControlStreamType, false);
+                }
+                return true;
+            }
+            // These are all the recognized media key codes in
+            // KeyEvent.isMediaKey()
+            case KeyEvent.KEYCODE_MEDIA_PLAY:
+            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+            case KeyEvent.KEYCODE_MUTE:
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            case KeyEvent.KEYCODE_MEDIA_STOP:
+            case KeyEvent.KEYCODE_MEDIA_NEXT:
+            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+            case KeyEvent.KEYCODE_MEDIA_REWIND:
+            case KeyEvent.KEYCODE_MEDIA_RECORD:
+            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
+                if (mMediaController != null) {
+                    if (mMediaController.dispatchMediaButtonEvent(event)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            case KeyEvent.KEYCODE_MENU: {
+                onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event);
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_BACK: {
+                if (event.getRepeatCount() > 0) break;
+                if (featureId < 0) break;
+                // Currently don't do anything with long press.
+                if (dispatcher != null) {
+                    dispatcher.startTracking(event, this);
+                }
+                return true;
+            }
+
+        }
+
+        return false;
+    }
+
+    private KeyguardManager getKeyguardManager() {
+        if (mKeyguardManager == null) {
+            mKeyguardManager = (KeyguardManager) getContext().getSystemService(
+                    Context.KEYGUARD_SERVICE);
+        }
+        return mKeyguardManager;
+    }
+
+    AudioManager getAudioManager() {
+        if (mAudioManager == null) {
+            mAudioManager = (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE);
+        }
+        return mAudioManager;
+    }
+
+    /**
+     * A key was released and not handled by anything else in the window.
+     *
+     * @see #onKeyDown
+     * @see android.view.KeyEvent
+     */
+    protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) {
+        final KeyEvent.DispatcherState dispatcher =
+                mDecor != null ? mDecor.getKeyDispatcherState() : null;
+        if (dispatcher != null) {
+            dispatcher.handleUpEvent(event);
+        }
+        //Log.i(TAG, "Key up: repeat=" + event.getRepeatCount()
+        //        + " flags=0x" + Integer.toHexString(event.getFlags()));
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN: {
+                // If we have a session send it the volume command, otherwise
+                // use the suggested stream.
+                if (mMediaController != null) {
+                    final int flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE
+                            | AudioManager.FLAG_FROM_KEY;
+                    mMediaController.adjustVolume(0, flags);
+                } else {
+                    MediaSessionLegacyHelper.getHelper(getContext()).sendVolumeKeyEvent(
+                            event, mVolumeControlStreamType, false);
+                }
+                return true;
+            }
+            case KeyEvent.KEYCODE_VOLUME_MUTE: {
+                // Similar code is in PhoneFallbackEventHandler in case the window
+                // doesn't have one of these.  In this case, we execute it here and
+                // eat the event instead, because we have mVolumeControlStreamType
+                // and they don't.
+                MediaSessionLegacyHelper.getHelper(getContext()).sendVolumeKeyEvent(
+                        event, AudioManager.USE_DEFAULT_STREAM_TYPE, false);
+                return true;
+            }
+            // These are all the recognized media key codes in
+            // KeyEvent.isMediaKey()
+            case KeyEvent.KEYCODE_MEDIA_PLAY:
+            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+            case KeyEvent.KEYCODE_MUTE:
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            case KeyEvent.KEYCODE_MEDIA_STOP:
+            case KeyEvent.KEYCODE_MEDIA_NEXT:
+            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+            case KeyEvent.KEYCODE_MEDIA_REWIND:
+            case KeyEvent.KEYCODE_MEDIA_RECORD:
+            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
+                if (mMediaController != null) {
+                    if (mMediaController.dispatchMediaButtonEvent(event)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            case KeyEvent.KEYCODE_MENU: {
+                onKeyUpPanel(featureId < 0 ? FEATURE_OPTIONS_PANEL : featureId,
+                        event);
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_BACK: {
+                if (featureId < 0) break;
+                if (event.isTracking() && !event.isCanceled()) {
+                    if (featureId == FEATURE_OPTIONS_PANEL) {
+                        PanelFeatureState st = getPanelState(featureId, false);
+                        if (st != null && st.isInExpandedMode) {
+                            // If the user is in an expanded menu and hits back, it
+                            // should go back to the icon menu
+                            reopenMenu(true);
+                            return true;
+                        }
+                    }
+                    closePanel(featureId);
+                    return true;
+                }
+                break;
+            }
+
+            case KeyEvent.KEYCODE_SEARCH: {
+                /*
+                 * Do this in onKeyUp since the Search key is also used for
+                 * chording quick launch shortcuts.
+                 */
+                if (getKeyguardManager().inKeyguardRestrictedInputMode()) {
+                    break;
+                }
+                if (event.isTracking() && !event.isCanceled()) {
+                    launchDefaultSearch(event);
+                }
+                return true;
+            }
+
+            case KeyEvent.KEYCODE_WINDOW: {
+                if (mSupportsPictureInPicture && !event.isCanceled()) {
+                    getWindowControllerCallback().enterPictureInPictureModeIfPossible();
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void onActive() {
+    }
+
+    @Override
+    public final View getDecorView() {
+        if (mDecor == null || mForceDecorInstall) {
+            installDecor();
+        }
+        return mDecor;
+    }
+
+    @Override
+    public final View peekDecorView() {
+        return mDecor;
+    }
+
+    /** Notify when decor view is attached to window and {@link ViewRootImpl} is available. */
+    void onViewRootImplSet(ViewRootImpl viewRoot) {
+        viewRoot.setActivityConfigCallback(mActivityConfigCallback);
+    }
+
+    static private final String FOCUSED_ID_TAG = "android:focusedViewId";
+    static private final String VIEWS_TAG = "android:views";
+    static private final String PANELS_TAG = "android:Panels";
+    static private final String ACTION_BAR_TAG = "android:ActionBar";
+
+    /** {@inheritDoc} */
+    @Override
+    public Bundle saveHierarchyState() {
+        Bundle outState = new Bundle();
+        if (mContentParent == null) {
+            return outState;
+        }
+
+        SparseArray<Parcelable> states = new SparseArray<Parcelable>();
+        mContentParent.saveHierarchyState(states);
+        outState.putSparseParcelableArray(VIEWS_TAG, states);
+
+        // Save the focused view ID.
+        final View focusedView = mContentParent.findFocus();
+        if (focusedView != null && focusedView.getId() != View.NO_ID) {
+            outState.putInt(FOCUSED_ID_TAG, focusedView.getId());
+        }
+
+        // save the panels
+        SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>();
+        savePanelState(panelStates);
+        if (panelStates.size() > 0) {
+            outState.putSparseParcelableArray(PANELS_TAG, panelStates);
+        }
+
+        if (mDecorContentParent != null) {
+            SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>();
+            mDecorContentParent.saveToolbarHierarchyState(actionBarStates);
+            outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates);
+        }
+
+        return outState;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void restoreHierarchyState(Bundle savedInstanceState) {
+        if (mContentParent == null) {
+            return;
+        }
+
+        SparseArray<Parcelable> savedStates
+                = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);
+        if (savedStates != null) {
+            mContentParent.restoreHierarchyState(savedStates);
+        }
+
+        // restore the focused view
+        int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID);
+        if (focusedViewId != View.NO_ID) {
+            View needsFocus = mContentParent.findViewById(focusedViewId);
+            if (needsFocus != null) {
+                needsFocus.requestFocus();
+            } else {
+                Log.w(TAG,
+                        "Previously focused view reported id " + focusedViewId
+                                + " during save, but can't be found during restore.");
+            }
+        }
+
+        // Restore the panels.
+        SparseArray<Parcelable> panelStates = savedInstanceState.getSparseParcelableArray(PANELS_TAG);
+        if (panelStates != null) {
+            restorePanelState(panelStates);
+        }
+
+        if (mDecorContentParent != null) {
+            SparseArray<Parcelable> actionBarStates =
+                    savedInstanceState.getSparseParcelableArray(ACTION_BAR_TAG);
+            if (actionBarStates != null) {
+                doPendingInvalidatePanelMenu();
+                mDecorContentParent.restoreToolbarHierarchyState(actionBarStates);
+            } else {
+                Log.w(TAG, "Missing saved instance states for action bar views! " +
+                        "State will not be restored.");
+            }
+        }
+    }
+
+    /**
+     * Invoked when the panels should freeze their state.
+     *
+     * @param icicles Save state into this. This is usually indexed by the
+     *            featureId. This will be given to {@link #restorePanelState} in the
+     *            future.
+     */
+    private void savePanelState(SparseArray<Parcelable> icicles) {
+        PanelFeatureState[] panels = mPanels;
+        if (panels == null) {
+            return;
+        }
+
+        for (int curFeatureId = panels.length - 1; curFeatureId >= 0; curFeatureId--) {
+            if (panels[curFeatureId] != null) {
+                icicles.put(curFeatureId, panels[curFeatureId].onSaveInstanceState());
+            }
+        }
+    }
+
+    /**
+     * Invoked when the panels should thaw their state from a previously frozen state.
+     *
+     * @param icicles The state saved by {@link #savePanelState} that needs to be thawed.
+     */
+    private void restorePanelState(SparseArray<Parcelable> icicles) {
+        PanelFeatureState st;
+        int curFeatureId;
+        for (int i = icicles.size() - 1; i >= 0; i--) {
+            curFeatureId = icicles.keyAt(i);
+            st = getPanelState(curFeatureId, false /* required */);
+            if (st == null) {
+                // The panel must not have been required, and is currently not around, skip it
+                continue;
+            }
+
+            st.onRestoreInstanceState(icicles.get(curFeatureId));
+            invalidatePanelMenu(curFeatureId);
+        }
+
+        /*
+         * Implementation note: call openPanelsAfterRestore later to actually open the
+         * restored panels.
+         */
+    }
+
+    /**
+     * Opens the panels that have had their state restored. This should be
+     * called sometime after {@link #restorePanelState} when it is safe to add
+     * to the window manager.
+     */
+    void openPanelsAfterRestore() {
+        PanelFeatureState[] panels = mPanels;
+
+        if (panels == null) {
+            return;
+        }
+
+        PanelFeatureState st;
+        for (int i = panels.length - 1; i >= 0; i--) {
+            st = panels[i];
+            // We restore the panel if it was last open; we skip it if it
+            // now is open, to avoid a race condition if the user immediately
+            // opens it when we are resuming.
+            if (st != null) {
+                st.applyFrozenState();
+                if (!st.isOpen && st.wasLastOpen) {
+                    st.isInExpandedMode = st.wasLastExpanded;
+                    openPanel(st, null);
+                }
+            }
+        }
+    }
+
+    private class PanelMenuPresenterCallback implements MenuPresenter.Callback {
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+            final Menu parentMenu = menu.getRootMenu();
+            final boolean isSubMenu = parentMenu != menu;
+            final PanelFeatureState panel = findMenuPanel(isSubMenu ? parentMenu : menu);
+            if (panel != null) {
+                if (isSubMenu) {
+                    callOnPanelClosed(panel.featureId, panel, parentMenu);
+                    closePanel(panel, true);
+                } else {
+                    // Close the panel and only do the callback if the menu is being
+                    // closed completely, not if opening a sub menu
+                    closePanel(panel, allMenusAreClosing);
+                }
+            }
+        }
+
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            if (subMenu == null && hasFeature(FEATURE_ACTION_BAR)) {
+                Callback cb = getCallback();
+                if (cb != null && !isDestroyed()) {
+                    cb.onMenuOpened(FEATURE_ACTION_BAR, subMenu);
+                }
+            }
+
+            return true;
+        }
+    }
+
+    private final class ActionMenuPresenterCallback implements MenuPresenter.Callback {
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            Callback cb = getCallback();
+            if (cb != null) {
+                cb.onMenuOpened(FEATURE_ACTION_BAR, subMenu);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+            checkCloseActionMenu(menu);
+        }
+    }
+
+    protected DecorView generateDecor(int featureId) {
+        // System process doesn't have application context and in that case we need to directly use
+        // the context we have. Otherwise we want the application context, so we don't cling to the
+        // activity.
+        Context context;
+        if (mUseDecorContext) {
+            Context applicationContext = getContext().getApplicationContext();
+            if (applicationContext == null) {
+                context = getContext();
+            } else {
+                context = new DecorContext(applicationContext, getContext().getResources());
+                if (mTheme != -1) {
+                    context.setTheme(mTheme);
+                }
+            }
+        } else {
+            context = getContext();
+        }
+        return new DecorView(context, featureId, this, getAttributes());
+    }
+
+    protected ViewGroup generateLayout(DecorView decor) {
+        // Apply data from current theme.
+
+        TypedArray a = getWindowStyle();
+
+        if (false) {
+            System.out.println("From style:");
+            String s = "Attrs:";
+            for (int i = 0; i < R.styleable.Window.length; i++) {
+                s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
+                        + a.getString(i);
+            }
+            System.out.println(s);
+        }
+
+        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
+        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
+                & (~getForcedWindowFlags());
+        if (mIsFloating) {
+            setLayout(WRAP_CONTENT, WRAP_CONTENT);
+            setFlags(0, flagsToUpdate);
+        } else {
+            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
+            requestFeature(FEATURE_NO_TITLE);
+        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
+            // Don't allow an action bar if there is no title.
+            requestFeature(FEATURE_ACTION_BAR);
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
+            requestFeature(FEATURE_ACTION_BAR_OVERLAY);
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
+            requestFeature(FEATURE_ACTION_MODE_OVERLAY);
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
+            requestFeature(FEATURE_SWIPE_TO_DISMISS);
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
+            setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
+                false)) {
+            setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
+                    & (~getForcedWindowFlags()));
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation,
+                false)) {
+            setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION
+                    & (~getForcedWindowFlags()));
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowOverscan, false)) {
+            setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN&(~getForcedWindowFlags()));
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
+            setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags()));
+        }
+
+        if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch,
+                getContext().getApplicationInfo().targetSdkVersion
+                        >= android.os.Build.VERSION_CODES.HONEYCOMB)) {
+            setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH&(~getForcedWindowFlags()));
+        }
+
+        a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
+        a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
+        if (DEBUG) Log.d(TAG, "Min width minor: " + mMinWidthMinor.coerceToString()
+                + ", major: " + mMinWidthMajor.coerceToString());
+        if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) {
+            if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
+            a.getValue(R.styleable.Window_windowFixedWidthMajor,
+                    mFixedWidthMajor);
+        }
+        if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {
+            if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
+            a.getValue(R.styleable.Window_windowFixedWidthMinor,
+                    mFixedWidthMinor);
+        }
+        if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) {
+            if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
+            a.getValue(R.styleable.Window_windowFixedHeightMajor,
+                    mFixedHeightMajor);
+        }
+        if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) {
+            if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
+            a.getValue(R.styleable.Window_windowFixedHeightMinor,
+                    mFixedHeightMinor);
+        }
+        if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
+            requestFeature(FEATURE_CONTENT_TRANSITIONS);
+        }
+        if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
+            requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
+        }
+
+        mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
+
+        final Context context = getContext();
+        final int targetSdk = context.getApplicationInfo().targetSdkVersion;
+        final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
+        final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+        final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
+        final boolean targetHcNeedsOptions = context.getResources().getBoolean(
+                R.bool.target_honeycomb_needs_options_menu);
+        final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);
+
+        if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
+            setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);
+        } else {
+            setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);
+        }
+
+        if (!mForcedStatusBarColor) {
+            mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
+        }
+        if (!mForcedNavigationBarColor) {
+            mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
+            mNavigationBarDividerColor = a.getColor(R.styleable.Window_navigationBarDividerColor,
+                    0x00000000);
+        }
+
+        WindowManager.LayoutParams params = getAttributes();
+
+        // Non-floating windows on high end devices must put up decor beneath the system bars and
+        // therefore must know about visibility changes of those.
+        if (!mIsFloating) {
+            if (!targetPreL && a.getBoolean(
+                    R.styleable.Window_windowDrawsSystemBarBackgrounds,
+                    false)) {
+                setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+                        FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags());
+            }
+            if (mDecor.mForceWindowDrawsStatusBarBackground) {
+                params.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
+            }
+        }
+        if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
+            decor.setSystemUiVisibility(
+                    decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+        }
+        if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) {
+            decor.setSystemUiVisibility(
+                    decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+        }
+
+        if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
+                >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+            if (a.getBoolean(
+                    R.styleable.Window_windowCloseOnTouchOutside,
+                    false)) {
+                setCloseOnTouchOutsideIfNotSet(true);
+            }
+        }
+
+        if (!hasSoftInputMode()) {
+            params.softInputMode = a.getInt(
+                    R.styleable.Window_windowSoftInputMode,
+                    params.softInputMode);
+        }
+
+        if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
+                mIsFloating)) {
+            /* All dialogs should have the window dimmed */
+            if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
+                params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
+            }
+            if (!haveDimAmount()) {
+                params.dimAmount = a.getFloat(
+                        android.R.styleable.Window_backgroundDimAmount, 0.5f);
+            }
+        }
+
+        if (params.windowAnimations == 0) {
+            params.windowAnimations = a.getResourceId(
+                    R.styleable.Window_windowAnimationStyle, 0);
+        }
+
+        // The rest are only done if this window is not embedded; otherwise,
+        // the values are inherited from our container.
+        if (getContainer() == null) {
+            if (mBackgroundDrawable == null) {
+                if (mBackgroundResource == 0) {
+                    mBackgroundResource = a.getResourceId(
+                            R.styleable.Window_windowBackground, 0);
+                }
+                if (mFrameResource == 0) {
+                    mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
+                }
+                mBackgroundFallbackResource = a.getResourceId(
+                        R.styleable.Window_windowBackgroundFallback, 0);
+                if (false) {
+                    System.out.println("Background: "
+                            + Integer.toHexString(mBackgroundResource) + " Frame: "
+                            + Integer.toHexString(mFrameResource));
+                }
+            }
+            if (mLoadElevation) {
+                mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
+            }
+            mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
+            mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
+        }
+
+        // Inflate the window decor.
+
+        int layoutResource;
+        int features = getLocalFeatures();
+        // System.out.println("Features: 0x" + Integer.toHexString(features));
+        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
+            layoutResource = R.layout.screen_swipe_dismiss;
+            setCloseOnSwipeEnabled(true);
+        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
+            if (mIsFloating) {
+                TypedValue res = new TypedValue();
+                getContext().getTheme().resolveAttribute(
+                        R.attr.dialogTitleIconsDecorLayout, res, true);
+                layoutResource = res.resourceId;
+            } else {
+                layoutResource = R.layout.screen_title_icons;
+            }
+            // XXX Remove this once action bar supports these features.
+            removeFeature(FEATURE_ACTION_BAR);
+            // System.out.println("Title Icons!");
+        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
+                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
+            // Special case for a window with only a progress bar (and title).
+            // XXX Need to have a no-title version of embedded windows.
+            layoutResource = R.layout.screen_progress;
+            // System.out.println("Progress!");
+        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
+            // Special case for a window with a custom title.
+            // If the window is floating, we need a dialog layout
+            if (mIsFloating) {
+                TypedValue res = new TypedValue();
+                getContext().getTheme().resolveAttribute(
+                        R.attr.dialogCustomTitleDecorLayout, res, true);
+                layoutResource = res.resourceId;
+            } else {
+                layoutResource = R.layout.screen_custom_title;
+            }
+            // XXX Remove this once action bar supports these features.
+            removeFeature(FEATURE_ACTION_BAR);
+        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
+            // If no other features and not embedded, only need a title.
+            // If the window is floating, we need a dialog layout
+            if (mIsFloating) {
+                TypedValue res = new TypedValue();
+                getContext().getTheme().resolveAttribute(
+                        R.attr.dialogTitleDecorLayout, res, true);
+                layoutResource = res.resourceId;
+            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
+                layoutResource = a.getResourceId(
+                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
+                        R.layout.screen_action_bar);
+            } else {
+                layoutResource = R.layout.screen_title;
+            }
+            // System.out.println("Title!");
+        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
+            layoutResource = R.layout.screen_simple_overlay_action_mode;
+        } else {
+            // Embedded, so no decoration is needed.
+            layoutResource = R.layout.screen_simple;
+            // System.out.println("Simple!");
+        }
+
+        mDecor.startChanging();
+        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
+
+        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
+        if (contentParent == null) {
+            throw new RuntimeException("Window couldn't find content container view");
+        }
+
+        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
+            ProgressBar progress = getCircularProgressBar(false);
+            if (progress != null) {
+                progress.setIndeterminate(true);
+            }
+        }
+
+        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
+            registerSwipeCallbacks(contentParent);
+        }
+
+        // Remaining setup -- of background and title -- that only applies
+        // to top-level windows.
+        if (getContainer() == null) {
+            final Drawable background;
+            if (mBackgroundResource != 0) {
+                background = getContext().getDrawable(mBackgroundResource);
+            } else {
+                background = mBackgroundDrawable;
+            }
+            mDecor.setWindowBackground(background);
+
+            final Drawable frame;
+            if (mFrameResource != 0) {
+                frame = getContext().getDrawable(mFrameResource);
+            } else {
+                frame = null;
+            }
+            mDecor.setWindowFrame(frame);
+
+            mDecor.setElevation(mElevation);
+            mDecor.setClipToOutline(mClipToOutline);
+
+            if (mTitle != null) {
+                setTitle(mTitle);
+            }
+
+            if (mTitleColor == 0) {
+                mTitleColor = mTextColor;
+            }
+            setTitleColor(mTitleColor);
+        }
+
+        mDecor.finishChanging();
+
+        return contentParent;
+    }
+
+    /** @hide */
+    public void alwaysReadCloseOnTouchAttr() {
+        mAlwaysReadCloseOnTouchAttr = true;
+    }
+
+    private void installDecor() {
+        mForceDecorInstall = false;
+        if (mDecor == null) {
+            mDecor = generateDecor(-1);
+            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+            mDecor.setIsRootNamespace(true);
+            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
+                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
+            }
+        } else {
+            mDecor.setWindow(this);
+        }
+        if (mContentParent == null) {
+            mContentParent = generateLayout(mDecor);
+
+            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
+            mDecor.makeOptionalFitsSystemWindows();
+
+            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
+                    R.id.decor_content_parent);
+
+            if (decorContentParent != null) {
+                mDecorContentParent = decorContentParent;
+                mDecorContentParent.setWindowCallback(getCallback());
+                if (mDecorContentParent.getTitle() == null) {
+                    mDecorContentParent.setWindowTitle(mTitle);
+                }
+
+                final int localFeatures = getLocalFeatures();
+                for (int i = 0; i < FEATURE_MAX; i++) {
+                    if ((localFeatures & (1 << i)) != 0) {
+                        mDecorContentParent.initFeature(i);
+                    }
+                }
+
+                mDecorContentParent.setUiOptions(mUiOptions);
+
+                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
+                        (mIconRes != 0 && !mDecorContentParent.hasIcon())) {
+                    mDecorContentParent.setIcon(mIconRes);
+                } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
+                        mIconRes == 0 && !mDecorContentParent.hasIcon()) {
+                    mDecorContentParent.setIcon(
+                            getContext().getPackageManager().getDefaultActivityIcon());
+                    mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
+                }
+                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
+                        (mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
+                    mDecorContentParent.setLogo(mLogoRes);
+                }
+
+                // Invalidate if the panel menu hasn't been created before this.
+                // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
+                // being called in the middle of onCreate or similar.
+                // A pending invalidation will typically be resolved before the posted message
+                // would run normally in order to satisfy instance state restoration.
+                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+                if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
+                    invalidatePanelMenu(FEATURE_ACTION_BAR);
+                }
+            } else {
+                mTitleView = findViewById(R.id.title);
+                if (mTitleView != null) {
+                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
+                        final View titleContainer = findViewById(R.id.title_container);
+                        if (titleContainer != null) {
+                            titleContainer.setVisibility(View.GONE);
+                        } else {
+                            mTitleView.setVisibility(View.GONE);
+                        }
+                        mContentParent.setForeground(null);
+                    } else {
+                        mTitleView.setText(mTitle);
+                    }
+                }
+            }
+
+            if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
+                mDecor.setBackgroundFallback(mBackgroundFallbackResource);
+            }
+
+            // Only inflate or create a new TransitionManager if the caller hasn't
+            // already set a custom one.
+            if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
+                if (mTransitionManager == null) {
+                    final int transitionRes = getWindowStyle().getResourceId(
+                            R.styleable.Window_windowContentTransitionManager,
+                            0);
+                    if (transitionRes != 0) {
+                        final TransitionInflater inflater = TransitionInflater.from(getContext());
+                        mTransitionManager = inflater.inflateTransitionManager(transitionRes,
+                                mContentParent);
+                    } else {
+                        mTransitionManager = new TransitionManager();
+                    }
+                }
+
+                mEnterTransition = getTransition(mEnterTransition, null,
+                        R.styleable.Window_windowEnterTransition);
+                mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION,
+                        R.styleable.Window_windowReturnTransition);
+                mExitTransition = getTransition(mExitTransition, null,
+                        R.styleable.Window_windowExitTransition);
+                mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION,
+                        R.styleable.Window_windowReenterTransition);
+                mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null,
+                        R.styleable.Window_windowSharedElementEnterTransition);
+                mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition,
+                        USE_DEFAULT_TRANSITION,
+                        R.styleable.Window_windowSharedElementReturnTransition);
+                mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null,
+                        R.styleable.Window_windowSharedElementExitTransition);
+                mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition,
+                        USE_DEFAULT_TRANSITION,
+                        R.styleable.Window_windowSharedElementReenterTransition);
+                if (mAllowEnterTransitionOverlap == null) {
+                    mAllowEnterTransitionOverlap = getWindowStyle().getBoolean(
+                            R.styleable.Window_windowAllowEnterTransitionOverlap, true);
+                }
+                if (mAllowReturnTransitionOverlap == null) {
+                    mAllowReturnTransitionOverlap = getWindowStyle().getBoolean(
+                            R.styleable.Window_windowAllowReturnTransitionOverlap, true);
+                }
+                if (mBackgroundFadeDurationMillis < 0) {
+                    mBackgroundFadeDurationMillis = getWindowStyle().getInteger(
+                            R.styleable.Window_windowTransitionBackgroundFadeDuration,
+                            DEFAULT_BACKGROUND_FADE_DURATION_MS);
+                }
+                if (mSharedElementsUseOverlay == null) {
+                    mSharedElementsUseOverlay = getWindowStyle().getBoolean(
+                            R.styleable.Window_windowSharedElementsUseOverlay, true);
+                }
+            }
+        }
+    }
+
+    private Transition getTransition(Transition currentValue, Transition defaultValue, int id) {
+        if (currentValue != defaultValue) {
+            return currentValue;
+        }
+        int transitionId = getWindowStyle().getResourceId(id, -1);
+        Transition transition = defaultValue;
+        if (transitionId != -1 && transitionId != R.transition.no_transition) {
+            TransitionInflater inflater = TransitionInflater.from(getContext());
+            transition = inflater.inflateTransition(transitionId);
+            if (transition instanceof TransitionSet &&
+                    ((TransitionSet)transition).getTransitionCount() == 0) {
+                transition = null;
+            }
+        }
+        return transition;
+    }
+
+    private Drawable loadImageURI(Uri uri) {
+        try {
+            return Drawable.createFromStream(
+                    getContext().getContentResolver().openInputStream(uri), null);
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to open content: " + uri);
+        }
+        return null;
+    }
+
+    private DrawableFeatureState getDrawableState(int featureId, boolean required) {
+        if ((getFeatures() & (1 << featureId)) == 0) {
+            if (!required) {
+                return null;
+            }
+            throw new RuntimeException("The feature has not been requested");
+        }
+
+        DrawableFeatureState[] ar;
+        if ((ar = mDrawables) == null || ar.length <= featureId) {
+            DrawableFeatureState[] nar = new DrawableFeatureState[featureId + 1];
+            if (ar != null) {
+                System.arraycopy(ar, 0, nar, 0, ar.length);
+            }
+            mDrawables = ar = nar;
+        }
+
+        DrawableFeatureState st = ar[featureId];
+        if (st == null) {
+            ar[featureId] = st = new DrawableFeatureState(featureId);
+        }
+        return st;
+    }
+
+    /**
+     * Gets a panel's state based on its feature ID.
+     *
+     * @param featureId The feature ID of the panel.
+     * @param required Whether the panel is required (if it is required and it
+     *            isn't in our features, this throws an exception).
+     * @return The panel state.
+     */
+    PanelFeatureState getPanelState(int featureId, boolean required) {
+        return getPanelState(featureId, required, null);
+    }
+
+    /**
+     * Gets a panel's state based on its feature ID.
+     *
+     * @param featureId The feature ID of the panel.
+     * @param required Whether the panel is required (if it is required and it
+     *            isn't in our features, this throws an exception).
+     * @param convertPanelState Optional: If the panel state does not exist, use
+     *            this as the panel state.
+     * @return The panel state.
+     */
+    private PanelFeatureState getPanelState(int featureId, boolean required,
+            PanelFeatureState convertPanelState) {
+        if ((getFeatures() & (1 << featureId)) == 0) {
+            if (!required) {
+                return null;
+            }
+            throw new RuntimeException("The feature has not been requested");
+        }
+
+        PanelFeatureState[] ar;
+        if ((ar = mPanels) == null || ar.length <= featureId) {
+            PanelFeatureState[] nar = new PanelFeatureState[featureId + 1];
+            if (ar != null) {
+                System.arraycopy(ar, 0, nar, 0, ar.length);
+            }
+            mPanels = ar = nar;
+        }
+
+        PanelFeatureState st = ar[featureId];
+        if (st == null) {
+            ar[featureId] = st = (convertPanelState != null)
+                    ? convertPanelState
+                    : new PanelFeatureState(featureId);
+        }
+        return st;
+    }
+
+    @Override
+    public final void setChildDrawable(int featureId, Drawable drawable) {
+        DrawableFeatureState st = getDrawableState(featureId, true);
+        st.child = drawable;
+        updateDrawable(featureId, st, false);
+    }
+
+    @Override
+    public final void setChildInt(int featureId, int value) {
+        updateInt(featureId, value, false);
+    }
+
+    @Override
+    public boolean isShortcutKey(int keyCode, KeyEvent event) {
+        PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+        return st != null && st.menu != null && st.menu.isShortcutKey(keyCode, event);
+    }
+
+    private void updateDrawable(int featureId, DrawableFeatureState st, boolean fromResume) {
+        // Do nothing if the decor is not yet installed... an update will
+        // need to be forced when we eventually become active.
+        if (mContentParent == null) {
+            return;
+        }
+
+        final int featureMask = 1 << featureId;
+
+        if ((getFeatures() & featureMask) == 0 && !fromResume) {
+            return;
+        }
+
+        Drawable drawable = null;
+        if (st != null) {
+            drawable = st.child;
+            if (drawable == null)
+                drawable = st.local;
+            if (drawable == null)
+                drawable = st.def;
+        }
+        if ((getLocalFeatures() & featureMask) == 0) {
+            if (getContainer() != null) {
+                if (isActive() || fromResume) {
+                    getContainer().setChildDrawable(featureId, drawable);
+                }
+            }
+        } else if (st != null && (st.cur != drawable || st.curAlpha != st.alpha)) {
+            // System.out.println("Drawable changed: old=" + st.cur
+            // + ", new=" + drawable);
+            st.cur = drawable;
+            st.curAlpha = st.alpha;
+            onDrawableChanged(featureId, drawable, st.alpha);
+        }
+    }
+
+    private void updateInt(int featureId, int value, boolean fromResume) {
+
+        // Do nothing if the decor is not yet installed... an update will
+        // need to be forced when we eventually become active.
+        if (mContentParent == null) {
+            return;
+        }
+
+        final int featureMask = 1 << featureId;
+
+        if ((getFeatures() & featureMask) == 0 && !fromResume) {
+            return;
+        }
+
+        if ((getLocalFeatures() & featureMask) == 0) {
+            if (getContainer() != null) {
+                getContainer().setChildInt(featureId, value);
+            }
+        } else {
+            onIntChanged(featureId, value);
+        }
+    }
+
+    private ImageView getLeftIconView() {
+        if (mLeftIconView != null) {
+            return mLeftIconView;
+        }
+        if (mContentParent == null) {
+            installDecor();
+        }
+        return (mLeftIconView = (ImageView)findViewById(R.id.left_icon));
+    }
+
+    @Override
+    protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) {
+        super.dispatchWindowAttributesChanged(attrs);
+        if (mDecor != null) {
+            mDecor.updateColorViews(null /* insets */, true /* animate */);
+        }
+    }
+
+    private ProgressBar getCircularProgressBar(boolean shouldInstallDecor) {
+        if (mCircularProgressBar != null) {
+            return mCircularProgressBar;
+        }
+        if (mContentParent == null && shouldInstallDecor) {
+            installDecor();
+        }
+        mCircularProgressBar = findViewById(R.id.progress_circular);
+        if (mCircularProgressBar != null) {
+            mCircularProgressBar.setVisibility(View.INVISIBLE);
+        }
+        return mCircularProgressBar;
+    }
+
+    private ProgressBar getHorizontalProgressBar(boolean shouldInstallDecor) {
+        if (mHorizontalProgressBar != null) {
+            return mHorizontalProgressBar;
+        }
+        if (mContentParent == null && shouldInstallDecor) {
+            installDecor();
+        }
+        mHorizontalProgressBar = findViewById(R.id.progress_horizontal);
+        if (mHorizontalProgressBar != null) {
+            mHorizontalProgressBar.setVisibility(View.INVISIBLE);
+        }
+        return mHorizontalProgressBar;
+    }
+
+    private ImageView getRightIconView() {
+        if (mRightIconView != null) {
+            return mRightIconView;
+        }
+        if (mContentParent == null) {
+            installDecor();
+        }
+        return (mRightIconView = (ImageView)findViewById(R.id.right_icon));
+    }
+
+    private void registerSwipeCallbacks(ViewGroup contentParent) {
+        if (!(contentParent instanceof SwipeDismissLayout)) {
+            Log.w(TAG, "contentParent is not a SwipeDismissLayout: " + contentParent);
+            return;
+        }
+        SwipeDismissLayout swipeDismiss = (SwipeDismissLayout) contentParent;
+        swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() {
+            @Override
+            public void onDismissed(SwipeDismissLayout layout) {
+                dispatchOnWindowSwipeDismissed();
+                dispatchOnWindowDismissed(false /*finishTask*/, true /*suppressWindowTransition*/);
+            }
+        });
+        swipeDismiss.setOnSwipeProgressChangedListener(
+                new SwipeDismissLayout.OnSwipeProgressChangedListener() {
+                    @Override
+                    public void onSwipeProgressChanged(
+                            SwipeDismissLayout layout, float alpha, float translate) {
+                        WindowManager.LayoutParams newParams = getAttributes();
+                        newParams.x = (int) translate;
+                        newParams.alpha = alpha;
+                        setAttributes(newParams);
+
+                        int flags = 0;
+                        if (newParams.x == 0) {
+                            flags = FLAG_FULLSCREEN;
+                        } else {
+                            flags = FLAG_LAYOUT_NO_LIMITS;
+                        }
+                        setFlags(flags, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
+                    }
+
+                    @Override
+                    public void onSwipeCancelled(SwipeDismissLayout layout) {
+                        WindowManager.LayoutParams newParams = getAttributes();
+                        // Swipe changes only affect the x-translation and alpha, check to see if
+                        // those values have changed first before resetting them.
+                        if (newParams.x != 0 || newParams.alpha != 1) {
+                            newParams.x = 0;
+                            newParams.alpha = 1;
+                            setAttributes(newParams);
+                            setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
+                        }
+                    }
+                });
+    }
+
+    /** @hide */
+    @Override
+    public void setCloseOnSwipeEnabled(boolean closeOnSwipeEnabled) {
+        if (hasFeature(Window.FEATURE_SWIPE_TO_DISMISS) // swipe-to-dismiss feature is requested
+                && mContentParent instanceof SwipeDismissLayout) { // check casting mContentParent
+            ((SwipeDismissLayout) mContentParent).setDismissable(closeOnSwipeEnabled);
+        }
+        super.setCloseOnSwipeEnabled(closeOnSwipeEnabled);
+    }
+
+    /**
+     * Helper method for calling the {@link Callback#onPanelClosed(int, Menu)}
+     * callback. This method will grab whatever extra state is needed for the
+     * callback that isn't given in the parameters. If the panel is not open,
+     * this will not perform the callback.
+     *
+     * @param featureId Feature ID of the panel that was closed. Must be given.
+     * @param panel Panel that was closed. Optional but useful if there is no
+     *            menu given.
+     * @param menu The menu that was closed. Optional, but give if you have.
+     */
+    private void callOnPanelClosed(int featureId, PanelFeatureState panel, Menu menu) {
+        final Callback cb = getCallback();
+        if (cb == null)
+            return;
+
+        // Try to get a menu
+        if (menu == null) {
+            // Need a panel to grab the menu, so try to get that
+            if (panel == null) {
+                if ((featureId >= 0) && (featureId < mPanels.length)) {
+                    panel = mPanels[featureId];
+                }
+            }
+
+            if (panel != null) {
+                // menu still may be null, which is okay--we tried our best
+                menu = panel.menu;
+            }
+        }
+
+        // If the panel is not open, do not callback
+        if ((panel != null) && (!panel.isOpen))
+            return;
+
+        if (!isDestroyed()) {
+            cb.onPanelClosed(featureId, menu);
+        }
+    }
+
+    /**
+     * Check if Setup or Post-Setup update is completed on TV
+     * @return true if completed
+     */
+    private boolean isTvUserSetupComplete() {
+        boolean isTvSetupComplete = Settings.Secure.getInt(getContext().getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE, 0) != 0;
+        isTvSetupComplete &= Settings.Secure.getInt(getContext().getContentResolver(),
+                Settings.Secure.TV_USER_SETUP_COMPLETE, 0) != 0;
+        return isTvSetupComplete;
+    }
+
+    /**
+     * Helper method for adding launch-search to most applications. Opens the
+     * search window using default settings.
+     *
+     * @return true if search window opened
+     */
+    private boolean launchDefaultSearch(KeyEvent event) {
+        if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+                && !isTvUserSetupComplete()) {
+            // If we are in Setup or Post-Setup update mode on TV, consume the search key
+            return false;
+        }
+        boolean result;
+        final Callback cb = getCallback();
+        if (cb == null || isDestroyed()) {
+            result = false;
+        } else {
+            sendCloseSystemWindows("search");
+            int deviceId = event.getDeviceId();
+            SearchEvent searchEvent = null;
+            if (deviceId != 0) {
+                searchEvent = new SearchEvent(InputDevice.getDevice(deviceId));
+            }
+            try {
+                result = cb.onSearchRequested(searchEvent);
+            } catch (AbstractMethodError e) {
+                Log.e(TAG, "WindowCallback " + cb.getClass().getName() + " does not implement"
+                        + " method onSearchRequested(SearchEvent); fa", e);
+                result = cb.onSearchRequested();
+            }
+        }
+        if (!result && (getContext().getResources().getConfiguration().uiMode
+                & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) {
+            // On TVs, if the app doesn't implement search, we want to launch assist.
+            Bundle args = new Bundle();
+            args.putInt(Intent.EXTRA_ASSIST_INPUT_DEVICE_ID, event.getDeviceId());
+            return ((SearchManager)getContext().getSystemService(Context.SEARCH_SERVICE))
+                    .launchLegacyAssist(null, UserHandle.myUserId(), args);
+        }
+        return result;
+    }
+
+    @Override
+    public void setVolumeControlStream(int streamType) {
+        mVolumeControlStreamType = streamType;
+    }
+
+    @Override
+    public int getVolumeControlStream() {
+        return mVolumeControlStreamType;
+    }
+
+    @Override
+    public void setMediaController(MediaController controller) {
+        mMediaController = controller;
+    }
+
+    @Override
+    public MediaController getMediaController() {
+        return mMediaController;
+    }
+
+    @Override
+    public void setEnterTransition(Transition enterTransition) {
+        mEnterTransition = enterTransition;
+    }
+
+    @Override
+    public void setReturnTransition(Transition transition) {
+        mReturnTransition = transition;
+    }
+
+    @Override
+    public void setExitTransition(Transition exitTransition) {
+        mExitTransition = exitTransition;
+    }
+
+    @Override
+    public void setReenterTransition(Transition transition) {
+        mReenterTransition = transition;
+    }
+
+    @Override
+    public void setSharedElementEnterTransition(Transition sharedElementEnterTransition) {
+        mSharedElementEnterTransition = sharedElementEnterTransition;
+    }
+
+    @Override
+    public void setSharedElementReturnTransition(Transition transition) {
+        mSharedElementReturnTransition = transition;
+    }
+
+    @Override
+    public void setSharedElementExitTransition(Transition sharedElementExitTransition) {
+        mSharedElementExitTransition = sharedElementExitTransition;
+    }
+
+    @Override
+    public void setSharedElementReenterTransition(Transition transition) {
+        mSharedElementReenterTransition = transition;
+    }
+
+    @Override
+    public Transition getEnterTransition() {
+        return mEnterTransition;
+    }
+
+    @Override
+    public Transition getReturnTransition() {
+        return mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition()
+                : mReturnTransition;
+    }
+
+    @Override
+    public Transition getExitTransition() {
+        return mExitTransition;
+    }
+
+    @Override
+    public Transition getReenterTransition() {
+        return mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition()
+                : mReenterTransition;
+    }
+
+    @Override
+    public Transition getSharedElementEnterTransition() {
+        return mSharedElementEnterTransition;
+    }
+
+    @Override
+    public Transition getSharedElementReturnTransition() {
+        return mSharedElementReturnTransition == USE_DEFAULT_TRANSITION
+                ? getSharedElementEnterTransition() : mSharedElementReturnTransition;
+    }
+
+    @Override
+    public Transition getSharedElementExitTransition() {
+        return mSharedElementExitTransition;
+    }
+
+    @Override
+    public Transition getSharedElementReenterTransition() {
+        return mSharedElementReenterTransition == USE_DEFAULT_TRANSITION
+                ? getSharedElementExitTransition() : mSharedElementReenterTransition;
+    }
+
+    @Override
+    public void setAllowEnterTransitionOverlap(boolean allow) {
+        mAllowEnterTransitionOverlap = allow;
+    }
+
+    @Override
+    public boolean getAllowEnterTransitionOverlap() {
+        return (mAllowEnterTransitionOverlap == null) ? true : mAllowEnterTransitionOverlap;
+    }
+
+    @Override
+    public void setAllowReturnTransitionOverlap(boolean allowExitTransitionOverlap) {
+        mAllowReturnTransitionOverlap = allowExitTransitionOverlap;
+    }
+
+    @Override
+    public boolean getAllowReturnTransitionOverlap() {
+        return (mAllowReturnTransitionOverlap == null) ? true : mAllowReturnTransitionOverlap;
+    }
+
+    @Override
+    public long getTransitionBackgroundFadeDuration() {
+        return (mBackgroundFadeDurationMillis < 0) ? DEFAULT_BACKGROUND_FADE_DURATION_MS
+                : mBackgroundFadeDurationMillis;
+    }
+
+    @Override
+    public void setTransitionBackgroundFadeDuration(long fadeDurationMillis) {
+        if (fadeDurationMillis < 0) {
+            throw new IllegalArgumentException("negative durations are not allowed");
+        }
+        mBackgroundFadeDurationMillis = fadeDurationMillis;
+    }
+
+    @Override
+    public void setSharedElementsUseOverlay(boolean sharedElementsUseOverlay) {
+        mSharedElementsUseOverlay = sharedElementsUseOverlay;
+    }
+
+    @Override
+    public boolean getSharedElementsUseOverlay() {
+        return (mSharedElementsUseOverlay == null) ? true : mSharedElementsUseOverlay;
+    }
+
+    private static final class DrawableFeatureState {
+        DrawableFeatureState(int _featureId) {
+            featureId = _featureId;
+        }
+
+        final int featureId;
+
+        int resid;
+
+        Uri uri;
+
+        Drawable local;
+
+        Drawable child;
+
+        Drawable def;
+
+        Drawable cur;
+
+        int alpha = 255;
+
+        int curAlpha = 255;
+    }
+
+    static final class PanelFeatureState {
+
+        /** Feature ID for this panel. */
+        int featureId;
+
+        // Information pulled from the style for this panel.
+
+        int background;
+
+        /** The background when the panel spans the entire available width. */
+        int fullBackground;
+
+        int gravity;
+
+        int x;
+
+        int y;
+
+        int windowAnimations;
+
+        /** Dynamic state of the panel. */
+        DecorView decorView;
+
+        /** The panel that was returned by onCreatePanelView(). */
+        View createdPanelView;
+
+        /** The panel that we are actually showing. */
+        View shownPanelView;
+
+        /** Use {@link #setMenu} to set this. */
+        MenuBuilder menu;
+
+        IconMenuPresenter iconMenuPresenter;
+        ListMenuPresenter listMenuPresenter;
+
+        /** true if this menu will show in single-list compact mode */
+        boolean isCompact;
+
+        /** Theme resource ID for list elements of the panel menu */
+        int listPresenterTheme;
+
+        /**
+         * Whether the panel has been prepared (see
+         * {@link PhoneWindow#preparePanel}).
+         */
+        boolean isPrepared;
+
+        /**
+         * Whether an item's action has been performed. This happens in obvious
+         * scenarios (user clicks on menu item), but can also happen with
+         * chording menu+(shortcut key).
+         */
+        boolean isHandled;
+
+        boolean isOpen;
+
+        /**
+         * True if the menu is in expanded mode, false if the menu is in icon
+         * mode
+         */
+        boolean isInExpandedMode;
+
+        public boolean qwertyMode;
+
+        boolean refreshDecorView;
+
+        boolean refreshMenuContent;
+
+        boolean wasLastOpen;
+
+        boolean wasLastExpanded;
+
+        /**
+         * Contains the state of the menu when told to freeze.
+         */
+        Bundle frozenMenuState;
+
+        /**
+         * Contains the state of associated action views when told to freeze.
+         * These are saved across invalidations.
+         */
+        Bundle frozenActionViewState;
+
+        PanelFeatureState(int featureId) {
+            this.featureId = featureId;
+
+            refreshDecorView = false;
+        }
+
+        public boolean isInListMode() {
+            return isInExpandedMode || isCompact;
+        }
+
+        public boolean hasPanelItems() {
+            if (shownPanelView == null) return false;
+            if (createdPanelView != null) return true;
+
+            if (isCompact || isInExpandedMode) {
+                return listMenuPresenter.getAdapter().getCount() > 0;
+            } else {
+                return ((ViewGroup) shownPanelView).getChildCount() > 0;
+            }
+        }
+
+        /**
+         * Unregister and free attached MenuPresenters. They will be recreated as needed.
+         */
+        public void clearMenuPresenters() {
+            if (menu != null) {
+                menu.removeMenuPresenter(iconMenuPresenter);
+                menu.removeMenuPresenter(listMenuPresenter);
+            }
+            iconMenuPresenter = null;
+            listMenuPresenter = null;
+        }
+
+        void setStyle(Context context) {
+            TypedArray a = context.obtainStyledAttributes(R.styleable.Theme);
+            background = a.getResourceId(
+                    R.styleable.Theme_panelBackground, 0);
+            fullBackground = a.getResourceId(
+                    R.styleable.Theme_panelFullBackground, 0);
+            windowAnimations = a.getResourceId(
+                    R.styleable.Theme_windowAnimationStyle, 0);
+            isCompact = a.getBoolean(
+                    R.styleable.Theme_panelMenuIsCompact, false);
+            listPresenterTheme = a.getResourceId(
+                    R.styleable.Theme_panelMenuListTheme,
+                    R.style.Theme_ExpandedMenu);
+            a.recycle();
+        }
+
+        void setMenu(MenuBuilder menu) {
+            if (menu == this.menu) return;
+
+            if (this.menu != null) {
+                this.menu.removeMenuPresenter(iconMenuPresenter);
+                this.menu.removeMenuPresenter(listMenuPresenter);
+            }
+            this.menu = menu;
+            if (menu != null) {
+                if (iconMenuPresenter != null) menu.addMenuPresenter(iconMenuPresenter);
+                if (listMenuPresenter != null) menu.addMenuPresenter(listMenuPresenter);
+            }
+        }
+
+        MenuView getListMenuView(Context context, MenuPresenter.Callback cb) {
+            if (menu == null) return null;
+
+            if (!isCompact) {
+                getIconMenuView(context, cb); // Need this initialized to know where our offset goes
+            }
+
+            if (listMenuPresenter == null) {
+                listMenuPresenter = new ListMenuPresenter(
+                        R.layout.list_menu_item_layout, listPresenterTheme);
+                listMenuPresenter.setCallback(cb);
+                listMenuPresenter.setId(R.id.list_menu_presenter);
+                menu.addMenuPresenter(listMenuPresenter);
+            }
+
+            if (iconMenuPresenter != null) {
+                listMenuPresenter.setItemIndexOffset(
+                        iconMenuPresenter.getNumActualItemsShown());
+            }
+            MenuView result = listMenuPresenter.getMenuView(decorView);
+
+            return result;
+        }
+
+        MenuView getIconMenuView(Context context, MenuPresenter.Callback cb) {
+            if (menu == null) return null;
+
+            if (iconMenuPresenter == null) {
+                iconMenuPresenter = new IconMenuPresenter(context);
+                iconMenuPresenter.setCallback(cb);
+                iconMenuPresenter.setId(R.id.icon_menu_presenter);
+                menu.addMenuPresenter(iconMenuPresenter);
+            }
+
+            MenuView result = iconMenuPresenter.getMenuView(decorView);
+
+            return result;
+        }
+
+        Parcelable onSaveInstanceState() {
+            SavedState savedState = new SavedState();
+            savedState.featureId = featureId;
+            savedState.isOpen = isOpen;
+            savedState.isInExpandedMode = isInExpandedMode;
+
+            if (menu != null) {
+                savedState.menuState = new Bundle();
+                menu.savePresenterStates(savedState.menuState);
+            }
+
+            return savedState;
+        }
+
+        void onRestoreInstanceState(Parcelable state) {
+            SavedState savedState = (SavedState) state;
+            featureId = savedState.featureId;
+            wasLastOpen = savedState.isOpen;
+            wasLastExpanded = savedState.isInExpandedMode;
+            frozenMenuState = savedState.menuState;
+
+            /*
+             * A LocalActivityManager keeps the same instance of this class around.
+             * The first time the menu is being shown after restoring, the
+             * Activity.onCreateOptionsMenu should be called. But, if it is the
+             * same instance then menu != null and we won't call that method.
+             * We clear any cached views here. The caller should invalidatePanelMenu.
+             */
+            createdPanelView = null;
+            shownPanelView = null;
+            decorView = null;
+        }
+
+        void applyFrozenState() {
+            if (menu != null && frozenMenuState != null) {
+                menu.restorePresenterStates(frozenMenuState);
+                frozenMenuState = null;
+            }
+        }
+
+        private static class SavedState implements Parcelable {
+            int featureId;
+            boolean isOpen;
+            boolean isInExpandedMode;
+            Bundle menuState;
+
+            public int describeContents() {
+                return 0;
+            }
+
+            public void writeToParcel(Parcel dest, int flags) {
+                dest.writeInt(featureId);
+                dest.writeInt(isOpen ? 1 : 0);
+                dest.writeInt(isInExpandedMode ? 1 : 0);
+
+                if (isOpen) {
+                    dest.writeBundle(menuState);
+                }
+            }
+
+            private static SavedState readFromParcel(Parcel source) {
+                SavedState savedState = new SavedState();
+                savedState.featureId = source.readInt();
+                savedState.isOpen = source.readInt() == 1;
+                savedState.isInExpandedMode = source.readInt() == 1;
+
+                if (savedState.isOpen) {
+                    savedState.menuState = source.readBundle();
+                }
+
+                return savedState;
+            }
+
+            public static final Parcelable.Creator<SavedState> CREATOR
+                    = new Parcelable.Creator<SavedState>() {
+                public SavedState createFromParcel(Parcel in) {
+                    return readFromParcel(in);
+                }
+
+                public SavedState[] newArray(int size) {
+                    return new SavedState[size];
+                }
+            };
+        }
+
+    }
+
+    static class RotationWatcher extends Stub {
+        private Handler mHandler;
+        private final Runnable mRotationChanged = new Runnable() {
+            public void run() {
+                dispatchRotationChanged();
+            }
+        };
+        private final ArrayList<WeakReference<PhoneWindow>> mWindows =
+                new ArrayList<WeakReference<PhoneWindow>>();
+        private boolean mIsWatching;
+
+        @Override
+        public void onRotationChanged(int rotation) throws RemoteException {
+            mHandler.post(mRotationChanged);
+        }
+
+        public void addWindow(PhoneWindow phoneWindow) {
+            synchronized (mWindows) {
+                if (!mIsWatching) {
+                    try {
+                        WindowManagerHolder.sWindowManager.watchRotation(this,
+                                phoneWindow.getContext().getDisplay().getDisplayId());
+                        mHandler = new Handler();
+                        mIsWatching = true;
+                    } catch (RemoteException ex) {
+                        Log.e(TAG, "Couldn't start watching for device rotation", ex);
+                    }
+                }
+                mWindows.add(new WeakReference<PhoneWindow>(phoneWindow));
+            }
+        }
+
+        public void removeWindow(PhoneWindow phoneWindow) {
+            synchronized (mWindows) {
+                int i = 0;
+                while (i < mWindows.size()) {
+                    final WeakReference<PhoneWindow> ref = mWindows.get(i);
+                    final PhoneWindow win = ref.get();
+                    if (win == null || win == phoneWindow) {
+                        mWindows.remove(i);
+                    } else {
+                        i++;
+                    }
+                }
+            }
+        }
+
+        void dispatchRotationChanged() {
+            synchronized (mWindows) {
+                int i = 0;
+                while (i < mWindows.size()) {
+                    final WeakReference<PhoneWindow> ref = mWindows.get(i);
+                    final PhoneWindow win = ref.get();
+                    if (win != null) {
+                        win.onOptionsPanelRotationChanged();
+                        i++;
+                    } else {
+                        mWindows.remove(i);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Simple implementation of MenuBuilder.Callback that:
+     * <li> Opens a submenu when selected.
+     * <li> Calls back to the callback's onMenuItemSelected when an item is
+     * selected.
+     */
+    public static final class PhoneWindowMenuCallback
+            implements MenuBuilder.Callback, MenuPresenter.Callback {
+        private static final int FEATURE_ID = FEATURE_CONTEXT_MENU;
+
+        private final PhoneWindow mWindow;
+
+        private MenuDialogHelper mSubMenuHelper;
+
+        private boolean mShowDialogForSubmenu;
+
+        public PhoneWindowMenuCallback(PhoneWindow window) {
+            mWindow = window;
+        }
+
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+            if (menu.getRootMenu() != menu) {
+                onCloseSubMenu(menu);
+            }
+
+            if (allMenusAreClosing) {
+                final Callback callback = mWindow.getCallback();
+                if (callback != null && !mWindow.isDestroyed()) {
+                    callback.onPanelClosed(FEATURE_ID, menu);
+                }
+
+                if (menu == mWindow.mContextMenu) {
+                    mWindow.dismissContextMenu();
+                }
+
+                // Dismiss the submenu, if it is showing
+                if (mSubMenuHelper != null) {
+                    mSubMenuHelper.dismiss();
+                    mSubMenuHelper = null;
+                }
+            }
+        }
+
+        private void onCloseSubMenu(MenuBuilder menu) {
+            final Callback callback = mWindow.getCallback();
+            if (callback != null && !mWindow.isDestroyed()) {
+                callback.onPanelClosed(FEATURE_ID, menu.getRootMenu());
+            }
+        }
+
+        @Override
+        public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+            final Callback callback = mWindow.getCallback();
+            return callback != null && !mWindow.isDestroyed()
+                    && callback.onMenuItemSelected(FEATURE_ID, item);
+        }
+
+        @Override
+        public void onMenuModeChange(MenuBuilder menu) {
+        }
+
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            if (subMenu == null) {
+                return false;
+            }
+
+            // Set a simple callback for the submenu
+            subMenu.setCallback(this);
+
+            if (mShowDialogForSubmenu) {
+                // The window manager will give us a valid window token
+                mSubMenuHelper = new MenuDialogHelper(subMenu);
+                mSubMenuHelper.show(null);
+                return true;
+            }
+
+            return false;
+        }
+
+        public void setShowDialogForSubmenu(boolean enabled) {
+            mShowDialogForSubmenu = enabled;
+        }
+    }
+
+    int getLocalFeaturesPrivate() {
+        return super.getLocalFeatures();
+    }
+
+    protected void setDefaultWindowFormat(int format) {
+        super.setDefaultWindowFormat(format);
+    }
+
+    void sendCloseSystemWindows() {
+        sendCloseSystemWindows(getContext(), null);
+    }
+
+    void sendCloseSystemWindows(String reason) {
+        sendCloseSystemWindows(getContext(), reason);
+    }
+
+    public static void sendCloseSystemWindows(Context context, String reason) {
+        if (ActivityManager.isSystemReady()) {
+            try {
+                ActivityManager.getService().closeSystemDialogs(reason);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    @Override
+    public int getStatusBarColor() {
+        return mStatusBarColor;
+    }
+
+    @Override
+    public void setStatusBarColor(int color) {
+        mStatusBarColor = color;
+        mForcedStatusBarColor = true;
+        if (mDecor != null) {
+            mDecor.updateColorViews(null, false /* animate */);
+        }
+    }
+
+    @Override
+    public int getNavigationBarColor() {
+        return mNavigationBarColor;
+    }
+
+    @Override
+    public void setNavigationBarColor(int color) {
+        mNavigationBarColor = color;
+        mForcedNavigationBarColor = true;
+        if (mDecor != null) {
+            mDecor.updateColorViews(null, false /* animate */);
+            mDecor.updateNavigationGuardColor();
+        }
+    }
+
+    public void setIsStartingWindow(boolean isStartingWindow) {
+        mIsStartingWindow = isStartingWindow;
+    }
+
+    @Override
+    public void setTheme(int resid) {
+        mTheme = resid;
+        if (mDecor != null) {
+            Context context = mDecor.getContext();
+            if (context instanceof DecorContext) {
+                context.setTheme(resid);
+            }
+        }
+    }
+
+    @Override
+    public void setResizingCaptionDrawable(Drawable drawable) {
+        mDecor.setUserCaptionBackgroundDrawable(drawable);
+    }
+
+    @Override
+    public void setDecorCaptionShade(int decorCaptionShade) {
+        mDecorCaptionShade = decorCaptionShade;
+        if (mDecor != null) {
+            mDecor.updateDecorCaptionShade();
+        }
+    }
+
+    int getDecorCaptionShade() {
+        return mDecorCaptionShade;
+    }
+
+    @Override
+    public void setAttributes(WindowManager.LayoutParams params) {
+        super.setAttributes(params);
+        if (mDecor != null) {
+            mDecor.updateLogTag(params);
+        }
+    }
+}
diff --git a/com/android/internal/policy/PipSnapAlgorithm.java b/com/android/internal/policy/PipSnapAlgorithm.java
new file mode 100644
index 0000000..749d00c
--- /dev/null
+++ b/com/android/internal/policy/PipSnapAlgorithm.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2016 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.internal.policy;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.util.Size;
+import android.view.Gravity;
+import android.view.ViewConfiguration;
+import android.widget.Scroller;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
+ * All bounds are relative to the display top/left.
+ */
+public class PipSnapAlgorithm {
+
+    // The below SNAP_MODE_* constants correspond to the config resource value
+    // config_pictureInPictureSnapMode and should not be changed independently.
+    // Allows snapping to the four corners
+    private static final int SNAP_MODE_CORNERS_ONLY = 0;
+    // Allows snapping to the four corners and the mid-points on the long edge in each orientation
+    private static final int SNAP_MODE_CORNERS_AND_SIDES = 1;
+    // Allows snapping to anywhere along the edge of the screen
+    private static final int SNAP_MODE_EDGE = 2;
+    // Allows snapping anywhere along the edge of the screen and magnets towards corners
+    private static final int SNAP_MODE_EDGE_MAGNET_CORNERS = 3;
+    // Allows snapping on the long edge in each orientation and magnets towards corners
+    private static final int SNAP_MODE_LONG_EDGE_MAGNET_CORNERS = 4;
+
+    // Threshold to magnet to a corner
+    private static final float CORNER_MAGNET_THRESHOLD = 0.3f;
+
+    private final Context mContext;
+
+    private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
+    private final int mDefaultSnapMode = SNAP_MODE_EDGE_MAGNET_CORNERS;
+    private int mSnapMode = mDefaultSnapMode;
+
+    private final float mDefaultSizePercent;
+    private final float mMinAspectRatioForMinSize;
+    private final float mMaxAspectRatioForMinSize;
+    private final int mFlingDeceleration;
+
+    private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
+
+    private final int mMinimizedVisibleSize;
+    private boolean mIsMinimized;
+
+    public PipSnapAlgorithm(Context context) {
+        Resources res = context.getResources();
+        mContext = context;
+        mMinimizedVisibleSize = res.getDimensionPixelSize(
+                com.android.internal.R.dimen.pip_minimized_visible_size);
+        mDefaultSizePercent = res.getFloat(
+                com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
+        mMaxAspectRatioForMinSize = res.getFloat(
+                com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
+        mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
+        mFlingDeceleration = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.pip_fling_deceleration);
+        onConfigurationChanged();
+    }
+
+    /**
+     * Updates the snap algorithm when the configuration changes.
+     */
+    public void onConfigurationChanged() {
+        Resources res = mContext.getResources();
+        mOrientation = res.getConfiguration().orientation;
+        mSnapMode = res.getInteger(com.android.internal.R.integer.config_pictureInPictureSnapMode);
+        calculateSnapTargets();
+    }
+
+    /**
+     * Sets the PIP's minimized state.
+     */
+    public void setMinimized(boolean isMinimized) {
+        mIsMinimized = isMinimized;
+    }
+
+    /**
+     * @return the closest absolute snap stack bounds for the given {@param stackBounds} moving at
+     * the given {@param velocityX} and {@param velocityY}.  The {@param movementBounds} should be
+     * those for the given {@param stackBounds}.
+     */
+    public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
+            float velocityY, Point dragStartPosition) {
+        final Rect intersectStackBounds = new Rect(stackBounds);
+        final Point intersect = getEdgeIntersect(stackBounds, movementBounds, velocityX, velocityY,
+                dragStartPosition);
+        intersectStackBounds.offsetTo(intersect.x, intersect.y);
+        return findClosestSnapBounds(movementBounds, intersectStackBounds);
+    }
+
+    /**
+     * @return The point along the {@param movementBounds} that the PIP would intersect with based
+     *         on the provided {@param velX}, {@param velY} along with the position of the PIP when
+     *         the gesture started, {@param dragStartPosition}.
+     */
+    public Point getEdgeIntersect(Rect stackBounds, Rect movementBounds, float velX, float velY,
+            Point dragStartPosition) {
+        final boolean isLandscape = mOrientation == Configuration.ORIENTATION_LANDSCAPE;
+        final int x = stackBounds.left;
+        final int y = stackBounds.top;
+
+        // Find the line of movement the PIP is on. Line defined by: y = slope * x + yIntercept
+        final float slope = velY / velX; // slope = rise / run
+        final float yIntercept = y - slope * x; // rearrange line equation for yIntercept
+        // The PIP can have two intercept points:
+        // 1) Where the line intersects with one of the edges of the screen (vertical line)
+        Point vertPoint = new Point();
+        // 2) Where the line intersects with the top or bottom of the screen (horizontal line)
+        Point horizPoint = new Point();
+
+        // Find the vertical line intersection, x will be one of the edges
+        vertPoint.x = velX > 0 ? movementBounds.right : movementBounds.left;
+        // Sub in x in our line equation to determine y position
+        vertPoint.y = findY(slope, yIntercept, vertPoint.x);
+
+        // Find the horizontal line intersection, y will be the top or bottom of the screen
+        horizPoint.y = velY > 0 ? movementBounds.bottom : movementBounds.top;
+        // Sub in y in our line equation to determine x position
+        horizPoint.x = findX(slope, yIntercept, horizPoint.y);
+
+        // Now pick one of these points -- first determine if we're flinging along the current edge.
+        // Only fling along current edge if it's a direction with space for the PIP to move to
+        int maxDistance;
+        if (isLandscape) {
+            maxDistance = velX > 0
+                    ? movementBounds.right - stackBounds.left
+                    : stackBounds.left - movementBounds.left;
+        } else {
+            maxDistance = velY > 0
+                    ? movementBounds.bottom - stackBounds.top
+                    : stackBounds.top - movementBounds.top;
+        }
+        if (maxDistance > 0) {
+            // Only fling along the current edge if the start and end point are on the same side
+            final int startPoint = isLandscape ? dragStartPosition.y : dragStartPosition.x;
+            final int endPoint = isLandscape ? horizPoint.y : horizPoint.x;
+            final int center = movementBounds.centerX();
+            if ((startPoint < center && endPoint < center)
+                    || (startPoint > center && endPoint > center)) {
+                // We are flinging along the current edge, figure out how far it should travel
+                // based on velocity and assumed deceleration.
+                int distance = (int) (0 - Math.pow(isLandscape ? velX : velY, 2))
+                        / (2 * mFlingDeceleration);
+                distance = Math.min(distance, maxDistance);
+                // Adjust the point for the distance
+                if (isLandscape) {
+                    horizPoint.x = stackBounds.left + (velX > 0 ? distance : -distance);
+                } else {
+                    horizPoint.y = stackBounds.top + (velY > 0 ? distance : -distance);
+                }
+                return horizPoint;
+            }
+        }
+        // If we're not flinging along the current edge, find the closest point instead.
+        final double distanceVert = Math.hypot(vertPoint.x - x, vertPoint.y - y);
+        final double distanceHoriz = Math.hypot(horizPoint.x - x, horizPoint.y - y);
+        // Ensure that we're actually going somewhere
+        if (distanceVert == 0) {
+            return horizPoint;
+        }
+        if (distanceHoriz == 0) {
+            return vertPoint;
+        }
+        // Otherwise use the closest point
+        return Math.abs(distanceVert) > Math.abs(distanceHoriz) ? horizPoint : vertPoint;
+    }
+
+    private int findY(float slope, float yIntercept, float x) {
+        return (int) ((slope * x) + yIntercept);
+    }
+
+    private int findX(float slope, float yIntercept, float y) {
+        return (int) ((y - yIntercept) / slope);
+    }
+
+    /**
+     * @return the closest absolute snap stack bounds for the given {@param stackBounds}.  The
+     * {@param movementBounds} should be those for the given {@param stackBounds}.
+     */
+    public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
+        final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
+                movementBounds.right + stackBounds.width(),
+                movementBounds.bottom + stackBounds.height());
+        final Rect newBounds = new Rect(stackBounds);
+        if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS
+                || mSnapMode == SNAP_MODE_EDGE_MAGNET_CORNERS) {
+            final Rect tmpBounds = new Rect();
+            final Point[] snapTargets = new Point[mSnapGravities.size()];
+            for (int i = 0; i < mSnapGravities.size(); i++) {
+                Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
+                        pipBounds, 0, 0, tmpBounds);
+                snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
+            }
+            Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
+            float distance = distanceToPoint(snapTarget, stackBounds.left, stackBounds.top);
+            final float thresh = Math.max(stackBounds.width(), stackBounds.height())
+                    * CORNER_MAGNET_THRESHOLD;
+            if (distance < thresh) {
+                newBounds.offsetTo(snapTarget.x, snapTarget.y);
+            } else {
+                snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
+            }
+        } else if (mSnapMode == SNAP_MODE_EDGE) {
+            // Find the closest edge to the given stack bounds and snap to it
+            snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
+        } else {
+            // Find the closest snap point
+            final Rect tmpBounds = new Rect();
+            final Point[] snapTargets = new Point[mSnapGravities.size()];
+            for (int i = 0; i < mSnapGravities.size(); i++) {
+                Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
+                        pipBounds, 0, 0, tmpBounds);
+                snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
+            }
+            Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
+            newBounds.offsetTo(snapTarget.x, snapTarget.y);
+        }
+        return newBounds;
+    }
+
+    /**
+     * Applies the offset to the {@param stackBounds} to adjust it to a minimized state.
+     */
+    public void applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize,
+            Rect stableInsets) {
+        if (stackBounds.left <= movementBounds.centerX()) {
+            stackBounds.offsetTo(stableInsets.left + mMinimizedVisibleSize - stackBounds.width(),
+                    stackBounds.top);
+        } else {
+            stackBounds.offsetTo(displaySize.x - stableInsets.right - mMinimizedVisibleSize,
+                    stackBounds.top);
+        }
+    }
+
+    /**
+     * @return returns a fraction that describes where along the {@param movementBounds} the
+     *         {@param stackBounds} are. If the {@param stackBounds} are not currently on the
+     *         {@param movementBounds} exactly, then they will be snapped to the movement bounds.
+     *
+     *         The fraction is defined in a clockwise fashion against the {@param movementBounds}:
+     *
+     *            0   1
+     *          4 +---+ 1
+     *            |   |
+     *          3 +---+ 2
+     *            3   2
+     */
+    public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
+        final Rect tmpBounds = new Rect();
+        snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
+        final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
+                movementBounds.width();
+        final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
+                movementBounds.height();
+        if (tmpBounds.top == movementBounds.top) {
+            return widthFraction;
+        } else if (tmpBounds.left == movementBounds.right) {
+            return 1f + heightFraction;
+        } else if (tmpBounds.top == movementBounds.bottom) {
+            return 2f + (1f - widthFraction);
+        } else {
+            return 3f + (1f - heightFraction);
+        }
+    }
+
+    /**
+     * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction.
+     * See {@link #getSnapFraction(Rect, Rect)}.
+     *
+     * The fraction is define in a clockwise fashion against the {@param movementBounds}:
+     *
+     *    0   1
+     *  4 +---+ 1
+     *    |   |
+     *  3 +---+ 2
+     *    3   2
+     */
+    public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
+        if (snapFraction < 1f) {
+            int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
+            stackBounds.offsetTo(offset, movementBounds.top);
+        } else if (snapFraction < 2f) {
+            snapFraction -= 1f;
+            int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
+            stackBounds.offsetTo(movementBounds.right, offset);
+        } else if (snapFraction < 3f) {
+            snapFraction -= 2f;
+            int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
+            stackBounds.offsetTo(offset, movementBounds.bottom);
+        } else {
+            snapFraction -= 3f;
+            int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
+            stackBounds.offsetTo(movementBounds.left, offset);
+        }
+    }
+
+    /**
+     * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given
+     * {@param stackBounds}.
+     */
+    public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
+            int imeHeight) {
+        // Adjust the right/bottom to ensure the stack bounds never goes offscreen
+        movementBoundsOut.set(insetBounds);
+        movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right -
+                stackBounds.width());
+        movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom -
+                stackBounds.height());
+        movementBoundsOut.bottom -= imeHeight;
+    }
+
+    /**
+     * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge
+     * is at least {@param minEdgeSize}.
+     */
+    public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
+            int displayHeight) {
+        final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
+        final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
+
+        final int width;
+        final int height;
+        if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
+            // Beyond these points, we can just use the min size as the shorter edge
+            if (aspectRatio <= 1) {
+                // Portrait, width is the minimum size
+                width = minSize;
+                height = Math.round(width / aspectRatio);
+            } else {
+                // Landscape, height is the minimum size
+                height = minSize;
+                width = Math.round(height * aspectRatio);
+            }
+        } else {
+            // Within these points, we ensure that the bounds fit within the radius of the limits
+            // at the points
+            final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
+            final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
+            height = (int) Math.round(Math.sqrt((radius * radius) /
+                    (aspectRatio * aspectRatio + 1)));
+            width = Math.round(height * aspectRatio);
+        }
+        return new Size(width, height);
+    }
+
+    /**
+     * @return the closest point in {@param points} to the given {@param x} and {@param y}.
+     */
+    private Point findClosestPoint(int x, int y, Point[] points) {
+        Point closestPoint = null;
+        float minDistance = Float.MAX_VALUE;
+        for (Point p : points) {
+            float distance = distanceToPoint(p, x, y);
+            if (distance < minDistance) {
+                closestPoint = p;
+                minDistance = distance;
+            }
+        }
+        return closestPoint;
+    }
+
+    /**
+     * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes
+     * the new bounds out to {@param boundsOut}.
+     */
+    private void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
+        // If the stackBounds are minimized, then it should only be snapped back horizontally
+        final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
+                stackBounds.left));
+        final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
+                stackBounds.top));
+        boundsOut.set(stackBounds);
+        if (mIsMinimized) {
+            boundsOut.offsetTo(boundedLeft, boundedTop);
+            return;
+        }
+
+        // Otherwise, just find the closest edge
+        final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
+        final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
+        final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
+        final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
+        int shortest;
+        if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS) {
+            // Only check longest edges
+            shortest = (mOrientation == Configuration.ORIENTATION_LANDSCAPE)
+                    ? Math.min(fromTop, fromBottom)
+                    : Math.min(fromLeft, fromRight);
+        } else {
+            shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
+        }
+        if (shortest == fromLeft) {
+            boundsOut.offsetTo(movementBounds.left, boundedTop);
+        } else if (shortest == fromTop) {
+            boundsOut.offsetTo(boundedLeft, movementBounds.top);
+        } else if (shortest == fromRight) {
+            boundsOut.offsetTo(movementBounds.right, boundedTop);
+        } else {
+            boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
+        }
+    }
+
+    /**
+     * @return the distance between point {@param p} and the given {@param x} and {@param y}.
+     */
+    private float distanceToPoint(Point p, int x, int y) {
+        return PointF.length(p.x - x, p.y - y);
+    }
+
+    /**
+     * Calculate the snap targets for the discrete snap modes.
+     */
+    private void calculateSnapTargets() {
+        mSnapGravities.clear();
+        switch (mSnapMode) {
+            case SNAP_MODE_CORNERS_AND_SIDES:
+                if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+                    mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+                    mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+                } else {
+                    mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+                    mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+                }
+                // Fall through
+            case SNAP_MODE_CORNERS_ONLY:
+            case SNAP_MODE_EDGE_MAGNET_CORNERS:
+            case SNAP_MODE_LONG_EDGE_MAGNET_CORNERS:
+                mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
+                mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
+                mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
+                mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
+                break;
+            default:
+                // Skip otherwise
+                break;
+        }
+    }
+
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + PipSnapAlgorithm.class.getSimpleName());
+        pw.println(innerPrefix + "mSnapMode=" + mSnapMode);
+        pw.println(innerPrefix + "mOrientation=" + mOrientation);
+        pw.println(innerPrefix + "mMinimizedVisibleSize=" + mMinimizedVisibleSize);
+        pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
+    }
+}
diff --git a/com/android/internal/preference/YesNoPreference.java b/com/android/internal/preference/YesNoPreference.java
new file mode 100644
index 0000000..7abf416
--- /dev/null
+++ b/com/android/internal/preference/YesNoPreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2007 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.internal.preference;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+
+/**
+ * The {@link YesNoPreference} is a preference to show a dialog with Yes and No
+ * buttons.
+ * <p>
+ * This preference will store a boolean into the SharedPreferences.
+ */
+public class YesNoPreference extends DialogPreference {
+    private boolean mWasPositiveResult;
+
+    public YesNoPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public YesNoPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public YesNoPreference(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.yesNoPreferenceStyle);
+    }
+
+    public YesNoPreference(Context context) {
+        this(context, null);
+    }
+
+    @Override
+    protected void onDialogClosed(boolean positiveResult) {
+        super.onDialogClosed(positiveResult);
+
+        if (callChangeListener(positiveResult)) {
+            setValue(positiveResult);
+        }
+    }
+
+    /**
+     * Sets the value of this preference, and saves it to the persistent store
+     * if required.
+     * 
+     * @param value The value of the preference.
+     */
+    public void setValue(boolean value) {
+        mWasPositiveResult = value;
+        
+        persistBoolean(value);
+        
+        notifyDependencyChange(!value);
+    }
+    
+    /**
+     * Gets the value of this preference.
+     * 
+     * @return The value of the preference.
+     */
+    public boolean getValue() {
+        return mWasPositiveResult;
+    }
+    
+    @Override
+    protected Object onGetDefaultValue(TypedArray a, int index) {
+        return a.getBoolean(index, false);
+    }
+
+    @Override
+    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
+        setValue(restorePersistedValue ? getPersistedBoolean(mWasPositiveResult) :
+            (Boolean) defaultValue);
+    }
+
+    @Override
+    public boolean shouldDisableDependents() {
+        return !mWasPositiveResult || super.shouldDisableDependents();
+    }
+    
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        final Parcelable superState = super.onSaveInstanceState();
+        if (isPersistent()) {
+            // No need to save instance state since it's persistent
+            return superState;
+        }
+        
+        final SavedState myState = new SavedState(superState);
+        myState.wasPositiveResult = getValue();
+        return myState;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (!state.getClass().equals(SavedState.class)) {
+            // Didn't save state for us in onSaveInstanceState
+            super.onRestoreInstanceState(state);
+            return;
+        }
+         
+        SavedState myState = (SavedState) state;
+        super.onRestoreInstanceState(myState.getSuperState());
+        setValue(myState.wasPositiveResult);
+    }
+    
+    private static class SavedState extends BaseSavedState {
+        boolean wasPositiveResult;
+        
+        public SavedState(Parcel source) {
+            super(source);
+            wasPositiveResult = source.readInt() == 1;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(wasPositiveResult ? 1 : 0);
+        }
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+    
+}
diff --git a/com/android/internal/print/DumpUtils.java b/com/android/internal/print/DumpUtils.java
new file mode 100644
index 0000000..28c7fc2
--- /dev/null
+++ b/com/android/internal/print/DumpUtils.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2017 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.internal.print;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.ComponentNameProto;
+import android.content.Context;
+import android.print.PageRange;
+import android.print.PrintAttributes;
+import android.print.PrintDocumentInfo;
+import android.print.PrintJobId;
+import android.print.PrintJobInfo;
+import android.print.PrinterCapabilitiesInfo;
+import android.print.PrinterId;
+import android.print.PrinterInfo;
+import android.service.print.MarginsProto;
+import android.service.print.MediaSizeProto;
+import android.service.print.PageRangeProto;
+import android.service.print.PrintAttributesProto;
+import android.service.print.PrintDocumentInfoProto;
+import android.service.print.PrintJobInfoProto;
+import android.service.print.PrinterCapabilitiesProto;
+import android.service.print.PrinterIdProto;
+import android.service.print.PrinterInfoProto;
+import android.service.print.ResolutionProto;
+import android.util.proto.ProtoOutputStream;
+
+/**
+ * Utilities for dumping print related proto buffer
+ */
+public class DumpUtils {
+    /**
+     * Write a string to a proto if the string is not {@code null}.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the string
+     * @param string The string to write
+     */
+    public static void writeStringIfNotNull(@NonNull ProtoOutputStream proto, long id,
+            @Nullable String string) {
+        if (string != null) {
+            proto.write(id, string);
+        }
+    }
+
+    /**
+     * Write a {@link ComponentName} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param component The component name to write
+     */
+    public static void writeComponentName(@NonNull ProtoOutputStream proto, long id,
+            @NonNull ComponentName component) {
+        long token = proto.start(id);
+        proto.write(ComponentNameProto.PACKAGE_NAME, component.getPackageName());
+        proto.write(ComponentNameProto.CLASS_NAME, component.getClassName());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrinterId} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param printerId The printer id to write
+     */
+    public static void writePrinterId(@NonNull ProtoOutputStream proto, long id,
+            @NonNull PrinterId printerId) {
+        long token = proto.start(id);
+        writeComponentName(proto, PrinterIdProto.SERVICE_NAME, printerId.getServiceName());
+        proto.write(PrinterIdProto.LOCAL_ID, printerId.getLocalId());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrinterCapabilitiesInfo} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param cap The capabilities to write
+     */
+    public static void writePrinterCapabilities(@NonNull Context context,
+            @NonNull ProtoOutputStream proto, long id, @NonNull PrinterCapabilitiesInfo cap) {
+        long token = proto.start(id);
+        writeMargins(proto, PrinterCapabilitiesProto.MIN_MARGINS, cap.getMinMargins());
+
+        int numMediaSizes = cap.getMediaSizes().size();
+        for (int i = 0; i < numMediaSizes; i++) {
+            writeMediaSize(context, proto, PrinterCapabilitiesProto.MEDIA_SIZES,
+                    cap.getMediaSizes().get(i));
+        }
+
+        int numResolutions = cap.getResolutions().size();
+        for (int i = 0; i < numResolutions; i++) {
+            writeResolution(proto, PrinterCapabilitiesProto.RESOLUTIONS,
+                    cap.getResolutions().get(i));
+        }
+
+        if ((cap.getColorModes() & PrintAttributes.COLOR_MODE_MONOCHROME) != 0) {
+            proto.write(PrinterCapabilitiesProto.COLOR_MODES,
+                    PrintAttributesProto.COLOR_MODE_MONOCHROME);
+        }
+        if ((cap.getColorModes() & PrintAttributes.COLOR_MODE_COLOR) != 0) {
+            proto.write(PrinterCapabilitiesProto.COLOR_MODES,
+                    PrintAttributesProto.COLOR_MODE_COLOR);
+        }
+
+        if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_NONE) != 0) {
+            proto.write(PrinterCapabilitiesProto.DUPLEX_MODES,
+                    PrintAttributesProto.DUPLEX_MODE_NONE);
+        }
+        if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_LONG_EDGE) != 0) {
+            proto.write(PrinterCapabilitiesProto.DUPLEX_MODES,
+                    PrintAttributesProto.DUPLEX_MODE_LONG_EDGE);
+        }
+        if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_SHORT_EDGE) != 0) {
+            proto.write(PrinterCapabilitiesProto.DUPLEX_MODES,
+                    PrintAttributesProto.DUPLEX_MODE_SHORT_EDGE);
+        }
+
+        proto.end(token);
+    }
+
+
+    /**
+     * Write a {@link PrinterInfo} to a proto.
+     *
+     * @param context The context used to resolve resources
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param info The printer info to write
+     */
+    public static void writePrinterInfo(@NonNull Context context, @NonNull ProtoOutputStream proto,
+            long id, @NonNull PrinterInfo info) {
+        long token = proto.start(id);
+        writePrinterId(proto, PrinterInfoProto.ID, info.getId());
+        proto.write(PrinterInfoProto.NAME, info.getName());
+        proto.write(PrinterInfoProto.STATUS, info.getStatus());
+        proto.write(PrinterInfoProto.DESCRIPTION, info.getDescription());
+
+        PrinterCapabilitiesInfo cap = info.getCapabilities();
+        if (cap != null) {
+            writePrinterCapabilities(context, proto, PrinterInfoProto.CAPABILITIES, cap);
+        }
+
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrintAttributes.MediaSize} to a proto.
+     *
+     * @param context The context used to resolve resources
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param mediaSize The media size to write
+     */
+    public static void writeMediaSize(@NonNull Context context, @NonNull ProtoOutputStream proto,
+            long id, @NonNull PrintAttributes.MediaSize mediaSize) {
+        long token = proto.start(id);
+        proto.write(MediaSizeProto.ID, mediaSize.getId());
+        proto.write(MediaSizeProto.LABEL, mediaSize.getLabel(context.getPackageManager()));
+        proto.write(MediaSizeProto.HEIGHT_MILS, mediaSize.getHeightMils());
+        proto.write(MediaSizeProto.WIDTH_MILS, mediaSize.getWidthMils());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrintAttributes.Resolution} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param res The resolution to write
+     */
+    public static void writeResolution(@NonNull ProtoOutputStream proto, long id,
+            @NonNull PrintAttributes.Resolution res) {
+        long token = proto.start(id);
+        proto.write(ResolutionProto.ID, res.getId());
+        proto.write(ResolutionProto.LABEL, res.getLabel());
+        proto.write(ResolutionProto.HORIZONTAL_DPI, res.getHorizontalDpi());
+        proto.write(ResolutionProto.VERTICAL_DPI, res.getVerticalDpi());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrintAttributes.Margins} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param margins The margins to write
+     */
+    public static void writeMargins(@NonNull ProtoOutputStream proto, long id,
+            @NonNull PrintAttributes.Margins margins) {
+        long token = proto.start(id);
+        proto.write(MarginsProto.TOP_MILS, margins.getTopMils());
+        proto.write(MarginsProto.LEFT_MILS, margins.getLeftMils());
+        proto.write(MarginsProto.RIGHT_MILS, margins.getRightMils());
+        proto.write(MarginsProto.BOTTOM_MILS, margins.getBottomMils());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrintAttributes} to a proto.
+     *
+     * @param context The context used to resolve resources
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param attributes The attributes to write
+     */
+    public static void writePrintAttributes(@NonNull Context context,
+            @NonNull ProtoOutputStream proto, long id, @NonNull PrintAttributes attributes) {
+        long token = proto.start(id);
+
+        PrintAttributes.MediaSize mediaSize = attributes.getMediaSize();
+        if (mediaSize != null) {
+            writeMediaSize(context, proto, PrintAttributesProto.MEDIA_SIZE, mediaSize);
+        }
+
+        proto.write(PrintAttributesProto.IS_PORTRAIT, attributes.isPortrait());
+
+        PrintAttributes.Resolution res = attributes.getResolution();
+        if (res != null) {
+            writeResolution(proto, PrintAttributesProto.RESOLUTION, res);
+        }
+
+        PrintAttributes.Margins minMargins = attributes.getMinMargins();
+        if (minMargins != null) {
+            writeMargins(proto, PrintAttributesProto.MIN_MARGINS, minMargins);
+        }
+
+        proto.write(PrintAttributesProto.COLOR_MODE, attributes.getColorMode());
+        proto.write(PrintAttributesProto.DUPLEX_MODE, attributes.getDuplexMode());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrintDocumentInfo} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param info The info to write
+     */
+    public static void writePrintDocumentInfo(@NonNull ProtoOutputStream proto, long id,
+            @NonNull PrintDocumentInfo info) {
+        long token = proto.start(id);
+        proto.write(PrintDocumentInfoProto.NAME, info.getName());
+
+        int pageCount = info.getPageCount();
+        if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
+            proto.write(PrintDocumentInfoProto.PAGE_COUNT, pageCount);
+        }
+
+        proto.write(PrintDocumentInfoProto.CONTENT_TYPE, info.getContentType());
+        proto.write(PrintDocumentInfoProto.DATA_SIZE, info.getDataSize());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PageRange} to a proto.
+     *
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param range The range to write
+     */
+    public static void writePageRange(@NonNull ProtoOutputStream proto, long id,
+            @NonNull PageRange range) {
+        long token = proto.start(id);
+        proto.write(PageRangeProto.START, range.getStart());
+        proto.write(PageRangeProto.END, range.getEnd());
+        proto.end(token);
+    }
+
+    /**
+     * Write a {@link PrintJobInfo} to a proto.
+     *
+     * @param context The context used to resolve resources
+     * @param proto The proto to write to
+     * @param id The proto-id of the component name
+     * @param printJobInfo The print job info to write
+     */
+    public static void writePrintJobInfo(@NonNull Context context, @NonNull ProtoOutputStream proto,
+            long id, @NonNull PrintJobInfo printJobInfo) {
+        long token = proto.start(id);
+        proto.write(PrintJobInfoProto.LABEL, printJobInfo.getLabel());
+
+        PrintJobId printJobId = printJobInfo.getId();
+        if (printJobId != null) {
+            proto.write(PrintJobInfoProto.PRINT_JOB_ID, printJobId.flattenToString());
+        }
+
+        int state = printJobInfo.getState();
+        if (state >= PrintJobInfoProto.STATE_CREATED && state <= PrintJobInfoProto.STATE_CANCELED) {
+            proto.write(PrintJobInfoProto.STATE, state);
+        } else {
+            proto.write(PrintJobInfoProto.STATE, PrintJobInfoProto.STATE_UNKNOWN);
+        }
+
+        PrinterId printer = printJobInfo.getPrinterId();
+        if (printer != null) {
+            writePrinterId(proto, PrintJobInfoProto.PRINTER, printer);
+        }
+
+        String tag = printJobInfo.getTag();
+        if (tag != null) {
+            proto.write(PrintJobInfoProto.TAG, tag);
+        }
+
+        proto.write(PrintJobInfoProto.CREATION_TIME, printJobInfo.getCreationTime());
+
+        PrintAttributes attributes = printJobInfo.getAttributes();
+        if (attributes != null) {
+            writePrintAttributes(context, proto, PrintJobInfoProto.ATTRIBUTES, attributes);
+        }
+
+        PrintDocumentInfo docInfo = printJobInfo.getDocumentInfo();
+        if (docInfo != null) {
+            writePrintDocumentInfo(proto, PrintJobInfoProto.DOCUMENT_INFO, docInfo);
+        }
+
+        proto.write(PrintJobInfoProto.IS_CANCELING, printJobInfo.isCancelling());
+
+        PageRange[] pages = printJobInfo.getPages();
+        if (pages != null) {
+            for (int i = 0; i < pages.length; i++) {
+                writePageRange(proto, PrintJobInfoProto.PAGES, pages[i]);
+            }
+        }
+
+        proto.write(PrintJobInfoProto.HAS_ADVANCED_OPTIONS,
+                printJobInfo.getAdvancedOptions() != null);
+        proto.write(PrintJobInfoProto.PROGRESS, printJobInfo.getProgress());
+
+        CharSequence status = printJobInfo.getStatus(context.getPackageManager());
+        if (status != null) {
+            proto.write(PrintJobInfoProto.STATUS, status.toString());
+        }
+
+        proto.end(token);
+    }
+}
diff --git a/com/android/internal/statusbar/NotificationVisibility.java b/com/android/internal/statusbar/NotificationVisibility.java
new file mode 100644
index 0000000..2139ad0
--- /dev/null
+++ b/com/android/internal/statusbar/NotificationVisibility.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 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.internal.statusbar;
+
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+
+public class NotificationVisibility implements Parcelable {
+    private static final String TAG = "NoViz";
+    private static final int MAX_POOL_SIZE = 25;
+    private static ArrayDeque<NotificationVisibility> sPool = new ArrayDeque<>(MAX_POOL_SIZE);
+    private static int sNexrId = 0;
+
+    public String key;
+    public int rank;
+    public boolean visible = true;
+    /*package*/ int id;
+
+    private NotificationVisibility() {
+        id = sNexrId++;
+    }
+
+    private NotificationVisibility(String key, int rank, boolean visibile) {
+        this();
+        this.key = key;
+        this.rank = rank;
+        this.visible = visibile;
+    }
+
+    @Override
+    public String toString() {
+        return "NotificationVisibility(id=" + id
+                + "key=" + key
+                + " rank=" + rank
+                + (visible?" visible":"")
+                + " )";
+    }
+
+    @Override
+    public NotificationVisibility clone() {
+        return obtain(this.key, this.rank, this.visible);
+    }
+
+    @Override
+    public int hashCode() {
+        // allow lookups by key, which _should_ never be null.
+        return key == null ? 0 : key.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object that) {
+        // allow lookups by key, which _should_ never be null.
+        if (that instanceof NotificationVisibility) {
+            NotificationVisibility thatViz = (NotificationVisibility) that;
+            return (key == null && thatViz.key == null) || key.equals(thatViz.key);
+        }
+        return false;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(this.key);
+        out.writeInt(this.rank);
+        out.writeInt(this.visible ? 1 : 0);
+    }
+
+    private void readFromParcel(Parcel in) {
+        this.key = in.readString();
+        this.rank = in.readInt();
+        this.visible = in.readInt() != 0;
+    }
+
+    /**
+     * Return a new NotificationVisibility instance from the global pool. Allows us to
+     * avoid allocating new objects in many cases.
+     */
+    public static NotificationVisibility obtain(String key, int rank, boolean visible) {
+        NotificationVisibility vo = obtain();
+        vo.key = key;
+        vo.rank = rank;
+        vo.visible = visible;
+        return vo;
+    }
+
+    private static NotificationVisibility obtain(Parcel in) {
+        NotificationVisibility vo = obtain();
+        vo.readFromParcel(in);
+        return vo;
+    }
+
+    private static NotificationVisibility obtain() {
+        synchronized (sPool) {
+            if (!sPool.isEmpty()) {
+                return sPool.poll();
+            }
+        }
+        return new NotificationVisibility();
+    }
+
+    /**
+     * Return a NotificationVisibility instance to the global pool.
+     * <p>
+     * You MUST NOT touch the NotificationVisibility after calling this function because it has
+     * effectively been freed.
+     * </p>
+     */
+    public void recycle() {
+        if (key == null) {
+            // do nothing on multiple recycles
+            return;
+        }
+        key = null;
+        if (sPool.size() < MAX_POOL_SIZE) {
+            synchronized (sPool) {
+                sPool.offer(this);
+            }
+        }
+    }
+
+    /**
+     * Parcelable.Creator that instantiates NotificationVisibility objects
+     */
+    public static final Parcelable.Creator<NotificationVisibility> CREATOR
+            = new Parcelable.Creator<NotificationVisibility>()
+    {
+        public NotificationVisibility createFromParcel(Parcel parcel)
+        {
+            return obtain(parcel);
+        }
+
+        public NotificationVisibility[] newArray(int size)
+        {
+            return new NotificationVisibility[size];
+        }
+    };
+}
+
diff --git a/com/android/internal/statusbar/StatusBarIcon.java b/com/android/internal/statusbar/StatusBarIcon.java
new file mode 100644
index 0000000..1d62623
--- /dev/null
+++ b/com/android/internal/statusbar/StatusBarIcon.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 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.internal.statusbar;
+
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+
+public class StatusBarIcon implements Parcelable {
+    public UserHandle user;
+    public String pkg;
+    public Icon icon;
+    public int iconLevel;
+    public boolean visible = true;
+    public int number;
+    public CharSequence contentDescription;
+
+    public StatusBarIcon(UserHandle user, String resPackage, Icon icon, int iconLevel, int number,
+            CharSequence contentDescription) {
+        if (icon.getType() == Icon.TYPE_RESOURCE
+                && TextUtils.isEmpty(icon.getResPackage())) {
+            // This is an odd situation where someone's managed to hand us an icon without a
+            // package inside, probably by mashing an int res into a Notification object.
+            // Now that we have the correct package name handy, let's fix it.
+            icon = Icon.createWithResource(resPackage, icon.getResId());
+        }
+        this.pkg = resPackage;
+        this.user = user;
+        this.icon = icon;
+        this.iconLevel = iconLevel;
+        this.number = number;
+        this.contentDescription = contentDescription;
+    }
+
+    public StatusBarIcon(String iconPackage, UserHandle user,
+            int iconId, int iconLevel, int number,
+            CharSequence contentDescription) {
+        this(user, iconPackage, Icon.createWithResource(iconPackage, iconId),
+                iconLevel, number, contentDescription);
+    }
+
+    @Override
+    public String toString() {
+        return "StatusBarIcon(icon=" + icon
+                + ((iconLevel != 0)?(" level=" + iconLevel):"")
+                + (visible?" visible":"")
+                + " user=" + user.getIdentifier()
+                + ((number != 0)?(" num=" + number):"")
+                + " )";
+    }
+
+    @Override
+    public StatusBarIcon clone() {
+        StatusBarIcon that = new StatusBarIcon(this.user, this.pkg, this.icon,
+                this.iconLevel, this.number, this.contentDescription);
+        that.visible = this.visible;
+        return that;
+    }
+
+    /**
+     * Unflatten the StatusBarIcon from a parcel.
+     */
+    public StatusBarIcon(Parcel in) {
+        readFromParcel(in);
+    }
+
+    public void readFromParcel(Parcel in) {
+        this.icon = (Icon) in.readParcelable(null);
+        this.pkg = in.readString();
+        this.user = (UserHandle) in.readParcelable(null);
+        this.iconLevel = in.readInt();
+        this.visible = in.readInt() != 0;
+        this.number = in.readInt();
+        this.contentDescription = in.readCharSequence();
+    }
+
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(this.icon, 0);
+        out.writeString(this.pkg);
+        out.writeParcelable(this.user, 0);
+        out.writeInt(this.iconLevel);
+        out.writeInt(this.visible ? 1 : 0);
+        out.writeInt(this.number);
+        out.writeCharSequence(this.contentDescription);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable.Creator that instantiates StatusBarIcon objects
+     */
+    public static final Parcelable.Creator<StatusBarIcon> CREATOR
+            = new Parcelable.Creator<StatusBarIcon>()
+    {
+        public StatusBarIcon createFromParcel(Parcel parcel)
+        {
+            return new StatusBarIcon(parcel);
+        }
+
+        public StatusBarIcon[] newArray(int size)
+        {
+            return new StatusBarIcon[size];
+        }
+    };
+}
+
diff --git a/com/android/internal/telephony/ATParseEx.java b/com/android/internal/telephony/ATParseEx.java
new file mode 100644
index 0000000..c93b875
--- /dev/null
+++ b/com/android/internal/telephony/ATParseEx.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * {@hide}
+ */
+public class ATParseEx extends RuntimeException
+{
+    public
+    ATParseEx()
+    {
+        super();
+    }
+
+    public
+    ATParseEx(String s)
+    {
+        super(s);
+    }
+}
diff --git a/com/android/internal/telephony/ATResponseParser.java b/com/android/internal/telephony/ATResponseParser.java
new file mode 100644
index 0000000..1d4d7c7
--- /dev/null
+++ b/com/android/internal/telephony/ATResponseParser.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * {@hide}
+ */
+public class ATResponseParser
+{
+    /*************************** Instance Variables **************************/
+
+    private String mLine;
+    private int mNext = 0;
+    private int mTokStart, mTokEnd;
+
+    /***************************** Class Methods *****************************/
+
+    public
+    ATResponseParser (String line)
+    {
+        mLine = line;
+    }
+
+    public boolean
+    nextBoolean()
+    {
+        // "\s*(\d)(,|$)"
+        // \d is '0' or '1'
+
+        nextTok();
+
+        if (mTokEnd - mTokStart > 1) {
+            throw new ATParseEx();
+        }
+        char c = mLine.charAt(mTokStart);
+
+        if (c == '0') return false;
+        if (c ==  '1') return true;
+        throw new ATParseEx();
+    }
+
+
+    /** positive int only */
+    public int
+    nextInt()
+    {
+        // "\s*(\d+)(,|$)"
+        int ret = 0;
+
+        nextTok();
+
+        for (int i = mTokStart ; i < mTokEnd ; i++) {
+            char c = mLine.charAt(i);
+
+            // Yes, ASCII decimal digits only
+            if (c < '0' || c > '9') {
+                throw new ATParseEx();
+            }
+
+            ret *= 10;
+            ret += c - '0';
+        }
+
+        return ret;
+    }
+
+    public String
+    nextString()
+    {
+        nextTok();
+
+        return mLine.substring(mTokStart, mTokEnd);
+    }
+
+    public boolean
+    hasMore()
+    {
+        return mNext < mLine.length();
+    }
+
+    private void
+    nextTok()
+    {
+        int len = mLine.length();
+
+        if (mNext == 0) {
+            skipPrefix();
+        }
+
+        if (mNext >= len) {
+            throw new ATParseEx();
+        }
+
+        try {
+            // \s*("([^"]*)"|(.*)\s*)(,|$)
+
+            char c = mLine.charAt(mNext++);
+            boolean hasQuote = false;
+
+            c = skipWhiteSpace(c);
+
+            if (c == '"') {
+                if (mNext >= len) {
+                    throw new ATParseEx();
+                }
+                c = mLine.charAt(mNext++);
+                mTokStart = mNext - 1;
+                while (c != '"' && mNext < len) {
+                    c = mLine.charAt(mNext++);
+                }
+                if (c != '"') {
+                    throw new ATParseEx();
+                }
+                mTokEnd = mNext - 1;
+                if (mNext < len && mLine.charAt(mNext++) != ',') {
+                    throw new ATParseEx();
+                }
+            } else {
+                mTokStart = mNext - 1;
+                mTokEnd = mTokStart;
+                while (c != ',') {
+                    if (!Character.isWhitespace(c)) {
+                        mTokEnd = mNext;
+                    }
+                    if (mNext == len) {
+                        break;
+                    }
+                    c = mLine.charAt(mNext++);
+                }
+            }
+        } catch (StringIndexOutOfBoundsException ex) {
+            throw new ATParseEx();
+        }
+    }
+
+
+    /** Throws ATParseEx if whitespace extends to the end of string */
+    private char
+    skipWhiteSpace (char c)
+    {
+        int len;
+        len = mLine.length();
+        while (mNext < len && Character.isWhitespace(c)) {
+            c = mLine.charAt(mNext++);
+        }
+
+        if (Character.isWhitespace(c)) {
+            throw new ATParseEx();
+        }
+        return c;
+    }
+
+
+    private void
+    skipPrefix()
+    {
+        // consume "^[^:]:"
+
+        mNext = 0;
+        int s = mLine.length();
+        while (mNext < s){
+            char c = mLine.charAt(mNext++);
+
+            if (c == ':') {
+                return;
+            }
+        }
+
+        throw new ATParseEx("missing prefix");
+    }
+
+}
diff --git a/com/android/internal/telephony/AppSmsManager.java b/com/android/internal/telephony/AppSmsManager.java
new file mode 100644
index 0000000..11e7f10
--- /dev/null
+++ b/com/android/internal/telephony/AppSmsManager.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.SmsMessage;
+import android.util.ArrayMap;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.security.SecureRandom;
+import java.util.Map;
+
+
+/**
+ *  Manager for app specific incoming SMS requests. This can be used to implement SMS based
+ *  communication channels (e.g. for SMS based phone number verification) without needing the
+ *  {@link Manifest.permission#RECEIVE_SMS} permission.
+ *
+ *  {@link #createAppSpecificSmsRequest} allows an application to provide a {@link PendingIntent}
+ *  that is triggered when an incoming SMS is received that contains the provided token.
+ */
+public class AppSmsManager {
+    private static final String LOG_TAG = "AppSmsManager";
+
+    private final SecureRandom mRandom;
+    private final Context mContext;
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final Map<String, AppRequestInfo> mTokenMap;
+    @GuardedBy("mLock")
+    private final Map<String, AppRequestInfo> mPackageMap;
+
+    public AppSmsManager(Context context) {
+        mRandom = new SecureRandom();
+        mTokenMap = new ArrayMap<>();
+        mPackageMap = new ArrayMap<>();
+        mContext = context;
+    }
+
+    /**
+     * Create an app specific incoming SMS request for the the calling package.
+     *
+     * This method returns a token that if included in a subsequent incoming SMS message the
+     * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and
+     * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission.
+     *
+     * An app can only have one request at a time, if the app already has a request it will be
+     * dropped and the new one will be added.
+     *
+     * @return Token to include in an SMS to have it delivered directly to the app.
+     */
+    public String createAppSpecificSmsToken(String callingPkg, PendingIntent intent) {
+        // Check calling uid matches callingpkg.
+        AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+        appOps.checkPackage(Binder.getCallingUid(), callingPkg);
+
+        // Generate a nonce to store the request under.
+        String token = generateNonce();
+        synchronized (mLock) {
+            // Only allow one request in flight from a package.
+            if (mPackageMap.containsKey(callingPkg)) {
+                removeRequestLocked(mPackageMap.get(callingPkg));
+            }
+            // Store state.
+            AppRequestInfo info = new AppRequestInfo(callingPkg, intent, token);
+            addRequestLocked(info);
+        }
+        return token;
+    }
+
+    /**
+     * Handle an incoming SMS_DELIVER_ACTION intent if it is an app-only SMS.
+     */
+    public boolean handleSmsReceivedIntent(Intent intent) {
+        // Sanity check the action.
+        if (intent.getAction() != Intents.SMS_DELIVER_ACTION) {
+            Log.wtf(LOG_TAG, "Got intent with incorrect action: " + intent.getAction());
+            return false;
+        }
+
+        synchronized (mLock) {
+            AppRequestInfo info = findAppRequestInfoSmsIntentLocked(intent);
+            if (info == null) {
+                // The message didn't contain a token -- nothing to do.
+                return false;
+            }
+            try {
+                Intent fillIn = new Intent();
+                fillIn.putExtras(intent.getExtras());
+                info.pendingIntent.send(mContext, 0, fillIn);
+            } catch (PendingIntent.CanceledException e) {
+                // The pending intent is canceled, send this SMS as normal.
+                removeRequestLocked(info);
+                return false;
+            }
+
+            removeRequestLocked(info);
+            return true;
+        }
+    }
+
+    private AppRequestInfo findAppRequestInfoSmsIntentLocked(Intent intent) {
+        SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
+        if (messages == null) {
+            return null;
+        }
+        StringBuilder fullMessageBuilder = new StringBuilder();
+        for (SmsMessage message : messages) {
+            if (message == null || message.getMessageBody() == null) {
+                continue;
+            }
+            fullMessageBuilder.append(message.getMessageBody());
+        }
+
+        String fullMessage = fullMessageBuilder.toString();
+
+        // Look for any tokens in the full message.
+        for (String token : mTokenMap.keySet()) {
+            if (fullMessage.contains(token)) {
+                return mTokenMap.get(token);
+            }
+        }
+        return null;
+    }
+
+    private String generateNonce() {
+        byte[] bytes = new byte[8];
+        mRandom.nextBytes(bytes);
+        return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
+    }
+
+    private void removeRequestLocked(AppRequestInfo info) {
+        mTokenMap.remove(info.token);
+        mPackageMap.remove(info.packageName);
+    }
+
+    private void addRequestLocked(AppRequestInfo info) {
+        mTokenMap.put(info.token, info);
+        mPackageMap.put(info.packageName, info);
+    }
+
+    private final class AppRequestInfo {
+        public final String packageName;
+        public final PendingIntent pendingIntent;
+        public final String token;
+
+        AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) {
+            this.packageName = packageName;
+            this.pendingIntent = pendingIntent;
+            this.token = token;
+        }
+    }
+
+}
diff --git a/com/android/internal/telephony/AsyncEmergencyContactNotifier.java b/com/android/internal/telephony/AsyncEmergencyContactNotifier.java
new file mode 100644
index 0000000..820a052
--- /dev/null
+++ b/com/android/internal/telephony/AsyncEmergencyContactNotifier.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.provider.BlockedNumberContract;
+import android.telephony.Rlog;
+
+/**
+ * An {@link AsyncTask} that notifies the Blocked number provider that emergency services were
+ * contacted. See {@link BlockedNumberContract.SystemContract#notifyEmergencyContact(Context)}
+ * for details.
+ * {@hide}
+ */
+public class AsyncEmergencyContactNotifier extends AsyncTask<Void, Void, Void> {
+    private static final String TAG = "AsyncEmergencyContactNotifier";
+
+    private final Context mContext;
+
+    public AsyncEmergencyContactNotifier(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    protected Void doInBackground(Void... params) {
+        try {
+            BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext);
+        } catch (Exception e) {
+            Rlog.e(TAG, "Exception notifying emergency contact: " + e);
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/BaseCommands.java b/com/android/internal/telephony/BaseCommands.java
new file mode 100644
index 0000000..137b2a7
--- /dev/null
+++ b/com/android/internal/telephony/BaseCommands.java
@@ -0,0 +1,942 @@
+
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.telephony.TelephonyManager;
+
+/**
+ * {@hide}
+ */
+public abstract class BaseCommands implements CommandsInterface {
+    //***** Instance Variables
+    protected Context mContext;
+    protected RadioState mState = RadioState.RADIO_UNAVAILABLE;
+    protected Object mStateMonitor = new Object();
+
+    protected RegistrantList mRadioStateChangedRegistrants = new RegistrantList();
+    protected RegistrantList mOnRegistrants = new RegistrantList();
+    protected RegistrantList mAvailRegistrants = new RegistrantList();
+    protected RegistrantList mOffOrNotAvailRegistrants = new RegistrantList();
+    protected RegistrantList mNotAvailRegistrants = new RegistrantList();
+    protected RegistrantList mCallStateRegistrants = new RegistrantList();
+    protected RegistrantList mNetworkStateRegistrants = new RegistrantList();
+    protected RegistrantList mDataCallListChangedRegistrants = new RegistrantList();
+    protected RegistrantList mVoiceRadioTechChangedRegistrants = new RegistrantList();
+    protected RegistrantList mImsNetworkStateChangedRegistrants = new RegistrantList();
+    protected RegistrantList mIccStatusChangedRegistrants = new RegistrantList();
+    protected RegistrantList mVoicePrivacyOnRegistrants = new RegistrantList();
+    protected RegistrantList mVoicePrivacyOffRegistrants = new RegistrantList();
+    protected Registrant mUnsolOemHookRawRegistrant;
+    protected RegistrantList mOtaProvisionRegistrants = new RegistrantList();
+    protected RegistrantList mCallWaitingInfoRegistrants = new RegistrantList();
+    protected RegistrantList mDisplayInfoRegistrants = new RegistrantList();
+    protected RegistrantList mSignalInfoRegistrants = new RegistrantList();
+    protected RegistrantList mNumberInfoRegistrants = new RegistrantList();
+    protected RegistrantList mRedirNumInfoRegistrants = new RegistrantList();
+    protected RegistrantList mLineControlInfoRegistrants = new RegistrantList();
+    protected RegistrantList mT53ClirInfoRegistrants = new RegistrantList();
+    protected RegistrantList mT53AudCntrlInfoRegistrants = new RegistrantList();
+    protected RegistrantList mRingbackToneRegistrants = new RegistrantList();
+    protected RegistrantList mResendIncallMuteRegistrants = new RegistrantList();
+    protected RegistrantList mCdmaSubscriptionChangedRegistrants = new RegistrantList();
+    protected RegistrantList mCdmaPrlChangedRegistrants = new RegistrantList();
+    protected RegistrantList mExitEmergencyCallbackModeRegistrants = new RegistrantList();
+    protected RegistrantList mRilConnectedRegistrants = new RegistrantList();
+    protected RegistrantList mIccRefreshRegistrants = new RegistrantList();
+    protected RegistrantList mRilCellInfoListRegistrants = new RegistrantList();
+    protected RegistrantList mSubscriptionStatusRegistrants = new RegistrantList();
+    protected RegistrantList mSrvccStateRegistrants = new RegistrantList();
+    protected RegistrantList mHardwareConfigChangeRegistrants = new RegistrantList();
+    protected RegistrantList mPhoneRadioCapabilityChangedRegistrants =
+            new RegistrantList();
+    protected RegistrantList mPcoDataRegistrants = new RegistrantList();
+    protected RegistrantList mCarrierInfoForImsiEncryptionRegistrants = new RegistrantList();
+    protected RegistrantList mRilNetworkScanResultRegistrants = new RegistrantList();
+    protected RegistrantList mModemResetRegistrants = new RegistrantList();
+
+
+    protected Registrant mGsmSmsRegistrant;
+    protected Registrant mCdmaSmsRegistrant;
+    protected Registrant mNITZTimeRegistrant;
+    protected Registrant mSignalStrengthRegistrant;
+    protected Registrant mUSSDRegistrant;
+    protected Registrant mSmsOnSimRegistrant;
+    protected Registrant mSmsStatusRegistrant;
+    protected Registrant mSsnRegistrant;
+    protected Registrant mCatSessionEndRegistrant;
+    protected Registrant mCatProCmdRegistrant;
+    protected Registrant mCatEventRegistrant;
+    protected Registrant mCatCallSetUpRegistrant;
+    protected Registrant mIccSmsFullRegistrant;
+    protected Registrant mEmergencyCallbackModeRegistrant;
+    protected Registrant mRingRegistrant;
+    protected Registrant mRestrictedStateRegistrant;
+    protected Registrant mGsmBroadcastSmsRegistrant;
+    protected Registrant mCatCcAlphaRegistrant;
+    protected Registrant mSsRegistrant;
+    protected Registrant mLceInfoRegistrant;
+
+    // Preferred network type received from PhoneFactory.
+    // This is used when establishing a connection to the
+    // vendor ril so it starts up in the correct mode.
+    protected int mPreferredNetworkType;
+    // CDMA subscription received from PhoneFactory
+    protected int mCdmaSubscription;
+    // Type of Phone, GSM or CDMA. Set by GsmCdmaPhone.
+    protected int mPhoneType;
+    // RIL Version
+    protected int mRilVersion = -1;
+
+    public BaseCommands(Context context) {
+        mContext = context;  // May be null (if so we won't log statistics)
+    }
+
+    //***** CommandsInterface implementation
+
+    @Override
+    public RadioState getRadioState() {
+        return mState;
+    }
+
+    @Override
+    public void registerForRadioStateChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        synchronized (mStateMonitor) {
+            mRadioStateChangedRegistrants.add(r);
+            r.notifyRegistrant();
+        }
+    }
+
+    @Override
+    public void unregisterForRadioStateChanged(Handler h) {
+        synchronized (mStateMonitor) {
+            mRadioStateChangedRegistrants.remove(h);
+        }
+    }
+
+    public void registerForImsNetworkStateChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mImsNetworkStateChangedRegistrants.add(r);
+    }
+
+    public void unregisterForImsNetworkStateChanged(Handler h) {
+        mImsNetworkStateChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForOn(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        synchronized (mStateMonitor) {
+            mOnRegistrants.add(r);
+
+            if (mState.isOn()) {
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+    @Override
+    public void unregisterForOn(Handler h) {
+        synchronized (mStateMonitor) {
+            mOnRegistrants.remove(h);
+        }
+    }
+
+
+    @Override
+    public void registerForAvailable(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        synchronized (mStateMonitor) {
+            mAvailRegistrants.add(r);
+
+            if (mState.isAvailable()) {
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+
+    @Override
+    public void unregisterForAvailable(Handler h) {
+        synchronized(mStateMonitor) {
+            mAvailRegistrants.remove(h);
+        }
+    }
+
+    @Override
+    public void registerForNotAvailable(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        synchronized (mStateMonitor) {
+            mNotAvailRegistrants.add(r);
+
+            if (!mState.isAvailable()) {
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+
+    @Override
+    public void unregisterForNotAvailable(Handler h) {
+        synchronized (mStateMonitor) {
+            mNotAvailRegistrants.remove(h);
+        }
+    }
+
+    @Override
+    public void registerForOffOrNotAvailable(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        synchronized (mStateMonitor) {
+            mOffOrNotAvailRegistrants.add(r);
+
+            if (mState == RadioState.RADIO_OFF || !mState.isAvailable()) {
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+    @Override
+    public void unregisterForOffOrNotAvailable(Handler h) {
+        synchronized(mStateMonitor) {
+            mOffOrNotAvailRegistrants.remove(h);
+        }
+    }
+
+    @Override
+    public void registerForCallStateChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        mCallStateRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForCallStateChanged(Handler h) {
+        mCallStateRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForNetworkStateChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        mNetworkStateRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForNetworkStateChanged(Handler h) {
+        mNetworkStateRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForDataCallListChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        mDataCallListChangedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForDataCallListChanged(Handler h) {
+        mDataCallListChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForVoiceRadioTechChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mVoiceRadioTechChangedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForVoiceRadioTechChanged(Handler h) {
+        mVoiceRadioTechChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForIccStatusChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mIccStatusChangedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForIccStatusChanged(Handler h) {
+        mIccStatusChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void setOnNewGsmSms(Handler h, int what, Object obj) {
+        mGsmSmsRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnNewGsmSms(Handler h) {
+        if (mGsmSmsRegistrant != null && mGsmSmsRegistrant.getHandler() == h) {
+            mGsmSmsRegistrant.clear();
+            mGsmSmsRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnNewCdmaSms(Handler h, int what, Object obj) {
+        mCdmaSmsRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnNewCdmaSms(Handler h) {
+        if (mCdmaSmsRegistrant != null && mCdmaSmsRegistrant.getHandler() == h) {
+            mCdmaSmsRegistrant.clear();
+            mCdmaSmsRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnNewGsmBroadcastSms(Handler h, int what, Object obj) {
+        mGsmBroadcastSmsRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnNewGsmBroadcastSms(Handler h) {
+        if (mGsmBroadcastSmsRegistrant != null && mGsmBroadcastSmsRegistrant.getHandler() == h) {
+            mGsmBroadcastSmsRegistrant.clear();
+            mGsmBroadcastSmsRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnSmsOnSim(Handler h, int what, Object obj) {
+        mSmsOnSimRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnSmsOnSim(Handler h) {
+        if (mSmsOnSimRegistrant != null && mSmsOnSimRegistrant.getHandler() == h) {
+            mSmsOnSimRegistrant.clear();
+            mSmsOnSimRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnSmsStatus(Handler h, int what, Object obj) {
+        mSmsStatusRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnSmsStatus(Handler h) {
+        if (mSmsStatusRegistrant != null && mSmsStatusRegistrant.getHandler() == h) {
+            mSmsStatusRegistrant.clear();
+            mSmsStatusRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnSignalStrengthUpdate(Handler h, int what, Object obj) {
+        mSignalStrengthRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnSignalStrengthUpdate(Handler h) {
+        if (mSignalStrengthRegistrant != null && mSignalStrengthRegistrant.getHandler() == h) {
+            mSignalStrengthRegistrant.clear();
+            mSignalStrengthRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnNITZTime(Handler h, int what, Object obj) {
+        mNITZTimeRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnNITZTime(Handler h) {
+        if (mNITZTimeRegistrant != null && mNITZTimeRegistrant.getHandler() == h) {
+            mNITZTimeRegistrant.clear();
+            mNITZTimeRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnUSSD(Handler h, int what, Object obj) {
+        mUSSDRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnUSSD(Handler h) {
+        if (mUSSDRegistrant != null && mUSSDRegistrant.getHandler() == h) {
+            mUSSDRegistrant.clear();
+            mUSSDRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnSuppServiceNotification(Handler h, int what, Object obj) {
+        mSsnRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnSuppServiceNotification(Handler h) {
+        if (mSsnRegistrant != null && mSsnRegistrant.getHandler() == h) {
+            mSsnRegistrant.clear();
+            mSsnRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnCatSessionEnd(Handler h, int what, Object obj) {
+        mCatSessionEndRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnCatSessionEnd(Handler h) {
+        if (mCatSessionEndRegistrant != null && mCatSessionEndRegistrant.getHandler() == h) {
+            mCatSessionEndRegistrant.clear();
+            mCatSessionEndRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnCatProactiveCmd(Handler h, int what, Object obj) {
+        mCatProCmdRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnCatProactiveCmd(Handler h) {
+        if (mCatProCmdRegistrant != null && mCatProCmdRegistrant.getHandler() == h) {
+            mCatProCmdRegistrant.clear();
+            mCatProCmdRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnCatEvent(Handler h, int what, Object obj) {
+        mCatEventRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnCatEvent(Handler h) {
+        if (mCatEventRegistrant != null && mCatEventRegistrant.getHandler() == h) {
+            mCatEventRegistrant.clear();
+            mCatEventRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnCatCallSetUp(Handler h, int what, Object obj) {
+        mCatCallSetUpRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnCatCallSetUp(Handler h) {
+        if (mCatCallSetUpRegistrant != null && mCatCallSetUpRegistrant.getHandler() == h) {
+            mCatCallSetUpRegistrant.clear();
+            mCatCallSetUpRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnIccSmsFull(Handler h, int what, Object obj) {
+        mIccSmsFullRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnIccSmsFull(Handler h) {
+        if (mIccSmsFullRegistrant != null && mIccSmsFullRegistrant.getHandler() == h) {
+            mIccSmsFullRegistrant.clear();
+            mIccSmsFullRegistrant = null;
+        }
+    }
+
+    @Override
+    public void registerForIccRefresh(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mIccRefreshRegistrants.add(r);
+    }
+    @Override
+    public void setOnIccRefresh(Handler h, int what, Object obj) {
+        registerForIccRefresh(h, what, obj);
+    }
+
+    @Override
+    public void setEmergencyCallbackMode(Handler h, int what, Object obj) {
+        mEmergencyCallbackModeRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unregisterForIccRefresh(Handler h) {
+        mIccRefreshRegistrants.remove(h);
+    }
+    @Override
+    public void unsetOnIccRefresh(Handler h) {
+        unregisterForIccRefresh(h);
+    }
+
+    @Override
+    public void setOnCallRing(Handler h, int what, Object obj) {
+        mRingRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnCallRing(Handler h) {
+        if (mRingRegistrant != null && mRingRegistrant.getHandler() == h) {
+            mRingRegistrant.clear();
+            mRingRegistrant = null;
+        }
+    }
+
+    @Override
+    public void setOnSs(Handler h, int what, Object obj) {
+        mSsRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnSs(Handler h) {
+        mSsRegistrant.clear();
+    }
+
+    @Override
+    public void setOnCatCcAlphaNotify(Handler h, int what, Object obj) {
+        mCatCcAlphaRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnCatCcAlphaNotify(Handler h) {
+        mCatCcAlphaRegistrant.clear();
+    }
+
+    @Override
+    public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mVoicePrivacyOnRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForInCallVoicePrivacyOn(Handler h){
+        mVoicePrivacyOnRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mVoicePrivacyOffRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForInCallVoicePrivacyOff(Handler h){
+        mVoicePrivacyOffRegistrants.remove(h);
+    }
+
+    @Override
+    public void setOnRestrictedStateChanged(Handler h, int what, Object obj) {
+        mRestrictedStateRegistrant = new Registrant (h, what, obj);
+    }
+
+    @Override
+    public void unSetOnRestrictedStateChanged(Handler h) {
+        if (mRestrictedStateRegistrant != null && mRestrictedStateRegistrant.getHandler() == h) {
+            mRestrictedStateRegistrant.clear();
+            mRestrictedStateRegistrant = null;
+        }
+    }
+
+    @Override
+    public void registerForDisplayInfo(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mDisplayInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForDisplayInfo(Handler h) {
+        mDisplayInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForCallWaitingInfo(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mCallWaitingInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForCallWaitingInfo(Handler h) {
+        mCallWaitingInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForSignalInfo(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mSignalInfoRegistrants.add(r);
+    }
+
+    public void setOnUnsolOemHookRaw(Handler h, int what, Object obj) {
+        mUnsolOemHookRawRegistrant = new Registrant (h, what, obj);
+    }
+
+    public void unSetOnUnsolOemHookRaw(Handler h) {
+        if (mUnsolOemHookRawRegistrant != null && mUnsolOemHookRawRegistrant.getHandler() == h) {
+            mUnsolOemHookRawRegistrant.clear();
+            mUnsolOemHookRawRegistrant = null;
+        }
+    }
+
+    @Override
+    public void unregisterForSignalInfo(Handler h) {
+        mSignalInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForCdmaOtaProvision(Handler h,int what, Object obj){
+        Registrant r = new Registrant (h, what, obj);
+        mOtaProvisionRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForCdmaOtaProvision(Handler h){
+        mOtaProvisionRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForNumberInfo(Handler h,int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mNumberInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForNumberInfo(Handler h){
+        mNumberInfoRegistrants.remove(h);
+    }
+
+     @Override
+    public void registerForRedirectedNumberInfo(Handler h,int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mRedirNumInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForRedirectedNumberInfo(Handler h) {
+        mRedirNumInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForLineControlInfo(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mLineControlInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForLineControlInfo(Handler h) {
+        mLineControlInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerFoT53ClirlInfo(Handler h,int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mT53ClirInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForT53ClirInfo(Handler h) {
+        mT53ClirInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForT53AudioControlInfo(Handler h,int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mT53AudCntrlInfoRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForT53AudioControlInfo(Handler h) {
+        mT53AudCntrlInfoRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForRingbackTone(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mRingbackToneRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForRingbackTone(Handler h) {
+        mRingbackToneRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForResendIncallMute(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mResendIncallMuteRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForResendIncallMute(Handler h) {
+        mResendIncallMuteRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForCdmaSubscriptionChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mCdmaSubscriptionChangedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForCdmaSubscriptionChanged(Handler h) {
+        mCdmaSubscriptionChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForCdmaPrlChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mCdmaPrlChangedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForCdmaPrlChanged(Handler h) {
+        mCdmaPrlChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mExitEmergencyCallbackModeRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForExitEmergencyCallbackMode(Handler h) {
+        mExitEmergencyCallbackModeRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForHardwareConfigChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mHardwareConfigChangeRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForHardwareConfigChanged(Handler h) {
+        mHardwareConfigChangeRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForNetworkScanResult(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mRilNetworkScanResultRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForNetworkScanResult(Handler h) {
+        mRilNetworkScanResultRegistrants.remove(h);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void registerForRilConnected(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mRilConnectedRegistrants.add(r);
+        if (mRilVersion != -1) {
+            r.notifyRegistrant(new AsyncResult(null, new Integer(mRilVersion), null));
+        }
+    }
+
+    @Override
+    public void unregisterForRilConnected(Handler h) {
+        mRilConnectedRegistrants.remove(h);
+    }
+
+    public void registerForSubscriptionStatusChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mSubscriptionStatusRegistrants.add(r);
+    }
+
+    public void unregisterForSubscriptionStatusChanged(Handler h) {
+        mSubscriptionStatusRegistrants.remove(h);
+    }
+
+    //***** Protected Methods
+    /**
+     * Store new RadioState and send notification based on the changes
+     *
+     * This function is called only by RIL.java when receiving unsolicited
+     * RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED
+     *
+     * RadioState has 3 values : RADIO_OFF, RADIO_UNAVAILABLE, RADIO_ON.
+     *
+     * @param newState new RadioState decoded from RIL_UNSOL_RADIO_STATE_CHANGED
+     */
+    protected void setRadioState(RadioState newState) {
+        RadioState oldState;
+
+        synchronized (mStateMonitor) {
+            oldState = mState;
+            mState = newState;
+
+            if (oldState == mState) {
+                // no state transition
+                return;
+            }
+
+            mRadioStateChangedRegistrants.notifyRegistrants();
+
+            if (mState.isAvailable() && !oldState.isAvailable()) {
+                mAvailRegistrants.notifyRegistrants();
+            }
+
+            if (!mState.isAvailable() && oldState.isAvailable()) {
+                mNotAvailRegistrants.notifyRegistrants();
+            }
+
+            if (mState.isOn() && !oldState.isOn()) {
+                mOnRegistrants.notifyRegistrants();
+            }
+
+            if ((!mState.isOn() || !mState.isAvailable())
+                && !((!oldState.isOn() || !oldState.isAvailable()))
+            ) {
+                mOffOrNotAvailRegistrants.notifyRegistrants();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getLteOnCdmaMode() {
+        return TelephonyManager.getLteOnCdmaModeStatic();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void registerForCellInfoList(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mRilCellInfoListRegistrants.add(r);
+    }
+    @Override
+    public void unregisterForCellInfoList(Handler h) {
+        mRilCellInfoListRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForSrvccStateChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+
+        mSrvccStateRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForSrvccStateChanged(Handler h) {
+        mSrvccStateRegistrants.remove(h);
+    }
+
+    @Override
+    public void testingEmergencyCall() {}
+
+    @Override
+    public int getRilVersion() {
+        return mRilVersion;
+    }
+
+    public void setUiccSubscription(int slotId, int appIndex, int subId, int subStatus,
+            Message response) {
+    }
+
+    public void setDataAllowed(boolean allowed, Message response) {
+    }
+
+    @Override
+    public void requestShutdown(Message result) {
+    }
+
+    @Override
+    public void getRadioCapability(Message result) {
+    }
+
+    @Override
+    public void setRadioCapability(RadioCapability rc, Message response) {
+    }
+
+    @Override
+    public void registerForRadioCapabilityChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mPhoneRadioCapabilityChangedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForRadioCapabilityChanged(Handler h) {
+        mPhoneRadioCapabilityChangedRegistrants.remove(h);
+    }
+
+    @Override
+    public void startLceService(int reportIntervalMs, boolean pullMode, Message result) {
+    }
+
+    @Override
+    public void stopLceService(Message result) {
+    }
+
+    @Override
+    public void pullLceData(Message result) {
+    }
+
+    @Override
+    public void registerForLceInfo(Handler h, int what, Object obj) {
+      mLceInfoRegistrant = new Registrant(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForLceInfo(Handler h) {
+      if (mLceInfoRegistrant != null && mLceInfoRegistrant.getHandler() == h) {
+          mLceInfoRegistrant.clear();
+          mLceInfoRegistrant = null;
+      }
+    }
+
+    @Override
+    public void registerForModemReset(Handler h, int what, Object obj) {
+        mModemResetRegistrants.add(new Registrant(h, what, obj));
+    }
+
+    @Override
+    public void unregisterForModemReset(Handler h) {
+        mModemResetRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForPcoData(Handler h, int what, Object obj) {
+        mPcoDataRegistrants.add(new Registrant(h, what, obj));
+    }
+
+    @Override
+    public void unregisterForPcoData(Handler h) {
+        mPcoDataRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForCarrierInfoForImsiEncryption(Handler h, int what, Object obj) {
+        mCarrierInfoForImsiEncryptionRegistrants.add(new Registrant(h, what, obj));
+    }
+
+    @Override
+    public void unregisterForCarrierInfoForImsiEncryption(Handler h) {
+        mCarrierInfoForImsiEncryptionRegistrants.remove(h);
+    }
+}
diff --git a/com/android/internal/telephony/BlockChecker.java b/com/android/internal/telephony/BlockChecker.java
new file mode 100644
index 0000000..dcbeea0
--- /dev/null
+++ b/com/android/internal/telephony/BlockChecker.java
@@ -0,0 +1,40 @@
+package com.android.internal.telephony;
+
+import android.content.Context;
+import android.provider.BlockedNumberContract;
+import android.telephony.Rlog;
+
+/**
+ * {@hide} Checks for blocked phone numbers against {@link BlockedNumberContract}
+ */
+public class BlockChecker {
+    private static final String TAG = "BlockChecker";
+    private static final boolean VDBG = false; // STOPSHIP if true.
+
+    /**
+     * Returns {@code true} if {@code phoneNumber} is blocked.
+     * <p>
+     * This method catches all underlying exceptions to ensure that this method never throws any
+     * exception.
+     */
+    public static boolean isBlocked(Context context, String phoneNumber) {
+        boolean isBlocked = false;
+        long startTimeNano = System.nanoTime();
+
+        try {
+            if (BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
+                    context, phoneNumber)) {
+                Rlog.d(TAG, phoneNumber + " is blocked.");
+                isBlocked = true;
+            }
+        } catch (Exception e) {
+            Rlog.e(TAG, "Exception checking for blocked number: " + e);
+        }
+
+        int durationMillis = (int) ((System.nanoTime() - startTimeNano) / 1000000);
+        if (durationMillis > 500 || VDBG) {
+            Rlog.d(TAG, "Blocked number lookup took: " + durationMillis + " ms.");
+        }
+        return isBlocked;
+    }
+}
diff --git a/com/android/internal/telephony/Call.java b/com/android/internal/telephony/Call.java
new file mode 100644
index 0000000..775ef46
--- /dev/null
+++ b/com/android/internal/telephony/Call.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.telecom.ConferenceParticipant;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.telephony.Rlog;
+
+/**
+ * {@hide}
+ */
+public abstract class Call {
+    protected final String LOG_TAG = "Call";
+
+    /* Enums */
+
+    public enum State {
+        IDLE, ACTIVE, HOLDING, DIALING, ALERTING, INCOMING, WAITING, DISCONNECTED, DISCONNECTING;
+
+        public boolean isAlive() {
+            return !(this == IDLE || this == DISCONNECTED || this == DISCONNECTING);
+        }
+
+        public boolean isRinging() {
+            return this == INCOMING || this == WAITING;
+        }
+
+        public boolean isDialing() {
+            return this == DIALING || this == ALERTING;
+        }
+    }
+
+    public static State
+    stateFromDCState (DriverCall.State dcState) {
+        switch (dcState) {
+            case ACTIVE:        return State.ACTIVE;
+            case HOLDING:       return State.HOLDING;
+            case DIALING:       return State.DIALING;
+            case ALERTING:      return State.ALERTING;
+            case INCOMING:      return State.INCOMING;
+            case WAITING:       return State.WAITING;
+            default:            throw new RuntimeException ("illegal call state:" + dcState);
+        }
+    }
+
+    public enum SrvccState {
+        NONE, STARTED, COMPLETED, FAILED, CANCELED;
+    }
+
+    /* Instance Variables */
+
+    public State mState = State.IDLE;
+
+    public ArrayList<Connection> mConnections = new ArrayList<Connection>();
+
+    /* Instance Methods */
+
+    /** Do not modify the List result!!! This list is not yours to keep
+     *  It will change across event loop iterations            top
+     */
+
+    public abstract List<Connection> getConnections();
+    public abstract Phone getPhone();
+    public abstract boolean isMultiparty();
+    public abstract void hangup() throws CallStateException;
+
+
+    /**
+     * hasConnection
+     *
+     * @param c a Connection object
+     * @return true if the call contains the connection object passed in
+     */
+    public boolean hasConnection(Connection c) {
+        return c.getCall() == this;
+    }
+
+    /**
+     * hasConnections
+     * @return true if the call contains one or more connections
+     */
+    public boolean hasConnections() {
+        List<Connection> connections = getConnections();
+
+        if (connections == null) {
+            return false;
+        }
+
+        return connections.size() > 0;
+    }
+
+    /**
+     * getState
+     * @return state of class call
+     */
+    public State getState() {
+        return mState;
+    }
+
+    /**
+     * getConferenceParticipants
+     * @return List of conference participants.
+     */
+    public List<ConferenceParticipant> getConferenceParticipants() {
+        return null;
+    }
+
+    /**
+     * isIdle
+     *
+     * FIXME rename
+     * @return true if the call contains only disconnected connections (if any)
+     */
+    public boolean isIdle() {
+        return !getState().isAlive();
+    }
+
+    /**
+     * Returns the Connection associated with this Call that was created
+     * first, or null if there are no Connections in this Call
+     */
+    public Connection
+    getEarliestConnection() {
+        List<Connection> l;
+        long time = Long.MAX_VALUE;
+        Connection c;
+        Connection earliest = null;
+
+        l = getConnections();
+
+        if (l.size() == 0) {
+            return null;
+        }
+
+        for (int i = 0, s = l.size() ; i < s ; i++) {
+            c = l.get(i);
+            long t;
+
+            t = c.getCreateTime();
+
+            if (t < time) {
+                earliest = c;
+                time = t;
+            }
+        }
+
+        return earliest;
+    }
+
+    public long
+    getEarliestCreateTime() {
+        List<Connection> l;
+        long time = Long.MAX_VALUE;
+
+        l = getConnections();
+
+        if (l.size() == 0) {
+            return 0;
+        }
+
+        for (int i = 0, s = l.size() ; i < s ; i++) {
+            Connection c = l.get(i);
+            long t;
+
+            t = c.getCreateTime();
+
+            time = t < time ? t : time;
+        }
+
+        return time;
+    }
+
+    public long
+    getEarliestConnectTime() {
+        long time = Long.MAX_VALUE;
+        List<Connection> l = getConnections();
+
+        if (l.size() == 0) {
+            return 0;
+        }
+
+        for (int i = 0, s = l.size() ; i < s ; i++) {
+            Connection c = l.get(i);
+            long t;
+
+            t = c.getConnectTime();
+
+            time = t < time ? t : time;
+        }
+
+        return time;
+    }
+
+
+    public boolean
+    isDialingOrAlerting() {
+        return getState().isDialing();
+    }
+
+    public boolean
+    isRinging() {
+        return getState().isRinging();
+    }
+
+    /**
+     * Returns the Connection associated with this Call that was created
+     * last, or null if there are no Connections in this Call
+     */
+    public Connection
+    getLatestConnection() {
+        List<Connection> l = getConnections();
+        if (l.size() == 0) {
+            return null;
+        }
+
+        long time = 0;
+        Connection latest = null;
+        for (int i = 0, s = l.size() ; i < s ; i++) {
+            Connection c = l.get(i);
+            long t = c.getCreateTime();
+
+            if (t > time) {
+                latest = c;
+                time = t;
+            }
+        }
+
+        return latest;
+    }
+
+    /**
+     * Hangup call if it is alive
+     */
+    public void hangupIfAlive() {
+        if (getState().isAlive()) {
+            try {
+                hangup();
+            } catch (CallStateException ex) {
+                Rlog.w(LOG_TAG, " hangupIfActive: caught " + ex);
+            }
+        }
+    }
+
+    /**
+     * Called when it's time to clean up disconnected Connection objects
+     */
+    public void clearDisconnected() {
+        for (int i = mConnections.size() - 1 ; i >= 0 ; i--) {
+            Connection c = mConnections.get(i);
+            if (c.getState() == State.DISCONNECTED) {
+                mConnections.remove(i);
+            }
+        }
+
+        if (mConnections.size() == 0) {
+            setState(State.IDLE);
+        }
+    }
+
+    protected void setState(State newState) {
+        mState = newState;
+    }
+}
diff --git a/com/android/internal/telephony/CallFailCause.java b/com/android/internal/telephony/CallFailCause.java
new file mode 100644
index 0000000..ed39b4d
--- /dev/null
+++ b/com/android/internal/telephony/CallFailCause.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * Call fail causes from TS 24.008 .
+ * These are mostly the cause codes we need to distinguish for the UI.
+ * See 22.001 Annex F.4 for mapping of cause codes to local tones.
+ *
+ * CDMA call failure reasons are derived from the possible call failure scenarios described
+ * in "CDMA IS2000 - Release A (C.S0005-A v6.0)" standard.
+ *
+ * {@hide}
+ *
+ */
+public interface CallFailCause {
+    // Unassigned/Unobtainable number
+    int UNOBTAINABLE_NUMBER = 1;
+
+    int OPERATOR_DETERMINED_BARRING = 8;
+    int NORMAL_CLEARING     = 16;
+    // Busy Tone
+    int USER_BUSY           = 17;
+
+    // No Tone
+    int NUMBER_CHANGED      = 22;
+    int STATUS_ENQUIRY      = 30;
+    int NORMAL_UNSPECIFIED  = 31;
+
+    // Congestion Tone
+    int NO_CIRCUIT_AVAIL    = 34;
+    int TEMPORARY_FAILURE   = 41;
+    int SWITCHING_CONGESTION    = 42;
+    int CHANNEL_NOT_AVAIL   = 44;
+    int QOS_NOT_AVAIL       = 49;
+    int BEARER_NOT_AVAIL    = 58;
+
+    // others
+    int ACM_LIMIT_EXCEEDED = 68;
+    int CALL_BARRED        = 240;
+    int FDN_BLOCKED        = 241;
+    int IMEI_NOT_ACCEPTED  = 243;
+
+    // Stk Call Control
+    int DIAL_MODIFIED_TO_USSD = 244;
+    int DIAL_MODIFIED_TO_SS   = 245;
+    int DIAL_MODIFIED_TO_DIAL = 246;
+
+    //Emergency Redial
+    int EMERGENCY_TEMP_FAILURE = 325;
+    int EMERGENCY_PERM_FAILURE = 326;
+
+    int CDMA_LOCKED_UNTIL_POWER_CYCLE  = 1000;
+    int CDMA_DROP                      = 1001;
+    int CDMA_INTERCEPT                 = 1002;
+    int CDMA_REORDER                   = 1003;
+    int CDMA_SO_REJECT                 = 1004;
+    int CDMA_RETRY_ORDER               = 1005;
+    int CDMA_ACCESS_FAILURE            = 1006;
+    int CDMA_PREEMPTED                 = 1007;
+
+    // For non-emergency number dialed while in emergency callback mode.
+    int CDMA_NOT_EMERGENCY             = 1008;
+
+    // Access Blocked by CDMA Network.
+    int CDMA_ACCESS_BLOCKED            = 1009;
+
+    int ERROR_UNSPECIFIED = 0xffff;
+
+}
diff --git a/com/android/internal/telephony/CallForwardInfo.java b/com/android/internal/telephony/CallForwardInfo.java
new file mode 100644
index 0000000..dccf306
--- /dev/null
+++ b/com/android/internal/telephony/CallForwardInfo.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * See also RIL_CallForwardInfo in include/telephony/ril.h
+ *
+ * {@hide}
+ */
+public class CallForwardInfo {
+    public int             status;      /*1 = active, 0 = not active */
+    public int             reason;      /* from TS 27.007 7.11 "reason" */
+    public int             serviceClass; /* Saum of CommandsInterface.SERVICE_CLASS */
+    public int             toa;         /* "type" from TS 27.007 7.11 */
+    public String          number;      /* "number" from TS 27.007 7.11 */
+    public int             timeSeconds; /* for CF no reply only */
+
+    @Override
+    public String toString() {
+        return super.toString() + (status == 0 ? " not active " : " active ")
+            + " reason: " + reason
+            + " serviceClass: " + serviceClass + " " + timeSeconds + " seconds";
+
+    }
+}
diff --git a/com/android/internal/telephony/CallInfo.java b/com/android/internal/telephony/CallInfo.java
new file mode 100644
index 0000000..6bfc9d7
--- /dev/null
+++ b/com/android/internal/telephony/CallInfo.java
@@ -0,0 +1,77 @@
+/*
+** Copyright 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.internal.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ *  A parcelable holder class of Call information data.
+ */
+public class CallInfo implements Parcelable {
+
+    /**
+     * Endpoint to which the call is connected.
+     * This could be the dialed value for outgoing calls or the caller id of incoming calls.
+     */
+    private String handle;
+
+    public CallInfo(String handle) {
+        this.handle = handle;
+    }
+
+    public String getHandle() {
+        return handle;
+    }
+
+    //
+    // Parcelling related code below here.
+    //
+
+    /**
+     * Responsible for creating CallInfo objects for deserialized Parcels.
+     */
+    public static final Parcelable.Creator<CallInfo> CREATOR
+            = new Parcelable.Creator<CallInfo> () {
+
+        @Override
+        public CallInfo createFromParcel(Parcel source) {
+            return new CallInfo(source.readString());
+        }
+
+        @Override
+        public CallInfo[] newArray(int size) {
+            return new CallInfo[size];
+        }
+    };
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Writes CallInfo object into a serializeable Parcel.
+     */
+    @Override
+    public void writeToParcel(Parcel destination, int flags) {
+        destination.writeString(handle);
+    }
+}
diff --git a/com/android/internal/telephony/CallManager.java b/com/android/internal/telephony/CallManager.java
new file mode 100644
index 0000000..2775fe6
--- /dev/null
+++ b/com/android/internal/telephony/CallManager.java
@@ -0,0 +1,2419 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony;
+
+import com.android.internal.telephony.sip.SipPhone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RegistrantList;
+import android.os.Registrant;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+
+
+/**
+ * @hide
+ *
+ * CallManager class provides an abstract layer for PhoneApp to access
+ * and control calls. It implements Phone interface.
+ *
+ * CallManager provides call and connection control as well as
+ * channel capability.
+ *
+ * There are three categories of APIs CallManager provided
+ *
+ *  1. Call control and operation, such as dial() and hangup()
+ *  2. Channel capabilities, such as CanConference()
+ *  3. Register notification
+ *
+ *
+ */
+public class CallManager {
+
+    private static final String LOG_TAG ="CallManager";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    private static final int EVENT_DISCONNECT = 100;
+    private static final int EVENT_PRECISE_CALL_STATE_CHANGED = 101;
+    private static final int EVENT_NEW_RINGING_CONNECTION = 102;
+    private static final int EVENT_UNKNOWN_CONNECTION = 103;
+    private static final int EVENT_INCOMING_RING = 104;
+    private static final int EVENT_RINGBACK_TONE = 105;
+    private static final int EVENT_IN_CALL_VOICE_PRIVACY_ON = 106;
+    private static final int EVENT_IN_CALL_VOICE_PRIVACY_OFF = 107;
+    private static final int EVENT_CALL_WAITING = 108;
+    private static final int EVENT_DISPLAY_INFO = 109;
+    private static final int EVENT_SIGNAL_INFO = 110;
+    private static final int EVENT_CDMA_OTA_STATUS_CHANGE = 111;
+    private static final int EVENT_RESEND_INCALL_MUTE = 112;
+    private static final int EVENT_MMI_INITIATE = 113;
+    private static final int EVENT_MMI_COMPLETE = 114;
+    private static final int EVENT_ECM_TIMER_RESET = 115;
+    private static final int EVENT_SUBSCRIPTION_INFO_READY = 116;
+    private static final int EVENT_SUPP_SERVICE_FAILED = 117;
+    private static final int EVENT_SERVICE_STATE_CHANGED = 118;
+    private static final int EVENT_POST_DIAL_CHARACTER = 119;
+    private static final int EVENT_ONHOLD_TONE = 120;
+    // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+    //private static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE = 121;
+    private static final int EVENT_TTY_MODE_RECEIVED = 122;
+
+    // Singleton instance
+    private static final CallManager INSTANCE = new CallManager();
+
+    // list of registered phones, which are Phone objs
+    private final ArrayList<Phone> mPhones;
+
+    // list of supported ringing calls
+    private final ArrayList<Call> mRingingCalls;
+
+    // list of supported background calls
+    private final ArrayList<Call> mBackgroundCalls;
+
+    // list of supported foreground calls
+    private final ArrayList<Call> mForegroundCalls;
+
+    // empty connection list
+    private final ArrayList<Connection> mEmptyConnections = new ArrayList<Connection>();
+
+    // mapping of phones to registered handler instances used for callbacks from RIL
+    private final HashMap<Phone, CallManagerHandler> mHandlerMap = new HashMap<>();
+
+    // default phone as the first phone registered, which is Phone obj
+    private Phone mDefaultPhone;
+
+    private boolean mSpeedUpAudioForMtCall = false;
+    // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+    //private boolean mIsEccDialing = false;
+
+    private Object mRegistrantidentifier = new Object();
+
+    // state registrants
+    protected final RegistrantList mPreciseCallStateRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mNewRingingConnectionRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mIncomingRingRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mDisconnectRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mMmiRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mUnknownConnectionRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mRingbackToneRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mOnHoldToneRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mInCallVoicePrivacyOnRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mInCallVoicePrivacyOffRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mCallWaitingRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mDisplayInfoRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mSignalInfoRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mCdmaOtaStatusChangeRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mResendIncallMuteRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mMmiInitiateRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mMmiCompleteRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mEcmTimerResetRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mSubscriptionInfoReadyRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mSuppServiceFailedRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mServiceStateChangedRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mPostDialCharacterRegistrants
+    = new RegistrantList();
+
+    protected final RegistrantList mTtyModeReceivedRegistrants
+    = new RegistrantList();
+
+    private CallManager() {
+        mPhones = new ArrayList<Phone>();
+        mRingingCalls = new ArrayList<Call>();
+        mBackgroundCalls = new ArrayList<Call>();
+        mForegroundCalls = new ArrayList<Call>();
+        mDefaultPhone = null;
+    }
+
+    /**
+     * get singleton instance of CallManager
+     * @return CallManager
+     */
+    public static CallManager getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Returns all the registered phone objects.
+     * @return all the registered phone objects.
+     */
+    public List<Phone> getAllPhones() {
+        return Collections.unmodifiableList(mPhones);
+    }
+
+    /**
+     * get Phone object corresponds to subId
+     * @return Phone
+     */
+    private Phone getPhone(int subId) {
+        Phone p = null;
+        for (Phone phone : mPhones) {
+            if (phone.getSubId() == subId &&
+                    phone.getPhoneType() != PhoneConstants.PHONE_TYPE_IMS) {
+                p = phone;
+                break;
+            }
+        }
+        return p;
+    }
+
+    /**
+     * Get current coarse-grained voice call state.
+     * If the Call Manager has an active call and call waiting occurs,
+     * then the phone state is RINGING not OFFHOOK
+     *
+     */
+    public PhoneConstants.State getState() {
+        PhoneConstants.State s = PhoneConstants.State.IDLE;
+
+        for (Phone phone : mPhones) {
+            if (phone.getState() == PhoneConstants.State.RINGING) {
+                s = PhoneConstants.State.RINGING;
+            } else if (phone.getState() == PhoneConstants.State.OFFHOOK) {
+                if (s == PhoneConstants.State.IDLE) s = PhoneConstants.State.OFFHOOK;
+            }
+        }
+        return s;
+    }
+
+    /**
+     * Get current coarse-grained voice call state on a subId.
+     * If the Call Manager has an active call and call waiting occurs,
+     * then the phone state is RINGING not OFFHOOK
+     *
+     */
+    public PhoneConstants.State getState(int subId) {
+        PhoneConstants.State s = PhoneConstants.State.IDLE;
+
+        for (Phone phone : mPhones) {
+            if (phone.getSubId() == subId) {
+                if (phone.getState() == PhoneConstants.State.RINGING) {
+                    s = PhoneConstants.State.RINGING;
+                } else if (phone.getState() == PhoneConstants.State.OFFHOOK) {
+                    if (s == PhoneConstants.State.IDLE) s = PhoneConstants.State.OFFHOOK;
+                }
+            }
+        }
+        return s;
+    }
+
+    /**
+     * @return the service state of CallManager, which represents the
+     * highest priority state of all the service states of phones
+     *
+     * The priority is defined as
+     *
+     * STATE_IN_SERIVCE > STATE_OUT_OF_SERIVCE > STATE_EMERGENCY > STATE_POWER_OFF
+     *
+     */
+
+    public int getServiceState() {
+        int resultState = ServiceState.STATE_OUT_OF_SERVICE;
+
+        for (Phone phone : mPhones) {
+            int serviceState = phone.getServiceState().getState();
+            if (serviceState == ServiceState.STATE_IN_SERVICE) {
+                // IN_SERVICE has the highest priority
+                resultState = serviceState;
+                break;
+            } else if (serviceState == ServiceState.STATE_OUT_OF_SERVICE) {
+                // OUT_OF_SERVICE replaces EMERGENCY_ONLY and POWER_OFF
+                // Note: EMERGENCY_ONLY is not in use at this moment
+                if ( resultState == ServiceState.STATE_EMERGENCY_ONLY ||
+                        resultState == ServiceState.STATE_POWER_OFF) {
+                    resultState = serviceState;
+                }
+            } else if (serviceState == ServiceState.STATE_EMERGENCY_ONLY) {
+                if (resultState == ServiceState.STATE_POWER_OFF) {
+                    resultState = serviceState;
+                }
+            }
+        }
+        return resultState;
+    }
+
+    /**
+     * @return the Phone service state corresponds to subId
+     */
+    public int getServiceState(int subId) {
+        int resultState = ServiceState.STATE_OUT_OF_SERVICE;
+
+        for (Phone phone : mPhones) {
+            if (phone.getSubId() == subId) {
+                int serviceState = phone.getServiceState().getState();
+                if (serviceState == ServiceState.STATE_IN_SERVICE) {
+                    // IN_SERVICE has the highest priority
+                    resultState = serviceState;
+                    break;
+                } else if (serviceState == ServiceState.STATE_OUT_OF_SERVICE) {
+                    // OUT_OF_SERVICE replaces EMERGENCY_ONLY and POWER_OFF
+                    // Note: EMERGENCY_ONLY is not in use at this moment
+                    if ( resultState == ServiceState.STATE_EMERGENCY_ONLY ||
+                            resultState == ServiceState.STATE_POWER_OFF) {
+                        resultState = serviceState;
+                    }
+                } else if (serviceState == ServiceState.STATE_EMERGENCY_ONLY) {
+                    if (resultState == ServiceState.STATE_POWER_OFF) {
+                        resultState = serviceState;
+                    }
+                }
+            }
+        }
+        return resultState;
+    }
+
+    /**
+     * @return the phone associated with any call
+     */
+    public Phone getPhoneInCall() {
+        Phone phone = null;
+        if (!getFirstActiveRingingCall().isIdle()) {
+            phone = getFirstActiveRingingCall().getPhone();
+        } else if (!getActiveFgCall().isIdle()) {
+            phone = getActiveFgCall().getPhone();
+        } else {
+            // If BG call is idle, we return default phone
+            phone = getFirstActiveBgCall().getPhone();
+        }
+        return phone;
+    }
+
+    public Phone getPhoneInCall(int subId) {
+        Phone phone = null;
+        if (!getFirstActiveRingingCall(subId).isIdle()) {
+            phone = getFirstActiveRingingCall(subId).getPhone();
+        } else if (!getActiveFgCall(subId).isIdle()) {
+            phone = getActiveFgCall(subId).getPhone();
+        } else {
+            // If BG call is idle, we return default phone
+            phone = getFirstActiveBgCall(subId).getPhone();
+        }
+        return phone;
+    }
+
+    /**
+     * Register phone to CallManager
+     * @param phone to be registered
+     * @return true if register successfully
+     */
+    public boolean registerPhone(Phone phone) {
+        if (phone != null && !mPhones.contains(phone)) {
+
+            if (DBG) {
+                Rlog.d(LOG_TAG, "registerPhone(" +
+                        phone.getPhoneName() + " " + phone + ")");
+            }
+
+            if (mPhones.isEmpty()) {
+                mDefaultPhone = phone;
+            }
+            mPhones.add(phone);
+            mRingingCalls.add(phone.getRingingCall());
+            mBackgroundCalls.add(phone.getBackgroundCall());
+            mForegroundCalls.add(phone.getForegroundCall());
+            registerForPhoneStates(phone);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * unregister phone from CallManager
+     * @param phone to be unregistered
+     */
+    public void unregisterPhone(Phone phone) {
+        if (phone != null && mPhones.contains(phone)) {
+
+            if (DBG) {
+                Rlog.d(LOG_TAG, "unregisterPhone(" +
+                        phone.getPhoneName() + " " + phone + ")");
+            }
+
+            Phone imsPhone = phone.getImsPhone();
+            if (imsPhone != null) {
+                unregisterPhone(imsPhone);
+            }
+
+            mPhones.remove(phone);
+            mRingingCalls.remove(phone.getRingingCall());
+            mBackgroundCalls.remove(phone.getBackgroundCall());
+            mForegroundCalls.remove(phone.getForegroundCall());
+            unregisterForPhoneStates(phone);
+            if (phone == mDefaultPhone) {
+                if (mPhones.isEmpty()) {
+                    mDefaultPhone = null;
+                } else {
+                    mDefaultPhone = mPhones.get(0);
+                }
+            }
+        }
+    }
+
+    /**
+     * return the default phone or null if no phone available
+     */
+    public Phone getDefaultPhone() {
+        return mDefaultPhone;
+    }
+
+    /**
+     * @return the phone associated with the foreground call
+     */
+    public Phone getFgPhone() {
+        return getActiveFgCall().getPhone();
+    }
+
+    /**
+     * @return the phone associated with the foreground call
+     * of a particular subId
+     */
+    public Phone getFgPhone(int subId) {
+        return getActiveFgCall(subId).getPhone();
+    }
+
+    /**
+     * @return the phone associated with the background call
+     */
+    public Phone getBgPhone() {
+        return getFirstActiveBgCall().getPhone();
+    }
+
+    /**
+     * @return the phone associated with the background call
+     * of a particular subId
+     */
+    public Phone getBgPhone(int subId) {
+        return getFirstActiveBgCall(subId).getPhone();
+    }
+
+    /**
+     * @return the phone associated with the ringing call
+     */
+    public Phone getRingingPhone() {
+        return getFirstActiveRingingCall().getPhone();
+    }
+
+    /**
+     * @return the phone associated with the ringing call
+     * of a particular subId
+     */
+    public Phone getRingingPhone(int subId) {
+        return getFirstActiveRingingCall(subId).getPhone();
+    }
+
+    /* FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+    public void setAudioMode() {
+        Context context = getContext();
+        if (context == null) return;
+        AudioManager audioManager = (AudioManager)
+                context.getSystemService(Context.AUDIO_SERVICE);
+
+        if (!isServiceStateInService() && !mIsEccDialing) {
+            if (audioManager.getMode() != AudioManager.MODE_NORMAL) {
+                if (VDBG) Rlog.d(LOG_TAG, "abandonAudioFocus");
+                // abandon audio focus after the mode has been set back to normal
+                audioManager.abandonAudioFocusForCall();
+                audioManager.setMode(AudioManager.MODE_NORMAL);
+            }
+            return;
+        }
+
+        // change the audio mode and request/abandon audio focus according to phone state,
+        // but only on audio mode transitions
+        switch (getState()) {
+            case RINGING:
+                int curAudioMode = audioManager.getMode();
+                if (curAudioMode != AudioManager.MODE_RINGTONE) {
+                    if (VDBG) Rlog.d(LOG_TAG, "requestAudioFocus on STREAM_RING");
+                    audioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
+                            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                    if(!mSpeedUpAudioForMtCall) {
+                        audioManager.setMode(AudioManager.MODE_RINGTONE);
+                    }
+                }
+
+                if (mSpeedUpAudioForMtCall && (curAudioMode != AudioManager.MODE_IN_CALL)) {
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+                break;
+            case OFFHOOK:
+                Phone offhookPhone = getFgPhone();
+                if (getActiveFgCallState() == Call.State.IDLE) {
+                    // There is no active Fg calls, the OFFHOOK state
+                    // is set by the Bg call. So set the phone to bgPhone.
+                    offhookPhone = getBgPhone();
+                }
+
+                int newAudioMode = AudioManager.MODE_IN_CALL;
+                if (offhookPhone instanceof SipPhone) {
+                    Rlog.d(LOG_TAG, "setAudioMode Set audio mode for SIP call!");
+                    // enable IN_COMMUNICATION audio mode instead for sipPhone
+                    newAudioMode = AudioManager.MODE_IN_COMMUNICATION;
+                }
+                int currMode = audioManager.getMode();
+                if (currMode != newAudioMode || mSpeedUpAudioForMtCall) {
+                    // request audio focus before setting the new mode
+                    if (VDBG) Rlog.d(LOG_TAG, "requestAudioFocus on STREAM_VOICE_CALL");
+                    audioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                    Rlog.d(LOG_TAG, "setAudioMode Setting audio mode from "
+                            + currMode + " to " + newAudioMode);
+                    audioManager.setMode(newAudioMode);
+                }
+                mSpeedUpAudioForMtCall = false;
+                break;
+            case IDLE:
+                if (audioManager.getMode() != AudioManager.MODE_NORMAL) {
+                    audioManager.setMode(AudioManager.MODE_NORMAL);
+                    if (VDBG) Rlog.d(LOG_TAG, "abandonAudioFocus");
+                    // abandon audio focus after the mode has been set back to normal
+                    audioManager.abandonAudioFocusForCall();
+                }
+                mSpeedUpAudioForMtCall = false;
+                break;
+        }
+        Rlog.d(LOG_TAG, "setAudioMode state = " + getState());
+    }
+    */
+
+    private Context getContext() {
+        Phone defaultPhone = getDefaultPhone();
+        return ((defaultPhone == null) ? null : defaultPhone.getContext());
+    }
+
+    public Object getRegistrantIdentifier() {
+        return mRegistrantidentifier;
+    }
+
+    private void registerForPhoneStates(Phone phone) {
+        // We need to keep a mapping of handler to Phone for proper unregistration.
+        // TODO: Clean up this solution as it is just a work around for each Phone instance
+        // using the same Handler to register with the RIL. When time permits, we should consider
+        // moving the handler (or the reference ot the handler) into the Phone object.
+        // See b/17414427.
+        CallManagerHandler handler = mHandlerMap.get(phone);
+        if (handler != null) {
+            Rlog.d(LOG_TAG, "This phone has already been registered.");
+            return;
+        }
+
+        // New registration, create a new handler instance and register the phone.
+        handler = new CallManagerHandler();
+        mHandlerMap.put(phone, handler);
+
+        // for common events supported by all phones
+        // The mRegistrantIdentifier passed here, is to identify in the Phone
+        // that the registrants are coming from the CallManager.
+        phone.registerForPreciseCallStateChanged(handler, EVENT_PRECISE_CALL_STATE_CHANGED,
+                mRegistrantidentifier);
+        phone.registerForDisconnect(handler, EVENT_DISCONNECT,
+                mRegistrantidentifier);
+        phone.registerForNewRingingConnection(handler, EVENT_NEW_RINGING_CONNECTION,
+                mRegistrantidentifier);
+        phone.registerForUnknownConnection(handler, EVENT_UNKNOWN_CONNECTION,
+                mRegistrantidentifier);
+        phone.registerForIncomingRing(handler, EVENT_INCOMING_RING,
+                mRegistrantidentifier);
+        phone.registerForRingbackTone(handler, EVENT_RINGBACK_TONE,
+                mRegistrantidentifier);
+        phone.registerForInCallVoicePrivacyOn(handler, EVENT_IN_CALL_VOICE_PRIVACY_ON,
+                mRegistrantidentifier);
+        phone.registerForInCallVoicePrivacyOff(handler, EVENT_IN_CALL_VOICE_PRIVACY_OFF,
+                mRegistrantidentifier);
+        phone.registerForDisplayInfo(handler, EVENT_DISPLAY_INFO,
+                mRegistrantidentifier);
+        phone.registerForSignalInfo(handler, EVENT_SIGNAL_INFO,
+                mRegistrantidentifier);
+        phone.registerForResendIncallMute(handler, EVENT_RESEND_INCALL_MUTE,
+                mRegistrantidentifier);
+        phone.registerForMmiInitiate(handler, EVENT_MMI_INITIATE,
+                mRegistrantidentifier);
+        phone.registerForMmiComplete(handler, EVENT_MMI_COMPLETE,
+                mRegistrantidentifier);
+        phone.registerForSuppServiceFailed(handler, EVENT_SUPP_SERVICE_FAILED,
+                mRegistrantidentifier);
+        phone.registerForServiceStateChanged(handler, EVENT_SERVICE_STATE_CHANGED,
+                mRegistrantidentifier);
+
+        // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+        //phone.registerForRadioOffOrNotAvailable(handler, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null);
+
+        // for events supported only by GSM, CDMA and IMS phone
+        phone.setOnPostDialCharacter(handler, EVENT_POST_DIAL_CHARACTER, null);
+
+        // for events supported only by CDMA phone
+        phone.registerForCdmaOtaStatusChange(handler, EVENT_CDMA_OTA_STATUS_CHANGE, null);
+        phone.registerForSubscriptionInfoReady(handler, EVENT_SUBSCRIPTION_INFO_READY, null);
+        phone.registerForCallWaiting(handler, EVENT_CALL_WAITING, null);
+        phone.registerForEcmTimerReset(handler, EVENT_ECM_TIMER_RESET, null);
+
+        // for events supported only by IMS phone
+        phone.registerForOnHoldTone(handler, EVENT_ONHOLD_TONE, null);
+        phone.registerForSuppServiceFailed(handler, EVENT_SUPP_SERVICE_FAILED, null);
+        phone.registerForTtyModeReceived(handler, EVENT_TTY_MODE_RECEIVED, null);
+    }
+
+    private void unregisterForPhoneStates(Phone phone) {
+        // Make sure that we clean up our map of handlers to Phones.
+        CallManagerHandler handler = mHandlerMap.get(phone);
+        if (handler == null) {
+            Rlog.e(LOG_TAG, "Could not find Phone handler for unregistration");
+            return;
+        }
+        mHandlerMap.remove(phone);
+
+        //  for common events supported by all phones
+        phone.unregisterForPreciseCallStateChanged(handler);
+        phone.unregisterForDisconnect(handler);
+        phone.unregisterForNewRingingConnection(handler);
+        phone.unregisterForUnknownConnection(handler);
+        phone.unregisterForIncomingRing(handler);
+        phone.unregisterForRingbackTone(handler);
+        phone.unregisterForInCallVoicePrivacyOn(handler);
+        phone.unregisterForInCallVoicePrivacyOff(handler);
+        phone.unregisterForDisplayInfo(handler);
+        phone.unregisterForSignalInfo(handler);
+        phone.unregisterForResendIncallMute(handler);
+        phone.unregisterForMmiInitiate(handler);
+        phone.unregisterForMmiComplete(handler);
+        phone.unregisterForSuppServiceFailed(handler);
+        phone.unregisterForServiceStateChanged(handler);
+        phone.unregisterForTtyModeReceived(handler);
+        // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+        //phone.unregisterForRadioOffOrNotAvailable(handler);
+
+        // for events supported only by GSM, CDMA and IMS phone
+        phone.setOnPostDialCharacter(null, EVENT_POST_DIAL_CHARACTER, null);
+
+        // for events supported only by CDMA phone
+        phone.unregisterForCdmaOtaStatusChange(handler);
+        phone.unregisterForSubscriptionInfoReady(handler);
+        phone.unregisterForCallWaiting(handler);
+        phone.unregisterForEcmTimerReset(handler);
+
+        // for events supported only by IMS phone
+        phone.unregisterForOnHoldTone(handler);
+        phone.unregisterForSuppServiceFailed(handler);
+    }
+
+    /**
+     * Answers a ringing or waiting call.
+     *
+     * Active call, if any, go on hold.
+     * If active call can't be held, i.e., a background call of the same channel exists,
+     * the active call will be hang up.
+     *
+     * Answering occurs asynchronously, and final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException when call is not ringing or waiting
+     */
+    public void acceptCall(Call ringingCall) throws CallStateException {
+        Phone ringingPhone = ringingCall.getPhone();
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "acceptCall(" +ringingCall + " from " + ringingCall.getPhone() + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if ( hasActiveFgCall() ) {
+            Phone activePhone = getActiveFgCall().getPhone();
+            boolean hasBgCall = ! (activePhone.getBackgroundCall().isIdle());
+            boolean sameChannel = (activePhone == ringingPhone);
+
+            if (VDBG) {
+                Rlog.d(LOG_TAG, "hasBgCall: "+ hasBgCall + "sameChannel:" + sameChannel);
+            }
+
+            if (sameChannel && hasBgCall) {
+                getActiveFgCall().hangup();
+            } else if (!sameChannel && !hasBgCall) {
+                activePhone.switchHoldingAndActive();
+            } else if (!sameChannel && hasBgCall) {
+                getActiveFgCall().hangup();
+            }
+        }
+
+        // We only support the AUDIO_ONLY video state in this scenario.
+        ringingPhone.acceptCall(VideoProfile.STATE_AUDIO_ONLY);
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End acceptCall(" +ringingCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * Reject (ignore) a ringing call. In GSM, this means UDUB
+     * (User Determined User Busy). Reject occurs asynchronously,
+     * and final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException when no call is ringing or waiting
+     */
+    public void rejectCall(Call ringingCall) throws CallStateException {
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "rejectCall(" +ringingCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        Phone ringingPhone = ringingCall.getPhone();
+
+        ringingPhone.rejectCall();
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End rejectCall(" +ringingCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * Places active call on hold, and makes held call active.
+     * Switch occurs asynchronously and may fail.
+     *
+     * There are 4 scenarios
+     * 1. only active call but no held call, aka, hold
+     * 2. no active call but only held call, aka, unhold
+     * 3. both active and held calls from same phone, aka, swap
+     * 4. active and held calls from different phones, aka, phone swap
+     *
+     * Final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException if active call is ringing, waiting, or
+     * dialing/alerting, or heldCall can't be active.
+     * In these cases, this operation may not be performed.
+     */
+    public void switchHoldingAndActive(Call heldCall) throws CallStateException {
+        Phone activePhone = null;
+        Phone heldPhone = null;
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "switchHoldingAndActive(" +heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) {
+            activePhone = getActiveFgCall().getPhone();
+        }
+
+        if (heldCall != null) {
+            heldPhone = heldCall.getPhone();
+        }
+
+        if (activePhone != null) {
+            activePhone.switchHoldingAndActive();
+        }
+
+        if (heldPhone != null && heldPhone != activePhone) {
+            heldPhone.switchHoldingAndActive();
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End switchHoldingAndActive(" +heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * Hangup foreground call and resume the specific background call
+     *
+     * Note: this is noop if there is no foreground call or the heldCall is null
+     *
+     * @param heldCall to become foreground
+     * @throws CallStateException
+     */
+    public void hangupForegroundResumeBackground(Call heldCall) throws CallStateException {
+        Phone foregroundPhone = null;
+        Phone backgroundPhone = null;
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "hangupForegroundResumeBackground(" +heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) {
+            foregroundPhone = getFgPhone();
+            if (heldCall != null) {
+                backgroundPhone = heldCall.getPhone();
+                if (foregroundPhone == backgroundPhone) {
+                    getActiveFgCall().hangup();
+                } else {
+                // the call to be hangup and resumed belongs to different phones
+                    getActiveFgCall().hangup();
+                    switchHoldingAndActive(heldCall);
+                }
+            }
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End hangupForegroundResumeBackground(" +heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * Whether or not the phone can conference in the current phone
+     * state--that is, one call holding and one call active.
+     * @return true if the phone can conference; false otherwise.
+     */
+    public boolean canConference(Call heldCall) {
+        Phone activePhone = null;
+        Phone heldPhone = null;
+
+        if (hasActiveFgCall()) {
+            activePhone = getActiveFgCall().getPhone();
+        }
+
+        if (heldCall != null) {
+            heldPhone = heldCall.getPhone();
+        }
+
+        return heldPhone.getClass().equals(activePhone.getClass());
+    }
+
+    /**
+     * Whether or not the phone can conference in the current phone
+     * state--that is, one call holding and one call active.
+     * This method consider the phone object which is specific
+     * to the provided subId.
+     * @return true if the phone can conference; false otherwise.
+     */
+    public boolean canConference(Call heldCall, int subId) {
+        Phone activePhone = null;
+        Phone heldPhone = null;
+
+        if (hasActiveFgCall(subId)) {
+            activePhone = getActiveFgCall(subId).getPhone();
+        }
+
+        if (heldCall != null) {
+            heldPhone = heldCall.getPhone();
+        }
+
+        return heldPhone.getClass().equals(activePhone.getClass());
+    }
+
+    /**
+     * Conferences holding and active. Conference occurs asynchronously
+     * and may fail. Final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException if canConference() would return false.
+     * In these cases, this operation may not be performed.
+     */
+    public void conference(Call heldCall) throws CallStateException {
+        int subId  = heldCall.getPhone().getSubId();
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "conference(" +heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        Phone fgPhone = getFgPhone(subId);
+        if (fgPhone != null) {
+            if (fgPhone instanceof SipPhone) {
+                ((SipPhone) fgPhone).conference(heldCall);
+            } else if (canConference(heldCall)) {
+                fgPhone.conference();
+            } else {
+                throw(new CallStateException("Can't conference foreground and selected background call"));
+            }
+        } else {
+            Rlog.d(LOG_TAG, "conference: fgPhone=null");
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End conference(" +heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+    }
+
+    /**
+     * Initiate a new voice connection. This happens asynchronously, so you
+     * cannot assume the audio path is connected (or a call index has been
+     * assigned) until PhoneStateChanged notification has occurred.
+     *
+     * @exception CallStateException if a new outgoing call is not currently
+     * possible because no more call slots exist or a call exists that is
+     * dialing, alerting, ringing, or waiting.  Other errors are
+     * handled asynchronously.
+     */
+    public Connection dial(Phone phone, String dialString, int videoState)
+            throws CallStateException {
+        int subId = phone.getSubId();
+        Connection result;
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " dial(" + phone + ", "+ dialString + ")" +
+                    " subId = " + subId);
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (!canDial(phone)) {
+            /*
+             * canDial function only checks whether the phone can make a new call.
+             * InCall MMI commmands are basically supplementary services
+             * within a call eg: call hold, call deflection, explicit call transfer etc.
+             */
+            String newDialString = PhoneNumberUtils.stripSeparators(dialString);
+            if (phone.handleInCallMmiCommands(newDialString)) {
+                return null;
+            } else {
+                throw new CallStateException("cannot dial in current state");
+            }
+        }
+
+        if ( hasActiveFgCall(subId) ) {
+            Phone activePhone = getActiveFgCall(subId).getPhone();
+            boolean hasBgCall = !(activePhone.getBackgroundCall().isIdle());
+
+            if (DBG) {
+                Rlog.d(LOG_TAG, "hasBgCall: "+ hasBgCall + " sameChannel:" + (activePhone == phone));
+            }
+
+            // Manipulation between IMS phone and its owner
+            // will be treated in GSM/CDMA phone.
+            Phone imsPhone = phone.getImsPhone();
+            if (activePhone != phone
+                    && (imsPhone == null || imsPhone != activePhone)) {
+                if (hasBgCall) {
+                    Rlog.d(LOG_TAG, "Hangup");
+                    getActiveFgCall(subId).hangup();
+                } else {
+                    Rlog.d(LOG_TAG, "Switch");
+                    activePhone.switchHoldingAndActive();
+                }
+            }
+        }
+
+        // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+        //mIsEccDialing = PhoneNumberUtils.isEmergencyNumber(dialString);
+
+        result = phone.dial(dialString, videoState);
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End dial(" + phone + ", "+ dialString + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        return result;
+    }
+
+    /**
+     * Initiate a new voice connection. This happens asynchronously, so you
+     * cannot assume the audio path is connected (or a call index has been
+     * assigned) until PhoneStateChanged notification has occurred.
+     *
+     * @exception CallStateException if a new outgoing call is not currently
+     * possible because no more call slots exist or a call exists that is
+     * dialing, alerting, ringing, or waiting.  Other errors are
+     * handled asynchronously.
+     */
+    public Connection dial(Phone phone, String dialString, UUSInfo uusInfo, int videoState)
+            throws CallStateException {
+        return phone.dial(dialString, uusInfo, videoState, null);
+    }
+
+    /**
+     * clear disconnect connection for each phone
+     */
+    public void clearDisconnected() {
+        for(Phone phone : mPhones) {
+            phone.clearDisconnected();
+        }
+    }
+
+    /**
+     * clear disconnect connection for a phone specific
+     * to the provided subId
+     */
+    public void clearDisconnected(int subId) {
+        for(Phone phone : mPhones) {
+            if (phone.getSubId() == subId) {
+                phone.clearDisconnected();
+            }
+        }
+    }
+
+    /**
+     * Phone can make a call only if ALL of the following are true:
+     *        - Phone is not powered off
+     *        - There's no incoming or waiting call
+     *        - The foreground call is ACTIVE or IDLE or DISCONNECTED.
+     *          (We mainly need to make sure it *isn't* DIALING or ALERTING.)
+     * @param phone
+     * @return true if the phone can make a new call
+     */
+    private boolean canDial(Phone phone) {
+        int serviceState = phone.getServiceState().getState();
+        int subId = phone.getSubId();
+        boolean hasRingingCall = hasActiveRingingCall();
+        Call.State fgCallState = getActiveFgCallState(subId);
+
+        boolean result = (serviceState != ServiceState.STATE_POWER_OFF
+                && !hasRingingCall
+                && ((fgCallState == Call.State.ACTIVE)
+                    || (fgCallState == Call.State.IDLE)
+                    || (fgCallState == Call.State.DISCONNECTED)
+                    /*As per 3GPP TS 51.010-1 section 31.13.1.4
+                    call should be alowed when the foreground
+                    call is in ALERTING state*/
+                    || (fgCallState == Call.State.ALERTING)));
+
+        if (result == false) {
+            Rlog.d(LOG_TAG, "canDial serviceState=" + serviceState
+                            + " hasRingingCall=" + hasRingingCall
+                            + " fgCallState=" + fgCallState);
+        }
+        return result;
+    }
+
+    /**
+     * Whether or not the phone can do explicit call transfer in the current
+     * phone state--that is, one call holding and one call active.
+     * @return true if the phone can do explicit call transfer; false otherwise.
+     */
+    public boolean canTransfer(Call heldCall) {
+        Phone activePhone = null;
+        Phone heldPhone = null;
+
+        if (hasActiveFgCall()) {
+            activePhone = getActiveFgCall().getPhone();
+        }
+
+        if (heldCall != null) {
+            heldPhone = heldCall.getPhone();
+        }
+
+        return (heldPhone == activePhone && activePhone.canTransfer());
+    }
+
+    /**
+     * Whether or not the phone specific to subId can do explicit call transfer
+     * in the current phone state--that is, one call holding and one call active.
+     * @return true if the phone can do explicit call transfer; false otherwise.
+     */
+    public boolean canTransfer(Call heldCall, int subId) {
+        Phone activePhone = null;
+        Phone heldPhone = null;
+
+        if (hasActiveFgCall(subId)) {
+            activePhone = getActiveFgCall(subId).getPhone();
+        }
+
+        if (heldCall != null) {
+            heldPhone = heldCall.getPhone();
+        }
+
+        return (heldPhone == activePhone && activePhone.canTransfer());
+    }
+
+    /**
+     * Connects the held call and active call
+     * Disconnects the subscriber from both calls
+     *
+     * Explicit Call Transfer occurs asynchronously
+     * and may fail. Final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException if canTransfer() would return false.
+     * In these cases, this operation may not be performed.
+     */
+    public void explicitCallTransfer(Call heldCall) throws CallStateException {
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " explicitCallTransfer(" + heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (canTransfer(heldCall)) {
+            heldCall.getPhone().explicitCallTransfer();
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End explicitCallTransfer(" + heldCall + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+    }
+
+    /**
+     * Returns a list of MMI codes that are pending for a phone. (They have initiated
+     * but have not yet completed).
+     * Presently there is only ever one.
+     *
+     * Use <code>registerForMmiInitiate</code>
+     * and <code>registerForMmiComplete</code> for change notification.
+     * @return null if phone doesn't have or support mmi code
+     */
+    public List<? extends MmiCode> getPendingMmiCodes(Phone phone) {
+        Rlog.e(LOG_TAG, "getPendingMmiCodes not implemented");
+        return null;
+    }
+
+    /**
+     * Sends user response to a USSD REQUEST message.  An MmiCode instance
+     * representing this response is sent to handlers registered with
+     * registerForMmiInitiate.
+     *
+     * @param ussdMessge    Message to send in the response.
+     * @return false if phone doesn't support ussd service
+     */
+    public boolean sendUssdResponse(Phone phone, String ussdMessge) {
+        Rlog.e(LOG_TAG, "sendUssdResponse not implemented");
+        return false;
+    }
+
+    /**
+     * Mutes or unmutes the microphone for the active call. The microphone
+     * is automatically unmuted if a call is answered, dialed, or resumed
+     * from a holding state.
+     *
+     * @param muted true to mute the microphone,
+     * false to activate the microphone.
+     */
+
+    public void setMute(boolean muted) {
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " setMute(" + muted + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) {
+            getActiveFgCall().getPhone().setMute(muted);
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End setMute(" + muted + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * Gets current mute status. Use
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}
+     * as a change notifcation, although presently phone state changed is not
+     * fired when setMute() is called.
+     *
+     * @return true is muting, false is unmuting
+     */
+    public boolean getMute() {
+        if (hasActiveFgCall()) {
+            return getActiveFgCall().getPhone().getMute();
+        } else if (hasActiveBgCall()) {
+            return getFirstActiveBgCall().getPhone().getMute();
+        }
+        return false;
+    }
+
+    /**
+     * Enables or disables echo suppression.
+     */
+    public void setEchoSuppressionEnabled() {
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " setEchoSuppression()");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) {
+            getActiveFgCall().getPhone().setEchoSuppressionEnabled();
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End setEchoSuppression()");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * Play a DTMF tone on the active call.
+     *
+     * @param c should be one of 0-9, '*' or '#'. Other values will be
+     * silently ignored.
+     * @return false if no active call or the active call doesn't support
+     *         dtmf tone
+     */
+    public boolean sendDtmf(char c) {
+        boolean result = false;
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " sendDtmf(" + c + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) {
+            getActiveFgCall().getPhone().sendDtmf(c);
+            result = true;
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End sendDtmf(" + c + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+        return result;
+    }
+
+    /**
+     * Start to paly a DTMF tone on the active call.
+     * or there is a playing DTMF tone.
+     * @param c should be one of 0-9, '*' or '#'. Other values will be
+     * silently ignored.
+     *
+     * @return false if no active call or the active call doesn't support
+     *         dtmf tone
+     */
+    public boolean startDtmf(char c) {
+        boolean result = false;
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " startDtmf(" + c + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) {
+            getActiveFgCall().getPhone().startDtmf(c);
+            result = true;
+        }
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End startDtmf(" + c + ")");
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        return result;
+    }
+
+    /**
+     * Stop the playing DTMF tone. Ignored if there is no playing DTMF
+     * tone or no active call.
+     */
+    public void stopDtmf() {
+        if (VDBG) {
+            Rlog.d(LOG_TAG, " stopDtmf()" );
+            Rlog.d(LOG_TAG, toString());
+        }
+
+        if (hasActiveFgCall()) getFgPhone().stopDtmf();
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "End stopDtmf()");
+            Rlog.d(LOG_TAG, toString());
+        }
+    }
+
+    /**
+     * send burst DTMF tone, it can send the string as single character or multiple character
+     * ignore if there is no active call or not valid digits string.
+     * Valid digit means only includes characters ISO-LATIN characters 0-9, *, #
+     * The difference between sendDtmf and sendBurstDtmf is sendDtmf only sends one character,
+     * this api can send single character and multiple character, also, this api has response
+     * back to caller.
+     *
+     * @param dtmfString is string representing the dialing digit(s) in the active call
+     * @param on the DTMF ON length in milliseconds, or 0 for default
+     * @param off the DTMF OFF length in milliseconds, or 0 for default
+     * @param onComplete is the callback message when the action is processed by BP
+     *
+     */
+    public boolean sendBurstDtmf(String dtmfString, int on, int off, Message onComplete) {
+        if (hasActiveFgCall()) {
+            getActiveFgCall().getPhone().sendBurstDtmf(dtmfString, on, off, onComplete);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Notifies when a voice connection has disconnected, either due to local
+     * or remote hangup or error.
+     *
+     *  Messages received from this will have the following members:<p>
+     *  <ul><li>Message.obj will be an AsyncResult</li>
+     *  <li>AsyncResult.userObj = obj</li>
+     *  <li>AsyncResult.result = a Connection object that is
+     *  no longer connected.</li></ul>
+     */
+    public void registerForDisconnect(Handler h, int what, Object obj) {
+        mDisconnectRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for voice disconnection notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForDisconnect(Handler h){
+        mDisconnectRegistrants.remove(h);
+    }
+
+    /**
+     * Register for getting notifications for change in the Call State {@link Call.State}
+     * This is called PreciseCallState because the call state is more precise than what
+     * can be obtained using the {@link PhoneStateListener}
+     *
+     * Resulting events will have an AsyncResult in <code>Message.obj</code>.
+     * AsyncResult.userData will be set to the obj argument here.
+     * The <em>h</em> parameter is held only by a weak reference.
+     */
+    public void registerForPreciseCallStateChanged(Handler h, int what, Object obj){
+        mPreciseCallStateRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for voice call state change notifications.
+     * Extraneous calls are tolerated silently.
+     */
+    public void unregisterForPreciseCallStateChanged(Handler h){
+        mPreciseCallStateRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when a previously untracked non-ringing/waiting connection has appeared.
+     * This is likely due to some other entity (eg, SIM card application) initiating a call.
+     */
+    public void registerForUnknownConnection(Handler h, int what, Object obj){
+        mUnknownConnectionRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for unknown connection notifications.
+     */
+    public void unregisterForUnknownConnection(Handler h){
+        mUnknownConnectionRegistrants.remove(h);
+    }
+
+
+    /**
+     * Notifies when a new ringing or waiting connection has appeared.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = a Connection. <p>
+     *  Please check Connection.isRinging() to make sure the Connection
+     *  has not dropped since this message was posted.
+     *  If Connection.isRinging() is true, then
+     *   Connection.getCall() == Phone.getRingingCall()
+     */
+    public void registerForNewRingingConnection(Handler h, int what, Object obj){
+        mNewRingingConnectionRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for new ringing connection notification.
+     * Extraneous calls are tolerated silently
+     */
+
+    public void unregisterForNewRingingConnection(Handler h){
+        mNewRingingConnectionRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when an incoming call rings.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = a Connection. <p>
+     */
+    public void registerForIncomingRing(Handler h, int what, Object obj){
+        mIncomingRingRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for ring notification.
+     * Extraneous calls are tolerated silently
+     */
+
+    public void unregisterForIncomingRing(Handler h){
+        mIncomingRingRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when out-band ringback tone is needed.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = boolean, true to start play ringback tone
+     *                       and false to stop. <p>
+     */
+    public void registerForRingbackTone(Handler h, int what, Object obj){
+        mRingbackToneRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for ringback tone notification.
+     */
+
+    public void unregisterForRingbackTone(Handler h){
+        mRingbackToneRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when out-band on-hold tone is needed.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = boolean, true to start play on-hold tone
+     *                       and false to stop. <p>
+     */
+    public void registerForOnHoldTone(Handler h, int what, Object obj){
+        mOnHoldToneRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for on-hold tone notification.
+     */
+
+    public void unregisterForOnHoldTone(Handler h){
+        mOnHoldToneRegistrants.remove(h);
+    }
+
+    /**
+     * Registers the handler to reset the uplink mute state to get
+     * uplink audio.
+     */
+    public void registerForResendIncallMute(Handler h, int what, Object obj){
+        mResendIncallMuteRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for resend incall mute notifications.
+     */
+    public void unregisterForResendIncallMute(Handler h){
+        mResendIncallMuteRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications of initiation of a new MMI code request.
+     * MMI codes for GSM are discussed in 3GPP TS 22.030.<p>
+     *
+     * Example: If Phone.dial is called with "*#31#", then the app will
+     * be notified here.<p>
+     *
+     * The returned <code>Message.obj</code> will contain an AsyncResult.
+     *
+     * <code>obj.result</code> will be an "MmiCode" object.
+     */
+    public void registerForMmiInitiate(Handler h, int what, Object obj){
+        mMmiInitiateRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for new MMI initiate notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForMmiInitiate(Handler h){
+        mMmiInitiateRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications that an MMI request has completed
+     * its network activity and is in its final state. This may mean a state
+     * of COMPLETE, FAILED, or CANCELLED.
+     *
+     * <code>Message.obj</code> will contain an AsyncResult.
+     * <code>obj.result</code> will be an "MmiCode" object
+     */
+    public void registerForMmiComplete(Handler h, int what, Object obj){
+        Rlog.d(LOG_TAG, "registerForMmiComplete");
+        mMmiCompleteRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for MMI complete notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForMmiComplete(Handler h){
+        mMmiCompleteRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for Ecm timer reset
+     * @param h handler to notify
+     * @param what user-defined message code
+     * @param obj placed in Message.obj
+     */
+    public void registerForEcmTimerReset(Handler h, int what, Object obj){
+        mEcmTimerResetRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notification for Ecm timer reset
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForEcmTimerReset(Handler h){
+        mEcmTimerResetRegistrants.remove(h);
+    }
+
+    /**
+     * Register for ServiceState changed.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a ServiceState instance
+     */
+    public void registerForServiceStateChanged(Handler h, int what, Object obj){
+        mServiceStateChangedRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for ServiceStateChange notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForServiceStateChanged(Handler h){
+        mServiceStateChangedRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when a supplementary service attempt fails.
+     * Message.obj will contain an AsyncResult.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForSuppServiceFailed(Handler h, int what, Object obj){
+        mSuppServiceFailedRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when a supplementary service attempt fails.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSuppServiceFailed(Handler h){
+        mSuppServiceFailedRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when a sInCall VoicePrivacy is enabled
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj){
+        mInCallVoicePrivacyOnRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when a sInCall VoicePrivacy is enabled
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForInCallVoicePrivacyOn(Handler h){
+        mInCallVoicePrivacyOnRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when a sInCall VoicePrivacy is disabled
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj){
+        mInCallVoicePrivacyOffRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when a sInCall VoicePrivacy is disabled
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForInCallVoicePrivacyOff(Handler h){
+        mInCallVoicePrivacyOffRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when CDMA call waiting comes
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForCallWaiting(Handler h, int what, Object obj){
+        mCallWaitingRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when CDMA Call waiting comes
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForCallWaiting(Handler h){
+        mCallWaitingRegistrants.remove(h);
+    }
+
+
+    /**
+     * Register for signal information notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a SuppServiceNotification instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+
+    public void registerForSignalInfo(Handler h, int what, Object obj){
+        mSignalInfoRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for signal information notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSignalInfo(Handler h){
+        mSignalInfoRegistrants.remove(h);
+    }
+
+    /**
+     * Register for display information notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a SuppServiceNotification instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForDisplayInfo(Handler h, int what, Object obj){
+        mDisplayInfoRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for display information notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForDisplayInfo(Handler h) {
+        mDisplayInfoRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when CDMA OTA Provision status change
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj){
+        mCdmaOtaStatusChangeRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when CDMA OTA Provision status change
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForCdmaOtaStatusChange(Handler h){
+        mCdmaOtaStatusChangeRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for subscription info ready
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForSubscriptionInfoReady(Handler h, int what, Object obj){
+        mSubscriptionInfoReadyRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications for subscription info
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSubscriptionInfoReady(Handler h){
+        mSubscriptionInfoReadyRegistrants.remove(h);
+    }
+
+    /**
+     * Sets an event to be fired when the telephony system processes
+     * a post-dial character on an outgoing call.<p>
+     *
+     * Messages of type <code>what</code> will be sent to <code>h</code>.
+     * The <code>obj</code> field of these Message's will be instances of
+     * <code>AsyncResult</code>. <code>Message.obj.result</code> will be
+     * a Connection object.<p>
+     *
+     * Message.arg1 will be the post dial character being processed,
+     * or 0 ('\0') if end of string.<p>
+     *
+     * If Connection.getPostDialState() == WAIT,
+     * the application must call
+     * {@link com.android.internal.telephony.Connection#proceedAfterWaitChar()
+     * Connection.proceedAfterWaitChar()} or
+     * {@link com.android.internal.telephony.Connection#cancelPostDial()
+     * Connection.cancelPostDial()}
+     * for the telephony system to continue playing the post-dial
+     * DTMF sequence.<p>
+     *
+     * If Connection.getPostDialState() == WILD,
+     * the application must call
+     * {@link com.android.internal.telephony.Connection#proceedAfterWildChar
+     * Connection.proceedAfterWildChar()}
+     * or
+     * {@link com.android.internal.telephony.Connection#cancelPostDial()
+     * Connection.cancelPostDial()}
+     * for the telephony system to continue playing the
+     * post-dial DTMF sequence.<p>
+     *
+     */
+    public void registerForPostDialCharacter(Handler h, int what, Object obj){
+        mPostDialCharacterRegistrants.addUnique(h, what, obj);
+    }
+
+    public void unregisterForPostDialCharacter(Handler h){
+        mPostDialCharacterRegistrants.remove(h);
+    }
+
+    /**
+     * Register for TTY mode change notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be an Integer containing new mode.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForTtyModeReceived(Handler h, int what, Object obj){
+        mTtyModeReceivedRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for TTY mode change notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForTtyModeReceived(Handler h) {
+        mTtyModeReceivedRegistrants.remove(h);
+    }
+
+    /* APIs to access foregroudCalls, backgroudCalls, and ringingCalls
+     * 1. APIs to access list of calls
+     * 2. APIs to check if any active call, which has connection other than
+     * disconnected ones, pleaser refer to Call.isIdle()
+     * 3. APIs to return first active call
+     * 4. APIs to return the connections of first active call
+     * 5. APIs to return other property of first active call
+     */
+
+    /**
+     * @return list of all ringing calls
+     */
+    public List<Call> getRingingCalls() {
+        return Collections.unmodifiableList(mRingingCalls);
+    }
+
+    /**
+     * @return list of all foreground calls
+     */
+    public List<Call> getForegroundCalls() {
+        return Collections.unmodifiableList(mForegroundCalls);
+    }
+
+    /**
+     * @return list of all background calls
+     */
+    public List<Call> getBackgroundCalls() {
+        return Collections.unmodifiableList(mBackgroundCalls);
+    }
+
+    /**
+     * Return true if there is at least one active foreground call
+     */
+    public boolean hasActiveFgCall() {
+        return (getFirstActiveCall(mForegroundCalls) != null);
+    }
+
+    /**
+     * Return true if there is at least one active foreground call
+     * on a particular subId or an active sip call
+     */
+    public boolean hasActiveFgCall(int subId) {
+        return (getFirstActiveCall(mForegroundCalls, subId) != null);
+    }
+
+    /**
+     * Return true if there is at least one active background call
+     */
+    public boolean hasActiveBgCall() {
+        // TODO since hasActiveBgCall may get called often
+        // better to cache it to improve performance
+        return (getFirstActiveCall(mBackgroundCalls) != null);
+    }
+
+    /**
+     * Return true if there is at least one active background call
+     * on a particular subId or an active sip call
+     */
+    public boolean hasActiveBgCall(int subId) {
+        // TODO since hasActiveBgCall may get called often
+        // better to cache it to improve performance
+        return (getFirstActiveCall(mBackgroundCalls, subId) != null);
+    }
+
+    /**
+     * Return true if there is at least one active ringing call
+     *
+     */
+    public boolean hasActiveRingingCall() {
+        return (getFirstActiveCall(mRingingCalls) != null);
+    }
+
+    /**
+     * Return true if there is at least one active ringing call
+     */
+    public boolean hasActiveRingingCall(int subId) {
+        return (getFirstActiveCall(mRingingCalls, subId) != null);
+    }
+
+    /**
+     * return the active foreground call from foreground calls
+     *
+     * Active call means the call is NOT in Call.State.IDLE
+     *
+     * 1. If there is active foreground call, return it
+     * 2. If there is no active foreground call, return the
+     *    foreground call associated with default phone, which state is IDLE.
+     * 3. If there is no phone registered at all, return null.
+     *
+     */
+    public Call getActiveFgCall() {
+        Call call = getFirstNonIdleCall(mForegroundCalls);
+        if (call == null) {
+            call = (mDefaultPhone == null)
+                    ? null
+                    : mDefaultPhone.getForegroundCall();
+        }
+        return call;
+    }
+
+    public Call getActiveFgCall(int subId) {
+        Call call = getFirstNonIdleCall(mForegroundCalls, subId);
+        if (call == null) {
+            Phone phone = getPhone(subId);
+            call = (phone == null)
+                    ? null
+                    : phone.getForegroundCall();
+        }
+        return call;
+    }
+
+    // Returns the first call that is not in IDLE state. If both active calls
+    // and disconnecting/disconnected calls exist, return the first active call.
+    private Call getFirstNonIdleCall(List<Call> calls) {
+        Call result = null;
+        for (Call call : calls) {
+            if (!call.isIdle()) {
+                return call;
+            } else if (call.getState() != Call.State.IDLE) {
+                if (result == null) result = call;
+            }
+        }
+        return result;
+    }
+
+    // Returns the first call that is not in IDLE state. If both active calls
+    // and disconnecting/disconnected calls exist, return the first active call.
+    private Call getFirstNonIdleCall(List<Call> calls, int subId) {
+        Call result = null;
+        for (Call call : calls) {
+            if ((call.getPhone().getSubId() == subId) ||
+                    (call.getPhone() instanceof SipPhone)) {
+                if (!call.isIdle()) {
+                    return call;
+                } else if (call.getState() != Call.State.IDLE) {
+                    if (result == null) result = call;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * return one active background call from background calls
+     *
+     * Active call means the call is NOT idle defined by Call.isIdle()
+     *
+     * 1. If there is only one active background call, return it
+     * 2. If there is more than one active background call, return the first one
+     * 3. If there is no active background call, return the background call
+     *    associated with default phone, which state is IDLE.
+     * 4. If there is no background call at all, return null.
+     *
+     * Complete background calls list can be get by getBackgroundCalls()
+     */
+    public Call getFirstActiveBgCall() {
+        Call call = getFirstNonIdleCall(mBackgroundCalls);
+        if (call == null) {
+            call = (mDefaultPhone == null)
+                    ? null
+                    : mDefaultPhone.getBackgroundCall();
+        }
+        return call;
+    }
+
+    /**
+     * return one active background call from background calls of the
+     * requested subId.
+     *
+     * Active call means the call is NOT idle defined by Call.isIdle()
+     *
+     * 1. If there is only one active background call on given sub or
+     *    on SIP Phone, return it
+     * 2. If there is more than one active background call, return the background call
+     *    associated with the active sub.
+     * 3. If there is no background call at all, return null.
+     *
+     * Complete background calls list can be get by getBackgroundCalls()
+     */
+    public Call getFirstActiveBgCall(int subId) {
+        Phone phone = getPhone(subId);
+        if (hasMoreThanOneHoldingCall(subId)) {
+            return phone.getBackgroundCall();
+        } else {
+            Call call = getFirstNonIdleCall(mBackgroundCalls, subId);
+            if (call == null) {
+                call = (phone == null)
+                        ? null
+                        : phone.getBackgroundCall();
+            }
+            return call;
+        }
+    }
+
+    /**
+     * return one active ringing call from ringing calls
+     *
+     * Active call means the call is NOT idle defined by Call.isIdle()
+     *
+     * 1. If there is only one active ringing call, return it
+     * 2. If there is more than one active ringing call, return the first one
+     * 3. If there is no active ringing call, return the ringing call
+     *    associated with default phone, which state is IDLE.
+     * 4. If there is no ringing call at all, return null.
+     *
+     * Complete ringing calls list can be get by getRingingCalls()
+     */
+    public Call getFirstActiveRingingCall() {
+        Call call = getFirstNonIdleCall(mRingingCalls);
+        if (call == null) {
+            call = (mDefaultPhone == null)
+                    ? null
+                    : mDefaultPhone.getRingingCall();
+        }
+        return call;
+    }
+
+    public Call getFirstActiveRingingCall(int subId) {
+        Phone phone = getPhone(subId);
+        Call call = getFirstNonIdleCall(mRingingCalls, subId);
+        if (call == null) {
+            call = (phone == null)
+                    ? null
+                    : phone.getRingingCall();
+        }
+        return call;
+    }
+
+    /**
+     * @return the state of active foreground call
+     * return IDLE if there is no active foreground call
+     */
+    public Call.State getActiveFgCallState() {
+        Call fgCall = getActiveFgCall();
+
+        if (fgCall != null) {
+            return fgCall.getState();
+        }
+
+        return Call.State.IDLE;
+    }
+
+    public Call.State getActiveFgCallState(int subId) {
+        Call fgCall = getActiveFgCall(subId);
+
+        if (fgCall != null) {
+            return fgCall.getState();
+        }
+
+        return Call.State.IDLE;
+    }
+
+    /**
+     * @return the connections of active foreground call
+     * return empty list if there is no active foreground call
+     */
+    public List<Connection> getFgCallConnections() {
+        Call fgCall = getActiveFgCall();
+        if ( fgCall != null) {
+            return fgCall.getConnections();
+        }
+        return mEmptyConnections;
+    }
+
+    /**
+     * @return the connections of active foreground call
+     * return empty list if there is no active foreground call
+     */
+    public List<Connection> getFgCallConnections(int subId) {
+        Call fgCall = getActiveFgCall(subId);
+        if ( fgCall != null) {
+            return fgCall.getConnections();
+        }
+        return mEmptyConnections;
+    }
+
+    /**
+     * @return the connections of active background call
+     * return empty list if there is no active background call
+     */
+    public List<Connection> getBgCallConnections() {
+        Call bgCall = getFirstActiveBgCall();
+        if ( bgCall != null) {
+            return bgCall.getConnections();
+        }
+        return mEmptyConnections;
+    }
+
+    /**
+     * @return the connections of active background call
+     * return empty list if there is no active background call
+     */
+    public List<Connection> getBgCallConnections(int subId) {
+        Call bgCall = getFirstActiveBgCall(subId);
+        if ( bgCall != null) {
+            return bgCall.getConnections();
+        }
+        return mEmptyConnections;
+    }
+
+    /**
+     * @return the latest connection of active foreground call
+     * return null if there is no active foreground call
+     */
+    public Connection getFgCallLatestConnection() {
+        Call fgCall = getActiveFgCall();
+        if ( fgCall != null) {
+            return fgCall.getLatestConnection();
+        }
+        return null;
+    }
+
+    /**
+     * @return the latest connection of active foreground call
+     * return null if there is no active foreground call
+     */
+    public Connection getFgCallLatestConnection(int subId) {
+        Call fgCall = getActiveFgCall(subId);
+        if ( fgCall != null) {
+            return fgCall.getLatestConnection();
+        }
+        return null;
+    }
+
+    /**
+     * @return true if there is at least one Foreground call in disconnected state
+     */
+    public boolean hasDisconnectedFgCall() {
+        return (getFirstCallOfState(mForegroundCalls, Call.State.DISCONNECTED) != null);
+    }
+
+    /**
+     * @return true if there is at least one Foreground call in disconnected state
+     */
+    public boolean hasDisconnectedFgCall(int subId) {
+        return (getFirstCallOfState(mForegroundCalls, Call.State.DISCONNECTED,
+                subId) != null);
+    }
+
+    /**
+     * @return true if there is at least one background call in disconnected state
+     */
+    public boolean hasDisconnectedBgCall() {
+        return (getFirstCallOfState(mBackgroundCalls, Call.State.DISCONNECTED) != null);
+    }
+
+    /**
+     * @return true if there is at least one background call in disconnected state
+     */
+    public boolean hasDisconnectedBgCall(int subId) {
+        return (getFirstCallOfState(mBackgroundCalls, Call.State.DISCONNECTED,
+                subId) != null);
+    }
+
+
+    /**
+     * @return the first active call from a call list
+     */
+    private  Call getFirstActiveCall(ArrayList<Call> calls) {
+        for (Call call : calls) {
+            if (!call.isIdle()) {
+                return call;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the first active call from a call list
+     */
+    private  Call getFirstActiveCall(ArrayList<Call> calls, int subId) {
+        for (Call call : calls) {
+            if ((!call.isIdle()) && ((call.getPhone().getSubId() == subId) ||
+                    (call.getPhone() instanceof SipPhone))) {
+                return call;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the first call in a the Call.state from a call list
+     */
+    private Call getFirstCallOfState(ArrayList<Call> calls, Call.State state) {
+        for (Call call : calls) {
+            if (call.getState() == state) {
+                return call;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the first call in a the Call.state from a call list
+     */
+    private Call getFirstCallOfState(ArrayList<Call> calls, Call.State state,
+            int subId) {
+        for (Call call : calls) {
+            if ((call.getState() == state) ||
+                ((call.getPhone().getSubId() == subId) ||
+                (call.getPhone() instanceof SipPhone))) {
+                return call;
+            }
+        }
+        return null;
+    }
+
+    private boolean hasMoreThanOneRingingCall() {
+        int count = 0;
+        for (Call call : mRingingCalls) {
+            if (call.getState().isRinging()) {
+                if (++count > 1) return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if more than one active ringing call exists on
+     * the active subId.
+     * This checks for the active calls on provided
+     * subId and also active calls on SIP Phone.
+     *
+     */
+    private boolean hasMoreThanOneRingingCall(int subId) {
+        int count = 0;
+        for (Call call : mRingingCalls) {
+            if ((call.getState().isRinging()) &&
+                ((call.getPhone().getSubId() == subId) ||
+                (call.getPhone() instanceof SipPhone))) {
+                if (++count > 1) return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if more than one active background call exists on
+     * the provided subId.
+     * This checks for the background calls on provided
+     * subId and also background calls on SIP Phone.
+     *
+     */
+    private boolean hasMoreThanOneHoldingCall(int subId) {
+        int count = 0;
+        for (Call call : mBackgroundCalls) {
+            if ((call.getState() == Call.State.HOLDING) &&
+                ((call.getPhone().getSubId() == subId) ||
+                (call.getPhone() instanceof SipPhone))) {
+                if (++count > 1) return true;
+            }
+        }
+        return false;
+    }
+
+    /* FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+    private boolean isServiceStateInService() {
+        boolean bInService = false;
+
+        for (Phone phone : mPhones) {
+            bInService = (phone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE);
+            if (bInService) {
+                break;
+            }
+        }
+
+        if (VDBG) Rlog.d(LOG_TAG, "[isServiceStateInService] bInService = " + bInService);
+        return bInService;
+    }
+    */
+
+    private class CallManagerHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+
+            switch (msg.what) {
+                case EVENT_DISCONNECT:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_DISCONNECT)");
+                    mDisconnectRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+                    //mIsEccDialing = false;
+                    break;
+                case EVENT_PRECISE_CALL_STATE_CHANGED:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_PRECISE_CALL_STATE_CHANGED)");
+                    mPreciseCallStateRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_NEW_RINGING_CONNECTION:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_NEW_RINGING_CONNECTION)");
+                    Connection c = (Connection) ((AsyncResult) msg.obj).result;
+                    int subId = c.getCall().getPhone().getSubId();
+                    if (getActiveFgCallState(subId).isDialing() || hasMoreThanOneRingingCall()) {
+                        try {
+                            Rlog.d(LOG_TAG, "silently drop incoming call: " + c.getCall());
+                            c.getCall().hangup();
+                        } catch (CallStateException e) {
+                            Rlog.w(LOG_TAG, "new ringing connection", e);
+                        }
+                    } else {
+                        mNewRingingConnectionRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    }
+                    break;
+                case EVENT_UNKNOWN_CONNECTION:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_UNKNOWN_CONNECTION)");
+                    mUnknownConnectionRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_INCOMING_RING:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_INCOMING_RING)");
+                    // The event may come from RIL who's not aware of an ongoing fg call
+                    if (!hasActiveFgCall()) {
+                        mIncomingRingRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    }
+                    break;
+                case EVENT_RINGBACK_TONE:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_RINGBACK_TONE)");
+                    mRingbackToneRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_IN_CALL_VOICE_PRIVACY_ON:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_IN_CALL_VOICE_PRIVACY_ON)");
+                    mInCallVoicePrivacyOnRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_IN_CALL_VOICE_PRIVACY_OFF:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_IN_CALL_VOICE_PRIVACY_OFF)");
+                    mInCallVoicePrivacyOffRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_CALL_WAITING:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_CALL_WAITING)");
+                    mCallWaitingRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_DISPLAY_INFO:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_DISPLAY_INFO)");
+                    mDisplayInfoRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_SIGNAL_INFO:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_SIGNAL_INFO)");
+                    mSignalInfoRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_CDMA_OTA_STATUS_CHANGE:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_CDMA_OTA_STATUS_CHANGE)");
+                    mCdmaOtaStatusChangeRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_RESEND_INCALL_MUTE:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_RESEND_INCALL_MUTE)");
+                    mResendIncallMuteRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_MMI_INITIATE:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_MMI_INITIATE)");
+                    mMmiInitiateRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_MMI_COMPLETE:
+                    Rlog.d(LOG_TAG, "CallManager: handleMessage (EVENT_MMI_COMPLETE)");
+                    mMmiCompleteRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_ECM_TIMER_RESET:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_ECM_TIMER_RESET)");
+                    mEcmTimerResetRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_SUBSCRIPTION_INFO_READY:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_SUBSCRIPTION_INFO_READY)");
+                    mSubscriptionInfoReadyRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_SUPP_SERVICE_FAILED:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_SUPP_SERVICE_FAILED)");
+                    mSuppServiceFailedRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_SERVICE_STATE_CHANGED:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_SERVICE_STATE_CHANGED)");
+                    mServiceStateChangedRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    // FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+                    //setAudioMode();
+                    break;
+                case EVENT_POST_DIAL_CHARACTER:
+                    // we need send the character that is being processed in msg.arg1
+                    // so can't use notifyRegistrants()
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_POST_DIAL_CHARACTER)");
+                    for(int i=0; i < mPostDialCharacterRegistrants.size(); i++) {
+                        Message notifyMsg;
+                        notifyMsg = ((Registrant)mPostDialCharacterRegistrants.get(i)).messageForRegistrant();
+                        notifyMsg.obj = msg.obj;
+                        notifyMsg.arg1 = msg.arg1;
+                        notifyMsg.sendToTarget();
+                    }
+                    break;
+                case EVENT_ONHOLD_TONE:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_ONHOLD_TONE)");
+                    mOnHoldToneRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                case EVENT_TTY_MODE_RECEIVED:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_TTY_MODE_RECEIVED)");
+                    mTtyModeReceivedRegistrants.notifyRegistrants((AsyncResult) msg.obj);
+                    break;
+                /* FIXME Taken from klp-sprout-dev but setAudioMode was removed in L.
+                case EVENT_RADIO_OFF_OR_NOT_AVAILABLE:
+                    if (VDBG) Rlog.d(LOG_TAG, " handleMessage (EVENT_RADIO_OFF_OR_NOT_AVAILABLE)");
+                    setAudioMode();
+                    break;
+                */
+            }
+        }
+    };
+
+    @Override
+    public String toString() {
+        Call call;
+        StringBuilder b = new StringBuilder();
+        for (int i = 0; i < TelephonyManager.getDefault().getPhoneCount(); i++) {
+            b.append("CallManager {");
+            b.append("\nstate = " + getState(i));
+            call = getActiveFgCall(i);
+            if (call != null) {
+                b.append("\n- Foreground: " + getActiveFgCallState(i));
+                b.append(" from " + call.getPhone());
+                b.append("\n  Conn: ").append(getFgCallConnections(i));
+            }
+            call = getFirstActiveBgCall(i);
+            if (call != null) {
+                b.append("\n- Background: " + call.getState());
+                b.append(" from " + call.getPhone());
+                b.append("\n  Conn: ").append(getBgCallConnections(i));
+            }
+            call = getFirstActiveRingingCall(i);
+            if (call != null) {
+                b.append("\n- Ringing: " +call.getState());
+                b.append(" from " + call.getPhone());
+            }
+        }
+
+        for (Phone phone : getAllPhones()) {
+            if (phone != null) {
+                b.append("\nPhone: " + phone + ", name = " + phone.getPhoneName()
+                        + ", state = " + phone.getState());
+                call = phone.getForegroundCall();
+                if (call != null) {
+                    b.append("\n- Foreground: ").append(call);
+                }
+                call = phone.getBackgroundCall();
+                if (call != null) {
+                    b.append(" Background: ").append(call);
+                }
+                call = phone.getRingingCall();
+                if (call != null) {
+                    b.append(" Ringing: ").append(call);
+                }
+            }
+        }
+        b.append("\n}");
+        return b.toString();
+    }
+}
diff --git a/com/android/internal/telephony/CallStateException.java b/com/android/internal/telephony/CallStateException.java
new file mode 100644
index 0000000..8429146
--- /dev/null
+++ b/com/android/internal/telephony/CallStateException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * {@hide}
+ */
+public class CallStateException extends Exception
+{
+    private int mError = ERROR_INVALID;
+
+    /** The error code is not valid (Not received a disconnect cause) */
+    public static final int ERROR_INVALID = -1;
+
+    public static final int ERROR_OUT_OF_SERVICE = 1;
+    public static final int ERROR_POWER_OFF = 2;
+
+    public
+    CallStateException()
+    {
+    }
+
+    public
+    CallStateException(String string)
+    {
+        super(string);
+    }
+
+    public
+    CallStateException(int error, String string)
+    {
+        super(string);
+        mError = error;
+    }
+
+    public int getError() {
+        return mError;
+    }
+}
diff --git a/com/android/internal/telephony/CallTracker.java b/com/android/internal/telephony/CallTracker.java
new file mode 100644
index 0000000..23874e2
--- /dev/null
+++ b/com/android/internal/telephony/CallTracker.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.SystemProperties;
+import android.telephony.CarrierConfigManager;
+import android.text.TextUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+
+/**
+ * {@hide}
+ */
+public abstract class CallTracker extends Handler {
+
+    private static final boolean DBG_POLL = false;
+
+    //***** Constants
+
+    static final int POLL_DELAY_MSEC = 250;
+
+    protected int mPendingOperations;
+    protected boolean mNeedsPoll;
+    protected Message mLastRelevantPoll;
+    protected ArrayList<Connection> mHandoverConnections = new ArrayList<Connection>();
+
+    public CommandsInterface mCi;
+
+    protected boolean mNumberConverted = false;
+    private final int VALID_COMPARE_LENGTH   = 3;
+
+    //***** Events
+
+    protected static final int EVENT_POLL_CALLS_RESULT             = 1;
+    protected static final int EVENT_CALL_STATE_CHANGE             = 2;
+    protected static final int EVENT_REPOLL_AFTER_DELAY            = 3;
+    protected static final int EVENT_OPERATION_COMPLETE            = 4;
+    protected static final int EVENT_GET_LAST_CALL_FAIL_CAUSE      = 5;
+
+    protected static final int EVENT_SWITCH_RESULT                 = 8;
+    protected static final int EVENT_RADIO_AVAILABLE               = 9;
+    protected static final int EVENT_RADIO_NOT_AVAILABLE           = 10;
+    protected static final int EVENT_CONFERENCE_RESULT             = 11;
+    protected static final int EVENT_SEPARATE_RESULT               = 12;
+    protected static final int EVENT_ECT_RESULT                    = 13;
+    protected static final int EVENT_EXIT_ECM_RESPONSE_CDMA        = 14;
+    protected static final int EVENT_CALL_WAITING_INFO_CDMA        = 15;
+    protected static final int EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA = 16;
+    protected static final int EVENT_THREE_WAY_DIAL_BLANK_FLASH    = 20;
+
+    protected void pollCallsWhenSafe() {
+        mNeedsPoll = true;
+
+        if (checkNoOperationsPending()) {
+            mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);
+            mCi.getCurrentCalls(mLastRelevantPoll);
+        }
+    }
+
+    protected void
+    pollCallsAfterDelay() {
+        Message msg = obtainMessage();
+
+        msg.what = EVENT_REPOLL_AFTER_DELAY;
+        sendMessageDelayed(msg, POLL_DELAY_MSEC);
+    }
+
+    protected boolean
+    isCommandExceptionRadioNotAvailable(Throwable e) {
+        return e != null && e instanceof CommandException
+                && ((CommandException)e).getCommandError()
+                        == CommandException.Error.RADIO_NOT_AVAILABLE;
+    }
+
+    protected abstract void handlePollCalls(AsyncResult ar);
+
+    protected Connection getHoConnection(DriverCall dc) {
+        for (Connection hoConn : mHandoverConnections) {
+            log("getHoConnection - compare number: hoConn= " + hoConn.toString());
+            if (hoConn.getAddress() != null && hoConn.getAddress().contains(dc.number)) {
+                log("getHoConnection: Handover connection match found = " + hoConn.toString());
+                return hoConn;
+            }
+        }
+        for (Connection hoConn : mHandoverConnections) {
+            log("getHoConnection: compare state hoConn= " + hoConn.toString());
+            if (hoConn.getStateBeforeHandover() == Call.stateFromDCState(dc.state)) {
+                log("getHoConnection: Handover connection match found = " + hoConn.toString());
+                return hoConn;
+            }
+        }
+        return null;
+    }
+
+    protected void notifySrvccState(Call.SrvccState state, ArrayList<Connection> c) {
+        if (state == Call.SrvccState.STARTED && c != null) {
+            // SRVCC started. Prepare handover connections list
+            mHandoverConnections.addAll(c);
+        } else if (state != Call.SrvccState.COMPLETED) {
+            // SRVCC FAILED/CANCELED. Clear the handover connections list
+            // Individual connections will be removed from the list in handlePollCalls()
+            mHandoverConnections.clear();
+        }
+        log("notifySrvccState: mHandoverConnections= " + mHandoverConnections.toString());
+    }
+
+    protected void handleRadioAvailable() {
+        pollCallsWhenSafe();
+    }
+
+    /**
+     * Obtain a complete message that indicates that this operation
+     * does not require polling of getCurrentCalls(). However, if other
+     * operations that do need getCurrentCalls() are pending or are
+     * scheduled while this operation is pending, the invocation
+     * of getCurrentCalls() will be postponed until this
+     * operation is also complete.
+     */
+    protected Message
+    obtainNoPollCompleteMessage(int what) {
+        mPendingOperations++;
+        mLastRelevantPoll = null;
+        return obtainMessage(what);
+    }
+
+    /**
+     * @return true if we're idle or there's a call to getCurrentCalls() pending
+     * but nothing else
+     */
+    private boolean
+    checkNoOperationsPending() {
+        if (DBG_POLL) log("checkNoOperationsPending: pendingOperations=" +
+                mPendingOperations);
+        return mPendingOperations == 0;
+    }
+
+    /**
+     * Routine called from dial to check if the number is a test Emergency number
+     * and if so remap the number. This allows a short emergency number to be remapped
+     * to a regular number for testing how the frameworks handles emergency numbers
+     * without actually calling an emergency number.
+     *
+     * This is not a full test and is not a substitute for testing real emergency
+     * numbers but can be useful.
+     *
+     * To use this feature set a system property ril.test.emergencynumber to a pair of
+     * numbers separated by a colon. If the first number matches the number parameter
+     * this routine returns the second number. Example:
+     *
+     * ril.test.emergencynumber=112:1-123-123-45678
+     *
+     * To test Dial 112 take call then hang up on MO device to enter ECM
+     * see RIL#processSolicited RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND
+     *
+     * @param dialString to test if it should be remapped
+     * @return the same number or the remapped number.
+     */
+    protected String checkForTestEmergencyNumber(String dialString) {
+        String testEn = SystemProperties.get("ril.test.emergencynumber");
+        if (DBG_POLL) {
+            log("checkForTestEmergencyNumber: dialString=" + dialString +
+                " testEn=" + testEn);
+        }
+        if (!TextUtils.isEmpty(testEn)) {
+            String values[] = testEn.split(":");
+            log("checkForTestEmergencyNumber: values.length=" + values.length);
+            if (values.length == 2) {
+                if (values[0].equals(
+                        android.telephony.PhoneNumberUtils.stripSeparators(dialString))) {
+                    // mCi will be null for ImsPhoneCallTracker.
+                    if (mCi != null) {
+                        mCi.testingEmergencyCall();
+                    }
+                    log("checkForTestEmergencyNumber: remap " +
+                            dialString + " to " + values[1]);
+                    dialString = values[1];
+                }
+            }
+        }
+        return dialString;
+    }
+
+    protected String convertNumberIfNecessary(Phone phone, String dialNumber) {
+        if (dialNumber == null) {
+            return dialNumber;
+        }
+        String[] convertMaps = null;
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle bundle = configManager.getConfig();
+        if (bundle != null) {
+            convertMaps =
+                    bundle.getStringArray(CarrierConfigManager.KEY_DIAL_STRING_REPLACE_STRING_ARRAY);
+        }
+        if (convertMaps == null) {
+            // By default no replacement is necessary
+            log("convertNumberIfNecessary convertMaps is null");
+            return dialNumber;
+        }
+
+        log("convertNumberIfNecessary Roaming"
+            + " convertMaps.length " + convertMaps.length
+            + " dialNumber.length() " + dialNumber.length());
+
+        if (convertMaps.length < 1 || dialNumber.length() < VALID_COMPARE_LENGTH) {
+            return dialNumber;
+        }
+
+        String[] entry;
+        String outNumber = "";
+        for(String convertMap : convertMaps) {
+            log("convertNumberIfNecessary: " + convertMap);
+            // entry format is  "dialStringToReplace:dialStringReplacement"
+            entry = convertMap.split(":");
+            if (entry != null && entry.length > 1) {
+                String dsToReplace = entry[0];
+                String dsReplacement = entry[1];
+                if (!TextUtils.isEmpty(dsToReplace) && dialNumber.equals(dsToReplace)) {
+                    // Needs to be converted
+                    if (!TextUtils.isEmpty(dsReplacement) && dsReplacement.endsWith("MDN")) {
+                        String mdn = phone.getLine1Number();
+                        if (!TextUtils.isEmpty(mdn)) {
+                            if (mdn.startsWith("+")) {
+                                outNumber = mdn;
+                            } else {
+                                outNumber = dsReplacement.substring(0, dsReplacement.length() -3)
+                                        + mdn;
+                            }
+                        }
+                    } else {
+                        outNumber = dsReplacement;
+                    }
+                    break;
+                }
+            }
+        }
+
+        if (!TextUtils.isEmpty(outNumber)) {
+            log("convertNumberIfNecessary: convert service number");
+            mNumberConverted = true;
+            return outNumber;
+        }
+
+        return dialNumber;
+
+    }
+
+    private boolean compareGid1(Phone phone, String serviceGid1) {
+        String gid1 = phone.getGroupIdLevel1();
+        int gid_length = serviceGid1.length();
+        boolean ret = true;
+
+        if (serviceGid1 == null || serviceGid1.equals("")) {
+            log("compareGid1 serviceGid is empty, return " + ret);
+            return ret;
+        }
+        // Check if gid1 match service GID1
+        if (!((gid1 != null) && (gid1.length() >= gid_length) &&
+                gid1.substring(0, gid_length).equalsIgnoreCase(serviceGid1))) {
+            log(" gid1 " + gid1 + " serviceGid1 " + serviceGid1);
+            ret = false;
+        }
+        log("compareGid1 is " + (ret?"Same":"Different"));
+        return ret;
+    }
+
+    //***** Overridden from Handler
+    @Override
+    public abstract void handleMessage (Message msg);
+    public abstract void registerForVoiceCallStarted(Handler h, int what, Object obj);
+    public abstract void unregisterForVoiceCallStarted(Handler h);
+    public abstract void registerForVoiceCallEnded(Handler h, int what, Object obj);
+    public abstract void unregisterForVoiceCallEnded(Handler h);
+    public abstract PhoneConstants.State getState();
+    protected abstract void log(String msg);
+
+    /**
+     * Called when the call tracker should attempt to reconcile its calls against its underlying
+     * phone implementation and cleanup any stale calls.
+     */
+    public void cleanupCalls() {
+        // no base implementation
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CallTracker:");
+        pw.println(" mPendingOperations=" + mPendingOperations);
+        pw.println(" mNeedsPoll=" + mNeedsPoll);
+        pw.println(" mLastRelevantPoll=" + mLastRelevantPoll);
+    }
+}
diff --git a/com/android/internal/telephony/CallerInfo.java b/com/android/internal/telephony/CallerInfo.java
new file mode 100644
index 0000000..f646028
--- /dev/null
+++ b/com/android/internal/telephony/CallerInfo.java
@@ -0,0 +1,685 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.location.Country;
+import android.location.CountryDetector;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.RawContacts;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import android.telephony.SubscriptionManager;
+
+import java.util.Locale;
+
+
+/**
+ * Looks up caller information for the given phone number.
+ *
+ * {@hide}
+ */
+public class CallerInfo {
+    private static final String TAG = "CallerInfo";
+    private static final boolean VDBG = Rlog.isLoggable(TAG, Log.VERBOSE);
+
+    public static final long USER_TYPE_CURRENT = 0;
+    public static final long USER_TYPE_WORK = 1;
+
+    /**
+     * Please note that, any one of these member variables can be null,
+     * and any accesses to them should be prepared to handle such a case.
+     *
+     * Also, it is implied that phoneNumber is more often populated than
+     * name is, (think of calls being dialed/received using numbers where
+     * names are not known to the device), so phoneNumber should serve as
+     * a dependable fallback when name is unavailable.
+     *
+     * One other detail here is that this CallerInfo object reflects
+     * information found on a connection, it is an OUTPUT that serves
+     * mainly to display information to the user.  In no way is this object
+     * used as input to make a connection, so we can choose to display
+     * whatever human-readable text makes sense to the user for a
+     * connection.  This is especially relevant for the phone number field,
+     * since it is the one field that is most likely exposed to the user.
+     *
+     * As an example:
+     *   1. User dials "911"
+     *   2. Device recognizes that this is an emergency number
+     *   3. We use the "Emergency Number" string instead of "911" in the
+     *     phoneNumber field.
+     *
+     * What we're really doing here is treating phoneNumber as an essential
+     * field here, NOT name.  We're NOT always guaranteed to have a name
+     * for a connection, but the number should be displayable.
+     */
+    public String name;
+    public String phoneNumber;
+    public String normalizedNumber;
+    public String geoDescription;
+
+    public String cnapName;
+    public int numberPresentation;
+    public int namePresentation;
+    public boolean contactExists;
+
+    public String phoneLabel;
+    /* Split up the phoneLabel into number type and label name */
+    public int    numberType;
+    public String numberLabel;
+
+    public int photoResource;
+
+    // Contact ID, which will be 0 if a contact comes from the corp CP2.
+    public long contactIdOrZero;
+    public boolean needUpdate;
+    public Uri contactRefUri;
+    public String lookupKey;
+
+    public long userType;
+
+    /**
+     * Contact display photo URI.  If a contact has no display photo but a thumbnail, it'll be
+     * the thumbnail URI instead.
+     */
+    public Uri contactDisplayPhotoUri;
+
+    // fields to hold individual contact preference data,
+    // including the send to voicemail flag and the ringtone
+    // uri reference.
+    public Uri contactRingtoneUri;
+    public boolean shouldSendToVoicemail;
+
+    /**
+     * Drawable representing the caller image.  This is essentially
+     * a cache for the image data tied into the connection /
+     * callerinfo object.
+     *
+     * This might be a high resolution picture which is more suitable
+     * for full-screen image view than for smaller icons used in some
+     * kinds of notifications.
+     *
+     * The {@link #isCachedPhotoCurrent} flag indicates if the image
+     * data needs to be reloaded.
+     */
+    public Drawable cachedPhoto;
+    /**
+     * Bitmap representing the caller image which has possibly lower
+     * resolution than {@link #cachedPhoto} and thus more suitable for
+     * icons (like notification icons).
+     *
+     * In usual cases this is just down-scaled image of {@link #cachedPhoto}.
+     * If the down-scaling fails, this will just become null.
+     *
+     * The {@link #isCachedPhotoCurrent} flag indicates if the image
+     * data needs to be reloaded.
+     */
+    public Bitmap cachedPhotoIcon;
+    /**
+     * Boolean which indicates if {@link #cachedPhoto} and
+     * {@link #cachedPhotoIcon} is fresh enough. If it is false,
+     * those images aren't pointing to valid objects.
+     */
+    public boolean isCachedPhotoCurrent;
+
+    private boolean mIsEmergency;
+    private boolean mIsVoiceMail;
+
+    public CallerInfo() {
+        // TODO: Move all the basic initialization here?
+        mIsEmergency = false;
+        mIsVoiceMail = false;
+        userType = USER_TYPE_CURRENT;
+    }
+
+    /**
+     * getCallerInfo given a Cursor.
+     * @param context the context used to retrieve string constants
+     * @param contactRef the URI to attach to this CallerInfo object
+     * @param cursor the first object in the cursor is used to build the CallerInfo object.
+     * @return the CallerInfo which contains the caller id for the given
+     * number. The returned CallerInfo is null if no number is supplied.
+     */
+    public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) {
+        CallerInfo info = new CallerInfo();
+        info.photoResource = 0;
+        info.phoneLabel = null;
+        info.numberType = 0;
+        info.numberLabel = null;
+        info.cachedPhoto = null;
+        info.isCachedPhotoCurrent = false;
+        info.contactExists = false;
+        info.userType = USER_TYPE_CURRENT;
+
+        if (VDBG) Rlog.v(TAG, "getCallerInfo() based on cursor...");
+
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                // TODO: photo_id is always available but not taken
+                // care of here. Maybe we should store it in the
+                // CallerInfo object as well.
+
+                int columnIndex;
+
+                // Look for the name
+                columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+                if (columnIndex != -1) {
+                    info.name = cursor.getString(columnIndex);
+                }
+
+                // Look for the number
+                columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
+                if (columnIndex != -1) {
+                    info.phoneNumber = cursor.getString(columnIndex);
+                }
+
+                // Look for the normalized number
+                columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER);
+                if (columnIndex != -1) {
+                    info.normalizedNumber = cursor.getString(columnIndex);
+                }
+
+                // Look for the label/type combo
+                columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL);
+                if (columnIndex != -1) {
+                    int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE);
+                    if (typeColumnIndex != -1) {
+                        info.numberType = cursor.getInt(typeColumnIndex);
+                        info.numberLabel = cursor.getString(columnIndex);
+                        info.phoneLabel = Phone.getDisplayLabel(context,
+                                info.numberType, info.numberLabel)
+                                .toString();
+                    }
+                }
+
+                // Look for the person_id.
+                columnIndex = getColumnIndexForPersonId(contactRef, cursor);
+                if (columnIndex != -1) {
+                    final long contactId = cursor.getLong(columnIndex);
+                    if (contactId != 0 && !Contacts.isEnterpriseContactId(contactId)) {
+                        info.contactIdOrZero = contactId;
+                        if (VDBG) {
+                            Rlog.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero);
+                        }
+                    }
+                    if (Contacts.isEnterpriseContactId(contactId)) {
+                        info.userType = USER_TYPE_WORK;
+                    }
+                } else {
+                    // No valid columnIndex, so we can't look up person_id.
+                    Rlog.w(TAG, "Couldn't find contact_id column for " + contactRef);
+                    // Watch out: this means that anything that depends on
+                    // person_id will be broken (like contact photo lookups in
+                    // the in-call UI, for example.)
+                }
+
+                // Contact lookupKey
+                columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+                if (columnIndex != -1) {
+                    info.lookupKey = cursor.getString(columnIndex);
+                }
+
+                // Display photo URI.
+                columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+                if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+                    info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex));
+                } else {
+                    info.contactDisplayPhotoUri = null;
+                }
+
+                // look for the custom ringtone, create from the string stored
+                // in the database.
+                // An empty string ("") in the database indicates a silent ringtone,
+                // and we set contactRingtoneUri = Uri.EMPTY, so that no ringtone will be played.
+                // {null} in the database indicates the default ringtone,
+                // and we set contactRingtoneUri = null, so that default ringtone will be played.
+                columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE);
+                if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+                    if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
+                        info.contactRingtoneUri = Uri.EMPTY;
+                    } else {
+                        info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex));
+                    }
+                } else {
+                    info.contactRingtoneUri = null;
+                }
+
+                // look for the send to voicemail flag, set it to true only
+                // under certain circumstances.
+                columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL);
+                info.shouldSendToVoicemail = (columnIndex != -1) &&
+                        ((cursor.getInt(columnIndex)) == 1);
+                info.contactExists = true;
+            }
+            cursor.close();
+            cursor = null;
+        }
+
+        info.needUpdate = false;
+        info.name = normalize(info.name);
+        info.contactRefUri = contactRef;
+
+        return info;
+    }
+
+    /**
+     * getCallerInfo given a URI, look up in the call-log database
+     * for the uri unique key.
+     * @param context the context used to get the ContentResolver
+     * @param contactRef the URI used to lookup caller id
+     * @return the CallerInfo which contains the caller id for the given
+     * number. The returned CallerInfo is null if no number is supplied.
+     */
+    public static CallerInfo getCallerInfo(Context context, Uri contactRef) {
+        CallerInfo info = null;
+        ContentResolver cr = CallerInfoAsyncQuery.getCurrentProfileContentResolver(context);
+        if (cr != null) {
+            try {
+                info = getCallerInfo(context, contactRef,
+                        cr.query(contactRef, null, null, null, null));
+            } catch (RuntimeException re) {
+                Rlog.e(TAG, "Error getting caller info.", re);
+            }
+        }
+        return info;
+    }
+
+    /**
+     * getCallerInfo given a phone number, look up in the call-log database
+     * for the matching caller id info.
+     * @param context the context used to get the ContentResolver
+     * @param number the phone number used to lookup caller id
+     * @return the CallerInfo which contains the caller id for the given
+     * number. The returned CallerInfo is null if no number is supplied. If
+     * a matching number is not found, then a generic caller info is returned,
+     * with all relevant fields empty or null.
+     */
+    public static CallerInfo getCallerInfo(Context context, String number) {
+        if (VDBG) Rlog.v(TAG, "getCallerInfo() based on number...");
+
+        int subId = SubscriptionManager.getDefaultSubscriptionId();
+        return getCallerInfo(context, number, subId);
+    }
+
+    /**
+     * getCallerInfo given a phone number and subscription, look up in the call-log database
+     * for the matching caller id info.
+     * @param context the context used to get the ContentResolver
+     * @param number the phone number used to lookup caller id
+     * @param subId the subscription for checking for if voice mail number or not
+     * @return the CallerInfo which contains the caller id for the given
+     * number. The returned CallerInfo is null if no number is supplied. If
+     * a matching number is not found, then a generic caller info is returned,
+     * with all relevant fields empty or null.
+     */
+    public static CallerInfo getCallerInfo(Context context, String number, int subId) {
+
+        if (TextUtils.isEmpty(number)) {
+            return null;
+        }
+
+        // Change the callerInfo number ONLY if it is an emergency number
+        // or if it is the voicemail number.  If it is either, take a
+        // shortcut and skip the query.
+        if (PhoneNumberUtils.isLocalEmergencyNumber(context, number)) {
+            return new CallerInfo().markAsEmergency(context);
+        } else if (PhoneNumberUtils.isVoiceMailNumber(subId, number)) {
+            return new CallerInfo().markAsVoiceMail();
+        }
+
+        Uri contactUri = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
+                Uri.encode(number));
+
+        CallerInfo info = getCallerInfo(context, contactUri);
+        info = doSecondaryLookupIfNecessary(context, number, info);
+
+        // if no query results were returned with a viable number,
+        // fill in the original number value we used to query with.
+        if (TextUtils.isEmpty(info.phoneNumber)) {
+            info.phoneNumber = number;
+        }
+
+        return info;
+    }
+
+    /**
+     * Performs another lookup if previous lookup fails and it's a SIP call
+     * and the peer's username is all numeric. Look up the username as it
+     * could be a PSTN number in the contact database.
+     *
+     * @param context the query context
+     * @param number the original phone number, could be a SIP URI
+     * @param previousResult the result of previous lookup
+     * @return previousResult if it's not the case
+     */
+    static CallerInfo doSecondaryLookupIfNecessary(Context context,
+            String number, CallerInfo previousResult) {
+        if (!previousResult.contactExists
+                && PhoneNumberUtils.isUriNumber(number)) {
+            String username = PhoneNumberUtils.getUsernameFromUriNumber(number);
+            if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+                previousResult = getCallerInfo(context,
+                        Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
+                                Uri.encode(username)));
+            }
+        }
+        return previousResult;
+    }
+
+    // Accessors
+
+    /**
+     * @return true if the caller info is an emergency number.
+     */
+    public boolean isEmergencyNumber() {
+        return mIsEmergency;
+    }
+
+    /**
+     * @return true if the caller info is a voicemail number.
+     */
+    public boolean isVoiceMailNumber() {
+        return mIsVoiceMail;
+    }
+
+    /**
+     * Mark this CallerInfo as an emergency call.
+     * @param context To lookup the localized 'Emergency Number' string.
+     * @return this instance.
+     */
+    // TODO: Note we're setting the phone number here (refer to
+    // javadoc comments at the top of CallerInfo class) to a localized
+    // string 'Emergency Number'. This is pretty bad because we are
+    // making UI work here instead of just packaging the data. We
+    // should set the phone number to the dialed number and name to
+    // 'Emergency Number' and let the UI make the decision about what
+    // should be displayed.
+    /* package */ CallerInfo markAsEmergency(Context context) {
+        phoneNumber = context.getString(
+            com.android.internal.R.string.emergency_call_dialog_number_for_display);
+        photoResource = com.android.internal.R.drawable.picture_emergency;
+        mIsEmergency = true;
+        return this;
+    }
+
+
+    /**
+     * Mark this CallerInfo as a voicemail call. The voicemail label
+     * is obtained from the telephony manager. Caller must hold the
+     * READ_PHONE_STATE permission otherwise the phoneNumber will be
+     * set to null.
+     * @return this instance.
+     */
+    // TODO: As in the emergency number handling, we end up writing a
+    // string in the phone number field.
+    /* package */ CallerInfo markAsVoiceMail() {
+
+        int subId = SubscriptionManager.getDefaultSubscriptionId();
+        return markAsVoiceMail(subId);
+
+    }
+
+    /* package */ CallerInfo markAsVoiceMail(int subId) {
+        mIsVoiceMail = true;
+
+        try {
+            String voiceMailLabel = TelephonyManager.getDefault().getVoiceMailAlphaTag(subId);
+
+            phoneNumber = voiceMailLabel;
+        } catch (SecurityException se) {
+            // Should never happen: if this process does not have
+            // permission to retrieve VM tag, it should not have
+            // permission to retrieve VM number and would not call
+            // this method.
+            // Leave phoneNumber untouched.
+            Rlog.e(TAG, "Cannot access VoiceMail.", se);
+        }
+        // TODO: There is no voicemail picture?
+        // FIXME: FIND ANOTHER ICON
+        // photoResource = android.R.drawable.badge_voicemail;
+        return this;
+    }
+
+    private static String normalize(String s) {
+        if (s == null || s.length() > 0) {
+            return s;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the column index to use to find the "person_id" field in
+     * the specified cursor, based on the contact URI that was originally
+     * queried.
+     *
+     * This is a helper function for the getCallerInfo() method that takes
+     * a Cursor.  Looking up the person_id is nontrivial (compared to all
+     * the other CallerInfo fields) since the column we need to use
+     * depends on what query we originally ran.
+     *
+     * Watch out: be sure to not do any database access in this method, since
+     * it's run from the UI thread (see comments below for more info.)
+     *
+     * @return the columnIndex to use (with cursor.getLong()) to get the
+     * person_id, or -1 if we couldn't figure out what colum to use.
+     *
+     * TODO: Add a unittest for this method.  (This is a little tricky to
+     * test, since we'll need a live contacts database to test against,
+     * preloaded with at least some phone numbers and SIP addresses.  And
+     * we'll probably have to hardcode the column indexes we expect, so
+     * the test might break whenever the contacts schema changes.  But we
+     * can at least make sure we handle all the URI patterns we claim to,
+     * and that the mime types match what we expect...)
+     */
+    private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) {
+        // TODO: This is pretty ugly now, see bug 2269240 for
+        // more details. The column to use depends upon the type of URL:
+        // - content://com.android.contacts/data/phones ==> use the "contact_id" column
+        // - content://com.android.contacts/phone_lookup ==> use the "_ID" column
+        // - content://com.android.contacts/data ==> use the "contact_id" column
+        // If it's none of the above, we leave columnIndex=-1 which means
+        // that the person_id field will be left unset.
+        //
+        // The logic here *used* to be based on the mime type of contactRef
+        // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the
+        // RawContacts.CONTACT_ID column).  But looking up the mime type requires
+        // a call to context.getContentResolver().getType(contactRef), which
+        // isn't safe to do from the UI thread since it can cause an ANR if
+        // the contacts provider is slow or blocked (like during a sync.)
+        //
+        // So instead, figure out the column to use for person_id by just
+        // looking at the URI itself.
+
+        if (VDBG) Rlog.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '"
+                        + contactRef + "'...");
+        // Warning: Do not enable the following logging (due to ANR risk.)
+        // if (VDBG) Rlog.v(TAG, "- MIME type: "
+        //                 + context.getContentResolver().getType(contactRef));
+
+        String url = contactRef.toString();
+        String columnName = null;
+        if (url.startsWith("content://com.android.contacts/data/phones")) {
+            // Direct lookup in the Phone table.
+            // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2")
+            if (VDBG) Rlog.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID");
+            columnName = RawContacts.CONTACT_ID;
+        } else if (url.startsWith("content://com.android.contacts/data")) {
+            // Direct lookup in the Data table.
+            // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data")
+            if (VDBG) Rlog.v(TAG, "'data' URI; using Data.CONTACT_ID");
+            // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.)
+            columnName = Data.CONTACT_ID;
+        } else if (url.startsWith("content://com.android.contacts/phone_lookup")) {
+            // Lookup in the PhoneLookup table, which provides "fuzzy matching"
+            // for phone numbers.
+            // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup")
+            if (VDBG) Rlog.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID");
+            columnName = PhoneLookup._ID;
+        } else {
+            Rlog.w(TAG, "Unexpected prefix for contactRef '" + url + "'");
+        }
+        int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1;
+        if (VDBG) Rlog.v(TAG, "==> Using column '" + columnName
+                        + "' (columnIndex = " + columnIndex + ") for person_id lookup...");
+        return columnIndex;
+    }
+
+    /**
+     * Updates this CallerInfo's geoDescription field, based on the raw
+     * phone number in the phoneNumber field.
+     *
+     * (Note that the various getCallerInfo() methods do *not* set the
+     * geoDescription automatically; you need to call this method
+     * explicitly to get it.)
+     *
+     * @param context the context used to look up the current locale / country
+     * @param fallbackNumber if this CallerInfo's phoneNumber field is empty,
+     *        this specifies a fallback number to use instead.
+     */
+    public void updateGeoDescription(Context context, String fallbackNumber) {
+        String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber;
+        geoDescription = getGeoDescription(context, number);
+    }
+
+    /**
+     * @return a geographical description string for the specified number.
+     * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
+     */
+    public static String getGeoDescription(Context context, String number) {
+        if (VDBG) Rlog.v(TAG, "getGeoDescription('" + number + "')...");
+
+        if (TextUtils.isEmpty(number)) {
+            return null;
+        }
+
+        PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+        PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+
+        Locale locale = context.getResources().getConfiguration().locale;
+        String countryIso = getCurrentCountryIso(context, locale);
+        PhoneNumber pn = null;
+        try {
+            if (VDBG) Rlog.v(TAG, "parsing '" + number
+                            + "' for countryIso '" + countryIso + "'...");
+            pn = util.parse(number, countryIso);
+            if (VDBG) Rlog.v(TAG, "- parsed number: " + pn);
+        } catch (NumberParseException e) {
+            Rlog.w(TAG, "getGeoDescription: NumberParseException for incoming number '"
+                    + Rlog.pii(TAG, number) + "'");
+        }
+
+        if (pn != null) {
+            String description = geocoder.getDescriptionForNumber(pn, locale);
+            if (VDBG) Rlog.v(TAG, "- got description: '" + description + "'");
+            return description;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @return The ISO 3166-1 two letters country code of the country the user
+     *         is in.
+     */
+    private static String getCurrentCountryIso(Context context, Locale locale) {
+        String countryIso = null;
+        CountryDetector detector = (CountryDetector) context.getSystemService(
+                Context.COUNTRY_DETECTOR);
+        if (detector != null) {
+            Country country = detector.detectCountry();
+            if (country != null) {
+                countryIso = country.getCountryIso();
+            } else {
+                Rlog.e(TAG, "CountryDetector.detectCountry() returned null.");
+            }
+        }
+        if (countryIso == null) {
+            countryIso = locale.getCountry();
+            Rlog.w(TAG, "No CountryDetector; falling back to countryIso based on locale: "
+                    + countryIso);
+        }
+        return countryIso;
+    }
+
+    protected static String getCurrentCountryIso(Context context) {
+        return getCurrentCountryIso(context, Locale.getDefault());
+    }
+
+    /**
+     * @return a string debug representation of this instance.
+     */
+    @Override
+    public String toString() {
+        // Warning: never check in this file with VERBOSE_DEBUG = true
+        // because that will result in PII in the system log.
+        final boolean VERBOSE_DEBUG = false;
+
+        if (VERBOSE_DEBUG) {
+            return new StringBuilder(384)
+                    .append(super.toString() + " { ")
+                    .append("\nname: " + name)
+                    .append("\nphoneNumber: " + phoneNumber)
+                    .append("\nnormalizedNumber: " + normalizedNumber)
+                    .append("\ngeoDescription: " + geoDescription)
+                    .append("\ncnapName: " + cnapName)
+                    .append("\nnumberPresentation: " + numberPresentation)
+                    .append("\nnamePresentation: " + namePresentation)
+                    .append("\ncontactExits: " + contactExists)
+                    .append("\nphoneLabel: " + phoneLabel)
+                    .append("\nnumberType: " + numberType)
+                    .append("\nnumberLabel: " + numberLabel)
+                    .append("\nphotoResource: " + photoResource)
+                    .append("\ncontactIdOrZero: " + contactIdOrZero)
+                    .append("\nneedUpdate: " + needUpdate)
+                    .append("\ncontactRingtoneUri: " + contactRingtoneUri)
+                    .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri)
+                    .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail)
+                    .append("\ncachedPhoto: " + cachedPhoto)
+                    .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent)
+                    .append("\nemergency: " + mIsEmergency)
+                    .append("\nvoicemail " + mIsVoiceMail)
+                    .append("\ncontactExists " + contactExists)
+                    .append("\nuserType " + userType)
+                    .append(" }")
+                    .toString();
+        } else {
+            return new StringBuilder(128)
+                    .append(super.toString() + " { ")
+                    .append("name " + ((name == null) ? "null" : "non-null"))
+                    .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null"))
+                    .append(" }")
+                    .toString();
+        }
+    }
+}
diff --git a/com/android/internal/telephony/CallerInfoAsyncQuery.java b/com/android/internal/telephony/CallerInfoAsyncQuery.java
new file mode 100644
index 0000000..af993be
--- /dev/null
+++ b/com/android/internal/telephony/CallerInfoAsyncQuery.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.app.ActivityManager;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to make it easier to run asynchronous caller-id lookup queries.
+ * @see CallerInfo
+ *
+ * {@hide}
+ */
+public class CallerInfoAsyncQuery {
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "CallerInfoAsyncQuery";
+
+    private static final int EVENT_NEW_QUERY = 1;
+    private static final int EVENT_ADD_LISTENER = 2;
+    private static final int EVENT_END_OF_QUEUE = 3;
+    private static final int EVENT_EMERGENCY_NUMBER = 4;
+    private static final int EVENT_VOICEMAIL_NUMBER = 5;
+    private static final int EVENT_GET_GEO_DESCRIPTION = 6;
+
+    private CallerInfoAsyncQueryHandler mHandler;
+
+    // If the CallerInfo query finds no contacts, should we use the
+    // PhoneNumberOfflineGeocoder to look up a "geo description"?
+    // (TODO: This could become a flag in config.xml if it ever needs to be
+    // configured on a per-product basis.)
+    private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
+
+    /**
+     * Interface for a CallerInfoAsyncQueryHandler result return.
+     */
+    public interface OnQueryCompleteListener {
+        /**
+         * Called when the query is complete.
+         */
+        public void onQueryComplete(int token, Object cookie, CallerInfo ci);
+    }
+
+
+    /**
+     * Wrap the cookie from the WorkerArgs with additional information needed by our
+     * classes.
+     */
+    private static final class CookieWrapper {
+        public OnQueryCompleteListener listener;
+        public Object cookie;
+        public int event;
+        public String number;
+        public String geoDescription;
+
+        public int subId;
+    }
+
+
+    /**
+     * Simple exception used to communicate problems with the query pool.
+     */
+    public static class QueryPoolException extends SQLException {
+        public QueryPoolException(String error) {
+            super(error);
+        }
+    }
+
+    /**
+     * @return {@link ContentResolver} for the "current" user.
+     */
+    static ContentResolver getCurrentProfileContentResolver(Context context) {
+
+        if (DBG) Rlog.d(LOG_TAG, "Trying to get current content resolver...");
+
+        final int currentUser = ActivityManager.getCurrentUser();
+        final int myUser = UserManager.get(context).getUserHandle();
+
+        if (DBG) Rlog.d(LOG_TAG, "myUser=" + myUser + "currentUser=" + currentUser);
+
+        if (myUser != currentUser) {
+            final Context otherContext;
+            try {
+                otherContext = context.createPackageContextAsUser(context.getPackageName(),
+                        /* flags =*/ 0, new UserHandle(currentUser));
+                return otherContext.getContentResolver();
+            } catch (NameNotFoundException e) {
+                Rlog.e(LOG_TAG, "Can't find self package", e);
+                // Fall back to the primary user.
+            }
+        }
+        return context.getContentResolver();
+    }
+
+    /**
+     * Our own implementation of the AsyncQueryHandler.
+     */
+    private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
+
+        /*
+         * The information relevant to each CallerInfo query.  Each query may have multiple
+         * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper
+         * objects in the queue (one with a new query event, and one with a end event, with
+         * 0 or more additional listeners in between).
+         */
+
+        /**
+         * Context passed by the caller.
+         *
+         * NOTE: The actual context we use for query may *not* be this context; since we query
+         * against the "current" contacts provider.  In the constructor we pass the "current"
+         * context resolver (obtained via {@link #getCurrentProfileContentResolver) and pass it
+         * to the super class.
+         */
+        private Context mContext;
+        private Uri mQueryUri;
+        private CallerInfo mCallerInfo;
+        private List<Runnable> mPendingListenerCallbacks = new ArrayList<>();
+
+        /**
+         * Our own query worker thread.
+         *
+         * This thread handles the messages enqueued in the looper.  The normal sequence
+         * of events is that a new query shows up in the looper queue, followed by 0 or
+         * more add listener requests, and then an end request.  Of course, these requests
+         * can be interlaced with requests from other tokens, but is irrelevant to this
+         * handler since the handler has no state.
+         *
+         * Note that we depend on the queue to keep things in order; in other words, the
+         * looper queue must be FIFO with respect to input from the synchronous startQuery
+         * calls and output to this handleMessage call.
+         *
+         * This use of the queue is required because CallerInfo objects may be accessed
+         * multiple times before the query is complete.  All accesses (listeners) must be
+         * queued up and informed in order when the query is complete.
+         */
+        protected class CallerInfoWorkerHandler extends WorkerHandler {
+            public CallerInfoWorkerHandler(Looper looper) {
+                super(looper);
+            }
+
+            @Override
+            public void handleMessage(Message msg) {
+                WorkerArgs args = (WorkerArgs) msg.obj;
+                CookieWrapper cw = (CookieWrapper) args.cookie;
+
+                if (cw == null) {
+                    // Normally, this should never be the case for calls originating
+                    // from within this code.
+                    // However, if there is any code that this Handler calls (such as in
+                    // super.handleMessage) that DOES place unexpected messages on the
+                    // queue, then we need pass these messages on.
+                    Rlog.i(LOG_TAG, "Unexpected command (CookieWrapper is null): " + msg.what +
+                            " ignored by CallerInfoWorkerHandler, passing onto parent.");
+
+                    super.handleMessage(msg);
+                } else {
+
+                    Rlog.d(LOG_TAG, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 +
+                        " command: " + msg.what + " query URI: " + sanitizeUriToString(args.uri));
+
+                    switch (cw.event) {
+                        case EVENT_NEW_QUERY:
+                            //start the sql command.
+                            super.handleMessage(msg);
+                            break;
+
+                        // shortcuts to avoid query for recognized numbers.
+                        case EVENT_EMERGENCY_NUMBER:
+                        case EVENT_VOICEMAIL_NUMBER:
+
+                        case EVENT_ADD_LISTENER:
+                        case EVENT_END_OF_QUEUE:
+                            // query was already completed, so just send the reply.
+                            // passing the original token value back to the caller
+                            // on top of the event values in arg1.
+                            Message reply = args.handler.obtainMessage(msg.what);
+                            reply.obj = args;
+                            reply.arg1 = msg.arg1;
+
+                            reply.sendToTarget();
+
+                            break;
+                        case EVENT_GET_GEO_DESCRIPTION:
+                            handleGeoDescription(msg);
+                            break;
+                        default:
+                    }
+                }
+            }
+
+            private void handleGeoDescription(Message msg) {
+                WorkerArgs args = (WorkerArgs) msg.obj;
+                CookieWrapper cw = (CookieWrapper) args.cookie;
+                if (!TextUtils.isEmpty(cw.number) && cw.cookie != null && mContext != null) {
+                    final long startTimeMillis = SystemClock.elapsedRealtime();
+                    cw.geoDescription = CallerInfo.getGeoDescription(mContext, cw.number);
+                    final long duration = SystemClock.elapsedRealtime() - startTimeMillis;
+                    if (duration > 500) {
+                        if (DBG) Rlog.d(LOG_TAG, "[handleGeoDescription]" +
+                                "Spends long time to retrieve Geo description: " + duration);
+                    }
+                }
+                Message reply = args.handler.obtainMessage(msg.what);
+                reply.obj = args;
+                reply.arg1 = msg.arg1;
+                reply.sendToTarget();
+            }
+        }
+
+
+        /**
+         * Asynchronous query handler class for the contact / callerinfo object.
+         */
+        private CallerInfoAsyncQueryHandler(Context context) {
+            super(getCurrentProfileContentResolver(context));
+            mContext = context;
+        }
+
+        @Override
+        protected Handler createHandler(Looper looper) {
+            return new CallerInfoWorkerHandler(looper);
+        }
+
+        /**
+         * Overrides onQueryComplete from AsyncQueryHandler.
+         *
+         * This method takes into account the state of this class; we construct the CallerInfo
+         * object only once for each set of listeners. When the query thread has done its work
+         * and calls this method, we inform the remaining listeners in the queue, until we're
+         * out of listeners.  Once we get the message indicating that we should expect no new
+         * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the
+         * pool.
+         */
+        @Override
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            Rlog.d(LOG_TAG, "##### onQueryComplete() #####   query complete for token: " + token);
+
+            //get the cookie and notify the listener.
+            CookieWrapper cw = (CookieWrapper) cookie;
+            if (cw == null) {
+                // Normally, this should never be the case for calls originating
+                // from within this code.
+                // However, if there is any code that calls this method, we should
+                // check the parameters to make sure they're viable.
+                Rlog.i(LOG_TAG, "Cookie is null, ignoring onQueryComplete() request.");
+                if (cursor != null) {
+                    cursor.close();
+                }
+                return;
+            }
+
+            if (cw.event == EVENT_END_OF_QUEUE) {
+                for (Runnable r : mPendingListenerCallbacks) {
+                    r.run();
+                }
+                mPendingListenerCallbacks.clear();
+
+                release();
+                if (cursor != null) {
+                    cursor.close();
+                }
+                return;
+            }
+
+            // If the cw.event == EVENT_GET_GEO_DESCRIPTION, means it would not be the 1st
+            // time entering the onQueryComplete(), mCallerInfo should not be null.
+            if (cw.event == EVENT_GET_GEO_DESCRIPTION) {
+                if (mCallerInfo != null) {
+                    mCallerInfo.geoDescription = cw.geoDescription;
+                }
+                // notify that we can clean up the queue after this.
+                CookieWrapper endMarker = new CookieWrapper();
+                endMarker.event = EVENT_END_OF_QUEUE;
+                startQuery(token, endMarker, null, null, null, null, null);
+            }
+
+            // check the token and if needed, create the callerinfo object.
+            if (mCallerInfo == null) {
+                if ((mContext == null) || (mQueryUri == null)) {
+                    throw new QueryPoolException
+                            ("Bad context or query uri, or CallerInfoAsyncQuery already released.");
+                }
+
+                // adjust the callerInfo data as needed, and only if it was set from the
+                // initial query request.
+                // Change the callerInfo number ONLY if it is an emergency number or the
+                // voicemail number, and adjust other data (including photoResource)
+                // accordingly.
+                if (cw.event == EVENT_EMERGENCY_NUMBER) {
+                    // Note we're setting the phone number here (refer to javadoc
+                    // comments at the top of CallerInfo class).
+                    mCallerInfo = new CallerInfo().markAsEmergency(mContext);
+                } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
+                    mCallerInfo = new CallerInfo().markAsVoiceMail(cw.subId);
+                } else {
+                    mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor);
+                    if (DBG) Rlog.d(LOG_TAG, "==> Got mCallerInfo: " + mCallerInfo);
+
+                    CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary(
+                            mContext, cw.number, mCallerInfo);
+                    if (newCallerInfo != mCallerInfo) {
+                        mCallerInfo = newCallerInfo;
+                        if (DBG) Rlog.d(LOG_TAG, "#####async contact look up with numeric username"
+                                + mCallerInfo);
+                    }
+
+                    // Use the number entered by the user for display.
+                    if (!TextUtils.isEmpty(cw.number)) {
+                        mCallerInfo.phoneNumber = PhoneNumberUtils.formatNumber(cw.number,
+                                mCallerInfo.normalizedNumber,
+                                CallerInfo.getCurrentCountryIso(mContext));
+                    }
+
+                    // This condition refer to the google default code for geo.
+                    // If the number exists in Contacts, the CallCard would never show
+                    // the geo description, so it would be unnecessary to query it.
+                    if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
+                        if (TextUtils.isEmpty(mCallerInfo.name)) {
+                            if (DBG) Rlog.d(LOG_TAG, "start querying geo description");
+                            cw.event = EVENT_GET_GEO_DESCRIPTION;
+                            startQuery(token, cw, null, null, null, null, null);
+                            return;
+                        }
+                    }
+                }
+
+                if (DBG) Rlog.d(LOG_TAG, "constructing CallerInfo object for token: " + token);
+
+                //notify that we can clean up the queue after this.
+                CookieWrapper endMarker = new CookieWrapper();
+                endMarker.event = EVENT_END_OF_QUEUE;
+                startQuery(token, endMarker, null, null, null, null, null);
+            }
+
+            //notify the listener that the query is complete.
+            if (cw.listener != null) {
+                mPendingListenerCallbacks.add(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (DBG) Rlog.d(LOG_TAG, "notifying listener: "
+                                + cw.listener.getClass().toString() + " for token: " + token
+                                + mCallerInfo);
+                        cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
+                    }
+                });
+            } else {
+                Rlog.w(LOG_TAG, "There is no listener to notify for this query.");
+            }
+
+            if (cursor != null) {
+               cursor.close();
+            }
+        }
+    }
+
+    /**
+     * Private constructor for factory methods.
+     */
+    private CallerInfoAsyncQuery() {
+    }
+
+
+    /**
+     * Factory method to start query with a Uri query spec
+     */
+    public static CallerInfoAsyncQuery startQuery(int token, Context context, Uri contactRef,
+            OnQueryCompleteListener listener, Object cookie) {
+
+        CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
+        c.allocate(context, contactRef);
+
+        if (DBG) Rlog.d(LOG_TAG, "starting query for URI: " + contactRef + " handler: " + c.toString());
+
+        //create cookieWrapper, start query
+        CookieWrapper cw = new CookieWrapper();
+        cw.listener = listener;
+        cw.cookie = cookie;
+        cw.event = EVENT_NEW_QUERY;
+
+        c.mHandler.startQuery(token, cw, contactRef, null, null, null, null);
+
+        return c;
+    }
+
+    /**
+     * Factory method to start the query based on a number.
+     *
+     * Note: if the number contains an "@" character we treat it
+     * as a SIP address, and look it up directly in the Data table
+     * rather than using the PhoneLookup table.
+     * TODO: But eventually we should expose two separate methods, one for
+     * numbers and one for SIP addresses, and then have
+     * PhoneUtils.startGetCallerInfo() decide which one to call based on
+     * the phone type of the incoming connection.
+     */
+    public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
+            OnQueryCompleteListener listener, Object cookie) {
+
+        int subId = SubscriptionManager.getDefaultSubscriptionId();
+        return startQuery(token, context, number, listener, cookie, subId);
+    }
+
+    /**
+     * Factory method to start the query based on a number with specific subscription.
+     *
+     * Note: if the number contains an "@" character we treat it
+     * as a SIP address, and look it up directly in the Data table
+     * rather than using the PhoneLookup table.
+     * TODO: But eventually we should expose two separate methods, one for
+     * numbers and one for SIP addresses, and then have
+     * PhoneUtils.startGetCallerInfo() decide which one to call based on
+     * the phone type of the incoming connection.
+     */
+    public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
+            OnQueryCompleteListener listener, Object cookie, int subId) {
+
+        if (DBG) {
+            Rlog.d(LOG_TAG, "##### CallerInfoAsyncQuery startQuery()... #####");
+            Rlog.d(LOG_TAG, "- number: " + /*number*/ "xxxxxxx");
+            Rlog.d(LOG_TAG, "- cookie: " + cookie);
+        }
+
+        // Construct the URI object and query params, and start the query.
+
+        final Uri contactRef = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon()
+                .appendPath(number)
+                .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+                        String.valueOf(PhoneNumberUtils.isUriNumber(number)))
+                .build();
+
+        if (DBG) {
+            Rlog.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
+        }
+
+        CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
+        c.allocate(context, contactRef);
+
+        //create cookieWrapper, start query
+        CookieWrapper cw = new CookieWrapper();
+        cw.listener = listener;
+        cw.cookie = cookie;
+        cw.number = number;
+        cw.subId = subId;
+
+        // check to see if these are recognized numbers, and use shortcuts if we can.
+        if (PhoneNumberUtils.isLocalEmergencyNumber(context, number)) {
+            cw.event = EVENT_EMERGENCY_NUMBER;
+        } else if (PhoneNumberUtils.isVoiceMailNumber(context, subId, number)) {
+            cw.event = EVENT_VOICEMAIL_NUMBER;
+        } else {
+            cw.event = EVENT_NEW_QUERY;
+        }
+
+        c.mHandler.startQuery(token,
+                              cw,  // cookie
+                              contactRef,  // uri
+                              null,  // projection
+                              null,  // selection
+                              null,  // selectionArgs
+                              null);  // orderBy
+        return c;
+    }
+
+    /**
+     * Method to add listeners to a currently running query
+     */
+    public void addQueryListener(int token, OnQueryCompleteListener listener, Object cookie) {
+
+        if (DBG) Rlog.d(LOG_TAG, "adding listener to query: " + sanitizeUriToString(mHandler.mQueryUri) +
+                " handler: " + mHandler.toString());
+
+        //create cookieWrapper, add query request to end of queue.
+        CookieWrapper cw = new CookieWrapper();
+        cw.listener = listener;
+        cw.cookie = cookie;
+        cw.event = EVENT_ADD_LISTENER;
+
+        mHandler.startQuery(token, cw, null, null, null, null, null);
+    }
+
+    /**
+     * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct
+     * state of context and uri.
+     */
+    private void allocate(Context context, Uri contactRef) {
+        if ((context == null) || (contactRef == null)){
+            throw new QueryPoolException("Bad context or query uri.");
+        }
+        mHandler = new CallerInfoAsyncQueryHandler(context);
+        mHandler.mQueryUri = contactRef;
+    }
+
+    /**
+     * Releases the relevant data.
+     */
+    private void release() {
+        mHandler.mContext = null;
+        mHandler.mQueryUri = null;
+        mHandler.mCallerInfo = null;
+        mHandler = null;
+    }
+
+    private static String sanitizeUriToString(Uri uri) {
+        if (uri != null) {
+            String uriString = uri.toString();
+            int indexOfLastSlash = uriString.lastIndexOf('/');
+            if (indexOfLastSlash > 0) {
+                return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
+            } else {
+                return uriString;
+            }
+        } else {
+            return "";
+        }
+    }
+}
diff --git a/com/android/internal/telephony/CarrierActionAgent.java b/com/android/internal/telephony/CarrierActionAgent.java
new file mode 100644
index 0000000..6b9a70a
--- /dev/null
+++ b/com/android/internal/telephony/CarrierActionAgent.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.provider.Settings;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Carrier Action Agent(CAA) paired with
+ * {@link com.android.internal.telephony.CarrierSignalAgent CarrierSignalAgent},
+ * serves as an agent to dispatch carrier actions from carrier apps to different telephony modules,
+ * {@link android.telephony.TelephonyManager#carrierActionSetRadioEnabled(int, boolean)
+ * carrierActionSetRadioEnabled} for example.
+ *
+ * CAA supports dynamic registration where different telephony modules could listen for a specific
+ * carrier action event and implement their own handler. CCA will dispatch the event to all
+ * interested parties and maintain the received action states internally for future inspection.
+ * Each CarrierActionAgent is associated with a phone object.
+ */
+public class CarrierActionAgent extends Handler {
+    private static final String LOG_TAG = "CarrierActionAgent";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
+
+    /** A list of carrier actions */
+    public static final int CARRIER_ACTION_SET_METERED_APNS_ENABLED        = 0;
+    public static final int CARRIER_ACTION_SET_RADIO_ENABLED               = 1;
+    public static final int CARRIER_ACTION_RESET                           = 2;
+    public static final int CARRIER_ACTION_REPORT_DEFAULT_NETWORK_STATUS   = 3;
+    public static final int EVENT_APM_SETTINGS_CHANGED                     = 4;
+    public static final int EVENT_MOBILE_DATA_SETTINGS_CHANGED             = 5;
+    public static final int EVENT_DATA_ROAMING_OFF                         = 6;
+    public static final int EVENT_SIM_STATE_CHANGED                        = 7;
+
+    /** Member variables */
+    private final Phone mPhone;
+    /** registrant list per carrier action */
+    private RegistrantList mMeteredApnEnableRegistrants = new RegistrantList();
+    private RegistrantList mRadioEnableRegistrants = new RegistrantList();
+    private RegistrantList mDefaultNetworkReportRegistrants = new RegistrantList();
+    /** local log for carrier actions */
+    private LocalLog mMeteredApnEnabledLog = new LocalLog(10);
+    private LocalLog mRadioEnabledLog = new LocalLog(10);
+    private LocalLog mReportDefaultNetworkStatusLog = new LocalLog(10);
+    /** carrier actions */
+    private Boolean mCarrierActionOnMeteredApnEnabled = true;
+    private Boolean mCarrierActionOnRadioEnabled = true;
+    private Boolean mCarrierActionReportDefaultNetworkStatus = false;
+    /** content observer for APM change */
+    private final SettingsObserver mSettingsObserver;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            final String iccState = intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE);
+            if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)){
+                if (intent.getBooleanExtra(TelephonyIntents.EXTRA_REBROADCAST_ON_UNLOCK, false)) {
+                    // ignore rebroadcast since carrier apps are direct boot aware.
+                    return;
+                }
+                sendMessage(obtainMessage(EVENT_SIM_STATE_CHANGED, iccState));
+            }
+        }
+    };
+
+    /** Constructor */
+    public CarrierActionAgent(Phone phone) {
+        mPhone = phone;
+        mPhone.getContext().registerReceiver(mReceiver,
+                new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED));
+        mSettingsObserver = new SettingsObserver(mPhone.getContext(), this);
+        if (DBG) log("Creating CarrierActionAgent");
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        // skip notification if the input carrier action is same as the current one.
+        Boolean enabled = getCarrierActionEnabled(msg.what);
+        if (enabled != null && enabled == (boolean) msg.obj) return;
+        switch (msg.what) {
+            case CARRIER_ACTION_SET_METERED_APNS_ENABLED:
+                mCarrierActionOnMeteredApnEnabled = (boolean) msg.obj;
+                log("SET_METERED_APNS_ENABLED: " + mCarrierActionOnMeteredApnEnabled);
+                mMeteredApnEnabledLog.log("SET_METERED_APNS_ENABLED: "
+                        + mCarrierActionOnMeteredApnEnabled);
+                mMeteredApnEnableRegistrants.notifyRegistrants(
+                        new AsyncResult(null, mCarrierActionOnMeteredApnEnabled, null));
+                break;
+            case CARRIER_ACTION_SET_RADIO_ENABLED:
+                mCarrierActionOnRadioEnabled = (boolean) msg.obj;
+                log("SET_RADIO_ENABLED: " + mCarrierActionOnRadioEnabled);
+                mRadioEnabledLog.log("SET_RADIO_ENABLED: " + mCarrierActionOnRadioEnabled);
+                mRadioEnableRegistrants.notifyRegistrants(
+                        new AsyncResult(null, mCarrierActionOnRadioEnabled, null));
+                break;
+            case CARRIER_ACTION_REPORT_DEFAULT_NETWORK_STATUS:
+                mCarrierActionReportDefaultNetworkStatus = (boolean) msg.obj;
+                log("CARRIER_ACTION_REPORT_AT_DEFAULT_NETWORK_STATUS: "
+                        + mCarrierActionReportDefaultNetworkStatus);
+                mReportDefaultNetworkStatusLog.log("REGISTER_DEFAULT_NETWORK_STATUS: "
+                        + mCarrierActionReportDefaultNetworkStatus);
+                mDefaultNetworkReportRegistrants.notifyRegistrants(
+                        new AsyncResult(null, mCarrierActionReportDefaultNetworkStatus, null));
+                break;
+            case CARRIER_ACTION_RESET:
+                log("CARRIER_ACTION_RESET");
+                carrierActionReset();
+                break;
+            case EVENT_APM_SETTINGS_CHANGED:
+                log("EVENT_APM_SETTINGS_CHANGED");
+                if ((Settings.Global.getInt(mPhone.getContext().getContentResolver(),
+                        Settings.Global.AIRPLANE_MODE_ON, 0) != 0)) {
+                    carrierActionReset();
+                }
+                break;
+            case EVENT_MOBILE_DATA_SETTINGS_CHANGED:
+                log("EVENT_MOBILE_DATA_SETTINGS_CHANGED");
+                if (!mPhone.getDataEnabled()) carrierActionReset();
+                break;
+            case EVENT_DATA_ROAMING_OFF:
+                log("EVENT_DATA_ROAMING_OFF");
+                // reset carrier actions when exit roaming state.
+                carrierActionReset();
+                break;
+            case EVENT_SIM_STATE_CHANGED:
+                String iccState = (String) msg.obj;
+                if (IccCardConstants.INTENT_VALUE_ICC_LOADED.equals(iccState)) {
+                    log("EVENT_SIM_STATE_CHANGED status: " + iccState);
+                    carrierActionReset();
+                    String mobileData = Settings.Global.MOBILE_DATA;
+                    if (TelephonyManager.getDefault().getSimCount() != 1) {
+                        mobileData = mobileData + mPhone.getSubId();
+                    }
+                    mSettingsObserver.observe(Settings.Global.getUriFor(mobileData),
+                            EVENT_MOBILE_DATA_SETTINGS_CHANGED);
+                    mSettingsObserver.observe(
+                            Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
+                            EVENT_APM_SETTINGS_CHANGED);
+                    if (mPhone.getServiceStateTracker() != null) {
+                        mPhone.getServiceStateTracker().registerForDataRoamingOff(
+                                this, EVENT_DATA_ROAMING_OFF, null, false);
+                    }
+                } else if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(iccState)) {
+                    log("EVENT_SIM_STATE_CHANGED status: " + iccState);
+                    carrierActionReset();
+                    mSettingsObserver.unobserve();
+                    if (mPhone.getServiceStateTracker() != null) {
+                        mPhone.getServiceStateTracker().unregisterForDataRoamingOff(this);
+                    }
+                }
+                break;
+            default:
+                loge("Unknown carrier action: " + msg.what);
+        }
+    }
+
+    /**
+     * Action set from carrier app to enable/disable radio
+     */
+    public void carrierActionSetRadioEnabled(boolean enabled) {
+        sendMessage(obtainMessage(CARRIER_ACTION_SET_RADIO_ENABLED, enabled));
+    }
+
+    /**
+     * Action set from carrier app to enable/disable metered APNs
+     */
+    public void carrierActionSetMeteredApnsEnabled(boolean enabled) {
+        sendMessage(obtainMessage(CARRIER_ACTION_SET_METERED_APNS_ENABLED, enabled));
+    }
+
+    /**
+     * Action set from carrier app to start/stop reporting default network status.
+     */
+    public void carrierActionReportDefaultNetworkStatus(boolean report) {
+        sendMessage(obtainMessage(CARRIER_ACTION_REPORT_DEFAULT_NETWORK_STATUS, report));
+    }
+
+    private void carrierActionReset() {
+        carrierActionReportDefaultNetworkStatus(false);
+        carrierActionSetMeteredApnsEnabled(true);
+        carrierActionSetRadioEnabled(true);
+        // notify configured carrier apps for reset
+        mPhone.getCarrierSignalAgent().notifyCarrierSignalReceivers(
+                new Intent(TelephonyIntents.ACTION_CARRIER_SIGNAL_RESET));
+    }
+
+    private RegistrantList getRegistrantsFromAction(int action) {
+        switch (action) {
+            case CARRIER_ACTION_SET_METERED_APNS_ENABLED:
+                return mMeteredApnEnableRegistrants;
+            case CARRIER_ACTION_SET_RADIO_ENABLED:
+                return mRadioEnableRegistrants;
+            case CARRIER_ACTION_REPORT_DEFAULT_NETWORK_STATUS:
+                return mDefaultNetworkReportRegistrants;
+            default:
+                loge("Unsupported action: " + action);
+                return null;
+        }
+    }
+
+    private Boolean getCarrierActionEnabled(int action) {
+        switch (action) {
+            case CARRIER_ACTION_SET_METERED_APNS_ENABLED:
+                return mCarrierActionOnMeteredApnEnabled;
+            case CARRIER_ACTION_SET_RADIO_ENABLED:
+                return mCarrierActionOnRadioEnabled;
+            case CARRIER_ACTION_REPORT_DEFAULT_NETWORK_STATUS:
+                return mCarrierActionReportDefaultNetworkStatus;
+            default:
+                loge("Unsupported action: " + action);
+                return null;
+        }
+    }
+
+    /**
+     * Register with CAA for a specific event.
+     * @param action which carrier action registrant is interested in
+     * @param notifyNow if carrier action has once set, notify registrant right after
+     *                  registering, so that registrants will get the latest carrier action.
+     */
+    public void registerForCarrierAction(int action, Handler h, int what, Object obj,
+                                         boolean notifyNow) {
+        Boolean carrierAction = getCarrierActionEnabled(action);
+        if (carrierAction == null) {
+            throw new IllegalArgumentException("invalid carrier action: " + action);
+        }
+        RegistrantList list = getRegistrantsFromAction(action);
+        Registrant r = new Registrant(h, what, obj);
+        list.add(r);
+        if (notifyNow) {
+            r.notifyRegistrant(new AsyncResult(null, carrierAction, null));
+        }
+    }
+
+    /**
+     * Unregister with CAA for a specific event. Callers will no longer be notified upon such event.
+     * @param action which carrier action caller is no longer interested in
+     */
+    public void unregisterForCarrierAction(Handler h, int action) {
+        RegistrantList list = getRegistrantsFromAction(action);
+        if (list == null) {
+            throw new IllegalArgumentException("invalid carrier action: " + action);
+        }
+        list.remove(h);
+    }
+
+    @VisibleForTesting
+    public ContentObserver getContentObserver() {
+        return mSettingsObserver;
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void logv(String s) {
+        Rlog.v(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        pw.println(" mCarrierActionOnMeteredApnsEnabled Log:");
+        ipw.increaseIndent();
+        mMeteredApnEnabledLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        pw.println(" mCarrierActionOnRadioEnabled Log:");
+        ipw.increaseIndent();
+        mRadioEnabledLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        pw.println(" mCarrierActionReportDefaultNetworkStatus Log:");
+        ipw.increaseIndent();
+        mReportDefaultNetworkStatusLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+    }
+}
diff --git a/com/android/internal/telephony/CarrierAppUtils.java b/com/android/internal/telephony/CarrierAppUtils.java
new file mode 100644
index 0000000..8b81b0d
--- /dev/null
+++ b/com/android/internal/telephony/CarrierAppUtils.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utilities for handling carrier applications.
+ * @hide
+ */
+public final class CarrierAppUtils {
+    private static final String TAG = "CarrierAppUtils";
+
+    private static final boolean DEBUG = false; // STOPSHIP if true
+
+    private CarrierAppUtils() {}
+
+    /**
+     * Handle preinstalled carrier apps which should be disabled until a matching SIM is inserted.
+     *
+     * Evaluates the list of applications in config_disabledUntilUsedPreinstalledCarrierApps. We
+     * want to disable each such application which is present on the system image until the user
+     * inserts a SIM which causes that application to gain carrier privilege (indicating a "match"),
+     * without interfering with the user if they opt to enable/disable the app explicitly.
+     *
+     * So, for each such app, we either disable until used IFF the app is not carrier privileged AND
+     * in the default state (e.g. not explicitly DISABLED/DISABLED_BY_USER/ENABLED), or we enable if
+     * the app is carrier privileged and in either the default state or DISABLED_UNTIL_USED.
+     *
+     * In addition, there is a list of carrier-associated applications in
+     * {@link SystemConfig#getDisabledUntilUsedPreinstalledCarrierAssociatedApps}. Each app in this
+     * list is associated with a carrier app. When the given carrier app is enabled/disabled per the
+     * above, the associated applications are enabled/disabled to match.
+     *
+     * When enabling a carrier app we also grant it default permissions.
+     *
+     * This method is idempotent and is safe to be called at any time; it should be called once at
+     * system startup prior to any application running, as well as any time the set of carrier
+     * privileged apps may have changed.
+     */
+    public synchronized static void disableCarrierAppsUntilPrivileged(String callingPackage,
+            IPackageManager packageManager, TelephonyManager telephonyManager,
+            ContentResolver contentResolver, int userId) {
+        if (DEBUG) {
+            Slog.d(TAG, "disableCarrierAppsUntilPrivileged");
+        }
+        SystemConfig config = SystemConfig.getInstance();
+        String[] systemCarrierAppsDisabledUntilUsed = Resources.getSystem().getStringArray(
+                com.android.internal.R.array.config_disabledUntilUsedPreinstalledCarrierApps);
+        ArrayMap<String, List<String>> systemCarrierAssociatedAppsDisabledUntilUsed =
+                config.getDisabledUntilUsedPreinstalledCarrierAssociatedApps();
+        disableCarrierAppsUntilPrivileged(callingPackage, packageManager, telephonyManager,
+                contentResolver, userId, systemCarrierAppsDisabledUntilUsed,
+                systemCarrierAssociatedAppsDisabledUntilUsed);
+    }
+
+    /**
+     * Like {@link #disableCarrierAppsUntilPrivileged(String, IPackageManager, TelephonyManager,
+     * ContentResolver, int)}, but assumes that no carrier apps have carrier privileges.
+     *
+     * This prevents a potential race condition on first boot - since the app's default state is
+     * enabled, we will initially disable it when the telephony stack is first initialized as it has
+     * not yet read the carrier privilege rules. However, since telephony is initialized later on
+     * late in boot, the app being disabled may have already been started in response to certain
+     * broadcasts. The app will continue to run (briefly) after being disabled, before the Package
+     * Manager can kill it, and this can lead to crashes as the app is in an unexpected state.
+     */
+    public synchronized static void disableCarrierAppsUntilPrivileged(String callingPackage,
+            IPackageManager packageManager, ContentResolver contentResolver, int userId) {
+        if (DEBUG) {
+            Slog.d(TAG, "disableCarrierAppsUntilPrivileged");
+        }
+        SystemConfig config = SystemConfig.getInstance();
+        String[] systemCarrierAppsDisabledUntilUsed = Resources.getSystem().getStringArray(
+                com.android.internal.R.array.config_disabledUntilUsedPreinstalledCarrierApps);
+        ArrayMap<String, List<String>> systemCarrierAssociatedAppsDisabledUntilUsed =
+                config.getDisabledUntilUsedPreinstalledCarrierAssociatedApps();
+        disableCarrierAppsUntilPrivileged(callingPackage, packageManager,
+                null /* telephonyManager */, contentResolver, userId,
+                systemCarrierAppsDisabledUntilUsed, systemCarrierAssociatedAppsDisabledUntilUsed);
+    }
+
+    // Must be public b/c framework unit tests can't access package-private methods.
+    @VisibleForTesting
+    public static void disableCarrierAppsUntilPrivileged(String callingPackage,
+            IPackageManager packageManager, @Nullable TelephonyManager telephonyManager,
+            ContentResolver contentResolver, int userId,
+            String[] systemCarrierAppsDisabledUntilUsed,
+            ArrayMap<String, List<String>> systemCarrierAssociatedAppsDisabledUntilUsed) {
+        List<ApplicationInfo> candidates = getDefaultCarrierAppCandidatesHelper(packageManager,
+                userId, systemCarrierAppsDisabledUntilUsed);
+        if (candidates == null || candidates.isEmpty()) {
+            return;
+        }
+
+        Map<String, List<ApplicationInfo>> associatedApps = getDefaultCarrierAssociatedAppsHelper(
+                packageManager,
+                userId,
+                systemCarrierAssociatedAppsDisabledUntilUsed);
+
+        List<String> enabledCarrierPackages = new ArrayList<>();
+
+        boolean hasRunOnce = Settings.Secure.getIntForUser(
+                contentResolver, Settings.Secure.CARRIER_APPS_HANDLED, 0, userId) == 1;
+
+        try {
+            for (ApplicationInfo ai : candidates) {
+                String packageName = ai.packageName;
+                boolean hasPrivileges = telephonyManager != null &&
+                        telephonyManager.checkCarrierPrivilegesForPackageAnyPhone(packageName) ==
+                                TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+
+                if (hasPrivileges) {
+                    // Only update enabled state for the app on /system. Once it has been
+                    // updated we shouldn't touch it.
+                    if (!ai.isUpdatedSystemApp()
+                            && (ai.enabledSetting ==
+                            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+                            || ai.enabledSetting ==
+                            PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) {
+                        Slog.i(TAG, "Update state(" + packageName + "): ENABLED for user "
+                                + userId);
+                        packageManager.setApplicationEnabledSetting(
+                                packageName,
+                                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                                PackageManager.DONT_KILL_APP,
+                                userId,
+                                callingPackage);
+                    }
+
+                    // Also enable any associated apps for this carrier app.
+                    List<ApplicationInfo> associatedAppList = associatedApps.get(packageName);
+                    if (associatedAppList != null) {
+                        for (ApplicationInfo associatedApp : associatedAppList) {
+                            if (associatedApp.enabledSetting ==
+                                    PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+                                    || associatedApp.enabledSetting ==
+                                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+                                Slog.i(TAG, "Update associated state(" + associatedApp.packageName
+                                        + "): ENABLED for user " + userId);
+                                packageManager.setApplicationEnabledSetting(
+                                        associatedApp.packageName,
+                                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                                        PackageManager.DONT_KILL_APP,
+                                        userId,
+                                        callingPackage);
+                            }
+                        }
+                    }
+
+                    // Always re-grant default permissions to carrier apps w/ privileges.
+                    enabledCarrierPackages.add(ai.packageName);
+                } else {  // No carrier privileges
+                    // Only update enabled state for the app on /system. Once it has been
+                    // updated we shouldn't touch it.
+                    if (!ai.isUpdatedSystemApp()
+                            && ai.enabledSetting ==
+                            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+                        Slog.i(TAG, "Update state(" + packageName
+                                + "): DISABLED_UNTIL_USED for user " + userId);
+                        packageManager.setApplicationEnabledSetting(
+                                packageName,
+                                PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
+                                0,
+                                userId,
+                                callingPackage);
+                    }
+
+                    // Also disable any associated apps for this carrier app if this is the first
+                    // run. We avoid doing this a second time because it is brittle to rely on the
+                    // distinction between "default" and "enabled".
+                    if (!hasRunOnce) {
+                        List<ApplicationInfo> associatedAppList = associatedApps.get(packageName);
+                        if (associatedAppList != null) {
+                            for (ApplicationInfo associatedApp : associatedAppList) {
+                                if (associatedApp.enabledSetting
+                                        == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+                                    Slog.i(TAG,
+                                            "Update associated state(" + associatedApp.packageName
+                                                    + "): DISABLED_UNTIL_USED for user " + userId);
+                                    packageManager.setApplicationEnabledSetting(
+                                            associatedApp.packageName,
+                                            PackageManager
+                                                    .COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
+                                            0,
+                                            userId,
+                                            callingPackage);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Mark the execution so we do not disable apps again.
+            if (!hasRunOnce) {
+                Settings.Secure.putIntForUser(
+                        contentResolver, Settings.Secure.CARRIER_APPS_HANDLED, 1, userId);
+            }
+
+            if (!enabledCarrierPackages.isEmpty()) {
+                // Since we enabled at least one app, ensure we grant default permissions to those
+                // apps.
+                String[] packageNames = new String[enabledCarrierPackages.size()];
+                enabledCarrierPackages.toArray(packageNames);
+                packageManager.grantDefaultPermissionsToEnabledCarrierApps(packageNames, userId);
+            }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Could not reach PackageManager", e);
+        }
+    }
+
+    /**
+     * Returns the list of "default" carrier apps.
+     *
+     * This is the subset of apps returned by
+     * {@link #getDefaultCarrierAppCandidates(IPackageManager, int)} which currently have carrier
+     * privileges per the SIM(s) inserted in the device.
+     */
+    public static List<ApplicationInfo> getDefaultCarrierApps(IPackageManager packageManager,
+            TelephonyManager telephonyManager, int userId) {
+        // Get all system apps from the default list.
+        List<ApplicationInfo> candidates = getDefaultCarrierAppCandidates(packageManager, userId);
+        if (candidates == null || candidates.isEmpty()) {
+            return null;
+        }
+
+        // Filter out apps without carrier privileges.
+        // Iterate from the end to avoid creating an Iterator object and because we will be removing
+        // elements from the list as we pass through it.
+        for (int i = candidates.size() - 1; i >= 0; i--) {
+            ApplicationInfo ai = candidates.get(i);
+            String packageName = ai.packageName;
+            boolean hasPrivileges =
+                    telephonyManager.checkCarrierPrivilegesForPackageAnyPhone(packageName) ==
+                            TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+            if (!hasPrivileges) {
+                candidates.remove(i);
+            }
+        }
+
+        return candidates;
+    }
+
+    /**
+     * Returns the list of "default" carrier app candidates.
+     *
+     * These are the apps subject to the hiding/showing logic in
+     * {@link CarrierAppUtils#disableCarrierAppsUntilPrivileged(String, IPackageManager,
+     * TelephonyManager, ContentResolver, int)}, as well as the apps which should have default
+     * permissions granted, when a matching SIM is inserted.
+     *
+     * Whether or not the app is actually considered a default app depends on whether the app has
+     * carrier privileges as determined by the SIMs in the device.
+     */
+    public static List<ApplicationInfo> getDefaultCarrierAppCandidates(
+            IPackageManager packageManager, int userId) {
+        String[] systemCarrierAppsDisabledUntilUsed = Resources.getSystem().getStringArray(
+                com.android.internal.R.array.config_disabledUntilUsedPreinstalledCarrierApps);
+        return getDefaultCarrierAppCandidatesHelper(packageManager, userId,
+                systemCarrierAppsDisabledUntilUsed);
+    }
+
+    private static List<ApplicationInfo> getDefaultCarrierAppCandidatesHelper(
+            IPackageManager packageManager,
+            int userId,
+            String[] systemCarrierAppsDisabledUntilUsed) {
+        if (systemCarrierAppsDisabledUntilUsed == null
+                || systemCarrierAppsDisabledUntilUsed.length == 0) {
+            return null;
+        }
+        List<ApplicationInfo> apps = new ArrayList<>(systemCarrierAppsDisabledUntilUsed.length);
+        for (int i = 0; i < systemCarrierAppsDisabledUntilUsed.length; i++) {
+            String packageName = systemCarrierAppsDisabledUntilUsed[i];
+            ApplicationInfo ai =
+                    getApplicationInfoIfSystemApp(packageManager, userId, packageName);
+            if (ai != null) {
+                apps.add(ai);
+            }
+        }
+        return apps;
+    }
+
+    private static Map<String, List<ApplicationInfo>> getDefaultCarrierAssociatedAppsHelper(
+            IPackageManager packageManager,
+            int userId,
+            ArrayMap<String, List<String>> systemCarrierAssociatedAppsDisabledUntilUsed) {
+        int size = systemCarrierAssociatedAppsDisabledUntilUsed.size();
+        Map<String, List<ApplicationInfo>> associatedApps = new ArrayMap<>(size);
+        for (int i = 0; i < size; i++) {
+            String carrierAppPackage = systemCarrierAssociatedAppsDisabledUntilUsed.keyAt(i);
+            List<String> associatedAppPackages =
+                    systemCarrierAssociatedAppsDisabledUntilUsed.valueAt(i);
+            for (int j = 0; j < associatedAppPackages.size(); j++) {
+                ApplicationInfo ai =
+                        getApplicationInfoIfSystemApp(
+                                packageManager, userId, associatedAppPackages.get(j));
+                // Only update enabled state for the app on /system. Once it has been updated we
+                // shouldn't touch it.
+                if (ai != null && !ai.isUpdatedSystemApp()) {
+                    List<ApplicationInfo> appList = associatedApps.get(carrierAppPackage);
+                    if (appList == null) {
+                        appList = new ArrayList<>();
+                        associatedApps.put(carrierAppPackage, appList);
+                    }
+                    appList.add(ai);
+                }
+            }
+        }
+        return associatedApps;
+    }
+
+    @Nullable
+    private static ApplicationInfo getApplicationInfoIfSystemApp(
+            IPackageManager packageManager,
+            int userId,
+            String packageName) {
+        try {
+            ApplicationInfo ai = packageManager.getApplicationInfo(packageName,
+                    PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS, userId);
+            if (ai != null && ai.isSystemApp()) {
+                return ai;
+            }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Could not reach PackageManager", e);
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/CarrierInfoManager.java b/com/android/internal/telephony/CarrierInfoManager.java
new file mode 100644
index 0000000..ebf04e8
--- /dev/null
+++ b/com/android/internal/telephony/CarrierInfoManager.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteConstraintException;
+import android.provider.Telephony;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Date;
+
+/**
+ * This class provides methods to retreive information from the CarrierKeyProvider.
+ */
+public class CarrierInfoManager {
+    private static final String LOG_TAG = "CarrierInfoManager";
+
+    /**
+     * Returns Carrier specific information that will be used to encrypt the IMSI and IMPI.
+     * @param keyType whether the key is being used for WLAN or ePDG.
+     * @param mContext
+     * @return ImsiEncryptionInfo which contains the information, including the public key, to be
+     *         used for encryption.
+     */
+    public static ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int keyType,
+                                                                     Context mContext) {
+        String mcc = "";
+        String mnc = "";
+        final TelephonyManager telephonyManager =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        String networkOperator = telephonyManager.getNetworkOperator();
+        if (!TextUtils.isEmpty(networkOperator)) {
+            mcc = networkOperator.substring(0, 3);
+            mnc = networkOperator.substring(3);
+            Log.i(LOG_TAG, "using values for mnc, mcc: " + mnc + "," + mcc);
+        } else {
+            Log.e(LOG_TAG, "Invalid networkOperator: " + networkOperator);
+            return null;
+        }
+        Cursor findCursor = null;
+        try {
+            // In the current design, MVNOs are not supported. If we decide to support them,
+            // we'll need to add to this CL.
+            ContentResolver mContentResolver = mContext.getContentResolver();
+            String[] columns = {Telephony.CarrierColumns.PUBLIC_KEY,
+                    Telephony.CarrierColumns.EXPIRATION_TIME,
+                    Telephony.CarrierColumns.KEY_IDENTIFIER};
+            findCursor = mContentResolver.query(Telephony.CarrierColumns.CONTENT_URI, columns,
+                    "mcc=? and mnc=? and key_type=?",
+                    new String[]{mcc, mnc, String.valueOf(keyType)}, null);
+            if (findCursor == null || !findCursor.moveToFirst()) {
+                Log.d(LOG_TAG, "No rows found for keyType: " + keyType);
+                return null;
+            }
+            if (findCursor.getCount() > 1) {
+                Log.e(LOG_TAG, "More than 1 row found for the keyType: " + keyType);
+            }
+            byte[] carrier_key = findCursor.getBlob(0);
+            Date expirationTime = new Date(findCursor.getLong(1));
+            String keyIdentifier = findCursor.getString(2);
+            return new ImsiEncryptionInfo(mcc, mnc, keyType, keyIdentifier, carrier_key,
+                    expirationTime);
+        } catch (IllegalArgumentException e) {
+            Log.e(LOG_TAG, "Bad arguments:" + e);
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Query failed:" + e);
+        } finally {
+            if (findCursor != null) {
+                findCursor.close();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Inserts or update the Carrier Key in the database
+     * @param imsiEncryptionInfo ImsiEncryptionInfo object.
+     * @param mContext Context.
+     */
+    public static void updateOrInsertCarrierKey(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                Context mContext) {
+        byte[] keyBytes = imsiEncryptionInfo.getPublicKey().getEncoded();
+        ContentResolver mContentResolver = mContext.getContentResolver();
+        // In the current design, MVNOs are not supported. If we decide to support them,
+        // we'll need to add to this CL.
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(Telephony.CarrierColumns.MCC, imsiEncryptionInfo.getMcc());
+        contentValues.put(Telephony.CarrierColumns.MNC, imsiEncryptionInfo.getMnc());
+        contentValues.put(Telephony.CarrierColumns.KEY_TYPE,
+                imsiEncryptionInfo.getKeyType());
+        contentValues.put(Telephony.CarrierColumns.KEY_IDENTIFIER,
+                imsiEncryptionInfo.getKeyIdentifier());
+        contentValues.put(Telephony.CarrierColumns.PUBLIC_KEY, keyBytes);
+        contentValues.put(Telephony.CarrierColumns.EXPIRATION_TIME,
+                imsiEncryptionInfo.getExpirationTime().getTime());
+        try {
+            Log.i(LOG_TAG, "Inserting imsiEncryptionInfo into db");
+            mContentResolver.insert(Telephony.CarrierColumns.CONTENT_URI, contentValues);
+        } catch (SQLiteConstraintException e) {
+            Log.i(LOG_TAG, "Insert failed, updating imsiEncryptionInfo into db");
+            ContentValues updatedValues = new ContentValues();
+            updatedValues.put(Telephony.CarrierColumns.PUBLIC_KEY, keyBytes);
+            updatedValues.put(Telephony.CarrierColumns.EXPIRATION_TIME,
+                    imsiEncryptionInfo.getExpirationTime().getTime());
+            updatedValues.put(Telephony.CarrierColumns.KEY_IDENTIFIER,
+                    imsiEncryptionInfo.getKeyIdentifier());
+            try {
+                int nRows = mContentResolver.update(Telephony.CarrierColumns.CONTENT_URI,
+                        updatedValues,
+                        "mcc=? and mnc=? and key_type=?", new String[]{
+                                imsiEncryptionInfo.getMcc(),
+                                imsiEncryptionInfo.getMnc(),
+                                String.valueOf(imsiEncryptionInfo.getKeyType())});
+                if (nRows == 0) {
+                    Log.d(LOG_TAG, "Error updating values:" + imsiEncryptionInfo);
+                }
+            } catch (Exception ex) {
+                Log.d(LOG_TAG, "Error updating values:" + imsiEncryptionInfo + ex);
+            }
+        }  catch (Exception e) {
+            Log.d(LOG_TAG, "Error inserting/updating values:" + imsiEncryptionInfo + e);
+        }
+    }
+
+    /**
+     * Sets the Carrier specific information that will be used to encrypt the IMSI and IMPI.
+     * This includes the public key and the key identifier. This information will be stored in the
+     * device keystore.
+     * @param imsiEncryptionInfo which includes the Key Type, the Public Key
+     *        {@link java.security.PublicKey} and the Key Identifier.
+     *        The keyIdentifier Attribute value pair that helps a server locate
+     *        the private key to decrypt the permanent identity.
+     * @param mContext Context.
+     */
+    public static void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                       Context mContext) {
+        Log.i(LOG_TAG, "inserting carrier key: " + imsiEncryptionInfo);
+        updateOrInsertCarrierKey(imsiEncryptionInfo, mContext);
+        //todo send key to modem. Will be done in a subsequent CL.
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/CarrierKeyDownloadManager.java b/com/android/internal/telephony/CarrierKeyDownloadManager.java
new file mode 100644
index 0000000..bca337d
--- /dev/null
+++ b/com/android/internal/telephony/CarrierKeyDownloadManager.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import static android.preference.PreferenceManager.getDefaultSharedPreferences;
+
+import android.app.AlarmManager;
+import android.app.DownloadManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Date;
+
+/**
+ * This class contains logic to get Certificates and keep them current.
+ * The class will be instantiated by various Phone implementations.
+ */
+public class CarrierKeyDownloadManager {
+    private static final String LOG_TAG = "CarrierKeyDownloadManager";
+
+    private static final String MCC_MNC_PREF_TAG = "CARRIER_KEY_DM_MCC_MNC";
+
+    private static final int DAY_IN_MILLIS = 24 * 3600 * 1000;
+
+    // Start trying to renew the cert X days before it expires.
+    private static final int DEFAULT_RENEWAL_WINDOW_DAYS = 7;
+
+    /* Intent for downloading the public key */
+    private static final String INTENT_KEY_RENEWAL_ALARM_PREFIX =
+            "com.android.internal.telephony.carrier_key_download_alarm";
+
+    private int mKeyAvailability = 0;
+
+    public static final String MNC = "MNC";
+    public static final String MCC = "MCC";
+    private static final String SEPARATOR = ":";
+
+    private static final String JSON_KEY = "key";
+    private static final String JSON_TYPE = "type";
+    private static final String JSON_IDENTIFIER = "identifier";
+    private static final String JSON_EXPIRATION_DATE = "expiration-date";
+    private static final String JSON_CARRIER_KEYS = "carrier-keys";
+    private static final String JSON_TYPE_VALUE_WLAN = "WLAN";
+    private static final String JSON_TYPE_VALUE_EPDG = "EPDG";
+
+
+    private static final int[] CARRIER_KEY_TYPES = {TelephonyManager.KEY_TYPE_EPDG,
+            TelephonyManager.KEY_TYPE_WLAN};
+    private static final int UNINITIALIZED_KEY_TYPE = -1;
+
+    private final Phone mPhone;
+    private final Context mContext;
+    private final DownloadManager mDownloadManager;
+    private String mURL;
+
+    public CarrierKeyDownloadManager(Phone phone) {
+        mPhone = phone;
+        mContext = phone.getContext();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+        filter.addAction(INTENT_KEY_RENEWAL_ALARM_PREFIX + mPhone.getPhoneId());
+        mContext.registerReceiver(mBroadcastReceiver, filter, null, phone);
+        mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
+    }
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            int slotId = mPhone.getPhoneId();
+            if (action.equals(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId)) {
+                Log.d(LOG_TAG, "Handling key renewal alarm: " + action);
+                handleAlarmOrConfigChange();
+            } else if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                if (slotId == intent.getIntExtra(PhoneConstants.PHONE_KEY,
+                        SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
+                    Log.d(LOG_TAG, "Carrier Config changed: " + action);
+                    handleAlarmOrConfigChange();
+                }
+            } else if (action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
+                Log.d(LOG_TAG, "Download Complete");
+                long carrierKeyDownloadIdentifier =
+                        intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
+                String mccMnc = getMccMncSetFromPref();
+                if (isValidDownload(mccMnc)) {
+                    onDownloadComplete(carrierKeyDownloadIdentifier, mccMnc);
+                    onPostDownloadProcessing(carrierKeyDownloadIdentifier);
+                }
+            }
+        }
+    };
+
+    private void onPostDownloadProcessing(long carrierKeyDownloadIdentifier) {
+        resetRenewalAlarm();
+        cleanupDownloadPreferences(carrierKeyDownloadIdentifier);
+    }
+
+    private void handleAlarmOrConfigChange() {
+        if (carrierUsesKeys()) {
+            if (areCarrierKeysAbsentOrExpiring()) {
+                boolean downloadStartedSuccessfully = downloadKey();
+                // if the download was attemped, but not started successfully, and if carriers uses
+                // keys, we'll still want to renew the alarms, and try downloading the key a day
+                // later.
+                if (!downloadStartedSuccessfully) {
+                    resetRenewalAlarm();
+                }
+            } else {
+                return;
+            }
+        } else {
+            // delete any existing alarms.
+            cleanupRenewalAlarms();
+        }
+    }
+
+    private void cleanupDownloadPreferences(long carrierKeyDownloadIdentifier) {
+        Log.d(LOG_TAG, "Cleaning up download preferences: " + carrierKeyDownloadIdentifier);
+        SharedPreferences.Editor editor = getDefaultSharedPreferences(mContext).edit();
+        editor.remove(String.valueOf(carrierKeyDownloadIdentifier));
+        editor.commit();
+    }
+
+    private void cleanupRenewalAlarms() {
+        Log.d(LOG_TAG, "Cleaning up existing renewal alarms");
+        int slotId = mPhone.getPhoneId();
+        Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId);
+        PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        AlarmManager alarmManager =
+                (AlarmManager) mContext.getSystemService(mContext.ALARM_SERVICE);
+        alarmManager.cancel(carrierKeyDownloadIntent);
+    }
+
+    /**
+     * this method resets the alarm. Starts by cleaning up the existing alarms.
+     * We look at the earliest expiration date, and setup an alarms X days prior.
+     * If the expiration date is in the past, we'll setup an alarm to run the next day. This
+     * could happen if the download has failed.
+     **/
+    private void resetRenewalAlarm() {
+        cleanupRenewalAlarms();
+        int slotId = mPhone.getPhoneId();
+        long minExpirationDate = Long.MAX_VALUE;
+        for (int key_type : CARRIER_KEY_TYPES) {
+            if (!isKeyEnabled(key_type)) {
+                continue;
+            }
+            ImsiEncryptionInfo imsiEncryptionInfo =
+                    mPhone.getCarrierInfoForImsiEncryption(key_type);
+            if (imsiEncryptionInfo != null && imsiEncryptionInfo.getExpirationTime() != null) {
+                if (minExpirationDate > imsiEncryptionInfo.getExpirationTime().getTime()) {
+                    minExpirationDate = imsiEncryptionInfo.getExpirationTime().getTime();
+                }
+            }
+        }
+
+        // if there are no keys, or expiration date is in the past, or within 7 days, then we
+        // set the alarm to run in a day. Else, we'll set the alarm to run 7 days prior to
+        // expiration.
+        if (minExpirationDate == Long.MAX_VALUE || (minExpirationDate
+                < System.currentTimeMillis() + DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) {
+            minExpirationDate = System.currentTimeMillis() + DAY_IN_MILLIS;
+        } else {
+            minExpirationDate = minExpirationDate - DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
+        }
+        Log.d(LOG_TAG, "minExpirationDate: " + new Date(minExpirationDate));
+        final AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
+                Context.ALARM_SERVICE);
+        Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId);
+        PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, minExpirationDate,
+                carrierKeyDownloadIntent);
+        Log.d(LOG_TAG, "setRenewelAlarm: action=" + intent.getAction() + " time="
+                + new Date(minExpirationDate));
+    }
+
+    private String getMccMncSetFromPref() {
+        // check if this is a download that we had created. We do this by checking if the
+        // downloadId is stored in the shared prefs.
+        int slotId = mPhone.getPhoneId();
+        SharedPreferences preferences = getDefaultSharedPreferences(mContext);
+        return preferences.getString(MCC_MNC_PREF_TAG + slotId, null);
+    }
+
+    /**
+     *  checks if the download was sent by this particular instance. We do this by including the
+     *  slot id in the key. If no value is found, we know that the download was not for this
+     *  instance of the phone.
+     **/
+    private boolean isValidDownload(String mccMnc) {
+        String mccCurrent = "";
+        String mncCurrent = "";
+        String mccSource = "";
+        String mncSource = "";
+        final TelephonyManager telephonyManager =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        String networkOperator = telephonyManager.getNetworkOperator(mPhone.getSubId());
+
+        if (TextUtils.isEmpty(networkOperator) || TextUtils.isEmpty(mccMnc)) {
+            Log.e(LOG_TAG, "networkOperator or mcc/mnc is empty");
+            return false;
+        }
+
+        String[] splitValue = mccMnc.split(SEPARATOR);
+        mccSource = splitValue[0];
+        mncSource = splitValue[1];
+        Log.d(LOG_TAG, "values from sharedPrefs mcc, mnc: " + mccSource + "," + mncSource);
+
+        mccCurrent = networkOperator.substring(0, 3);
+        mncCurrent = networkOperator.substring(3);
+        Log.d(LOG_TAG, "using values for mcc, mnc: " + mccCurrent + "," + mncCurrent);
+
+        if (TextUtils.equals(mncSource, mncCurrent) &&  TextUtils.equals(mccSource, mccCurrent)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * This method will try to parse the downloaded information, and persist it in the database.
+     **/
+    private void onDownloadComplete(long carrierKeyDownloadIdentifier, String mccMnc) {
+        Log.d(LOG_TAG, "onDownloadComplete: " + carrierKeyDownloadIdentifier);
+        String jsonStr;
+        DownloadManager.Query query = new DownloadManager.Query();
+        query.setFilterById(carrierKeyDownloadIdentifier);
+        Cursor cursor = mDownloadManager.query(query);
+
+        if (cursor == null) {
+            return;
+        }
+        if (cursor.moveToFirst()) {
+            int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
+            if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) {
+                try {
+                    final InputStream source = new FileInputStream(
+                            mDownloadManager.openDownloadedFile(carrierKeyDownloadIdentifier)
+                                    .getFileDescriptor());
+                    jsonStr = convertToString(source);
+                    parseJsonAndPersistKey(jsonStr, mccMnc);
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Error in download:" + carrierKeyDownloadIdentifier
+                            + ". " + e);
+                } finally {
+                    mDownloadManager.remove(carrierKeyDownloadIdentifier);
+                }
+            }
+            Log.d(LOG_TAG, "Completed downloading keys");
+        }
+        cursor.close();
+        return;
+    }
+
+    /**
+     * This method checks if the carrier requires key. We'll read the carrier config to make that
+     * determination.
+     * @return boolean returns true if carrier requires keys, else false.
+     **/
+    private boolean carrierUsesKeys() {
+        CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
+                mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (carrierConfigManager == null) {
+            return false;
+        }
+        int subId = mPhone.getSubId();
+        PersistableBundle b = carrierConfigManager.getConfigForSubId(subId);
+        if (b == null) {
+            return false;
+        }
+        mKeyAvailability = b.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT);
+        mURL = b.getString(CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING);
+        if (TextUtils.isEmpty(mURL) || mKeyAvailability == 0) {
+            Log.d(LOG_TAG, "Carrier not enabled or invalid values");
+            return false;
+        }
+        for (int key_type : CARRIER_KEY_TYPES) {
+            if (isKeyEnabled(key_type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String convertToString(InputStream is) {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+        StringBuilder sb = new StringBuilder();
+
+        String line;
+        try {
+            while ((line = reader.readLine()) != null) {
+                sb.append(line).append('\n');
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                is.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Converts the string into a json object to retreive the nodes. The Json should have 3 nodes,
+     * including the Carrier public key, the key type and the key identifier. Once the nodes have
+     * been extracted, they get persisted to the database. Sample:
+     *      "carrier-keys": [ { "key": "",
+     *                         "type": WLAN,
+     *                         "identifier": "",
+     *                         "expiration-date": 1502577746000
+     *                        } ]
+     * @param jsonStr the json string.
+     * @param mccMnc contains the mcc, mnc
+     */
+    private void parseJsonAndPersistKey(String jsonStr, String mccMnc) {
+        if (TextUtils.isEmpty(jsonStr) || TextUtils.isEmpty(mccMnc)) {
+            Log.e(LOG_TAG, "jsonStr or mcc, mnc: is empty");
+            return;
+        }
+        try {
+            String mcc = "";
+            String mnc = "";
+            String[] splitValue = mccMnc.split(SEPARATOR);
+            mcc = splitValue[0];
+            mnc = splitValue[1];
+            JSONObject jsonObj = new JSONObject(jsonStr);
+            JSONArray keys = jsonObj.getJSONArray(JSON_CARRIER_KEYS);
+
+            for (int i = 0; i < keys.length(); i++) {
+                JSONObject key = keys.getJSONObject(i);
+                String carrierKey = key.getString(JSON_KEY);
+                String typeString = key.getString(JSON_TYPE);
+                int type = UNINITIALIZED_KEY_TYPE;
+                if (typeString.equals(JSON_TYPE_VALUE_WLAN)) {
+                    type = TelephonyManager.KEY_TYPE_WLAN;
+                } else if (typeString.equals(JSON_TYPE_VALUE_EPDG)) {
+                    type = TelephonyManager.KEY_TYPE_EPDG;
+                }
+                long expiration_date = key.getLong(JSON_EXPIRATION_DATE);
+                String identifier = key.getString(JSON_IDENTIFIER);
+                savePublicKey(carrierKey, type, identifier, expiration_date,
+                        mcc, mnc);
+            }
+        } catch (final JSONException e) {
+            Log.e(LOG_TAG, "Json parsing error: " + e.getMessage());
+        }
+    }
+
+    /**
+     * introspects the mKeyAvailability bitmask
+     * @return true if the digit at position k is 1, else false.
+     */
+
+    private boolean isKeyEnabled(int keyType) {
+        //since keytype has values of 1, 2.... we need to subtract 1 from the keytype.
+        int returnValue = (mKeyAvailability >> (keyType - 1)) & 1;
+        return (returnValue == 1) ? true : false;
+    }
+
+    /**
+     * Checks whether is the keys are absent or close to expiration. Returns true, if either of
+     * those conditions are true.
+     * @return boolean returns true when keys are absent or close to expiration, else false.
+     */
+    @VisibleForTesting
+    public boolean areCarrierKeysAbsentOrExpiring() {
+        for (int key_type : CARRIER_KEY_TYPES) {
+            if (!isKeyEnabled(key_type)) {
+                continue;
+            }
+            ImsiEncryptionInfo imsiEncryptionInfo =
+                    mPhone.getCarrierInfoForImsiEncryption(key_type);
+            if (imsiEncryptionInfo == null) {
+                Log.d(LOG_TAG, "Key not found for: " + key_type);
+                return true;
+            }
+            Date imsiDate = imsiEncryptionInfo.getExpirationTime();
+            long timeToExpire = imsiDate.getTime() - System.currentTimeMillis();
+            return (timeToExpire < DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS) ? true : false;
+        }
+        return false;
+    }
+
+    private boolean downloadKey() {
+        Log.d(LOG_TAG, "starting download from: " + mURL);
+        final TelephonyManager telephonyManager =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        String mcc = "";
+        String mnc = "";
+        String networkOperator = telephonyManager.getNetworkOperator(mPhone.getSubId());
+
+        if (!TextUtils.isEmpty(networkOperator)) {
+            mcc = networkOperator.substring(0, 3);
+            mnc = networkOperator.substring(3);
+            Log.d(LOG_TAG, "using values for mcc, mnc: " + mcc + "," + mnc);
+        } else {
+            Log.e(LOG_TAG, "mcc, mnc: is empty");
+            return false;
+        }
+        try {
+            DownloadManager.Request request = new DownloadManager.Request(Uri.parse(mURL));
+            request.setAllowedOverMetered(false);
+            request.setVisibleInDownloadsUi(false);
+            Long carrierKeyDownloadRequestId = mDownloadManager.enqueue(request);
+            SharedPreferences.Editor editor = getDefaultSharedPreferences(mContext).edit();
+
+            String mccMnc = mcc + SEPARATOR + mnc;
+            int slotId = mPhone.getPhoneId();
+            Log.d(LOG_TAG, "storing values in sharedpref mcc, mnc, days: " + mcc + "," + mnc
+                    + "," + carrierKeyDownloadRequestId);
+            editor.putString(MCC_MNC_PREF_TAG + slotId, mccMnc);
+            editor.commit();
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "exception trying to dowload key from url: " + mURL);
+            return false;
+        }
+        return true;
+    }
+
+    private void savePublicKey(String key, int type, String identifier, long expirationDate,
+                               String mcc, String mnc) {
+        byte[] keyBytes = Base64.decode(key.getBytes(), Base64.DEFAULT);
+        ImsiEncryptionInfo imsiEncryptionInfo = new ImsiEncryptionInfo(mcc, mnc, type, identifier,
+                keyBytes, new Date(expirationDate));
+        mPhone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo);
+    }
+}
diff --git a/com/android/internal/telephony/CarrierServiceBindHelper.java b/com/android/internal/telephony/CarrierServiceBindHelper.java
new file mode 100644
index 0000000..ab010cc
--- /dev/null
+++ b/com/android/internal/telephony/CarrierServiceBindHelper.java
@@ -0,0 +1,395 @@
+/*
+* Copyright (C) 2015 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.service.carrier.CarrierService;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.content.PackageMonitor;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Manages long-lived bindings to carrier services
+ * @hide
+ */
+public class CarrierServiceBindHelper {
+    private static final String LOG_TAG = "CarrierSvcBindHelper";
+
+    /**
+     * How long to linger a binding after an app loses carrier privileges, as long as no new
+     * binding comes in to take its place.
+     */
+    private static final int UNBIND_DELAY_MILLIS = 30 * 1000; // 30 seconds
+
+    private Context mContext;
+    private AppBinding[] mBindings;
+    private String[] mLastSimState;
+    private final PackageMonitor mPackageMonitor = new CarrierServicePackageMonitor();
+
+    private BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            log("Received " + action);
+
+            if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
+                // On user unlock, new components might become available, so reevaluate all
+                // bindings.
+                for (int phoneId = 0; phoneId < mBindings.length; phoneId++) {
+                    mBindings[phoneId].rebind();
+                }
+            }
+        }
+    };
+
+    private static final int EVENT_REBIND = 0;
+    private static final int EVENT_PERFORM_IMMEDIATE_UNBIND = 1;
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            AppBinding binding;
+            log("mHandler: " + msg.what);
+
+            switch (msg.what) {
+                case EVENT_REBIND:
+                    binding = (AppBinding) msg.obj;
+                    log("Rebinding if necessary for phoneId: " + binding.getPhoneId());
+                    binding.rebind();
+                    break;
+                case EVENT_PERFORM_IMMEDIATE_UNBIND:
+                    binding = (AppBinding) msg.obj;
+                    binding.performImmediateUnbind();
+                    break;
+            }
+        }
+    };
+
+    public CarrierServiceBindHelper(Context context) {
+        mContext = context;
+
+        int numPhones = TelephonyManager.from(context).getPhoneCount();
+        mBindings = new AppBinding[numPhones];
+        mLastSimState = new String[numPhones];
+
+        for (int phoneId = 0; phoneId < numPhones; phoneId++) {
+            mBindings[phoneId] = new AppBinding(phoneId);
+        }
+
+        mPackageMonitor.register(
+                context, mHandler.getLooper(), UserHandle.ALL, false /* externalStorage */);
+        mContext.registerReceiverAsUser(mUserUnlockedReceiver, UserHandle.SYSTEM,
+                new IntentFilter(Intent.ACTION_USER_UNLOCKED), null /* broadcastPermission */,
+                mHandler);
+    }
+
+    void updateForPhoneId(int phoneId, String simState) {
+        log("update binding for phoneId: " + phoneId + " simState: " + simState);
+        if (!SubscriptionManager.isValidPhoneId(phoneId)) {
+            return;
+        }
+        if (TextUtils.isEmpty(simState)) return;
+        if (simState.equals(mLastSimState[phoneId])) {
+            // ignore consecutive duplicated events
+            return;
+        } else {
+            mLastSimState[phoneId] = simState;
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REBIND, mBindings[phoneId]));
+    }
+
+    private class AppBinding {
+        private int phoneId;
+        private CarrierServiceConnection connection;
+        private int bindCount;
+        private long lastBindStartMillis;
+        private int unbindCount;
+        private long lastUnbindMillis;
+        private String carrierPackage;
+        private String carrierServiceClass;
+        private long mUnbindScheduledUptimeMillis = -1;
+
+        public AppBinding(int phoneId) {
+            this.phoneId = phoneId;
+        }
+
+        public int getPhoneId() {
+            return phoneId;
+        }
+
+        /** Return the package that is currently being bound to, or null if there is no binding. */
+        public String getPackage() {
+            return carrierPackage;
+        }
+
+        /**
+         * Update the bindings for the current carrier app for this phone.
+         *
+         * <p>Safe to call even if a binding already exists. If the current binding is invalid, it
+         * will be dropped. If it is valid, it will be left untouched.
+         */
+        void rebind() {
+            // Get the package name for the carrier app
+            List<String> carrierPackageNames =
+                TelephonyManager.from(mContext).getCarrierPackageNamesForIntentAndPhone(
+                    new Intent(CarrierService.CARRIER_SERVICE_INTERFACE), phoneId
+                );
+
+            if (carrierPackageNames == null || carrierPackageNames.size() <= 0) {
+                log("No carrier app for: " + phoneId);
+                // Unbind after a delay in case this is a temporary blip in carrier privileges.
+                unbind(false /* immediate */);
+                return;
+            }
+
+            log("Found carrier app: " + carrierPackageNames);
+            String candidateCarrierPackage = carrierPackageNames.get(0);
+            // If we are binding to a different package, unbind immediately from the current one.
+            if (!TextUtils.equals(carrierPackage, candidateCarrierPackage)) {
+                unbind(true /* immediate */);
+            }
+
+            // Look up the carrier service
+            Intent carrierService = new Intent(CarrierService.CARRIER_SERVICE_INTERFACE);
+            carrierService.setPackage(candidateCarrierPackage);
+
+            ResolveInfo carrierResolveInfo = mContext.getPackageManager().resolveService(
+                carrierService, PackageManager.GET_META_DATA);
+            Bundle metadata = null;
+            String candidateServiceClass = null;
+            if (carrierResolveInfo != null) {
+                metadata = carrierResolveInfo.serviceInfo.metaData;
+                candidateServiceClass =
+                        carrierResolveInfo.getComponentInfo().getComponentName().getClassName();
+            }
+
+            // Only bind if the service wants it
+            if (metadata == null ||
+                !metadata.getBoolean("android.service.carrier.LONG_LIVED_BINDING", false)) {
+                log("Carrier app does not want a long lived binding");
+                unbind(true /* immediate */);
+                return;
+            }
+
+            if (!TextUtils.equals(carrierServiceClass, candidateServiceClass)) {
+                // Unbind immediately if the carrier service component has changed.
+                unbind(true /* immediate */);
+            } else if (connection != null) {
+                // Component is unchanged and connection is up - do nothing, but cancel any
+                // scheduled unbinds.
+                cancelScheduledUnbind();
+                return;
+            }
+
+            carrierPackage = candidateCarrierPackage;
+            carrierServiceClass = candidateServiceClass;
+
+            log("Binding to " + carrierPackage + " for phone " + phoneId);
+
+            // Log debug information
+            bindCount++;
+            lastBindStartMillis = System.currentTimeMillis();
+
+            connection = new CarrierServiceConnection();
+
+            String error;
+            try {
+                if (mContext.bindServiceAsUser(carrierService, connection,
+                        Context.BIND_AUTO_CREATE |  Context.BIND_FOREGROUND_SERVICE,
+                        mHandler, Process.myUserHandle())) {
+                    return;
+                }
+
+                error = "bindService returned false";
+            } catch (SecurityException ex) {
+                error = ex.getMessage();
+            }
+
+            log("Unable to bind to " + carrierPackage + " for phone " + phoneId +
+                ". Error: " + error);
+            unbind(true /* immediate */);
+        }
+
+        /**
+         * Release the binding.
+         *
+         * @param immediate whether the binding should be released immediately or after a short
+         *                  delay. This should be true unless the reason for the unbind is that no
+         *                  app has carrier privileges, in which case it is useful to delay
+         *                  unbinding in case this is a temporary SIM blip.
+         */
+        void unbind(boolean immediate) {
+            if (connection == null) {
+                // Already fully unbound.
+                return;
+            }
+
+            // Only let the binding linger if a delayed unbind is requested *and* the connection is
+            // currently active. If the connection is down, unbind immediately as the app is likely
+            // not running anyway and it may be a permanent disconnection (e.g. the app was
+            // disabled).
+            if (immediate || !connection.connected) {
+                cancelScheduledUnbind();
+                performImmediateUnbind();
+            } else if (mUnbindScheduledUptimeMillis == -1) {
+                long currentUptimeMillis = SystemClock.uptimeMillis();
+                mUnbindScheduledUptimeMillis = currentUptimeMillis + UNBIND_DELAY_MILLIS;
+                log("Scheduling unbind in " + UNBIND_DELAY_MILLIS + " millis");
+                mHandler.sendMessageAtTime(
+                        mHandler.obtainMessage(EVENT_PERFORM_IMMEDIATE_UNBIND, this),
+                        mUnbindScheduledUptimeMillis);
+            }
+        }
+
+        private void performImmediateUnbind() {
+            // Log debug information
+            unbindCount++;
+            lastUnbindMillis = System.currentTimeMillis();
+
+            // Clear package state now that no binding is desired.
+            carrierPackage = null;
+            carrierServiceClass = null;
+
+            // Actually unbind
+            log("Unbinding from carrier app");
+            mContext.unbindService(connection);
+            connection = null;
+            mUnbindScheduledUptimeMillis = -1;
+        }
+
+        private void cancelScheduledUnbind() {
+            mHandler.removeMessages(EVENT_PERFORM_IMMEDIATE_UNBIND);
+            mUnbindScheduledUptimeMillis = -1;
+        }
+
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            pw.println("Carrier app binding for phone " + phoneId);
+            pw.println("  connection: " + connection);
+            pw.println("  bindCount: " + bindCount);
+            pw.println("  lastBindStartMillis: " + lastBindStartMillis);
+            pw.println("  unbindCount: " + unbindCount);
+            pw.println("  lastUnbindMillis: " + lastUnbindMillis);
+            pw.println("  mUnbindScheduledUptimeMillis: " + mUnbindScheduledUptimeMillis);
+            pw.println();
+        }
+    }
+
+    private class CarrierServiceConnection implements ServiceConnection {
+        private boolean connected;
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            log("Connected to carrier app: " + name.flattenToString());
+            connected = true;
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            log("Disconnected from carrier app: " + name.flattenToString());
+            connected = false;
+        }
+
+        @Override
+        public String toString() {
+            return "CarrierServiceConnection[connected=" + connected + "]";
+        }
+    }
+
+    private class CarrierServicePackageMonitor extends PackageMonitor {
+        @Override
+        public void onPackageAdded(String packageName, int reason) {
+            evaluateBinding(packageName, true /* forceUnbind */);
+        }
+
+        @Override
+        public void onPackageRemoved(String packageName, int reason) {
+            evaluateBinding(packageName, true /* forceUnbind */);
+        }
+
+        @Override
+        public void onPackageUpdateFinished(String packageName, int uid) {
+            evaluateBinding(packageName, true /* forceUnbind */);
+        }
+
+        @Override
+        public void onPackageModified(String packageName) {
+            evaluateBinding(packageName, false /* forceUnbind */);
+        }
+
+        @Override
+        public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
+            if (doit) {
+                for (String packageName : packages) {
+                    evaluateBinding(packageName, true /* forceUnbind */);
+                }
+            }
+            return super.onHandleForceStop(intent, packages, uid, doit);
+        }
+
+        private void evaluateBinding(String carrierPackageName, boolean forceUnbind) {
+            for (AppBinding appBinding : mBindings) {
+                String appBindingPackage = appBinding.getPackage();
+                boolean isBindingForPackage = carrierPackageName.equals(appBindingPackage);
+                // Only log if this package was a carrier package to avoid log spam in the common
+                // case that there are no carrier packages, but evaluate the binding if the package
+                // is unset, in case this package change resulted in a new carrier package becoming
+                // available for binding.
+                if (isBindingForPackage) {
+                    log(carrierPackageName + " changed and corresponds to a phone. Rebinding.");
+                }
+                if (appBindingPackage == null || isBindingForPackage) {
+                    if (forceUnbind) {
+                        appBinding.unbind(true /* immediate */);
+                    }
+                    appBinding.rebind();
+                }
+            }
+        }
+    }
+
+    private static void log(String message) {
+        Log.d(LOG_TAG, message);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CarrierServiceBindHelper:");
+        for (AppBinding binding : mBindings) {
+            binding.dump(fd, pw, args);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/CarrierServiceStateTracker.java b/com/android/internal/telephony/CarrierServiceStateTracker.java
new file mode 100644
index 0000000..8df201e
--- /dev/null
+++ b/com/android/internal/telephony/CarrierServiceStateTracker.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.util.NotificationChannelController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * This contains Carrier specific logic based on the states/events
+ * managed in ServiceStateTracker.
+ * {@hide}
+ */
+public class CarrierServiceStateTracker extends Handler {
+    private static final String LOG_TAG = "CSST";
+    protected static final int CARRIER_EVENT_BASE = 100;
+    protected static final int CARRIER_EVENT_VOICE_REGISTRATION = CARRIER_EVENT_BASE + 1;
+    protected static final int CARRIER_EVENT_VOICE_DEREGISTRATION = CARRIER_EVENT_BASE + 2;
+    protected static final int CARRIER_EVENT_DATA_REGISTRATION = CARRIER_EVENT_BASE + 3;
+    protected static final int CARRIER_EVENT_DATA_DEREGISTRATION = CARRIER_EVENT_BASE + 4;
+    private static final int UNINITIALIZED_DELAY_VALUE = -1;
+    private Phone mPhone;
+    private ServiceStateTracker mSST;
+
+    public static final int NOTIFICATION_PREF_NETWORK = 1000;
+    public static final int NOTIFICATION_EMERGENCY_NETWORK = 1001;
+
+    private final Map<Integer, NotificationType> mNotificationTypeMap = new HashMap<>();
+
+    public CarrierServiceStateTracker(Phone phone, ServiceStateTracker sst) {
+        this.mPhone = phone;
+        this.mSST = sst;
+        phone.getContext().registerReceiver(mBroadcastReceiver, new IntentFilter(
+                CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+        registerNotificationTypes();
+    }
+
+    private void registerNotificationTypes() {
+        mNotificationTypeMap.put(NOTIFICATION_PREF_NETWORK,
+                new PrefNetworkNotification(NOTIFICATION_PREF_NETWORK));
+        mNotificationTypeMap.put(NOTIFICATION_EMERGENCY_NETWORK,
+                new EmergencyNetworkNotification(NOTIFICATION_EMERGENCY_NETWORK));
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case CARRIER_EVENT_VOICE_REGISTRATION:
+            case CARRIER_EVENT_DATA_REGISTRATION:
+                handleConfigChanges();
+                break;
+            case CARRIER_EVENT_VOICE_DEREGISTRATION:
+            case CARRIER_EVENT_DATA_DEREGISTRATION:
+                if (isRadioOffOrAirplaneMode()) {
+                    break;
+                }
+                handleConfigChanges();
+                break;
+            case NOTIFICATION_EMERGENCY_NETWORK:
+            case NOTIFICATION_PREF_NETWORK:
+                Rlog.d(LOG_TAG, "sending notification after delay: " + msg.what);
+                NotificationType notificationType = mNotificationTypeMap.get(msg.what);
+                if (notificationType != null) {
+                    sendNotification(notificationType);
+                }
+                break;
+        }
+    }
+
+    private boolean isPhoneStillRegistered() {
+        if (mSST.mSS == null) {
+            return true; //something has gone wrong, return true and not show the notification.
+        }
+        return (mSST.mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE
+                || mSST.mSS.getDataRegState() == ServiceState.STATE_IN_SERVICE);
+    }
+
+    private boolean isPhoneVoiceRegistered() {
+        if (mSST.mSS == null) {
+            return true; //something has gone wrong, return true and not show the notification.
+        }
+        return (mSST.mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE);
+    }
+
+    private boolean isPhoneRegisteredForWifiCalling() {
+        Rlog.d(LOG_TAG, "isPhoneRegisteredForWifiCalling: " + mPhone.isWifiCallingEnabled());
+        return mPhone.isWifiCallingEnabled();
+    }
+
+    /**
+     * Returns true if the radio is off or in Airplane Mode else returns false.
+     */
+    @VisibleForTesting
+    public boolean isRadioOffOrAirplaneMode() {
+        Context context = mPhone.getContext();
+        int airplaneMode = -1;
+        try {
+            airplaneMode = Settings.Global.getInt(context.getContentResolver(),
+                    Settings.Global.AIRPLANE_MODE_ON, 0);
+        } catch (Exception e) {
+            Rlog.e(LOG_TAG, "Unable to get AIRPLACE_MODE_ON.");
+            return true;
+        }
+        return (!mSST.isRadioOn() || (airplaneMode != 0));
+    }
+
+    /**
+     * Returns true if the preferred network is set to 'Global'.
+     */
+    private boolean isGlobalMode() {
+        Context context = mPhone.getContext();
+        int preferredNetworkSetting = -1;
+        try {
+            preferredNetworkSetting =
+                    android.provider.Settings.Global.getInt(context.getContentResolver(),
+                            android.provider.Settings.Global.PREFERRED_NETWORK_MODE
+                                    + mPhone.getSubId(), Phone.PREFERRED_NT_MODE);
+        } catch (Exception e) {
+            Rlog.e(LOG_TAG, "Unable to get PREFERRED_NETWORK_MODE.");
+            return true;
+        }
+        return (preferredNetworkSetting == RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA);
+    }
+
+    private void handleConfigChanges() {
+        for (Map.Entry<Integer, NotificationType> entry : mNotificationTypeMap.entrySet()) {
+            NotificationType notificationType = entry.getValue();
+            if (evaluateSendingMessage(notificationType)) {
+                Message notificationMsg = obtainMessage(notificationType.getTypeId(), null);
+                Rlog.i(LOG_TAG, "starting timer for notifications." + notificationType.getTypeId());
+                sendMessageDelayed(notificationMsg, getDelay(notificationType));
+            } else {
+                cancelNotification(notificationType.getTypeId());
+                Rlog.i(LOG_TAG, "canceling notifications: " + notificationType.getTypeId());
+            }
+        }
+    }
+
+    /**
+     * This method adds a level of indirection, and was created so we can unit the class.
+     **/
+    @VisibleForTesting
+    public boolean evaluateSendingMessage(NotificationType notificationType) {
+        return notificationType.sendMessage();
+    }
+
+    /**
+     * This method adds a level of indirection, and was created so we can unit the class.
+     **/
+    @VisibleForTesting
+    public int getDelay(NotificationType notificationType) {
+        return notificationType.getDelay();
+    }
+
+    /**
+     * This method adds a level of indirection, and was created so we can unit the class.
+     **/
+    @VisibleForTesting
+    public Notification.Builder getNotificationBuilder(NotificationType notificationType) {
+        return notificationType.getNotificationBuilder();
+    }
+
+    /**
+     * This method adds a level of indirection, and was created so we can unit the class.
+     **/
+    @VisibleForTesting
+    public NotificationManager getNotificationManager(Context context) {
+        return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
+                    context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            PersistableBundle b = carrierConfigManager.getConfigForSubId(mPhone.getSubId());
+
+            for (Map.Entry<Integer, NotificationType> entry : mNotificationTypeMap.entrySet()) {
+                NotificationType notificationType = entry.getValue();
+                notificationType.setDelay(b);
+            }
+            handleConfigChanges();
+        }
+    };
+
+    /**
+     * Post a notification to the NotificationManager for changing network type.
+     */
+    @VisibleForTesting
+    public void sendNotification(NotificationType notificationType) {
+        if (!evaluateSendingMessage(notificationType)) {
+            return;
+        }
+
+        Context context = mPhone.getContext();
+        Notification.Builder builder = getNotificationBuilder(notificationType);
+        // set some common attributes
+        builder.setWhen(System.currentTimeMillis())
+                .setAutoCancel(true)
+                .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
+                .setColor(context.getResources().getColor(
+                       com.android.internal.R.color.system_notification_accent_color));
+
+        getNotificationManager(context).notify(notificationType.getTypeId(), builder.build());
+    }
+
+    /**
+     * Cancel notifications if a registration is pending or has been sent.
+     **/
+    public void cancelNotification(int notificationId) {
+        Context context = mPhone.getContext();
+        removeMessages(notificationId);
+        getNotificationManager(context).cancel(notificationId);
+    }
+
+    /**
+     * Class that defines the different types of notifications.
+     */
+    public interface NotificationType {
+
+        /**
+         * decides if the message should be sent, Returns boolean
+         **/
+        boolean sendMessage();
+
+        /**
+         * returns the interval by which the message is delayed.
+         **/
+        int getDelay();
+
+        /** sets the interval by which the message is delayed.
+         * @param bundle PersistableBundle
+        **/
+        void setDelay(PersistableBundle bundle);
+
+        /**
+         * returns notification type id.
+         **/
+        int getTypeId();
+
+        /**
+         * returns the notification builder, for the notification to be displayed.
+         **/
+        Notification.Builder getNotificationBuilder();
+    }
+
+    /**
+     * Class that defines the network notification, which is shown when the phone cannot camp on
+     * a network, and has 'preferred mode' set to global.
+     */
+    public class PrefNetworkNotification implements NotificationType {
+
+        private final int mTypeId;
+        private int mDelay = UNINITIALIZED_DELAY_VALUE;
+
+        PrefNetworkNotification(int typeId) {
+            this.mTypeId = typeId;
+        }
+
+        /** sets the interval by which the message is delayed.
+         * @param bundle PersistableBundle
+         **/
+        public void setDelay(PersistableBundle bundle) {
+            if (bundle == null) {
+                Rlog.e(LOG_TAG, "bundle is null");
+                return;
+            }
+            this.mDelay = bundle.getInt(
+                    CarrierConfigManager.KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT);
+            Rlog.i(LOG_TAG, "reading time to delay notification emergency: " + mDelay);
+        }
+
+        public int getDelay() {
+            return mDelay;
+        }
+
+        public int getTypeId() {
+            return mTypeId;
+        }
+
+        /**
+         * Contains logic on sending notifications.
+         */
+        public boolean sendMessage() {
+            Rlog.i(LOG_TAG, "PrefNetworkNotification: sendMessage() w/values: "
+                    + "," + isPhoneStillRegistered() + "," + mDelay + "," + isGlobalMode()
+                    + "," + mSST.isRadioOn());
+            if (mDelay == UNINITIALIZED_DELAY_VALUE ||  isPhoneStillRegistered()
+                    || isGlobalMode()) {
+                return false;
+            }
+            return true;
+        }
+
+        /**
+         * Builds a partial notificaiton builder, and returns it.
+         */
+        public Notification.Builder getNotificationBuilder() {
+            Context context = mPhone.getContext();
+            Intent notificationIntent = new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);
+            PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, notificationIntent,
+                    PendingIntent.FLAG_ONE_SHOT);
+            CharSequence title = context.getText(
+                    com.android.internal.R.string.NetworkPreferenceSwitchTitle);
+            CharSequence details = context.getText(
+                    com.android.internal.R.string.NetworkPreferenceSwitchSummary);
+            return new Notification.Builder(context)
+                    .setContentTitle(title)
+                    .setStyle(new Notification.BigTextStyle().bigText(details))
+                    .setContentText(details)
+                    .setChannel(NotificationChannelController.CHANNEL_ID_ALERT)
+                    .setContentIntent(settingsIntent);
+        }
+    }
+
+    /**
+     * Class that defines the emergency notification, which is shown when the user is out of cell
+     * connectivity, but has wifi enabled.
+     */
+    public class EmergencyNetworkNotification implements NotificationType {
+
+        private final int mTypeId;
+        private int mDelay = UNINITIALIZED_DELAY_VALUE;
+
+        EmergencyNetworkNotification(int typeId) {
+            this.mTypeId = typeId;
+        }
+
+        /** sets the interval by which the message is delayed.
+         * @param bundle PersistableBundle
+         **/
+        public void setDelay(PersistableBundle bundle) {
+            if (bundle == null) {
+                Rlog.e(LOG_TAG, "bundle is null");
+                return;
+            }
+            this.mDelay = bundle.getInt(
+                    CarrierConfigManager.KEY_EMERGENCY_NOTIFICATION_DELAY_INT);
+            Rlog.i(LOG_TAG, "reading time to delay notification emergency: " + mDelay);
+        }
+
+        public int getDelay() {
+            return mDelay;
+        }
+
+        public int getTypeId() {
+            return mTypeId;
+        }
+
+        /**
+         * Contains logic on sending notifications,
+         */
+        public boolean sendMessage() {
+            Rlog.i(LOG_TAG, "EmergencyNetworkNotification: sendMessage() w/values: "
+                    + "," + isPhoneVoiceRegistered() + "," + mDelay + ","
+                    + isPhoneRegisteredForWifiCalling() + "," + mSST.isRadioOn());
+            if (mDelay == UNINITIALIZED_DELAY_VALUE || isPhoneVoiceRegistered()
+                    || !isPhoneRegisteredForWifiCalling()) {
+                return false;
+            }
+            return true;
+        }
+
+        /**
+         * Builds a partial notificaiton builder, and returns it.
+         */
+        public Notification.Builder getNotificationBuilder() {
+            Context context = mPhone.getContext();
+            CharSequence title = context.getText(
+                    com.android.internal.R.string.EmergencyCallWarningTitle);
+            CharSequence details = context.getText(
+                    com.android.internal.R.string.EmergencyCallWarningSummary);
+            return new Notification.Builder(context)
+                    .setContentTitle(title)
+                    .setStyle(new Notification.BigTextStyle().bigText(details))
+                    .setContentText(details)
+                    .setChannel(NotificationChannelController.CHANNEL_ID_WFC);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/CarrierServicesSmsFilter.java b/com/android/internal/telephony/CarrierServicesSmsFilter.java
new file mode 100644
index 0000000..f3bc1fd
--- /dev/null
+++ b/com/android/internal/telephony/CarrierServicesSmsFilter.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.service.carrier.CarrierMessagingService;
+import android.service.carrier.ICarrierMessagingCallback;
+import android.service.carrier.ICarrierMessagingService;
+import android.service.carrier.MessagePdu;
+import android.telephony.CarrierMessagingServiceManager;
+import android.telephony.Rlog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccController;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Filters incoming SMS with carrier services.
+ * <p> A new instance must be created for filtering each message.
+ */
+public class CarrierServicesSmsFilter {
+    protected static final boolean DBG = true;
+
+    private final Context mContext;
+    private final Phone mPhone;
+    private final byte[][] mPdus;
+    private final int mDestPort;
+    private final String mPduFormat;
+    private final CarrierServicesSmsFilterCallbackInterface mCarrierServicesSmsFilterCallback;
+    private final String mLogTag;
+
+    @VisibleForTesting
+    public CarrierServicesSmsFilter(
+            Context context,
+            Phone phone,
+            byte[][] pdus,
+            int destPort,
+            String pduFormat,
+            CarrierServicesSmsFilterCallbackInterface carrierServicesSmsFilterCallback,
+            String logTag) {
+        mContext = context;
+        mPhone = phone;
+        mPdus = pdus;
+        mDestPort = destPort;
+        mPduFormat = pduFormat;
+        mCarrierServicesSmsFilterCallback = carrierServicesSmsFilterCallback;
+        mLogTag = logTag;
+    }
+
+    /**
+     * @return {@code true} if the SMS was handled by carrier services.
+     */
+    @VisibleForTesting
+    public boolean filter() {
+        Optional<String> carrierAppForFiltering = getCarrierAppPackageForFiltering();
+        List<String> smsFilterPackages = new ArrayList<>();
+        if (carrierAppForFiltering.isPresent()) {
+            smsFilterPackages.add(carrierAppForFiltering.get());
+        }
+        String carrierImsPackage = CarrierSmsUtils.getCarrierImsPackageForIntent(mContext, mPhone,
+                new Intent(CarrierMessagingService.SERVICE_INTERFACE));
+        if (carrierImsPackage != null) {
+            smsFilterPackages.add(carrierImsPackage);
+        }
+        FilterAggregator filterAggregator = new FilterAggregator(smsFilterPackages.size());
+        for (String smsFilterPackage : smsFilterPackages) {
+            filterWithPackage(smsFilterPackage, filterAggregator);
+        }
+        boolean handled = smsFilterPackages.size() > 0;
+        return handled;
+    }
+
+    private Optional<String> getCarrierAppPackageForFiltering() {
+        List<String> carrierPackages = null;
+        UiccCard card = UiccController.getInstance().getUiccCard(mPhone.getPhoneId());
+        if (card != null) {
+            carrierPackages = card.getCarrierPackageNamesForIntent(
+                    mContext.getPackageManager(),
+                    new Intent(CarrierMessagingService.SERVICE_INTERFACE));
+        } else {
+            Rlog.e(mLogTag, "UiccCard not initialized.");
+        }
+        if (carrierPackages != null && carrierPackages.size() == 1) {
+            log("Found carrier package.");
+            return Optional.of(carrierPackages.get(0));
+        }
+
+        // It is possible that carrier app is not present as a CarrierPackage, but instead as a
+        // system app
+        List<String> systemPackages =
+                getSystemAppForIntent(new Intent(CarrierMessagingService.SERVICE_INTERFACE));
+
+        if (systemPackages != null && systemPackages.size() == 1) {
+            log("Found system package.");
+            return Optional.of(systemPackages.get(0));
+        }
+        logv("Unable to find carrier package: " + carrierPackages
+                + ", nor systemPackages: " + systemPackages);
+        return Optional.empty();
+    }
+
+    private void filterWithPackage(String packageName, FilterAggregator filterAggregator) {
+        CarrierSmsFilter smsFilter = new CarrierSmsFilter(mPdus, mDestPort, mPduFormat);
+        CarrierSmsFilterCallback smsFilterCallback =
+                new CarrierSmsFilterCallback(filterAggregator, smsFilter);
+        smsFilter.filterSms(packageName, smsFilterCallback);
+    }
+
+    private List<String> getSystemAppForIntent(Intent intent) {
+        List<String> packages = new ArrayList<String>();
+        PackageManager packageManager = mContext.getPackageManager();
+        List<ResolveInfo> receivers = packageManager.queryIntentServices(intent, 0);
+        String carrierFilterSmsPerm = "android.permission.CARRIER_FILTER_SMS";
+
+        for (ResolveInfo info : receivers) {
+            if (info.serviceInfo == null) {
+                loge("Can't get service information from " + info);
+                continue;
+            }
+            String packageName = info.serviceInfo.packageName;
+            if (packageManager.checkPermission(carrierFilterSmsPerm, packageName)
+                    == packageManager.PERMISSION_GRANTED) {
+                packages.add(packageName);
+                if (DBG) log("getSystemAppForIntent: added package " + packageName);
+            }
+        }
+        return packages;
+    }
+
+    private void log(String message) {
+        Rlog.d(mLogTag, message);
+    }
+
+    private void loge(String message) {
+        Rlog.e(mLogTag, message);
+    }
+
+    private void logv(String message) {
+        Rlog.e(mLogTag, message);
+    }
+
+    /**
+     * Result of filtering SMS is returned in this callback.
+     */
+    @VisibleForTesting
+    public interface CarrierServicesSmsFilterCallbackInterface {
+        void onFilterComplete(int result);
+    }
+
+    /**
+     * Asynchronously binds to the carrier messaging service, and filters out the message if
+     * instructed to do so by the carrier messaging service. A new instance must be used for every
+     * message.
+     */
+    private final class CarrierSmsFilter extends CarrierMessagingServiceManager {
+        private final byte[][] mPdus;
+        private final int mDestPort;
+        private final String mSmsFormat;
+        // Instantiated in filterSms.
+        private volatile CarrierSmsFilterCallback mSmsFilterCallback;
+
+        CarrierSmsFilter(byte[][] pdus, int destPort, String smsFormat) {
+            mPdus = pdus;
+            mDestPort = destPort;
+            mSmsFormat = smsFormat;
+        }
+
+        /**
+         * Attempts to bind to a {@link ICarrierMessagingService}. Filtering is initiated
+         * asynchronously once the service is ready using {@link #onServiceReady}.
+         */
+        void filterSms(String carrierPackageName, CarrierSmsFilterCallback smsFilterCallback) {
+            mSmsFilterCallback = smsFilterCallback;
+            if (!bindToCarrierMessagingService(mContext, carrierPackageName)) {
+                loge("bindService() for carrier messaging service failed");
+                smsFilterCallback.onFilterComplete(CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT);
+            } else {
+                logv("bindService() for carrier messaging service succeeded");
+            }
+        }
+
+        /**
+         * Invokes the {@code carrierMessagingService} to filter messages. The filtering result is
+         * delivered to {@code smsFilterCallback}.
+         */
+        @Override
+        protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
+            try {
+                carrierMessagingService.filterSms(
+                        new MessagePdu(Arrays.asList(mPdus)), mSmsFormat, mDestPort,
+                        mPhone.getSubId(), mSmsFilterCallback);
+            } catch (RemoteException e) {
+                loge("Exception filtering the SMS: " + e);
+                mSmsFilterCallback.onFilterComplete(
+                        CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT);
+            }
+        }
+    }
+
+    /**
+     * A callback used to notify the platform of the carrier messaging app filtering result. Once
+     * the result is ready, the carrier messaging service connection is disposed.
+     */
+    private final class CarrierSmsFilterCallback extends ICarrierMessagingCallback.Stub {
+        private final FilterAggregator mFilterAggregator;
+        private final CarrierMessagingServiceManager mCarrierMessagingServiceManager;
+
+        CarrierSmsFilterCallback(FilterAggregator filterAggregator,
+                                 CarrierMessagingServiceManager carrierMessagingServiceManager) {
+            mFilterAggregator = filterAggregator;
+            mCarrierMessagingServiceManager = carrierMessagingServiceManager;
+        }
+
+        /**
+         * This method should be called only once.
+         */
+        @Override
+        public void onFilterComplete(int result) {
+            mCarrierMessagingServiceManager.disposeConnection(mContext);
+            mFilterAggregator.onFilterComplete(result);
+        }
+
+        @Override
+        public void onSendSmsComplete(int result, int messageRef) {
+            loge("Unexpected onSendSmsComplete call with result: " + result);
+        }
+
+        @Override
+        public void onSendMultipartSmsComplete(int result, int[] messageRefs) {
+            loge("Unexpected onSendMultipartSmsComplete call with result: " + result);
+        }
+
+        @Override
+        public void onSendMmsComplete(int result, byte[] sendConfPdu) {
+            loge("Unexpected onSendMmsComplete call with result: " + result);
+        }
+
+        @Override
+        public void onDownloadMmsComplete(int result) {
+            loge("Unexpected onDownloadMmsComplete call with result: " + result);
+        }
+    }
+
+    private final class FilterAggregator {
+        private final Object mFilterLock = new Object();
+        private int mNumPendingFilters;
+        private int mFilterResult;
+
+        FilterAggregator(int numFilters) {
+            mNumPendingFilters = numFilters;
+            mFilterResult = CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT;
+        }
+
+        void onFilterComplete(int result) {
+            synchronized (mFilterLock) {
+                mNumPendingFilters--;
+                combine(result);
+                if (mNumPendingFilters == 0) {
+                    // Calling identity was the CarrierMessagingService in this callback, change it
+                    // back to ours.
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        mCarrierServicesSmsFilterCallback.onFilterComplete(mFilterResult);
+                    } finally {
+                        // return back to the CarrierMessagingService, restore the calling identity.
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            }
+        }
+
+        private void combine(int result) {
+            mFilterResult = mFilterResult | result;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/CarrierSignalAgent.java b/com/android/internal/telephony/CarrierSignalAgent.java
new file mode 100644
index 0000000..f2dd2aa
--- /dev/null
+++ b/com/android/internal/telephony/CarrierSignalAgent.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static android.telephony.CarrierConfigManager.KEY_CARRIER_APP_WAKE_SIGNAL_CONFIG_STRING_ARRAY;
+import static android.telephony.CarrierConfigManager.KEY_CARRIER_APP_NO_WAKE_SIGNAL_CONFIG_STRING_ARRAY;
+
+/**
+ * This class act as an CarrierSignalling Agent.
+ * it load registered carrier signalling receivers from carrier config, cache the result to avoid
+ * repeated polling and send the intent to the interested receivers.
+ * Each CarrierSignalAgent is associated with a phone object.
+ */
+public class CarrierSignalAgent extends Handler {
+
+    private static final String LOG_TAG = CarrierSignalAgent.class.getSimpleName();
+    private static final boolean DBG = true;
+    private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
+    private static final boolean WAKE = true;
+    private static final boolean NO_WAKE = false;
+
+    /** delimiters for parsing config of the form: pakName./receiverName : signal1, signal2,..*/
+    private static final String COMPONENT_NAME_DELIMITER = "\\s*:\\s*";
+    private static final String CARRIER_SIGNAL_DELIMITER = "\\s*,\\s*";
+
+    /** Member variables */
+    private final Phone mPhone;
+    private boolean mDefaultNetworkAvail;
+
+    /**
+     * This is a map of intent action -> set of component name of statically registered
+     * carrier signal receivers(wakeup receivers).
+     * Those intents are declared in the Manifest files, aiming to wakeup broadcast receivers.
+     * Carrier apps should be careful when configuring the wake signal list to avoid unnecessary
+     * wakeup. Note we use Set as the entry value to compare config directly regardless of element
+     * order.
+     * @see CarrierConfigManager#KEY_CARRIER_APP_WAKE_SIGNAL_CONFIG_STRING_ARRAY
+     */
+    private Map<String, Set<ComponentName>> mCachedWakeSignalConfigs = new HashMap<>();
+
+    /**
+     * This is a map of intent action -> set of component name of dynamically registered
+     * carrier signal receivers(non-wakeup receivers). Those intents will not wake up the apps.
+     * Note Carrier apps should avoid configuring no wake signals in there Manifest files.
+     * Note we use Set as the entry value to compare config directly regardless of element order.
+     * @see CarrierConfigManager#KEY_CARRIER_APP_NO_WAKE_SIGNAL_CONFIG_STRING_ARRAY
+     */
+    private Map<String, Set<ComponentName>> mCachedNoWakeSignalConfigs = new HashMap<>();
+
+    private static final int EVENT_REGISTER_DEFAULT_NETWORK_AVAIL = 0;
+
+    /**
+     * This is a list of supported signals from CarrierSignalAgent
+     */
+    private final Set<String> mCarrierSignalList = new HashSet<>(Arrays.asList(
+            TelephonyIntents.ACTION_CARRIER_SIGNAL_PCO_VALUE,
+            TelephonyIntents.ACTION_CARRIER_SIGNAL_REDIRECTED,
+            TelephonyIntents.ACTION_CARRIER_SIGNAL_REQUEST_NETWORK_FAILED,
+            TelephonyIntents.ACTION_CARRIER_SIGNAL_RESET,
+            TelephonyIntents.ACTION_CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE));
+
+    private final LocalLog mErrorLocalLog = new LocalLog(20);
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (DBG) log("CarrierSignalAgent receiver action: " + action);
+            if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                loadCarrierConfig();
+            }
+        }
+    };
+
+    private ConnectivityManager.NetworkCallback mNetworkCallback;
+
+    /** Constructor */
+    public CarrierSignalAgent(Phone phone) {
+        mPhone = phone;
+        loadCarrierConfig();
+        // reload configurations on CARRIER_CONFIG_CHANGED
+        mPhone.getContext().registerReceiver(mReceiver,
+                new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+        mPhone.getCarrierActionAgent().registerForCarrierAction(
+                CarrierActionAgent.CARRIER_ACTION_REPORT_DEFAULT_NETWORK_STATUS, this,
+                EVENT_REGISTER_DEFAULT_NETWORK_AVAIL, null, false);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case EVENT_REGISTER_DEFAULT_NETWORK_AVAIL:
+                AsyncResult ar = (AsyncResult) msg.obj;
+                if (ar.exception != null) {
+                    Rlog.e(LOG_TAG, "Register default network exception: " + ar.exception);
+                    return;
+                }
+                final ConnectivityManager connectivityMgr =  ConnectivityManager
+                        .from(mPhone.getContext());
+                if ((boolean) ar.result) {
+                    mNetworkCallback = new ConnectivityManager.NetworkCallback() {
+                        @Override
+                        public void onAvailable(Network network) {
+                            // an optimization to avoid signaling on every default network switch.
+                            if (!mDefaultNetworkAvail) {
+                                if (DBG) log("Default network available: " + network);
+                                Intent intent = new Intent(TelephonyIntents
+                                        .ACTION_CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE);
+                                intent.putExtra(
+                                        TelephonyIntents.EXTRA_DEFAULT_NETWORK_AVAILABLE_KEY, true);
+                                notifyCarrierSignalReceivers(intent);
+                                mDefaultNetworkAvail = true;
+                            }
+                        }
+                        @Override
+                        public void onLost(Network network) {
+                            if (DBG) log("Default network lost: " + network);
+                            Intent intent = new Intent(TelephonyIntents
+                                    .ACTION_CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE);
+                            intent.putExtra(
+                                    TelephonyIntents.EXTRA_DEFAULT_NETWORK_AVAILABLE_KEY, false);
+                            notifyCarrierSignalReceivers(intent);
+                            mDefaultNetworkAvail = false;
+                        }
+                    };
+                    connectivityMgr.registerDefaultNetworkCallback(mNetworkCallback, mPhone);
+                    log("Register default network");
+
+                } else if (mNetworkCallback != null) {
+                    connectivityMgr.unregisterNetworkCallback(mNetworkCallback);
+                    mNetworkCallback = null;
+                    mDefaultNetworkAvail = false;
+                    log("unregister default network");
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * load carrier config and cached the results into a hashMap action -> array list of components.
+     */
+    private void loadCarrierConfig() {
+        CarrierConfigManager configManager = (CarrierConfigManager) mPhone.getContext()
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle b = null;
+        if (configManager != null) {
+            b = configManager.getConfig();
+        }
+        if (b != null) {
+            synchronized (mCachedWakeSignalConfigs) {
+                log("Loading carrier config: " + KEY_CARRIER_APP_WAKE_SIGNAL_CONFIG_STRING_ARRAY);
+                Map<String, Set<ComponentName>> config = parseAndCache(
+                        b.getStringArray(KEY_CARRIER_APP_WAKE_SIGNAL_CONFIG_STRING_ARRAY));
+                // In some rare cases, up-to-date config could be fetched with delay and all signals
+                // have already been delivered the receivers from the default carrier config.
+                // To handle this raciness, we should notify those receivers (from old configs)
+                // and reset carrier actions. This should be done before cached Config got purged
+                // and written with the up-to-date value, Otherwise those receivers from the
+                // old config might lingers without properly clean-up.
+                if (!mCachedWakeSignalConfigs.isEmpty()
+                        && !config.equals(mCachedWakeSignalConfigs)) {
+                    if (VDBG) log("carrier config changed, reset receivers from old config");
+                    mPhone.getCarrierActionAgent().sendEmptyMessage(
+                            CarrierActionAgent.CARRIER_ACTION_RESET);
+                }
+                mCachedWakeSignalConfigs = config;
+            }
+
+            synchronized (mCachedNoWakeSignalConfigs) {
+                log("Loading carrier config: "
+                        + KEY_CARRIER_APP_NO_WAKE_SIGNAL_CONFIG_STRING_ARRAY);
+                Map<String, Set<ComponentName>> config = parseAndCache(
+                        b.getStringArray(KEY_CARRIER_APP_NO_WAKE_SIGNAL_CONFIG_STRING_ARRAY));
+                if (!mCachedNoWakeSignalConfigs.isEmpty()
+                        && !config.equals(mCachedNoWakeSignalConfigs)) {
+                    if (VDBG) log("carrier config changed, reset receivers from old config");
+                    mPhone.getCarrierActionAgent().sendEmptyMessage(
+                            CarrierActionAgent.CARRIER_ACTION_RESET);
+                }
+                mCachedNoWakeSignalConfigs = config;
+            }
+        }
+    }
+
+    /**
+     * Parse each config with the form {pakName./receiverName : signal1, signal2,.} and cached the
+     * result internally to avoid repeated polling
+     * @see #CARRIER_SIGNAL_DELIMITER
+     * @see #COMPONENT_NAME_DELIMITER
+     * @param configs raw information from carrier config
+     */
+    private Map<String, Set<ComponentName>> parseAndCache(String[] configs) {
+        Map<String, Set<ComponentName>> newCachedWakeSignalConfigs = new HashMap<>();
+        if (!ArrayUtils.isEmpty(configs)) {
+            for (String config : configs) {
+                if (!TextUtils.isEmpty(config)) {
+                    String[] splitStr = config.trim().split(COMPONENT_NAME_DELIMITER, 2);
+                    if (splitStr.length == 2) {
+                        ComponentName componentName = ComponentName
+                                .unflattenFromString(splitStr[0]);
+                        if (componentName == null) {
+                            loge("Invalid component name: " + splitStr[0]);
+                            continue;
+                        }
+                        String[] signals = splitStr[1].split(CARRIER_SIGNAL_DELIMITER);
+                        for (String s : signals) {
+                            if (!mCarrierSignalList.contains(s)) {
+                                loge("Invalid signal name: " + s);
+                                continue;
+                            }
+                            Set<ComponentName> componentList = newCachedWakeSignalConfigs.get(s);
+                            if (componentList == null) {
+                                componentList = new HashSet<>();
+                                newCachedWakeSignalConfigs.put(s, componentList);
+                            }
+                            componentList.add(componentName);
+                            if (VDBG) {
+                                logv("Add config " + "{signal: " + s
+                                        + " componentName: " + componentName + "}");
+                            }
+                        }
+                    } else {
+                        loge("invalid config format: " + config);
+                    }
+                }
+            }
+        }
+        return newCachedWakeSignalConfigs;
+    }
+
+    /**
+     * Check if there are registered carrier broadcast receivers to handle the passing intent
+     */
+    public boolean hasRegisteredReceivers(String action) {
+        return mCachedWakeSignalConfigs.containsKey(action)
+                || mCachedNoWakeSignalConfigs.containsKey(action);
+    }
+
+    /**
+     * Broadcast the intents explicitly.
+     * Some sanity check will be applied before broadcasting.
+     * - for non-wakeup(runtime) receivers, make sure the intent is not declared in their manifests
+     * and apply FLAG_EXCLUDE_STOPPED_PACKAGES to avoid wake-up
+     * - for wakeup(manifest) receivers, make sure there are matched receivers with registered
+     * intents.
+     *
+     * @param intent intent which signals carrier apps
+     * @param receivers a list of component name for broadcast receivers.
+     *                  Those receivers could either be statically declared in Manifest or
+     *                  registered during run-time.
+     * @param wakeup true indicate wakeup receivers otherwise non-wakeup receivers
+     */
+    private void broadcast(Intent intent, Set<ComponentName> receivers, boolean wakeup) {
+        final PackageManager packageManager = mPhone.getContext().getPackageManager();
+        for (ComponentName name : receivers) {
+            Intent signal = new Intent(intent);
+            signal.setComponent(name);
+
+            if (wakeup && packageManager.queryBroadcastReceivers(signal,
+                    PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) {
+                loge("Carrier signal receivers are configured but unavailable: "
+                        + signal.getComponent());
+                return;
+            }
+            if (!wakeup && !packageManager.queryBroadcastReceivers(signal,
+                    PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) {
+                loge("Runtime signals shouldn't be configured in Manifest: "
+                        + signal.getComponent());
+                return;
+            }
+
+            signal.putExtra(PhoneConstants.SUBSCRIPTION_KEY, mPhone.getSubId());
+            signal.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+            if (!wakeup) signal.setFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
+
+            try {
+                mPhone.getContext().sendBroadcast(signal);
+                if (DBG) {
+                    log("Sending signal " + signal.getAction() + ((signal.getComponent() != null)
+                            ? " to the carrier signal receiver: " + signal.getComponent() : ""));
+                }
+            } catch (ActivityNotFoundException e) {
+                loge("Send broadcast failed: " + e);
+            }
+        }
+    }
+
+    /**
+     * Match the intent against cached tables to find a list of registered carrier signal
+     * receivers and broadcast the intent.
+     * @param intent broadcasting intent, it could belong to wakeup, non-wakeup signal list or both
+     *
+     */
+    public void notifyCarrierSignalReceivers(Intent intent) {
+        Set<ComponentName> receiverSet;
+
+        synchronized (mCachedWakeSignalConfigs) {
+            receiverSet = mCachedWakeSignalConfigs.get(intent.getAction());
+            if (!ArrayUtils.isEmpty(receiverSet)) {
+                broadcast(intent, receiverSet, WAKE);
+            }
+        }
+
+        synchronized (mCachedNoWakeSignalConfigs) {
+            receiverSet = mCachedNoWakeSignalConfigs.get(intent.getAction());
+            if (!ArrayUtils.isEmpty(receiverSet)) {
+                broadcast(intent, receiverSet, NO_WAKE);
+            }
+        }
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void loge(String s) {
+        mErrorLocalLog.log(s);
+        Rlog.e(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void logv(String s) {
+        Rlog.v(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        pw.println("mCachedWakeSignalConfigs:");
+        ipw.increaseIndent();
+        for (Map.Entry<String, Set<ComponentName>> entry : mCachedWakeSignalConfigs.entrySet()) {
+            pw.println("signal: " + entry.getKey() + " componentName list: " + entry.getValue());
+        }
+        ipw.decreaseIndent();
+
+        pw.println("mCachedNoWakeSignalConfigs:");
+        ipw.increaseIndent();
+        for (Map.Entry<String, Set<ComponentName>> entry : mCachedNoWakeSignalConfigs.entrySet()) {
+            pw.println("signal: " + entry.getKey() + " componentName list: " + entry.getValue());
+        }
+        ipw.decreaseIndent();
+
+        pw.println("mDefaultNetworkAvail: " + mDefaultNetworkAvail);
+
+        pw.println("error log:");
+        ipw.increaseIndent();
+        mErrorLocalLog.dump(fd, pw, args);
+        ipw.decreaseIndent();
+    }
+}
diff --git a/com/android/internal/telephony/CarrierSmsUtils.java b/com/android/internal/telephony/CarrierSmsUtils.java
new file mode 100644
index 0000000..a64aea7
--- /dev/null
+++ b/com/android/internal/telephony/CarrierSmsUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+
+import java.util.List;
+
+/**
+ * This is a basic utility class for common Carrier SMS Functions
+ */
+public class CarrierSmsUtils {
+    protected static final boolean VDBG = false;
+    protected static final String TAG = CarrierSmsUtils.class.getSimpleName();
+
+    private static final String CARRIER_IMS_PACKAGE_KEY =
+            CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING;
+
+    /** Return a Carrier-overridden IMS package, if it exists and is a CarrierSmsFilter
+     *
+     * @param context calling context
+     * @param phone object from telephony
+     * @param intent that should match a CarrierSmsFilter
+     * @return the name of the IMS CarrierService package
+     */
+    @Nullable
+    public static String getCarrierImsPackageForIntent(
+            Context context, Phone phone, Intent intent) {
+
+        String carrierImsPackage = getCarrierImsPackage(context, phone);
+        if (carrierImsPackage == null) {
+            if (VDBG) Rlog.v(TAG, "No CarrierImsPackage override found");
+            return null;
+        }
+
+        PackageManager packageManager = context.getPackageManager();
+        List<ResolveInfo> receivers = packageManager.queryIntentServices(intent, 0);
+        for (ResolveInfo info : receivers) {
+            if (info.serviceInfo == null) {
+                Rlog.e(TAG, "Can't get service information from " + info);
+                continue;
+            }
+
+            if (carrierImsPackage.equals(info.serviceInfo.packageName)) {
+                return carrierImsPackage;
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    private static String getCarrierImsPackage(Context context, Phone phone) {
+        CarrierConfigManager cm = (CarrierConfigManager) context.getSystemService(
+                Context.CARRIER_CONFIG_SERVICE);
+        if (cm == null) {
+            Rlog.e(TAG, "Failed to retrieve CarrierConfigManager");
+            return null;
+        }
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            PersistableBundle config = cm.getConfigForSubId(phone.getSubId());
+            if (config == null) {
+                if (VDBG) Rlog.v(TAG, "No CarrierConfig for subId:" + phone.getSubId());
+                return null;
+            }
+            return config.getString(CARRIER_IMS_PACKAGE_KEY, null);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private CarrierSmsUtils() {}
+}
diff --git a/com/android/internal/telephony/CellBroadcastHandler.java b/com/android/internal/telephony/CellBroadcastHandler.java
new file mode 100644
index 0000000..19b7b40
--- /dev/null
+++ b/com/android/internal/telephony/CellBroadcastHandler.java
@@ -0,0 +1,132 @@
+/*
+ * 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.internal.telephony;
+
+import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.telephony.SmsCbMessage;
+import android.telephony.SubscriptionManager;
+
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+
+/**
+ * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast
+ * completes and our result receiver is called.
+ */
+public class CellBroadcastHandler extends WakeLockStateMachine {
+
+    private CellBroadcastHandler(Context context, Phone phone) {
+        this("CellBroadcastHandler", context, phone);
+    }
+
+    protected CellBroadcastHandler(String debugTag, Context context, Phone phone) {
+        super(debugTag, context, phone);
+    }
+
+    /**
+     * Create a new CellBroadcastHandler.
+     * @param context the context to use for dispatching Intents
+     * @return the new handler
+     */
+    public static CellBroadcastHandler makeCellBroadcastHandler(Context context, Phone phone) {
+        CellBroadcastHandler handler = new CellBroadcastHandler(context, phone);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}.
+     * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass.
+     *
+     * @param message the message to process
+     * @return true if an ordered broadcast was sent; false on failure
+     */
+    @Override
+    protected boolean handleSmsMessage(Message message) {
+        if (message.obj instanceof SmsCbMessage) {
+            handleBroadcastSms((SmsCbMessage) message.obj);
+            return true;
+        } else {
+            loge("handleMessage got object of type: " + message.obj.getClass().getName());
+            return false;
+        }
+    }
+
+    /**
+     * Dispatch a Cell Broadcast message to listeners.
+     * @param message the Cell Broadcast to broadcast
+     */
+    protected void handleBroadcastSms(SmsCbMessage message) {
+        String receiverPermission;
+        int appOp;
+
+        // Log Cellbroadcast msg received event
+        TelephonyMetrics metrics = TelephonyMetrics.getInstance();
+        metrics.writeNewCBSms(mPhone.getPhoneId(), message.getMessageFormat(),
+                message.getMessagePriority(), message.isCmasMessage(), message.isEtwsMessage(),
+                message.getServiceCategory());
+
+        Intent intent;
+        if (message.isEmergencyMessage()) {
+            log("Dispatching emergency SMS CB, SmsCbMessage is: " + message);
+            intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION);
+            // Explicitly send the intent to the default cell broadcast receiver.
+            intent.setPackage(mContext.getResources().getString(
+                    com.android.internal.R.string.config_defaultCellBroadcastReceiverPkg));
+            receiverPermission = Manifest.permission.RECEIVE_EMERGENCY_BROADCAST;
+            appOp = AppOpsManager.OP_RECEIVE_EMERGECY_SMS;
+        } else {
+            log("Dispatching SMS CB, SmsCbMessage is: " + message);
+            intent = new Intent(Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION);
+            // Send implicit intent since there are various 3rd party carrier apps listen to
+            // this intent.
+            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+            receiverPermission = Manifest.permission.RECEIVE_SMS;
+            appOp = AppOpsManager.OP_RECEIVE_SMS;
+        }
+
+        intent.putExtra("message", message);
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
+
+        if (Build.IS_DEBUGGABLE) {
+            // Send additional broadcast intent to the specified package. This is only for sl4a
+            // automation tests.
+            final String additionalPackage = Settings.Secure.getString(
+                    mContext.getContentResolver(), CMAS_ADDITIONAL_BROADCAST_PKG);
+            if (additionalPackage != null) {
+                Intent additionalIntent = new Intent(intent);
+                additionalIntent.setPackage(additionalPackage);
+                mContext.sendOrderedBroadcastAsUser(additionalIntent, UserHandle.ALL,
+                        receiverPermission, appOp, null, getHandler(), Activity.RESULT_OK, null,
+                        null);
+            }
+        }
+
+        mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission, appOp,
+                mReceiver, getHandler(), Activity.RESULT_OK, null, null);
+    }
+}
diff --git a/com/android/internal/telephony/CellNetworkScanResult.java b/com/android/internal/telephony/CellNetworkScanResult.java
new file mode 100644
index 0000000..5a6bd1d
--- /dev/null
+++ b/com/android/internal/telephony/CellNetworkScanResult.java
@@ -0,0 +1,126 @@
+/*
+** Copyright 2015, 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.internal.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Response for querying available cellular networks.
+ *
+ * @hide
+ */
+public class CellNetworkScanResult implements Parcelable {
+
+    /**
+     * Possible status values.
+     */
+    public static final int STATUS_SUCCESS = 1;
+    public static final int STATUS_RADIO_NOT_AVAILABLE = 2;
+    public static final int STATUS_RADIO_GENERIC_FAILURE = 3;
+    public static final int STATUS_UNKNOWN_ERROR = 4;
+
+    private final int mStatus;
+    private final List<OperatorInfo> mOperators;
+
+    /**
+     * Constructor.
+     *
+     * @hide
+     */
+    public CellNetworkScanResult(int status, List<OperatorInfo> operators) {
+        mStatus = status;
+        mOperators = operators;
+    }
+
+    /**
+     * Construct a CellNetworkScanResult from a given parcel.
+     */
+    private CellNetworkScanResult(Parcel in) {
+        mStatus = in.readInt();
+        int len = in.readInt();
+        if (len > 0) {
+            mOperators = new ArrayList();
+            for (int i = 0; i < len; ++i) {
+                mOperators.add(OperatorInfo.CREATOR.createFromParcel(in));
+            }
+        } else {
+            mOperators = null;
+        }
+    }
+
+    /**
+     * @return the status of the command.
+     */
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * @return the operators.
+     */
+    public List<OperatorInfo> getOperators() {
+        return mOperators;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mStatus);
+        if (mOperators != null && mOperators.size() > 0) {
+            out.writeInt(mOperators.size());
+            for (OperatorInfo network : mOperators) {
+                network.writeToParcel(out, flags);
+            }
+        } else {
+            out.writeInt(0);
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("CellNetworkScanResult: {");
+        sb.append(" status:").append(mStatus);
+        if (mOperators != null) {
+            for (OperatorInfo network : mOperators) {
+              sb.append(" network:").append(network);
+            }
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    public static final Parcelable.Creator<CellNetworkScanResult> CREATOR
+             = new Parcelable.Creator<CellNetworkScanResult>() {
+
+        @Override
+        public CellNetworkScanResult createFromParcel(Parcel in) {
+             return new CellNetworkScanResult(in);
+         }
+
+         public CellNetworkScanResult[] newArray(int size) {
+             return new CellNetworkScanResult[size];
+         }
+     };
+}
diff --git a/com/android/internal/telephony/ClientWakelockAccountant.java b/com/android/internal/telephony/ClientWakelockAccountant.java
new file mode 100644
index 0000000..c47faab
--- /dev/null
+++ b/com/android/internal/telephony/ClientWakelockAccountant.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.telephony.ClientRequestStats;
+import android.telephony.Rlog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import java.util.ArrayList;
+
+public class ClientWakelockAccountant {
+    public static final String LOG_TAG = "ClientWakelockAccountant: ";
+
+    @VisibleForTesting
+    public ClientRequestStats mRequestStats = new ClientRequestStats();
+    @VisibleForTesting
+    public ArrayList<RilWakelockInfo> mPendingRilWakelocks = new ArrayList<>();
+
+    @VisibleForTesting
+    public ClientWakelockAccountant(String callingPackage) {
+        mRequestStats.setCallingPackage(callingPackage);
+    }
+
+    @VisibleForTesting
+    public void startAttributingWakelock(int request,
+            int token, int concurrentRequests, long time) {
+
+        RilWakelockInfo wlInfo = new RilWakelockInfo(request, token, concurrentRequests, time);
+        synchronized (mPendingRilWakelocks) {
+            mPendingRilWakelocks.add(wlInfo);
+        }
+    }
+
+    @VisibleForTesting
+    public void stopAttributingWakelock(int request, int token, long time) {
+        RilWakelockInfo wlInfo = removePendingWakelock(request, token);
+        if (wlInfo != null) {
+            completeRequest(wlInfo, time);
+        }
+    }
+
+    @VisibleForTesting
+    public void stopAllPendingRequests(long time) {
+        synchronized (mPendingRilWakelocks) {
+            for (RilWakelockInfo wlInfo : mPendingRilWakelocks) {
+                completeRequest(wlInfo, time);
+            }
+            mPendingRilWakelocks.clear();
+        }
+    }
+
+    @VisibleForTesting
+    public void changeConcurrentRequests(int concurrentRequests, long time) {
+        synchronized (mPendingRilWakelocks) {
+            for (RilWakelockInfo wlInfo : mPendingRilWakelocks) {
+                wlInfo.updateConcurrentRequests(concurrentRequests, time);
+            }
+        }
+    }
+
+    private void completeRequest(RilWakelockInfo wlInfo, long time) {
+        wlInfo.setResponseTime(time);
+        synchronized (mRequestStats) {
+            mRequestStats.addCompletedWakelockTime(wlInfo.getWakelockTimeAttributedToClient());
+            mRequestStats.incrementCompletedRequestsCount();
+            mRequestStats.updateRequestHistograms(wlInfo.getRilRequestSent(),
+                    (int) wlInfo.getWakelockTimeAttributedToClient());
+        }
+    }
+
+    @VisibleForTesting
+    public int getPendingRequestCount() {
+        return mPendingRilWakelocks.size();
+    }
+
+    @VisibleForTesting
+    public synchronized long updatePendingRequestWakelockTime(long uptime) {
+        long totalPendingWakelockTime = 0;
+        synchronized (mPendingRilWakelocks) {
+            for (RilWakelockInfo wlInfo : mPendingRilWakelocks) {
+                wlInfo.updateTime(uptime);
+                totalPendingWakelockTime += wlInfo.getWakelockTimeAttributedToClient();
+            }
+        }
+        synchronized (mRequestStats) {
+            mRequestStats.setPendingRequestsCount(getPendingRequestCount());
+            mRequestStats.setPendingRequestsWakelockTime(totalPendingWakelockTime);
+        }
+        return totalPendingWakelockTime;
+    }
+
+    private RilWakelockInfo removePendingWakelock(int request, int token) {
+        RilWakelockInfo result = null;
+        synchronized (mPendingRilWakelocks) {
+            for (RilWakelockInfo wlInfo : mPendingRilWakelocks) {
+                if ((wlInfo.getTokenNumber() == token) &&
+                    (wlInfo.getRilRequestSent() == request)) {
+                    result = wlInfo;
+                }
+            }
+            if( result != null ) {
+                mPendingRilWakelocks.remove(result);
+            }
+        }
+        if(result == null) {
+            Rlog.w(LOG_TAG, "Looking for Request<" + request + "," + token + "> in "
+                + mPendingRilWakelocks);
+        }
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "ClientWakelockAccountant{" +
+                "mRequestStats=" + mRequestStats +
+                ", mPendingRilWakelocks=" + mPendingRilWakelocks +
+                '}';
+    }
+}
diff --git a/com/android/internal/telephony/ClientWakelockTracker.java b/com/android/internal/telephony/ClientWakelockTracker.java
new file mode 100644
index 0000000..5bec60b
--- /dev/null
+++ b/com/android/internal/telephony/ClientWakelockTracker.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.os.SystemClock;
+import android.telephony.ClientRequestStats;
+import android.telephony.Rlog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class ClientWakelockTracker {
+    public static final String LOG_TAG = "ClientWakelockTracker";
+    @VisibleForTesting
+    public HashMap<String, ClientWakelockAccountant> mClients =
+        new HashMap<String, ClientWakelockAccountant>();
+    @VisibleForTesting
+    public ArrayList<ClientWakelockAccountant> mActiveClients = new ArrayList<>();
+
+    @VisibleForTesting
+    public void startTracking(String clientId, int requestId, int token, int numRequestsInQueue) {
+        ClientWakelockAccountant client = getClientWakelockAccountant(clientId);
+        long uptime = SystemClock.uptimeMillis();
+        client.startAttributingWakelock(requestId, token, numRequestsInQueue, uptime);
+        updateConcurrentRequests(numRequestsInQueue, uptime);
+        synchronized (mActiveClients) {
+            if (!mActiveClients.contains(client)) {
+                mActiveClients.add(client);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void stopTracking(String clientId, int requestId, int token, int numRequestsInQueue) {
+        ClientWakelockAccountant client = getClientWakelockAccountant(clientId);
+        long uptime = SystemClock.uptimeMillis();
+        client.stopAttributingWakelock(requestId, token, uptime);
+        if(client.getPendingRequestCount() == 0) {
+            synchronized (mActiveClients) {
+                mActiveClients.remove(client);
+            }
+        }
+        updateConcurrentRequests(numRequestsInQueue, uptime);
+    }
+
+    @VisibleForTesting
+    public void stopTrackingAll() {
+        long uptime = SystemClock.uptimeMillis();
+        synchronized (mActiveClients) {
+            for (ClientWakelockAccountant client : mActiveClients) {
+                client.stopAllPendingRequests(uptime);
+            }
+            mActiveClients.clear();
+        }
+    }
+
+    List<ClientRequestStats> getClientRequestStats() {
+        List<ClientRequestStats> list;
+        long uptime = SystemClock.uptimeMillis();
+        synchronized (mClients) {
+            list = new ArrayList<>(mClients.size());
+            for (String key :  mClients.keySet()) {
+                ClientWakelockAccountant client = mClients.get(key);
+                client.updatePendingRequestWakelockTime(uptime);
+                list.add(new ClientRequestStats(client.mRequestStats));
+            }
+        }
+        return list;
+    }
+
+    private ClientWakelockAccountant getClientWakelockAccountant(String clientId) {
+        ClientWakelockAccountant client;
+        synchronized (mClients) {
+            if (mClients.containsKey(clientId)) {
+                client = mClients.get(clientId);
+            } else {
+                client = new ClientWakelockAccountant(clientId);
+                mClients.put(clientId, client);
+            }
+        }
+        return client;
+    }
+
+    private void updateConcurrentRequests(int numRequestsInQueue, long time) {
+        if(numRequestsInQueue != 0) {
+            synchronized (mActiveClients) {
+                for (ClientWakelockAccountant cI : mActiveClients) {
+                    cI.changeConcurrentRequests(numRequestsInQueue, time);
+                }
+            }
+        }
+    }
+
+    public boolean isClientActive(String clientId) {
+        ClientWakelockAccountant client = getClientWakelockAccountant(clientId);
+        synchronized (mActiveClients) {
+            if (mActiveClients.contains(client)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    void dumpClientRequestTracker() {
+        Rlog.d(RIL.RILJ_LOG_TAG, "-------mClients---------------");
+        synchronized (mClients) {
+            for (String key : mClients.keySet()) {
+                Rlog.d(RIL.RILJ_LOG_TAG, "Client : " + key);
+                Rlog.d(RIL.RILJ_LOG_TAG, mClients.get(key).toString());
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/CommandException.java b/com/android/internal/telephony/CommandException.java
new file mode 100644
index 0000000..6cba8f2
--- /dev/null
+++ b/com/android/internal/telephony/CommandException.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony;
+
+import com.android.internal.telephony.RILConstants;
+
+import android.telephony.Rlog;
+
+/**
+ * {@hide}
+ */
+public class CommandException extends RuntimeException {
+    private Error mError;
+
+    public enum Error {
+        INVALID_RESPONSE,
+        RADIO_NOT_AVAILABLE,
+        GENERIC_FAILURE,
+        PASSWORD_INCORRECT,
+        SIM_PIN2,
+        SIM_PUK2,
+        REQUEST_NOT_SUPPORTED,
+        OP_NOT_ALLOWED_DURING_VOICE_CALL,
+        OP_NOT_ALLOWED_BEFORE_REG_NW,
+        SMS_FAIL_RETRY,
+        SIM_ABSENT,
+        SUBSCRIPTION_NOT_AVAILABLE,
+        MODE_NOT_SUPPORTED,
+        FDN_CHECK_FAILURE,
+        ILLEGAL_SIM_OR_ME,
+        MISSING_RESOURCE,
+        NO_SUCH_ELEMENT,
+        SUBSCRIPTION_NOT_SUPPORTED,
+        DIAL_MODIFIED_TO_USSD,
+        DIAL_MODIFIED_TO_SS,
+        DIAL_MODIFIED_TO_DIAL,
+        USSD_MODIFIED_TO_DIAL,
+        USSD_MODIFIED_TO_SS,
+        USSD_MODIFIED_TO_USSD,
+        SS_MODIFIED_TO_DIAL,
+        SS_MODIFIED_TO_USSD,
+        SS_MODIFIED_TO_SS,
+        SIM_ALREADY_POWERED_OFF,
+        SIM_ALREADY_POWERED_ON,
+        SIM_DATA_NOT_AVAILABLE,
+        SIM_SAP_CONNECT_FAILURE,
+        SIM_SAP_MSG_SIZE_TOO_LARGE,
+        SIM_SAP_MSG_SIZE_TOO_SMALL,
+        SIM_SAP_CONNECT_OK_CALL_ONGOING,
+        LCE_NOT_SUPPORTED,
+        NO_MEMORY,
+        INTERNAL_ERR,
+        SYSTEM_ERR,
+        MODEM_ERR,
+        INVALID_STATE,
+        NO_RESOURCES,
+        SIM_ERR,
+        INVALID_ARGUMENTS,
+        INVALID_SIM_STATE,
+        INVALID_MODEM_STATE,
+        INVALID_CALL_ID,
+        NO_SMS_TO_ACK,
+        NETWORK_ERR,
+        REQUEST_RATE_LIMITED,
+        SIM_BUSY,
+        SIM_FULL,
+        NETWORK_REJECT,
+        OPERATION_NOT_ALLOWED,
+        EMPTY_RECORD,
+        INVALID_SMS_FORMAT,
+        ENCODING_ERR,
+        INVALID_SMSC_ADDRESS,
+        NO_SUCH_ENTRY,
+        NETWORK_NOT_READY,
+        NOT_PROVISIONED,
+        NO_SUBSCRIPTION,
+        NO_NETWORK_FOUND,
+        DEVICE_IN_USE,
+        ABORTED,
+        OEM_ERROR_1,
+        OEM_ERROR_2,
+        OEM_ERROR_3,
+        OEM_ERROR_4,
+        OEM_ERROR_5,
+        OEM_ERROR_6,
+        OEM_ERROR_7,
+        OEM_ERROR_8,
+        OEM_ERROR_9,
+        OEM_ERROR_10,
+        OEM_ERROR_11,
+        OEM_ERROR_12,
+        OEM_ERROR_13,
+        OEM_ERROR_14,
+        OEM_ERROR_15,
+        OEM_ERROR_16,
+        OEM_ERROR_17,
+        OEM_ERROR_18,
+        OEM_ERROR_19,
+        OEM_ERROR_20,
+        OEM_ERROR_21,
+        OEM_ERROR_22,
+        OEM_ERROR_23,
+        OEM_ERROR_24,
+        OEM_ERROR_25,
+    }
+
+    public CommandException(Error e) {
+        super(e.toString());
+        mError = e;
+    }
+
+    public CommandException(Error e, String errString) {
+        super(errString);
+        mError = e;
+    }
+
+    public static CommandException
+    fromRilErrno(int ril_errno) {
+        switch(ril_errno) {
+            case RILConstants.SUCCESS:                       return null;
+            case RILConstants.RIL_ERRNO_INVALID_RESPONSE:
+                return new CommandException(Error.INVALID_RESPONSE);
+            case RILConstants.RADIO_NOT_AVAILABLE:
+                return new CommandException(Error.RADIO_NOT_AVAILABLE);
+            case RILConstants.GENERIC_FAILURE:
+                return new CommandException(Error.GENERIC_FAILURE);
+            case RILConstants.PASSWORD_INCORRECT:
+                return new CommandException(Error.PASSWORD_INCORRECT);
+            case RILConstants.SIM_PIN2:
+                return new CommandException(Error.SIM_PIN2);
+            case RILConstants.SIM_PUK2:
+                return new CommandException(Error.SIM_PUK2);
+            case RILConstants.REQUEST_NOT_SUPPORTED:
+                return new CommandException(Error.REQUEST_NOT_SUPPORTED);
+            case RILConstants.OP_NOT_ALLOWED_DURING_VOICE_CALL:
+                return new CommandException(Error.OP_NOT_ALLOWED_DURING_VOICE_CALL);
+            case RILConstants.OP_NOT_ALLOWED_BEFORE_REG_NW:
+                return new CommandException(Error.OP_NOT_ALLOWED_BEFORE_REG_NW);
+            case RILConstants.SMS_SEND_FAIL_RETRY:
+                return new CommandException(Error.SMS_FAIL_RETRY);
+            case RILConstants.SIM_ABSENT:
+                return new CommandException(Error.SIM_ABSENT);
+            case RILConstants.SUBSCRIPTION_NOT_AVAILABLE:
+                return new CommandException(Error.SUBSCRIPTION_NOT_AVAILABLE);
+            case RILConstants.MODE_NOT_SUPPORTED:
+                return new CommandException(Error.MODE_NOT_SUPPORTED);
+            case RILConstants.FDN_CHECK_FAILURE:
+                return new CommandException(Error.FDN_CHECK_FAILURE);
+            case RILConstants.ILLEGAL_SIM_OR_ME:
+                return new CommandException(Error.ILLEGAL_SIM_OR_ME);
+            case RILConstants.MISSING_RESOURCE:
+                return new CommandException(Error.MISSING_RESOURCE);
+            case RILConstants.NO_SUCH_ELEMENT:
+                return new CommandException(Error.NO_SUCH_ELEMENT);
+            case RILConstants.SUBSCRIPTION_NOT_SUPPORTED:
+                return new CommandException(Error.SUBSCRIPTION_NOT_SUPPORTED);
+            case RILConstants.DIAL_MODIFIED_TO_USSD:
+                return new CommandException(Error.DIAL_MODIFIED_TO_USSD);
+            case RILConstants.DIAL_MODIFIED_TO_SS:
+                return new CommandException(Error.DIAL_MODIFIED_TO_SS);
+            case RILConstants.DIAL_MODIFIED_TO_DIAL:
+                return new CommandException(Error.DIAL_MODIFIED_TO_DIAL);
+            case RILConstants.USSD_MODIFIED_TO_DIAL:
+                return new CommandException(Error.USSD_MODIFIED_TO_DIAL);
+            case RILConstants.USSD_MODIFIED_TO_SS:
+                return new CommandException(Error.USSD_MODIFIED_TO_SS);
+            case RILConstants.USSD_MODIFIED_TO_USSD:
+                return new CommandException(Error.USSD_MODIFIED_TO_USSD);
+            case RILConstants.SS_MODIFIED_TO_DIAL:
+                return new CommandException(Error.SS_MODIFIED_TO_DIAL);
+            case RILConstants.SS_MODIFIED_TO_USSD:
+                return new CommandException(Error.SS_MODIFIED_TO_USSD);
+            case RILConstants.SS_MODIFIED_TO_SS:
+                return new CommandException(Error.SS_MODIFIED_TO_SS);
+            case RILConstants.SIM_ALREADY_POWERED_OFF:
+                return new CommandException(Error.SIM_ALREADY_POWERED_OFF);
+            case RILConstants.SIM_ALREADY_POWERED_ON:
+                return new CommandException(Error.SIM_ALREADY_POWERED_ON);
+            case RILConstants.SIM_DATA_NOT_AVAILABLE:
+                return new CommandException(Error.SIM_DATA_NOT_AVAILABLE);
+            case RILConstants.SIM_SAP_CONNECT_FAILURE:
+                return new CommandException(Error.SIM_SAP_CONNECT_FAILURE);
+            case RILConstants.SIM_SAP_MSG_SIZE_TOO_LARGE:
+                return new CommandException(Error.SIM_SAP_MSG_SIZE_TOO_LARGE);
+            case RILConstants.SIM_SAP_MSG_SIZE_TOO_SMALL:
+                return new CommandException(Error.SIM_SAP_MSG_SIZE_TOO_SMALL);
+            case RILConstants.SIM_SAP_CONNECT_OK_CALL_ONGOING:
+                return new CommandException(Error.SIM_SAP_CONNECT_OK_CALL_ONGOING);
+            case RILConstants.LCE_NOT_SUPPORTED:
+                return new CommandException(Error.LCE_NOT_SUPPORTED);
+            case RILConstants.NO_MEMORY:
+                return new CommandException(Error.NO_MEMORY);
+            case RILConstants.INTERNAL_ERR:
+                return new CommandException(Error.INTERNAL_ERR);
+            case RILConstants.SYSTEM_ERR:
+                return new CommandException(Error.SYSTEM_ERR);
+            case RILConstants.MODEM_ERR:
+                return new CommandException(Error.MODEM_ERR);
+            case RILConstants.INVALID_STATE:
+                return new CommandException(Error.INVALID_STATE);
+            case RILConstants.NO_RESOURCES:
+                return new CommandException(Error.NO_RESOURCES);
+            case RILConstants.SIM_ERR:
+                return new CommandException(Error.SIM_ERR);
+            case RILConstants.INVALID_ARGUMENTS:
+                return new CommandException(Error.INVALID_ARGUMENTS);
+            case RILConstants.INVALID_SIM_STATE:
+                return new CommandException(Error.INVALID_SIM_STATE);
+            case RILConstants.INVALID_MODEM_STATE:
+                return new CommandException(Error.INVALID_MODEM_STATE);
+            case RILConstants.INVALID_CALL_ID:
+                return new CommandException(Error.INVALID_CALL_ID);
+            case RILConstants.NO_SMS_TO_ACK:
+                return new CommandException(Error.NO_SMS_TO_ACK);
+            case RILConstants.NETWORK_ERR:
+                return new CommandException(Error.NETWORK_ERR);
+            case RILConstants.REQUEST_RATE_LIMITED:
+                return new CommandException(Error.REQUEST_RATE_LIMITED);
+            case RILConstants.SIM_BUSY:
+                return new CommandException(Error.SIM_BUSY);
+            case RILConstants.SIM_FULL:
+                return new CommandException(Error.SIM_FULL);
+            case RILConstants.NETWORK_REJECT:
+                return new CommandException(Error.NETWORK_REJECT);
+            case RILConstants.OPERATION_NOT_ALLOWED:
+                return new CommandException(Error.OPERATION_NOT_ALLOWED);
+            case RILConstants.EMPTY_RECORD:
+                return new CommandException(Error.EMPTY_RECORD);
+            case RILConstants.INVALID_SMS_FORMAT:
+                return new CommandException(Error.INVALID_SMS_FORMAT);
+            case RILConstants.ENCODING_ERR:
+                return new CommandException(Error.ENCODING_ERR);
+            case RILConstants.INVALID_SMSC_ADDRESS:
+                return new CommandException(Error.INVALID_SMSC_ADDRESS);
+            case RILConstants.NO_SUCH_ENTRY:
+                return new CommandException(Error.NO_SUCH_ENTRY);
+            case RILConstants.NETWORK_NOT_READY:
+                return new CommandException(Error.NETWORK_NOT_READY);
+            case RILConstants.NOT_PROVISIONED:
+                return new CommandException(Error.NOT_PROVISIONED);
+            case RILConstants.NO_SUBSCRIPTION:
+                return new CommandException(Error.NO_SUBSCRIPTION);
+            case RILConstants.NO_NETWORK_FOUND:
+                return new CommandException(Error.NO_NETWORK_FOUND);
+            case RILConstants.DEVICE_IN_USE:
+                return new CommandException(Error.DEVICE_IN_USE);
+            case RILConstants.ABORTED:
+                return new CommandException(Error.ABORTED);
+            case RILConstants.OEM_ERROR_1:
+                return new CommandException(Error.OEM_ERROR_1);
+            case RILConstants.OEM_ERROR_2:
+                return new CommandException(Error.OEM_ERROR_2);
+            case RILConstants.OEM_ERROR_3:
+                return new CommandException(Error.OEM_ERROR_3);
+            case RILConstants.OEM_ERROR_4:
+                return new CommandException(Error.OEM_ERROR_4);
+            case RILConstants.OEM_ERROR_5:
+                return new CommandException(Error.OEM_ERROR_5);
+            case RILConstants.OEM_ERROR_6:
+                return new CommandException(Error.OEM_ERROR_6);
+            case RILConstants.OEM_ERROR_7:
+                return new CommandException(Error.OEM_ERROR_7);
+            case RILConstants.OEM_ERROR_8:
+                return new CommandException(Error.OEM_ERROR_8);
+            case RILConstants.OEM_ERROR_9:
+                return new CommandException(Error.OEM_ERROR_9);
+            case RILConstants.OEM_ERROR_10:
+                return new CommandException(Error.OEM_ERROR_10);
+            case RILConstants.OEM_ERROR_11:
+                return new CommandException(Error.OEM_ERROR_11);
+            case RILConstants.OEM_ERROR_12:
+                return new CommandException(Error.OEM_ERROR_12);
+            case RILConstants.OEM_ERROR_13:
+                return new CommandException(Error.OEM_ERROR_13);
+            case RILConstants.OEM_ERROR_14:
+                return new CommandException(Error.OEM_ERROR_14);
+            case RILConstants.OEM_ERROR_15:
+                return new CommandException(Error.OEM_ERROR_15);
+            case RILConstants.OEM_ERROR_16:
+                return new CommandException(Error.OEM_ERROR_16);
+            case RILConstants.OEM_ERROR_17:
+                return new CommandException(Error.OEM_ERROR_17);
+            case RILConstants.OEM_ERROR_18:
+                return new CommandException(Error.OEM_ERROR_18);
+            case RILConstants.OEM_ERROR_19:
+                return new CommandException(Error.OEM_ERROR_19);
+            case RILConstants.OEM_ERROR_20:
+                return new CommandException(Error.OEM_ERROR_20);
+            case RILConstants.OEM_ERROR_21:
+                return new CommandException(Error.OEM_ERROR_21);
+            case RILConstants.OEM_ERROR_22:
+                return new CommandException(Error.OEM_ERROR_22);
+            case RILConstants.OEM_ERROR_23:
+                return new CommandException(Error.OEM_ERROR_23);
+            case RILConstants.OEM_ERROR_24:
+                return new CommandException(Error.OEM_ERROR_24);
+            case RILConstants.OEM_ERROR_25:
+                return new CommandException(Error.OEM_ERROR_25);
+
+            default:
+                Rlog.e("GSM", "Unrecognized RIL errno " + ril_errno);
+                return new CommandException(Error.INVALID_RESPONSE);
+        }
+    }
+
+    public Error getCommandError() {
+        return mError;
+    }
+
+
+
+}
diff --git a/com/android/internal/telephony/CommandsInterface.java b/com/android/internal/telephony/CommandsInterface.java
new file mode 100644
index 0000000..f339693
--- /dev/null
+++ b/com/android/internal/telephony/CommandsInterface.java
@@ -0,0 +1,2158 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.WorkSource;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.ClientRequestStats;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.dataconnection.DataProfile;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+import com.android.internal.telephony.uicc.IccCardStatus;
+
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public interface CommandsInterface {
+    enum RadioState {
+        RADIO_OFF,         /* Radio explicitly powered off (eg CFUN=0) */
+        RADIO_UNAVAILABLE, /* Radio unavailable (eg, resetting or not booted) */
+        RADIO_ON;          /* Radio is on */
+
+        public boolean isOn() /* and available...*/ {
+            return this == RADIO_ON;
+        }
+
+        public boolean isAvailable() {
+            return this != RADIO_UNAVAILABLE;
+        }
+    }
+
+    //***** Constants
+
+    // Used as parameter to dial() and setCLIR() below
+    static final int CLIR_DEFAULT = 0;      // "use subscription default value"
+    static final int CLIR_INVOCATION = 1;   // (restrict CLI presentation)
+    static final int CLIR_SUPPRESSION = 2;  // (allow CLI presentation)
+
+
+    // Used as parameters for call forward methods below
+    static final int CF_ACTION_DISABLE          = 0;
+    static final int CF_ACTION_ENABLE           = 1;
+//  static final int CF_ACTION_UNUSED           = 2;
+    static final int CF_ACTION_REGISTRATION     = 3;
+    static final int CF_ACTION_ERASURE          = 4;
+
+    static final int CF_REASON_UNCONDITIONAL    = 0;
+    static final int CF_REASON_BUSY             = 1;
+    static final int CF_REASON_NO_REPLY         = 2;
+    static final int CF_REASON_NOT_REACHABLE    = 3;
+    static final int CF_REASON_ALL              = 4;
+    static final int CF_REASON_ALL_CONDITIONAL  = 5;
+
+    // Used for call barring methods below
+    static final String CB_FACILITY_BAOC         = "AO";
+    static final String CB_FACILITY_BAOIC        = "OI";
+    static final String CB_FACILITY_BAOICxH      = "OX";
+    static final String CB_FACILITY_BAIC         = "AI";
+    static final String CB_FACILITY_BAICr        = "IR";
+    static final String CB_FACILITY_BA_ALL       = "AB";
+    static final String CB_FACILITY_BA_MO        = "AG";
+    static final String CB_FACILITY_BA_MT        = "AC";
+    static final String CB_FACILITY_BA_SIM       = "SC";
+    static final String CB_FACILITY_BA_FD        = "FD";
+
+
+    // Used for various supp services apis
+    // See 27.007 +CCFC or +CLCK
+    static final int SERVICE_CLASS_NONE     = 0; // no user input
+    static final int SERVICE_CLASS_VOICE    = (1 << 0);
+    static final int SERVICE_CLASS_DATA     = (1 << 1); //synonym for 16+32+64+128
+    static final int SERVICE_CLASS_FAX      = (1 << 2);
+    static final int SERVICE_CLASS_SMS      = (1 << 3);
+    static final int SERVICE_CLASS_DATA_SYNC = (1 << 4);
+    static final int SERVICE_CLASS_DATA_ASYNC = (1 << 5);
+    static final int SERVICE_CLASS_PACKET   = (1 << 6);
+    static final int SERVICE_CLASS_PAD      = (1 << 7);
+    static final int SERVICE_CLASS_MAX      = (1 << 7); // Max SERVICE_CLASS value
+
+    // Numeric representation of string values returned
+    // by messages sent to setOnUSSD handler
+    static final int USSD_MODE_NOTIFY        = 0;
+    static final int USSD_MODE_REQUEST       = 1;
+    static final int USSD_MODE_NW_RELEASE    = 2;
+    static final int USSD_MODE_LOCAL_CLIENT  = 3;
+    static final int USSD_MODE_NOT_SUPPORTED = 4;
+    static final int USSD_MODE_NW_TIMEOUT    = 5;
+
+    // GSM SMS fail cause for acknowledgeLastIncomingSMS. From TS 23.040, 9.2.3.22.
+    static final int GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED    = 0xD3;
+    static final int GSM_SMS_FAIL_CAUSE_USIM_APP_TOOLKIT_BUSY       = 0xD4;
+    static final int GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR    = 0xD5;
+    static final int GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR           = 0xFF;
+
+    // CDMA SMS fail cause for acknowledgeLastIncomingCdmaSms.  From TS N.S0005, 6.5.2.125.
+    static final int CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID     = 4;
+    static final int CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE          = 35;
+    static final int CDMA_SMS_FAIL_CAUSE_OTHER_TERMINAL_PROBLEM     = 39;
+    static final int CDMA_SMS_FAIL_CAUSE_ENCODING_PROBLEM           = 96;
+
+    //***** Methods
+    RadioState getRadioState();
+
+    /**
+     * response.obj.result is an int[2]
+     *
+     * response.obj.result[0] is IMS registration state
+     *                        0 - Not registered
+     *                        1 - Registered
+     * response.obj.result[1] is of type RILConstants.GSM_PHONE or
+     *                                    RILConstants.CDMA_PHONE
+     */
+    void getImsRegistrationState(Message result);
+
+    /**
+     * Fires on any RadioState transition
+     * Always fires immediately as well
+     *
+     * do not attempt to calculate transitions by storing getRadioState() values
+     * on previous invocations of this notification. Instead, use the other
+     * registration methods
+     */
+    void registerForRadioStateChanged(Handler h, int what, Object obj);
+    void unregisterForRadioStateChanged(Handler h);
+
+    void registerForVoiceRadioTechChanged(Handler h, int what, Object obj);
+    void unregisterForVoiceRadioTechChanged(Handler h);
+    void registerForImsNetworkStateChanged(Handler h, int what, Object obj);
+    void unregisterForImsNetworkStateChanged(Handler h);
+
+    /**
+     * Fires on any transition into RadioState.isOn()
+     * Fires immediately if currently in that state
+     * In general, actions should be idempotent. State may change
+     * before event is received.
+     */
+    void registerForOn(Handler h, int what, Object obj);
+    void unregisterForOn(Handler h);
+
+    /**
+     * Fires on any transition out of RadioState.isAvailable()
+     * Fires immediately if currently in that state
+     * In general, actions should be idempotent. State may change
+     * before event is received.
+     */
+    void registerForAvailable(Handler h, int what, Object obj);
+    void unregisterForAvailable(Handler h);
+
+    /**
+     * Fires on any transition into !RadioState.isAvailable()
+     * Fires immediately if currently in that state
+     * In general, actions should be idempotent. State may change
+     * before event is received.
+     */
+    void registerForNotAvailable(Handler h, int what, Object obj);
+    void unregisterForNotAvailable(Handler h);
+
+    /**
+     * Fires on any transition into RADIO_OFF or !RadioState.isAvailable()
+     * Fires immediately if currently in that state
+     * In general, actions should be idempotent. State may change
+     * before event is received.
+     */
+    void registerForOffOrNotAvailable(Handler h, int what, Object obj);
+    void unregisterForOffOrNotAvailable(Handler h);
+
+    /**
+     * Fires on any change in ICC status
+     */
+    void registerForIccStatusChanged(Handler h, int what, Object obj);
+    void unregisterForIccStatusChanged(Handler h);
+
+    void registerForCallStateChanged(Handler h, int what, Object obj);
+    void unregisterForCallStateChanged(Handler h);
+    /** Register for network state changed event */
+    void registerForNetworkStateChanged(Handler h, int what, Object obj);
+    /** Unregister from network state changed event */
+    void unregisterForNetworkStateChanged(Handler h);
+    /** Register for data call list changed event */
+    void registerForDataCallListChanged(Handler h, int what, Object obj);
+    /** Unregister from data call list changed event */
+    void unregisterForDataCallListChanged(Handler h);
+
+    /** InCall voice privacy notifications */
+    void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj);
+    void unregisterForInCallVoicePrivacyOn(Handler h);
+    void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj);
+    void unregisterForInCallVoicePrivacyOff(Handler h);
+
+    /** Single Radio Voice Call State progress notifications */
+    void registerForSrvccStateChanged(Handler h, int what, Object obj);
+    void unregisterForSrvccStateChanged(Handler h);
+
+    /**
+     * Handlers for subscription status change indications.
+     *
+     * @param h Handler for subscription status change messages.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForSubscriptionStatusChanged(Handler h, int what, Object obj);
+    void unregisterForSubscriptionStatusChanged(Handler h);
+
+    /**
+     * fires on any change in hardware configuration.
+     */
+    void registerForHardwareConfigChanged(Handler h, int what, Object obj);
+    void unregisterForHardwareConfigChanged(Handler h);
+
+    /**
+     * unlike the register* methods, there's only one new 3GPP format SMS handler.
+     * if you need to unregister, you should also tell the radio to stop
+     * sending SMS's to you (via AT+CNMI)
+     *
+     * AsyncResult.result is a String containing the SMS PDU
+     */
+    void setOnNewGsmSms(Handler h, int what, Object obj);
+    void unSetOnNewGsmSms(Handler h);
+
+    /**
+     * unlike the register* methods, there's only one new 3GPP2 format SMS handler.
+     * if you need to unregister, you should also tell the radio to stop
+     * sending SMS's to you (via AT+CNMI)
+     *
+     * AsyncResult.result is a String containing the SMS PDU
+     */
+    void setOnNewCdmaSms(Handler h, int what, Object obj);
+    void unSetOnNewCdmaSms(Handler h);
+
+    /**
+     * Set the handler for SMS Cell Broadcast messages.
+     *
+     * AsyncResult.result is a byte array containing the SMS-CB PDU
+     */
+    void setOnNewGsmBroadcastSms(Handler h, int what, Object obj);
+    void unSetOnNewGsmBroadcastSms(Handler h);
+
+    /**
+     * Register for NEW_SMS_ON_SIM unsolicited message
+     *
+     * AsyncResult.result is an int array containing the index of new SMS
+     */
+    void setOnSmsOnSim(Handler h, int what, Object obj);
+    void unSetOnSmsOnSim(Handler h);
+
+    /**
+     * Register for NEW_SMS_STATUS_REPORT unsolicited message
+     *
+     * AsyncResult.result is a String containing the status report PDU
+     */
+    void setOnSmsStatus(Handler h, int what, Object obj);
+    void unSetOnSmsStatus(Handler h);
+
+    /**
+     * unlike the register* methods, there's only one NITZ time handler
+     *
+     * AsyncResult.result is an Object[]
+     * ((Object[])AsyncResult.result)[0] is a String containing the NITZ time string
+     * ((Object[])AsyncResult.result)[1] is a Long containing the milliseconds since boot as
+     *                                   returned by elapsedRealtime() when this NITZ time
+     *                                   was posted.
+     *
+     * Please note that the delivery of this message may be delayed several
+     * seconds on system startup
+     */
+    void setOnNITZTime(Handler h, int what, Object obj);
+    void unSetOnNITZTime(Handler h);
+
+    /**
+     * unlike the register* methods, there's only one USSD notify handler
+     *
+     * Represents the arrival of a USSD "notify" message, which may
+     * or may not have been triggered by a previous USSD send
+     *
+     * AsyncResult.result is a String[]
+     * ((String[])(AsyncResult.result))[0] contains status code
+     *      "0"   USSD-Notify -- text in ((const char **)data)[1]
+     *      "1"   USSD-Request -- text in ((const char **)data)[1]
+     *      "2"   Session terminated by network
+     *      "3"   other local client (eg, SIM Toolkit) has responded
+     *      "4"   Operation not supported
+     *      "5"   Network timeout
+     *
+     * ((String[])(AsyncResult.result))[1] contains the USSD message
+     * The numeric representations of these are in USSD_MODE_*
+     */
+
+    void setOnUSSD(Handler h, int what, Object obj);
+    void unSetOnUSSD(Handler h);
+
+    /**
+     * unlike the register* methods, there's only one signal strength handler
+     * AsyncResult.result is an int[2]
+     * response.obj.result[0] is received signal strength (0-31, 99)
+     * response.obj.result[1] is  bit error rate (0-7, 99)
+     * as defined in TS 27.007 8.5
+     */
+
+    void setOnSignalStrengthUpdate(Handler h, int what, Object obj);
+    void unSetOnSignalStrengthUpdate(Handler h);
+
+    /**
+     * Sets the handler for SIM/RUIM SMS storage full unsolicited message.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnIccSmsFull(Handler h, int what, Object obj);
+    void unSetOnIccSmsFull(Handler h);
+
+    /**
+     * Sets the handler for SIM Refresh notifications.
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForIccRefresh(Handler h, int what, Object obj);
+    void unregisterForIccRefresh(Handler h);
+
+    void setOnIccRefresh(Handler h, int what, Object obj);
+    void unsetOnIccRefresh(Handler h);
+
+    /**
+     * Sets the handler for RING notifications.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnCallRing(Handler h, int what, Object obj);
+    void unSetOnCallRing(Handler h);
+
+    /**
+     * Sets the handler for RESTRICTED_STATE changed notification,
+     * eg, for Domain Specific Access Control
+     * unlike the register* methods, there's only one signal strength handler
+     *
+     * AsyncResult.result is an int[1]
+     * response.obj.result[0] is a bitmask of RIL_RESTRICTED_STATE_* values
+     */
+
+    void setOnRestrictedStateChanged(Handler h, int what, Object obj);
+    void unSetOnRestrictedStateChanged(Handler h);
+
+    /**
+     * Sets the handler for Supplementary Service Notifications.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnSuppServiceNotification(Handler h, int what, Object obj);
+    void unSetOnSuppServiceNotification(Handler h);
+
+    /**
+     * Sets the handler for Session End Notifications for CAT.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnCatSessionEnd(Handler h, int what, Object obj);
+    void unSetOnCatSessionEnd(Handler h);
+
+    /**
+     * Sets the handler for Proactive Commands for CAT.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnCatProactiveCmd(Handler h, int what, Object obj);
+    void unSetOnCatProactiveCmd(Handler h);
+
+    /**
+     * Sets the handler for Event Notifications for CAT.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnCatEvent(Handler h, int what, Object obj);
+    void unSetOnCatEvent(Handler h);
+
+    /**
+     * Sets the handler for Call Set Up Notifications for CAT.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnCatCallSetUp(Handler h, int what, Object obj);
+    void unSetOnCatCallSetUp(Handler h);
+
+    /**
+     * Enables/disbables supplementary service related notifications from
+     * the network.
+     *
+     * @param enable true to enable notifications, false to disable.
+     * @param result Message to be posted when command completes.
+     */
+    void setSuppServiceNotifications(boolean enable, Message result);
+    //void unSetSuppServiceNotifications(Handler h);
+
+    /**
+     * Sets the handler for Alpha Notification during STK Call Control.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnCatCcAlphaNotify(Handler h, int what, Object obj);
+    void unSetOnCatCcAlphaNotify(Handler h);
+
+    /**
+     * Sets the handler for notifying Suplementary Services (SS)
+     * Data during STK Call Control.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void setOnSs(Handler h, int what, Object obj);
+    void unSetOnSs(Handler h);
+
+    /**
+     * Sets the handler for Event Notifications for CDMA Display Info.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForDisplayInfo(Handler h, int what, Object obj);
+    void unregisterForDisplayInfo(Handler h);
+
+    /**
+     * Sets the handler for Event Notifications for CallWaiting Info.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForCallWaitingInfo(Handler h, int what, Object obj);
+    void unregisterForCallWaitingInfo(Handler h);
+
+    /**
+     * Sets the handler for Event Notifications for Signal Info.
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForSignalInfo(Handler h, int what, Object obj);
+    void unregisterForSignalInfo(Handler h);
+
+    /**
+     * Registers the handler for CDMA number information record
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForNumberInfo(Handler h, int what, Object obj);
+    void unregisterForNumberInfo(Handler h);
+
+    /**
+     * Registers the handler for CDMA redirected number Information record
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForRedirectedNumberInfo(Handler h, int what, Object obj);
+    void unregisterForRedirectedNumberInfo(Handler h);
+
+    /**
+     * Registers the handler for CDMA line control information record
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForLineControlInfo(Handler h, int what, Object obj);
+    void unregisterForLineControlInfo(Handler h);
+
+    /**
+     * Registers the handler for CDMA T53 CLIR information record
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerFoT53ClirlInfo(Handler h, int what, Object obj);
+    void unregisterForT53ClirInfo(Handler h);
+
+    /**
+     * Registers the handler for CDMA T53 audio control information record
+     * Unlike the register* methods, there's only one notification handler
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForT53AudioControlInfo(Handler h, int what, Object obj);
+    void unregisterForT53AudioControlInfo(Handler h);
+
+    /**
+     * Fires on if Modem enters Emergency Callback mode
+     */
+    void setEmergencyCallbackMode(Handler h, int what, Object obj);
+
+     /**
+      * Fires on any CDMA OTA provision status change
+      */
+     void registerForCdmaOtaProvision(Handler h,int what, Object obj);
+     void unregisterForCdmaOtaProvision(Handler h);
+
+     /**
+      * Registers the handler when out-band ringback tone is needed.<p>
+      *
+      *  Messages received from this:
+      *  Message.obj will be an AsyncResult
+      *  AsyncResult.userObj = obj
+      *  AsyncResult.result = boolean. <p>
+      */
+     void registerForRingbackTone(Handler h, int what, Object obj);
+     void unregisterForRingbackTone(Handler h);
+
+     /**
+      * Registers the handler when mute/unmute need to be resent to get
+      * uplink audio during a call.<p>
+      *
+      * @param h Handler for notification message.
+      * @param what User-defined message code.
+      * @param obj User object.
+      *
+      */
+     void registerForResendIncallMute(Handler h, int what, Object obj);
+     void unregisterForResendIncallMute(Handler h);
+
+     /**
+      * Registers the handler for when Cdma subscription changed events
+      *
+      * @param h Handler for notification message.
+      * @param what User-defined message code.
+      * @param obj User object.
+      *
+      */
+     void registerForCdmaSubscriptionChanged(Handler h, int what, Object obj);
+     void unregisterForCdmaSubscriptionChanged(Handler h);
+
+     /**
+      * Registers the handler for when Cdma prl changed events
+      *
+      * @param h Handler for notification message.
+      * @param what User-defined message code.
+      * @param obj User object.
+      *
+      */
+     void registerForCdmaPrlChanged(Handler h, int what, Object obj);
+     void unregisterForCdmaPrlChanged(Handler h);
+
+     /**
+      * Registers the handler for when Cdma prl changed events
+      *
+      * @param h Handler for notification message.
+      * @param what User-defined message code.
+      * @param obj User object.
+      *
+      */
+     void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj);
+     void unregisterForExitEmergencyCallbackMode(Handler h);
+
+     /**
+      * Registers the handler for RIL_UNSOL_RIL_CONNECT events.
+      *
+      * When ril connects or disconnects a message is sent to the registrant
+      * which contains an AsyncResult, ar, in msg.obj. The ar.result is an
+      * Integer which is the version of the ril or -1 if the ril disconnected.
+      *
+      * @param h Handler for notification message.
+      * @param what User-defined message code.
+      * @param obj User object.
+      */
+     void registerForRilConnected(Handler h, int what, Object obj);
+     void unregisterForRilConnected(Handler h);
+
+    /**
+     * Supply the ICC PIN to the ICC card
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC will be PUK locked.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPin(String pin, Message result);
+
+    /**
+     * Supply the PIN for the app with this AID on the ICC card
+     *
+     *  AID (Application ID), See ETSI 102.221 8.1 and 101.220 4
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC will be PUK locked.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPinForApp(String pin, String aid, Message result);
+
+    /**
+     * Supply the ICC PUK and newPin to the ICC card
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC is permanently disabled.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPuk(String puk, String newPin, Message result);
+
+    /**
+     * Supply the PUK, new pin for the app with this AID on the ICC card
+     *
+     *  AID (Application ID), See ETSI 102.221 8.1 and 101.220 4
+     *
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC is permanently disabled.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPukForApp(String puk, String newPin, String aid, Message result);
+
+    /**
+     * Supply the ICC PIN2 to the ICC card
+     * Only called following operation where ICC_PIN2 was
+     * returned as a a failure from a previous operation
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC will be PUK locked.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPin2(String pin2, Message result);
+
+    /**
+     * Supply the PIN2 for the app with this AID on the ICC card
+     * Only called following operation where ICC_PIN2 was
+     * returned as a a failure from a previous operation
+     *
+     *  AID (Application ID), See ETSI 102.221 8.1 and 101.220 4
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC will be PUK locked.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPin2ForApp(String pin2, String aid, Message result);
+
+    /**
+     * Supply the SIM PUK2 to the SIM card
+     * Only called following operation where SIM_PUK2 was
+     * returned as a a failure from a previous operation
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC is permanently disabled.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPuk2(String puk2, String newPin2, Message result);
+
+    /**
+     * Supply the PUK2, newPin2 for the app with this AID on the ICC card
+     * Only called following operation where SIM_PUK2 was
+     * returned as a a failure from a previous operation
+     *
+     *  AID (Application ID), See ETSI 102.221 8.1 and 101.220 4
+     *
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  This exception is CommandException with an error of PASSWORD_INCORRECT
+     *  if the password is incorrect
+     *
+     *  ar.result is an optional array of integers where the first entry
+     *  is the number of attempts remaining before the ICC is permanently disabled.
+     *
+     * ar.exception and ar.result are null on success
+     */
+
+    void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message result);
+
+    // TODO: Add java doc and indicate that msg.arg1 contains the number of attempts remaining.
+    void changeIccPin(String oldPin, String newPin, Message result);
+    void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message result);
+    void changeIccPin2(String oldPin2, String newPin2, Message result);
+    void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr, Message result);
+
+    void changeBarringPassword(String facility, String oldPwd, String newPwd, Message result);
+
+    void supplyNetworkDepersonalization(String netpin, Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result contains a List of DriverCall
+     *      The ar.result List is sorted by DriverCall.index
+     */
+    void getCurrentCalls (Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result contains a List of DataCallResponse
+     *  @deprecated Do not use.
+     */
+    @Deprecated
+    void getPDPContextList(Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result contains a List of DataCallResponse
+     */
+    void getDataCallList(Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     *
+     * CLIR_DEFAULT     == on "use subscription default value"
+     * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation)
+     * CLIR_INVOCATION  == on "CLIR invocation" (restrict CLI presentation)
+     */
+    void dial (String address, int clirMode, Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     *
+     * CLIR_DEFAULT     == on "use subscription default value"
+     * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation)
+     * CLIR_INVOCATION  == on "CLIR invocation" (restrict CLI presentation)
+     */
+    void dial(String address, int clirMode, UUSInfo uusInfo, Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is String containing IMSI on success
+     */
+    void getIMSI(Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is String containing IMSI on success
+     */
+    void getIMSIForApp(String aid, Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is String containing IMEI on success
+     */
+    void getIMEI(Message result);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is String containing IMEISV on success
+     */
+    void getIMEISV(Message result);
+
+    /**
+     * Hang up one individual connection.
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     *
+     *  3GPP 22.030 6.5.5
+     *  "Releases a specific active call X"
+     */
+    void hangupConnection (int gsmIndex, Message result);
+
+    /**
+     * 3GPP 22.030 6.5.5
+     *  "Releases all held calls or sets User Determined User Busy (UDUB)
+     *   for a waiting call."
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void hangupWaitingOrBackground (Message result);
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Releases all active calls (if any exist) and accepts
+     *  the other (held or waiting) call."
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void hangupForegroundResumeBackground (Message result);
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Places all active calls (if any exist) on hold and accepts
+     *  the other (held or waiting) call."
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void switchWaitingOrHoldingAndActive (Message result);
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Adds a held call to the conversation"
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void conference (Message result);
+
+    /**
+     * Set preferred Voice Privacy (VP).
+     *
+     * @param enable true is enhanced and false is normal VP
+     * @param result is a callback message
+     */
+    void setPreferredVoicePrivacy(boolean enable, Message result);
+
+    /**
+     * Get currently set preferred Voice Privacy (VP) mode.
+     *
+     * @param result is a callback message
+     */
+    void getPreferredVoicePrivacy(Message result);
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Places all active calls on hold except call X with which
+     *  communication shall be supported."
+     */
+    void separateConnection (int gsmIndex, Message result);
+
+    /**
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void acceptCall (Message result);
+
+    /**
+     *  also known as UDUB
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void rejectCall (Message result);
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Connects the two calls and disconnects the subscriber from both calls"
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void explicitCallTransfer (Message result);
+
+    /**
+     * cause code returned as int[0] in Message.obj.response
+     * Returns integer cause code defined in TS 24.008
+     * Annex H or closest approximation.
+     * Most significant codes:
+     * - Any defined in 22.001 F.4 (for generating busy/congestion)
+     * - Cause 68: ACM >= ACMMax
+     */
+    void getLastCallFailCause (Message result);
+
+
+    /**
+     * Reason for last PDP context deactivate or failure to activate
+     * cause code returned as int[0] in Message.obj.response
+     * returns an integer cause code defined in TS 24.008
+     * section 6.1.3.1.3 or close approximation
+     * @deprecated Do not use.
+     */
+    @Deprecated
+    void getLastPdpFailCause (Message result);
+
+    /**
+     * The preferred new alternative to getLastPdpFailCause
+     * that is also CDMA-compatible.
+     */
+    void getLastDataCallFailCause (Message result);
+
+    void setMute (boolean enableMute, Message response);
+
+    void getMute (Message response);
+
+    /**
+     * response.obj is an AsyncResult
+     * response.obj.result is an int[2]
+     * response.obj.result[0] is received signal strength (0-31, 99)
+     * response.obj.result[1] is  bit error rate (0-7, 99)
+     * as defined in TS 27.007 8.5
+     */
+    void getSignalStrength (Message response);
+
+
+    /**
+     * response.obj.result is an int[3]
+     * response.obj.result[0] is registration state 0-5 from TS 27.007 7.2
+     * response.obj.result[1] is LAC if registered or -1 if not
+     * response.obj.result[2] is CID if registered or -1 if not
+     * valid LAC and CIDs are 0x0000 - 0xffff
+     *
+     * Please note that registration state 4 ("unknown") is treated
+     * as "out of service" above
+     */
+    void getVoiceRegistrationState (Message response);
+
+    /**
+     * response.obj.result is an int[3]
+     * response.obj.result[0] is registration state 0-5 from TS 27.007 7.2
+     * response.obj.result[1] is LAC if registered or -1 if not
+     * response.obj.result[2] is CID if registered or -1 if not
+     * valid LAC and CIDs are 0x0000 - 0xffff
+     *
+     * Please note that registration state 4 ("unknown") is treated
+     * as "out of service" above
+     */
+    void getDataRegistrationState (Message response);
+
+    /**
+     * response.obj.result is a String[3]
+     * response.obj.result[0] is long alpha or null if unregistered
+     * response.obj.result[1] is short alpha or null if unregistered
+     * response.obj.result[2] is numeric or null if unregistered
+     */
+    void getOperator(Message response);
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void sendDtmf(char c, Message result);
+
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void startDtmf(char c, Message result);
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void stopDtmf(Message result);
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result is null on success and failure
+     */
+    void sendBurstDtmf(String dtmfString, int on, int off, Message result);
+
+    /**
+     * smscPDU is smsc address in PDU form GSM BCD format prefixed
+     *      by a length byte (as expected by TS 27.005) or NULL for default SMSC
+     * pdu is SMS in PDU format as an ASCII hex string
+     *      less the SMSC address
+     */
+    void sendSMS (String smscPDU, String pdu, Message response);
+
+    /**
+     * Send an SMS message, Identical to sendSMS,
+     * except that more messages are expected to be sent soon
+     * smscPDU is smsc address in PDU form GSM BCD format prefixed
+     *      by a length byte (as expected by TS 27.005) or NULL for default SMSC
+     * pdu is SMS in PDU format as an ASCII hex string
+     *      less the SMSC address
+     */
+    void sendSMSExpectMore (String smscPDU, String pdu, Message response);
+
+    /**
+     * @param pdu is CDMA-SMS in internal pseudo-PDU format
+     * @param response sent when operation completes
+     */
+    void sendCdmaSms(byte[] pdu, Message response);
+
+    /**
+     * send SMS over IMS with 3GPP/GSM SMS format
+     * @param smscPDU is smsc address in PDU form GSM BCD format prefixed
+     *      by a length byte (as expected by TS 27.005) or NULL for default SMSC
+     * @param pdu is SMS in PDU format as an ASCII hex string
+     *      less the SMSC address
+     * @param retry indicates if this is a retry; 0 == not retry, nonzero = retry
+     * @param messageRef valid field if retry is set to nonzero.
+     *        Contains messageRef from RIL_SMS_Response corresponding to failed MO SMS
+     * @param response sent when operation completes
+     */
+    void sendImsGsmSms (String smscPDU, String pdu, int retry, int messageRef,
+            Message response);
+
+    /**
+     * send SMS over IMS with 3GPP2/CDMA SMS format
+     * @param pdu is CDMA-SMS in internal pseudo-PDU format
+     * @param response sent when operation completes
+     * @param retry indicates if this is a retry; 0 == not retry, nonzero = retry
+     * @param messageRef valid field if retry is set to nonzero.
+     *        Contains messageRef from RIL_SMS_Response corresponding to failed MO SMS
+     * @param response sent when operation completes
+     */
+    void sendImsCdmaSms(byte[] pdu, int retry, int messageRef, Message response);
+
+    /**
+     * Deletes the specified SMS record from SIM memory (EF_SMS).
+     *
+     * @param index index of the SMS record to delete
+     * @param response sent when operation completes
+     */
+    void deleteSmsOnSim(int index, Message response);
+
+    /**
+     * Deletes the specified SMS record from RUIM memory (EF_SMS in DF_CDMA).
+     *
+     * @param index index of the SMS record to delete
+     * @param response sent when operation completes
+     */
+    void deleteSmsOnRuim(int index, Message response);
+
+    /**
+     * Writes an SMS message to SIM memory (EF_SMS).
+     *
+     * @param status status of message on SIM.  One of:
+     *                  SmsManger.STATUS_ON_ICC_READ
+     *                  SmsManger.STATUS_ON_ICC_UNREAD
+     *                  SmsManger.STATUS_ON_ICC_SENT
+     *                  SmsManger.STATUS_ON_ICC_UNSENT
+     * @param pdu message PDU, as hex string
+     * @param response sent when operation completes.
+     *                  response.obj will be an AsyncResult, and will indicate
+     *                  any error that may have occurred (eg, out of memory).
+     */
+    void writeSmsToSim(int status, String smsc, String pdu, Message response);
+
+    void writeSmsToRuim(int status, String pdu, Message response);
+
+    void setRadioPower(boolean on, Message response);
+
+    void acknowledgeLastIncomingGsmSms(boolean success, int cause, Message response);
+
+    void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message response);
+
+    /**
+     * Acknowledge successful or failed receipt of last incoming SMS,
+     * including acknowledgement TPDU to send as the RP-User-Data element
+     * of the RP-ACK or RP-ERROR PDU.
+     *
+     * @param success true to send RP-ACK, false to send RP-ERROR
+     * @param ackPdu the acknowledgement TPDU in hexadecimal format
+     * @param response sent when operation completes.
+     */
+    void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, Message response);
+
+    /**
+     * parameters equivalent to 27.007 AT+CRSM command
+     * response.obj will be an AsyncResult
+     * response.obj.result will be an IccIoResult on success
+     */
+    void iccIO (int command, int fileid, String path, int p1, int p2, int p3,
+            String data, String pin2, Message response);
+
+    /**
+     * parameters equivalent to 27.007 AT+CRSM command
+     * response.obj will be an AsyncResult
+     * response.obj.userObj will be a IccIoResult on success
+     */
+    void iccIOForApp (int command, int fileid, String path, int p1, int p2, int p3,
+            String data, String pin2, String aid, Message response);
+
+    /**
+     * (AsyncResult)response.obj).result is an int[] with element [0] set to
+     * 1 for "CLIP is provisioned", and 0 for "CLIP is not provisioned".
+     *
+     * @param response is callback message
+     */
+
+    void queryCLIP(Message response);
+
+    /**
+     * response.obj will be a an int[2]
+     *
+     * response.obj[0] will be TS 27.007 +CLIR parameter 'n'
+     *  0 presentation indicator is used according to the subscription of the CLIR service
+     *  1 CLIR invocation
+     *  2 CLIR suppression
+     *
+     * response.obj[1] will be TS 27.007 +CLIR parameter 'm'
+     *  0 CLIR not provisioned
+     *  1 CLIR provisioned in permanent mode
+     *  2 unknown (e.g. no network, etc.)
+     *  3 CLIR temporary mode presentation restricted
+     *  4 CLIR temporary mode presentation allowed
+     */
+
+    void getCLIR(Message response);
+
+    /**
+     * clirMode is one of the CLIR_* constants above
+     *
+     * response.obj is null
+     */
+
+    void setCLIR(int clirMode, Message response);
+
+    /**
+     * (AsyncResult)response.obj).result is an int[] with element [0] set to
+     * 0 for disabled, 1 for enabled.
+     *
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param response is callback message
+     */
+
+    void queryCallWaiting(int serviceClass, Message response);
+
+    /**
+     * @param enable is true to enable, false to disable
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param response is callback message
+     */
+
+    void setCallWaiting(boolean enable, int serviceClass, Message response);
+
+    /**
+     * @param action is one of CF_ACTION_*
+     * @param cfReason is one of CF_REASON_*
+     * @param serviceClass is a sum of SERVICE_CLASSS_*
+     */
+    void setCallForward(int action, int cfReason, int serviceClass,
+                String number, int timeSeconds, Message response);
+
+    /**
+     * cfReason is one of CF_REASON_*
+     *
+     * ((AsyncResult)response.obj).result will be an array of
+     * CallForwardInfo's
+     *
+     * An array of length 0 means "disabled for all codes"
+     */
+    void queryCallForwardStatus(int cfReason, int serviceClass,
+            String number, Message response);
+
+    void setNetworkSelectionModeAutomatic(Message response);
+
+    void setNetworkSelectionModeManual(String operatorNumeric, Message response);
+
+    /**
+     * Queries whether the current network selection mode is automatic
+     * or manual
+     *
+     * ((AsyncResult)response.obj).result  is an int[] with element [0] being
+     * a 0 for automatic selection and a 1 for manual selection
+     */
+
+    void getNetworkSelectionMode(Message response);
+
+    /**
+     * Queries the currently available networks
+     *
+     * ((AsyncResult)response.obj).result is a List of NetworkInfo objects
+     */
+    void getAvailableNetworks(Message response);
+
+    /**
+     * Starts a radio network scan
+     *
+     * ((AsyncResult)response.obj).result is a NetworkScanResult object
+     */
+    void startNetworkScan(NetworkScanRequest nsr, Message response);
+
+    /**
+     * Stops the ongoing network scan
+     *
+     * ((AsyncResult)response.obj).result is a NetworkScanResult object
+     *
+     */
+    void stopNetworkScan(Message response);
+
+    /**
+     * Gets the baseband version
+     */
+    void getBasebandVersion(Message response);
+
+    /**
+     * (AsyncResult)response.obj).result will be an Integer representing
+     * the sum of enabled service classes (sum of SERVICE_CLASS_*)
+     *
+     * @param facility one of CB_FACILTY_*
+     * @param password password or "" if not required
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param response is callback message
+     */
+
+    void queryFacilityLock (String facility, String password, int serviceClass,
+        Message response);
+
+    /**
+     * (AsyncResult)response.obj).result will be an Integer representing
+     * the sum of enabled service classes (sum of SERVICE_CLASS_*) for the
+     * application with appId.
+     *
+     * @param facility one of CB_FACILTY_*
+     * @param password password or "" if not required
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param appId is application Id or null if none
+     * @param response is callback message
+     */
+
+    void queryFacilityLockForApp(String facility, String password, int serviceClass, String appId,
+        Message response);
+
+    /**
+     * @param facility one of CB_FACILTY_*
+     * @param lockState true means lock, false means unlock
+     * @param password password or "" if not required
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param response is callback message
+     */
+    void setFacilityLock (String facility, boolean lockState, String password,
+        int serviceClass, Message response);
+
+    /**
+     * Set the facility lock for the app with this AID on the ICC card.
+     *
+     * @param facility one of CB_FACILTY_*
+     * @param lockState true means lock, false means unlock
+     * @param password password or "" if not required
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param appId is application Id or null if none
+     * @param response is callback message
+     */
+    void setFacilityLockForApp(String facility, boolean lockState, String password,
+        int serviceClass, String appId, Message response);
+
+    void sendUSSD (String ussdString, Message response);
+
+    /**
+     * Cancels a pending USSD session if one exists.
+     * @param response callback message
+     */
+    void cancelPendingUssd (Message response);
+
+    void resetRadio(Message result);
+
+    /**
+     * Assign a specified band for RF configuration.
+     *
+     * @param bandMode one of BM_*_BAND
+     * @param response is callback message
+     */
+    void setBandMode (int bandMode, Message response);
+
+    /**
+     * Query the list of band mode supported by RF.
+     *
+     * @param response is callback message
+     *        ((AsyncResult)response.obj).result  is an int[] where int[0] is
+     *        the size of the array and the rest of each element representing
+     *        one available BM_*_BAND
+     */
+    void queryAvailableBandMode (Message response);
+
+    /**
+     *  Requests to set the preferred network type for searching and registering
+     * (CS/PS domain, RAT, and operation mode)
+     * @param networkType one of  NT_*_TYPE
+     * @param response is callback message
+     */
+    void setPreferredNetworkType(int networkType , Message response);
+
+     /**
+     *  Query the preferred network type setting
+     *
+     * @param response is callback message to report one of  NT_*_TYPE
+     */
+    void getPreferredNetworkType(Message response);
+
+    /**
+     * Query neighboring cell ids
+     *
+     * @param response s callback message to cell ids
+     * @param workSource calling WorkSource
+     */
+    default void getNeighboringCids(Message response, WorkSource workSource){}
+
+    /**
+     * Request to enable/disable network state change notifications when
+     * location information (lac and/or cid) has changed.
+     *
+     * @param enable true to enable, false to disable
+     * @param response callback message
+     */
+    void setLocationUpdates(boolean enable, Message response);
+
+    /**
+     * Gets the default SMSC address.
+     *
+     * @param result Callback message contains the SMSC address.
+     */
+    void getSmscAddress(Message result);
+
+    /**
+     * Sets the default SMSC address.
+     *
+     * @param address new SMSC address
+     * @param result Callback message is empty on completion
+     */
+    void setSmscAddress(String address, Message result);
+
+    /**
+     * Indicates whether there is storage available for new SMS messages.
+     * @param available true if storage is available
+     * @param result callback message
+     */
+    void reportSmsMemoryStatus(boolean available, Message result);
+
+    /**
+     * Indicates to the vendor ril that StkService is running
+     * and is ready to receive RIL_UNSOL_STK_XXXX commands.
+     *
+     * @param result callback message
+     */
+    void reportStkServiceIsRunning(Message result);
+
+    void invokeOemRilRequestRaw(byte[] data, Message response);
+
+    /**
+     * Sends carrier specific information to the vendor ril that can be used to
+     * encrypt the IMSI and IMPI.
+     *
+     * @param publicKey the public key of the carrier used to encrypt IMSI/IMPI.
+     * @param keyIdentifier the key identifier is optional information that is carrier
+     *        specific.
+     * @param response callback message
+     */
+    void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                         Message response);
+
+    void invokeOemRilRequestStrings(String[] strings, Message response);
+
+    /**
+     * Fires when RIL_UNSOL_OEM_HOOK_RAW is received from the RIL.
+     */
+    void setOnUnsolOemHookRaw(Handler h, int what, Object obj);
+    void unSetOnUnsolOemHookRaw(Handler h);
+
+    /**
+     * Send TERMINAL RESPONSE to the SIM, after processing a proactive command
+     * sent by the SIM.
+     *
+     * @param contents  String containing SAT/USAT response in hexadecimal
+     *                  format starting with first byte of response data. See
+     *                  TS 102 223 for details.
+     * @param response  Callback message
+     */
+    public void sendTerminalResponse(String contents, Message response);
+
+    /**
+     * Send ENVELOPE to the SIM, after processing a proactive command sent by
+     * the SIM.
+     *
+     * @param contents  String containing SAT/USAT response in hexadecimal
+     *                  format starting with command tag. See TS 102 223 for
+     *                  details.
+     * @param response  Callback message
+     */
+    public void sendEnvelope(String contents, Message response);
+
+    /**
+     * Send ENVELOPE to the SIM, such as an SMS-PP data download envelope
+     * for a SIM data download message. This method has one difference
+     * from {@link #sendEnvelope}: The SW1 and SW2 status bytes from the UICC response
+     * are returned along with the response data.
+     *
+     * response.obj will be an AsyncResult
+     * response.obj.result will be an IccIoResult on success
+     *
+     * @param contents  String containing SAT/USAT response in hexadecimal
+     *                  format starting with command tag. See TS 102 223 for
+     *                  details.
+     * @param response  Callback message
+     */
+    public void sendEnvelopeWithStatus(String contents, Message response);
+
+    /**
+     * Accept or reject the call setup request from SIM.
+     *
+     * @param accept   true if the call is to be accepted, false otherwise.
+     * @param response Callback message
+     */
+    public void handleCallSetupRequestFromSim(boolean accept, Message response);
+
+    /**
+     * Activate or deactivate cell broadcast SMS for GSM.
+     *
+     * @param activate
+     *            true = activate, false = deactivate
+     * @param result Callback message is empty on completion
+     */
+    public void setGsmBroadcastActivation(boolean activate, Message result);
+
+    /**
+     * Configure cell broadcast SMS for GSM.
+     *
+     * @param response Callback message is empty on completion
+     */
+    public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response);
+
+    /**
+     * Query the current configuration of cell broadcast SMS of GSM.
+     *
+     * @param response
+     *        Callback message contains the configuration from the modem
+     *        on completion
+     */
+    public void getGsmBroadcastConfig(Message response);
+
+    //***** new Methods for CDMA support
+
+    /**
+     * Request the device ESN / MEID / IMEI / IMEISV.
+     * "response" is const char **
+     *   [0] is IMEI if GSM subscription is available
+     *   [1] is IMEISV if GSM subscription is available
+     *   [2] is ESN if CDMA subscription is available
+     *   [3] is MEID if CDMA subscription is available
+     */
+    public void getDeviceIdentity(Message response);
+
+    /**
+     * Request the device MDN / H_SID / H_NID / MIN.
+     * "response" is const char **
+     *   [0] is MDN if CDMA subscription is available
+     *   [1] is a comma separated list of H_SID (Home SID) in decimal format
+     *       if CDMA subscription is available
+     *   [2] is a comma separated list of H_NID (Home NID) in decimal format
+     *       if CDMA subscription is available
+     *   [3] is MIN (10 digits, MIN2+MIN1) if CDMA subscription is available
+     */
+    public void getCDMASubscription(Message response);
+
+    /**
+     * Send Flash Code.
+     * "response" is is NULL
+     *   [0] is a FLASH string
+     */
+    public void sendCDMAFeatureCode(String FeatureCode, Message response);
+
+    /** Set the Phone type created */
+    void setPhoneType(int phoneType);
+
+    /**
+     *  Query the CDMA roaming preference setting
+     *
+     * @param response is callback message to report one of  CDMA_RM_*
+     */
+    void queryCdmaRoamingPreference(Message response);
+
+    /**
+     *  Requests to set the CDMA roaming preference
+     * @param cdmaRoamingType one of  CDMA_RM_*
+     * @param response is callback message
+     */
+    void setCdmaRoamingPreference(int cdmaRoamingType, Message response);
+
+    /**
+     *  Requests to set the CDMA subscription mode
+     * @param cdmaSubscriptionType one of  CDMA_SUBSCRIPTION_*
+     * @param response is callback message
+     */
+    void setCdmaSubscriptionSource(int cdmaSubscriptionType, Message response);
+
+    /**
+     *  Requests to get the CDMA subscription srouce
+     * @param response is callback message
+     */
+    void getCdmaSubscriptionSource(Message response);
+
+    /**
+     *  Set the TTY mode
+     *
+     * @param ttyMode one of the following:
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+     * @param response is callback message
+     */
+    void setTTYMode(int ttyMode, Message response);
+
+    /**
+     *  Query the TTY mode
+     * (AsyncResult)response.obj).result is an int[] with element [0] set to
+     * tty mode:
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+     * @param response is callback message
+     */
+    void queryTTYMode(Message response);
+
+    /**
+     * Setup a packet data connection On successful completion, the result
+     * message will return a {@link com.android.internal.telephony.dataconnection.DataCallResponse}
+     * object containing the connection information.
+     *
+     * @param radioTechnology
+     *            Radio technology to use. Values is one of RIL_RADIO_TECHNOLOGY_*
+     * @param dataProfile
+     *            Data profile for data call setup
+     * @param isRoaming
+     *            Device is roaming or not
+     * @param allowRoaming
+     *            Flag indicating data roaming is enabled or not
+     * @param result
+     *            Callback message
+     */
+    void setupDataCall(int radioTechnology, DataProfile dataProfile, boolean isRoaming,
+                       boolean allowRoaming, Message result);
+
+    /**
+     * Deactivate packet data connection
+     *
+     * @param cid
+     *            The connection ID
+     * @param reason
+     *            Data disconnect reason.
+     * @param result
+     *            Callback message is empty on completion
+     */
+    public void deactivateDataCall(int cid, int reason, Message result);
+
+    /**
+     * Activate or deactivate cell broadcast SMS for CDMA.
+     *
+     * @param activate
+     *            true = activate, false = deactivate
+     * @param result
+     *            Callback message is empty on completion
+     */
+    public void setCdmaBroadcastActivation(boolean activate, Message result);
+
+    /**
+     * Configure cdma cell broadcast SMS.
+     *
+     * @param response
+     *            Callback message is empty on completion
+     */
+    public void setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs, Message response);
+
+    /**
+     * Query the current configuration of cdma cell broadcast SMS.
+     *
+     * @param result
+     *            Callback message contains the configuration from the modem on completion
+     */
+    public void getCdmaBroadcastConfig(Message result);
+
+    /**
+     *  Requests the radio's system selection module to exit emergency callback mode.
+     *  This function should only be called from for CDMA.
+     *
+     * @param response callback message
+     */
+    public void exitEmergencyCallbackMode(Message response);
+
+    /**
+     * Request the status of the ICC and UICC cards.
+     *
+     * @param result
+     *          Callback message containing {@link IccCardStatus} structure for the card.
+     */
+    public void getIccCardStatus(Message result);
+
+    /**
+     * Return if the current radio is LTE on CDMA. This
+     * is a tri-state return value as for a period of time
+     * the mode may be unknown.
+     *
+     * @return {@link PhoneConstants#LTE_ON_CDMA_UNKNOWN}, {@link PhoneConstants#LTE_ON_CDMA_FALSE}
+     * or {@link PhoneConstants#LTE_ON_CDMA_TRUE}
+     */
+    public int getLteOnCdmaMode();
+
+    /**
+     * Request the ISIM application on the UICC to perform the AKA
+     * challenge/response algorithm for IMS authentication. The nonce string
+     * and challenge response are Base64 encoded Strings.
+     *
+     * @param nonce the nonce string to pass with the ISIM authentication request
+     * @param response a callback message with the String response in the obj field
+     * @deprecated
+     * @see requestIccSimAuthentication
+     */
+    public void requestIsimAuthentication(String nonce, Message response);
+
+    /**
+     * Request the SIM application on the UICC to perform authentication
+     * challenge/response algorithm. The data string and challenge response are
+     * Base64 encoded Strings.
+     * Can support EAP-SIM, EAP-AKA with results encoded per 3GPP TS 31.102.
+     *
+     * @param authContext is the P2 parameter that specifies the authentication context per 3GPP TS
+     *                    31.102 (Section 7.1.2)
+     * @param data authentication challenge data
+     * @param aid used to determine which application/slot to send the auth command to. See ETSI
+     *            102.221 8.1 and 101.220 4
+     * @param response a callback message with the String response in the obj field
+     */
+    public void requestIccSimAuthentication(int authContext, String data, String aid, Message response);
+
+    /**
+     * Get the current Voice Radio Technology.
+     *
+     * AsyncResult.result is an int array with the first value
+     * being one of the ServiceState.RIL_RADIO_TECHNOLOGY_xxx values.
+     *
+     * @param result is sent back to handler and result.obj is a AsyncResult
+     */
+    void getVoiceRadioTechnology(Message result);
+
+    /**
+     * Return the current set of CellInfo records
+     *
+     * AsyncResult.result is a of Collection<CellInfo>
+     *
+     * @param result is sent back to handler and result.obj is a AsyncResult
+     * @param workSource calling WorkSource
+     */
+    default void getCellInfoList(Message result, WorkSource workSource) {}
+
+    /**
+     * Sets the minimum time in milli-seconds between when RIL_UNSOL_CELL_INFO_LIST
+     * should be invoked.
+     *
+     * The default, 0, means invoke RIL_UNSOL_CELL_INFO_LIST when any of the reported
+     * information changes. Setting the value to INT_MAX(0x7fffffff) means never issue
+     * A RIL_UNSOL_CELL_INFO_LIST.
+     *
+     *
+
+     * @param rateInMillis is sent back to handler and result.obj is a AsyncResult
+     * @param response.obj is AsyncResult ar when sent to associated handler
+     *                        ar.exception carries exception on failure or null on success
+     *                        otherwise the error.
+     * @param workSource calling WorkSource
+     */
+    default void setCellInfoListRate(int rateInMillis, Message response, WorkSource workSource){}
+
+    /**
+     * Fires when RIL_UNSOL_CELL_INFO_LIST is received from the RIL.
+     */
+    void registerForCellInfoList(Handler h, int what, Object obj);
+    void unregisterForCellInfoList(Handler h);
+
+    /**
+     * Set Initial Attach Apn
+     *
+     * @param dataProfile
+     *            data profile for initial APN attach
+     * @param isRoaming
+     *            indicating the device is roaming or not
+     * @param result
+     *            callback message contains the information of SUCCESS/FAILURE
+     */
+    void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming, Message result);
+
+    /**
+     * Set data profiles in modem
+     *
+     * @param dps
+     *            Array of the data profiles set to modem
+     * @param isRoaming
+     *            Indicating if the device is roaming or not
+     * @param result
+     *            callback message contains the information of SUCCESS/FAILURE
+     */
+    void setDataProfile(DataProfile[] dps, boolean isRoaming, Message result);
+
+    /**
+     * Notifiy that we are testing an emergency call
+     */
+    public void testingEmergencyCall();
+
+    /**
+     * Open a logical channel to the SIM.
+     *
+     * Input parameters equivalent to TS 27.007 AT+CCHO command.
+     *
+     * @param AID Application id. See ETSI 102.221 and 101.220.
+     * @param p2 P2 parameter (described in ISO 7816-4).
+     * @param response Callback message. response.obj will be an int [1] with
+     *            element [0] set to the id of the logical channel.
+     */
+    public void iccOpenLogicalChannel(String AID, int p2, Message response);
+
+    /**
+     * Close a previously opened logical channel to the SIM.
+     *
+     * Input parameters equivalent to TS 27.007 AT+CCHC command.
+     *
+     * @param channel Channel id. Id of the channel to be closed.
+     * @param response Callback message.
+     */
+    public void iccCloseLogicalChannel(int channel, Message response);
+
+    /**
+     * Exchange APDUs with the SIM on a logical channel.
+     *
+     * Input parameters equivalent to TS 27.007 AT+CGLA command.
+     *
+     * @param channel Channel id of the channel to use for communication. Has to
+     *            be greater than zero.
+     * @param cla Class of the APDU command.
+     * @param instruction Instruction of the APDU command.
+     * @param p1 P1 value of the APDU command.
+     * @param p2 P2 value of the APDU command.
+     * @param p3 P3 value of the APDU command. If p3 is negative a 4 byte APDU
+     *            is sent to the SIM.
+     * @param data Data to be sent with the APDU.
+     * @param response Callback message. response.obj.userObj will be
+     *            an IccIoResult on success.
+     */
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction,
+            int p1, int p2, int p3, String data, Message response);
+
+    /**
+     * Exchange APDUs with the SIM on a basic channel.
+     *
+     * Input parameters equivalent to TS 27.007 AT+CSIM command.
+     *
+     * @param cla Class of the APDU command.
+     * @param instruction Instruction of the APDU command.
+     * @param p1 P1 value of the APDU command.
+     * @param p2 P2 value of the APDU command.
+     * @param p3 P3 value of the APDU command. If p3 is negative a 4 byte APDU
+     *            is sent to the SIM.
+     * @param data Data to be sent with the APDU.
+     * @param response Callback message. response.obj.userObj will be
+     *            an IccIoResult on success.
+     */
+    public void iccTransmitApduBasicChannel(int cla, int instruction, int p1, int p2,
+            int p3, String data, Message response);
+
+    /**
+     * Read one of the NV items defined in {@link RadioNVItems} / {@code ril_nv_items.h}.
+     * Used for device configuration by some CDMA operators.
+     *
+     * @param itemID the ID of the item to read
+     * @param response callback message with the String response in the obj field
+     */
+    void nvReadItem(int itemID, Message response);
+
+    /**
+     * Write one of the NV items defined in {@link RadioNVItems} / {@code ril_nv_items.h}.
+     * Used for device configuration by some CDMA operators.
+     *
+     * @param itemID the ID of the item to read
+     * @param itemValue the value to write, as a String
+     * @param response Callback message.
+     */
+    void nvWriteItem(int itemID, String itemValue, Message response);
+
+    /**
+     * Update the CDMA Preferred Roaming List (PRL) in the radio NV storage.
+     * Used for device configuration by some CDMA operators.
+     *
+     * @param preferredRoamingList byte array containing the new PRL
+     * @param response Callback message.
+     */
+    void nvWriteCdmaPrl(byte[] preferredRoamingList, Message response);
+
+    /**
+     * Perform the specified type of NV config reset. The radio will be taken offline
+     * and the device must be rebooted after erasing the NV. Used for device
+     * configuration by some CDMA operators.
+     *
+     * @param resetType reset type: 1: reload NV reset, 2: erase NV reset, 3: factory NV reset
+     * @param response Callback message.
+     */
+    void nvResetConfig(int resetType, Message response);
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the orignal value of result.obj
+     *  ar.result contains a List of HardwareConfig
+     */
+    void getHardwareConfig (Message result);
+
+    /**
+     * @return version of the ril.
+     */
+    int getRilVersion();
+
+   /**
+     * Sets user selected subscription at Modem.
+     *
+     * @param slotId
+     *          Slot.
+     * @param appIndex
+     *          Application index in the card.
+     * @param subId
+     *          Indicates subscription 0 or subscription 1.
+     * @param subStatus
+     *          Activation status, 1 = activate and 0 = deactivate.
+     * @param result
+     *          Callback message contains the information of SUCCESS/FAILURE.
+     */
+    // FIXME Update the doc and consider modifying the request to make more generic.
+    public void setUiccSubscription(int slotId, int appIndex, int subId, int subStatus,
+            Message result);
+
+    /**
+     * Tells the modem if data is allowed or not.
+     *
+     * @param allowed
+     *          true = allowed, false = not alowed
+     * @param result
+     *          Callback message contains the information of SUCCESS/FAILURE.
+     */
+    // FIXME We may need to pass AID and slotid also
+    public void setDataAllowed(boolean allowed, Message result);
+
+    /**
+     * Inform RIL that the device is shutting down
+     *
+     * @param result Callback message contains the information of SUCCESS/FAILURE
+     */
+    public void requestShutdown(Message result);
+
+    /**
+     *  Set phone radio type and access technology.
+     *
+     *  @param rc the phone radio capability defined in
+     *         RadioCapability. It's a input object used to transfer parameter to logic modem
+     *
+     *  @param result Callback message.
+     */
+    public void setRadioCapability(RadioCapability rc, Message result);
+
+    /**
+     *  Get phone radio capability
+     *
+     *  @param result Callback message.
+     */
+    public void getRadioCapability(Message result);
+
+    /**
+     * Registers the handler when phone radio capability is changed.
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForRadioCapabilityChanged(Handler h, int what, Object obj);
+
+    /**
+     * Unregister for notifications when phone radio capability is changed.
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForRadioCapabilityChanged(Handler h);
+
+    /**
+     * Start LCE (Link Capacity Estimation) service with a desired reporting interval.
+     *
+     * @param reportIntervalMs
+     *        LCE info reporting interval (ms).
+     *
+     * @param result Callback message contains the current LCE status.
+     * {byte status, int actualIntervalMs}
+     */
+    public void startLceService(int reportIntervalMs, boolean pullMode, Message result);
+
+    /**
+     * Stop LCE service.
+     *
+     * @param result Callback message contains the current LCE status:
+     * {byte status, int actualIntervalMs}
+     *
+     */
+    public void stopLceService(Message result);
+
+    /**
+     * Pull LCE service for capacity data.
+     *
+     * @param result Callback message contains the capacity info:
+     * {int capacityKbps, byte confidenceLevel, byte lceSuspendedTemporarily}
+     */
+    public void pullLceData(Message result);
+
+    /**
+     * Register a LCE info listener.
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForLceInfo(Handler h, int what, Object obj);
+
+    /**
+     * Unregister the LCE Info listener.
+     *
+     * @param h handle to be removed.
+     */
+    void unregisterForLceInfo(Handler h);
+
+    /**
+     *
+     * Get modem activity info and stats
+     *
+     * @param result Callback message contains the modem activity information
+     */
+    public void getModemActivityInfo(Message result);
+
+    /**
+     * Set allowed carriers
+     *
+     * @param carriers Allowed carriers
+     * @param result Callback message contains the number of carriers set successfully
+     */
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message result);
+
+    /**
+     * Get allowed carriers
+     *
+     * @param result Callback message contains the allowed carriers
+     */
+    public void getAllowedCarriers(Message result);
+
+    /**
+     * Register for unsolicited PCO data.  This information is carrier-specific,
+     * opaque binary blobs destined for carrier apps for interpretation.
+     *
+     * @param h Handler for notificaiton message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForPcoData(Handler h, int what, Object obj);
+
+    /**
+     * Unregister for PCO data.
+     *
+     * @param h handler to be removed
+     */
+    public void unregisterForPcoData(Handler h);
+
+    /**
+     * Register for modem reset indication.
+     *
+     * @param h  Handler for the notification message
+     * @param what User-defined message code
+     * @param obj User object
+     */
+    void registerForModemReset(Handler h, int what, Object obj);
+
+    /**
+     * Unregister for modem reset
+     *
+     * @param h handler to be removed
+     */
+    void unregisterForModemReset(Handler h);
+
+    /**
+     * Send the updated device state
+     *
+     * @param stateType Device state type
+     * @param state True if enabled, otherwise disabled
+     * @param result callback message contains the information of SUCCESS/FAILURE
+     */
+    void sendDeviceState(int stateType, boolean state, Message result);
+
+    /**
+     * Send the device state to the modem
+     *
+     * @param filter unsolicited response filter. See DeviceStateMonitor.UnsolicitedResponseFilter
+     * @param result callback message contains the information of SUCCESS/FAILURE
+     */
+    void setUnsolResponseFilter(int filter, Message result);
+
+    /**
+     * Set SIM card power up or down
+     *
+     * @param state  State of SIM (power down, power up, pass through)
+     * - {@link android.telephony.TelephonyManager#CARD_POWER_DOWN}
+     * - {@link android.telephony.TelephonyManager#CARD_POWER_UP}
+     * - {@link android.telephony.TelephonyManager#CARD_POWER_UP_PASS_THROUGH}
+     * @param result callback message contains the information of SUCCESS/FAILURE
+     */
+    void setSimCardPower(int state, Message result);
+
+    /**
+     * Register for unsolicited Carrier Public Key.
+     *
+     * @param h Handler for notificaiton message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForCarrierInfoForImsiEncryption(Handler h, int what, Object obj);
+
+    /**
+     * DeRegister for unsolicited Carrier Public Key.
+     *
+     * @param h Handler for notificaiton message.
+     */
+    void unregisterForCarrierInfoForImsiEncryption(Handler h);
+
+    /**
+     * Register for unsolicited Network Scan result.
+     *
+     * @param h Handler for notificaiton message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForNetworkScanResult(Handler h, int what, Object obj);
+
+    /**
+     * DeRegister for unsolicited Network Scan result.
+     *
+     * @param h Handler for notificaiton message.
+     */
+    void unregisterForNetworkScanResult(Handler h);
+
+    default public List<ClientRequestStats> getClientRequestStats() {
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/Connection.java b/com/android/internal/telephony/Connection.java
new file mode 100644
index 0000000..245f76c
--- /dev/null
+++ b/com/android/internal/telephony/Connection.java
@@ -0,0 +1,1077 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.telecom.ConferenceParticipant;
+import android.telephony.DisconnectCause;
+import android.telephony.Rlog;
+import android.util.Log;
+
+import java.lang.Override;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * {@hide}
+ */
+public abstract class Connection {
+
+    public interface PostDialListener {
+        void onPostDialWait();
+        void onPostDialChar(char c);
+    }
+
+    /**
+     * Capabilities that will be mapped to telecom connection
+     * capabilities.
+     */
+    public static class Capability {
+
+        /**
+         * For an IMS video call, indicates that the local side of the call supports downgrading
+         * from a video call to an audio-only call.
+         */
+        public static final int SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL = 0x00000001;
+
+        /**
+         * For an IMS video call, indicates that the peer supports downgrading to an audio-only
+         * call.
+         */
+        public static final int SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE = 0x00000002;
+
+        /**
+         * For an IMS call, indicates that the call supports video locally.
+         */
+        public static final int SUPPORTS_VT_LOCAL_BIDIRECTIONAL = 0x00000004;
+
+        /**
+         * For an IMS call, indicates that the peer supports video.
+         */
+        public static final int SUPPORTS_VT_REMOTE_BIDIRECTIONAL = 0x00000008;
+
+        /**
+         * Indicates that the connection is an external connection (e.g. an instance of the class
+         * {@link com.android.internal.telephony.imsphone.ImsExternalConnection}.
+         */
+        public static final int IS_EXTERNAL_CONNECTION = 0x00000010;
+
+        /**
+         * Indicates that this external connection can be pulled from the remote device to the
+         * local device.
+         */
+        public static final int IS_PULLABLE = 0x00000020;
+    }
+
+    /**
+     * Listener interface for events related to the connection which should be reported to the
+     * {@link android.telecom.Connection}.
+     */
+    public interface Listener {
+        public void onVideoStateChanged(int videoState);
+        public void onConnectionCapabilitiesChanged(int capability);
+        public void onWifiChanged(boolean isWifi);
+        public void onVideoProviderChanged(
+                android.telecom.Connection.VideoProvider videoProvider);
+        public void onAudioQualityChanged(int audioQuality);
+        public void onConferenceParticipantsChanged(List<ConferenceParticipant> participants);
+        public void onCallSubstateChanged(int callSubstate);
+        public void onMultipartyStateChanged(boolean isMultiParty);
+        public void onConferenceMergedFailed();
+        public void onExtrasChanged(Bundle extras);
+        public void onExitedEcmMode();
+        public void onCallPullFailed(Connection externalConnection);
+        public void onHandoverToWifiFailed();
+        public void onConnectionEvent(String event, Bundle extras);
+        public void onRttModifyRequestReceived();
+        public void onRttModifyResponseReceived(int status);
+    }
+
+    /**
+     * Base listener implementation.
+     */
+    public abstract static class ListenerBase implements Listener {
+        @Override
+        public void onVideoStateChanged(int videoState) {}
+        @Override
+        public void onConnectionCapabilitiesChanged(int capability) {}
+        @Override
+        public void onWifiChanged(boolean isWifi) {}
+        @Override
+        public void onVideoProviderChanged(
+                android.telecom.Connection.VideoProvider videoProvider) {}
+        @Override
+        public void onAudioQualityChanged(int audioQuality) {}
+        @Override
+        public void onConferenceParticipantsChanged(List<ConferenceParticipant> participants) {}
+        @Override
+        public void onCallSubstateChanged(int callSubstate) {}
+        @Override
+        public void onMultipartyStateChanged(boolean isMultiParty) {}
+        @Override
+        public void onConferenceMergedFailed() {}
+        @Override
+        public void onExtrasChanged(Bundle extras) {}
+        @Override
+        public void onExitedEcmMode() {}
+        @Override
+        public void onCallPullFailed(Connection externalConnection) {}
+        @Override
+        public void onHandoverToWifiFailed() {}
+        @Override
+        public void onConnectionEvent(String event, Bundle extras) {}
+        @Override
+        public void onRttModifyRequestReceived() {}
+        @Override
+        public void onRttModifyResponseReceived(int status) {}
+    }
+
+    public static final int AUDIO_QUALITY_STANDARD = 1;
+    public static final int AUDIO_QUALITY_HIGH_DEFINITION = 2;
+
+    /**
+     * The telecom internal call ID associated with this connection.  Only to be used for debugging
+     * purposes.
+     */
+    private String mTelecomCallId;
+
+    //Caller Name Display
+    protected String mCnapName;
+    protected int mCnapNamePresentation  = PhoneConstants.PRESENTATION_ALLOWED;
+    protected String mAddress;     // MAY BE NULL!!!
+    protected String mDialString;          // outgoing calls only
+    protected int mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
+    protected boolean mIsIncoming;
+    /*
+     * These time/timespan values are based on System.currentTimeMillis(),
+     * i.e., "wall clock" time.
+     */
+    protected long mCreateTime;
+    protected long mConnectTime;
+    /*
+     * These time/timespan values are based on SystemClock.elapsedRealTime(),
+     * i.e., time since boot.  They are appropriate for comparison and
+     * calculating deltas.
+     */
+    protected long mConnectTimeReal;
+    protected long mDuration;
+    protected long mHoldingStartTime;  // The time when the Connection last transitioned
+                            // into HOLDING
+    protected Connection mOrigConnection;
+    private List<PostDialListener> mPostDialListeners = new ArrayList<>();
+    public Set<Listener> mListeners = new CopyOnWriteArraySet<>();
+
+    protected boolean mNumberConverted = false;
+    protected String mConvertedNumber;
+
+    protected String mPostDialString;      // outgoing calls only
+    protected int mNextPostDialChar;       // index into postDialString
+
+    protected int mCause = DisconnectCause.NOT_DISCONNECTED;
+    protected PostDialState mPostDialState = PostDialState.NOT_STARTED;
+
+    private static String LOG_TAG = "Connection";
+
+    Object mUserData;
+    private int mVideoState;
+    private int mConnectionCapabilities;
+    private boolean mIsWifi;
+    private boolean mAudioModeIsVoip;
+    private int mAudioQuality;
+    private int mCallSubstate;
+    private android.telecom.Connection.VideoProvider mVideoProvider;
+    public Call.State mPreHandoverState = Call.State.IDLE;
+    private Bundle mExtras;
+    private int mPhoneType;
+    private boolean mAnsweringDisconnectsActiveCall;
+    private boolean mAllowAddCallDuringVideoCall;
+
+    /**
+     * Used to indicate that this originated from pulling a {@link android.telecom.Connection} with
+     * {@link android.telecom.Connection#PROPERTY_IS_EXTERNAL_CALL}.
+     */
+    private boolean mIsPulledCall = false;
+
+    /**
+     * Where {@link #mIsPulledCall} is {@code true}, contains the dialog Id of the external call
+     * which is being pulled (e.g.
+     * {@link com.android.internal.telephony.imsphone.ImsExternalConnection#getCallId()}).
+     */
+    private int mPulledDialogId;
+
+    protected Connection(int phoneType) {
+        mPhoneType = phoneType;
+    }
+
+    /* Instance Methods */
+
+    /**
+     * @return The telecom internal call ID associated with this connection.  Only to be used for
+     * debugging purposes.
+     */
+    public String getTelecomCallId() {
+        return mTelecomCallId;
+    }
+
+    /**
+     * Sets the telecom call ID associated with this connection.
+     *
+     * @param telecomCallId The telecom call ID.
+     */
+    public void setTelecomCallId(String telecomCallId) {
+        mTelecomCallId = telecomCallId;
+    }
+
+    /**
+     * Gets address (e.g. phone number) associated with connection.
+     * TODO: distinguish reasons for unavailability
+     *
+     * @return address or null if unavailable
+     */
+
+    public String getAddress() {
+        return mAddress;
+    }
+
+    /**
+     * Gets CNAP name associated with connection.
+     * @return cnap name or null if unavailable
+     */
+    public String getCnapName() {
+        return mCnapName;
+    }
+
+    /**
+     * Get original dial string.
+     * @return original dial string or null if unavailable
+     */
+    public String getOrigDialString(){
+        return null;
+    }
+
+    /**
+     * Gets CNAP presentation associated with connection.
+     * @return cnap name or null if unavailable
+     */
+
+    public int getCnapNamePresentation() {
+       return mCnapNamePresentation;
+    }
+
+    /**
+     * @return Call that owns this Connection, or null if none
+     */
+    public abstract Call getCall();
+
+    /**
+     * Connection create time in currentTimeMillis() format
+     * Basically, set when object is created.
+     * Effectively, when an incoming call starts ringing or an
+     * outgoing call starts dialing
+     */
+    public long getCreateTime() {
+        return mCreateTime;
+    }
+
+    /**
+     * Connection connect time in currentTimeMillis() format.
+     * For outgoing calls: Begins at (DIALING|ALERTING) -> ACTIVE transition.
+     * For incoming calls: Begins at (INCOMING|WAITING) -> ACTIVE transition.
+     * Returns 0 before then.
+     */
+    public long getConnectTime() {
+        return mConnectTime;
+    }
+
+    /**
+     * Sets the Connection connect time in currentTimeMillis() format.
+     *
+     * @param connectTime the new connect time.
+     */
+    public void setConnectTime(long connectTime) {
+        mConnectTime = connectTime;
+    }
+
+    /**
+     * Sets the Connection connect time in {@link SystemClock#elapsedRealtime()} format.
+     *
+     * @param connectTimeReal the new connect time.
+     */
+    public void setConnectTimeReal(long connectTimeReal) {
+        mConnectTimeReal = connectTimeReal;
+    }
+
+    /**
+     * Connection connect time in elapsedRealtime() format.
+     * For outgoing calls: Begins at (DIALING|ALERTING) -> ACTIVE transition.
+     * For incoming calls: Begins at (INCOMING|WAITING) -> ACTIVE transition.
+     * Returns 0 before then.
+     */
+    public long getConnectTimeReal() {
+        return mConnectTimeReal;
+    }
+
+    /**
+     * Disconnect time in currentTimeMillis() format.
+     * The time when this Connection makes a transition into ENDED or FAIL.
+     * Returns 0 before then.
+     */
+    public abstract long getDisconnectTime();
+
+    /**
+     * Returns the number of milliseconds the call has been connected,
+     * or 0 if the call has never connected.
+     * If the call is still connected, then returns the elapsed
+     * time since connect.
+     */
+    public long getDurationMillis() {
+        if (mConnectTimeReal == 0) {
+            return 0;
+        } else if (mDuration == 0) {
+            return SystemClock.elapsedRealtime() - mConnectTimeReal;
+        } else {
+            return mDuration;
+        }
+    }
+
+    /**
+     * The time when this Connection last transitioned into HOLDING
+     * in elapsedRealtime() format.
+     * Returns 0, if it has never made a transition into HOLDING.
+     */
+    public long getHoldingStartTime() {
+        return mHoldingStartTime;
+    }
+
+    /**
+     * If this connection is HOLDING, return the number of milliseconds
+     * that it has been on hold for (approximately).
+     * If this connection is in any other state, return 0.
+     */
+
+    public abstract long getHoldDurationMillis();
+
+    /**
+     * Returns call disconnect cause. Values are defined in
+     * {@link android.telephony.DisconnectCause}. If the call is not yet
+     * disconnected, NOT_DISCONNECTED is returned.
+     */
+    public int getDisconnectCause() {
+        return mCause;
+    }
+
+    /**
+     * Returns a string disconnect cause which is from vendor.
+     * Vendors may use this string to explain the underline causes of failed calls.
+     * There is no guarantee that it is non-null nor it'll have meaningful stable values.
+     * Only use it when getDisconnectCause() returns a value that is not specific enough, like
+     * ERROR_UNSPECIFIED.
+     */
+    public abstract String getVendorDisconnectCause();
+
+    /**
+     * Returns true of this connection originated elsewhere
+     * ("MT" or mobile terminated; another party called this terminal)
+     * or false if this call originated here (MO or mobile originated).
+     */
+    public boolean isIncoming() {
+        return mIsIncoming;
+    }
+
+    /**
+     * Sets whether this call is an incoming call or not.
+     * @param isIncoming {@code true} if the call is an incoming call, {@code false} if it is an
+     *                               outgoing call.
+     */
+    public void setIsIncoming(boolean isIncoming) {
+        mIsIncoming = isIncoming;
+    }
+
+    /**
+     * If this Connection is connected, then it is associated with
+     * a Call.
+     *
+     * Returns getCall().getState() or Call.State.IDLE if not
+     * connected
+     */
+    public Call.State getState() {
+        Call c;
+
+        c = getCall();
+
+        if (c == null) {
+            return Call.State.IDLE;
+        } else {
+            return c.getState();
+        }
+    }
+
+    /**
+     * If this connection went through handover return the state of the
+     * call that contained this connection before handover.
+     */
+    public Call.State getStateBeforeHandover() {
+        return mPreHandoverState;
+   }
+
+    /**
+     * Get the details of conference participants. Expected to be
+     * overwritten by the Connection subclasses.
+     */
+    public List<ConferenceParticipant> getConferenceParticipants() {
+        Call c;
+
+        c = getCall();
+
+        if (c == null) {
+            return null;
+        } else {
+            return c.getConferenceParticipants();
+        }
+    }
+
+    /**
+     * isAlive()
+     *
+     * @return true if the connection isn't disconnected
+     * (could be active, holding, ringing, dialing, etc)
+     */
+    public boolean
+    isAlive() {
+        return getState().isAlive();
+    }
+
+    /**
+     * Returns true if Connection is connected and is INCOMING or WAITING
+     */
+    public boolean
+    isRinging() {
+        return getState().isRinging();
+    }
+
+    /**
+     *
+     * @return the userdata set in setUserData()
+     */
+    public Object getUserData() {
+        return mUserData;
+    }
+
+    /**
+     *
+     * @param userdata user can store an any userdata in the Connection object.
+     */
+    public void setUserData(Object userdata) {
+        mUserData = userdata;
+    }
+
+    /**
+     * Hangup individual Connection
+     */
+    public abstract void hangup() throws CallStateException;
+
+    /**
+     * Separate this call from its owner Call and assigns it to a new Call
+     * (eg if it is currently part of a Conference call
+     * TODO: Throw exception? Does GSM require error display on failure here?
+     */
+    public abstract void separate() throws CallStateException;
+
+    public enum PostDialState {
+        NOT_STARTED,    /* The post dial string playback hasn't
+                           been started, or this call is not yet
+                           connected, or this is an incoming call */
+        STARTED,        /* The post dial string playback has begun */
+        WAIT,           /* The post dial string playback is waiting for a
+                           call to proceedAfterWaitChar() */
+        WILD,           /* The post dial string playback is waiting for a
+                           call to proceedAfterWildChar() */
+        COMPLETE,       /* The post dial string playback is complete */
+        CANCELLED,       /* The post dial string playback was cancelled
+                           with cancelPostDial() */
+        PAUSE           /* The post dial string playback is pausing for a
+                           call to processNextPostDialChar*/
+    }
+
+    public void clearUserData(){
+        mUserData = null;
+    }
+
+    public final void addPostDialListener(PostDialListener listener) {
+        if (!mPostDialListeners.contains(listener)) {
+            mPostDialListeners.add(listener);
+        }
+    }
+
+    public final void removePostDialListener(PostDialListener listener) {
+        mPostDialListeners.remove(listener);
+    }
+
+    protected final void clearPostDialListeners() {
+        if (mPostDialListeners != null) {
+            mPostDialListeners.clear();
+        }
+    }
+
+    protected final void notifyPostDialListeners() {
+        if (getPostDialState() == PostDialState.WAIT) {
+            for (PostDialListener listener : new ArrayList<>(mPostDialListeners)) {
+                listener.onPostDialWait();
+            }
+        }
+    }
+
+    protected final void notifyPostDialListenersNextChar(char c) {
+        for (PostDialListener listener : new ArrayList<>(mPostDialListeners)) {
+            listener.onPostDialChar(c);
+        }
+    }
+
+    public PostDialState getPostDialState() {
+        return mPostDialState;
+    }
+
+    /**
+     * Returns the portion of the post dial string that has not
+     * yet been dialed, or "" if none
+     */
+    public String getRemainingPostDialString() {
+        if (mPostDialState == PostDialState.CANCELLED
+                || mPostDialState == PostDialState.COMPLETE
+                || mPostDialString == null
+                || mPostDialString.length() <= mNextPostDialChar) {
+            return "";
+        }
+
+        return mPostDialString.substring(mNextPostDialChar);
+    }
+
+    /**
+     * See Phone.setOnPostDialWaitCharacter()
+     */
+
+    public abstract void proceedAfterWaitChar();
+
+    /**
+     * See Phone.setOnPostDialWildCharacter()
+     */
+    public abstract void proceedAfterWildChar(String str);
+    /**
+     * Cancel any post
+     */
+    public abstract void cancelPostDial();
+
+    /** Called when the connection has been disconnected */
+    public boolean onDisconnect(int cause) {
+        return false;
+    }
+
+    /**
+     * Returns the caller id presentation type for incoming and waiting calls
+     * @return one of PRESENTATION_*
+     */
+    public abstract int getNumberPresentation();
+
+    /**
+     * Returns the User to User Signaling (UUS) information associated with
+     * incoming and waiting calls
+     * @return UUSInfo containing the UUS userdata.
+     */
+    public abstract UUSInfo getUUSInfo();
+
+    /**
+     * Returns the CallFail reason provided by the RIL with the result of
+     * RIL_REQUEST_LAST_CALL_FAIL_CAUSE
+     */
+    public abstract int getPreciseDisconnectCause();
+
+    /**
+     * Returns the original Connection instance associated with
+     * this Connection
+     */
+    public Connection getOrigConnection() {
+        return mOrigConnection;
+    }
+
+    /**
+     * Returns whether the original ImsPhoneConnection was a member
+     * of a conference call
+     * @return valid only when getOrigConnection() is not null
+     */
+    public abstract boolean isMultiparty();
+
+    /**
+     * Applicable only for IMS Call. Determines if this call is the origin of the conference call
+     * (i.e. {@code #isConferenceHost()} is {@code true}), or if it is a member of a conference
+     * hosted on another device.
+     *
+     * @return {@code true} if this call is the origin of the conference call it is a member of,
+     *      {@code false} otherwise.
+     */
+    public boolean isConferenceHost() {
+        return false;
+    }
+
+    /**
+     * Applicable only for IMS Call. Determines if a connection is a member of a conference hosted
+     * on another device.
+     *
+     * @return {@code true} if the connection is a member of a conference hosted on another device.
+     */
+    public boolean isMemberOfPeerConference() {
+        return false;
+    }
+
+    public void migrateFrom(Connection c) {
+        if (c == null) return;
+        mListeners = c.mListeners;
+        mDialString = c.getOrigDialString();
+        mCreateTime = c.getCreateTime();
+        mConnectTime = c.getConnectTime();
+        mConnectTimeReal = c.getConnectTimeReal();
+        mHoldingStartTime = c.getHoldingStartTime();
+        mOrigConnection = c.getOrigConnection();
+        mPostDialString = c.mPostDialString;
+        mNextPostDialChar = c.mNextPostDialChar;
+        mPostDialState = c.mPostDialState;
+    }
+
+    /**
+     * Assign a listener to be notified of state changes.
+     *
+     * @param listener A listener.
+     */
+    public final void addListener(Listener listener) {
+        mListeners.add(listener);
+    }
+
+    /**
+     * Removes a listener.
+     *
+     * @param listener A listener.
+     */
+    public final void removeListener(Listener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Returns the current video state of the connection.
+     *
+     * @return The video state of the connection.
+     */
+    public int getVideoState() {
+        return mVideoState;
+    }
+
+    /**
+     * Called to get Connection capabilities.Returns Capabilities bitmask.
+     * @See Connection.Capability.
+     */
+    public int getConnectionCapabilities() {
+        return mConnectionCapabilities;
+    }
+
+    /**
+     * @return {@code} true if the connection has the specified capabilities.
+     */
+    public boolean hasCapabilities(int connectionCapabilities) {
+        return (mConnectionCapabilities & connectionCapabilities) == connectionCapabilities;
+    }
+
+    /**
+     * Applies a capability to a capabilities bit-mask.
+     *
+     * @param capabilities The capabilities bit-mask.
+     * @param capability The capability to apply.
+     * @return The capabilities bit-mask with the capability applied.
+     */
+    public static int addCapability(int capabilities, int capability) {
+        return capabilities | capability;
+    }
+
+    /**
+     * Removes a capability to a capabilities bit-mask.
+     *
+     * @param capabilities The capabilities bit-mask.
+     * @param capability The capability to remove.
+     * @return The capabilities bit-mask with the capability removed.
+     */
+    public static int removeCapability(int capabilities, int capability) {
+        return capabilities & ~capability;
+    }
+
+    /**
+     * Returns whether the connection is using a wifi network.
+     *
+     * @return {@code True} if the connection is using a wifi network.
+     */
+    public boolean isWifi() {
+        return mIsWifi;
+    }
+
+    /**
+     * Returns whether the connection uses voip audio mode
+     *
+     * @return {@code True} if the connection uses voip audio mode
+     */
+    public boolean getAudioModeIsVoip() {
+        return mAudioModeIsVoip;
+    }
+
+    /**
+     * Returns the {@link android.telecom.Connection.VideoProvider} for the connection.
+     *
+     * @return The {@link android.telecom.Connection.VideoProvider}.
+     */
+    public android.telecom.Connection.VideoProvider getVideoProvider() {
+        return mVideoProvider;
+    }
+
+    /**
+     * Returns the audio-quality for the connection.
+     *
+     * @return The audio quality for the connection.
+     */
+    public int getAudioQuality() {
+        return mAudioQuality;
+    }
+
+
+    /**
+     * Returns the current call substate of the connection.
+     *
+     * @return The call substate of the connection.
+     */
+    public int getCallSubstate() {
+        return mCallSubstate;
+    }
+
+
+    /**
+     * Sets the videoState for the current connection and reports the changes to all listeners.
+     * Valid video states are defined in {@link android.telecom.VideoProfile}.
+     *
+     * @return The video state.
+     */
+    public void setVideoState(int videoState) {
+        mVideoState = videoState;
+        for (Listener l : mListeners) {
+            l.onVideoStateChanged(mVideoState);
+        }
+    }
+
+    /**
+     * Called to set Connection capabilities.  This will take Capabilities bitmask as input which is
+     * converted from Capabilities constants.
+     *
+     * @See Connection.Capability.
+     * @param capabilities The Capabilities bitmask.
+     */
+    public void setConnectionCapabilities(int capabilities) {
+        if (mConnectionCapabilities != capabilities) {
+            mConnectionCapabilities = capabilities;
+            for (Listener l : mListeners) {
+                l.onConnectionCapabilitiesChanged(mConnectionCapabilities);
+            }
+        }
+    }
+
+    /**
+     * Sets whether a wifi network is used for the connection.
+     *
+     * @param isWifi {@code True} if wifi is being used.
+     */
+    public void setWifi(boolean isWifi) {
+        mIsWifi = isWifi;
+        for (Listener l : mListeners) {
+            l.onWifiChanged(mIsWifi);
+        }
+    }
+
+    /**
+     * Set the voip audio mode for the connection
+     *
+     * @param isVoip {@code True} if voip audio mode is being used.
+     */
+    public void setAudioModeIsVoip(boolean isVoip) {
+        mAudioModeIsVoip = isVoip;
+    }
+
+    /**
+     * Set the audio quality for the connection.
+     *
+     * @param audioQuality The audio quality.
+     */
+    public void setAudioQuality(int audioQuality) {
+        mAudioQuality = audioQuality;
+        for (Listener l : mListeners) {
+            l.onAudioQualityChanged(mAudioQuality);
+        }
+    }
+
+    /**
+     * Notifies listeners that connection extras has changed.
+     * @param extras New connection extras. This Bundle will be cloned to ensure that any concurrent
+     * modifications to the extras Bundle do not affect Bundle operations in the onExtrasChanged
+     * listeners.
+     */
+    public void setConnectionExtras(Bundle extras) {
+        if (extras != null) {
+            mExtras = new Bundle(extras);
+        } else {
+            mExtras = null;
+        }
+
+        for (Listener l : mListeners) {
+            l.onExtrasChanged(mExtras);
+        }
+    }
+
+    /**
+     * Retrieves the current connection extras.
+     * @return the connection extras.
+     */
+    public Bundle getConnectionExtras() {
+        return mExtras == null ? null : new Bundle(mExtras);
+    }
+
+    /**
+     * @return {@code true} if answering the call will cause the current active call to be
+     *      disconnected, {@code false} otherwise.
+     */
+    public boolean isActiveCallDisconnectedOnAnswer() {
+        return mAnsweringDisconnectsActiveCall;
+    }
+
+    /**
+     * Sets whether answering this call will cause the active call to be disconnected.
+     * <p>
+     * Should only be set {@code true} if there is an active call and this call is ringing.
+     *
+     * @param answeringDisconnectsActiveCall {@code true} if answering the call will call the active
+     *      call to be disconnected.
+     */
+    public void setActiveCallDisconnectedOnAnswer(boolean answeringDisconnectsActiveCall) {
+        mAnsweringDisconnectsActiveCall = answeringDisconnectsActiveCall;
+    }
+
+    public boolean shouldAllowAddCallDuringVideoCall() {
+        return mAllowAddCallDuringVideoCall;
+    }
+
+    public void setAllowAddCallDuringVideoCall(boolean allowAddCallDuringVideoCall) {
+        mAllowAddCallDuringVideoCall = allowAddCallDuringVideoCall;
+    }
+
+    /**
+     * Sets whether the connection is the result of an external call which was pulled to the local
+     * device.
+     *
+     * @param isPulledCall {@code true} if this connection is the result of pulling an external call
+     *      to the local device.
+     */
+    public void setIsPulledCall(boolean isPulledCall) {
+        mIsPulledCall = isPulledCall;
+    }
+
+    public boolean isPulledCall() {
+        return mIsPulledCall;
+    }
+
+    /**
+     * For an external call which is being pulled (e.g. {@link #isPulledCall()} is {@code true}),
+     * sets the dialog Id for the external call.  Used to handle failures to pull a call so that the
+     * pulled call can be reconciled with its original external connection.
+     *
+     * @param pulledDialogId The dialog id associated with a pulled call.
+     */
+    public void setPulledDialogId(int pulledDialogId) {
+        mPulledDialogId = pulledDialogId;
+    }
+
+    public int getPulledDialogId() {
+        return mPulledDialogId;
+    }
+
+    /**
+     * Sets the call substate for the current connection and reports the changes to all listeners.
+     * Valid call substates are defined in {@link android.telecom.Connection}.
+     *
+     * @return The call substate.
+     */
+    public void setCallSubstate(int callSubstate) {
+        mCallSubstate = callSubstate;
+        for (Listener l : mListeners) {
+            l.onCallSubstateChanged(mCallSubstate);
+        }
+    }
+
+    /**
+     * Sets the {@link android.telecom.Connection.VideoProvider} for the connection.
+     *
+     * @param videoProvider The video call provider.
+     */
+    public void setVideoProvider(android.telecom.Connection.VideoProvider videoProvider) {
+        mVideoProvider = videoProvider;
+        for (Listener l : mListeners) {
+            l.onVideoProviderChanged(mVideoProvider);
+        }
+    }
+
+    public void setConverted(String oriNumber) {
+        mNumberConverted = true;
+        mConvertedNumber = mAddress;
+        mAddress = oriNumber;
+        mDialString = oriNumber;
+    }
+
+    /**
+     * Notifies listeners of a change to conference participant(s).
+     *
+     * @param conferenceParticipants The participant(s).
+     */
+    public void updateConferenceParticipants(List<ConferenceParticipant> conferenceParticipants) {
+        for (Listener l : mListeners) {
+            l.onConferenceParticipantsChanged(conferenceParticipants);
+        }
+    }
+
+    /**
+     * Notifies listeners of a change to the multiparty state of the connection.
+     *
+     * @param isMultiparty The participant(s).
+     */
+    public void updateMultipartyState(boolean isMultiparty) {
+        for (Listener l : mListeners) {
+            l.onMultipartyStateChanged(isMultiparty);
+        }
+    }
+
+    /**
+     * Notifies listeners of a failure in merging this connection with the background connection.
+     */
+    public void onConferenceMergeFailed() {
+        for (Listener l : mListeners) {
+            l.onConferenceMergedFailed();
+        }
+    }
+
+    /**
+     * Notifies that the underlying phone has exited ECM mode.
+     */
+    public void onExitedEcmMode() {
+        for (Listener l : mListeners) {
+            l.onExitedEcmMode();
+        }
+    }
+
+    /**
+     * Notifies the connection that a call to {@link #pullExternalCall()} has failed to pull the
+     * call to the local device.
+     *
+     * @param externalConnection The original
+     *      {@link com.android.internal.telephony.imsphone.ImsExternalConnection} from which the
+     *      pull was initiated.
+     */
+    public void onCallPullFailed(Connection externalConnection) {
+        for (Listener l : mListeners) {
+            l.onCallPullFailed(externalConnection);
+        }
+    }
+
+    /**
+     * Notifies the connection that there was a failure while handing over to WIFI.
+     */
+    public void onHandoverToWifiFailed() {
+        for (Listener l : mListeners) {
+            l.onHandoverToWifiFailed();
+        }
+    }
+
+    /**
+     * Notifies the connection of a connection event.
+     */
+    public void onConnectionEvent(String event, Bundle extras) {
+        for (Listener l : mListeners) {
+            l.onConnectionEvent(event, extras);
+        }
+    }
+
+    /**
+     * Notifies this Connection of a request to disconnect a participant of the conference managed
+     * by the connection.
+     *
+     * @param endpoint the {@link Uri} of the participant to disconnect.
+     */
+    public void onDisconnectConferenceParticipant(Uri endpoint) {
+    }
+
+    /**
+     * Called by a {@link android.telecom.Connection} to indicate that this call should be pulled
+     * to the local device.
+     */
+    public void pullExternalCall() {
+    }
+
+    public void onRttModifyRequestReceived() {
+        for (Listener l : mListeners) {
+            l.onRttModifyRequestReceived();
+        }
+    }
+
+    public void onRttModifyResponseReceived(int status) {
+        for (Listener l : mListeners) {
+            l.onRttModifyResponseReceived(status);
+        }
+    }
+
+    /**
+     *
+     */
+    public int getPhoneType() {
+        return mPhoneType;
+    }
+
+    /**
+     * Build a human representation of a connection instance, suitable for debugging.
+     * Don't log personal stuff unless in debug mode.
+     * @return a string representing the internal state of this connection.
+     */
+    public String toString() {
+        StringBuilder str = new StringBuilder(128);
+
+        str.append(" callId: " + getTelecomCallId());
+        str.append(" isExternal: " + (((mConnectionCapabilities & Capability.IS_EXTERNAL_CONNECTION)
+                == Capability.IS_EXTERNAL_CONNECTION) ? "Y" : "N"));
+        if (Rlog.isLoggable(LOG_TAG, Log.DEBUG)) {
+            str.append("addr: " + getAddress())
+                    .append(" pres.: " + getNumberPresentation())
+                    .append(" dial: " + getOrigDialString())
+                    .append(" postdial: " + getRemainingPostDialString())
+                    .append(" cnap name: " + getCnapName())
+                    .append("(" + getCnapNamePresentation() + ")");
+        }
+        str.append(" incoming: " + isIncoming())
+                .append(" state: " + getState())
+                .append(" post dial state: " + getPostDialState());
+        return str.toString();
+    }
+}
diff --git a/com/android/internal/telephony/DcParamObject.java b/com/android/internal/telephony/DcParamObject.java
new file mode 100644
index 0000000..139939c
--- /dev/null
+++ b/com/android/internal/telephony/DcParamObject.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+public class DcParamObject implements Parcelable {
+
+    private int mSubId;
+
+    public DcParamObject(int subId) {
+        mSubId = subId;
+    }
+
+    public DcParamObject(Parcel in) {
+        readFromParcel(in);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mSubId);
+    }
+
+    private void readFromParcel(Parcel in) {
+        mSubId = in.readInt();
+    }
+
+    public static final Parcelable.Creator<DcParamObject> CREATOR = new Parcelable.Creator<DcParamObject>() {
+        public DcParamObject createFromParcel(Parcel in) {
+            return new DcParamObject(in);
+        }
+        public DcParamObject[] newArray(int size) {
+            return new DcParamObject[size];
+        }
+    };
+
+    public int getSubId() {
+        return mSubId;
+    }
+}
diff --git a/com/android/internal/telephony/DctConstants.java b/com/android/internal/telephony/DctConstants.java
new file mode 100644
index 0000000..91032f3
--- /dev/null
+++ b/com/android/internal/telephony/DctConstants.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony;
+
+import com.android.internal.util.Protocol;
+
+/**
+ * @hide
+ */
+public class DctConstants {
+    /**
+     * IDLE: ready to start data connection setup, default state
+     * CONNECTING: state of issued startPppd() but not finish yet
+     * SCANNING: data connection fails with one apn but other apns are available
+     *           ready to start data connection on other apns (before INITING)
+     * CONNECTED: IP connection is setup
+     * DISCONNECTING: Connection.disconnect() has been called, but PDP
+     *                context is not yet deactivated
+     * FAILED: data connection fail for all apns settings
+     * RETRYING: data connection failed but we're going to retry.
+     *
+     * getDataConnectionState() maps State to DataState
+     *      FAILED or IDLE : DISCONNECTED
+     *      RETRYING or CONNECTING or SCANNING: CONNECTING
+     *      CONNECTED : CONNECTED or DISCONNECTING
+     */
+    public enum State {
+        IDLE,
+        CONNECTING,
+        SCANNING,
+        CONNECTED,
+        DISCONNECTING,
+        FAILED,
+        RETRYING        // After moving retry manager to ApnContext, we'll never enter this state!
+                        // Todo: Remove this state and other places that use this state and then
+                        // rename SCANNING to RETRYING.
+    }
+
+    public enum Activity {
+        NONE,
+        DATAIN,
+        DATAOUT,
+        DATAINANDOUT,
+        DORMANT
+    }
+
+    /***** Event Codes *****/
+    public static final int BASE = Protocol.BASE_DATA_CONNECTION_TRACKER;
+    public static final int EVENT_DATA_SETUP_COMPLETE = BASE + 0;
+    public static final int EVENT_RADIO_AVAILABLE = BASE + 1;
+    public static final int EVENT_RECORDS_LOADED = BASE + 2;
+    public static final int EVENT_TRY_SETUP_DATA = BASE + 3;
+    public static final int EVENT_DATA_STATE_CHANGED = BASE + 4;
+    public static final int EVENT_POLL_PDP = BASE + 5;
+    public static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE = BASE + 6;
+    public static final int EVENT_VOICE_CALL_STARTED = BASE + 7;
+    public static final int EVENT_VOICE_CALL_ENDED = BASE + 8;
+    public static final int EVENT_DATA_CONNECTION_DETACHED = BASE + 9;
+    public static final int EVENT_LINK_STATE_CHANGED = BASE + 10;
+    public static final int EVENT_ROAMING_ON = BASE + 11;
+    public static final int EVENT_ROAMING_OFF = BASE + 12;
+    public static final int EVENT_ENABLE_NEW_APN = BASE + 13;
+    public static final int EVENT_RESTORE_DEFAULT_APN = BASE + 14;
+    public static final int EVENT_DISCONNECT_DONE = BASE + 15;
+    public static final int EVENT_DATA_CONNECTION_ATTACHED = BASE + 16;
+    public static final int EVENT_DATA_STALL_ALARM = BASE + 17;
+    public static final int EVENT_DO_RECOVERY = BASE + 18;
+    public static final int EVENT_APN_CHANGED = BASE + 19;
+    public static final int EVENT_CDMA_DATA_DETACHED = BASE + 20;
+    public static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = BASE + 21;
+    public static final int EVENT_PS_RESTRICT_ENABLED = BASE + 22;
+    public static final int EVENT_PS_RESTRICT_DISABLED = BASE + 23;
+    public static final int EVENT_CLEAN_UP_CONNECTION = BASE + 24;
+    public static final int EVENT_CDMA_OTA_PROVISION = BASE + 25;
+    public static final int EVENT_RESTART_RADIO = BASE + 26;
+    public static final int EVENT_SET_INTERNAL_DATA_ENABLE = BASE + 27;
+    public static final int EVENT_RESET_DONE = BASE + 28;
+    public static final int EVENT_CLEAN_UP_ALL_CONNECTIONS = BASE + 29;
+    public static final int CMD_SET_USER_DATA_ENABLE = BASE + 30;
+    public static final int CMD_SET_DEPENDENCY_MET = BASE + 31;
+    public static final int CMD_SET_POLICY_DATA_ENABLE = BASE + 32;
+    public static final int EVENT_ICC_CHANGED = BASE + 33;
+    public static final int EVENT_DISCONNECT_DC_RETRYING = BASE + 34;
+    public static final int EVENT_DATA_SETUP_COMPLETE_ERROR = BASE + 35;
+    public static final int CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA = BASE + 36;
+    public static final int CMD_ENABLE_MOBILE_PROVISIONING = BASE + 37;
+    public static final int CMD_IS_PROVISIONING_APN = BASE + 38;
+    public static final int EVENT_PROVISIONING_APN_ALARM = BASE + 39;
+    public static final int CMD_NET_STAT_POLL = BASE + 40;
+    public static final int EVENT_DATA_RAT_CHANGED = BASE + 41;
+    public static final int CMD_CLEAR_PROVISIONING_SPINNER = BASE + 42;
+    public static final int EVENT_DEVICE_PROVISIONED_CHANGE = BASE + 43;
+    public static final int EVENT_REDIRECTION_DETECTED = BASE + 44;
+    public static final int EVENT_PCO_DATA_RECEIVED = BASE + 45;
+    public static final int EVENT_SET_CARRIER_DATA_ENABLED = BASE + 46;
+    public static final int EVENT_DATA_RECONNECT = BASE + 47;
+    public static final int EVENT_ROAMING_SETTING_CHANGE = BASE + 48;
+
+    /***** Constants *****/
+
+    public static final int APN_INVALID_ID = -1;
+    public static final int APN_DEFAULT_ID = 0;
+    public static final int APN_MMS_ID = 1;
+    public static final int APN_SUPL_ID = 2;
+    public static final int APN_DUN_ID = 3;
+    public static final int APN_HIPRI_ID = 4;
+    public static final int APN_IMS_ID = 5;
+    public static final int APN_FOTA_ID = 6;
+    public static final int APN_CBS_ID = 7;
+    public static final int APN_IA_ID = 8;
+    public static final int APN_EMERGENCY_ID = 9;
+    public static final int APN_NUM_TYPES = 10;
+
+    public static final int INVALID = -1;
+    public static final int DISABLED = 0;
+    public static final int ENABLED = 1;
+
+    public static final String APN_TYPE_KEY = "apnType";
+    public static final String PROVISIONING_URL_KEY = "provisioningUrl";
+}
diff --git a/com/android/internal/telephony/DebugService.java b/com/android/internal/telephony/DebugService.java
new file mode 100644
index 0000000..28a2094
--- /dev/null
+++ b/com/android/internal/telephony/DebugService.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony;
+
+import android.telephony.Rlog;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A debug service that will dump telephony's state
+ *
+ * Currently this "Service" has a proxy in the phone app
+ * com.android.phone.TelephonyDebugService which actually
+ * invokes the dump method.
+ */
+public class DebugService {
+    private static String TAG = "DebugService";
+
+    /** Constructor */
+    public DebugService() {
+        log("DebugService:");
+    }
+
+    /**
+     * Dump the state of various objects, add calls to other objects as desired.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (args != null && args.length > 0) {
+            if (TextUtils.equals(args[0], "--metrics") ||
+                    TextUtils.equals(args[0], "--metricsproto"))
+            {
+                log("Collecting telephony metrics..");
+                TelephonyMetrics.getInstance().dump(fd, pw, args);
+                return;
+            }
+        }
+        log("Dump telephony.");
+        PhoneFactory.dump(fd, pw, args);
+    }
+
+    private static void log(String s) {
+        Rlog.d(TAG, "DebugService " + s);
+    }
+}
diff --git a/com/android/internal/telephony/DefaultPhoneNotifier.java b/com/android/internal/telephony/DefaultPhoneNotifier.java
new file mode 100644
index 0000000..c13e540
--- /dev/null
+++ b/com/android/internal/telephony/DefaultPhoneNotifier.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.CellInfo;
+import android.telephony.PreciseCallState;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VoLteServiceState;
+
+import java.util.List;
+
+/**
+ * broadcast intents
+ */
+public class DefaultPhoneNotifier implements PhoneNotifier {
+    private static final String LOG_TAG = "DefaultPhoneNotifier";
+    private static final boolean DBG = false; // STOPSHIP if true
+
+    protected ITelephonyRegistry mRegistry;
+
+    public DefaultPhoneNotifier() {
+        mRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(
+                    "telephony.registry"));
+    }
+
+    @Override
+    public void notifyPhoneState(Phone sender) {
+        Call ringingCall = sender.getRingingCall();
+        int subId = sender.getSubId();
+        int phoneId = sender.getPhoneId();
+        String incomingNumber = "";
+        if (ringingCall != null && ringingCall.getEarliestConnection() != null) {
+            incomingNumber = ringingCall.getEarliestConnection().getAddress();
+        }
+        try {
+            if (mRegistry != null) {
+                  mRegistry.notifyCallStateForPhoneId(phoneId, subId,
+                        PhoneConstantConversions.convertCallState(
+                            sender.getState()), incomingNumber);
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyServiceState(Phone sender) {
+        ServiceState ss = sender.getServiceState();
+        int phoneId = sender.getPhoneId();
+        int subId = sender.getSubId();
+
+        Rlog.d(LOG_TAG, "nofityServiceState: mRegistry=" + mRegistry + " ss=" + ss
+                + " sender=" + sender + " phondId=" + phoneId + " subId=" + subId);
+        if (ss == null) {
+            ss = new ServiceState();
+            ss.setStateOutOfService();
+        }
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyServiceStateForPhoneId(phoneId, subId, ss);
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifySignalStrength(Phone sender) {
+        int phoneId = sender.getPhoneId();
+        int subId = sender.getSubId();
+        if (DBG) {
+            // too chatty to log constantly
+            Rlog.d(LOG_TAG, "notifySignalStrength: mRegistry=" + mRegistry
+                    + " ss=" + sender.getSignalStrength() + " sender=" + sender);
+        }
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifySignalStrengthForPhoneId(phoneId, subId,
+                        sender.getSignalStrength());
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyMessageWaitingChanged(Phone sender) {
+        int phoneId = sender.getPhoneId();
+        int subId = sender.getSubId();
+
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyMessageWaitingChangedForPhoneId(phoneId, subId,
+                        sender.getMessageWaitingIndicator());
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyCallForwardingChanged(Phone sender) {
+        int subId = sender.getSubId();
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyCallForwardingChangedForSubscriber(subId,
+                        sender.getCallForwardingIndicator());
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyDataActivity(Phone sender) {
+        int subId = sender.getSubId();
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyDataActivityForSubscriber(subId,
+                        convertDataActivityState(sender.getDataActivityState()));
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyDataConnection(Phone sender, String reason, String apnType,
+            PhoneConstants.DataState state) {
+        doNotifyDataConnection(sender, reason, apnType, state);
+    }
+
+    private void doNotifyDataConnection(Phone sender, String reason, String apnType,
+            PhoneConstants.DataState state) {
+        int subId = sender.getSubId();
+        long dds = SubscriptionManager.getDefaultDataSubscriptionId();
+        if (DBG) log("subId = " + subId + ", DDS = " + dds);
+
+        // TODO
+        // use apnType as the key to which connection we're talking about.
+        // pass apnType back up to fetch particular for this one.
+        TelephonyManager telephony = TelephonyManager.getDefault();
+        LinkProperties linkProperties = null;
+        NetworkCapabilities networkCapabilities = null;
+        boolean roaming = false;
+
+        if (state == PhoneConstants.DataState.CONNECTED) {
+            linkProperties = sender.getLinkProperties(apnType);
+            networkCapabilities = sender.getNetworkCapabilities(apnType);
+        }
+        ServiceState ss = sender.getServiceState();
+        if (ss != null) roaming = ss.getDataRoaming();
+
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyDataConnectionForSubscriber(subId,
+                    PhoneConstantConversions.convertDataState(state),
+                        sender.isDataAllowed(), reason,
+                        sender.getActiveApnHost(apnType),
+                        apnType,
+                        linkProperties,
+                        networkCapabilities,
+                        ((telephony != null) ? telephony.getDataNetworkType(subId) :
+                                TelephonyManager.NETWORK_TYPE_UNKNOWN), roaming);
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyDataConnectionFailed(Phone sender, String reason, String apnType) {
+        int subId = sender.getSubId();
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyDataConnectionFailedForSubscriber(subId, reason, apnType);
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyCellLocation(Phone sender) {
+        int subId = sender.getSubId();
+        Bundle data = new Bundle();
+        sender.getCellLocation().fillInNotifierBundle(data);
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyCellLocationForSubscriber(subId, data);
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyCellInfo(Phone sender, List<CellInfo> cellInfo) {
+        int subId = sender.getSubId();
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyCellInfoForSubscriber(subId, cellInfo);
+            }
+        } catch (RemoteException ex) {
+
+        }
+    }
+
+    @Override
+    public void notifyOtaspChanged(Phone sender, int otaspMode) {
+        // FIXME: subId?
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyOtaspChanged(otaspMode);
+            }
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    public void notifyPreciseCallState(Phone sender) {
+        // FIXME: subId?
+        Call ringingCall = sender.getRingingCall();
+        Call foregroundCall = sender.getForegroundCall();
+        Call backgroundCall = sender.getBackgroundCall();
+        if (ringingCall != null && foregroundCall != null && backgroundCall != null) {
+            try {
+                mRegistry.notifyPreciseCallState(
+                        convertPreciseCallState(ringingCall.getState()),
+                        convertPreciseCallState(foregroundCall.getState()),
+                        convertPreciseCallState(backgroundCall.getState()));
+            } catch (RemoteException ex) {
+                // system process is dead
+            }
+        }
+    }
+
+    public void notifyDisconnectCause(int cause, int preciseCause) {
+        // FIXME: subId?
+        try {
+            mRegistry.notifyDisconnectCause(cause, preciseCause);
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    public void notifyPreciseDataConnectionFailed(Phone sender, String reason, String apnType,
+            String apn, String failCause) {
+        // FIXME: subId?
+        try {
+            mRegistry.notifyPreciseDataConnectionFailed(reason, apnType, apn, failCause);
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyVoLteServiceStateChanged(Phone sender, VoLteServiceState lteState) {
+        // FIXME: subID
+        try {
+            mRegistry.notifyVoLteServiceStateChanged(lteState);
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyDataActivationStateChanged(Phone sender, int activationState) {
+        try {
+            mRegistry.notifySimActivationStateChangedForPhoneId(sender.getPhoneId(),
+                    sender.getSubId(), PhoneConstants.SIM_ACTIVATION_TYPE_DATA, activationState);
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyVoiceActivationStateChanged(Phone sender, int activationState) {
+        try {
+            mRegistry.notifySimActivationStateChangedForPhoneId(sender.getPhoneId(),
+                    sender.getSubId(), PhoneConstants.SIM_ACTIVATION_TYPE_VOICE, activationState);
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    @Override
+    public void notifyOemHookRawEventForSubscriber(int subId, byte[] rawData) {
+        try {
+            mRegistry.notifyOemHookRawEventForSubscriber(subId, rawData);
+        } catch (RemoteException ex) {
+            // system process is dead
+        }
+    }
+
+    /**
+     * Convert the {@link Phone.DataActivityState} enum into the TelephonyManager.DATA_* constants
+     * for the public API.
+     */
+    public static int convertDataActivityState(Phone.DataActivityState state) {
+        switch (state) {
+            case DATAIN:
+                return TelephonyManager.DATA_ACTIVITY_IN;
+            case DATAOUT:
+                return TelephonyManager.DATA_ACTIVITY_OUT;
+            case DATAINANDOUT:
+                return TelephonyManager.DATA_ACTIVITY_INOUT;
+            case DORMANT:
+                return TelephonyManager.DATA_ACTIVITY_DORMANT;
+            default:
+                return TelephonyManager.DATA_ACTIVITY_NONE;
+        }
+    }
+
+    /**
+     * Convert the {@link Call.State} enum into the PreciseCallState.PRECISE_CALL_STATE_* constants
+     * for the public API.
+     */
+    public static int convertPreciseCallState(Call.State state) {
+        switch (state) {
+            case ACTIVE:
+                return PreciseCallState.PRECISE_CALL_STATE_ACTIVE;
+            case HOLDING:
+                return PreciseCallState.PRECISE_CALL_STATE_HOLDING;
+            case DIALING:
+                return PreciseCallState.PRECISE_CALL_STATE_DIALING;
+            case ALERTING:
+                return PreciseCallState.PRECISE_CALL_STATE_ALERTING;
+            case INCOMING:
+                return PreciseCallState.PRECISE_CALL_STATE_INCOMING;
+            case WAITING:
+                return PreciseCallState.PRECISE_CALL_STATE_WAITING;
+            case DISCONNECTED:
+                return PreciseCallState.PRECISE_CALL_STATE_DISCONNECTED;
+            case DISCONNECTING:
+                return PreciseCallState.PRECISE_CALL_STATE_DISCONNECTING;
+            default:
+                return PreciseCallState.PRECISE_CALL_STATE_IDLE;
+        }
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/DeviceStateMonitor.java b/com/android/internal/telephony/DeviceStateMonitor.java
new file mode 100644
index 0000000..73ab075
--- /dev/null
+++ b/com/android/internal/telephony/DeviceStateMonitor.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import static android.hardware.radio.V1_0.DeviceStateType.CHARGING_STATE;
+import static android.hardware.radio.V1_0.DeviceStateType.LOW_DATA_EXPECTED;
+import static android.hardware.radio.V1_0.DeviceStateType.POWER_SAVE_MODE;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.display.DisplayManager;
+import android.hardware.radio.V1_0.IndicationFilter;
+import android.net.ConnectivityManager;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.telephony.Rlog;
+import android.util.LocalLog;
+import android.view.Display;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * The device state monitor monitors the device state such as charging state, power saving sate,
+ * and then passes down the information to the radio modem for the modem to perform its own
+ * proprietary power saving strategy. Device state monitor also turns off the unsolicited
+ * response from the modem when the device does not need to receive it, for example, device's
+ * screen is off and does not have activities like tethering, remote display, etc...This effectively
+ * prevents the CPU from waking up by those unnecessary unsolicited responses such as signal
+ * strength update.
+ */
+public class DeviceStateMonitor extends Handler {
+    protected static final boolean DBG = false;      /* STOPSHIP if true */
+    protected static final String TAG = DeviceStateMonitor.class.getSimpleName();
+
+    private static final int EVENT_RIL_CONNECTED                = 0;
+    private static final int EVENT_SCREEN_STATE_CHANGED         = 1;
+    private static final int EVENT_POWER_SAVE_MODE_CHANGED      = 2;
+    private static final int EVENT_CHARGING_STATE_CHANGED       = 3;
+    private static final int EVENT_TETHERING_STATE_CHANGED      = 4;
+
+    private final Phone mPhone;
+
+    private final LocalLog mLocalLog = new LocalLog(100);
+
+    /**
+     * Flag for wifi/usb/bluetooth tethering turned on or not
+     */
+    private boolean mIsTetheringOn;
+
+    /**
+     * Screen state provided by Display Manager. True indicates one of the screen is on, otherwise
+     * all off.
+     */
+    private boolean mIsScreenOn;
+
+    /**
+     * Indicating the device is plugged in and is supplying sufficient power that the battery level
+     * is going up (or the battery is fully charged). See BatteryManager.isCharging() for the
+     * details
+     */
+    private boolean mIsCharging;
+
+    /**
+     * Flag for device power save mode. See PowerManager.isPowerSaveMode() for the details.
+     * Note that it is not possible both mIsCharging and mIsPowerSaveOn are true at the same time.
+     * The system will automatically end power save mode when the device starts charging.
+     */
+    private boolean mIsPowerSaveOn;
+
+    /**
+     * Low data expected mode. True indicates low data traffic is expected, for example, when the
+     * device is idle (e.g. screen is off and not doing tethering in the background). Note this
+     * doesn't mean no data is expected.
+     */
+    private boolean mIsLowDataExpected;
+
+    /**
+     * The unsolicited response filter. See IndicationFilter defined in types.hal for the definition
+     * of each bit.
+     */
+    private int mUnsolicitedResponseFilter = IndicationFilter.ALL;
+
+    private final DisplayManager.DisplayListener mDisplayListener =
+            new DisplayManager.DisplayListener() {
+                @Override
+                public void onDisplayAdded(int displayId) { }
+
+                @Override
+                public void onDisplayRemoved(int displayId) { }
+
+                @Override
+                public void onDisplayChanged(int displayId) {
+                    boolean screenOn = isScreenOn();
+                    Message msg = obtainMessage(EVENT_SCREEN_STATE_CHANGED);
+                    msg.arg1 = screenOn ? 1 : 0;
+                    sendMessage(msg);
+                }
+            };
+
+    /**
+     * Device state broadcast receiver
+     */
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            log("received: " + intent, true);
+
+            Message msg;
+            switch (intent.getAction()) {
+                case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED:
+                    msg = obtainMessage(EVENT_POWER_SAVE_MODE_CHANGED);
+                    msg.arg1 = isPowerSaveModeOn() ? 1 : 0;
+                    log("Power Save mode " + ((msg.arg1 == 1) ? "on" : "off"), true);
+                    break;
+                case BatteryManager.ACTION_CHARGING:
+                    msg = obtainMessage(EVENT_CHARGING_STATE_CHANGED);
+                    msg.arg1 = 1;   // charging
+                    break;
+                case BatteryManager.ACTION_DISCHARGING:
+                    msg = obtainMessage(EVENT_CHARGING_STATE_CHANGED);
+                    msg.arg1 = 0;   // not charging
+                    break;
+                case ConnectivityManager.ACTION_TETHER_STATE_CHANGED:
+                    ArrayList<String> activeTetherIfaces = intent.getStringArrayListExtra(
+                            ConnectivityManager.EXTRA_ACTIVE_TETHER);
+
+                    boolean isTetheringOn = activeTetherIfaces != null
+                            && activeTetherIfaces.size() > 0;
+                    log("Tethering " + (isTetheringOn ? "on" : "off"), true);
+                    msg = obtainMessage(EVENT_TETHERING_STATE_CHANGED);
+                    msg.arg1 = isTetheringOn ? 1 : 0;
+                    break;
+                default:
+                    log("Unexpected broadcast intent: " + intent, false);
+                    return;
+            }
+            sendMessage(msg);
+        }
+    };
+
+    /**
+     * Device state monitor constructor. Note that each phone object should have its own device
+     * state monitor, meaning there will be two device monitors on the multi-sim device.
+     *
+     * @param phone Phone object
+     */
+    public DeviceStateMonitor(Phone phone) {
+        mPhone = phone;
+        DisplayManager dm = (DisplayManager) phone.getContext().getSystemService(
+                Context.DISPLAY_SERVICE);
+        dm.registerDisplayListener(mDisplayListener, null);
+
+        mIsPowerSaveOn = isPowerSaveModeOn();
+        mIsCharging = isDeviceCharging();
+        mIsScreenOn = isScreenOn();
+        // Assuming tethering is always off after boot up.
+        mIsTetheringOn = false;
+        mIsLowDataExpected = false;
+
+        log("DeviceStateMonitor mIsPowerSaveOn=" + mIsPowerSaveOn + ",mIsScreenOn="
+                + mIsScreenOn + ",mIsCharging=" + mIsCharging, false);
+
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+        filter.addAction(BatteryManager.ACTION_CHARGING);
+        filter.addAction(BatteryManager.ACTION_DISCHARGING);
+        filter.addAction(ConnectivityManager.ACTION_TETHER_STATE_CHANGED);
+        mPhone.getContext().registerReceiver(mBroadcastReceiver, filter, null, mPhone);
+
+        mPhone.mCi.registerForRilConnected(this, EVENT_RIL_CONNECTED, null);
+    }
+
+    /**
+     * @return True if low data is expected
+     */
+    private boolean isLowDataExpected() {
+        return mIsPowerSaveOn || (!mIsCharging && !mIsTetheringOn && !mIsScreenOn);
+    }
+
+    /**
+     * @return True if signal strength update should be turned off.
+     */
+    private boolean shouldTurnOffSignalStrength() {
+        return mIsPowerSaveOn || (!mIsCharging && !mIsScreenOn);
+    }
+
+    /**
+     * @return True if full network update should be turned off. Only significant changes will
+     * trigger the network update unsolicited response.
+     */
+    private boolean shouldTurnOffFullNetworkUpdate() {
+        return mIsPowerSaveOn || (!mIsCharging && !mIsScreenOn && !mIsTetheringOn);
+    }
+
+    /**
+     * @return True if data dormancy status update should be turned off.
+     */
+    private boolean shouldTurnOffDormancyUpdate() {
+        return mIsPowerSaveOn || (!mIsCharging && !mIsTetheringOn && !mIsScreenOn);
+    }
+
+    /**
+     * Message handler
+     *
+     * @param msg The message
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        log("handleMessage msg=" + msg, false);
+        switch (msg.what) {
+            case EVENT_RIL_CONNECTED:
+                onRilConnected();
+                break;
+            default:
+                updateDeviceState(msg.what, msg.arg1 != 0);
+        }
+    }
+
+    /**
+     * Update the device and send the information to the modem.
+     *
+     * @param eventType Device state event type
+     * @param state True if enabled/on, otherwise disabled/off.
+     */
+    private void updateDeviceState(int eventType, boolean state) {
+        switch (eventType) {
+            case EVENT_SCREEN_STATE_CHANGED:
+                if (mIsScreenOn == state) return;
+                mIsScreenOn = state;
+                break;
+            case EVENT_CHARGING_STATE_CHANGED:
+                if (mIsCharging == state) return;
+                mIsCharging = state;
+                sendDeviceState(CHARGING_STATE, mIsCharging);
+                break;
+            case EVENT_TETHERING_STATE_CHANGED:
+                if (mIsTetheringOn == state) return;
+                mIsTetheringOn = state;
+                break;
+            case EVENT_POWER_SAVE_MODE_CHANGED:
+                if (mIsPowerSaveOn == state) return;
+                mIsPowerSaveOn = state;
+                sendDeviceState(POWER_SAVE_MODE, mIsPowerSaveOn);
+                break;
+            default:
+                return;
+        }
+
+        if (mIsLowDataExpected != isLowDataExpected()) {
+            mIsLowDataExpected = !mIsLowDataExpected;
+            sendDeviceState(LOW_DATA_EXPECTED, mIsLowDataExpected);
+        }
+
+        int newFilter = 0;
+        if (!shouldTurnOffSignalStrength()) {
+            newFilter |= IndicationFilter.SIGNAL_STRENGTH;
+        }
+
+        if (!shouldTurnOffFullNetworkUpdate()) {
+            newFilter |= IndicationFilter.FULL_NETWORK_STATE;
+        }
+
+        if (!shouldTurnOffDormancyUpdate()) {
+            newFilter |= IndicationFilter.DATA_CALL_DORMANCY_CHANGED;
+        }
+
+        setUnsolResponseFilter(newFilter, false);
+    }
+
+    /**
+     * Called when RIL is connected during boot up or reconnected after modem restart.
+     *
+     * When modem crashes, if the user turns the screen off before RIL reconnects, device
+     * state and filter cannot be sent to modem. Resend the state here so that modem
+     * has the correct state (to stop signal strength reporting, etc).
+     */
+    private void onRilConnected() {
+        log("RIL connected.", true);
+        sendDeviceState(CHARGING_STATE, mIsCharging);
+        sendDeviceState(LOW_DATA_EXPECTED, mIsLowDataExpected);
+        sendDeviceState(POWER_SAVE_MODE, mIsPowerSaveOn);
+        setUnsolResponseFilter(mUnsolicitedResponseFilter, true);
+    }
+
+    /**
+     * Convert the device state type into string
+     *
+     * @param type Device state type
+     * @return The converted string
+     */
+    private String deviceTypeToString(int type) {
+        switch (type) {
+            case CHARGING_STATE: return "CHARGING_STATE";
+            case LOW_DATA_EXPECTED: return "LOW_DATA_EXPECTED";
+            case POWER_SAVE_MODE: return "POWER_SAVE_MODE";
+            default: return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Send the device state to the modem.
+     *
+     * @param type Device state type. See DeviceStateType defined in types.hal.
+     * @param state True if enabled/on, otherwise disabled/off
+     */
+    private void sendDeviceState(int type, boolean state) {
+        log("send type: " + deviceTypeToString(type) + ", state=" + state, true);
+        mPhone.mCi.sendDeviceState(type, state, null);
+    }
+
+    /**
+     * Turn on/off the unsolicited response from the modem.
+     *
+     * @param newFilter See UnsolicitedResponseFilter in types.hal for the definition of each bit.
+     * @param force Always set the filter when true.
+     */
+    private void setUnsolResponseFilter(int newFilter, boolean force) {
+        if (force || newFilter != mUnsolicitedResponseFilter) {
+            log("old filter: " + mUnsolicitedResponseFilter + ", new filter: " + newFilter, true);
+            mPhone.mCi.setUnsolResponseFilter(newFilter, null);
+            mUnsolicitedResponseFilter = newFilter;
+        }
+    }
+
+    /**
+     * @return True if the device is currently in power save mode.
+     * See {@link android.os.BatteryManager#isPowerSaveMode BatteryManager.isPowerSaveMode()}.
+     */
+    private boolean isPowerSaveModeOn() {
+        final PowerManager pm = (PowerManager) mPhone.getContext().getSystemService(
+                Context.POWER_SERVICE);
+        return pm.isPowerSaveMode();
+    }
+
+    /**
+     * @return Return true if the battery is currently considered to be charging. This means that
+     * the device is plugged in and is supplying sufficient power that the battery level is
+     * going up (or the battery is fully charged).
+     * See {@link android.os.BatteryManager#isCharging BatteryManager.isCharging()}.
+     */
+    private boolean isDeviceCharging() {
+        final BatteryManager bm = (BatteryManager) mPhone.getContext().getSystemService(
+                Context.BATTERY_SERVICE);
+        return bm.isCharging();
+    }
+
+    /**
+     * @return True if one the device's screen (e.g. main screen, wifi display, HDMI display, or
+     *         Android auto, etc...) is on.
+     */
+    private boolean isScreenOn() {
+        // Note that we don't listen to Intent.SCREEN_ON and Intent.SCREEN_OFF because they are no
+        // longer adequate for monitoring the screen state since they are not sent in cases where
+        // the screen is turned off transiently such as due to the proximity sensor.
+        final DisplayManager dm = (DisplayManager) mPhone.getContext().getSystemService(
+                Context.DISPLAY_SERVICE);
+        Display[] displays = dm.getDisplays();
+
+        if (displays != null) {
+            for (Display display : displays) {
+                // Anything other than STATE_ON is treated as screen off, such as STATE_DOZE,
+                // STATE_DOZE_SUSPEND, etc...
+                if (display.getState() == Display.STATE_ON) {
+                    log("Screen " + Display.typeToString(display.getType()) + " on", true);
+                    return true;
+                }
+            }
+            log("Screens all off", true);
+            return false;
+        }
+
+        log("No displays found", true);
+        return false;
+    }
+
+    /**
+     * @param msg Debug message
+     * @param logIntoLocalLog True if log into the local log
+     */
+    private void log(String msg, boolean logIntoLocalLog) {
+        if (DBG) Rlog.d(TAG, msg);
+        if (logIntoLocalLog) {
+            mLocalLog.log(msg);
+        }
+    }
+
+    /**
+     * Print the DeviceStateMonitor into the given stream.
+     *
+     * @param fd The raw file descriptor that the dump is being sent to.
+     * @param pw A PrintWriter to which the dump is to be set.
+     * @param args Additional arguments to the dump request.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        ipw.increaseIndent();
+        ipw.println("mIsTetheringOn=" + mIsTetheringOn);
+        ipw.println("mIsScreenOn=" + mIsScreenOn);
+        ipw.println("mIsCharging=" + mIsCharging);
+        ipw.println("mIsPowerSaveOn=" + mIsPowerSaveOn);
+        ipw.println("mIsLowDataExpected=" + mIsLowDataExpected);
+        ipw.println("mUnsolicitedResponseFilter=" + mUnsolicitedResponseFilter);
+        ipw.println("Local logs:");
+        ipw.increaseIndent();
+        mLocalLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+        ipw.decreaseIndent();
+        ipw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/DriverCall.java b/com/android/internal/telephony/DriverCall.java
new file mode 100644
index 0000000..a923d8f
--- /dev/null
+++ b/com/android/internal/telephony/DriverCall.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.telephony.Rlog;
+import java.lang.Comparable;
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * {@hide}
+ */
+public class DriverCall implements Comparable<DriverCall> {
+    static final String LOG_TAG = "DriverCall";
+
+    public enum State {
+        ACTIVE,
+        HOLDING,
+        DIALING,    // MO call only
+        ALERTING,   // MO call only
+        INCOMING,   // MT call only
+        WAITING;    // MT call only
+        // If you add a state, make sure to look for the switch()
+        // statements that use this enum
+    }
+
+    public int index;
+    public boolean isMT;
+    public State state;     // May be null if unavail
+    public boolean isMpty;
+    public String number;
+    public int TOA;
+    public boolean isVoice;
+    public boolean isVoicePrivacy;
+    public int als;
+    public int numberPresentation;
+    public String name;
+    public int namePresentation;
+    public UUSInfo uusInfo;
+
+    /** returns null on error */
+    static DriverCall
+    fromCLCCLine(String line) {
+        DriverCall ret = new DriverCall();
+
+        //+CLCC: 1,0,2,0,0,\"+18005551212\",145
+        //     index,isMT,state,mode,isMpty(,number,TOA)?
+        ATResponseParser p = new ATResponseParser(line);
+
+        try {
+            ret.index = p.nextInt();
+            ret.isMT = p.nextBoolean();
+            ret.state = stateFromCLCC(p.nextInt());
+
+            ret.isVoice = (0 == p.nextInt());
+            ret.isMpty = p.nextBoolean();
+
+            // use ALLOWED as default presentation while parsing CLCC
+            ret.numberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
+
+            if (p.hasMore()) {
+                // Some lame implementations return strings
+                // like "NOT AVAILABLE" in the CLCC line
+                ret.number = PhoneNumberUtils.extractNetworkPortionAlt(p.nextString());
+
+                if (ret.number.length() == 0) {
+                    ret.number = null;
+                }
+
+                ret.TOA = p.nextInt();
+
+                // Make sure there's a leading + on addresses with a TOA
+                // of 145
+
+                ret.number = PhoneNumberUtils.stringFromStringAndTOA(
+                                ret.number, ret.TOA);
+
+            }
+        } catch (ATParseEx ex) {
+            Rlog.e(LOG_TAG,"Invalid CLCC line: '" + line + "'");
+            return null;
+        }
+
+        return ret;
+    }
+
+    public
+    DriverCall() {
+    }
+
+    @Override
+    public String
+    toString() {
+        return "id=" + index + ","
+                + state + ","
+                + "toa=" + TOA + ","
+                + (isMpty ? "conf" : "norm") + ","
+                + (isMT ? "mt" : "mo") + ","
+                + als + ","
+                + (isVoice ? "voc" : "nonvoc") + ","
+                + (isVoicePrivacy ? "evp" : "noevp") + ","
+                /*+ "number=" + number */ + ",cli=" + numberPresentation + ","
+                /*+ "name="+ name */ + "," + namePresentation;
+    }
+
+    public static State
+    stateFromCLCC(int state) throws ATParseEx {
+        switch(state) {
+            case 0: return State.ACTIVE;
+            case 1: return State.HOLDING;
+            case 2: return State.DIALING;
+            case 3: return State.ALERTING;
+            case 4: return State.INCOMING;
+            case 5: return State.WAITING;
+            default:
+                throw new ATParseEx("illegal call state " + state);
+        }
+    }
+
+    public static int
+    presentationFromCLIP(int cli) throws ATParseEx
+    {
+        switch(cli) {
+            case 0: return PhoneConstants.PRESENTATION_ALLOWED;
+            case 1: return PhoneConstants.PRESENTATION_RESTRICTED;
+            case 2: return PhoneConstants.PRESENTATION_UNKNOWN;
+            case 3: return PhoneConstants.PRESENTATION_PAYPHONE;
+            default:
+                throw new ATParseEx("illegal presentation " + cli);
+        }
+    }
+
+    //***** Comparable Implementation
+
+    /** For sorting by index */
+    @Override
+    public int
+    compareTo(DriverCall dc) {
+
+        if (index < dc.index) {
+            return -1;
+        } else if (index == dc.index) {
+            return 0;
+        } else { /*index > dc.index*/
+            return 1;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/EncodeException.java b/com/android/internal/telephony/EncodeException.java
new file mode 100644
index 0000000..0436ba0
--- /dev/null
+++ b/com/android/internal/telephony/EncodeException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * {@hide}
+ */
+public class EncodeException extends Exception {
+    public EncodeException() {
+        super();
+    }
+
+    public EncodeException(String s) {
+        super(s);
+    }
+
+    public EncodeException(char c) {
+        super("Unencodable char: '" + c + "'");
+    }
+}
+
diff --git a/com/android/internal/telephony/ExponentialBackoff.java b/com/android/internal/telephony/ExponentialBackoff.java
new file mode 100644
index 0000000..80958c0
--- /dev/null
+++ b/com/android/internal/telephony/ExponentialBackoff.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+
+/** The implementation of exponential backoff with jitter applied. */
+public class ExponentialBackoff {
+    private int mRetryCounter;
+    private long mStartDelayMs;
+    private long mMaximumDelayMs;
+    private long mCurrentDelayMs;
+    private int mMultiplier;
+    private Runnable mRunnable;
+    private Handler mHandler;
+
+    public ExponentialBackoff(
+            long initialDelayMs,
+            long maximumDelayMs,
+            int multiplier,
+            @NonNull Looper looper,
+            @NonNull Runnable runnable) {
+        this(initialDelayMs, maximumDelayMs, multiplier, new Handler(looper), runnable);
+    }
+
+    public ExponentialBackoff(
+            long initialDelayMs,
+            long maximumDelayMs,
+            int multiplier,
+            @NonNull Handler handler,
+            @NonNull Runnable runnable) {
+        mRetryCounter = 0;
+        mStartDelayMs = initialDelayMs;
+        mMaximumDelayMs = maximumDelayMs;
+        mMultiplier = multiplier;
+        mHandler = handler;
+        mRunnable = runnable;
+    }
+
+    /** Starts the backoff, the runnable will be executed after {@link #mStartDelayMs}. */
+    public void start() {
+        mRetryCounter = 0;
+        mCurrentDelayMs = mStartDelayMs;
+        mHandler.removeCallbacks(mRunnable);
+        mHandler.postDelayed(mRunnable, mCurrentDelayMs);
+    }
+
+    /** Stops the backoff, all pending messages will be removed from the message queue. */
+    public void stop() {
+        mRetryCounter = 0;
+        mHandler.removeCallbacks(mRunnable);
+    }
+
+    /** Should call when the retry action has failed and we want to retry after a longer delay. */
+    public void notifyFailed() {
+        mRetryCounter++;
+        long temp = Math.min(
+                mMaximumDelayMs, (long) (mStartDelayMs * Math.pow(mMultiplier, mRetryCounter)));
+        mCurrentDelayMs = (long) (((1 + Math.random()) / 2) * temp);
+        mHandler.removeCallbacks(mRunnable);
+        mHandler.postDelayed(mRunnable, mCurrentDelayMs);
+    }
+
+    /** Returns the delay for the most recently posted message. */
+    public long getCurrentDelay() {
+        return mCurrentDelayMs;
+    }
+}
diff --git a/com/android/internal/telephony/GsmAlphabet.java b/com/android/internal/telephony/GsmAlphabet.java
new file mode 100644
index 0000000..4785169
--- /dev/null
+++ b/com/android/internal/telephony/GsmAlphabet.java
@@ -0,0 +1,1505 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.SparseIntArray;
+
+import android.telephony.Rlog;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class implements the character set mapping between
+ * the GSM SMS 7-bit alphabet specified in TS 23.038 6.2.1
+ * and UTF-16
+ *
+ * {@hide}
+ */
+public class GsmAlphabet {
+    private static final String TAG = "GSM";
+
+    private GsmAlphabet() { }
+
+    /**
+     * This escapes extended characters, and when present indicates that the
+     * following character should be looked up in the "extended" table.
+     *
+     * gsmToChar(GSM_EXTENDED_ESCAPE) returns 0xffff
+     */
+    public static final byte GSM_EXTENDED_ESCAPE = 0x1B;
+
+    /**
+     * User data header requires one octet for length. Count as one septet, because
+     * all combinations of header elements below will have at least one free bit
+     * when padding to the nearest septet boundary.
+     */
+    public static final int UDH_SEPTET_COST_LENGTH = 1;
+
+    /**
+     * Using a non-default language locking shift table OR single shift table
+     * requires a user data header of 3 octets, or 4 septets, plus UDH length.
+     */
+    public static final int UDH_SEPTET_COST_ONE_SHIFT_TABLE = 4;
+
+    /**
+     * Using a non-default language locking shift table AND single shift table
+     * requires a user data header of 6 octets, or 7 septets, plus UDH length.
+     */
+    public static final int UDH_SEPTET_COST_TWO_SHIFT_TABLES = 7;
+
+    /**
+     * Multi-part messages require a user data header of 5 octets, or 6 septets,
+     * plus UDH length.
+     */
+    public static final int UDH_SEPTET_COST_CONCATENATED_MESSAGE = 6;
+
+    /**
+     * For a specific text string, this object describes protocol
+     * properties of encoding it for transmission as message user
+     * data.
+     */
+    public static class TextEncodingDetails {
+        /**
+         *The number of SMS's required to encode the text.
+         */
+        public int msgCount;
+
+        /**
+         * The number of code units consumed so far, where code units
+         * are basically characters in the encoding -- for example,
+         * septets for the standard ASCII and GSM encodings, and 16
+         * bits for Unicode.
+         */
+        public int codeUnitCount;
+
+        /**
+         * How many code units are still available without spilling
+         * into an additional message.
+         */
+        public int codeUnitsRemaining;
+
+        /**
+         * The encoding code unit size (specified using
+         * android.telephony.SmsMessage ENCODING_*).
+         */
+        public int codeUnitSize;
+
+        /**
+         * The GSM national language table to use, or 0 for the default 7-bit alphabet.
+         */
+        public int languageTable;
+
+        /**
+         * The GSM national language shift table to use, or 0 for the default 7-bit extension table.
+         */
+        public int languageShiftTable;
+
+        @Override
+        public String toString() {
+            return "TextEncodingDetails " +
+                    "{ msgCount=" + msgCount +
+                    ", codeUnitCount=" + codeUnitCount +
+                    ", codeUnitsRemaining=" + codeUnitsRemaining +
+                    ", codeUnitSize=" + codeUnitSize +
+                    ", languageTable=" + languageTable +
+                    ", languageShiftTable=" + languageShiftTable +
+                    " }";
+        }
+    }
+
+    /**
+     * Converts a char to a GSM 7 bit table index.
+     * Returns ' ' in GSM alphabet if there's no possible match. Returns
+     * GSM_EXTENDED_ESCAPE if this character is in the extended table.
+     * In this case, you must call charToGsmExtended() for the value
+     * that should follow GSM_EXTENDED_ESCAPE in the GSM alphabet string.
+     * @param c the character to convert
+     * @return the GSM 7 bit table index for the specified character
+     */
+    public static int
+    charToGsm(char c) {
+        try {
+            return charToGsm(c, false);
+        } catch (EncodeException ex) {
+            // this should never happen
+            return sCharsToGsmTables[0].get(' ', ' ');
+        }
+    }
+
+    /**
+     * Converts a char to a GSM 7 bit table index.
+     * Returns GSM_EXTENDED_ESCAPE if this character is in the extended table.
+     * In this case, you must call charToGsmExtended() for the value that
+     * should follow GSM_EXTENDED_ESCAPE in the GSM alphabet string.
+     *
+     * @param c the character to convert
+     * @param throwException If true, throws EncodeException on invalid char.
+     *   If false, returns GSM alphabet ' ' char.
+     * @throws EncodeException encode error when throwException is true
+     * @return the GSM 7 bit table index for the specified character
+     */
+    public static int
+    charToGsm(char c, boolean throwException) throws EncodeException {
+        int ret;
+
+        ret = sCharsToGsmTables[0].get(c, -1);
+
+        if (ret == -1) {
+            ret = sCharsToShiftTables[0].get(c, -1);
+
+            if (ret == -1) {
+                if (throwException) {
+                    throw new EncodeException(c);
+                } else {
+                    return sCharsToGsmTables[0].get(' ', ' ');
+                }
+            } else {
+                return GSM_EXTENDED_ESCAPE;
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Converts a char to an extended GSM 7 bit table index.
+     * Extended chars should be escaped with GSM_EXTENDED_ESCAPE.
+     * Returns ' ' in GSM alphabet if there's no possible match.
+     * @param c the character to convert
+     * @return the GSM 7 bit extended table index for the specified character
+     */
+    public static int
+    charToGsmExtended(char c) {
+        int ret;
+
+        ret = sCharsToShiftTables[0].get(c, -1);
+
+        if (ret == -1) {
+            return sCharsToGsmTables[0].get(' ', ' ');
+        }
+
+        return ret;
+    }
+
+    /**
+     * Converts a character in the GSM alphabet into a char.
+     *
+     * If GSM_EXTENDED_ESCAPE is passed, 0xffff is returned. In this case,
+     * the following character in the stream should be decoded with
+     * gsmExtendedToChar().
+     *
+     * If an unmappable value is passed (one greater than 127), ' ' is returned.
+     *
+     * @param gsmChar the GSM 7 bit table index to convert
+     * @return the decoded character
+     */
+    public static char
+    gsmToChar(int gsmChar) {
+        if (gsmChar >= 0 && gsmChar < 128) {
+            return sLanguageTables[0].charAt(gsmChar);
+        } else {
+            return ' ';
+        }
+    }
+
+    /**
+     * Converts a character in the extended GSM alphabet into a char
+     *
+     * if GSM_EXTENDED_ESCAPE is passed, ' ' is returned since no second
+     * extension page has yet been defined (see Note 1 in table 6.2.1.1 of
+     * TS 23.038 v7.00)
+     *
+     * If an unmappable value is passed, the character from the GSM 7 bit
+     * default table will be used (table 6.2.1.1 of TS 23.038).
+     *
+     * @param gsmChar the GSM 7 bit extended table index to convert
+     * @return the decoded character
+     */
+    public static char
+    gsmExtendedToChar(int gsmChar) {
+        if (gsmChar == GSM_EXTENDED_ESCAPE) {
+            return ' ';
+        } else if (gsmChar >= 0 && gsmChar < 128) {
+            char c = sLanguageShiftTables[0].charAt(gsmChar);
+            if (c == ' ') {
+                return sLanguageTables[0].charAt(gsmChar);
+            } else {
+                return c;
+            }
+        } else {
+            return ' ';     // out of range
+        }
+    }
+
+    /**
+     * Converts a String into a byte array containing the 7-bit packed
+     * GSM Alphabet representation of the string. If a header is provided,
+     * this is included in the returned byte array and padded to a septet
+     * boundary. This method is used by OEM code.
+     *
+     * @param data The text string to encode.
+     * @param header Optional header (including length byte) that precedes
+     * the encoded data, padded to septet boundary.
+     * @return Byte array containing header and encoded data.
+     * @throws EncodeException if String is too large to encode
+     * @see #stringToGsm7BitPackedWithHeader(String, byte[], int, int)
+     */
+    public static byte[] stringToGsm7BitPackedWithHeader(String data, byte[] header)
+            throws EncodeException {
+        return stringToGsm7BitPackedWithHeader(data, header, 0, 0);
+    }
+
+    /**
+     * Converts a String into a byte array containing the 7-bit packed
+     * GSM Alphabet representation of the string. If a header is provided,
+     * this is included in the returned byte array and padded to a septet
+     * boundary.
+     *
+     * Unencodable chars are encoded as spaces
+     *
+     * Byte 0 in the returned byte array is the count of septets used,
+     * including the header and header padding. The returned byte array is
+     * the minimum size required to store the packed septets. The returned
+     * array cannot contain more than 255 septets.
+     *
+     * @param data The text string to encode.
+     * @param header Optional header (including length byte) that precedes
+     * the encoded data, padded to septet boundary.
+     * @param languageTable the 7 bit language table, or 0 for the default GSM alphabet
+     * @param languageShiftTable the 7 bit single shift language table, or 0 for the default
+     *     GSM extension table
+     * @return Byte array containing header and encoded data.
+     * @throws EncodeException if String is too large to encode
+     */
+    public static byte[] stringToGsm7BitPackedWithHeader(String data, byte[] header,
+            int languageTable, int languageShiftTable)
+            throws EncodeException {
+        if (header == null || header.length == 0) {
+            return stringToGsm7BitPacked(data, languageTable, languageShiftTable);
+        }
+
+        int headerBits = (header.length + 1) * 8;
+        int headerSeptets = (headerBits + 6) / 7;
+
+        byte[] ret = stringToGsm7BitPacked(data, headerSeptets, true, languageTable,
+                languageShiftTable);
+
+        // Paste in the header
+        ret[1] = (byte)header.length;
+        System.arraycopy(header, 0, ret, 2, header.length);
+        return ret;
+    }
+
+    /**
+     * Converts a String into a byte array containing
+     * the 7-bit packed GSM Alphabet representation of the string.
+     *
+     * Unencodable chars are encoded as spaces
+     *
+     * Byte 0 in the returned byte array is the count of septets used
+     * The returned byte array is the minimum size required to store
+     * the packed septets. The returned array cannot contain more than 255
+     * septets.
+     *
+     * @param data the data string to encode
+     * @return the encoded string
+     * @throws EncodeException if String is too large to encode
+     */
+    public static byte[] stringToGsm7BitPacked(String data)
+            throws EncodeException {
+        return stringToGsm7BitPacked(data, 0, true, 0, 0);
+    }
+
+    /**
+     * Converts a String into a byte array containing
+     * the 7-bit packed GSM Alphabet representation of the string.
+     *
+     * Unencodable chars are encoded as spaces
+     *
+     * Byte 0 in the returned byte array is the count of septets used
+     * The returned byte array is the minimum size required to store
+     * the packed septets. The returned array cannot contain more than 255
+     * septets.
+     *
+     * @param data the data string to encode
+     * @param languageTable the 7 bit language table, or 0 for the default GSM alphabet
+     * @param languageShiftTable the 7 bit single shift language table, or 0 for the default
+     *     GSM extension table
+     * @return the encoded string
+     * @throws EncodeException if String is too large to encode
+     */
+    public static byte[] stringToGsm7BitPacked(String data, int languageTable,
+            int languageShiftTable)
+            throws EncodeException {
+        return stringToGsm7BitPacked(data, 0, true, languageTable, languageShiftTable);
+    }
+
+    /**
+     * Converts a String into a byte array containing
+     * the 7-bit packed GSM Alphabet representation of the string.
+     *
+     * Byte 0 in the returned byte array is the count of septets used
+     * The returned byte array is the minimum size required to store
+     * the packed septets. The returned array cannot contain more than 255
+     * septets.
+     *
+     * @param data the text to convert to septets
+     * @param startingSeptetOffset the number of padding septets to put before
+     *  the character data at the beginning of the array
+     * @param throwException If true, throws EncodeException on invalid char.
+     *   If false, replaces unencodable char with GSM alphabet space char.
+     * @param languageTable the 7 bit language table, or 0 for the default GSM alphabet
+     * @param languageShiftTable the 7 bit single shift language table, or 0 for the default
+     *     GSM extension table
+     * @return the encoded message
+     *
+     * @throws EncodeException if String is too large to encode
+     */
+    public static byte[] stringToGsm7BitPacked(String data, int startingSeptetOffset,
+            boolean throwException, int languageTable, int languageShiftTable)
+            throws EncodeException {
+        int dataLen = data.length();
+        int septetCount = countGsmSeptetsUsingTables(data, !throwException,
+                languageTable, languageShiftTable);
+        if (septetCount == -1) {
+            throw new EncodeException("countGsmSeptetsUsingTables(): unencodable char");
+        }
+        septetCount += startingSeptetOffset;
+        if (septetCount > 255) {
+            throw new EncodeException("Payload cannot exceed 255 septets");
+        }
+        int byteCount = ((septetCount * 7) + 7) / 8;
+        byte[] ret = new byte[byteCount + 1];  // Include space for one byte length prefix.
+        SparseIntArray charToLanguageTable = sCharsToGsmTables[languageTable];
+        SparseIntArray charToShiftTable = sCharsToShiftTables[languageShiftTable];
+        for (int i = 0, septets = startingSeptetOffset, bitOffset = startingSeptetOffset * 7;
+                 i < dataLen && septets < septetCount;
+                 i++, bitOffset += 7) {
+            char c = data.charAt(i);
+            int v = charToLanguageTable.get(c, -1);
+            if (v == -1) {
+                v = charToShiftTable.get(c, -1);  // Lookup the extended char.
+                if (v == -1) {
+                    if (throwException) {
+                        throw new EncodeException("stringToGsm7BitPacked(): unencodable char");
+                    } else {
+                        v = charToLanguageTable.get(' ', ' ');   // should return ASCII space
+                    }
+                } else {
+                    packSmsChar(ret, bitOffset, GSM_EXTENDED_ESCAPE);
+                    bitOffset += 7;
+                    septets++;
+                }
+            }
+            packSmsChar(ret, bitOffset, v);
+            septets++;
+        }
+        ret[0] = (byte) (septetCount);  // Validated by check above.
+        return ret;
+    }
+
+    /**
+     * Pack a 7-bit char into its appropriate place in a byte array
+     *
+     * @param packedChars the destination byte array
+     * @param bitOffset the bit offset that the septet should be packed at
+     *                  (septet index * 7)
+     * @param value the 7-bit character to store
+     */
+    private static void
+    packSmsChar(byte[] packedChars, int bitOffset, int value) {
+        int byteOffset = bitOffset / 8;
+        int shift = bitOffset % 8;
+
+        packedChars[++byteOffset] |= value << shift;
+
+        if (shift > 1) {
+            packedChars[++byteOffset] = (byte)(value >> (8 - shift));
+        }
+    }
+
+    /**
+     * Convert a GSM alphabet 7 bit packed string (SMS string) into a
+     * {@link java.lang.String}.
+     *
+     * See TS 23.038 6.1.2.1 for SMS Character Packing
+     *
+     * @param pdu the raw data from the pdu
+     * @param offset the byte offset of
+     * @param lengthSeptets string length in septets, not bytes
+     * @return String representation or null on decoding exception
+     */
+    public static String gsm7BitPackedToString(byte[] pdu, int offset,
+            int lengthSeptets) {
+        return gsm7BitPackedToString(pdu, offset, lengthSeptets, 0, 0, 0);
+    }
+
+    /**
+     * Convert a GSM alphabet 7 bit packed string (SMS string) into a
+     * {@link java.lang.String}.
+     *
+     * See TS 23.038 6.1.2.1 for SMS Character Packing
+     *
+     * @param pdu the raw data from the pdu
+     * @param offset the byte offset of
+     * @param lengthSeptets string length in septets, not bytes
+     * @param numPaddingBits the number of padding bits before the start of the
+     *  string in the first byte
+     * @param languageTable the 7 bit language table, or 0 for the default GSM alphabet
+     * @param shiftTable the 7 bit single shift language table, or 0 for the default
+     *     GSM extension table
+     * @return String representation or null on decoding exception
+     */
+    public static String gsm7BitPackedToString(byte[] pdu, int offset,
+            int lengthSeptets, int numPaddingBits, int languageTable, int shiftTable) {
+        StringBuilder ret = new StringBuilder(lengthSeptets);
+
+        if (languageTable < 0 || languageTable > sLanguageTables.length) {
+            Rlog.w(TAG, "unknown language table " + languageTable + ", using default");
+            languageTable = 0;
+        }
+        if (shiftTable < 0 || shiftTable > sLanguageShiftTables.length) {
+            Rlog.w(TAG, "unknown single shift table " + shiftTable + ", using default");
+            shiftTable = 0;
+        }
+
+        try {
+            boolean prevCharWasEscape = false;
+            String languageTableToChar = sLanguageTables[languageTable];
+            String shiftTableToChar = sLanguageShiftTables[shiftTable];
+
+            if (languageTableToChar.isEmpty()) {
+                Rlog.w(TAG, "no language table for code " + languageTable + ", using default");
+                languageTableToChar = sLanguageTables[0];
+            }
+            if (shiftTableToChar.isEmpty()) {
+                Rlog.w(TAG, "no single shift table for code " + shiftTable + ", using default");
+                shiftTableToChar = sLanguageShiftTables[0];
+            }
+
+            for (int i = 0 ; i < lengthSeptets ; i++) {
+                int bitOffset = (7 * i) + numPaddingBits;
+
+                int byteOffset = bitOffset / 8;
+                int shift = bitOffset % 8;
+                int gsmVal;
+
+                gsmVal = (0x7f & (pdu[offset + byteOffset] >> shift));
+
+                // if it crosses a byte boundary
+                if (shift > 1) {
+                    // set msb bits to 0
+                    gsmVal &= 0x7f >> (shift - 1);
+
+                    gsmVal |= 0x7f & (pdu[offset + byteOffset + 1] << (8 - shift));
+                }
+
+                if (prevCharWasEscape) {
+                    if (gsmVal == GSM_EXTENDED_ESCAPE) {
+                        ret.append(' ');    // display ' ' for reserved double escape sequence
+                    } else {
+                        char c = shiftTableToChar.charAt(gsmVal);
+                        if (c == ' ') {
+                            ret.append(languageTableToChar.charAt(gsmVal));
+                        } else {
+                            ret.append(c);
+                        }
+                    }
+                    prevCharWasEscape = false;
+                } else if (gsmVal == GSM_EXTENDED_ESCAPE) {
+                    prevCharWasEscape = true;
+                } else {
+                    ret.append(languageTableToChar.charAt(gsmVal));
+                }
+            }
+        } catch (RuntimeException ex) {
+            Rlog.e(TAG, "Error GSM 7 bit packed: ", ex);
+            return null;
+        }
+
+        return ret.toString();
+    }
+
+
+    /**
+     * Convert a GSM alphabet string that's stored in 8-bit unpacked
+     * format (as it often appears in SIM records) into a String
+     *
+     * Field may be padded with trailing 0xff's. The decode stops
+     * at the first 0xff encountered.
+     *
+     * @param data the byte array to decode
+     * @param offset array offset for the first character to decode
+     * @param length the number of bytes to decode
+     * @return the decoded string
+     */
+    public static String
+    gsm8BitUnpackedToString(byte[] data, int offset, int length) {
+        return gsm8BitUnpackedToString(data, offset, length, "");
+    }
+
+    /**
+     * Convert a GSM alphabet string that's stored in 8-bit unpacked
+     * format (as it often appears in SIM records) into a String
+     *
+     * Field may be padded with trailing 0xff's. The decode stops
+     * at the first 0xff encountered.
+     *
+     * Additionally, in some country(ex. Korea), there are non-ASCII or MBCS characters.
+     * If a character set is given, characters in data are treat as MBCS.
+     */
+    public static String
+    gsm8BitUnpackedToString(byte[] data, int offset, int length, String characterset) {
+        boolean isMbcs = false;
+        Charset charset = null;
+        ByteBuffer mbcsBuffer = null;
+
+        if (!TextUtils.isEmpty(characterset)
+                && !characterset.equalsIgnoreCase("us-ascii")
+                && Charset.isSupported(characterset)) {
+            isMbcs = true;
+            charset = Charset.forName(characterset);
+            mbcsBuffer = ByteBuffer.allocate(2);
+        }
+
+        // Always use GSM 7 bit default alphabet table for this method
+        String languageTableToChar = sLanguageTables[0];
+        String shiftTableToChar = sLanguageShiftTables[0];
+
+        StringBuilder ret = new StringBuilder(length);
+        boolean prevWasEscape = false;
+        for (int i = offset ; i < offset + length ; i++) {
+            // Never underestimate the pain that can be caused
+            // by signed bytes
+            int c = data[i] & 0xff;
+
+            if (c == 0xff) {
+                break;
+            } else if (c == GSM_EXTENDED_ESCAPE) {
+                if (prevWasEscape) {
+                    // Two escape chars in a row
+                    // We treat this as a space
+                    // See Note 1 in table 6.2.1.1 of TS 23.038 v7.00
+                    ret.append(' ');
+                    prevWasEscape = false;
+                } else {
+                    prevWasEscape = true;
+                }
+            } else {
+                if (prevWasEscape) {
+                    char shiftChar =
+                            c < shiftTableToChar.length() ? shiftTableToChar.charAt(c) : ' ';
+                    if (shiftChar == ' ') {
+                        // display character from main table if not present in shift table
+                        if (c < languageTableToChar.length()) {
+                            ret.append(languageTableToChar.charAt(c));
+                        } else {
+                            ret.append(' ');
+                        }
+                    } else {
+                        ret.append(shiftChar);
+                    }
+                } else {
+                    if (!isMbcs || c < 0x80 || i + 1 >= offset + length) {
+                        if (c < languageTableToChar.length()) {
+                            ret.append(languageTableToChar.charAt(c));
+                        } else {
+                            ret.append(' ');
+                        }
+                    } else {
+                        // isMbcs must be true. So both mbcsBuffer and charset are initialized.
+                        mbcsBuffer.clear();
+                        mbcsBuffer.put(data, i++, 2);
+                        mbcsBuffer.flip();
+                        ret.append(charset.decode(mbcsBuffer).toString());
+                    }
+                }
+                prevWasEscape = false;
+            }
+        }
+
+        return ret.toString();
+    }
+
+    /**
+     * Convert a string into an 8-bit unpacked GSM alphabet byte array.
+     * Always uses GSM default 7-bit alphabet and extension table.
+     * @param s the string to encode
+     * @return the 8-bit GSM encoded byte array for the string
+     */
+    public static byte[]
+    stringToGsm8BitPacked(String s) {
+        byte[] ret;
+
+        int septets = countGsmSeptetsUsingTables(s, true, 0, 0);
+
+        // Enough for all the septets and the length byte prefix
+        ret = new byte[septets];
+
+        stringToGsm8BitUnpackedField(s, ret, 0, ret.length);
+
+        return ret;
+    }
+
+
+    /**
+     * Write a String into a GSM 8-bit unpacked field of
+     * Field is padded with 0xff's, string is truncated if necessary
+     *
+     * @param s the string to encode
+     * @param dest the destination byte array
+     * @param offset the starting offset for the encoded string
+     * @param length the maximum number of bytes to write
+     */
+    public static void
+    stringToGsm8BitUnpackedField(String s, byte dest[], int offset, int length) {
+        int outByteIndex = offset;
+        SparseIntArray charToLanguageTable = sCharsToGsmTables[0];
+        SparseIntArray charToShiftTable = sCharsToShiftTables[0];
+
+        // Septets are stored in byte-aligned octets
+        for (int i = 0, sz = s.length()
+                ; i < sz && (outByteIndex - offset) < length
+                ; i++
+        ) {
+            char c = s.charAt(i);
+
+            int v = charToLanguageTable.get(c, -1);
+
+            if (v == -1) {
+                v = charToShiftTable.get(c, -1);
+                if (v == -1) {
+                    v = charToLanguageTable.get(' ', ' ');  // fall back to ASCII space
+                } else {
+                    // make sure we can fit an escaped char
+                    if (! (outByteIndex + 1 - offset < length)) {
+                        break;
+                    }
+
+                    dest[outByteIndex++] = GSM_EXTENDED_ESCAPE;
+                }
+            }
+
+            dest[outByteIndex++] = (byte)v;
+        }
+
+        // pad with 0xff's
+        while((outByteIndex - offset) < length) {
+            dest[outByteIndex++] = (byte)0xff;
+        }
+    }
+
+    /**
+     * Returns the count of 7-bit GSM alphabet characters
+     * needed to represent this character. Counts unencodable char as 1 septet.
+     * @param c the character to examine
+     * @return the number of septets for this character
+     */
+    public static int
+    countGsmSeptets(char c) {
+        try {
+            return countGsmSeptets(c, false);
+        } catch (EncodeException ex) {
+            // This should never happen.
+            return 0;
+        }
+    }
+
+    /**
+     * Returns the count of 7-bit GSM alphabet characters
+     * needed to represent this character using the default 7 bit GSM alphabet.
+     * @param c the character to examine
+     * @param throwsException If true, throws EncodeException if unencodable
+     * char. Otherwise, counts invalid char as 1 septet.
+     * @return the number of septets for this character
+     * @throws EncodeException the character can't be encoded and throwsException is true
+     */
+    public static int
+    countGsmSeptets(char c, boolean throwsException) throws EncodeException {
+        if (sCharsToGsmTables[0].get(c, -1) != -1) {
+            return 1;
+        }
+
+        if (sCharsToShiftTables[0].get(c, -1) != -1) {
+            return 2;
+        }
+
+        if (throwsException) {
+            throw new EncodeException(c);
+        } else {
+            // count as a space char
+            return 1;
+        }
+    }
+
+    public static boolean isGsmSeptets(char c) {
+        if (sCharsToGsmTables[0].get(c, -1) != -1) {
+            return true;
+        }
+
+        if (sCharsToShiftTables[0].get(c, -1) != -1) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the count of 7-bit GSM alphabet characters needed
+     * to represent this string, using the specified 7-bit language table
+     * and extension table (0 for GSM default tables).
+     * @param s the Unicode string that will be encoded
+     * @param use7bitOnly allow using space in place of unencodable character if true,
+     *     otherwise, return -1 if any characters are unencodable
+     * @param languageTable the 7 bit language table, or 0 for the default GSM alphabet
+     * @param languageShiftTable the 7 bit single shift language table, or 0 for the default
+     *     GSM extension table
+     * @return the septet count for s using the specified language tables, or -1 if any
+     *     characters are unencodable and use7bitOnly is false
+     */
+    public static int countGsmSeptetsUsingTables(CharSequence s, boolean use7bitOnly,
+            int languageTable, int languageShiftTable) {
+        int count = 0;
+        int sz = s.length();
+        SparseIntArray charToLanguageTable = sCharsToGsmTables[languageTable];
+        SparseIntArray charToShiftTable = sCharsToShiftTables[languageShiftTable];
+        for (int i = 0; i < sz; i++) {
+            char c = s.charAt(i);
+            if (c == GSM_EXTENDED_ESCAPE) {
+                Rlog.w(TAG, "countGsmSeptets() string contains Escape character, skipping.");
+                continue;
+            }
+            if (charToLanguageTable.get(c, -1) != -1) {
+                count++;
+            } else if (charToShiftTable.get(c, -1) != -1) {
+                count += 2; // escape + shift table index
+            } else if (use7bitOnly) {
+                count++;    // encode as space
+            } else {
+                return -1;  // caller must check for this case
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Returns the count of 7-bit GSM alphabet characters
+     * needed to represent this string, and the language table and
+     * language shift table used to achieve this result.
+     * For multi-part text messages, each message part may use its
+     * own language table encoding as specified in the message header
+     * for that message. However, this method will only return the
+     * optimal encoding for the message as a whole. When the individual
+     * pieces are encoded, a more optimal encoding may be chosen for each
+     * piece of the message, but the message will be split into pieces
+     * based on the encoding chosen for the message as a whole.
+     * @param s the Unicode string that will be encoded
+     * @param use7bitOnly allow using space in place of unencodable character if true,
+     *     using the language table pair with the fewest unencodable characters
+     * @return a TextEncodingDetails object containing the message and
+     *     character counts for the most efficient 7-bit encoding,
+     *     or null if there are no suitable language tables to encode the string.
+     */
+    public static TextEncodingDetails
+    countGsmSeptets(CharSequence s, boolean use7bitOnly) {
+        // Load enabled language tables from config.xml, including any MCC overlays
+        if (!sDisableCountryEncodingCheck) {
+            enableCountrySpecificEncodings();
+        }
+        // fast path for common case where no national language shift tables are enabled
+        if (sEnabledSingleShiftTables.length + sEnabledLockingShiftTables.length == 0) {
+            TextEncodingDetails ted = new TextEncodingDetails();
+            int septets = GsmAlphabet.countGsmSeptetsUsingTables(s, use7bitOnly, 0, 0);
+            if (septets == -1) {
+                return null;
+            }
+            ted.codeUnitSize = SmsConstants.ENCODING_7BIT;
+            ted.codeUnitCount = septets;
+            if (septets > SmsConstants.MAX_USER_DATA_SEPTETS) {
+                ted.msgCount = (septets + (SmsConstants.MAX_USER_DATA_SEPTETS_WITH_HEADER - 1)) /
+                        SmsConstants.MAX_USER_DATA_SEPTETS_WITH_HEADER;
+                ted.codeUnitsRemaining = (ted.msgCount *
+                        SmsConstants.MAX_USER_DATA_SEPTETS_WITH_HEADER) - septets;
+            } else {
+                ted.msgCount = 1;
+                ted.codeUnitsRemaining = SmsConstants.MAX_USER_DATA_SEPTETS - septets;
+            }
+            ted.codeUnitSize = SmsConstants.ENCODING_7BIT;
+            return ted;
+        }
+
+        int maxSingleShiftCode = sHighestEnabledSingleShiftCode;
+        List<LanguagePairCount> lpcList = new ArrayList<LanguagePairCount>(
+                sEnabledLockingShiftTables.length + 1);
+
+        // Always add default GSM 7-bit alphabet table
+        lpcList.add(new LanguagePairCount(0));
+        for (int i : sEnabledLockingShiftTables) {
+            // Avoid adding default table twice in case 0 is in the list of allowed tables
+            if (i != 0 && !sLanguageTables[i].isEmpty()) {
+                lpcList.add(new LanguagePairCount(i));
+            }
+        }
+
+        int sz = s.length();
+        // calculate septet count for each valid table / shift table pair
+        for (int i = 0; i < sz && !lpcList.isEmpty(); i++) {
+            char c = s.charAt(i);
+            if (c == GSM_EXTENDED_ESCAPE) {
+                Rlog.w(TAG, "countGsmSeptets() string contains Escape character, ignoring!");
+                continue;
+            }
+            // iterate through enabled locking shift tables
+            for (LanguagePairCount lpc : lpcList) {
+                int tableIndex = sCharsToGsmTables[lpc.languageCode].get(c, -1);
+                if (tableIndex == -1) {
+                    // iterate through single shift tables for this locking table
+                    for (int table = 0; table <= maxSingleShiftCode; table++) {
+                        if (lpc.septetCounts[table] != -1) {
+                            int shiftTableIndex = sCharsToShiftTables[table].get(c, -1);
+                            if (shiftTableIndex == -1) {
+                                if (use7bitOnly) {
+                                    // can't encode char, use space instead
+                                    lpc.septetCounts[table]++;
+                                    lpc.unencodableCounts[table]++;
+                                } else {
+                                    // can't encode char, remove language pair from list
+                                    lpc.septetCounts[table] = -1;
+                                }
+                            } else {
+                                // encode as Escape + index into shift table
+                                lpc.septetCounts[table] += 2;
+                            }
+                        }
+                    }
+                } else {
+                    // encode as index into locking shift table for all pairs
+                    for (int table = 0; table <= maxSingleShiftCode; table++) {
+                        if (lpc.septetCounts[table] != -1) {
+                            lpc.septetCounts[table]++;
+                        }
+                    }
+                }
+            }
+        }
+
+        // find the least cost encoding (lowest message count and most code units remaining)
+        TextEncodingDetails ted = new TextEncodingDetails();
+        ted.msgCount = Integer.MAX_VALUE;
+        ted.codeUnitSize = SmsConstants.ENCODING_7BIT;
+        int minUnencodableCount = Integer.MAX_VALUE;
+        for (LanguagePairCount lpc : lpcList) {
+            for (int shiftTable = 0; shiftTable <= maxSingleShiftCode; shiftTable++) {
+                int septets = lpc.septetCounts[shiftTable];
+                if (septets == -1) {
+                    continue;
+                }
+                int udhLength;
+                if (lpc.languageCode != 0 && shiftTable != 0) {
+                    udhLength = UDH_SEPTET_COST_LENGTH + UDH_SEPTET_COST_TWO_SHIFT_TABLES;
+                } else if (lpc.languageCode != 0 || shiftTable != 0) {
+                    udhLength = UDH_SEPTET_COST_LENGTH + UDH_SEPTET_COST_ONE_SHIFT_TABLE;
+                } else {
+                    udhLength = 0;
+                }
+                int msgCount;
+                int septetsRemaining;
+                if (septets + udhLength > SmsConstants.MAX_USER_DATA_SEPTETS) {
+                    if (udhLength == 0) {
+                        udhLength = UDH_SEPTET_COST_LENGTH;
+                    }
+                    udhLength += UDH_SEPTET_COST_CONCATENATED_MESSAGE;
+                    int septetsPerMessage = SmsConstants.MAX_USER_DATA_SEPTETS - udhLength;
+                    msgCount = (septets + septetsPerMessage - 1) / septetsPerMessage;
+                    septetsRemaining = (msgCount * septetsPerMessage) - septets;
+                } else {
+                    msgCount = 1;
+                    septetsRemaining = SmsConstants.MAX_USER_DATA_SEPTETS - udhLength - septets;
+                }
+                // for 7-bit only mode, use language pair with the least unencodable chars
+                int unencodableCount = lpc.unencodableCounts[shiftTable];
+                if (use7bitOnly && unencodableCount > minUnencodableCount) {
+                    continue;
+                }
+                if ((use7bitOnly && unencodableCount < minUnencodableCount)
+                        || msgCount < ted.msgCount || (msgCount == ted.msgCount
+                        && septetsRemaining > ted.codeUnitsRemaining)) {
+                    minUnencodableCount = unencodableCount;
+                    ted.msgCount = msgCount;
+                    ted.codeUnitCount = septets;
+                    ted.codeUnitsRemaining = septetsRemaining;
+                    ted.languageTable = lpc.languageCode;
+                    ted.languageShiftTable = shiftTable;
+                }
+            }
+        }
+
+        if (ted.msgCount == Integer.MAX_VALUE) {
+            return null;
+        }
+
+        return ted;
+    }
+
+    /**
+     * Returns the index into <code>s</code> of the first character
+     * after <code>limit</code> septets have been reached, starting at
+     * index <code>start</code>.  This is used when dividing messages
+     * into units within the SMS message size limit.
+     *
+     * @param s source string
+     * @param start index of where to start counting septets
+     * @param limit maximum septets to include,
+     *   e.g. <code>MAX_USER_DATA_SEPTETS</code>
+     * @param langTable the 7 bit character table to use (0 for default GSM 7-bit alphabet)
+     * @param langShiftTable the 7 bit shift table to use (0 for default GSM extension table)
+     * @return index of first character that won't fit, or the length
+     *   of the entire string if everything fits
+     */
+    public static int
+    findGsmSeptetLimitIndex(String s, int start, int limit, int langTable, int langShiftTable) {
+        int accumulator = 0;
+        int size = s.length();
+
+        SparseIntArray charToLangTable = sCharsToGsmTables[langTable];
+        SparseIntArray charToLangShiftTable = sCharsToShiftTables[langShiftTable];
+        for (int i = start; i < size; i++) {
+            int encodedSeptet = charToLangTable.get(s.charAt(i), -1);
+            if (encodedSeptet == -1) {
+                encodedSeptet = charToLangShiftTable.get(s.charAt(i), -1);
+                if (encodedSeptet == -1) {
+                    // char not found, assume we're replacing with space
+                    accumulator++;
+                } else {
+                    accumulator += 2;  // escape character + shift table index
+                }
+            } else {
+                accumulator++;
+            }
+            if (accumulator > limit) {
+                return i;
+            }
+        }
+        return size;
+    }
+
+    /**
+     * Modify the array of enabled national language single shift tables for SMS
+     * encoding. This is used for unit testing, but could also be used to
+     * modify the enabled encodings based on the active MCC/MNC, for example.
+     *
+     * @param tables the new list of enabled single shift tables
+     */
+    public static synchronized void setEnabledSingleShiftTables(int[] tables) {
+        sEnabledSingleShiftTables = tables;
+        sDisableCountryEncodingCheck = true;
+
+        if (tables.length > 0) {
+            sHighestEnabledSingleShiftCode = tables[tables.length - 1];
+        } else {
+            sHighestEnabledSingleShiftCode = 0;
+        }
+    }
+
+    /**
+     * Modify the array of enabled national language locking shift tables for SMS
+     * encoding. This is used for unit testing, but could also be used to
+     * modify the enabled encodings based on the active MCC/MNC, for example.
+     *
+     * @param tables the new list of enabled locking shift tables
+     */
+    public static synchronized void setEnabledLockingShiftTables(int[] tables) {
+        sEnabledLockingShiftTables = tables;
+        sDisableCountryEncodingCheck = true;
+    }
+
+    /**
+     * Return the array of enabled national language single shift tables for SMS
+     * encoding. This is used for unit testing. The returned array is not a copy, so
+     * the caller should be careful not to modify it.
+     *
+     * @return the list of enabled single shift tables
+     */
+    public static synchronized int[] getEnabledSingleShiftTables() {
+        return sEnabledSingleShiftTables;
+    }
+
+    /**
+     * Return the array of enabled national language locking shift tables for SMS
+     * encoding. This is used for unit testing. The returned array is not a copy, so
+     * the caller should be careful not to modify it.
+     *
+     * @return the list of enabled locking shift tables
+     */
+    public static synchronized int[] getEnabledLockingShiftTables() {
+        return sEnabledLockingShiftTables;
+    }
+
+    /**
+     * Enable country-specific language tables from MCC-specific overlays.
+     * @context the context to use to get the TelephonyManager
+     */
+    private static void enableCountrySpecificEncodings() {
+        Resources r = Resources.getSystem();
+        // See comments in frameworks/base/core/res/res/values/config.xml for allowed values
+        sEnabledSingleShiftTables = r.getIntArray(R.array.config_sms_enabled_single_shift_tables);
+        sEnabledLockingShiftTables = r.getIntArray(R.array.config_sms_enabled_locking_shift_tables);
+
+        if (sEnabledSingleShiftTables.length > 0) {
+            sHighestEnabledSingleShiftCode =
+                    sEnabledSingleShiftTables[sEnabledSingleShiftTables.length-1];
+        } else {
+            sHighestEnabledSingleShiftCode = 0;
+        }
+    }
+
+    /** Reverse mapping from Unicode characters to indexes into language tables. */
+    private static final SparseIntArray[] sCharsToGsmTables;
+
+    /** Reverse mapping from Unicode characters to indexes into language shift tables. */
+    private static final SparseIntArray[] sCharsToShiftTables;
+
+    /** OEM configured list of enabled national language single shift tables for encoding. */
+    private static int[] sEnabledSingleShiftTables;
+
+    /** OEM configured list of enabled national language locking shift tables for encoding. */
+    private static int[] sEnabledLockingShiftTables;
+
+    /** Highest language code to include in array of single shift counters. */
+    private static int sHighestEnabledSingleShiftCode;
+
+    /** Flag to bypass check for country-specific overlays (for test cases only). */
+    private static boolean sDisableCountryEncodingCheck = false;
+
+    /**
+     * Septet counter for a specific locking shift table and all of
+     * the single shift tables that it can be paired with.
+     */
+    private static class LanguagePairCount {
+        final int languageCode;
+        final int[] septetCounts;
+        final int[] unencodableCounts;
+        LanguagePairCount(int code) {
+            this.languageCode = code;
+            int maxSingleShiftCode = sHighestEnabledSingleShiftCode;
+            septetCounts = new int[maxSingleShiftCode + 1];
+            unencodableCounts = new int[maxSingleShiftCode + 1];
+            // set counters for disabled single shift tables to -1
+            // (GSM default extension table index 0 is always enabled)
+            for (int i = 1, tableOffset = 0; i <= maxSingleShiftCode; i++) {
+                if (sEnabledSingleShiftTables[tableOffset] == i) {
+                    tableOffset++;
+                } else {
+                    septetCounts[i] = -1;   // disabled
+                }
+            }
+            // exclude Turkish locking + Turkish single shift table and
+            // Portuguese locking + Spanish single shift table (these
+            // combinations will never be optimal for any input).
+            if (code == 1 && maxSingleShiftCode >= 1) {
+                septetCounts[1] = -1;   // Turkish + Turkish
+            } else if (code == 3 && maxSingleShiftCode >= 2) {
+                septetCounts[2] = -1;   // Portuguese + Spanish
+            }
+        }
+    }
+
+    /**
+     * GSM default 7 bit alphabet plus national language locking shift character tables.
+     * Comment lines above strings indicate the lower four bits of the table position.
+     */
+    private static final String[] sLanguageTables = {
+        /* 3GPP TS 23.038 V9.1.1 section 6.2.1 - GSM 7 bit Default Alphabet
+         01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....0.....1 */
+        "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5\u0394_"
+            // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....
+            + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u00c6\u00e6\u00df"
+            // F.....012.34.....56789ABCDEF0123456789ABCDEF0.....123456789ABCDEF0123456789A
+            + "\u00c9 !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            // B.....C.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....
+            + "\u00c4\u00d6\u00d1\u00dc\u00a7\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1"
+            // E.....F.....
+            + "\u00fc\u00e0",
+
+        /* A.3.1 Turkish National Language Locking Shift Table
+         01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....0.....1 */
+        "@\u00a3$\u00a5\u20ac\u00e9\u00f9\u0131\u00f2\u00c7\n\u011e\u011f\r\u00c5\u00e5\u0394_"
+            // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....
+            + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u015e\u015f\u00df"
+            // F.....012.34.....56789ABCDEF0123456789ABCDEF0.....123456789ABCDEF0123456789A
+            + "\u00c9 !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u0130ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            // B.....C.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....
+            + "\u00c4\u00d6\u00d1\u00dc\u00a7\u00e7abcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1"
+            // E.....F.....
+            + "\u00fc\u00e0",
+
+        /* A.3.2 Void (no locking shift table for Spanish) */
+        "",
+
+        /* A.3.3 Portuguese National Language Locking Shift Table
+         01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....0.....1 */
+        "@\u00a3$\u00a5\u00ea\u00e9\u00fa\u00ed\u00f3\u00e7\n\u00d4\u00f4\r\u00c1\u00e1\u0394_"
+            // 2.....3.....4.....5.....67.8.....9.....AB.....C.....D.....E.....F.....012.34.....
+            + "\u00aa\u00c7\u00c0\u221e^\\\u20ac\u00d3|\uffff\u00c2\u00e2\u00ca\u00c9 !\"#\u00ba"
+            // 56789ABCDEF0123456789ABCDEF0.....123456789ABCDEF0123456789AB.....C.....D.....E.....
+            + "%&'()*+,-./0123456789:;<=>?\u00cdABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c3\u00d5\u00da\u00dc"
+            // F.....0123456789ABCDEF0123456789AB.....C.....DE.....F.....
+            + "\u00a7~abcdefghijklmnopqrstuvwxyz\u00e3\u00f5`\u00fc\u00e0",
+
+        /* A.3.4 Bengali National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.EF.....0..... */
+        "\u0981\u0982\u0983\u0985\u0986\u0987\u0988\u0989\u098a\u098b\n\u098c \r \u098f\u0990"
+            // 123.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....
+            + "  \u0993\u0994\u0995\u0996\u0997\u0998\u0999\u099a\uffff\u099b\u099c\u099d\u099e"
+            // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789ABC
+            + " !\u099f\u09a0\u09a1\u09a2\u09a3\u09a4)(\u09a5\u09a6,\u09a7.\u09a80123456789:; "
+            // D.....E.....F0.....1.....2.....3.....4.....56.....789A.....B.....C.....D.....
+            + "\u09aa\u09ab?\u09ac\u09ad\u09ae\u09af\u09b0 \u09b2   \u09b6\u09b7\u09b8\u09b9"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....789.....A.....BCD.....E.....
+            + "\u09bc\u09bd\u09be\u09bf\u09c0\u09c1\u09c2\u09c3\u09c4  \u09c7\u09c8  \u09cb\u09cc"
+            // F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....F.....
+            + "\u09cd\u09ceabcdefghijklmnopqrstuvwxyz\u09d7\u09dc\u09dd\u09f0\u09f1",
+
+        /* A.3.5 Gujarati National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.EF.....0.....*/
+        "\u0a81\u0a82\u0a83\u0a85\u0a86\u0a87\u0a88\u0a89\u0a8a\u0a8b\n\u0a8c\u0a8d\r \u0a8f\u0a90"
+            // 1.....23.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....
+            + "\u0a91 \u0a93\u0a94\u0a95\u0a96\u0a97\u0a98\u0a99\u0a9a\uffff\u0a9b\u0a9c\u0a9d"
+            // F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789AB
+            + "\u0a9e !\u0a9f\u0aa0\u0aa1\u0aa2\u0aa3\u0aa4)(\u0aa5\u0aa6,\u0aa7.\u0aa80123456789:;"
+            // CD.....E.....F0.....1.....2.....3.....4.....56.....7.....89.....A.....B.....C.....
+            + " \u0aaa\u0aab?\u0aac\u0aad\u0aae\u0aaf\u0ab0 \u0ab2\u0ab3 \u0ab5\u0ab6\u0ab7\u0ab8"
+            // D.....E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89.....A.....
+            + "\u0ab9\u0abc\u0abd\u0abe\u0abf\u0ac0\u0ac1\u0ac2\u0ac3\u0ac4\u0ac5 \u0ac7\u0ac8"
+            // B.....CD.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....
+            + "\u0ac9 \u0acb\u0acc\u0acd\u0ad0abcdefghijklmnopqrstuvwxyz\u0ae0\u0ae1\u0ae2\u0ae3"
+            // F.....
+            + "\u0af1",
+
+        /* A.3.6 Hindi National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....*/
+        "\u0901\u0902\u0903\u0905\u0906\u0907\u0908\u0909\u090a\u090b\n\u090c\u090d\r\u090e\u090f"
+            // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....
+            + "\u0910\u0911\u0912\u0913\u0914\u0915\u0916\u0917\u0918\u0919\u091a\uffff\u091b\u091c"
+            // E.....F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....012345
+            + "\u091d\u091e !\u091f\u0920\u0921\u0922\u0923\u0924)(\u0925\u0926,\u0927.\u0928012345"
+            // 6789ABC.....D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....8.....
+            + "6789:;\u0929\u092a\u092b?\u092c\u092d\u092e\u092f\u0930\u0931\u0932\u0933\u0934"
+            // 9.....A.....B.....C.....D.....E.....F.....0.....1.....2.....3.....4.....5.....6.....
+            + "\u0935\u0936\u0937\u0938\u0939\u093c\u093d\u093e\u093f\u0940\u0941\u0942\u0943\u0944"
+            // 7.....8.....9.....A.....B.....C.....D.....E.....F.....0.....123456789ABCDEF012345678
+            + "\u0945\u0946\u0947\u0948\u0949\u094a\u094b\u094c\u094d\u0950abcdefghijklmnopqrstuvwx"
+            // 9AB.....C.....D.....E.....F.....
+            + "yz\u0972\u097b\u097c\u097e\u097f",
+
+        /* A.3.7 Kannada National Language Locking Shift Table
+           NOTE: TS 23.038 V9.1.1 shows code 0x24 as \u0caa, corrected to \u0ca1 (typo)
+         01.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F.....0.....1 */
+        " \u0c82\u0c83\u0c85\u0c86\u0c87\u0c88\u0c89\u0c8a\u0c8b\n\u0c8c \r\u0c8e\u0c8f\u0c90 "
+            // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....
+            + "\u0c92\u0c93\u0c94\u0c95\u0c96\u0c97\u0c98\u0c99\u0c9a\uffff\u0c9b\u0c9c\u0c9d\u0c9e"
+            // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789ABC
+            + " !\u0c9f\u0ca0\u0ca1\u0ca2\u0ca3\u0ca4)(\u0ca5\u0ca6,\u0ca7.\u0ca80123456789:; "
+            // D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....89.....A.....B.....
+            + "\u0caa\u0cab?\u0cac\u0cad\u0cae\u0caf\u0cb0\u0cb1\u0cb2\u0cb3 \u0cb5\u0cb6\u0cb7"
+            // C.....D.....E.....F.....0.....1.....2.....3.....4.....5.....6.....78.....9.....
+            + "\u0cb8\u0cb9\u0cbc\u0cbd\u0cbe\u0cbf\u0cc0\u0cc1\u0cc2\u0cc3\u0cc4 \u0cc6\u0cc7"
+            // A.....BC.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....
+            + "\u0cc8 \u0cca\u0ccb\u0ccc\u0ccd\u0cd5abcdefghijklmnopqrstuvwxyz\u0cd6\u0ce0\u0ce1"
+            // E.....F.....
+            + "\u0ce2\u0ce3",
+
+        /* A.3.8 Malayalam National Language Locking Shift Table
+         01.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F.....0.....1 */
+        " \u0d02\u0d03\u0d05\u0d06\u0d07\u0d08\u0d09\u0d0a\u0d0b\n\u0d0c \r\u0d0e\u0d0f\u0d10 "
+            // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....
+            + "\u0d12\u0d13\u0d14\u0d15\u0d16\u0d17\u0d18\u0d19\u0d1a\uffff\u0d1b\u0d1c\u0d1d\u0d1e"
+            // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789ABC
+            + " !\u0d1f\u0d20\u0d21\u0d22\u0d23\u0d24)(\u0d25\u0d26,\u0d27.\u0d280123456789:; "
+            // D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....
+            + "\u0d2a\u0d2b?\u0d2c\u0d2d\u0d2e\u0d2f\u0d30\u0d31\u0d32\u0d33\u0d34\u0d35\u0d36"
+            // B.....C.....D.....EF.....0.....1.....2.....3.....4.....5.....6.....78.....9.....
+            + "\u0d37\u0d38\u0d39 \u0d3d\u0d3e\u0d3f\u0d40\u0d41\u0d42\u0d43\u0d44 \u0d46\u0d47"
+            // A.....BC.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....
+            + "\u0d48 \u0d4a\u0d4b\u0d4c\u0d4d\u0d57abcdefghijklmnopqrstuvwxyz\u0d60\u0d61\u0d62"
+            // E.....F.....
+            + "\u0d63\u0d79",
+
+        /* A.3.9 Oriya National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.EF.....0.....12 */
+        "\u0b01\u0b02\u0b03\u0b05\u0b06\u0b07\u0b08\u0b09\u0b0a\u0b0b\n\u0b0c \r \u0b0f\u0b10  "
+            // 3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....01
+            + "\u0b13\u0b14\u0b15\u0b16\u0b17\u0b18\u0b19\u0b1a\uffff\u0b1b\u0b1c\u0b1d\u0b1e !"
+            // 2.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789ABCD.....
+            + "\u0b1f\u0b20\u0b21\u0b22\u0b23\u0b24)(\u0b25\u0b26,\u0b27.\u0b280123456789:; \u0b2a"
+            // E.....F0.....1.....2.....3.....4.....56.....7.....89.....A.....B.....C.....D.....
+            + "\u0b2b?\u0b2c\u0b2d\u0b2e\u0b2f\u0b30 \u0b32\u0b33 \u0b35\u0b36\u0b37\u0b38\u0b39"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....789.....A.....BCD.....E.....
+            + "\u0b3c\u0b3d\u0b3e\u0b3f\u0b40\u0b41\u0b42\u0b43\u0b44  \u0b47\u0b48  \u0b4b\u0b4c"
+            // F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....F.....
+            + "\u0b4d\u0b56abcdefghijklmnopqrstuvwxyz\u0b57\u0b60\u0b61\u0b62\u0b63",
+
+        /* A.3.10 Punjabi National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9A.BCD.EF.....0.....123.....4.....*/
+        "\u0a01\u0a02\u0a03\u0a05\u0a06\u0a07\u0a08\u0a09\u0a0a \n  \r \u0a0f\u0a10  \u0a13\u0a14"
+            // 5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....012.....3.....
+            + "\u0a15\u0a16\u0a17\u0a18\u0a19\u0a1a\uffff\u0a1b\u0a1c\u0a1d\u0a1e !\u0a1f\u0a20"
+            // 4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789ABCD.....E.....F0.....
+            + "\u0a21\u0a22\u0a23\u0a24)(\u0a25\u0a26,\u0a27.\u0a280123456789:; \u0a2a\u0a2b?\u0a2c"
+            // 1.....2.....3.....4.....56.....7.....89.....A.....BC.....D.....E.....F0.....1.....
+            + "\u0a2d\u0a2e\u0a2f\u0a30 \u0a32\u0a33 \u0a35\u0a36 \u0a38\u0a39\u0a3c \u0a3e\u0a3f"
+            // 2.....3.....4.....56789.....A.....BCD.....E.....F.....0.....123456789ABCDEF012345678
+            + "\u0a40\u0a41\u0a42    \u0a47\u0a48  \u0a4b\u0a4c\u0a4d\u0a51abcdefghijklmnopqrstuvwx"
+            // 9AB.....C.....D.....E.....F.....
+            + "yz\u0a70\u0a71\u0a72\u0a73\u0a74",
+
+        /* A.3.11 Tamil National Language Locking Shift Table
+         01.....2.....3.....4.....5.....6.....7.....8.....9A.BCD.E.....F.....0.....12.....3..... */
+        " \u0b82\u0b83\u0b85\u0b86\u0b87\u0b88\u0b89\u0b8a \n  \r\u0b8e\u0b8f\u0b90 \u0b92\u0b93"
+            // 4.....5.....6789.....A.....B.....CD.....EF.....012.....3456.....7.....89ABCDEF.....
+            + "\u0b94\u0b95   \u0b99\u0b9a\uffff \u0b9c \u0b9e !\u0b9f   \u0ba3\u0ba4)(  , .\u0ba8"
+            // 0123456789ABC.....D.....EF012.....3.....4.....5.....6.....7.....8.....9.....A.....
+            + "0123456789:;\u0ba9\u0baa ?  \u0bae\u0baf\u0bb0\u0bb1\u0bb2\u0bb3\u0bb4\u0bb5\u0bb6"
+            // B.....C.....D.....EF0.....1.....2.....3.....4.....5678.....9.....A.....BC.....D.....
+            + "\u0bb7\u0bb8\u0bb9  \u0bbe\u0bbf\u0bc0\u0bc1\u0bc2   \u0bc6\u0bc7\u0bc8 \u0bca\u0bcb"
+            // E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....F.....
+            + "\u0bcc\u0bcd\u0bd0abcdefghijklmnopqrstuvwxyz\u0bd7\u0bf0\u0bf1\u0bf2\u0bf9",
+
+        /* A.3.12 Telugu National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F.....0.....*/
+        "\u0c01\u0c02\u0c03\u0c05\u0c06\u0c07\u0c08\u0c09\u0c0a\u0c0b\n\u0c0c \r\u0c0e\u0c0f\u0c10"
+            // 12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....
+            + " \u0c12\u0c13\u0c14\u0c15\u0c16\u0c17\u0c18\u0c19\u0c1a\uffff\u0c1b\u0c1c\u0c1d"
+            // F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789AB
+            + "\u0c1e !\u0c1f\u0c20\u0c21\u0c22\u0c23\u0c24)(\u0c25\u0c26,\u0c27.\u0c280123456789:;"
+            // CD.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....89.....A.....B.....
+            + " \u0c2a\u0c2b?\u0c2c\u0c2d\u0c2e\u0c2f\u0c30\u0c31\u0c32\u0c33 \u0c35\u0c36\u0c37"
+            // C.....D.....EF.....0.....1.....2.....3.....4.....5.....6.....78.....9.....A.....B
+            + "\u0c38\u0c39 \u0c3d\u0c3e\u0c3f\u0c40\u0c41\u0c42\u0c43\u0c44 \u0c46\u0c47\u0c48 "
+            // C.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....
+            + "\u0c4a\u0c4b\u0c4c\u0c4d\u0c55abcdefghijklmnopqrstuvwxyz\u0c56\u0c60\u0c61\u0c62"
+            // F.....
+            + "\u0c63",
+
+        /* A.3.13 Urdu National Language Locking Shift Table
+         0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....*/
+        "\u0627\u0622\u0628\u067b\u0680\u067e\u06a6\u062a\u06c2\u067f\n\u0679\u067d\r\u067a\u067c"
+            // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....
+            + "\u062b\u062c\u0681\u0684\u0683\u0685\u0686\u0687\u062d\u062e\u062f\uffff\u068c\u0688"
+            // E.....F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.....012345
+            + "\u0689\u068a !\u068f\u068d\u0630\u0631\u0691\u0693)(\u0699\u0632,\u0696.\u0698012345"
+            // 6789ABC.....D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....8.....
+            + "6789:;\u069a\u0633\u0634?\u0635\u0636\u0637\u0638\u0639\u0641\u0642\u06a9\u06aa"
+            // 9.....A.....B.....C.....D.....E.....F.....0.....1.....2.....3.....4.....5.....6.....
+            + "\u06ab\u06af\u06b3\u06b1\u0644\u0645\u0646\u06ba\u06bb\u06bc\u0648\u06c4\u06d5\u06c1"
+            // 7.....8.....9.....A.....B.....C.....D.....E.....F.....0.....123456789ABCDEF012345678
+            + "\u06be\u0621\u06cc\u06d0\u06d2\u064d\u0650\u064f\u0657\u0654abcdefghijklmnopqrstuvwx"
+            // 9AB.....C.....D.....E.....F.....
+            + "yz\u0655\u0651\u0653\u0656\u0670"
+    };
+
+    /**
+     * GSM default extension table plus national language single shift character tables.
+     */
+    private static final String[] sLanguageShiftTables = new String[]{
+        /* 6.2.1.1 GSM 7 bit Default Alphabet Extension Table
+         0123456789A.....BCDEF0123456789ABCDEF0123456789ABCDEF.0123456789ABCDEF0123456789ABCDEF */
+        "          \u000c         ^                   {}     \\            [~] |               "
+            // 0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + "                     \u20ac                          ",
+
+        /* A.2.1 Turkish National Language Single Shift Table
+         0123456789A.....BCDEF0123456789ABCDEF0123456789ABCDEF.0123456789ABCDEF01234567.....8 */
+        "          \u000c         ^                   {}     \\            [~] |      \u011e "
+            // 9.....ABCDEF0123.....456789ABCDEF0123.....45.....67.....89.....ABCDEF0123.....
+            + "\u0130         \u015e               \u00e7 \u20ac \u011f \u0131         \u015f"
+            // 456789ABCDEF
+            + "            ",
+
+        /* A.2.2 Spanish National Language Single Shift Table
+         0123456789.....A.....BCDEF0123456789ABCDEF0123456789ABCDEF.0123456789ABCDEF01.....23 */
+        "         \u00e7\u000c         ^                   {}     \\            [~] |\u00c1  "
+            // 456789.....ABCDEF.....012345.....6789ABCDEF01.....2345.....6789.....ABCDEF.....012
+            + "     \u00cd     \u00d3     \u00da           \u00e1   \u20ac   \u00ed     \u00f3   "
+            // 345.....6789ABCDEF
+            + "  \u00fa          ",
+
+        /* A.2.3 Portuguese National Language Single Shift Table
+         012345.....6789.....A.....B.....C.....DE.....F.....012.....3.....45.....6.....7.....8....*/
+        "     \u00ea   \u00e7\u000c\u00d4\u00f4 \u00c1\u00e1  \u03a6\u0393^\u03a9\u03a0\u03a8\u03a3"
+            // 9.....ABCDEF.....0123456789ABCDEF.0123456789ABCDEF01.....23456789.....ABCDE
+            + "\u0398     \u00ca        {}     \\            [~] |\u00c0       \u00cd     "
+            // F.....012345.....6789AB.....C.....DEF01.....2345.....6789.....ABCDEF.....01234
+            + "\u00d3     \u00da     \u00c3\u00d5    \u00c2   \u20ac   \u00ed     \u00f3     "
+            // 5.....6789AB.....C.....DEF.....
+            + "\u00fa     \u00e3\u00f5  \u00e2",
+
+        /* A.2.4 Bengali National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u09e6\u09e7 \u09e8\u09e9"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....
+            + "\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u09df\u09e0\u09e1\u09e2{}\u09e3\u09f2\u09f3"
+            // D.....E.....F.0.....1.....2.....3.....4.....56789ABCDEF0123456789ABCDEF
+            + "\u09f4\u09f5\\\u09f6\u09f7\u09f8\u09f9\u09fa       [~] |ABCDEFGHIJKLMNO"
+            // 0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + "PQRSTUVWXYZ          \u20ac                          ",
+
+        /* A.2.5 Gujarati National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0ae6\u0ae7"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6789ABCDEF.0123456789ABCDEF
+            + "\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef  {}     \\            [~] "
+            // 0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + "|ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                          ",
+
+        /* A.2.6 Hindi National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0966\u0967"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....
+            + "\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0951\u0952{}\u0953\u0954\u0958"
+            // D.....E.....F.0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....
+            + "\u0959\u095a\\\u095b\u095c\u095d\u095e\u095f\u0960\u0961\u0962\u0963\u0970\u0971"
+            // BCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + " [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                          ",
+
+        /* A.2.7 Kannada National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0ce6\u0ce7"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....BCDEF.01234567
+            + "\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0cde\u0cf1{}\u0cf2    \\        "
+            // 89ABCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + "    [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                          ",
+
+        /* A.2.8 Malayalam National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0d66\u0d67"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....
+            + "\u0d68\u0d69\u0d6a\u0d6b\u0d6c\u0d6d\u0d6e\u0d6f\u0d70\u0d71{}\u0d72\u0d73\u0d74"
+            // D.....E.....F.0.....1.....2.....3.....4.....56789ABCDEF0123456789ABCDEF0123456789A
+            + "\u0d75\u0d7a\\\u0d7b\u0d7c\u0d7d\u0d7e\u0d7f       [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            // BCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + "          \u20ac                          ",
+
+        /* A.2.9 Oriya National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0b66\u0b67"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....DE
+            + "\u0b68\u0b69\u0b6a\u0b6b\u0b6c\u0b6d\u0b6e\u0b6f\u0b5c\u0b5d{}\u0b5f\u0b70\u0b71  "
+            // F.0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789A
+            + "\\            [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                     "
+            // BCDEF
+            + "     ",
+
+        /* A.2.10 Punjabi National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0a66\u0a67"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....
+            + "\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a59\u0a5a{}\u0a5b\u0a5c\u0a5e"
+            // D.....EF.0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF01
+            + "\u0a75 \\            [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac            "
+            // 23456789ABCDEF
+            + "              ",
+
+        /* A.2.11 Tamil National Language Single Shift Table
+           NOTE: TS 23.038 V9.1.1 shows code 0x24 as \u0bef, corrected to \u0bee (typo)
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0be6\u0be7"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....
+            + "\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0bf3\u0bf4{}\u0bf5\u0bf6\u0bf7"
+            // D.....E.....F.0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABC
+            + "\u0bf8\u0bfa\\            [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac       "
+            // DEF0123456789ABCDEF
+            + "                   ",
+
+        /* A.2.12 Telugu National Language Single Shift Table
+           NOTE: TS 23.038 V9.1.1 shows code 0x22-0x23 as \u06cc\u06cd, corrected to \u0c6c\u0c6d
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789ABC.....D.....E.....F..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*   \u0c66\u0c67\u0c68\u0c69"
+            // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F.
+            + "\u0c6a\u0c6b\u0c6c\u0c6d\u0c6e\u0c6f\u0c58\u0c59{}\u0c78\u0c79\u0c7a\u0c7b\u0c7c\\"
+            // 0.....1.....2.....3456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCD
+            + "\u0c7d\u0c7e\u0c7f         [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac        "
+            // EF0123456789ABCDEF
+            + "                  ",
+
+        /* A.2.13 Urdu National Language Single Shift Table
+         01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D..... */
+        "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0600\u0601 \u06f0\u06f1"
+            // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....
+            + "\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u060c\u060d{}\u060e\u060f\u0610"
+            // D.....E.....F.0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....
+            + "\u0611\u0612\\\u0613\u0614\u061b\u061f\u0640\u0652\u0658\u066b\u066c\u0672\u0673"
+            // B.....CDEF.....0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+            + "\u06cd[~]\u06d4|ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                          "
+    };
+
+    static {
+        enableCountrySpecificEncodings();
+        int numTables = sLanguageTables.length;
+        int numShiftTables = sLanguageShiftTables.length;
+        if (numTables != numShiftTables) {
+            Rlog.e(TAG, "Error: language tables array length " + numTables +
+                    " != shift tables array length " + numShiftTables);
+        }
+
+        sCharsToGsmTables = new SparseIntArray[numTables];
+        for (int i = 0; i < numTables; i++) {
+            String table = sLanguageTables[i];
+
+            int tableLen = table.length();
+            if (tableLen != 0 && tableLen != 128) {
+                Rlog.e(TAG, "Error: language tables index " + i +
+                        " length " + tableLen + " (expected 128 or 0)");
+            }
+
+            SparseIntArray charToGsmTable = new SparseIntArray(tableLen);
+            sCharsToGsmTables[i] = charToGsmTable;
+            for (int j = 0; j < tableLen; j++) {
+                char c = table.charAt(j);
+                charToGsmTable.put(c, j);
+            }
+        }
+
+        sCharsToShiftTables = new SparseIntArray[numTables];
+        for (int i = 0; i < numShiftTables; i++) {
+            String shiftTable = sLanguageShiftTables[i];
+
+            int shiftTableLen = shiftTable.length();
+            if (shiftTableLen != 0 && shiftTableLen != 128) {
+                Rlog.e(TAG, "Error: language shift tables index " + i +
+                        " length " + shiftTableLen + " (expected 128 or 0)");
+            }
+
+            SparseIntArray charToShiftTable = new SparseIntArray(shiftTableLen);
+            sCharsToShiftTables[i] = charToShiftTable;
+            for (int j = 0; j < shiftTableLen; j++) {
+                char c = shiftTable.charAt(j);
+                if (c != ' ') {
+                    charToShiftTable.put(c, j);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/GsmCdmaCall.java b/com/android/internal/telephony/GsmCdmaCall.java
new file mode 100644
index 0000000..d671ef0
--- /dev/null
+++ b/com/android/internal/telephony/GsmCdmaCall.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public class GsmCdmaCall extends Call {
+    /*************************** Instance Variables **************************/
+
+    /*package*/ GsmCdmaCallTracker mOwner;
+
+    /****************************** Constructors *****************************/
+    /*package*/
+    public GsmCdmaCall (GsmCdmaCallTracker owner) {
+        mOwner = owner;
+    }
+
+    /************************** Overridden from Call *************************/
+
+    @Override
+    public List<Connection> getConnections() {
+        // FIXME should return Collections.unmodifiableList();
+        return mConnections;
+    }
+
+    @Override
+    public Phone getPhone() {
+        return mOwner.getPhone();
+    }
+
+    @Override
+    public boolean isMultiparty() {
+        return mConnections.size() > 1;
+    }
+
+    /** Please note: if this is the foreground call and a
+     *  background call exists, the background call will be resumed
+     *  because an AT+CHLD=1 will be sent
+     */
+    @Override
+    public void hangup() throws CallStateException {
+        mOwner.hangup(this);
+    }
+
+    @Override
+    public String toString() {
+        return mState.toString();
+    }
+
+    //***** Called from GsmCdmaConnection
+
+    public void attach(Connection conn, DriverCall dc) {
+        mConnections.add(conn);
+
+        mState = stateFromDCState (dc.state);
+    }
+
+    public void attachFake(Connection conn, State state) {
+        mConnections.add(conn);
+
+        mState = state;
+    }
+
+    /**
+     * Called by GsmCdmaConnection when it has disconnected
+     */
+    public boolean connectionDisconnected(GsmCdmaConnection conn) {
+        if (mState != State.DISCONNECTED) {
+            /* If only disconnected connections remain, we are disconnected*/
+
+            boolean hasOnlyDisconnectedConnections = true;
+
+            for (int i = 0, s = mConnections.size(); i < s; i ++) {
+                if (mConnections.get(i).getState() != State.DISCONNECTED) {
+                    hasOnlyDisconnectedConnections = false;
+                    break;
+                }
+            }
+
+            if (hasOnlyDisconnectedConnections) {
+                mState = State.DISCONNECTED;
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public void detach(GsmCdmaConnection conn) {
+        mConnections.remove(conn);
+
+        if (mConnections.size() == 0) {
+            mState = State.IDLE;
+        }
+    }
+
+    /*package*/ boolean update (GsmCdmaConnection conn, DriverCall dc) {
+        State newState;
+        boolean changed = false;
+
+        newState = stateFromDCState(dc.state);
+
+        if (newState != mState) {
+            mState = newState;
+            changed = true;
+        }
+
+        return changed;
+    }
+
+    /**
+     * @return true if there's no space in this call for additional
+     * connections to be added via "conference"
+     */
+    /*package*/ boolean isFull() {
+        return mConnections.size() == mOwner.getMaxConnectionsPerCall();
+    }
+
+    //***** Called from GsmCdmaCallTracker
+
+
+    /**
+     * Called when this Call is being hung up locally (eg, user pressed "end")
+     * Note that at this point, the hangup request has been dispatched to the radio
+     * but no response has yet been received so update() has not yet been called
+     */
+    void onHangupLocal() {
+        for (int i = 0, s = mConnections.size(); i < s; i++) {
+            GsmCdmaConnection cn = (GsmCdmaConnection)mConnections.get(i);
+
+            cn.onHangupLocal();
+        }
+        mState = State.DISCONNECTING;
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/GsmCdmaCallTracker.java b/com/android/internal/telephony/GsmCdmaCallTracker.java
new file mode 100644
index 0000000..5960051
--- /dev/null
+++ b/com/android/internal/telephony/GsmCdmaCallTracker.java
@@ -0,0 +1,1693 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.SystemProperties;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellLocation;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaCellLocation;
+import android.telephony.gsm.GsmCellLocation;
+import android.text.TextUtils;
+import android.util.EventLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public class GsmCdmaCallTracker extends CallTracker {
+    private static final String LOG_TAG = "GsmCdmaCallTracker";
+    private static final boolean REPEAT_POLLING = false;
+
+    private static final boolean DBG_POLL = false;
+    private static final boolean VDBG = false;
+
+    //***** Constants
+
+    public static final int MAX_CONNECTIONS_GSM = 19;   //7 allowed in GSM + 12 from IMS for SRVCC
+    private static final int MAX_CONNECTIONS_PER_CALL_GSM = 5; //only 5 connections allowed per call
+
+    private static final int MAX_CONNECTIONS_CDMA = 8;
+    private static final int MAX_CONNECTIONS_PER_CALL_CDMA = 1; //only 1 connection allowed per call
+
+    //***** Instance Variables
+    @VisibleForTesting
+    public GsmCdmaConnection[] mConnections;
+    private RegistrantList mVoiceCallEndedRegistrants = new RegistrantList();
+    private RegistrantList mVoiceCallStartedRegistrants = new RegistrantList();
+
+    // connections dropped during last poll
+    private ArrayList<GsmCdmaConnection> mDroppedDuringPoll =
+            new ArrayList<GsmCdmaConnection>(MAX_CONNECTIONS_GSM);
+
+    public GsmCdmaCall mRingingCall = new GsmCdmaCall(this);
+    // A call that is ringing or (call) waiting
+    public GsmCdmaCall mForegroundCall = new GsmCdmaCall(this);
+    public GsmCdmaCall mBackgroundCall = new GsmCdmaCall(this);
+
+    private GsmCdmaConnection mPendingMO;
+    private boolean mHangupPendingMO;
+
+    private GsmCdmaPhone mPhone;
+
+    private boolean mDesiredMute = false;    // false = mute off
+
+    public PhoneConstants.State mState = PhoneConstants.State.IDLE;
+
+    private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();
+
+    // Following member variables are for CDMA only
+    private RegistrantList mCallWaitingRegistrants = new RegistrantList();
+    private boolean mPendingCallInEcm;
+    private boolean mIsInEmergencyCall;
+    private int mPendingCallClirMode;
+    private boolean mIsEcmTimerCanceled;
+    private int m3WayCallFlashDelay;
+
+    /**
+     * Listens for Emergency Callback Mode state change intents
+     */
+    private BroadcastReceiver mEcmExitReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(
+                    TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED)) {
+
+                boolean isInEcm = intent.getBooleanExtra(PhoneConstants.PHONE_IN_ECM_STATE, false);
+                log("Received ACTION_EMERGENCY_CALLBACK_MODE_CHANGED isInEcm = " + isInEcm);
+
+                // If we exit ECM mode, notify all connections.
+                if (!isInEcm) {
+                    // Although mConnections seems to be the place to look, it is not guaranteed
+                    // to have all of the connections we're tracking.  THe best place to look is in
+                    // the Call objects associated with the tracker.
+                    List<Connection> toNotify = new ArrayList<Connection>();
+                    toNotify.addAll(mRingingCall.getConnections());
+                    toNotify.addAll(mForegroundCall.getConnections());
+                    toNotify.addAll(mBackgroundCall.getConnections());
+                    if (mPendingMO != null) {
+                        toNotify.add(mPendingMO);
+                    }
+
+                    // Notify connections that ECM mode exited.
+                    for (Connection connection : toNotify) {
+                        if (connection != null) {
+                            connection.onExitedEcmMode();
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    //***** Events
+
+
+    //***** Constructors
+
+    public GsmCdmaCallTracker (GsmCdmaPhone phone) {
+        this.mPhone = phone;
+        mCi = phone.mCi;
+        mCi.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null);
+        mCi.registerForOn(this, EVENT_RADIO_AVAILABLE, null);
+        mCi.registerForNotAvailable(this, EVENT_RADIO_NOT_AVAILABLE, null);
+
+        // Register receiver for ECM exit
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
+        mPhone.getContext().registerReceiver(mEcmExitReceiver, filter);
+
+        updatePhoneType(true);
+    }
+
+    public void updatePhoneType() {
+        updatePhoneType(false);
+    }
+
+    private void updatePhoneType(boolean duringInit) {
+        if (!duringInit) {
+            reset();
+            pollCallsWhenSafe();
+        }
+        if (mPhone.isPhoneTypeGsm()) {
+            mConnections = new GsmCdmaConnection[MAX_CONNECTIONS_GSM];
+            mCi.unregisterForCallWaitingInfo(this);
+            // Prior to phone switch to GSM, if CDMA has any emergency call
+            // data will be in disabled state, after switching to GSM enable data.
+            if (mIsInEmergencyCall) {
+                mPhone.mDcTracker.setInternalDataEnabled(true);
+            }
+        } else {
+            mConnections = new GsmCdmaConnection[MAX_CONNECTIONS_CDMA];
+            mPendingCallInEcm = false;
+            mIsInEmergencyCall = false;
+            mPendingCallClirMode = CommandsInterface.CLIR_DEFAULT;
+            mIsEcmTimerCanceled = false;
+            m3WayCallFlashDelay = 0;
+            mCi.registerForCallWaitingInfo(this, EVENT_CALL_WAITING_INFO_CDMA, null);
+        }
+    }
+
+    private void reset() {
+        Rlog.d(LOG_TAG, "reset");
+
+        for (GsmCdmaConnection gsmCdmaConnection : mConnections) {
+            if (gsmCdmaConnection != null) {
+                gsmCdmaConnection.onDisconnect(DisconnectCause.ERROR_UNSPECIFIED);
+                gsmCdmaConnection.dispose();
+            }
+        }
+
+        if (mPendingMO != null) {
+            mPendingMO.dispose();
+        }
+
+        mConnections = null;
+        mPendingMO = null;
+        clearDisconnected();
+    }
+
+    @Override
+    protected void finalize() {
+        Rlog.d(LOG_TAG, "GsmCdmaCallTracker finalized");
+    }
+
+    //***** Instance Methods
+
+    //***** Public Methods
+    @Override
+    public void registerForVoiceCallStarted(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mVoiceCallStartedRegistrants.add(r);
+        // Notify if in call when registering
+        if (mState != PhoneConstants.State.IDLE) {
+            r.notifyRegistrant(new AsyncResult(null, null, null));
+        }
+    }
+
+    @Override
+    public void unregisterForVoiceCallStarted(Handler h) {
+        mVoiceCallStartedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForVoiceCallEnded(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mVoiceCallEndedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForVoiceCallEnded(Handler h) {
+        mVoiceCallEndedRegistrants.remove(h);
+    }
+
+    public void registerForCallWaiting(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mCallWaitingRegistrants.add(r);
+    }
+
+    public void unregisterForCallWaiting(Handler h) {
+        mCallWaitingRegistrants.remove(h);
+    }
+
+    private void fakeHoldForegroundBeforeDial() {
+        List<Connection> connCopy;
+
+        // We need to make a copy here, since fakeHoldBeforeDial()
+        // modifies the lists, and we don't want to reverse the order
+        connCopy = (List<Connection>) mForegroundCall.mConnections.clone();
+
+        for (int i = 0, s = connCopy.size() ; i < s ; i++) {
+            GsmCdmaConnection conn = (GsmCdmaConnection)connCopy.get(i);
+
+            conn.fakeHoldBeforeDial();
+        }
+    }
+
+    //GSM
+    /**
+     * clirMode is one of the CLIR_ constants
+     */
+    public synchronized Connection dial(String dialString, int clirMode, UUSInfo uusInfo,
+                                        Bundle intentExtras)
+            throws CallStateException {
+        // note that this triggers call state changed notif
+        clearDisconnected();
+
+        if (!canDial()) {
+            throw new CallStateException("cannot dial in current state");
+        }
+
+        String origNumber = dialString;
+        dialString = convertNumberIfNecessary(mPhone, dialString);
+
+        // The new call must be assigned to the foreground call.
+        // That call must be idle, so place anything that's
+        // there on hold
+        if (mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE) {
+            // this will probably be done by the radio anyway
+            // but the dial might fail before this happens
+            // and we need to make sure the foreground call is clear
+            // for the newly dialed connection
+            switchWaitingOrHoldingAndActive();
+            // This is a hack to delay DIAL so that it is sent out to RIL only after
+            // EVENT_SWITCH_RESULT is received. We've seen failures when adding a new call to
+            // multi-way conference calls due to DIAL being sent out before SWITCH is processed
+            try {
+                Thread.sleep(500);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+
+            // Fake local state so that
+            // a) foregroundCall is empty for the newly dialed connection
+            // b) hasNonHangupStateChanged remains false in the
+            // next poll, so that we don't clear a failed dialing call
+            fakeHoldForegroundBeforeDial();
+        }
+
+        if (mForegroundCall.getState() != GsmCdmaCall.State.IDLE) {
+            //we should have failed in !canDial() above before we get here
+            throw new CallStateException("cannot dial in current state");
+        }
+        boolean isEmergencyCall = PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(),
+                dialString);
+        mPendingMO = new GsmCdmaConnection(mPhone, checkForTestEmergencyNumber(dialString),
+                this, mForegroundCall, isEmergencyCall);
+        mHangupPendingMO = false;
+        mMetrics.writeRilDial(mPhone.getPhoneId(), mPendingMO, clirMode, uusInfo);
+
+
+        if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0
+                || mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0) {
+            // Phone number is invalid
+            mPendingMO.mCause = DisconnectCause.INVALID_NUMBER;
+
+            // handlePollCalls() will notice this call not present
+            // and will mark it as dropped.
+            pollCallsWhenSafe();
+        } else {
+            // Always unmute when initiating a new call
+            setMute(false);
+
+            mCi.dial(mPendingMO.getAddress(), clirMode, uusInfo, obtainCompleteMessage());
+        }
+
+        if (mNumberConverted) {
+            mPendingMO.setConverted(origNumber);
+            mNumberConverted = false;
+        }
+
+        updatePhoneState();
+        mPhone.notifyPreciseCallStateChanged();
+
+        return mPendingMO;
+    }
+
+    //CDMA
+    /**
+     * Handle Ecm timer to be canceled or re-started
+     */
+    private void handleEcmTimer(int action) {
+        mPhone.handleTimerInEmergencyCallbackMode(action);
+        switch(action) {
+            case GsmCdmaPhone.CANCEL_ECM_TIMER: mIsEcmTimerCanceled = true; break;
+            case GsmCdmaPhone.RESTART_ECM_TIMER: mIsEcmTimerCanceled = false; break;
+            default:
+                Rlog.e(LOG_TAG, "handleEcmTimer, unsupported action " + action);
+        }
+    }
+
+    //CDMA
+    /**
+     * Disable data call when emergency call is connected
+     */
+    private void disableDataCallInEmergencyCall(String dialString) {
+        if (PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(), dialString)) {
+            if (Phone.DEBUG_PHONE) log("disableDataCallInEmergencyCall");
+            setIsInEmergencyCall();
+        }
+    }
+
+    //CDMA
+    public void setIsInEmergencyCall() {
+        mIsInEmergencyCall = true;
+        mPhone.mDcTracker.setInternalDataEnabled(false);
+        mPhone.notifyEmergencyCallRegistrants(true);
+        mPhone.sendEmergencyCallStateChange(true);
+    }
+
+    //CDMA
+    /**
+     * clirMode is one of the CLIR_ constants
+     */
+    private Connection dial(String dialString, int clirMode) throws CallStateException {
+        // note that this triggers call state changed notif
+        clearDisconnected();
+
+        if (!canDial()) {
+            throw new CallStateException("cannot dial in current state");
+        }
+
+        TelephonyManager tm =
+                (TelephonyManager) mPhone.getContext().getSystemService(Context.TELEPHONY_SERVICE);
+        String origNumber = dialString;
+        String operatorIsoContry = tm.getNetworkCountryIsoForPhone(mPhone.getPhoneId());
+        String simIsoContry = tm.getSimCountryIsoForPhone(mPhone.getPhoneId());
+        boolean internationalRoaming = !TextUtils.isEmpty(operatorIsoContry)
+                && !TextUtils.isEmpty(simIsoContry)
+                && !simIsoContry.equals(operatorIsoContry);
+        if (internationalRoaming) {
+            if ("us".equals(simIsoContry)) {
+                internationalRoaming = internationalRoaming && !"vi".equals(operatorIsoContry);
+            } else if ("vi".equals(simIsoContry)) {
+                internationalRoaming = internationalRoaming && !"us".equals(operatorIsoContry);
+            }
+        }
+        if (internationalRoaming) {
+            dialString = convertNumberIfNecessary(mPhone, dialString);
+        }
+
+        boolean isPhoneInEcmMode = mPhone.isInEcm();
+        boolean isEmergencyCall =
+                PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(), dialString);
+
+        // Cancel Ecm timer if a second emergency call is originating in Ecm mode
+        if (isPhoneInEcmMode && isEmergencyCall) {
+            handleEcmTimer(GsmCdmaPhone.CANCEL_ECM_TIMER);
+        }
+
+        // The new call must be assigned to the foreground call.
+        // That call must be idle, so place anything that's
+        // there on hold
+        if (mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE) {
+            return dialThreeWay(dialString);
+        }
+
+        mPendingMO = new GsmCdmaConnection(mPhone, checkForTestEmergencyNumber(dialString),
+                this, mForegroundCall, isEmergencyCall);
+        mHangupPendingMO = false;
+
+        if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0
+                || mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0 ) {
+            // Phone number is invalid
+            mPendingMO.mCause = DisconnectCause.INVALID_NUMBER;
+
+            // handlePollCalls() will notice this call not present
+            // and will mark it as dropped.
+            pollCallsWhenSafe();
+        } else {
+            // Always unmute when initiating a new call
+            setMute(false);
+
+            // Check data call
+            disableDataCallInEmergencyCall(dialString);
+
+            // In Ecm mode, if another emergency call is dialed, Ecm mode will not exit.
+            if(!isPhoneInEcmMode || (isPhoneInEcmMode && isEmergencyCall)) {
+                mCi.dial(mPendingMO.getAddress(), clirMode, obtainCompleteMessage());
+            } else {
+                mPhone.exitEmergencyCallbackMode();
+                mPhone.setOnEcbModeExitResponse(this,EVENT_EXIT_ECM_RESPONSE_CDMA, null);
+                mPendingCallClirMode=clirMode;
+                mPendingCallInEcm=true;
+            }
+        }
+
+        if (mNumberConverted) {
+            mPendingMO.setConverted(origNumber);
+            mNumberConverted = false;
+        }
+
+        updatePhoneState();
+        mPhone.notifyPreciseCallStateChanged();
+
+        return mPendingMO;
+    }
+
+    //CDMA
+    private Connection dialThreeWay(String dialString) {
+        if (!mForegroundCall.isIdle()) {
+            // Check data call and possibly set mIsInEmergencyCall
+            disableDataCallInEmergencyCall(dialString);
+
+            // Attach the new connection to foregroundCall
+            mPendingMO = new GsmCdmaConnection(mPhone,
+                    checkForTestEmergencyNumber(dialString), this, mForegroundCall,
+                    mIsInEmergencyCall);
+            // Some networks need an empty flash before sending the normal one
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            PersistableBundle bundle = configManager.getConfig();
+            if (bundle != null) {
+                m3WayCallFlashDelay =
+                        bundle.getInt(CarrierConfigManager.KEY_CDMA_3WAYCALL_FLASH_DELAY_INT);
+            } else {
+                // The default 3-way call flash delay is 0s
+                m3WayCallFlashDelay = 0;
+            }
+            if (m3WayCallFlashDelay > 0) {
+                mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_THREE_WAY_DIAL_BLANK_FLASH));
+            } else {
+                mCi.sendCDMAFeatureCode(mPendingMO.getAddress(),
+                        obtainMessage(EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA));
+            }
+            return mPendingMO;
+        }
+        return null;
+    }
+
+    public Connection dial(String dialString) throws CallStateException {
+        if (isPhoneTypeGsm()) {
+            return dial(dialString, CommandsInterface.CLIR_DEFAULT, null);
+        } else {
+            return dial(dialString, CommandsInterface.CLIR_DEFAULT);
+        }
+    }
+
+    //GSM
+    public Connection dial(String dialString, UUSInfo uusInfo, Bundle intentExtras)
+            throws CallStateException {
+        return dial(dialString, CommandsInterface.CLIR_DEFAULT, uusInfo, intentExtras);
+    }
+
+    //GSM
+    private Connection dial(String dialString, int clirMode, Bundle intentExtras)
+            throws CallStateException {
+        return dial(dialString, clirMode, null, intentExtras);
+    }
+
+    public void acceptCall() throws CallStateException {
+        // FIXME if SWITCH fails, should retry with ANSWER
+        // in case the active/holding call disappeared and this
+        // is no longer call waiting
+
+        if (mRingingCall.getState() == GsmCdmaCall.State.INCOMING) {
+            Rlog.i("phone", "acceptCall: incoming...");
+            // Always unmute when answering a new call
+            setMute(false);
+            mCi.acceptCall(obtainCompleteMessage());
+        } else if (mRingingCall.getState() == GsmCdmaCall.State.WAITING) {
+            if (isPhoneTypeGsm()) {
+                setMute(false);
+            } else {
+                GsmCdmaConnection cwConn = (GsmCdmaConnection)(mRingingCall.getLatestConnection());
+
+                // Since there is no network response for supplimentary
+                // service for CDMA, we assume call waiting is answered.
+                // ringing Call state change to idle is in GsmCdmaCall.detach
+                // triggered by updateParent.
+                cwConn.updateParent(mRingingCall, mForegroundCall);
+                cwConn.onConnectedInOrOut();
+                updatePhoneState();
+            }
+            switchWaitingOrHoldingAndActive();
+        } else {
+            throw new CallStateException("phone not ringing");
+        }
+    }
+
+    public void rejectCall() throws CallStateException {
+        // AT+CHLD=0 means "release held or UDUB"
+        // so if the phone isn't ringing, this could hang up held
+        if (mRingingCall.getState().isRinging()) {
+            mCi.rejectCall(obtainCompleteMessage());
+        } else {
+            throw new CallStateException("phone not ringing");
+        }
+    }
+
+    //CDMA
+    private void flashAndSetGenericTrue() {
+        mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT));
+
+        mPhone.notifyPreciseCallStateChanged();
+    }
+
+    public void switchWaitingOrHoldingAndActive() throws CallStateException {
+        // Should we bother with this check?
+        if (mRingingCall.getState() == GsmCdmaCall.State.INCOMING) {
+            throw new CallStateException("cannot be in the incoming state");
+        } else {
+            if (isPhoneTypeGsm()) {
+                mCi.switchWaitingOrHoldingAndActive(
+                        obtainCompleteMessage(EVENT_SWITCH_RESULT));
+            } else {
+                if (mForegroundCall.getConnections().size() > 1) {
+                    flashAndSetGenericTrue();
+                } else {
+                    // Send a flash command to CDMA network for putting the other party on hold.
+                    // For CDMA networks which do not support this the user would just hear a beep
+                    // from the network. For CDMA networks which do support it will put the other
+                    // party on hold.
+                    mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT));
+                }
+            }
+        }
+    }
+
+    public void conference() {
+        if (isPhoneTypeGsm()) {
+            mCi.conference(obtainCompleteMessage(EVENT_CONFERENCE_RESULT));
+        } else {
+            // Should we be checking state?
+            flashAndSetGenericTrue();
+        }
+    }
+
+    public void explicitCallTransfer() {
+        mCi.explicitCallTransfer(obtainCompleteMessage(EVENT_ECT_RESULT));
+    }
+
+    public void clearDisconnected() {
+        internalClearDisconnected();
+
+        updatePhoneState();
+        mPhone.notifyPreciseCallStateChanged();
+    }
+
+    public boolean canConference() {
+        return mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE
+                && mBackgroundCall.getState() == GsmCdmaCall.State.HOLDING
+                && !mBackgroundCall.isFull()
+                && !mForegroundCall.isFull();
+    }
+
+    private boolean canDial() {
+        boolean ret;
+        int serviceState = mPhone.getServiceState().getState();
+        String disableCall = SystemProperties.get(
+                TelephonyProperties.PROPERTY_DISABLE_CALL, "false");
+
+        ret = (serviceState != ServiceState.STATE_POWER_OFF)
+                && mPendingMO == null
+                && !mRingingCall.isRinging()
+                && !disableCall.equals("true")
+                && (!mForegroundCall.getState().isAlive()
+                    || !mBackgroundCall.getState().isAlive()
+                    || (!isPhoneTypeGsm()
+                        && mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE));
+
+        if (!ret) {
+            log(String.format("canDial is false\n" +
+                            "((serviceState=%d) != ServiceState.STATE_POWER_OFF)::=%s\n" +
+                            "&& pendingMO == null::=%s\n" +
+                            "&& !ringingCall.isRinging()::=%s\n" +
+                            "&& !disableCall.equals(\"true\")::=%s\n" +
+                            "&& (!foregroundCall.getState().isAlive()::=%s\n" +
+                            "   || foregroundCall.getState() == GsmCdmaCall.State.ACTIVE::=%s\n" +
+                            "   ||!backgroundCall.getState().isAlive())::=%s)",
+                    serviceState,
+                    serviceState != ServiceState.STATE_POWER_OFF,
+                    mPendingMO == null,
+                    !mRingingCall.isRinging(),
+                    !disableCall.equals("true"),
+                    !mForegroundCall.getState().isAlive(),
+                    mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE,
+                    !mBackgroundCall.getState().isAlive()));
+        }
+
+        return ret;
+    }
+
+    public boolean canTransfer() {
+        if (isPhoneTypeGsm()) {
+            return (mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE
+                    || mForegroundCall.getState() == GsmCdmaCall.State.ALERTING
+                    || mForegroundCall.getState() == GsmCdmaCall.State.DIALING)
+                    && mBackgroundCall.getState() == GsmCdmaCall.State.HOLDING;
+        } else {
+            Rlog.e(LOG_TAG, "canTransfer: not possible in CDMA");
+            return false;
+        }
+    }
+
+    //***** Private Instance Methods
+
+    private void internalClearDisconnected() {
+        mRingingCall.clearDisconnected();
+        mForegroundCall.clearDisconnected();
+        mBackgroundCall.clearDisconnected();
+    }
+
+    /**
+     * Obtain a message to use for signalling "invoke getCurrentCalls() when
+     * this operation and all other pending operations are complete
+     */
+    private Message obtainCompleteMessage() {
+        return obtainCompleteMessage(EVENT_OPERATION_COMPLETE);
+    }
+
+    /**
+     * Obtain a message to use for signalling "invoke getCurrentCalls() when
+     * this operation and all other pending operations are complete
+     */
+    private Message obtainCompleteMessage(int what) {
+        mPendingOperations++;
+        mLastRelevantPoll = null;
+        mNeedsPoll = true;
+
+        if (DBG_POLL) log("obtainCompleteMessage: pendingOperations=" +
+                mPendingOperations + ", needsPoll=" + mNeedsPoll);
+
+        return obtainMessage(what);
+    }
+
+    private void operationComplete() {
+        mPendingOperations--;
+
+        if (DBG_POLL) log("operationComplete: pendingOperations=" +
+                mPendingOperations + ", needsPoll=" + mNeedsPoll);
+
+        if (mPendingOperations == 0 && mNeedsPoll) {
+            mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);
+            mCi.getCurrentCalls(mLastRelevantPoll);
+        } else if (mPendingOperations < 0) {
+            // this should never happen
+            Rlog.e(LOG_TAG,"GsmCdmaCallTracker.pendingOperations < 0");
+            mPendingOperations = 0;
+        }
+    }
+
+    private void updatePhoneState() {
+        PhoneConstants.State oldState = mState;
+        if (mRingingCall.isRinging()) {
+            mState = PhoneConstants.State.RINGING;
+        } else if (mPendingMO != null ||
+                !(mForegroundCall.isIdle() && mBackgroundCall.isIdle())) {
+            mState = PhoneConstants.State.OFFHOOK;
+        } else {
+            Phone imsPhone = mPhone.getImsPhone();
+            if ( mState == PhoneConstants.State.OFFHOOK && (imsPhone != null)){
+                imsPhone.callEndCleanupHandOverCallIfAny();
+            }
+            mState = PhoneConstants.State.IDLE;
+        }
+
+        if (mState == PhoneConstants.State.IDLE && oldState != mState) {
+            mVoiceCallEndedRegistrants.notifyRegistrants(
+                new AsyncResult(null, null, null));
+        } else if (oldState == PhoneConstants.State.IDLE && oldState != mState) {
+            mVoiceCallStartedRegistrants.notifyRegistrants (
+                    new AsyncResult(null, null, null));
+        }
+        if (Phone.DEBUG_PHONE) {
+            log("update phone state, old=" + oldState + " new="+ mState);
+        }
+        if (mState != oldState) {
+            mPhone.notifyPhoneStateChanged();
+            mMetrics.writePhoneState(mPhone.getPhoneId(), mState);
+        }
+    }
+
+    // ***** Overwritten from CallTracker
+
+    @Override
+    protected synchronized void handlePollCalls(AsyncResult ar) {
+        List polledCalls;
+
+        if (VDBG) log("handlePollCalls");
+        if (ar.exception == null) {
+            polledCalls = (List)ar.result;
+        } else if (isCommandExceptionRadioNotAvailable(ar.exception)) {
+            // just a dummy empty ArrayList to cause the loop
+            // to hang up all the calls
+            polledCalls = new ArrayList();
+        } else {
+            // Radio probably wasn't ready--try again in a bit
+            // But don't keep polling if the channel is closed
+            pollCallsAfterDelay();
+            return;
+        }
+
+        Connection newRinging = null; //or waiting
+        ArrayList<Connection> newUnknownConnectionsGsm = new ArrayList<Connection>();
+        Connection newUnknownConnectionCdma = null;
+        boolean hasNonHangupStateChanged = false;   // Any change besides
+                                                    // a dropped connection
+        boolean hasAnyCallDisconnected = false;
+        boolean needsPollDelay = false;
+        boolean unknownConnectionAppeared = false;
+        int handoverConnectionsSize = mHandoverConnections.size();
+
+        //CDMA
+        boolean noConnectionExists = true;
+
+        for (int i = 0, curDC = 0, dcSize = polledCalls.size()
+                ; i < mConnections.length; i++) {
+            GsmCdmaConnection conn = mConnections[i];
+            DriverCall dc = null;
+
+            // polledCall list is sparse
+            if (curDC < dcSize) {
+                dc = (DriverCall) polledCalls.get(curDC);
+
+                if (dc.index == i+1) {
+                    curDC++;
+                } else {
+                    dc = null;
+                }
+            }
+
+            //CDMA
+            if (conn != null || dc != null) {
+                noConnectionExists = false;
+            }
+
+            if (DBG_POLL) log("poll: conn[i=" + i + "]=" +
+                    conn+", dc=" + dc);
+
+            if (conn == null && dc != null) {
+                // Connection appeared in CLCC response that we don't know about
+                if (mPendingMO != null && mPendingMO.compareTo(dc)) {
+
+                    if (DBG_POLL) log("poll: pendingMO=" + mPendingMO);
+
+                    // It's our pending mobile originating call
+                    mConnections[i] = mPendingMO;
+                    mPendingMO.mIndex = i;
+                    mPendingMO.update(dc);
+                    mPendingMO = null;
+
+                    // Someone has already asked to hangup this call
+                    if (mHangupPendingMO) {
+                        mHangupPendingMO = false;
+
+                        // Re-start Ecm timer when an uncompleted emergency call ends
+                        if (!isPhoneTypeGsm() && mIsEcmTimerCanceled) {
+                            handleEcmTimer(GsmCdmaPhone.RESTART_ECM_TIMER);
+                        }
+
+                        try {
+                            if (Phone.DEBUG_PHONE) log(
+                                    "poll: hangupPendingMO, hangup conn " + i);
+                            hangup(mConnections[i]);
+                        } catch (CallStateException ex) {
+                            Rlog.e(LOG_TAG, "unexpected error on hangup");
+                        }
+
+                        // Do not continue processing this poll
+                        // Wait for hangup and repoll
+                        return;
+                    }
+                } else {
+                    if (Phone.DEBUG_PHONE) {
+                        log("pendingMo=" + mPendingMO + ", dc=" + dc);
+                    }
+
+                    mConnections[i] = new GsmCdmaConnection(mPhone, dc, this, i);
+
+                    Connection hoConnection = getHoConnection(dc);
+                    if (hoConnection != null) {
+                        // Single Radio Voice Call Continuity (SRVCC) completed
+                        mConnections[i].migrateFrom(hoConnection);
+                        // Updating connect time for silent redial cases (ex: Calls are transferred
+                        // from DIALING/ALERTING/INCOMING/WAITING to ACTIVE)
+                        if (hoConnection.mPreHandoverState != GsmCdmaCall.State.ACTIVE &&
+                                hoConnection.mPreHandoverState != GsmCdmaCall.State.HOLDING &&
+                                dc.state == DriverCall.State.ACTIVE) {
+                            mConnections[i].onConnectedInOrOut();
+                        }
+
+                        mHandoverConnections.remove(hoConnection);
+
+                        if (isPhoneTypeGsm()) {
+                            for (Iterator<Connection> it = mHandoverConnections.iterator();
+                                 it.hasNext(); ) {
+                                Connection c = it.next();
+                                Rlog.i(LOG_TAG, "HO Conn state is " + c.mPreHandoverState);
+                                if (c.mPreHandoverState == mConnections[i].getState()) {
+                                    Rlog.i(LOG_TAG, "Removing HO conn "
+                                            + hoConnection + c.mPreHandoverState);
+                                    it.remove();
+                                }
+                            }
+                        }
+
+                        mPhone.notifyHandoverStateChanged(mConnections[i]);
+                    } else {
+                        // find if the MT call is a new ring or unknown connection
+                        newRinging = checkMtFindNewRinging(dc,i);
+                        if (newRinging == null) {
+                            unknownConnectionAppeared = true;
+                            if (isPhoneTypeGsm()) {
+                                newUnknownConnectionsGsm.add(mConnections[i]);
+                            } else {
+                                newUnknownConnectionCdma = mConnections[i];
+                            }
+                        }
+                    }
+                }
+                hasNonHangupStateChanged = true;
+            } else if (conn != null && dc == null) {
+                if (isPhoneTypeGsm()) {
+                    // Connection missing in CLCC response that we were
+                    // tracking.
+                    mDroppedDuringPoll.add(conn);
+                } else {
+                    // This case means the RIL has no more active call anymore and
+                    // we need to clean up the foregroundCall and ringingCall.
+                    // Loop through foreground call connections as
+                    // it contains the known logical connections.
+                    int count = mForegroundCall.mConnections.size();
+                    for (int n = 0; n < count; n++) {
+                        if (Phone.DEBUG_PHONE) log("adding fgCall cn " + n + " to droppedDuringPoll");
+                        GsmCdmaConnection cn = (GsmCdmaConnection)mForegroundCall.mConnections.get(n);
+                        mDroppedDuringPoll.add(cn);
+                    }
+                    count = mRingingCall.mConnections.size();
+                    // Loop through ringing call connections as
+                    // it may contain the known logical connections.
+                    for (int n = 0; n < count; n++) {
+                        if (Phone.DEBUG_PHONE) log("adding rgCall cn " + n + " to droppedDuringPoll");
+                        GsmCdmaConnection cn = (GsmCdmaConnection)mRingingCall.mConnections.get(n);
+                        mDroppedDuringPoll.add(cn);
+                    }
+
+                    // Re-start Ecm timer when the connected emergency call ends
+                    if (mIsEcmTimerCanceled) {
+                        handleEcmTimer(GsmCdmaPhone.RESTART_ECM_TIMER);
+                    }
+                    // If emergency call is not going through while dialing
+                    checkAndEnableDataCallAfterEmergencyCallDropped();
+                }
+                // Dropped connections are removed from the CallTracker
+                // list but kept in the Call list
+                mConnections[i] = null;
+            } else if (conn != null && dc != null && !conn.compareTo(dc) && isPhoneTypeGsm()) {
+                // Connection in CLCC response does not match what
+                // we were tracking. Assume dropped call and new call
+
+                mDroppedDuringPoll.add(conn);
+                mConnections[i] = new GsmCdmaConnection (mPhone, dc, this, i);
+
+                if (mConnections[i].getCall() == mRingingCall) {
+                    newRinging = mConnections[i];
+                } // else something strange happened
+                hasNonHangupStateChanged = true;
+            } else if (conn != null && dc != null) { /* implicit conn.compareTo(dc) */
+                // Call collision case
+                if (!isPhoneTypeGsm() && conn.isIncoming() != dc.isMT) {
+                    if (dc.isMT == true) {
+                        // Mt call takes precedence than Mo,drops Mo
+                        mDroppedDuringPoll.add(conn);
+                        // find if the MT call is a new ring or unknown connection
+                        newRinging = checkMtFindNewRinging(dc,i);
+                        if (newRinging == null) {
+                            unknownConnectionAppeared = true;
+                            newUnknownConnectionCdma = conn;
+                        }
+                        checkAndEnableDataCallAfterEmergencyCallDropped();
+                    } else {
+                        // Call info stored in conn is not consistent with the call info from dc.
+                        // We should follow the rule of MT calls taking precedence over MO calls
+                        // when there is conflict, so here we drop the call info from dc and
+                        // continue to use the call info from conn, and only take a log.
+                        Rlog.e(LOG_TAG,"Error in RIL, Phantom call appeared " + dc);
+                    }
+                } else {
+                    boolean changed;
+                    changed = conn.update(dc);
+                    hasNonHangupStateChanged = hasNonHangupStateChanged || changed;
+                }
+            }
+
+            if (REPEAT_POLLING) {
+                if (dc != null) {
+                    // FIXME with RIL, we should not need this anymore
+                    if ((dc.state == DriverCall.State.DIALING
+                            /*&& cm.getOption(cm.OPTION_POLL_DIALING)*/)
+                        || (dc.state == DriverCall.State.ALERTING
+                            /*&& cm.getOption(cm.OPTION_POLL_ALERTING)*/)
+                        || (dc.state == DriverCall.State.INCOMING
+                            /*&& cm.getOption(cm.OPTION_POLL_INCOMING)*/)
+                        || (dc.state == DriverCall.State.WAITING
+                            /*&& cm.getOption(cm.OPTION_POLL_WAITING)*/)) {
+                        // Sometimes there's no unsolicited notification
+                        // for state transitions
+                        needsPollDelay = true;
+                    }
+                }
+            }
+        }
+
+        // Safety check so that obj is not stuck with mIsInEmergencyCall set to true (and data
+        // disabled). This should never happen though.
+        if (!isPhoneTypeGsm() && noConnectionExists) {
+            checkAndEnableDataCallAfterEmergencyCallDropped();
+        }
+
+        // This is the first poll after an ATD.
+        // We expect the pending call to appear in the list
+        // If it does not, we land here
+        if (mPendingMO != null) {
+            Rlog.d(LOG_TAG, "Pending MO dropped before poll fg state:"
+                    + mForegroundCall.getState());
+
+            mDroppedDuringPoll.add(mPendingMO);
+            mPendingMO = null;
+            mHangupPendingMO = false;
+
+            if (!isPhoneTypeGsm()) {
+                if( mPendingCallInEcm) {
+                    mPendingCallInEcm = false;
+                }
+                checkAndEnableDataCallAfterEmergencyCallDropped();
+            }
+        }
+
+        if (newRinging != null) {
+            mPhone.notifyNewRingingConnection(newRinging);
+        }
+
+        // clear the "local hangup" and "missed/rejected call"
+        // cases from the "dropped during poll" list
+        // These cases need no "last call fail" reason
+        ArrayList<GsmCdmaConnection> locallyDisconnectedConnections = new ArrayList<>();
+        for (int i = mDroppedDuringPoll.size() - 1; i >= 0 ; i--) {
+            GsmCdmaConnection conn = mDroppedDuringPoll.get(i);
+            //CDMA
+            boolean wasDisconnected = false;
+
+            if (conn.isIncoming() && conn.getConnectTime() == 0) {
+                // Missed or rejected call
+                int cause;
+                if (conn.mCause == DisconnectCause.LOCAL) {
+                    cause = DisconnectCause.INCOMING_REJECTED;
+                } else {
+                    cause = DisconnectCause.INCOMING_MISSED;
+                }
+
+                if (Phone.DEBUG_PHONE) {
+                    log("missed/rejected call, conn.cause=" + conn.mCause);
+                    log("setting cause to " + cause);
+                }
+                mDroppedDuringPoll.remove(i);
+                hasAnyCallDisconnected |= conn.onDisconnect(cause);
+                wasDisconnected = true;
+                locallyDisconnectedConnections.add(conn);
+            } else if (conn.mCause == DisconnectCause.LOCAL
+                    || conn.mCause == DisconnectCause.INVALID_NUMBER) {
+                mDroppedDuringPoll.remove(i);
+                hasAnyCallDisconnected |= conn.onDisconnect(conn.mCause);
+                wasDisconnected = true;
+                locallyDisconnectedConnections.add(conn);
+            }
+
+            if (!isPhoneTypeGsm() && wasDisconnected && unknownConnectionAppeared
+                    && conn == newUnknownConnectionCdma) {
+                unknownConnectionAppeared = false;
+                newUnknownConnectionCdma = null;
+            }
+        }
+        if (locallyDisconnectedConnections.size() > 0) {
+            mMetrics.writeRilCallList(mPhone.getPhoneId(), locallyDisconnectedConnections);
+        }
+
+        /* Disconnect any pending Handover connections */
+        for (Iterator<Connection> it = mHandoverConnections.iterator();
+                it.hasNext();) {
+            Connection hoConnection = it.next();
+            log("handlePollCalls - disconnect hoConn= " + hoConnection +
+                    " hoConn.State= " + hoConnection.getState());
+            if (hoConnection.getState().isRinging()) {
+                hoConnection.onDisconnect(DisconnectCause.INCOMING_MISSED);
+            } else {
+                hoConnection.onDisconnect(DisconnectCause.NOT_VALID);
+            }
+            // TODO: Do we need to update these hoConnections in Metrics ?
+            it.remove();
+        }
+
+        // Any non-local disconnects: determine cause
+        if (mDroppedDuringPoll.size() > 0) {
+            mCi.getLastCallFailCause(
+                obtainNoPollCompleteMessage(EVENT_GET_LAST_CALL_FAIL_CAUSE));
+        }
+
+        if (needsPollDelay) {
+            pollCallsAfterDelay();
+        }
+
+        // Cases when we can no longer keep disconnected Connection's
+        // with their previous calls
+        // 1) the phone has started to ring
+        // 2) A Call/Connection object has changed state...
+        //    we may have switched or held or answered (but not hung up)
+        if (newRinging != null || hasNonHangupStateChanged || hasAnyCallDisconnected) {
+            internalClearDisconnected();
+        }
+
+        if (VDBG) log("handlePollCalls calling updatePhoneState()");
+        updatePhoneState();
+
+        if (unknownConnectionAppeared) {
+            if (isPhoneTypeGsm()) {
+                for (Connection c : newUnknownConnectionsGsm) {
+                    log("Notify unknown for " + c);
+                    mPhone.notifyUnknownConnection(c);
+                }
+            } else {
+                mPhone.notifyUnknownConnection(newUnknownConnectionCdma);
+            }
+        }
+
+        if (hasNonHangupStateChanged || newRinging != null || hasAnyCallDisconnected) {
+            mPhone.notifyPreciseCallStateChanged();
+            updateMetrics(mConnections);
+        }
+
+        // If all handover connections are mapped during this poll process clean it up
+        if (handoverConnectionsSize > 0 && mHandoverConnections.size() == 0) {
+            Phone imsPhone = mPhone.getImsPhone();
+            if (imsPhone != null) {
+                imsPhone.callEndCleanupHandOverCallIfAny();
+            }
+        }
+        //dumpState();
+    }
+
+    private void updateMetrics(GsmCdmaConnection[] connections) {
+        ArrayList<GsmCdmaConnection> activeConnections = new ArrayList<>();
+        for (GsmCdmaConnection conn : connections) {
+            if (conn != null) activeConnections.add(conn);
+        }
+        mMetrics.writeRilCallList(mPhone.getPhoneId(), activeConnections);
+    }
+
+    private void handleRadioNotAvailable() {
+        // handlePollCalls will clear out its
+        // call list when it gets the CommandException
+        // error result from this
+        pollCallsWhenSafe();
+    }
+
+    private void dumpState() {
+        List l;
+
+        Rlog.i(LOG_TAG,"Phone State:" + mState);
+
+        Rlog.i(LOG_TAG,"Ringing call: " + mRingingCall.toString());
+
+        l = mRingingCall.getConnections();
+        for (int i = 0, s = l.size(); i < s; i++) {
+            Rlog.i(LOG_TAG,l.get(i).toString());
+        }
+
+        Rlog.i(LOG_TAG,"Foreground call: " + mForegroundCall.toString());
+
+        l = mForegroundCall.getConnections();
+        for (int i = 0, s = l.size(); i < s; i++) {
+            Rlog.i(LOG_TAG,l.get(i).toString());
+        }
+
+        Rlog.i(LOG_TAG,"Background call: " + mBackgroundCall.toString());
+
+        l = mBackgroundCall.getConnections();
+        for (int i = 0, s = l.size(); i < s; i++) {
+            Rlog.i(LOG_TAG,l.get(i).toString());
+        }
+
+    }
+
+    //***** Called from GsmCdmaConnection
+
+    public void hangup(GsmCdmaConnection conn) throws CallStateException {
+        if (conn.mOwner != this) {
+            throw new CallStateException ("GsmCdmaConnection " + conn
+                                    + "does not belong to GsmCdmaCallTracker " + this);
+        }
+
+        if (conn == mPendingMO) {
+            // We're hanging up an outgoing call that doesn't have it's
+            // GsmCdma index assigned yet
+
+            if (Phone.DEBUG_PHONE) log("hangup: set hangupPendingMO to true");
+            mHangupPendingMO = true;
+        } else if (!isPhoneTypeGsm()
+                && conn.getCall() == mRingingCall
+                && mRingingCall.getState() == GsmCdmaCall.State.WAITING) {
+            // Handle call waiting hang up case.
+            //
+            // The ringingCall state will change to IDLE in GsmCdmaCall.detach
+            // if the ringing call connection size is 0. We don't specifically
+            // set the ringing call state to IDLE here to avoid a race condition
+            // where a new call waiting could get a hang up from an old call
+            // waiting ringingCall.
+            //
+            // PhoneApp does the call log itself since only PhoneApp knows
+            // the hangup reason is user ignoring or timing out. So conn.onDisconnect()
+            // is not called here. Instead, conn.onLocalDisconnect() is called.
+            conn.onLocalDisconnect();
+
+            updatePhoneState();
+            mPhone.notifyPreciseCallStateChanged();
+            return;
+        } else {
+            try {
+                mMetrics.writeRilHangup(mPhone.getPhoneId(), conn, conn.getGsmCdmaIndex());
+                mCi.hangupConnection (conn.getGsmCdmaIndex(), obtainCompleteMessage());
+            } catch (CallStateException ex) {
+                // Ignore "connection not found"
+                // Call may have hung up already
+                Rlog.w(LOG_TAG,"GsmCdmaCallTracker WARN: hangup() on absent connection "
+                                + conn);
+            }
+        }
+
+        conn.onHangupLocal();
+    }
+
+    public void separate(GsmCdmaConnection conn) throws CallStateException {
+        if (conn.mOwner != this) {
+            throw new CallStateException ("GsmCdmaConnection " + conn
+                                    + "does not belong to GsmCdmaCallTracker " + this);
+        }
+        try {
+            mCi.separateConnection (conn.getGsmCdmaIndex(),
+                obtainCompleteMessage(EVENT_SEPARATE_RESULT));
+        } catch (CallStateException ex) {
+            // Ignore "connection not found"
+            // Call may have hung up already
+            Rlog.w(LOG_TAG,"GsmCdmaCallTracker WARN: separate() on absent connection " + conn);
+        }
+    }
+
+    //***** Called from GsmCdmaPhone
+
+    public void setMute(boolean mute) {
+        mDesiredMute = mute;
+        mCi.setMute(mDesiredMute, null);
+    }
+
+    public boolean getMute() {
+        return mDesiredMute;
+    }
+
+
+    //***** Called from GsmCdmaCall
+
+    public void hangup(GsmCdmaCall call) throws CallStateException {
+        if (call.getConnections().size() == 0) {
+            throw new CallStateException("no connections in call");
+        }
+
+        if (call == mRingingCall) {
+            if (Phone.DEBUG_PHONE) log("(ringing) hangup waiting or background");
+            logHangupEvent(call);
+            mCi.hangupWaitingOrBackground(obtainCompleteMessage());
+        } else if (call == mForegroundCall) {
+            if (call.isDialingOrAlerting()) {
+                if (Phone.DEBUG_PHONE) {
+                    log("(foregnd) hangup dialing or alerting...");
+                }
+                hangup((GsmCdmaConnection)(call.getConnections().get(0)));
+            } else if (isPhoneTypeGsm()
+                    && mRingingCall.isRinging()) {
+                // Do not auto-answer ringing on CHUP, instead just end active calls
+                log("hangup all conns in active/background call, without affecting ringing call");
+                hangupAllConnections(call);
+            } else {
+                logHangupEvent(call);
+                hangupForegroundResumeBackground();
+            }
+        } else if (call == mBackgroundCall) {
+            if (mRingingCall.isRinging()) {
+                if (Phone.DEBUG_PHONE) {
+                    log("hangup all conns in background call");
+                }
+                hangupAllConnections(call);
+            } else {
+                hangupWaitingOrBackground();
+            }
+        } else {
+            throw new RuntimeException ("GsmCdmaCall " + call +
+                    "does not belong to GsmCdmaCallTracker " + this);
+        }
+
+        call.onHangupLocal();
+        mPhone.notifyPreciseCallStateChanged();
+    }
+
+    private void logHangupEvent(GsmCdmaCall call) {
+        int count = call.mConnections.size();
+        for (int i = 0; i < count; i++) {
+            GsmCdmaConnection cn = (GsmCdmaConnection) call.mConnections.get(i);
+            int call_index;
+            try {
+                call_index = cn.getGsmCdmaIndex();
+            } catch (CallStateException ex) {
+                call_index = -1;
+            }
+            mMetrics.writeRilHangup(mPhone.getPhoneId(), cn, call_index);
+        }
+        if (VDBG) Rlog.v(LOG_TAG, "logHangupEvent logged " + count + " Connections ");
+    }
+
+    public void hangupWaitingOrBackground() {
+        if (Phone.DEBUG_PHONE) log("hangupWaitingOrBackground");
+        logHangupEvent(mBackgroundCall);
+        mCi.hangupWaitingOrBackground(obtainCompleteMessage());
+    }
+
+    public void hangupForegroundResumeBackground() {
+        if (Phone.DEBUG_PHONE) log("hangupForegroundResumeBackground");
+        mCi.hangupForegroundResumeBackground(obtainCompleteMessage());
+    }
+
+    public void hangupConnectionByIndex(GsmCdmaCall call, int index)
+            throws CallStateException {
+        int count = call.mConnections.size();
+        for (int i = 0; i < count; i++) {
+            GsmCdmaConnection cn = (GsmCdmaConnection)call.mConnections.get(i);
+            if (!cn.mDisconnected && cn.getGsmCdmaIndex() == index) {
+                mMetrics.writeRilHangup(mPhone.getPhoneId(), cn, cn.getGsmCdmaIndex());
+                mCi.hangupConnection(index, obtainCompleteMessage());
+                return;
+            }
+        }
+
+        throw new CallStateException("no GsmCdma index found");
+    }
+
+    public void hangupAllConnections(GsmCdmaCall call) {
+        try {
+            int count = call.mConnections.size();
+            for (int i = 0; i < count; i++) {
+                GsmCdmaConnection cn = (GsmCdmaConnection)call.mConnections.get(i);
+                if (!cn.mDisconnected) {
+                    mMetrics.writeRilHangup(mPhone.getPhoneId(), cn, cn.getGsmCdmaIndex());
+                    mCi.hangupConnection(cn.getGsmCdmaIndex(), obtainCompleteMessage());
+                }
+            }
+        } catch (CallStateException ex) {
+            Rlog.e(LOG_TAG, "hangupConnectionByIndex caught " + ex);
+        }
+    }
+
+    public GsmCdmaConnection getConnectionByIndex(GsmCdmaCall call, int index)
+            throws CallStateException {
+        int count = call.mConnections.size();
+        for (int i = 0; i < count; i++) {
+            GsmCdmaConnection cn = (GsmCdmaConnection)call.mConnections.get(i);
+            if (!cn.mDisconnected && cn.getGsmCdmaIndex() == index) {
+                return cn;
+            }
+        }
+
+        return null;
+    }
+
+    //CDMA
+    private void notifyCallWaitingInfo(CdmaCallWaitingNotification obj) {
+        if (mCallWaitingRegistrants != null) {
+            mCallWaitingRegistrants.notifyRegistrants(new AsyncResult(null, obj, null));
+        }
+    }
+
+    //CDMA
+    private void handleCallWaitingInfo(CdmaCallWaitingNotification cw) {
+        // Create a new GsmCdmaConnection which attaches itself to ringingCall.
+        new GsmCdmaConnection(mPhone.getContext(), cw, this, mRingingCall);
+        updatePhoneState();
+
+        // Finally notify application
+        notifyCallWaitingInfo(cw);
+    }
+
+    private Phone.SuppService getFailedService(int what) {
+        switch (what) {
+            case EVENT_SWITCH_RESULT:
+                return Phone.SuppService.SWITCH;
+            case EVENT_CONFERENCE_RESULT:
+                return Phone.SuppService.CONFERENCE;
+            case EVENT_SEPARATE_RESULT:
+                return Phone.SuppService.SEPARATE;
+            case EVENT_ECT_RESULT:
+                return Phone.SuppService.TRANSFER;
+        }
+        return Phone.SuppService.UNKNOWN;
+    }
+
+    //****** Overridden from Handler
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+            case EVENT_POLL_CALLS_RESULT:
+                Rlog.d(LOG_TAG, "Event EVENT_POLL_CALLS_RESULT Received");
+
+                if (msg == mLastRelevantPoll) {
+                    if (DBG_POLL) log(
+                            "handle EVENT_POLL_CALL_RESULT: set needsPoll=F");
+                    mNeedsPoll = false;
+                    mLastRelevantPoll = null;
+                    handlePollCalls((AsyncResult)msg.obj);
+                }
+            break;
+
+            case EVENT_OPERATION_COMPLETE:
+                operationComplete();
+            break;
+
+            case EVENT_CONFERENCE_RESULT:
+                if (isPhoneTypeGsm()) {
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception != null) {
+                        // The conference merge failed, so notify listeners.  Ultimately this
+                        // bubbles up to Telecom, which will inform the InCall UI of the failure.
+                        Connection connection = mForegroundCall.getLatestConnection();
+                        if (connection != null) {
+                            connection.onConferenceMergeFailed();
+                        }
+                    }
+                }
+                // fall through
+            case EVENT_SEPARATE_RESULT:
+            case EVENT_ECT_RESULT:
+            case EVENT_SWITCH_RESULT:
+                if (isPhoneTypeGsm()) {
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception != null) {
+                        mPhone.notifySuppServiceFailed(getFailedService(msg.what));
+                    }
+                    operationComplete();
+                } else {
+                    if (msg.what != EVENT_SWITCH_RESULT) {
+                        // EVENT_SWITCH_RESULT in GSM call triggers operationComplete() which gets
+                        // the current call list. But in CDMA there is no list so there is nothing
+                        // to do. Other messages however are not expected in CDMA.
+                        throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
+                                "phone type " + mPhone.getPhoneType());
+                    }
+                }
+            break;
+
+            case EVENT_GET_LAST_CALL_FAIL_CAUSE:
+                int causeCode;
+                String vendorCause = null;
+                ar = (AsyncResult)msg.obj;
+
+                operationComplete();
+
+                if (ar.exception != null) {
+                    // An exception occurred...just treat the disconnect
+                    // cause as "normal"
+                    causeCode = CallFailCause.NORMAL_CLEARING;
+                    Rlog.i(LOG_TAG,
+                            "Exception during getLastCallFailCause, assuming normal disconnect");
+                } else {
+                    LastCallFailCause failCause = (LastCallFailCause)ar.result;
+                    causeCode = failCause.causeCode;
+                    vendorCause = failCause.vendorCause;
+                }
+                // Log the causeCode if its not normal
+                if (causeCode == CallFailCause.NO_CIRCUIT_AVAIL ||
+                    causeCode == CallFailCause.TEMPORARY_FAILURE ||
+                    causeCode == CallFailCause.SWITCHING_CONGESTION ||
+                    causeCode == CallFailCause.CHANNEL_NOT_AVAIL ||
+                    causeCode == CallFailCause.QOS_NOT_AVAIL ||
+                    causeCode == CallFailCause.BEARER_NOT_AVAIL ||
+                    causeCode == CallFailCause.ERROR_UNSPECIFIED) {
+
+                    CellLocation loc = mPhone.getCellLocation();
+                    int cid = -1;
+                    if (loc != null) {
+                        if (isPhoneTypeGsm()) {
+                            cid = ((GsmCellLocation)loc).getCid();
+                        } else {
+                            cid = ((CdmaCellLocation)loc).getBaseStationId();
+                        }
+                    }
+                    EventLog.writeEvent(EventLogTags.CALL_DROP, causeCode, cid,
+                            TelephonyManager.getDefault().getNetworkType());
+                }
+
+                for (int i = 0, s = mDroppedDuringPoll.size(); i < s ; i++) {
+                    GsmCdmaConnection conn = mDroppedDuringPoll.get(i);
+
+                    conn.onRemoteDisconnect(causeCode, vendorCause);
+                }
+
+                updatePhoneState();
+
+                mPhone.notifyPreciseCallStateChanged();
+                mMetrics.writeRilCallList(mPhone.getPhoneId(), mDroppedDuringPoll);
+                mDroppedDuringPoll.clear();
+            break;
+
+            case EVENT_REPOLL_AFTER_DELAY:
+            case EVENT_CALL_STATE_CHANGE:
+                pollCallsWhenSafe();
+            break;
+
+            case EVENT_RADIO_AVAILABLE:
+                handleRadioAvailable();
+            break;
+
+            case EVENT_RADIO_NOT_AVAILABLE:
+                handleRadioNotAvailable();
+            break;
+
+            case EVENT_EXIT_ECM_RESPONSE_CDMA:
+                if (!isPhoneTypeGsm()) {
+                    // no matter the result, we still do the same here
+                    if (mPendingCallInEcm) {
+                        mCi.dial(mPendingMO.getAddress(), mPendingCallClirMode, obtainCompleteMessage());
+                        mPendingCallInEcm = false;
+                    }
+                    mPhone.unsetOnEcbModeExitResponse(this);
+                } else {
+                    throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
+                            "phone type " + mPhone.getPhoneType());
+                }
+                break;
+
+            case EVENT_CALL_WAITING_INFO_CDMA:
+                if (!isPhoneTypeGsm()) {
+                    ar = (AsyncResult)msg.obj;
+                    if (ar.exception == null) {
+                        handleCallWaitingInfo((CdmaCallWaitingNotification)ar.result);
+                        Rlog.d(LOG_TAG, "Event EVENT_CALL_WAITING_INFO_CDMA Received");
+                    }
+                } else {
+                    throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
+                            "phone type " + mPhone.getPhoneType());
+                }
+                break;
+
+            case EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA:
+                if (!isPhoneTypeGsm()) {
+                    ar = (AsyncResult)msg.obj;
+                    if (ar.exception == null) {
+                        // Assume 3 way call is connected
+                        mPendingMO.onConnectedInOrOut();
+                        mPendingMO = null;
+                    }
+                } else {
+                    throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
+                            "phone type " + mPhone.getPhoneType());
+                }
+                break;
+
+            case EVENT_THREE_WAY_DIAL_BLANK_FLASH:
+                if (!isPhoneTypeGsm()) {
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception == null) {
+                        postDelayed(
+                                new Runnable() {
+                                    public void run() {
+                                        if (mPendingMO != null) {
+                                            mCi.sendCDMAFeatureCode(mPendingMO.getAddress(),
+                                                    obtainMessage(EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA));
+                                        }
+                                    }
+                                }, m3WayCallFlashDelay);
+                    } else {
+                        mPendingMO = null;
+                        Rlog.w(LOG_TAG, "exception happened on Blank Flash for 3-way call");
+                    }
+                } else {
+                    throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
+                            "phone type " + mPhone.getPhoneType());
+                }
+                break;
+
+            default:{
+                throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
+                        "phone type " + mPhone.getPhoneType());
+            }
+        }
+    }
+
+    //CDMA
+    /**
+     * Check and enable data call after an emergency call is dropped if it's
+     * not in ECM
+     */
+    private void checkAndEnableDataCallAfterEmergencyCallDropped() {
+        if (mIsInEmergencyCall) {
+            mIsInEmergencyCall = false;
+            boolean inEcm = mPhone.isInEcm();
+            if (Phone.DEBUG_PHONE) {
+                log("checkAndEnableDataCallAfterEmergencyCallDropped,inEcm=" + inEcm);
+            }
+            if (!inEcm) {
+                // Re-initiate data connection
+                mPhone.mDcTracker.setInternalDataEnabled(true);
+                mPhone.notifyEmergencyCallRegistrants(false);
+            }
+            mPhone.sendEmergencyCallStateChange(false);
+        }
+    }
+
+    /**
+     * Check the MT call to see if it's a new ring or
+     * a unknown connection.
+     */
+    private Connection checkMtFindNewRinging(DriverCall dc, int i) {
+
+        Connection newRinging = null;
+
+        // it's a ringing call
+        if (mConnections[i].getCall() == mRingingCall) {
+            newRinging = mConnections[i];
+            if (Phone.DEBUG_PHONE) log("Notify new ring " + dc);
+        } else {
+            // Something strange happened: a call which is neither
+            // a ringing call nor the one we created. It could be the
+            // call collision result from RIL
+            Rlog.e(LOG_TAG,"Phantom call appeared " + dc);
+            // If it's a connected call, set the connect time so that
+            // it's non-zero.  It may not be accurate, but at least
+            // it won't appear as a Missed Call.
+            if (dc.state != DriverCall.State.ALERTING
+                    && dc.state != DriverCall.State.DIALING) {
+                mConnections[i].onConnectedInOrOut();
+                if (dc.state == DriverCall.State.HOLDING) {
+                    // We've transitioned into HOLDING
+                    mConnections[i].onStartedHolding();
+                }
+            }
+        }
+        return newRinging;
+    }
+
+    //CDMA
+    /**
+     * Check if current call is in emergency call
+     *
+     * @return true if it is in emergency call
+     *         false if it is not in emergency call
+     */
+    public boolean isInEmergencyCall() {
+        return mIsInEmergencyCall;
+    }
+
+    private boolean isPhoneTypeGsm() {
+        return mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM;
+    }
+
+    public GsmCdmaPhone getPhone() {
+        return mPhone;
+    }
+
+    @Override
+    protected void log(String msg) {
+        Rlog.d(LOG_TAG, "[GsmCdmaCallTracker] " + msg);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("GsmCdmaCallTracker extends:");
+        super.dump(fd, pw, args);
+        pw.println("mConnections: length=" + mConnections.length);
+        for(int i=0; i < mConnections.length; i++) {
+            pw.printf("  mConnections[%d]=%s\n", i, mConnections[i]);
+        }
+        pw.println(" mVoiceCallEndedRegistrants=" + mVoiceCallEndedRegistrants);
+        pw.println(" mVoiceCallStartedRegistrants=" + mVoiceCallStartedRegistrants);
+        if (!isPhoneTypeGsm()) {
+            pw.println(" mCallWaitingRegistrants=" + mCallWaitingRegistrants);
+        }
+        pw.println(" mDroppedDuringPoll: size=" + mDroppedDuringPoll.size());
+        for(int i = 0; i < mDroppedDuringPoll.size(); i++) {
+            pw.printf( "  mDroppedDuringPoll[%d]=%s\n", i, mDroppedDuringPoll.get(i));
+        }
+        pw.println(" mRingingCall=" + mRingingCall);
+        pw.println(" mForegroundCall=" + mForegroundCall);
+        pw.println(" mBackgroundCall=" + mBackgroundCall);
+        pw.println(" mPendingMO=" + mPendingMO);
+        pw.println(" mHangupPendingMO=" + mHangupPendingMO);
+        pw.println(" mPhone=" + mPhone);
+        pw.println(" mDesiredMute=" + mDesiredMute);
+        pw.println(" mState=" + mState);
+        if (!isPhoneTypeGsm()) {
+            pw.println(" mPendingCallInEcm=" + mPendingCallInEcm);
+            pw.println(" mIsInEmergencyCall=" + mIsInEmergencyCall);
+            pw.println(" mPendingCallClirMode=" + mPendingCallClirMode);
+            pw.println(" mIsEcmTimerCanceled=" + mIsEcmTimerCanceled);
+        }
+
+    }
+
+    @Override
+    public PhoneConstants.State getState() {
+        return mState;
+    }
+
+    public int getMaxConnectionsPerCall() {
+        return mPhone.isPhoneTypeGsm() ?
+                MAX_CONNECTIONS_PER_CALL_GSM :
+                MAX_CONNECTIONS_PER_CALL_CDMA;
+    }
+
+    /**
+     * Called to force the call tracker to cleanup any stale calls.  Does this by triggering
+     * {@code GET_CURRENT_CALLS} on the RIL.
+     */
+    @Override
+    public void cleanupCalls() {
+        pollCallsWhenSafe();
+    }
+}
diff --git a/com/android/internal/telephony/GsmCdmaConnection.java b/com/android/internal/telephony/GsmCdmaConnection.java
new file mode 100644
index 0000000..79373ae
--- /dev/null
+++ b/com/android/internal/telephony/GsmCdmaConnection.java
@@ -0,0 +1,1119 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.Registrant;
+import android.os.SystemClock;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
+import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+
+/**
+ * {@hide}
+ */
+public class GsmCdmaConnection extends Connection {
+    private static final String LOG_TAG = "GsmCdmaConnection";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    //***** Instance Variables
+
+    GsmCdmaCallTracker mOwner;
+    GsmCdmaCall mParent;
+
+    boolean mDisconnected;
+
+    int mIndex;          // index in GsmCdmaCallTracker.connections[], -1 if unassigned
+                        // The GsmCdma index is 1 + this
+
+    /*
+     * These time/timespan values are based on System.currentTimeMillis(),
+     * i.e., "wall clock" time.
+     */
+    long mDisconnectTime;
+
+    UUSInfo mUusInfo;
+    int mPreciseCause = 0;
+    String mVendorCause;
+
+    Connection mOrigConnection;
+
+    Handler mHandler;
+
+    private PowerManager.WakeLock mPartialWakeLock;
+
+    private boolean mIsEmergencyCall = false;
+
+    // The cached delay to be used between DTMF tones fetched from carrier config.
+    private int mDtmfToneDelay = 0;
+
+    //***** Event Constants
+    static final int EVENT_DTMF_DONE = 1;
+    static final int EVENT_PAUSE_DONE = 2;
+    static final int EVENT_NEXT_POST_DIAL = 3;
+    static final int EVENT_WAKE_LOCK_TIMEOUT = 4;
+    static final int EVENT_DTMF_DELAY_DONE = 5;
+
+    //***** Constants
+    static final int PAUSE_DELAY_MILLIS_GSM = 3 * 1000;
+    static final int PAUSE_DELAY_MILLIS_CDMA = 2 * 1000;
+    static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000;
+
+    //***** Inner Classes
+
+    class MyHandler extends Handler {
+        MyHandler(Looper l) {super(l);}
+
+        @Override
+        public void
+        handleMessage(Message msg) {
+
+            switch (msg.what) {
+                case EVENT_NEXT_POST_DIAL:
+                case EVENT_DTMF_DELAY_DONE:
+                case EVENT_PAUSE_DONE:
+                    processNextPostDialChar();
+                    break;
+                case EVENT_WAKE_LOCK_TIMEOUT:
+                    releaseWakeLock();
+                    break;
+                case EVENT_DTMF_DONE:
+                    // We may need to add a delay specified by carrier between DTMF tones that are
+                    // sent out.
+                    mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_DTMF_DELAY_DONE),
+                            mDtmfToneDelay);
+                    break;
+            }
+        }
+    }
+
+    //***** Constructors
+
+    /** This is probably an MT call that we first saw in a CLCC response or a hand over. */
+    public GsmCdmaConnection (GsmCdmaPhone phone, DriverCall dc, GsmCdmaCallTracker ct, int index) {
+        super(phone.getPhoneType());
+        createWakeLock(phone.getContext());
+        acquireWakeLock();
+
+        mOwner = ct;
+        mHandler = new MyHandler(mOwner.getLooper());
+
+        mAddress = dc.number;
+        mIsEmergencyCall = PhoneNumberUtils.isLocalEmergencyNumber(phone.getContext(), mAddress);
+        mIsIncoming = dc.isMT;
+        mCreateTime = System.currentTimeMillis();
+        mCnapName = dc.name;
+        mCnapNamePresentation = dc.namePresentation;
+        mNumberPresentation = dc.numberPresentation;
+        mUusInfo = dc.uusInfo;
+
+        mIndex = index;
+
+        mParent = parentFromDCState(dc.state);
+        mParent.attach(this, dc);
+
+        fetchDtmfToneDelay(phone);
+    }
+
+    /** This is an MO call, created when dialing */
+    public GsmCdmaConnection (GsmCdmaPhone phone, String dialString, GsmCdmaCallTracker ct,
+                              GsmCdmaCall parent, boolean isEmergencyCall) {
+        super(phone.getPhoneType());
+        createWakeLock(phone.getContext());
+        acquireWakeLock();
+
+        mOwner = ct;
+        mHandler = new MyHandler(mOwner.getLooper());
+
+        if (isPhoneTypeGsm()) {
+            mDialString = dialString;
+        } else {
+            Rlog.d(LOG_TAG, "[GsmCdmaConn] GsmCdmaConnection: dialString=" +
+                    maskDialString(dialString));
+            dialString = formatDialString(dialString);
+            Rlog.d(LOG_TAG,
+                    "[GsmCdmaConn] GsmCdmaConnection:formated dialString=" +
+                            maskDialString(dialString));
+        }
+
+        mAddress = PhoneNumberUtils.extractNetworkPortionAlt(dialString);
+        mIsEmergencyCall = isEmergencyCall;
+        mPostDialString = PhoneNumberUtils.extractPostDialPortion(dialString);
+
+        mIndex = -1;
+
+        mIsIncoming = false;
+        mCnapName = null;
+        mCnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED;
+        mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
+        mCreateTime = System.currentTimeMillis();
+
+        if (parent != null) {
+            mParent = parent;
+            if (isPhoneTypeGsm()) {
+                parent.attachFake(this, GsmCdmaCall.State.DIALING);
+            } else {
+                //for the three way call case, not change parent state
+                if (parent.mState == GsmCdmaCall.State.ACTIVE) {
+                    parent.attachFake(this, GsmCdmaCall.State.ACTIVE);
+                } else {
+                    parent.attachFake(this, GsmCdmaCall.State.DIALING);
+                }
+
+            }
+        }
+
+        fetchDtmfToneDelay(phone);
+    }
+
+    //CDMA
+    /** This is a Call waiting call*/
+    public GsmCdmaConnection(Context context, CdmaCallWaitingNotification cw, GsmCdmaCallTracker ct,
+                             GsmCdmaCall parent) {
+        super(parent.getPhone().getPhoneType());
+        createWakeLock(context);
+        acquireWakeLock();
+
+        mOwner = ct;
+        mHandler = new MyHandler(mOwner.getLooper());
+        mAddress = cw.number;
+        mNumberPresentation = cw.numberPresentation;
+        mCnapName = cw.name;
+        mCnapNamePresentation = cw.namePresentation;
+        mIndex = -1;
+        mIsIncoming = true;
+        mCreateTime = System.currentTimeMillis();
+        mConnectTime = 0;
+        mParent = parent;
+        parent.attachFake(this, GsmCdmaCall.State.WAITING);
+    }
+
+
+    public void dispose() {
+        clearPostDialListeners();
+        if (mParent != null) {
+            mParent.detach(this);
+        }
+        releaseAllWakeLocks();
+    }
+
+    static boolean equalsHandlesNulls(Object a, Object b) {
+        return (a == null) ? (b == null) : a.equals (b);
+    }
+
+    static boolean
+    equalsBaseDialString (String a, String b) {
+        return (a == null) ? (b == null) : (b != null && a.startsWith (b));
+    }
+
+    //CDMA
+    /**
+     * format original dial string
+     * 1) convert international dialing prefix "+" to
+     *    string specified per region
+     *
+     * 2) handle corner cases for PAUSE/WAIT dialing:
+     *
+     *    If PAUSE/WAIT sequence at the end, ignore them.
+     *
+     *    If consecutive PAUSE/WAIT sequence in the middle of the string,
+     *    and if there is any WAIT in PAUSE/WAIT sequence, treat them like WAIT.
+     */
+    public static String formatDialString(String phoneNumber) {
+        /**
+         * TODO(cleanup): This function should move to PhoneNumberUtils, and
+         * tests should be added.
+         */
+
+        if (phoneNumber == null) {
+            return null;
+        }
+        int length = phoneNumber.length();
+        StringBuilder ret = new StringBuilder();
+        char c;
+        int currIndex = 0;
+
+        while (currIndex < length) {
+            c = phoneNumber.charAt(currIndex);
+            if (isPause(c) || isWait(c)) {
+                if (currIndex < length - 1) {
+                    // if PW not at the end
+                    int nextIndex = findNextPCharOrNonPOrNonWCharIndex(phoneNumber, currIndex);
+                    // If there is non PW char following PW sequence
+                    if (nextIndex < length) {
+                        char pC = findPOrWCharToAppend(phoneNumber, currIndex, nextIndex);
+                        ret.append(pC);
+                        // If PW char sequence has more than 2 PW characters,
+                        // skip to the last PW character since the sequence already be
+                        // converted to WAIT character
+                        if (nextIndex > (currIndex + 1)) {
+                            currIndex = nextIndex - 1;
+                        }
+                    } else if (nextIndex == length) {
+                        // It means PW characters at the end, ignore
+                        currIndex = length - 1;
+                    }
+                }
+            } else {
+                ret.append(c);
+            }
+            currIndex++;
+        }
+        return PhoneNumberUtils.cdmaCheckAndProcessPlusCode(ret.toString());
+    }
+
+    /*package*/ boolean
+    compareTo(DriverCall c) {
+        // On mobile originated (MO) calls, the phone number may have changed
+        // due to a SIM Toolkit call control modification.
+        //
+        // We assume we know when MO calls are created (since we created them)
+        // and therefore don't need to compare the phone number anyway.
+        if (! (mIsIncoming || c.isMT)) return true;
+
+        // A new call appearing by SRVCC may have invalid number
+        //  if IMS service is not tightly coupled with cellular modem stack.
+        // Thus we prefer the preexisting handover connection instance.
+        if (isPhoneTypeGsm() && mOrigConnection != null) return true;
+
+        // ... but we can compare phone numbers on MT calls, and we have
+        // no control over when they begin, so we might as well
+
+        String cAddress = PhoneNumberUtils.stringFromStringAndTOA(c.number, c.TOA);
+        return mIsIncoming == c.isMT && equalsHandlesNulls(mAddress, cAddress);
+    }
+
+    @Override
+    public String getOrigDialString(){
+        return mDialString;
+    }
+
+    @Override
+    public GsmCdmaCall getCall() {
+        return mParent;
+    }
+
+    @Override
+    public long getDisconnectTime() {
+        return mDisconnectTime;
+    }
+
+    @Override
+    public long getHoldDurationMillis() {
+        if (getState() != GsmCdmaCall.State.HOLDING) {
+            // If not holding, return 0
+            return 0;
+        } else {
+            return SystemClock.elapsedRealtime() - mHoldingStartTime;
+        }
+    }
+
+    @Override
+    public GsmCdmaCall.State getState() {
+        if (mDisconnected) {
+            return GsmCdmaCall.State.DISCONNECTED;
+        } else {
+            return super.getState();
+        }
+    }
+
+    @Override
+    public void hangup() throws CallStateException {
+        if (!mDisconnected) {
+            mOwner.hangup(this);
+        } else {
+            throw new CallStateException ("disconnected");
+        }
+    }
+
+    @Override
+    public void separate() throws CallStateException {
+        if (!mDisconnected) {
+            mOwner.separate(this);
+        } else {
+            throw new CallStateException ("disconnected");
+        }
+    }
+
+    @Override
+    public void proceedAfterWaitChar() {
+        if (mPostDialState != PostDialState.WAIT) {
+            Rlog.w(LOG_TAG, "GsmCdmaConnection.proceedAfterWaitChar(): Expected "
+                    + "getPostDialState() to be WAIT but was " + mPostDialState);
+            return;
+        }
+
+        setPostDialState(PostDialState.STARTED);
+
+        processNextPostDialChar();
+    }
+
+    @Override
+    public void proceedAfterWildChar(String str) {
+        if (mPostDialState != PostDialState.WILD) {
+            Rlog.w(LOG_TAG, "GsmCdmaConnection.proceedAfterWaitChar(): Expected "
+                + "getPostDialState() to be WILD but was " + mPostDialState);
+            return;
+        }
+
+        setPostDialState(PostDialState.STARTED);
+
+        // make a new postDialString, with the wild char replacement string
+        // at the beginning, followed by the remaining postDialString.
+
+        StringBuilder buf = new StringBuilder(str);
+        buf.append(mPostDialString.substring(mNextPostDialChar));
+        mPostDialString = buf.toString();
+        mNextPostDialChar = 0;
+        if (Phone.DEBUG_PHONE) {
+            log("proceedAfterWildChar: new postDialString is " +
+                    mPostDialString);
+        }
+
+        processNextPostDialChar();
+    }
+
+    @Override
+    public void cancelPostDial() {
+        setPostDialState(PostDialState.CANCELLED);
+    }
+
+    /**
+     * Called when this Connection is being hung up locally (eg, user pressed "end")
+     * Note that at this point, the hangup request has been dispatched to the radio
+     * but no response has yet been received so update() has not yet been called
+     */
+    void
+    onHangupLocal() {
+        mCause = DisconnectCause.LOCAL;
+        mPreciseCause = 0;
+        mVendorCause = null;
+    }
+
+    /**
+     * Maps RIL call disconnect code to {@link DisconnectCause}.
+     * @param causeCode RIL disconnect code
+     * @return the corresponding value from {@link DisconnectCause}
+     */
+    int disconnectCauseFromCode(int causeCode) {
+        /**
+         * See 22.001 Annex F.4 for mapping of cause codes
+         * to local tones
+         */
+
+        switch (causeCode) {
+            case CallFailCause.USER_BUSY:
+                return DisconnectCause.BUSY;
+
+            case CallFailCause.NO_CIRCUIT_AVAIL:
+            case CallFailCause.TEMPORARY_FAILURE:
+            case CallFailCause.SWITCHING_CONGESTION:
+            case CallFailCause.CHANNEL_NOT_AVAIL:
+            case CallFailCause.QOS_NOT_AVAIL:
+            case CallFailCause.BEARER_NOT_AVAIL:
+                return DisconnectCause.CONGESTION;
+
+            case CallFailCause.EMERGENCY_TEMP_FAILURE:
+                return DisconnectCause.EMERGENCY_TEMP_FAILURE;
+            case CallFailCause.EMERGENCY_PERM_FAILURE:
+                return DisconnectCause.EMERGENCY_PERM_FAILURE;
+
+            case CallFailCause.ACM_LIMIT_EXCEEDED:
+                return DisconnectCause.LIMIT_EXCEEDED;
+
+            case CallFailCause.OPERATOR_DETERMINED_BARRING:
+            case CallFailCause.CALL_BARRED:
+                return DisconnectCause.CALL_BARRED;
+
+            case CallFailCause.FDN_BLOCKED:
+                return DisconnectCause.FDN_BLOCKED;
+
+            case CallFailCause.IMEI_NOT_ACCEPTED:
+                return DisconnectCause.IMEI_NOT_ACCEPTED;
+
+            case CallFailCause.UNOBTAINABLE_NUMBER:
+                return DisconnectCause.UNOBTAINABLE_NUMBER;
+
+            case CallFailCause.DIAL_MODIFIED_TO_USSD:
+                return DisconnectCause.DIAL_MODIFIED_TO_USSD;
+
+            case CallFailCause.DIAL_MODIFIED_TO_SS:
+                return DisconnectCause.DIAL_MODIFIED_TO_SS;
+
+            case CallFailCause.DIAL_MODIFIED_TO_DIAL:
+                return DisconnectCause.DIAL_MODIFIED_TO_DIAL;
+
+            case CallFailCause.CDMA_LOCKED_UNTIL_POWER_CYCLE:
+                return DisconnectCause.CDMA_LOCKED_UNTIL_POWER_CYCLE;
+
+            case CallFailCause.CDMA_DROP:
+                return DisconnectCause.CDMA_DROP;
+
+            case CallFailCause.CDMA_INTERCEPT:
+                return DisconnectCause.CDMA_INTERCEPT;
+
+            case CallFailCause.CDMA_REORDER:
+                return DisconnectCause.CDMA_REORDER;
+
+            case CallFailCause.CDMA_SO_REJECT:
+                return DisconnectCause.CDMA_SO_REJECT;
+
+            case CallFailCause.CDMA_RETRY_ORDER:
+                return DisconnectCause.CDMA_RETRY_ORDER;
+
+            case CallFailCause.CDMA_ACCESS_FAILURE:
+                return DisconnectCause.CDMA_ACCESS_FAILURE;
+
+            case CallFailCause.CDMA_PREEMPTED:
+                return DisconnectCause.CDMA_PREEMPTED;
+
+            case CallFailCause.CDMA_NOT_EMERGENCY:
+                return DisconnectCause.CDMA_NOT_EMERGENCY;
+
+            case CallFailCause.CDMA_ACCESS_BLOCKED:
+                return DisconnectCause.CDMA_ACCESS_BLOCKED;
+
+            case CallFailCause.ERROR_UNSPECIFIED:
+            case CallFailCause.NORMAL_CLEARING:
+            default:
+                GsmCdmaPhone phone = mOwner.getPhone();
+                int serviceState = phone.getServiceState().getState();
+                UiccCardApplication cardApp = phone.getUiccCardApplication();
+                AppState uiccAppState = (cardApp != null) ? cardApp.getState() :
+                        AppState.APPSTATE_UNKNOWN;
+                if (serviceState == ServiceState.STATE_POWER_OFF) {
+                    return DisconnectCause.POWER_OFF;
+                }
+                if (!mIsEmergencyCall) {
+                    // Only send OUT_OF_SERVICE if it is not an emergency call. We can still
+                    // technically be in STATE_OUT_OF_SERVICE or STATE_EMERGENCY_ONLY during
+                    // an emergency call and when it ends, we do not want to mistakenly generate
+                    // an OUT_OF_SERVICE disconnect cause during normal call ending.
+                    if ((serviceState == ServiceState.STATE_OUT_OF_SERVICE
+                            || serviceState == ServiceState.STATE_EMERGENCY_ONLY)) {
+                        return DisconnectCause.OUT_OF_SERVICE;
+                    }
+                    // If we are placing an emergency call and the SIM is currently PIN/PUK
+                    // locked the AppState will always not be equal to APPSTATE_READY.
+                    if (uiccAppState != AppState.APPSTATE_READY) {
+                        if (isPhoneTypeGsm()) {
+                            return DisconnectCause.ICC_ERROR;
+                        } else { // CDMA
+                            if (phone.mCdmaSubscriptionSource ==
+                                    CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM) {
+                                return DisconnectCause.ICC_ERROR;
+                            }
+                        }
+                    }
+                }
+                if (isPhoneTypeGsm()) {
+                    if (causeCode == CallFailCause.ERROR_UNSPECIFIED) {
+                        if (phone.mSST.mRestrictedState.isCsRestricted()) {
+                            return DisconnectCause.CS_RESTRICTED;
+                        } else if (phone.mSST.mRestrictedState.isCsEmergencyRestricted()) {
+                            return DisconnectCause.CS_RESTRICTED_EMERGENCY;
+                        } else if (phone.mSST.mRestrictedState.isCsNormalRestricted()) {
+                            return DisconnectCause.CS_RESTRICTED_NORMAL;
+                        }
+                    }
+                }
+                if (causeCode == CallFailCause.NORMAL_CLEARING) {
+                    return DisconnectCause.NORMAL;
+                }
+                // If nothing else matches, report unknown call drop reason
+                // to app, not NORMAL call end.
+                return DisconnectCause.ERROR_UNSPECIFIED;
+        }
+    }
+
+    /*package*/ void
+    onRemoteDisconnect(int causeCode, String vendorCause) {
+        this.mPreciseCause = causeCode;
+        this.mVendorCause = vendorCause;
+        onDisconnect(disconnectCauseFromCode(causeCode));
+    }
+
+    /**
+     * Called when the radio indicates the connection has been disconnected.
+     * @param cause call disconnect cause; values are defined in {@link DisconnectCause}
+     */
+    @Override
+    public boolean onDisconnect(int cause) {
+        boolean changed = false;
+
+        mCause = cause;
+
+        if (!mDisconnected) {
+            doDisconnect();
+
+            if (DBG) Rlog.d(LOG_TAG, "onDisconnect: cause=" + cause);
+
+            mOwner.getPhone().notifyDisconnect(this);
+
+            if (mParent != null) {
+                changed = mParent.connectionDisconnected(this);
+            }
+
+            mOrigConnection = null;
+        }
+        clearPostDialListeners();
+        releaseWakeLock();
+        return changed;
+    }
+
+    //CDMA
+    /** Called when the call waiting connection has been hung up */
+    /*package*/ void
+    onLocalDisconnect() {
+        if (!mDisconnected) {
+            doDisconnect();
+            if (VDBG) Rlog.d(LOG_TAG, "onLoalDisconnect" );
+
+            if (mParent != null) {
+                mParent.detach(this);
+            }
+        }
+        releaseWakeLock();
+    }
+
+    // Returns true if state has changed, false if nothing changed
+    public boolean
+    update (DriverCall dc) {
+        GsmCdmaCall newParent;
+        boolean changed = false;
+        boolean wasConnectingInOrOut = isConnectingInOrOut();
+        boolean wasHolding = (getState() == GsmCdmaCall.State.HOLDING);
+
+        newParent = parentFromDCState(dc.state);
+
+        if (Phone.DEBUG_PHONE) log("parent= " +mParent +", newParent= " + newParent);
+
+        //Ignore dc.number and dc.name in case of a handover connection
+        if (isPhoneTypeGsm() && mOrigConnection != null) {
+            if (Phone.DEBUG_PHONE) log("update: mOrigConnection is not null");
+        } else if (isIncoming()) {
+            if (!equalsBaseDialString(mAddress, dc.number) && (!mNumberConverted
+                    || !equalsBaseDialString(mConvertedNumber, dc.number))) {
+                if (Phone.DEBUG_PHONE) log("update: phone # changed!");
+                mAddress = dc.number;
+                changed = true;
+            }
+        }
+
+        // A null cnapName should be the same as ""
+        if (TextUtils.isEmpty(dc.name)) {
+            if (!TextUtils.isEmpty(mCnapName)) {
+                changed = true;
+                mCnapName = "";
+            }
+        } else if (!dc.name.equals(mCnapName)) {
+            changed = true;
+            mCnapName = dc.name;
+        }
+
+        if (Phone.DEBUG_PHONE) log("--dssds----"+mCnapName);
+        mCnapNamePresentation = dc.namePresentation;
+        mNumberPresentation = dc.numberPresentation;
+
+        if (newParent != mParent) {
+            if (mParent != null) {
+                mParent.detach(this);
+            }
+            newParent.attach(this, dc);
+            mParent = newParent;
+            changed = true;
+        } else {
+            boolean parentStateChange;
+            parentStateChange = mParent.update (this, dc);
+            changed = changed || parentStateChange;
+        }
+
+        /** Some state-transition events */
+
+        if (Phone.DEBUG_PHONE) log(
+                "update: parent=" + mParent +
+                ", hasNewParent=" + (newParent != mParent) +
+                ", wasConnectingInOrOut=" + wasConnectingInOrOut +
+                ", wasHolding=" + wasHolding +
+                ", isConnectingInOrOut=" + isConnectingInOrOut() +
+                ", changed=" + changed);
+
+
+        if (wasConnectingInOrOut && !isConnectingInOrOut()) {
+            onConnectedInOrOut();
+        }
+
+        if (changed && !wasHolding && (getState() == GsmCdmaCall.State.HOLDING)) {
+            // We've transitioned into HOLDING
+            onStartedHolding();
+        }
+
+        return changed;
+    }
+
+    /**
+     * Called when this Connection is in the foregroundCall
+     * when a dial is initiated.
+     * We know we're ACTIVE, and we know we're going to end up
+     * HOLDING in the backgroundCall
+     */
+    void
+    fakeHoldBeforeDial() {
+        if (mParent != null) {
+            mParent.detach(this);
+        }
+
+        mParent = mOwner.mBackgroundCall;
+        mParent.attachFake(this, GsmCdmaCall.State.HOLDING);
+
+        onStartedHolding();
+    }
+
+    /*package*/ int
+    getGsmCdmaIndex() throws CallStateException {
+        if (mIndex >= 0) {
+            return mIndex + 1;
+        } else {
+            throw new CallStateException ("GsmCdma index not yet assigned");
+        }
+    }
+
+    /**
+     * An incoming or outgoing call has connected
+     */
+    void
+    onConnectedInOrOut() {
+        mConnectTime = System.currentTimeMillis();
+        mConnectTimeReal = SystemClock.elapsedRealtime();
+        mDuration = 0;
+
+        // bug #678474: incoming call interpreted as missed call, even though
+        // it sounds like the user has picked up the call.
+        if (Phone.DEBUG_PHONE) {
+            log("onConnectedInOrOut: connectTime=" + mConnectTime);
+        }
+
+        if (!mIsIncoming) {
+            // outgoing calls only
+            processNextPostDialChar();
+        } else {
+            // Only release wake lock for incoming calls, for outgoing calls the wake lock
+            // will be released after any pause-dial is completed
+            releaseWakeLock();
+        }
+    }
+
+    private void
+    doDisconnect() {
+        mIndex = -1;
+        mDisconnectTime = System.currentTimeMillis();
+        mDuration = SystemClock.elapsedRealtime() - mConnectTimeReal;
+        mDisconnected = true;
+        clearPostDialListeners();
+    }
+
+    /*package*/ void
+    onStartedHolding() {
+        mHoldingStartTime = SystemClock.elapsedRealtime();
+    }
+
+    /**
+     * Performs the appropriate action for a post-dial char, but does not
+     * notify application. returns false if the character is invalid and
+     * should be ignored
+     */
+    private boolean
+    processPostDialChar(char c) {
+        if (PhoneNumberUtils.is12Key(c)) {
+            mOwner.mCi.sendDtmf(c, mHandler.obtainMessage(EVENT_DTMF_DONE));
+        } else if (isPause(c)) {
+            if (!isPhoneTypeGsm()) {
+                setPostDialState(PostDialState.PAUSE);
+            }
+            // From TS 22.101:
+            // It continues...
+            // Upon the called party answering the UE shall send the DTMF digits
+            // automatically to the network after a delay of 3 seconds( 20 ).
+            // The digits shall be sent according to the procedures and timing
+            // specified in 3GPP TS 24.008 [13]. The first occurrence of the
+            // "DTMF Control Digits Separator" shall be used by the ME to
+            // distinguish between the addressing digits (i.e. the phone number)
+            // and the DTMF digits. Upon subsequent occurrences of the
+            // separator,
+            // the UE shall pause again for 3 seconds ( 20 ) before sending
+            // any further DTMF digits.
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_PAUSE_DONE),
+                    isPhoneTypeGsm() ? PAUSE_DELAY_MILLIS_GSM: PAUSE_DELAY_MILLIS_CDMA);
+        } else if (isWait(c)) {
+            setPostDialState(PostDialState.WAIT);
+        } else if (isWild(c)) {
+            setPostDialState(PostDialState.WILD);
+        } else {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public String
+    getRemainingPostDialString() {
+        String subStr = super.getRemainingPostDialString();
+        if (!isPhoneTypeGsm() && !TextUtils.isEmpty(subStr)) {
+            int wIndex = subStr.indexOf(PhoneNumberUtils.WAIT);
+            int pIndex = subStr.indexOf(PhoneNumberUtils.PAUSE);
+
+            if (wIndex > 0 && (wIndex < pIndex || pIndex <= 0)) {
+                subStr = subStr.substring(0, wIndex);
+            } else if (pIndex > 0) {
+                subStr = subStr.substring(0, pIndex);
+            }
+        }
+        return subStr;
+    }
+
+    //CDMA
+    public void updateParent(GsmCdmaCall oldParent, GsmCdmaCall newParent){
+        if (newParent != oldParent) {
+            if (oldParent != null) {
+                oldParent.detach(this);
+            }
+            newParent.attachFake(this, GsmCdmaCall.State.ACTIVE);
+            mParent = newParent;
+        }
+    }
+
+    @Override
+    protected void finalize()
+    {
+        /**
+         * It is understood that This finalizer is not guaranteed
+         * to be called and the release lock call is here just in
+         * case there is some path that doesn't call onDisconnect
+         * and or onConnectedInOrOut.
+         */
+        if (mPartialWakeLock != null && mPartialWakeLock.isHeld()) {
+            Rlog.e(LOG_TAG, "UNEXPECTED; mPartialWakeLock is held when finalizing.");
+        }
+        clearPostDialListeners();
+        releaseWakeLock();
+    }
+
+    private void
+    processNextPostDialChar() {
+        char c = 0;
+        Registrant postDialHandler;
+
+        if (mPostDialState == PostDialState.CANCELLED) {
+            releaseWakeLock();
+            return;
+        }
+
+        if (mPostDialString == null ||
+                mPostDialString.length() <= mNextPostDialChar) {
+            setPostDialState(PostDialState.COMPLETE);
+
+            // We were holding a wake lock until pause-dial was complete, so give it up now
+            releaseWakeLock();
+
+            // notifyMessage.arg1 is 0 on complete
+            c = 0;
+        } else {
+            boolean isValid;
+
+            setPostDialState(PostDialState.STARTED);
+
+            c = mPostDialString.charAt(mNextPostDialChar++);
+
+            isValid = processPostDialChar(c);
+
+            if (!isValid) {
+                // Will call processNextPostDialChar
+                mHandler.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget();
+                // Don't notify application
+                Rlog.e(LOG_TAG, "processNextPostDialChar: c=" + c + " isn't valid!");
+                return;
+            }
+        }
+
+        notifyPostDialListenersNextChar(c);
+
+        // TODO: remove the following code since the handler no longer executes anything.
+        postDialHandler = mOwner.getPhone().getPostDialHandler();
+
+        Message notifyMessage;
+
+        if (postDialHandler != null
+                && (notifyMessage = postDialHandler.messageForRegistrant()) != null) {
+            // The AsyncResult.result is the Connection object
+            PostDialState state = mPostDialState;
+            AsyncResult ar = AsyncResult.forMessage(notifyMessage);
+            ar.result = this;
+            ar.userObj = state;
+
+            // arg1 is the character that was/is being processed
+            notifyMessage.arg1 = c;
+
+            //Rlog.v("GsmCdma", "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c);
+            notifyMessage.sendToTarget();
+        }
+    }
+
+    /** "connecting" means "has never been ACTIVE" for both incoming
+     *  and outgoing calls
+     */
+    private boolean
+    isConnectingInOrOut() {
+        return mParent == null || mParent == mOwner.mRingingCall
+            || mParent.mState == GsmCdmaCall.State.DIALING
+            || mParent.mState == GsmCdmaCall.State.ALERTING;
+    }
+
+    private GsmCdmaCall
+    parentFromDCState (DriverCall.State state) {
+        switch (state) {
+            case ACTIVE:
+            case DIALING:
+            case ALERTING:
+                return mOwner.mForegroundCall;
+            //break;
+
+            case HOLDING:
+                return mOwner.mBackgroundCall;
+            //break;
+
+            case INCOMING:
+            case WAITING:
+                return mOwner.mRingingCall;
+            //break;
+
+            default:
+                throw new RuntimeException("illegal call state: " + state);
+        }
+    }
+
+    /**
+     * Set post dial state and acquire wake lock while switching to "started" or "pause"
+     * state, the wake lock will be released if state switches out of "started" or "pause"
+     * state or after WAKE_LOCK_TIMEOUT_MILLIS.
+     * @param s new PostDialState
+     */
+    private void setPostDialState(PostDialState s) {
+        if (s == PostDialState.STARTED ||
+                s == PostDialState.PAUSE) {
+            synchronized (mPartialWakeLock) {
+                if (mPartialWakeLock.isHeld()) {
+                    mHandler.removeMessages(EVENT_WAKE_LOCK_TIMEOUT);
+                } else {
+                    acquireWakeLock();
+                }
+                Message msg = mHandler.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT);
+                mHandler.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS);
+            }
+        } else {
+            mHandler.removeMessages(EVENT_WAKE_LOCK_TIMEOUT);
+            releaseWakeLock();
+        }
+        mPostDialState = s;
+        notifyPostDialListeners();
+    }
+
+    private void createWakeLock(Context context) {
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+    }
+
+    private void acquireWakeLock() {
+        if (mPartialWakeLock != null) {
+            synchronized (mPartialWakeLock) {
+                log("acquireWakeLock");
+                mPartialWakeLock.acquire();
+            }
+        }
+    }
+
+    private void releaseWakeLock() {
+        if (mPartialWakeLock != null) {
+            synchronized (mPartialWakeLock) {
+                if (mPartialWakeLock.isHeld()) {
+                    log("releaseWakeLock");
+                    mPartialWakeLock.release();
+                }
+            }
+        }
+    }
+
+    private void releaseAllWakeLocks() {
+        if (mPartialWakeLock != null) {
+            synchronized (mPartialWakeLock) {
+                while (mPartialWakeLock.isHeld()) {
+                    mPartialWakeLock.release();
+                }
+            }
+        }
+    }
+
+    private static boolean isPause(char c) {
+        return c == PhoneNumberUtils.PAUSE;
+    }
+
+    private static boolean isWait(char c) {
+        return c == PhoneNumberUtils.WAIT;
+    }
+
+    private static boolean isWild(char c) {
+        return c == PhoneNumberUtils.WILD;
+    }
+
+    //CDMA
+    // This function is to find the next PAUSE character index if
+    // multiple pauses in a row. Otherwise it finds the next non PAUSE or
+    // non WAIT character index.
+    private static int findNextPCharOrNonPOrNonWCharIndex(String phoneNumber, int currIndex) {
+        boolean wMatched = isWait(phoneNumber.charAt(currIndex));
+        int index = currIndex + 1;
+        int length = phoneNumber.length();
+        while (index < length) {
+            char cNext = phoneNumber.charAt(index);
+            // if there is any W inside P/W sequence,mark it
+            if (isWait(cNext)) {
+                wMatched = true;
+            }
+            // if any characters other than P/W chars after P/W sequence
+            // we break out the loop and append the correct
+            if (!isWait(cNext) && !isPause(cNext)) {
+                break;
+            }
+            index++;
+        }
+
+        // It means the PAUSE character(s) is in the middle of dial string
+        // and it needs to be handled one by one.
+        if ((index < length) && (index > (currIndex + 1))  &&
+                ((wMatched == false) && isPause(phoneNumber.charAt(currIndex)))) {
+            return (currIndex + 1);
+        }
+        return index;
+    }
+
+    // CDMA
+    // This function returns either PAUSE or WAIT character to append.
+    // It is based on the next non PAUSE/WAIT character in the phoneNumber and the
+    // index for the current PAUSE/WAIT character
+    private static char findPOrWCharToAppend(String phoneNumber, int currPwIndex,
+                                             int nextNonPwCharIndex) {
+        char c = phoneNumber.charAt(currPwIndex);
+        char ret;
+
+        // Append the PW char
+        ret = (isPause(c)) ? PhoneNumberUtils.PAUSE : PhoneNumberUtils.WAIT;
+
+        // If the nextNonPwCharIndex is greater than currPwIndex + 1,
+        // it means the PW sequence contains not only P characters.
+        // Since for the sequence that only contains P character,
+        // the P character is handled one by one, the nextNonPwCharIndex
+        // equals to currPwIndex + 1.
+        // In this case, skip P, append W.
+        if (nextNonPwCharIndex > (currPwIndex + 1)) {
+            ret = PhoneNumberUtils.WAIT;
+        }
+        return ret;
+    }
+
+    private String maskDialString(String dialString) {
+        if (VDBG) {
+            return dialString;
+        }
+
+        return "<MASKED>";
+    }
+
+    private void fetchDtmfToneDelay(GsmCdmaPhone phone) {
+        CarrierConfigManager configMgr = (CarrierConfigManager)
+                phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle b = configMgr.getConfigForSubId(phone.getSubId());
+        if (b != null) {
+            mDtmfToneDelay = b.getInt(phone.getDtmfToneDelayKey());
+        }
+    }
+
+    private boolean isPhoneTypeGsm() {
+        return mOwner.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_GSM;
+    }
+
+    private void log(String msg) {
+        Rlog.d(LOG_TAG, "[GsmCdmaConn] " + msg);
+    }
+
+    @Override
+    public int getNumberPresentation() {
+        return mNumberPresentation;
+    }
+
+    @Override
+    public UUSInfo getUUSInfo() {
+        return mUusInfo;
+    }
+
+    public int getPreciseDisconnectCause() {
+        return mPreciseCause;
+    }
+
+    @Override
+    public String getVendorDisconnectCause() {
+        return mVendorCause;
+    }
+
+    @Override
+    public void migrateFrom(Connection c) {
+        if (c == null) return;
+
+        super.migrateFrom(c);
+
+        this.mUusInfo = c.getUUSInfo();
+
+        this.setUserData(c.getUserData());
+    }
+
+    @Override
+    public Connection getOrigConnection() {
+        return mOrigConnection;
+    }
+
+    @Override
+    public boolean isMultiparty() {
+        if (mOrigConnection != null) {
+            return mOrigConnection.isMultiparty();
+        }
+
+        return false;
+    }
+}
diff --git a/com/android/internal/telephony/GsmCdmaPhone.java b/com/android/internal/telephony/GsmCdmaPhone.java
new file mode 100644
index 0000000..d95d018
--- /dev/null
+++ b/com/android/internal/telephony/GsmCdmaPhone.java
@@ -0,0 +1,3499 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_DISABLE;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_ENABLE;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_ERASURE;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_REGISTRATION;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_ALL;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_ALL_CONDITIONAL;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_BUSY;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_NOT_REACHABLE;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_NO_REPLY;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_UNCONDITIONAL;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_VOICE;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.ResultReceiver;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.telecom.VideoProfile;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellLocation;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UssdResponse;
+import android.telephony.cdma.CdmaCellLocation;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.ImsManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.CdmaMmiCode;
+import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.cdma.EriManager;
+import com.android.internal.telephony.gsm.GsmMmiCode;
+import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.test.SimulatedRadioControl;
+import com.android.internal.telephony.uicc.IccCardProxy;
+import com.android.internal.telephony.uicc.IccException;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.IccVmNotSupportedException;
+import com.android.internal.telephony.uicc.IsimRecords;
+import com.android.internal.telephony.uicc.IsimUiccRecords;
+import com.android.internal.telephony.uicc.RuimRecords;
+import com.android.internal.telephony.uicc.SIMRecords;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+import com.android.internal.telephony.uicc.UiccController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ */
+public class GsmCdmaPhone extends Phone {
+    // NOTE that LOG_TAG here is "GsmCdma", which means that log messages
+    // from this file will go into the radio log rather than the main
+    // log.  (Use "adb logcat -b radio" to see them.)
+    public static final String LOG_TAG = "GsmCdmaPhone";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; /* STOPSHIP if true */
+
+    //GSM
+    // Key used to read/write voice mail number
+    private static final String VM_NUMBER = "vm_number_key";
+    // Key used to read/write the SIM IMSI used for storing the voice mail
+    private static final String VM_SIM_IMSI = "vm_sim_imsi_key";
+    /** List of Registrants to receive Supplementary Service Notifications. */
+    private RegistrantList mSsnRegistrants = new RegistrantList();
+
+    //CDMA
+    // Default Emergency Callback Mode exit timer
+    private static final int DEFAULT_ECM_EXIT_TIMER_VALUE = 300000;
+    private static final String VM_NUMBER_CDMA = "vm_number_key_cdma";
+    public static final int RESTART_ECM_TIMER = 0; // restart Ecm timer
+    public static final int CANCEL_ECM_TIMER = 1; // cancel Ecm timer
+    private CdmaSubscriptionSourceManager mCdmaSSM;
+    public int mCdmaSubscriptionSource = CdmaSubscriptionSourceManager.SUBSCRIPTION_SOURCE_UNKNOWN;
+    public EriManager mEriManager;
+    private PowerManager.WakeLock mWakeLock;
+    // mEriFileLoadedRegistrants are informed after the ERI text has been loaded
+    private final RegistrantList mEriFileLoadedRegistrants = new RegistrantList();
+    // mEcmExitRespRegistrant is informed after the phone has been exited
+    private Registrant mEcmExitRespRegistrant;
+    private String mEsn;
+    private String mMeid;
+    // string to define how the carrier specifies its own ota sp number
+    private String mCarrierOtaSpNumSchema;
+
+    // A runnable which is used to automatically exit from Ecm after a period of time.
+    private Runnable mExitEcmRunnable = new Runnable() {
+        @Override
+        public void run() {
+            exitEmergencyCallbackMode();
+        }
+    };
+    public static final String PROPERTY_CDMA_HOME_OPERATOR_NUMERIC =
+            "ro.cdma.home.operator.numeric";
+
+    //CDMALTE
+    /** PHONE_TYPE_CDMA_LTE in addition to RuimRecords needs access to SIMRecords and
+     * IsimUiccRecords
+     */
+    private SIMRecords mSimRecords;
+
+    //Common
+    // Instance Variables
+    private IsimUiccRecords mIsimUiccRecords;
+    public GsmCdmaCallTracker mCT;
+    public ServiceStateTracker mSST;
+    private ArrayList <MmiCode> mPendingMMIs = new ArrayList<MmiCode>();
+    private IccPhoneBookInterfaceManager mIccPhoneBookIntManager;
+    private DeviceStateMonitor mDeviceStateMonitor;
+
+    private int mPrecisePhoneType;
+
+    // mEcmTimerResetRegistrants are informed after Ecm timer is canceled or re-started
+    private final RegistrantList mEcmTimerResetRegistrants = new RegistrantList();
+
+    private String mImei;
+    private String mImeiSv;
+    private String mVmNumber;
+
+    // Create Cfu (Call forward unconditional) so that dialing number &
+    // mOnComplete (Message object passed by client) can be packed &
+    // given as a single Cfu object as user data to RIL.
+    private static class Cfu {
+        final String mSetCfNumber;
+        final Message mOnComplete;
+
+        Cfu(String cfNumber, Message onComplete) {
+            mSetCfNumber = cfNumber;
+            mOnComplete = onComplete;
+        }
+    }
+
+    private IccSmsInterfaceManager mIccSmsInterfaceManager;
+    private IccCardProxy mIccCardProxy;
+
+    private boolean mResetModemOnRadioTechnologyChange = false;
+
+    private int mRilVersion;
+    private boolean mBroadcastEmergencyCallStateChanges = false;
+    private CarrierKeyDownloadManager mCDM;
+    // Constructors
+
+    public GsmCdmaPhone(Context context, CommandsInterface ci, PhoneNotifier notifier, int phoneId,
+                        int precisePhoneType, TelephonyComponentFactory telephonyComponentFactory) {
+        this(context, ci, notifier, false, phoneId, precisePhoneType, telephonyComponentFactory);
+    }
+
+    public GsmCdmaPhone(Context context, CommandsInterface ci, PhoneNotifier notifier,
+                        boolean unitTestMode, int phoneId, int precisePhoneType,
+                        TelephonyComponentFactory telephonyComponentFactory) {
+        super(precisePhoneType == PhoneConstants.PHONE_TYPE_GSM ? "GSM" : "CDMA",
+                notifier, context, ci, unitTestMode, phoneId, telephonyComponentFactory);
+
+        // phone type needs to be set before other initialization as other objects rely on it
+        mPrecisePhoneType = precisePhoneType;
+        initOnce(ci);
+        initRatSpecific(precisePhoneType);
+        // CarrierSignalAgent uses CarrierActionAgent in construction so it needs to be created
+        // after CarrierActionAgent.
+        mCarrierActionAgent = mTelephonyComponentFactory.makeCarrierActionAgent(this);
+        mCarrierSignalAgent = mTelephonyComponentFactory.makeCarrierSignalAgent(this);
+        mSST = mTelephonyComponentFactory.makeServiceStateTracker(this, this.mCi);
+        // DcTracker uses SST so needs to be created after it is instantiated
+        mDcTracker = mTelephonyComponentFactory.makeDcTracker(this);
+        mSST.registerForNetworkAttached(this, EVENT_REGISTERED_TO_NETWORK, null);
+        mDeviceStateMonitor = mTelephonyComponentFactory.makeDeviceStateMonitor(this);
+        logd("GsmCdmaPhone: constructor: sub = " + mPhoneId);
+    }
+
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Rlog.d(LOG_TAG, "mBroadcastReceiver: action " + intent.getAction());
+            if (intent.getAction().equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                sendMessage(obtainMessage(EVENT_CARRIER_CONFIG_CHANGED));
+            }
+        }
+    };
+
+    private void initOnce(CommandsInterface ci) {
+        if (ci instanceof SimulatedRadioControl) {
+            mSimulatedRadioControl = (SimulatedRadioControl) ci;
+        }
+
+        mCT = mTelephonyComponentFactory.makeGsmCdmaCallTracker(this);
+        mIccPhoneBookIntManager = mTelephonyComponentFactory.makeIccPhoneBookInterfaceManager(this);
+        PowerManager pm
+                = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+        mIccSmsInterfaceManager = mTelephonyComponentFactory.makeIccSmsInterfaceManager(this);
+        mIccCardProxy = mTelephonyComponentFactory.makeIccCardProxy(mContext, mCi, mPhoneId);
+
+        mCi.registerForAvailable(this, EVENT_RADIO_AVAILABLE, null);
+        mCi.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null);
+        mCi.registerForOn(this, EVENT_RADIO_ON, null);
+        mCi.setOnSuppServiceNotification(this, EVENT_SSN, null);
+
+        //GSM
+        mCi.setOnUSSD(this, EVENT_USSD, null);
+        mCi.setOnSs(this, EVENT_SS, null);
+
+        //CDMA
+        mCdmaSSM = mTelephonyComponentFactory.getCdmaSubscriptionSourceManagerInstance(mContext,
+                mCi, this, EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null);
+        mEriManager = mTelephonyComponentFactory.makeEriManager(this, mContext,
+                EriManager.ERI_FROM_XML);
+        mCi.setEmergencyCallbackMode(this, EVENT_EMERGENCY_CALLBACK_MODE_ENTER, null);
+        mCi.registerForExitEmergencyCallbackMode(this, EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE,
+                null);
+        mCi.registerForModemReset(this, EVENT_MODEM_RESET, null);
+        // get the string that specifies the carrier OTA Sp number
+        mCarrierOtaSpNumSchema = TelephonyManager.from(mContext).getOtaSpNumberSchemaForPhone(
+                getPhoneId(), "");
+
+        mResetModemOnRadioTechnologyChange = SystemProperties.getBoolean(
+                TelephonyProperties.PROPERTY_RESET_ON_RADIO_TECH_CHANGE, false);
+
+        mCi.registerForRilConnected(this, EVENT_RIL_CONNECTED, null);
+        mCi.registerForVoiceRadioTechChanged(this, EVENT_VOICE_RADIO_TECH_CHANGED, null);
+        mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(
+                CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+        mCDM = new CarrierKeyDownloadManager(this);
+    }
+
+    private void initRatSpecific(int precisePhoneType) {
+        mPendingMMIs.clear();
+        mIccPhoneBookIntManager.updateIccRecords(null);
+        mEsn = null;
+        mMeid = null;
+
+        mPrecisePhoneType = precisePhoneType;
+
+        TelephonyManager tm = TelephonyManager.from(mContext);
+        if (isPhoneTypeGsm()) {
+            mCi.setPhoneType(PhoneConstants.PHONE_TYPE_GSM);
+            tm.setPhoneType(getPhoneId(), PhoneConstants.PHONE_TYPE_GSM);
+            mIccCardProxy.setVoiceRadioTech(ServiceState.RIL_RADIO_TECHNOLOGY_UMTS);
+        } else {
+            mCdmaSubscriptionSource = CdmaSubscriptionSourceManager.SUBSCRIPTION_SOURCE_UNKNOWN;
+            // This is needed to handle phone process crashes
+            mIsPhoneInEcmState = getInEcmMode();
+            if (mIsPhoneInEcmState) {
+                // Send a message which will invoke handleExitEmergencyCallbackMode
+                mCi.exitEmergencyCallbackMode(
+                        obtainMessage(EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE));
+            }
+
+            mCi.setPhoneType(PhoneConstants.PHONE_TYPE_CDMA);
+            tm.setPhoneType(getPhoneId(), PhoneConstants.PHONE_TYPE_CDMA);
+            mIccCardProxy.setVoiceRadioTech(ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT);
+            // Sets operator properties by retrieving from build-time system property
+            String operatorAlpha = SystemProperties.get("ro.cdma.home.operator.alpha");
+            String operatorNumeric = SystemProperties.get(PROPERTY_CDMA_HOME_OPERATOR_NUMERIC);
+            logd("init: operatorAlpha='" + operatorAlpha
+                    + "' operatorNumeric='" + operatorNumeric + "'");
+            if (mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_3GPP) ==
+                    null || isPhoneTypeCdmaLte()) {
+                if (!TextUtils.isEmpty(operatorAlpha)) {
+                    logd("init: set 'gsm.sim.operator.alpha' to operator='" + operatorAlpha + "'");
+                    tm.setSimOperatorNameForPhone(mPhoneId, operatorAlpha);
+                }
+                if (!TextUtils.isEmpty(operatorNumeric)) {
+                    logd("init: set 'gsm.sim.operator.numeric' to operator='" + operatorNumeric +
+                            "'");
+                    logd("update icc_operator_numeric=" + operatorNumeric);
+                    tm.setSimOperatorNumericForPhone(mPhoneId, operatorNumeric);
+
+                    SubscriptionController.getInstance().setMccMnc(operatorNumeric, getSubId());
+                    // Sets iso country property by retrieving from build-time system property
+                    setIsoCountryProperty(operatorNumeric);
+                    // Updates MCC MNC device configuration information
+                    logd("update mccmnc=" + operatorNumeric);
+                    MccTable.updateMccMncConfiguration(mContext, operatorNumeric, false);
+                }
+            }
+
+            // Sets current entry in the telephony carrier table
+            updateCurrentCarrierInProvider(operatorNumeric);
+        }
+    }
+
+    //CDMA
+    /**
+     * Sets PROPERTY_ICC_OPERATOR_ISO_COUNTRY property
+     *
+     */
+    private void setIsoCountryProperty(String operatorNumeric) {
+        TelephonyManager tm = TelephonyManager.from(mContext);
+        if (TextUtils.isEmpty(operatorNumeric)) {
+            logd("setIsoCountryProperty: clear 'gsm.sim.operator.iso-country'");
+            tm.setSimCountryIsoForPhone(mPhoneId, "");
+        } else {
+            String iso = "";
+            try {
+                iso = MccTable.countryCodeForMcc(Integer.parseInt(
+                        operatorNumeric.substring(0,3)));
+            } catch (NumberFormatException ex) {
+                Rlog.e(LOG_TAG, "setIsoCountryProperty: countryCodeForMcc error", ex);
+            } catch (StringIndexOutOfBoundsException ex) {
+                Rlog.e(LOG_TAG, "setIsoCountryProperty: countryCodeForMcc error", ex);
+            }
+
+            logd("setIsoCountryProperty: set 'gsm.sim.operator.iso-country' to iso=" + iso);
+            tm.setSimCountryIsoForPhone(mPhoneId, iso);
+        }
+    }
+
+    public boolean isPhoneTypeGsm() {
+        return mPrecisePhoneType == PhoneConstants.PHONE_TYPE_GSM;
+    }
+
+    public boolean isPhoneTypeCdma() {
+        return mPrecisePhoneType == PhoneConstants.PHONE_TYPE_CDMA;
+    }
+
+    public boolean isPhoneTypeCdmaLte() {
+        return mPrecisePhoneType == PhoneConstants.PHONE_TYPE_CDMA_LTE;
+    }
+
+    private void switchPhoneType(int precisePhoneType) {
+        removeCallbacks(mExitEcmRunnable);
+
+        initRatSpecific(precisePhoneType);
+
+        mSST.updatePhoneType();
+        setPhoneName(precisePhoneType == PhoneConstants.PHONE_TYPE_GSM ? "GSM" : "CDMA");
+        onUpdateIccAvailability();
+        mCT.updatePhoneType();
+
+        CommandsInterface.RadioState radioState = mCi.getRadioState();
+        if (radioState.isAvailable()) {
+            handleRadioAvailable();
+            if (radioState.isOn()) {
+                handleRadioOn();
+            }
+        }
+        if (!radioState.isAvailable() || !radioState.isOn()) {
+            handleRadioOffOrNotAvailable();
+        }
+    }
+
+    @Override
+    protected void finalize() {
+        if(DBG) logd("GsmCdmaPhone finalized");
+        if (mWakeLock != null && mWakeLock.isHeld()) {
+            Rlog.e(LOG_TAG, "UNEXPECTED; mWakeLock is held when finalizing.");
+            mWakeLock.release();
+        }
+    }
+
+    @Override
+    public ServiceState getServiceState() {
+        if (mSST == null || mSST.mSS.getState() != ServiceState.STATE_IN_SERVICE) {
+            if (mImsPhone != null) {
+                return ServiceState.mergeServiceStates(
+                        (mSST == null) ? new ServiceState() : mSST.mSS,
+                        mImsPhone.getServiceState());
+            }
+        }
+
+        if (mSST != null) {
+            return mSST.mSS;
+        } else {
+            // avoid potential NPE in EmergencyCallHelper during Phone switch
+            return new ServiceState();
+        }
+    }
+
+    @Override
+    public CellLocation getCellLocation(WorkSource workSource) {
+        if (isPhoneTypeGsm()) {
+            return mSST.getCellLocation(workSource);
+        } else {
+            CdmaCellLocation loc = (CdmaCellLocation)mSST.mCellLoc;
+
+            int mode = Settings.Secure.getInt(getContext().getContentResolver(),
+                    Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+            if (mode == Settings.Secure.LOCATION_MODE_OFF) {
+                // clear lat/long values for location privacy
+                CdmaCellLocation privateLoc = new CdmaCellLocation();
+                privateLoc.setCellLocationData(loc.getBaseStationId(),
+                        CdmaCellLocation.INVALID_LAT_LONG,
+                        CdmaCellLocation.INVALID_LAT_LONG,
+                        loc.getSystemId(), loc.getNetworkId());
+                loc = privateLoc;
+            }
+            return loc;
+        }
+    }
+
+    @Override
+    public PhoneConstants.State getState() {
+        if (mImsPhone != null) {
+            PhoneConstants.State imsState = mImsPhone.getState();
+            if (imsState != PhoneConstants.State.IDLE) {
+                return imsState;
+            }
+        }
+
+        return mCT.mState;
+    }
+
+    @Override
+    public int getPhoneType() {
+        if (mPrecisePhoneType == PhoneConstants.PHONE_TYPE_GSM) {
+            return PhoneConstants.PHONE_TYPE_GSM;
+        } else {
+            return PhoneConstants.PHONE_TYPE_CDMA;
+        }
+    }
+
+    @Override
+    public ServiceStateTracker getServiceStateTracker() {
+        return mSST;
+    }
+
+    @Override
+    public CallTracker getCallTracker() {
+        return mCT;
+    }
+
+    @Override
+    public void updateVoiceMail() {
+        if (isPhoneTypeGsm()) {
+            int countVoiceMessages = 0;
+            IccRecords r = mIccRecords.get();
+            if (r != null) {
+                // get voice mail count from SIM
+                countVoiceMessages = r.getVoiceMessageCount();
+            }
+            if (countVoiceMessages == IccRecords.DEFAULT_VOICE_MESSAGE_COUNT) {
+                countVoiceMessages = getStoredVoiceMessageCount();
+            }
+            logd("updateVoiceMail countVoiceMessages = " + countVoiceMessages
+                    + " subId " + getSubId());
+            setVoiceMessageCount(countVoiceMessages);
+        } else {
+            setVoiceMessageCount(getStoredVoiceMessageCount());
+        }
+    }
+
+    @Override
+    public List<? extends MmiCode>
+    getPendingMmiCodes() {
+        return mPendingMMIs;
+    }
+
+    @Override
+    public PhoneConstants.DataState getDataConnectionState(String apnType) {
+        PhoneConstants.DataState ret = PhoneConstants.DataState.DISCONNECTED;
+
+        if (mSST == null) {
+            // Radio Technology Change is ongoning, dispose() and removeReferences() have
+            // already been called
+
+            ret = PhoneConstants.DataState.DISCONNECTED;
+        } else if (mSST.getCurrentDataConnectionState() != ServiceState.STATE_IN_SERVICE
+                && (isPhoneTypeCdma() ||
+                (isPhoneTypeGsm() && !apnType.equals(PhoneConstants.APN_TYPE_EMERGENCY)))) {
+            // If we're out of service, open TCP sockets may still work
+            // but no data will flow
+
+            // Emergency APN is available even in Out Of Service
+            // Pass the actual State of EPDN
+
+            ret = PhoneConstants.DataState.DISCONNECTED;
+        } else { /* mSST.gprsState == ServiceState.STATE_IN_SERVICE */
+            switch (mDcTracker.getState(apnType)) {
+                case RETRYING:
+                case FAILED:
+                case IDLE:
+                    ret = PhoneConstants.DataState.DISCONNECTED;
+                break;
+
+                case CONNECTED:
+                case DISCONNECTING:
+                    if ( mCT.mState != PhoneConstants.State.IDLE
+                            && !mSST.isConcurrentVoiceAndDataAllowed()) {
+                        ret = PhoneConstants.DataState.SUSPENDED;
+                    } else {
+                        ret = PhoneConstants.DataState.CONNECTED;
+                    }
+                break;
+
+                case CONNECTING:
+                case SCANNING:
+                    ret = PhoneConstants.DataState.CONNECTING;
+                break;
+            }
+        }
+
+        logd("getDataConnectionState apnType=" + apnType + " ret=" + ret);
+        return ret;
+    }
+
+    @Override
+    public DataActivityState getDataActivityState() {
+        DataActivityState ret = DataActivityState.NONE;
+
+        if (mSST.getCurrentDataConnectionState() == ServiceState.STATE_IN_SERVICE) {
+            switch (mDcTracker.getActivity()) {
+                case DATAIN:
+                    ret = DataActivityState.DATAIN;
+                break;
+
+                case DATAOUT:
+                    ret = DataActivityState.DATAOUT;
+                break;
+
+                case DATAINANDOUT:
+                    ret = DataActivityState.DATAINANDOUT;
+                break;
+
+                case DORMANT:
+                    ret = DataActivityState.DORMANT;
+                break;
+
+                default:
+                    ret = DataActivityState.NONE;
+                break;
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Notify any interested party of a Phone state change
+     * {@link com.android.internal.telephony.PhoneConstants.State}
+     */
+    public void notifyPhoneStateChanged() {
+        mNotifier.notifyPhoneState(this);
+    }
+
+    /**
+     * Notify registrants of a change in the call state. This notifies changes in
+     * {@link com.android.internal.telephony.Call.State}. Use this when changes
+     * in the precise call state are needed, else use notifyPhoneStateChanged.
+     */
+    public void notifyPreciseCallStateChanged() {
+        /* we'd love it if this was package-scoped*/
+        super.notifyPreciseCallStateChangedP();
+    }
+
+    public void notifyNewRingingConnection(Connection c) {
+        super.notifyNewRingingConnectionP(c);
+    }
+
+    public void notifyDisconnect(Connection cn) {
+        mDisconnectRegistrants.notifyResult(cn);
+
+        mNotifier.notifyDisconnectCause(cn.getDisconnectCause(), cn.getPreciseDisconnectCause());
+    }
+
+    public void notifyUnknownConnection(Connection cn) {
+        super.notifyUnknownConnectionP(cn);
+    }
+
+    @Override
+    public boolean isInEmergencyCall() {
+        if (isPhoneTypeGsm()) {
+            return false;
+        } else {
+            return mCT.isInEmergencyCall();
+        }
+    }
+
+    @Override
+    protected void setIsInEmergencyCall() {
+        if (!isPhoneTypeGsm()) {
+            mCT.setIsInEmergencyCall();
+        }
+    }
+
+    //CDMA
+    private void sendEmergencyCallbackModeChange(){
+        //Send an Intent
+        Intent intent = new Intent(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
+        intent.putExtra(PhoneConstants.PHONE_IN_ECM_STATE, isInEcm());
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, getPhoneId());
+        ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+        if (DBG) logd("sendEmergencyCallbackModeChange");
+    }
+
+    @Override
+    public void sendEmergencyCallStateChange(boolean callActive) {
+        if (mBroadcastEmergencyCallStateChanges) {
+            Intent intent = new Intent(TelephonyIntents.ACTION_EMERGENCY_CALL_STATE_CHANGED);
+            intent.putExtra(PhoneConstants.PHONE_IN_EMERGENCY_CALL, callActive);
+            SubscriptionManager.putPhoneIdAndSubIdExtra(intent, getPhoneId());
+            ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+            if (DBG) Rlog.d(LOG_TAG, "sendEmergencyCallStateChange: callActive " + callActive);
+        }
+    }
+
+    @Override
+    public void setBroadcastEmergencyCallStateChanges(boolean broadcast) {
+        mBroadcastEmergencyCallStateChanges = broadcast;
+    }
+
+    public void notifySuppServiceFailed(SuppService code) {
+        mSuppServiceFailedRegistrants.notifyResult(code);
+    }
+
+    public void notifyServiceStateChanged(ServiceState ss) {
+        super.notifyServiceStateChangedP(ss);
+    }
+
+    public void notifyLocationChanged() {
+        mNotifier.notifyCellLocation(this);
+    }
+
+    @Override
+    public void notifyCallForwardingIndicator() {
+        mNotifier.notifyCallForwardingChanged(this);
+    }
+
+    // override for allowing access from other classes of this package
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setSystemProperty(String property, String value) {
+        if (getUnitTestMode()) {
+            return;
+        }
+        if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
+            TelephonyManager.setTelephonyProperty(mPhoneId, property, value);
+        } else {
+            super.setSystemProperty(property, value);
+        }
+    }
+
+    @Override
+    public void registerForSuppServiceNotification(
+            Handler h, int what, Object obj) {
+        mSsnRegistrants.addUnique(h, what, obj);
+        if (mSsnRegistrants.size() == 1) mCi.setSuppServiceNotifications(true, null);
+    }
+
+    @Override
+    public void unregisterForSuppServiceNotification(Handler h) {
+        mSsnRegistrants.remove(h);
+        if (mSsnRegistrants.size() == 0) mCi.setSuppServiceNotifications(false, null);
+    }
+
+    @Override
+    public void registerForSimRecordsLoaded(Handler h, int what, Object obj) {
+        mSimRecordsLoadedRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForSimRecordsLoaded(Handler h) {
+        mSimRecordsLoadedRegistrants.remove(h);
+    }
+
+    @Override
+    public void acceptCall(int videoState) throws CallStateException {
+        Phone imsPhone = mImsPhone;
+        if ( imsPhone != null && imsPhone.getRingingCall().isRinging() ) {
+            imsPhone.acceptCall(videoState);
+        } else {
+            mCT.acceptCall();
+        }
+    }
+
+    @Override
+    public void rejectCall() throws CallStateException {
+        mCT.rejectCall();
+    }
+
+    @Override
+    public void switchHoldingAndActive() throws CallStateException {
+        mCT.switchWaitingOrHoldingAndActive();
+    }
+
+    @Override
+    public String getIccSerialNumber() {
+        IccRecords r = mIccRecords.get();
+        if (!isPhoneTypeGsm() && r == null) {
+            // to get ICCID form SIMRecords because it is on MF.
+            r = mUiccController.getIccRecords(mPhoneId, UiccController.APP_FAM_3GPP);
+        }
+        return (r != null) ? r.getIccId() : null;
+    }
+
+    @Override
+    public String getFullIccSerialNumber() {
+        IccRecords r = mIccRecords.get();
+        if (!isPhoneTypeGsm() && r == null) {
+            // to get ICCID form SIMRecords because it is on MF.
+            r = mUiccController.getIccRecords(mPhoneId, UiccController.APP_FAM_3GPP);
+        }
+        return (r != null) ? r.getFullIccId() : null;
+    }
+
+    @Override
+    public boolean canConference() {
+        if (mImsPhone != null && mImsPhone.canConference()) {
+            return true;
+        }
+        if (isPhoneTypeGsm()) {
+            return mCT.canConference();
+        } else {
+            loge("canConference: not possible in CDMA");
+            return false;
+        }
+    }
+
+    @Override
+    public void conference() {
+        if (mImsPhone != null && mImsPhone.canConference()) {
+            logd("conference() - delegated to IMS phone");
+            try {
+                mImsPhone.conference();
+            } catch (CallStateException e) {
+                loge(e.toString());
+            }
+            return;
+        }
+        if (isPhoneTypeGsm()) {
+            mCT.conference();
+        } else {
+            // three way calls in CDMA will be handled by feature codes
+            loge("conference: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void enableEnhancedVoicePrivacy(boolean enable, Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            loge("enableEnhancedVoicePrivacy: not expected on GSM");
+        } else {
+            mCi.setPreferredVoicePrivacy(enable, onComplete);
+        }
+    }
+
+    @Override
+    public void getEnhancedVoicePrivacy(Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            loge("getEnhancedVoicePrivacy: not expected on GSM");
+        } else {
+            mCi.getPreferredVoicePrivacy(onComplete);
+        }
+    }
+
+    @Override
+    public void clearDisconnected() {
+        mCT.clearDisconnected();
+    }
+
+    @Override
+    public boolean canTransfer() {
+        if (isPhoneTypeGsm()) {
+            return mCT.canTransfer();
+        } else {
+            loge("canTransfer: not possible in CDMA");
+            return false;
+        }
+    }
+
+    @Override
+    public void explicitCallTransfer() {
+        if (isPhoneTypeGsm()) {
+            mCT.explicitCallTransfer();
+        } else {
+            loge("explicitCallTransfer: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public GsmCdmaCall getForegroundCall() {
+        return mCT.mForegroundCall;
+    }
+
+    @Override
+    public GsmCdmaCall getBackgroundCall() {
+        return mCT.mBackgroundCall;
+    }
+
+    @Override
+    public Call getRingingCall() {
+        Phone imsPhone = mImsPhone;
+        // It returns the ringing call of ImsPhone if the ringing call of GSMPhone isn't ringing.
+        // In CallManager.registerPhone(), it always registers ringing call of ImsPhone, because
+        // the ringing call of GSMPhone isn't ringing. Consequently, it can't answer GSM call
+        // successfully by invoking TelephonyManager.answerRingingCall() since the implementation
+        // in PhoneInterfaceManager.answerRingingCallInternal() could not get the correct ringing
+        // call from CallManager. So we check the ringing call state of imsPhone first as
+        // accpetCall() does.
+        if ( imsPhone != null && imsPhone.getRingingCall().isRinging()) {
+            return imsPhone.getRingingCall();
+        }
+        return mCT.mRingingCall;
+    }
+
+    private boolean handleCallDeflectionIncallSupplementaryService(
+            String dialString) {
+        if (dialString.length() > 1) {
+            return false;
+        }
+
+        if (getRingingCall().getState() != GsmCdmaCall.State.IDLE) {
+            if (DBG) logd("MmiCode 0: rejectCall");
+            try {
+                mCT.rejectCall();
+            } catch (CallStateException e) {
+                if (DBG) Rlog.d(LOG_TAG,
+                        "reject failed", e);
+                notifySuppServiceFailed(Phone.SuppService.REJECT);
+            }
+        } else if (getBackgroundCall().getState() != GsmCdmaCall.State.IDLE) {
+            if (DBG) logd("MmiCode 0: hangupWaitingOrBackground");
+            mCT.hangupWaitingOrBackground();
+        }
+
+        return true;
+    }
+
+    //GSM
+    private boolean handleCallWaitingIncallSupplementaryService(String dialString) {
+        int len = dialString.length();
+
+        if (len > 2) {
+            return false;
+        }
+
+        GsmCdmaCall call = getForegroundCall();
+
+        try {
+            if (len > 1) {
+                char ch = dialString.charAt(1);
+                int callIndex = ch - '0';
+
+                if (callIndex >= 1 && callIndex <= GsmCdmaCallTracker.MAX_CONNECTIONS_GSM) {
+                    if (DBG) logd("MmiCode 1: hangupConnectionByIndex " + callIndex);
+                    mCT.hangupConnectionByIndex(call, callIndex);
+                }
+            } else {
+                if (call.getState() != GsmCdmaCall.State.IDLE) {
+                    if (DBG) logd("MmiCode 1: hangup foreground");
+                    //mCT.hangupForegroundResumeBackground();
+                    mCT.hangup(call);
+                } else {
+                    if (DBG) logd("MmiCode 1: switchWaitingOrHoldingAndActive");
+                    mCT.switchWaitingOrHoldingAndActive();
+                }
+            }
+        } catch (CallStateException e) {
+            if (DBG) Rlog.d(LOG_TAG,
+                    "hangup failed", e);
+            notifySuppServiceFailed(Phone.SuppService.HANGUP);
+        }
+
+        return true;
+    }
+
+    private boolean handleCallHoldIncallSupplementaryService(String dialString) {
+        int len = dialString.length();
+
+        if (len > 2) {
+            return false;
+        }
+
+        GsmCdmaCall call = getForegroundCall();
+
+        if (len > 1) {
+            try {
+                char ch = dialString.charAt(1);
+                int callIndex = ch - '0';
+                GsmCdmaConnection conn = mCT.getConnectionByIndex(call, callIndex);
+
+                // GsmCdma index starts at 1, up to 5 connections in a call,
+                if (conn != null && callIndex >= 1 && callIndex <= GsmCdmaCallTracker.MAX_CONNECTIONS_GSM) {
+                    if (DBG) logd("MmiCode 2: separate call " + callIndex);
+                    mCT.separate(conn);
+                } else {
+                    if (DBG) logd("separate: invalid call index " + callIndex);
+                    notifySuppServiceFailed(Phone.SuppService.SEPARATE);
+                }
+            } catch (CallStateException e) {
+                if (DBG) Rlog.d(LOG_TAG, "separate failed", e);
+                notifySuppServiceFailed(Phone.SuppService.SEPARATE);
+            }
+        } else {
+            try {
+                if (getRingingCall().getState() != GsmCdmaCall.State.IDLE) {
+                    if (DBG) logd("MmiCode 2: accept ringing call");
+                    mCT.acceptCall();
+                } else {
+                    if (DBG) logd("MmiCode 2: switchWaitingOrHoldingAndActive");
+                    mCT.switchWaitingOrHoldingAndActive();
+                }
+            } catch (CallStateException e) {
+                if (DBG) Rlog.d(LOG_TAG, "switch failed", e);
+                notifySuppServiceFailed(Phone.SuppService.SWITCH);
+            }
+        }
+
+        return true;
+    }
+
+    private boolean handleMultipartyIncallSupplementaryService(String dialString) {
+        if (dialString.length() > 1) {
+            return false;
+        }
+
+        if (DBG) logd("MmiCode 3: merge calls");
+        conference();
+        return true;
+    }
+
+    private boolean handleEctIncallSupplementaryService(String dialString) {
+
+        int len = dialString.length();
+
+        if (len != 1) {
+            return false;
+        }
+
+        if (DBG) logd("MmiCode 4: explicit call transfer");
+        explicitCallTransfer();
+        return true;
+    }
+
+    private boolean handleCcbsIncallSupplementaryService(String dialString) {
+        if (dialString.length() > 1) {
+            return false;
+        }
+
+        Rlog.i(LOG_TAG, "MmiCode 5: CCBS not supported!");
+        // Treat it as an "unknown" service.
+        notifySuppServiceFailed(Phone.SuppService.UNKNOWN);
+        return true;
+    }
+
+    @Override
+    public boolean handleInCallMmiCommands(String dialString) throws CallStateException {
+        if (!isPhoneTypeGsm()) {
+            loge("method handleInCallMmiCommands is NOT supported in CDMA!");
+            return false;
+        }
+
+        Phone imsPhone = mImsPhone;
+        if (imsPhone != null
+                && imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE) {
+            return imsPhone.handleInCallMmiCommands(dialString);
+        }
+
+        if (!isInCall()) {
+            return false;
+        }
+
+        if (TextUtils.isEmpty(dialString)) {
+            return false;
+        }
+
+        boolean result = false;
+        char ch = dialString.charAt(0);
+        switch (ch) {
+            case '0':
+                result = handleCallDeflectionIncallSupplementaryService(dialString);
+                break;
+            case '1':
+                result = handleCallWaitingIncallSupplementaryService(dialString);
+                break;
+            case '2':
+                result = handleCallHoldIncallSupplementaryService(dialString);
+                break;
+            case '3':
+                result = handleMultipartyIncallSupplementaryService(dialString);
+                break;
+            case '4':
+                result = handleEctIncallSupplementaryService(dialString);
+                break;
+            case '5':
+                result = handleCcbsIncallSupplementaryService(dialString);
+                break;
+            default:
+                break;
+        }
+
+        return result;
+    }
+
+    public boolean isInCall() {
+        GsmCdmaCall.State foregroundCallState = getForegroundCall().getState();
+        GsmCdmaCall.State backgroundCallState = getBackgroundCall().getState();
+        GsmCdmaCall.State ringingCallState = getRingingCall().getState();
+
+       return (foregroundCallState.isAlive() ||
+                backgroundCallState.isAlive() ||
+                ringingCallState.isAlive());
+    }
+
+    @Override
+    public Connection dial(String dialString, int videoState) throws CallStateException {
+        return dial(dialString, null, videoState, null);
+    }
+
+    @Override
+    public Connection dial(String dialString, UUSInfo uusInfo, int videoState, Bundle intentExtras)
+            throws CallStateException {
+        if (!isPhoneTypeGsm() && uusInfo != null) {
+            throw new CallStateException("Sending UUS information NOT supported in CDMA!");
+        }
+
+        boolean isEmergency = PhoneNumberUtils.isEmergencyNumber(getSubId(), dialString);
+        Phone imsPhone = mImsPhone;
+
+        CarrierConfigManager configManager =
+                (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        boolean alwaysTryImsForEmergencyCarrierConfig = configManager.getConfigForSubId(getSubId())
+                .getBoolean(CarrierConfigManager.KEY_CARRIER_USE_IMS_FIRST_FOR_EMERGENCY_BOOL);
+
+        boolean imsUseEnabled = isImsUseEnabled()
+                 && imsPhone != null
+                 && (imsPhone.isVolteEnabled() || imsPhone.isWifiCallingEnabled() ||
+                 (imsPhone.isVideoEnabled() && VideoProfile.isVideo(videoState)))
+                 && (imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE);
+
+        boolean useImsForEmergency = imsPhone != null
+                && isEmergency
+                && alwaysTryImsForEmergencyCarrierConfig
+                && ImsManager.isNonTtyOrTtyOnVolteEnabled(mContext)
+                && imsPhone.isImsAvailable();
+
+        String dialPart = PhoneNumberUtils.extractNetworkPortionAlt(PhoneNumberUtils.
+                stripSeparators(dialString));
+        boolean isUt = (dialPart.startsWith("*") || dialPart.startsWith("#"))
+                && dialPart.endsWith("#");
+
+        boolean useImsForUt = imsPhone != null && imsPhone.isUtEnabled();
+
+        if (DBG) {
+            logd("imsUseEnabled=" + imsUseEnabled
+                    + ", useImsForEmergency=" + useImsForEmergency
+                    + ", useImsForUt=" + useImsForUt
+                    + ", isUt=" + isUt
+                    + ", imsPhone=" + imsPhone
+                    + ", imsPhone.isVolteEnabled()="
+                    + ((imsPhone != null) ? imsPhone.isVolteEnabled() : "N/A")
+                    + ", imsPhone.isVowifiEnabled()="
+                    + ((imsPhone != null) ? imsPhone.isWifiCallingEnabled() : "N/A")
+                    + ", imsPhone.isVideoEnabled()="
+                    + ((imsPhone != null) ? imsPhone.isVideoEnabled() : "N/A")
+                    + ", imsPhone.getServiceState().getState()="
+                    + ((imsPhone != null) ? imsPhone.getServiceState().getState() : "N/A"));
+        }
+
+        Phone.checkWfcWifiOnlyModeBeforeDial(mImsPhone, mContext);
+
+        if ((imsUseEnabled && (!isUt || useImsForUt)) || useImsForEmergency) {
+            try {
+                if (DBG) logd("Trying IMS PS call");
+                return imsPhone.dial(dialString, uusInfo, videoState, intentExtras);
+            } catch (CallStateException e) {
+                if (DBG) logd("IMS PS call exception " + e +
+                        "imsUseEnabled =" + imsUseEnabled + ", imsPhone =" + imsPhone);
+                // Do not throw a CallStateException and instead fall back to Circuit switch
+                // for emergency calls and MMI codes.
+                if (Phone.CS_FALLBACK.equals(e.getMessage()) || isEmergency) {
+                    logi("IMS call failed with Exception: " + e.getMessage() + ". Falling back "
+                            + "to CS.");
+                } else {
+                    CallStateException ce = new CallStateException(e.getMessage());
+                    ce.setStackTrace(e.getStackTrace());
+                    throw ce;
+                }
+            }
+        }
+
+        if (mSST != null && mSST.mSS.getState() == ServiceState.STATE_OUT_OF_SERVICE
+                && mSST.mSS.getDataRegState() != ServiceState.STATE_IN_SERVICE && !isEmergency) {
+            throw new CallStateException("cannot dial in current state");
+        }
+        // Check non-emergency voice CS call - shouldn't dial when POWER_OFF
+        if (mSST != null && mSST.mSS.getState() == ServiceState.STATE_POWER_OFF /* CS POWER_OFF */
+                && !VideoProfile.isVideo(videoState) /* voice call */
+                && !isEmergency /* non-emergency call */) {
+            throw new CallStateException(
+                CallStateException.ERROR_POWER_OFF,
+                "cannot dial voice call in airplane mode");
+        }
+        // Check for service before placing non emergency CS voice call.
+        // Allow dial only if either CS is camped on any RAT (or) PS is in LTE service.
+        if (mSST != null
+                && mSST.mSS.getState() == ServiceState.STATE_OUT_OF_SERVICE /* CS out of service */
+                && !(mSST.mSS.getDataRegState() == ServiceState.STATE_IN_SERVICE
+                    && ServiceState.isLte(mSST.mSS.getRilDataRadioTechnology())) /* PS not in LTE */
+                && !VideoProfile.isVideo(videoState) /* voice call */
+                && !isEmergency /* non-emergency call */) {
+            throw new CallStateException(
+                CallStateException.ERROR_OUT_OF_SERVICE,
+                "cannot dial voice call in out of service");
+        }
+        if (DBG) logd("Trying (non-IMS) CS call");
+
+        if (isPhoneTypeGsm()) {
+            return dialInternal(dialString, null, VideoProfile.STATE_AUDIO_ONLY, intentExtras);
+        } else {
+            return dialInternal(dialString, null, videoState, intentExtras);
+        }
+    }
+
+    /**
+     * @return {@code true} if the user should be informed of an attempt to dial an international
+     * number while on WFC only, {@code false} otherwise.
+     */
+    public boolean isNotificationOfWfcCallRequired(String dialString) {
+        CarrierConfigManager configManager =
+                (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle config = configManager.getConfigForSubId(getSubId());
+
+        // Determine if carrier config indicates that international calls over WFC should trigger a
+        // notification to the user. This is controlled by carrier configuration and is off by
+        // default.
+        boolean shouldNotifyInternationalCallOnWfc = config != null
+                && config.getBoolean(
+                        CarrierConfigManager.KEY_NOTIFY_INTERNATIONAL_CALL_ON_WFC_BOOL);
+
+        if (!shouldNotifyInternationalCallOnWfc) {
+            return false;
+        }
+
+        Phone imsPhone = mImsPhone;
+        boolean isEmergency = PhoneNumberUtils.isEmergencyNumber(getSubId(), dialString);
+        boolean shouldConfirmCall =
+                        // Using IMS
+                        isImsUseEnabled()
+                        && imsPhone != null
+                        // VoLTE not available
+                        && !imsPhone.isVolteEnabled()
+                        // WFC is available
+                        && imsPhone.isWifiCallingEnabled()
+                        && !isEmergency
+                        // Dialing international number
+                        && PhoneNumberUtils.isInternationalNumber(dialString, getCountryIso());
+        return shouldConfirmCall;
+    }
+
+    @Override
+    protected Connection dialInternal(String dialString, UUSInfo uusInfo, int videoState,
+                                      Bundle intentExtras)
+            throws CallStateException {
+        return dialInternal(dialString, uusInfo, videoState, intentExtras, null);
+    }
+
+    protected Connection dialInternal(String dialString, UUSInfo uusInfo, int videoState,
+                                      Bundle intentExtras, ResultReceiver wrappedCallback)
+            throws CallStateException {
+
+        // Need to make sure dialString gets parsed properly
+        String newDialString = PhoneNumberUtils.stripSeparators(dialString);
+
+        if (isPhoneTypeGsm()) {
+            // handle in-call MMI first if applicable
+            if (handleInCallMmiCommands(newDialString)) {
+                return null;
+            }
+
+            // Only look at the Network portion for mmi
+            String networkPortion = PhoneNumberUtils.extractNetworkPortionAlt(newDialString);
+            GsmMmiCode mmi = GsmMmiCode.newFromDialString(networkPortion, this,
+                    mUiccApplication.get(), wrappedCallback);
+            if (DBG) logd("dialInternal: dialing w/ mmi '" + mmi + "'...");
+
+            if (mmi == null) {
+                return mCT.dial(newDialString, uusInfo, intentExtras);
+            } else if (mmi.isTemporaryModeCLIR()) {
+                return mCT.dial(mmi.mDialingNumber, mmi.getCLIRMode(), uusInfo, intentExtras);
+            } else {
+                mPendingMMIs.add(mmi);
+                mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
+                mmi.processCode();
+                return null;
+            }
+        } else {
+            return mCT.dial(newDialString);
+        }
+    }
+
+   @Override
+    public boolean handlePinMmi(String dialString) {
+        MmiCode mmi;
+        if (isPhoneTypeGsm()) {
+            mmi = GsmMmiCode.newFromDialString(dialString, this, mUiccApplication.get());
+        } else {
+            mmi = CdmaMmiCode.newFromDialString(dialString, this, mUiccApplication.get());
+        }
+
+        if (mmi != null && mmi.isPinPukCommand()) {
+            mPendingMMIs.add(mmi);
+            mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
+            try {
+                mmi.processCode();
+            } catch (CallStateException e) {
+                //do nothing
+            }
+            return true;
+        }
+
+        loge("Mmi is null or unrecognized!");
+        return false;
+    }
+
+    private void sendUssdResponse(String ussdRequest, CharSequence message, int returnCode,
+                                   ResultReceiver wrappedCallback) {
+        UssdResponse response = new UssdResponse(ussdRequest, message);
+        Bundle returnData = new Bundle();
+        returnData.putParcelable(TelephonyManager.USSD_RESPONSE, response);
+        wrappedCallback.send(returnCode, returnData);
+    }
+
+    @Override
+    public boolean handleUssdRequest(String ussdRequest, ResultReceiver wrappedCallback) {
+        if (!isPhoneTypeGsm() || mPendingMMIs.size() > 0) {
+            //todo: replace the generic failure with specific error code.
+            sendUssdResponse(ussdRequest, null, TelephonyManager.USSD_RETURN_FAILURE,
+                    wrappedCallback );
+            return true;
+        }
+
+        // Try over IMS if possible.
+        Phone imsPhone = mImsPhone;
+        if ((imsPhone != null)
+                && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+                || imsPhone.isUtEnabled())) {
+            try {
+                logd("handleUssdRequest: attempting over IMS");
+                return imsPhone.handleUssdRequest(ussdRequest, wrappedCallback);
+            } catch (CallStateException cse) {
+                if (!CS_FALLBACK.equals(cse.getMessage())) {
+                    return false;
+                }
+                // At this point we've tried over IMS but have been informed we need to handover
+                // back to GSM.
+                logd("handleUssdRequest: fallback to CS required");
+            }
+        }
+
+        // Try USSD over GSM.
+        try {
+            dialInternal(ussdRequest, null, VideoProfile.STATE_AUDIO_ONLY, null,
+                    wrappedCallback);
+        } catch (Exception e) {
+            logd("handleUssdRequest: exception" + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void sendUssdResponse(String ussdMessge) {
+        if (isPhoneTypeGsm()) {
+            GsmMmiCode mmi = GsmMmiCode.newFromUssdUserInput(ussdMessge, this, mUiccApplication.get());
+            mPendingMMIs.add(mmi);
+            mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
+            mmi.sendUssd(ussdMessge);
+        } else {
+            loge("sendUssdResponse: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void sendDtmf(char c) {
+        if (!PhoneNumberUtils.is12Key(c)) {
+            loge("sendDtmf called with invalid character '" + c + "'");
+        } else {
+            if (mCT.mState ==  PhoneConstants.State.OFFHOOK) {
+                mCi.sendDtmf(c, null);
+            }
+        }
+    }
+
+    @Override
+    public void startDtmf(char c) {
+        if (!PhoneNumberUtils.is12Key(c)) {
+            loge("startDtmf called with invalid character '" + c + "'");
+        } else {
+            mCi.startDtmf(c, null);
+        }
+    }
+
+    @Override
+    public void stopDtmf() {
+        mCi.stopDtmf(null);
+    }
+
+    @Override
+    public void sendBurstDtmf(String dtmfString, int on, int off, Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            loge("[GsmCdmaPhone] sendBurstDtmf() is a CDMA method");
+        } else {
+            boolean check = true;
+            for (int itr = 0;itr < dtmfString.length(); itr++) {
+                if (!PhoneNumberUtils.is12Key(dtmfString.charAt(itr))) {
+                    Rlog.e(LOG_TAG,
+                            "sendDtmf called with invalid character '" + dtmfString.charAt(itr)+ "'");
+                    check = false;
+                    break;
+                }
+            }
+            if (mCT.mState == PhoneConstants.State.OFFHOOK && check) {
+                mCi.sendBurstDtmf(dtmfString, on, off, onComplete);
+            }
+        }
+    }
+
+    @Override
+    public void setRadioPower(boolean power) {
+        mSST.setRadioPower(power);
+    }
+
+    private void storeVoiceMailNumber(String number) {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        SharedPreferences.Editor editor = sp.edit();
+        if (isPhoneTypeGsm()) {
+            editor.putString(VM_NUMBER + getPhoneId(), number);
+            editor.apply();
+            setVmSimImsi(getSubscriberId());
+        } else {
+            editor.putString(VM_NUMBER_CDMA + getPhoneId(), number);
+            editor.apply();
+        }
+    }
+
+    @Override
+    public String getVoiceMailNumber() {
+        String number = null;
+        if (isPhoneTypeGsm()) {
+            // Read from the SIM. If its null, try reading from the shared preference area.
+            IccRecords r = mIccRecords.get();
+            number = (r != null) ? r.getVoiceMailNumber() : "";
+            if (TextUtils.isEmpty(number)) {
+                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+                number = sp.getString(VM_NUMBER + getPhoneId(), null);
+            }
+        } else {
+            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+            number = sp.getString(VM_NUMBER_CDMA + getPhoneId(), null);
+        }
+
+        if (TextUtils.isEmpty(number)) {
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            PersistableBundle b = configManager.getConfig();
+            if (b != null) {
+                String defaultVmNumber =
+                        b.getString(CarrierConfigManager.KEY_DEFAULT_VM_NUMBER_STRING);
+                if (!TextUtils.isEmpty(defaultVmNumber)) {
+                    number = defaultVmNumber;
+                }
+            }
+        }
+
+        if (!isPhoneTypeGsm() && TextUtils.isEmpty(number)) {
+            // Read platform settings for dynamic voicemail number
+            if (getContext().getResources().getBoolean(com.android.internal
+                    .R.bool.config_telephony_use_own_number_for_voicemail)) {
+                number = getLine1Number();
+            } else {
+                number = "*86";
+            }
+        }
+
+        return number;
+    }
+
+    private String getVmSimImsi() {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        return sp.getString(VM_SIM_IMSI + getPhoneId(), null);
+    }
+
+    private void setVmSimImsi(String imsi) {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putString(VM_SIM_IMSI + getPhoneId(), imsi);
+        editor.apply();
+    }
+
+    @Override
+    public String getVoiceMailAlphaTag() {
+        String ret = "";
+
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+
+            ret = (r != null) ? r.getVoiceMailAlphaTag() : "";
+        }
+
+        if (ret == null || ret.length() == 0) {
+            return mContext.getText(
+                com.android.internal.R.string.defaultVoiceMailAlphaTag).toString();
+        }
+
+        return ret;
+    }
+
+    @Override
+    public String getDeviceId() {
+        if (isPhoneTypeGsm()) {
+            return mImei;
+        } else {
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            boolean force_imei = configManager.getConfigForSubId(getSubId())
+                    .getBoolean(CarrierConfigManager.KEY_FORCE_IMEI_BOOL);
+            if (force_imei) return mImei;
+
+            String id = getMeid();
+            if ((id == null) || id.matches("^0*$")) {
+                loge("getDeviceId(): MEID is not initialized use ESN");
+                id = getEsn();
+            }
+            return id;
+        }
+    }
+
+    @Override
+    public String getDeviceSvn() {
+        if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
+            return mImeiSv;
+        } else {
+            loge("getDeviceSvn(): return 0");
+            return "0";
+        }
+    }
+
+    @Override
+    public IsimRecords getIsimRecords() {
+        return mIsimUiccRecords;
+    }
+
+    @Override
+    public String getImei() {
+        return mImei;
+    }
+
+    @Override
+    public String getEsn() {
+        if (isPhoneTypeGsm()) {
+            loge("[GsmCdmaPhone] getEsn() is a CDMA method");
+            return "0";
+        } else {
+            return mEsn;
+        }
+    }
+
+    @Override
+    public String getMeid() {
+        return mMeid;
+    }
+
+    @Override
+    public String getNai() {
+        IccRecords r = mUiccController.getIccRecords(mPhoneId, UiccController.APP_FAM_3GPP2);
+        if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
+            Rlog.v(LOG_TAG, "IccRecords is " + r);
+        }
+        return (r != null) ? r.getNAI() : null;
+    }
+
+    @Override
+    public String getSubscriberId() {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            return (r != null) ? r.getIMSI() : null;
+        } else if (isPhoneTypeCdma()) {
+            return mSST.getImsi();
+        } else { //isPhoneTypeCdmaLte()
+            return (mSimRecords != null) ? mSimRecords.getIMSI() : "";
+        }
+    }
+
+    @Override
+    public ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int keyType) {
+        return CarrierInfoManager.getCarrierInfoForImsiEncryption(keyType, mContext);
+    }
+
+    @Override
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo) {
+        CarrierInfoManager.setCarrierInfoForImsiEncryption(imsiEncryptionInfo, mContext);
+    }
+
+    @Override
+    public String getGroupIdLevel1() {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            return (r != null) ? r.getGid1() : null;
+        } else if (isPhoneTypeCdma()) {
+            loge("GID1 is not available in CDMA");
+            return null;
+        } else { //isPhoneTypeCdmaLte()
+            return (mSimRecords != null) ? mSimRecords.getGid1() : "";
+        }
+    }
+
+    @Override
+    public String getGroupIdLevel2() {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            return (r != null) ? r.getGid2() : null;
+        } else if (isPhoneTypeCdma()) {
+            loge("GID2 is not available in CDMA");
+            return null;
+        } else { //isPhoneTypeCdmaLte()
+            return (mSimRecords != null) ? mSimRecords.getGid2() : "";
+        }
+    }
+
+    @Override
+    public String getLine1Number() {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            return (r != null) ? r.getMsisdnNumber() : null;
+        } else {
+            return mSST.getMdnNumber();
+        }
+    }
+
+    @Override
+    public String getCdmaPrlVersion() {
+        return mSST.getPrlVersion();
+    }
+
+    @Override
+    public String getCdmaMin() {
+        return mSST.getCdmaMin();
+    }
+
+    @Override
+    public boolean isMinInfoReady() {
+        return mSST.isMinInfoReady();
+    }
+
+    @Override
+    public String getMsisdn() {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            return (r != null) ? r.getMsisdnNumber() : null;
+        } else if (isPhoneTypeCdmaLte()) {
+            return (mSimRecords != null) ? mSimRecords.getMsisdnNumber() : null;
+        } else {
+            loge("getMsisdn: not expected on CDMA");
+            return null;
+        }
+    }
+
+    @Override
+    public String getLine1AlphaTag() {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            return (r != null) ? r.getMsisdnAlphaTag() : null;
+        } else {
+            loge("getLine1AlphaTag: not possible in CDMA");
+            return null;
+        }
+    }
+
+    @Override
+    public boolean setLine1Number(String alphaTag, String number, Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            if (r != null) {
+                r.setMsisdnNumber(alphaTag, number, onComplete);
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            loge("setLine1Number: not possible in CDMA");
+            return false;
+        }
+    }
+
+    @Override
+    public void setVoiceMailNumber(String alphaTag, String voiceMailNumber, Message onComplete) {
+        Message resp;
+        mVmNumber = voiceMailNumber;
+        resp = obtainMessage(EVENT_SET_VM_NUMBER_DONE, 0, 0, onComplete);
+        IccRecords r = mIccRecords.get();
+        if (r != null) {
+            r.setVoiceMailNumber(alphaTag, mVmNumber, resp);
+        }
+    }
+
+    private boolean isValidCommandInterfaceCFReason (int commandInterfaceCFReason) {
+        switch (commandInterfaceCFReason) {
+            case CF_REASON_UNCONDITIONAL:
+            case CF_REASON_BUSY:
+            case CF_REASON_NO_REPLY:
+            case CF_REASON_NOT_REACHABLE:
+            case CF_REASON_ALL:
+            case CF_REASON_ALL_CONDITIONAL:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public String getSystemProperty(String property, String defValue) {
+        if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
+            if (getUnitTestMode()) {
+                return null;
+            }
+            return TelephonyManager.getTelephonyProperty(mPhoneId, property, defValue);
+        } else {
+            return super.getSystemProperty(property, defValue);
+        }
+    }
+
+    private boolean isValidCommandInterfaceCFAction (int commandInterfaceCFAction) {
+        switch (commandInterfaceCFAction) {
+            case CF_ACTION_DISABLE:
+            case CF_ACTION_ENABLE:
+            case CF_ACTION_REGISTRATION:
+            case CF_ACTION_ERASURE:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private boolean isCfEnable(int action) {
+        return (action == CF_ACTION_ENABLE) || (action == CF_ACTION_REGISTRATION);
+    }
+
+    @Override
+    public void getCallForwardingOption(int commandInterfaceCFReason, Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            Phone imsPhone = mImsPhone;
+            if ((imsPhone != null)
+                    && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+                    || imsPhone.isUtEnabled())) {
+                imsPhone.getCallForwardingOption(commandInterfaceCFReason, onComplete);
+                return;
+            }
+
+            if (isValidCommandInterfaceCFReason(commandInterfaceCFReason)) {
+                if (DBG) logd("requesting call forwarding query.");
+                Message resp;
+                if (commandInterfaceCFReason == CF_REASON_UNCONDITIONAL) {
+                    resp = obtainMessage(EVENT_GET_CALL_FORWARD_DONE, onComplete);
+                } else {
+                    resp = onComplete;
+                }
+                mCi.queryCallForwardStatus(commandInterfaceCFReason, 0, null, resp);
+            }
+        } else {
+            loge("getCallForwardingOption: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void setCallForwardingOption(int commandInterfaceCFAction,
+            int commandInterfaceCFReason,
+            String dialingNumber,
+            int timerSeconds,
+            Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            Phone imsPhone = mImsPhone;
+            if ((imsPhone != null)
+                    && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+                    || imsPhone.isUtEnabled())) {
+                imsPhone.setCallForwardingOption(commandInterfaceCFAction,
+                        commandInterfaceCFReason, dialingNumber, timerSeconds, onComplete);
+                return;
+            }
+
+            if ((isValidCommandInterfaceCFAction(commandInterfaceCFAction)) &&
+                    (isValidCommandInterfaceCFReason(commandInterfaceCFReason))) {
+
+                Message resp;
+                if (commandInterfaceCFReason == CF_REASON_UNCONDITIONAL) {
+                    Cfu cfu = new Cfu(dialingNumber, onComplete);
+                    resp = obtainMessage(EVENT_SET_CALL_FORWARD_DONE,
+                            isCfEnable(commandInterfaceCFAction) ? 1 : 0, 0, cfu);
+                } else {
+                    resp = onComplete;
+                }
+                mCi.setCallForward(commandInterfaceCFAction,
+                        commandInterfaceCFReason,
+                        CommandsInterface.SERVICE_CLASS_VOICE,
+                        dialingNumber,
+                        timerSeconds,
+                        resp);
+            }
+        } else {
+            loge("setCallForwardingOption: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void getOutgoingCallerIdDisplay(Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            Phone imsPhone = mImsPhone;
+            if ((imsPhone != null)
+                    && (imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)) {
+                imsPhone.getOutgoingCallerIdDisplay(onComplete);
+                return;
+            }
+            mCi.getCLIR(onComplete);
+        } else {
+            loge("getOutgoingCallerIdDisplay: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            Phone imsPhone = mImsPhone;
+            if ((imsPhone != null)
+                    && (imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)) {
+                imsPhone.setOutgoingCallerIdDisplay(commandInterfaceCLIRMode, onComplete);
+                return;
+            }
+            // Packing CLIR value in the message. This will be required for
+            // SharedPreference caching, if the message comes back as part of
+            // a success response.
+            mCi.setCLIR(commandInterfaceCLIRMode,
+                    obtainMessage(EVENT_SET_CLIR_COMPLETE, commandInterfaceCLIRMode, 0, onComplete));
+        } else {
+            loge("setOutgoingCallerIdDisplay: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void getCallWaiting(Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            Phone imsPhone = mImsPhone;
+            if ((imsPhone != null)
+                    && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+                    || imsPhone.isUtEnabled())) {
+                imsPhone.getCallWaiting(onComplete);
+                return;
+            }
+
+            //As per 3GPP TS 24.083, section 1.6 UE doesn't need to send service
+            //class parameter in call waiting interrogation  to network
+            mCi.queryCallWaiting(CommandsInterface.SERVICE_CLASS_NONE, onComplete);
+        } else {
+            mCi.queryCallWaiting(CommandsInterface.SERVICE_CLASS_VOICE, onComplete);
+        }
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, Message onComplete) {
+        if (isPhoneTypeGsm()) {
+            Phone imsPhone = mImsPhone;
+            if ((imsPhone != null)
+                    && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+                    || imsPhone.isUtEnabled())) {
+                imsPhone.setCallWaiting(enable, onComplete);
+                return;
+            }
+
+            mCi.setCallWaiting(enable, CommandsInterface.SERVICE_CLASS_VOICE, onComplete);
+        } else {
+            loge("method setCallWaiting is NOT supported in CDMA!");
+        }
+    }
+
+    @Override
+    public void getAvailableNetworks(Message response) {
+        if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
+            mCi.getAvailableNetworks(response);
+        } else {
+            loge("getAvailableNetworks: not possible in CDMA");
+        }
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message response) {
+        mCi.startNetworkScan(nsr, response);
+    }
+
+    @Override
+    public void stopNetworkScan(Message response) {
+        mCi.stopNetworkScan(response);
+    }
+
+    @Override
+    public void getNeighboringCids(Message response, WorkSource workSource) {
+        if (isPhoneTypeGsm()) {
+            mCi.getNeighboringCids(response, workSource);
+        } else {
+            /*
+             * This is currently not implemented.  At least as of June
+             * 2009, there is no neighbor cell information available for
+             * CDMA because some party is resisting making this
+             * information readily available.  Consequently, calling this
+             * function can have no useful effect.  This situation may
+             * (and hopefully will) change in the future.
+             */
+            if (response != null) {
+                CommandException ce = new CommandException(
+                        CommandException.Error.REQUEST_NOT_SUPPORTED);
+                AsyncResult.forMessage(response).exception = ce;
+                response.sendToTarget();
+            }
+        }
+    }
+
+    @Override
+    public void setTTYMode(int ttyMode, Message onComplete) {
+        // Send out the TTY Mode change over RIL as well
+        super.setTTYMode(ttyMode, onComplete);
+        if (mImsPhone != null) {
+            mImsPhone.setTTYMode(ttyMode, onComplete);
+        }
+    }
+
+    @Override
+    public void setUiTTYMode(int uiTtyMode, Message onComplete) {
+       if (mImsPhone != null) {
+           mImsPhone.setUiTTYMode(uiTtyMode, onComplete);
+       }
+    }
+
+    @Override
+    public void setMute(boolean muted) {
+        mCT.setMute(muted);
+    }
+
+    @Override
+    public boolean getMute() {
+        return mCT.getMute();
+    }
+
+    @Override
+    public void getDataCallList(Message response) {
+        mCi.getDataCallList(response);
+    }
+
+    @Override
+    public void updateServiceLocation() {
+        mSST.enableSingleLocationUpdate();
+    }
+
+    @Override
+    public void enableLocationUpdates() {
+        mSST.enableLocationUpdates();
+    }
+
+    @Override
+    public void disableLocationUpdates() {
+        mSST.disableLocationUpdates();
+    }
+
+    @Override
+    public boolean getDataRoamingEnabled() {
+        return mDcTracker.getDataRoamingEnabled();
+    }
+
+    @Override
+    public void setDataRoamingEnabled(boolean enable) {
+        mDcTracker.setDataRoamingEnabledByUser(enable);
+    }
+
+    @Override
+    public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj) {
+        mCi.registerForCdmaOtaProvision(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForCdmaOtaStatusChange(Handler h) {
+        mCi.unregisterForCdmaOtaProvision(h);
+    }
+
+    @Override
+    public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) {
+        mSST.registerForSubscriptionInfoReady(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForSubscriptionInfoReady(Handler h) {
+        mSST.unregisterForSubscriptionInfoReady(h);
+    }
+
+    @Override
+    public void setOnEcbModeExitResponse(Handler h, int what, Object obj) {
+        mEcmExitRespRegistrant = new Registrant(h, what, obj);
+    }
+
+    @Override
+    public void unsetOnEcbModeExitResponse(Handler h) {
+        mEcmExitRespRegistrant.clear();
+    }
+
+    @Override
+    public void registerForCallWaiting(Handler h, int what, Object obj) {
+        mCT.registerForCallWaiting(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForCallWaiting(Handler h) {
+        mCT.unregisterForCallWaiting(h);
+    }
+
+    @Override
+    public boolean getDataEnabled() {
+        return mDcTracker.getDataEnabled();
+    }
+
+    @Override
+    public void setDataEnabled(boolean enable) {
+        mDcTracker.setDataEnabled(enable);
+    }
+
+    /**
+     * Removes the given MMI from the pending list and notifies
+     * registrants that it is complete.
+     * @param mmi MMI that is done
+     */
+    public void onMMIDone(MmiCode mmi) {
+
+        /* Only notify complete if it's on the pending list.
+         * Otherwise, it's already been handled (eg, previously canceled).
+         * The exception is cancellation of an incoming USSD-REQUEST, which is
+         * not on the list.
+         */
+        if (mPendingMMIs.remove(mmi) || (isPhoneTypeGsm() && (mmi.isUssdRequest() ||
+                ((GsmMmiCode)mmi).isSsInfo()))) {
+
+            ResultReceiver receiverCallback = mmi.getUssdCallbackReceiver();
+            if (receiverCallback != null) {
+                Rlog.i(LOG_TAG, "onMMIDone: invoking callback: " + mmi);
+                int returnCode = (mmi.getState() ==  MmiCode.State.COMPLETE) ?
+                    TelephonyManager.USSD_RETURN_SUCCESS : TelephonyManager.USSD_RETURN_FAILURE;
+                sendUssdResponse(mmi.getDialString(), mmi.getMessage(), returnCode,
+                        receiverCallback );
+            } else {
+                Rlog.i(LOG_TAG, "onMMIDone: notifying registrants: " + mmi);
+                mMmiCompleteRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
+            }
+        } else {
+            Rlog.i(LOG_TAG, "onMMIDone: invalid response or already handled; ignoring: " + mmi);
+        }
+    }
+
+    public boolean supports3gppCallForwardingWhileRoaming() {
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle b = configManager.getConfig();
+        if (b != null) {
+            return b.getBoolean(
+                    CarrierConfigManager.KEY_SUPPORT_3GPP_CALL_FORWARDING_WHILE_ROAMING_BOOL, true);
+        } else {
+            // Default value set in CarrierConfigManager
+            return true;
+        }
+    }
+
+    private void onNetworkInitiatedUssd(MmiCode mmi) {
+        Rlog.v(LOG_TAG, "onNetworkInitiatedUssd: mmi=" + mmi);
+        mMmiCompleteRegistrants.notifyRegistrants(
+            new AsyncResult(null, mmi, null));
+    }
+
+    /** ussdMode is one of CommandsInterface.USSD_MODE_* */
+    private void onIncomingUSSD (int ussdMode, String ussdMessage) {
+        if (!isPhoneTypeGsm()) {
+            loge("onIncomingUSSD: not expected on GSM");
+        }
+        boolean isUssdError;
+        boolean isUssdRequest;
+        boolean isUssdRelease;
+
+        isUssdRequest
+            = (ussdMode == CommandsInterface.USSD_MODE_REQUEST);
+
+        isUssdError
+            = (ussdMode != CommandsInterface.USSD_MODE_NOTIFY
+                && ussdMode != CommandsInterface.USSD_MODE_REQUEST);
+
+        isUssdRelease = (ussdMode == CommandsInterface.USSD_MODE_NW_RELEASE);
+
+
+        // See comments in GsmMmiCode.java
+        // USSD requests aren't finished until one
+        // of these two events happen
+        GsmMmiCode found = null;
+        for (int i = 0, s = mPendingMMIs.size() ; i < s; i++) {
+            if(((GsmMmiCode)mPendingMMIs.get(i)).isPendingUSSD()) {
+                found = (GsmMmiCode)mPendingMMIs.get(i);
+                break;
+            }
+        }
+
+        if (found != null) {
+            // Complete pending USSD
+
+            if (isUssdRelease) {
+                found.onUssdRelease();
+            } else if (isUssdError) {
+                found.onUssdFinishedError();
+            } else {
+                found.onUssdFinished(ussdMessage, isUssdRequest);
+            }
+        } else if (!isUssdError && ussdMessage != null) {
+            // pending USSD not found
+            // The network may initiate its own USSD request
+
+            // ignore everything that isnt a Notify or a Request
+            // also, discard if there is no message to present
+            GsmMmiCode mmi;
+            mmi = GsmMmiCode.newNetworkInitiatedUssd(ussdMessage,
+                                                   isUssdRequest,
+                                                   GsmCdmaPhone.this,
+                                                   mUiccApplication.get());
+            onNetworkInitiatedUssd(mmi);
+        }
+    }
+
+    /**
+     * Make sure the network knows our preferred setting.
+     */
+    private void syncClirSetting() {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        int clirSetting = sp.getInt(CLIR_KEY + getPhoneId(), -1);
+        Rlog.i(LOG_TAG, "syncClirSetting: " + CLIR_KEY + getPhoneId() + "=" + clirSetting);
+        if (clirSetting >= 0) {
+            mCi.setCLIR(clirSetting, null);
+        }
+    }
+
+    private void handleRadioAvailable() {
+        mCi.getBasebandVersion(obtainMessage(EVENT_GET_BASEBAND_VERSION_DONE));
+
+        mCi.getDeviceIdentity(obtainMessage(EVENT_GET_DEVICE_IDENTITY_DONE));
+        mCi.getRadioCapability(obtainMessage(EVENT_GET_RADIO_CAPABILITY));
+        startLceAfterRadioIsAvailable();
+    }
+
+    private void handleRadioOn() {
+        /* Proactively query voice radio technologies */
+        mCi.getVoiceRadioTechnology(obtainMessage(EVENT_REQUEST_VOICE_RADIO_TECH_DONE));
+
+        if (!isPhoneTypeGsm()) {
+            mCdmaSubscriptionSource = mCdmaSSM.getCdmaSubscriptionSource();
+        }
+
+        // If this is on APM off, SIM may already be loaded. Send setPreferredNetworkType
+        // request to RIL to preserve user setting across APM toggling
+        setPreferredNetworkTypeIfSimLoaded();
+    }
+
+    private void handleRadioOffOrNotAvailable() {
+        if (isPhoneTypeGsm()) {
+            // Some MMI requests (eg USSD) are not completed
+            // within the course of a CommandsInterface request
+            // If the radio shuts off or resets while one of these
+            // is pending, we need to clean up.
+
+            for (int i = mPendingMMIs.size() - 1; i >= 0; i--) {
+                if (((GsmMmiCode) mPendingMMIs.get(i)).isPendingUSSD()) {
+                    ((GsmMmiCode) mPendingMMIs.get(i)).onUssdFinishedError();
+                }
+            }
+        }
+        mRadioOffOrNotAvailableRegistrants.notifyRegistrants();
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+        Message onComplete;
+
+        switch (msg.what) {
+            case EVENT_RADIO_AVAILABLE: {
+                handleRadioAvailable();
+            }
+            break;
+
+            case EVENT_GET_DEVICE_IDENTITY_DONE:{
+                ar = (AsyncResult)msg.obj;
+
+                if (ar.exception != null) {
+                    break;
+                }
+                String[] respId = (String[])ar.result;
+                mImei = respId[0];
+                mImeiSv = respId[1];
+                mEsn  =  respId[2];
+                mMeid =  respId[3];
+            }
+            break;
+
+            case EVENT_EMERGENCY_CALLBACK_MODE_ENTER:{
+                handleEnterEmergencyCallbackMode(msg);
+            }
+            break;
+
+            case  EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE:{
+                handleExitEmergencyCallbackMode(msg);
+            }
+            break;
+
+            case EVENT_MODEM_RESET: {
+                logd("Event EVENT_MODEM_RESET Received" + " isInEcm = " + isInEcm()
+                        + " isPhoneTypeGsm = " + isPhoneTypeGsm() + " mImsPhone = " + mImsPhone);
+                if (isInEcm()) {
+                    if (isPhoneTypeGsm()) {
+                        if (mImsPhone != null) {
+                            mImsPhone.handleExitEmergencyCallbackMode();
+                        }
+                    } else {
+                        handleExitEmergencyCallbackMode(msg);
+                    }
+                }
+            }
+            break;
+
+            case EVENT_RUIM_RECORDS_LOADED:
+                logd("Event EVENT_RUIM_RECORDS_LOADED Received");
+                updateCurrentCarrierInProvider();
+                break;
+
+            case EVENT_RADIO_ON:
+                logd("Event EVENT_RADIO_ON Received");
+                handleRadioOn();
+                break;
+
+            case EVENT_RIL_CONNECTED:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null && ar.result != null) {
+                    mRilVersion = (Integer) ar.result;
+                } else {
+                    logd("Unexpected exception on EVENT_RIL_CONNECTED");
+                    mRilVersion = -1;
+                }
+                break;
+
+            case EVENT_VOICE_RADIO_TECH_CHANGED:
+            case EVENT_REQUEST_VOICE_RADIO_TECH_DONE:
+                String what = (msg.what == EVENT_VOICE_RADIO_TECH_CHANGED) ?
+                        "EVENT_VOICE_RADIO_TECH_CHANGED" : "EVENT_REQUEST_VOICE_RADIO_TECH_DONE";
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null) {
+                    if ((ar.result != null) && (((int[]) ar.result).length != 0)) {
+                        int newVoiceTech = ((int[]) ar.result)[0];
+                        logd(what + ": newVoiceTech=" + newVoiceTech);
+                        phoneObjectUpdater(newVoiceTech);
+                    } else {
+                        loge(what + ": has no tech!");
+                    }
+                } else {
+                    loge(what + ": exception=" + ar.exception);
+                }
+                break;
+
+            case EVENT_UPDATE_PHONE_OBJECT:
+                phoneObjectUpdater(msg.arg1);
+                break;
+
+            case EVENT_CARRIER_CONFIG_CHANGED:
+                // Only check for the voice radio tech if it not going to be updated by the voice
+                // registration changes.
+                if (!mContext.getResources().getBoolean(com.android.internal.R.bool.
+                        config_switch_phone_on_voice_reg_state_change)) {
+                    mCi.getVoiceRadioTechnology(obtainMessage(EVENT_REQUEST_VOICE_RADIO_TECH_DONE));
+                }
+                // Force update IMS service
+                ImsManager.updateImsServiceConfig(mContext, mPhoneId, true);
+
+                // Update broadcastEmergencyCallStateChanges
+                CarrierConfigManager configMgr = (CarrierConfigManager)
+                        getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+                PersistableBundle b = configMgr.getConfigForSubId(getSubId());
+                if (b != null) {
+                    boolean broadcastEmergencyCallStateChanges = b.getBoolean(
+                            CarrierConfigManager.KEY_BROADCAST_EMERGENCY_CALL_STATE_CHANGES_BOOL);
+                    logd("broadcastEmergencyCallStateChanges = " +
+                            broadcastEmergencyCallStateChanges);
+                    setBroadcastEmergencyCallStateChanges(broadcastEmergencyCallStateChanges);
+                } else {
+                    loge("didn't get broadcastEmergencyCallStateChanges from carrier config");
+                }
+
+                // Changing the cdma roaming settings based carrier config.
+                if (b != null) {
+                    int config_cdma_roaming_mode = b.getInt(
+                            CarrierConfigManager.KEY_CDMA_ROAMING_MODE_INT);
+                    int current_cdma_roaming_mode =
+                            Settings.Global.getInt(getContext().getContentResolver(),
+                            Settings.Global.CDMA_ROAMING_MODE,
+                            CarrierConfigManager.CDMA_ROAMING_MODE_RADIO_DEFAULT);
+                    switch (config_cdma_roaming_mode) {
+                        // Carrier's cdma_roaming_mode will overwrite the user's previous settings
+                        // Keep the user's previous setting in global variable which will be used
+                        // when carrier's setting is turn off.
+                        case CarrierConfigManager.CDMA_ROAMING_MODE_HOME:
+                        case CarrierConfigManager.CDMA_ROAMING_MODE_AFFILIATED:
+                        case CarrierConfigManager.CDMA_ROAMING_MODE_ANY:
+                            logd("cdma_roaming_mode is going to changed to "
+                                    + config_cdma_roaming_mode);
+                            setCdmaRoamingPreference(config_cdma_roaming_mode,
+                                    obtainMessage(EVENT_SET_ROAMING_PREFERENCE_DONE));
+                            break;
+
+                        // When carrier's setting is turn off, change the cdma_roaming_mode to the
+                        // previous user's setting
+                        case CarrierConfigManager.CDMA_ROAMING_MODE_RADIO_DEFAULT:
+                            if (current_cdma_roaming_mode != config_cdma_roaming_mode) {
+                                logd("cdma_roaming_mode is going to changed to "
+                                        + current_cdma_roaming_mode);
+                                setCdmaRoamingPreference(current_cdma_roaming_mode,
+                                        obtainMessage(EVENT_SET_ROAMING_PREFERENCE_DONE));
+                            }
+
+                        default:
+                            loge("Invalid cdma_roaming_mode settings: "
+                                    + config_cdma_roaming_mode);
+                    }
+                } else {
+                    loge("didn't get the cdma_roaming_mode changes from the carrier config.");
+                }
+
+                // Load the ERI based on carrier config. Carrier might have their specific ERI.
+                prepareEri();
+                if (!isPhoneTypeGsm()) {
+                    mSST.pollState();
+                }
+
+                break;
+
+            case EVENT_SET_ROAMING_PREFERENCE_DONE:
+                logd("cdma_roaming_mode change is done");
+                break;
+
+            case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED:
+                logd("EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED");
+                mCdmaSubscriptionSource = mCdmaSSM.getCdmaSubscriptionSource();
+                break;
+
+            case EVENT_REGISTERED_TO_NETWORK:
+                logd("Event EVENT_REGISTERED_TO_NETWORK Received");
+                if (isPhoneTypeGsm()) {
+                    syncClirSetting();
+                }
+                break;
+
+            case EVENT_SIM_RECORDS_LOADED:
+                updateCurrentCarrierInProvider();
+
+                // Check if this is a different SIM than the previous one. If so unset the
+                // voice mail number.
+                String imsi = getVmSimImsi();
+                String imsiFromSIM = getSubscriberId();
+                if ((!isPhoneTypeGsm() || imsi != null) && imsiFromSIM != null
+                        && !imsiFromSIM.equals(imsi)) {
+                    storeVoiceMailNumber(null);
+                    setVmSimImsi(null);
+                }
+
+                mSimRecordsLoadedRegistrants.notifyRegistrants();
+                break;
+
+            case EVENT_GET_BASEBAND_VERSION_DONE:
+                ar = (AsyncResult)msg.obj;
+
+                if (ar.exception != null) {
+                    break;
+                }
+
+                if (DBG) logd("Baseband version: " + ar.result);
+                TelephonyManager.from(mContext).setBasebandVersionForPhone(getPhoneId(),
+                        (String)ar.result);
+            break;
+
+            case EVENT_GET_IMEI_DONE:
+                ar = (AsyncResult)msg.obj;
+
+                if (ar.exception != null) {
+                    break;
+                }
+
+                mImei = (String)ar.result;
+            break;
+
+            case EVENT_GET_IMEISV_DONE:
+                ar = (AsyncResult)msg.obj;
+
+                if (ar.exception != null) {
+                    break;
+                }
+
+                mImeiSv = (String)ar.result;
+            break;
+
+            case EVENT_USSD:
+                ar = (AsyncResult)msg.obj;
+
+                String[] ussdResult = (String[]) ar.result;
+
+                if (ussdResult.length > 1) {
+                    try {
+                        onIncomingUSSD(Integer.parseInt(ussdResult[0]), ussdResult[1]);
+                    } catch (NumberFormatException e) {
+                        Rlog.w(LOG_TAG, "error parsing USSD");
+                    }
+                }
+            break;
+
+            case EVENT_RADIO_OFF_OR_NOT_AVAILABLE: {
+                logd("Event EVENT_RADIO_OFF_OR_NOT_AVAILABLE Received");
+                handleRadioOffOrNotAvailable();
+                break;
+            }
+
+            case EVENT_SSN:
+                logd("Event EVENT_SSN Received");
+                if (isPhoneTypeGsm()) {
+                    ar = (AsyncResult) msg.obj;
+                    SuppServiceNotification not = (SuppServiceNotification) ar.result;
+                    mSsnRegistrants.notifyRegistrants(ar);
+                }
+                break;
+
+            case EVENT_SET_CALL_FORWARD_DONE:
+                ar = (AsyncResult)msg.obj;
+                IccRecords r = mIccRecords.get();
+                Cfu cfu = (Cfu) ar.userObj;
+                if (ar.exception == null && r != null) {
+                    setVoiceCallForwardingFlag(1, msg.arg1 == 1, cfu.mSetCfNumber);
+                }
+                if (cfu.mOnComplete != null) {
+                    AsyncResult.forMessage(cfu.mOnComplete, ar.result, ar.exception);
+                    cfu.mOnComplete.sendToTarget();
+                }
+                break;
+
+            case EVENT_SET_VM_NUMBER_DONE:
+                ar = (AsyncResult)msg.obj;
+                if ((isPhoneTypeGsm() && IccVmNotSupportedException.class.isInstance(ar.exception)) ||
+                        (!isPhoneTypeGsm() && IccException.class.isInstance(ar.exception))){
+                    storeVoiceMailNumber(mVmNumber);
+                    ar.exception = null;
+                }
+                onComplete = (Message) ar.userObj;
+                if (onComplete != null) {
+                    AsyncResult.forMessage(onComplete, ar.result, ar.exception);
+                    onComplete.sendToTarget();
+                }
+                break;
+
+
+            case EVENT_GET_CALL_FORWARD_DONE:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    handleCfuQueryResult((CallForwardInfo[])ar.result);
+                }
+                onComplete = (Message) ar.userObj;
+                if (onComplete != null) {
+                    AsyncResult.forMessage(onComplete, ar.result, ar.exception);
+                    onComplete.sendToTarget();
+                }
+                break;
+
+            case EVENT_SET_NETWORK_AUTOMATIC:
+                // Automatic network selection from EF_CSP SIM record
+                ar = (AsyncResult) msg.obj;
+                if (mSST.mSS.getIsManualSelection()) {
+                    setNetworkSelectionModeAutomatic((Message) ar.result);
+                    logd("SET_NETWORK_SELECTION_AUTOMATIC: set to automatic");
+                } else {
+                    // prevent duplicate request which will push current PLMN to low priority
+                    logd("SET_NETWORK_SELECTION_AUTOMATIC: already automatic, ignore");
+                }
+                break;
+
+            case EVENT_ICC_RECORD_EVENTS:
+                ar = (AsyncResult)msg.obj;
+                processIccRecordEvents((Integer)ar.result);
+                break;
+
+            case EVENT_SET_CLIR_COMPLETE:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    saveClirSetting(msg.arg1);
+                }
+                onComplete = (Message) ar.userObj;
+                if (onComplete != null) {
+                    AsyncResult.forMessage(onComplete, ar.result, ar.exception);
+                    onComplete.sendToTarget();
+                }
+                break;
+
+            case EVENT_SS:
+                ar = (AsyncResult)msg.obj;
+                logd("Event EVENT_SS received");
+                if (isPhoneTypeGsm()) {
+                    // SS data is already being handled through MMI codes.
+                    // So, this result if processed as MMI response would help
+                    // in re-using the existing functionality.
+                    GsmMmiCode mmi = new GsmMmiCode(this, mUiccApplication.get());
+                    mmi.processSsData(ar);
+                }
+                break;
+
+            case EVENT_GET_RADIO_CAPABILITY:
+                ar = (AsyncResult) msg.obj;
+                RadioCapability rc = (RadioCapability) ar.result;
+                if (ar.exception != null) {
+                    Rlog.d(LOG_TAG, "get phone radio capability fail, no need to change " +
+                            "mRadioCapability");
+                } else {
+                    radioCapabilityUpdated(rc);
+                }
+                Rlog.d(LOG_TAG, "EVENT_GET_RADIO_CAPABILITY: phone rc: " + rc);
+                break;
+
+            default:
+                super.handleMessage(msg);
+        }
+    }
+
+    public UiccCardApplication getUiccCardApplication() {
+        if (isPhoneTypeGsm()) {
+            return mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_3GPP);
+        } else {
+            return mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_3GPP2);
+        }
+    }
+
+    @Override
+    protected void onUpdateIccAvailability() {
+        if (mUiccController == null ) {
+            return;
+        }
+
+        UiccCardApplication newUiccApplication = null;
+
+        // Update mIsimUiccRecords
+        if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
+            newUiccApplication =
+                    mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_IMS);
+            IsimUiccRecords newIsimUiccRecords = null;
+
+            if (newUiccApplication != null) {
+                newIsimUiccRecords = (IsimUiccRecords) newUiccApplication.getIccRecords();
+                if (DBG) logd("New ISIM application found");
+            }
+            mIsimUiccRecords = newIsimUiccRecords;
+        }
+
+        // Update mSimRecords
+        if (mSimRecords != null) {
+            mSimRecords.unregisterForRecordsLoaded(this);
+        }
+        if (isPhoneTypeCdmaLte()) {
+            newUiccApplication = mUiccController.getUiccCardApplication(mPhoneId,
+                    UiccController.APP_FAM_3GPP);
+            SIMRecords newSimRecords = null;
+            if (newUiccApplication != null) {
+                newSimRecords = (SIMRecords) newUiccApplication.getIccRecords();
+            }
+            mSimRecords = newSimRecords;
+            if (mSimRecords != null) {
+                mSimRecords.registerForRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
+            }
+        } else {
+            mSimRecords = null;
+        }
+
+        // Update mIccRecords, mUiccApplication, mIccPhoneBookIntManager
+        newUiccApplication = getUiccCardApplication();
+        if (!isPhoneTypeGsm() && newUiccApplication == null) {
+            logd("can't find 3GPP2 application; trying APP_FAM_3GPP");
+            newUiccApplication = mUiccController.getUiccCardApplication(mPhoneId,
+                    UiccController.APP_FAM_3GPP);
+        }
+
+        UiccCardApplication app = mUiccApplication.get();
+        if (app != newUiccApplication) {
+            if (app != null) {
+                if (DBG) logd("Removing stale icc objects.");
+                if (mIccRecords.get() != null) {
+                    unregisterForIccRecordEvents();
+                    mIccPhoneBookIntManager.updateIccRecords(null);
+                }
+                mIccRecords.set(null);
+                mUiccApplication.set(null);
+            }
+            if (newUiccApplication != null) {
+                if (DBG) {
+                    logd("New Uicc application found. type = " + newUiccApplication.getType());
+                }
+                mUiccApplication.set(newUiccApplication);
+                mIccRecords.set(newUiccApplication.getIccRecords());
+                registerForIccRecordEvents();
+                mIccPhoneBookIntManager.updateIccRecords(mIccRecords.get());
+            }
+        }
+    }
+
+    private void processIccRecordEvents(int eventCode) {
+        switch (eventCode) {
+            case IccRecords.EVENT_CFI:
+                notifyCallForwardingIndicator();
+                break;
+        }
+    }
+
+    /**
+     * Sets the "current" field in the telephony provider according to the SIM's operator
+     *
+     * @return true for success; false otherwise.
+     */
+    @Override
+    public boolean updateCurrentCarrierInProvider() {
+        if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
+            long currentDds = SubscriptionManager.getDefaultDataSubscriptionId();
+            String operatorNumeric = getOperatorNumeric();
+
+            logd("updateCurrentCarrierInProvider: mSubId = " + getSubId()
+                    + " currentDds = " + currentDds + " operatorNumeric = " + operatorNumeric);
+
+            if (!TextUtils.isEmpty(operatorNumeric) && (getSubId() == currentDds)) {
+                try {
+                    Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current");
+                    ContentValues map = new ContentValues();
+                    map.put(Telephony.Carriers.NUMERIC, operatorNumeric);
+                    mContext.getContentResolver().insert(uri, map);
+                    return true;
+                } catch (SQLException e) {
+                    Rlog.e(LOG_TAG, "Can't store current operator", e);
+                }
+            }
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    //CDMA
+    /**
+     * Sets the "current" field in the telephony provider according to the
+     * build-time operator numeric property
+     *
+     * @return true for success; false otherwise.
+     */
+    private boolean updateCurrentCarrierInProvider(String operatorNumeric) {
+        if (isPhoneTypeCdma()
+                || (isPhoneTypeCdmaLte() && mUiccController.getUiccCardApplication(mPhoneId,
+                        UiccController.APP_FAM_3GPP) == null)) {
+            logd("CDMAPhone: updateCurrentCarrierInProvider called");
+            if (!TextUtils.isEmpty(operatorNumeric)) {
+                try {
+                    Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current");
+                    ContentValues map = new ContentValues();
+                    map.put(Telephony.Carriers.NUMERIC, operatorNumeric);
+                    logd("updateCurrentCarrierInProvider from system: numeric=" + operatorNumeric);
+                    getContext().getContentResolver().insert(uri, map);
+
+                    // Updates MCC MNC device configuration information
+                    logd("update mccmnc=" + operatorNumeric);
+                    MccTable.updateMccMncConfiguration(mContext, operatorNumeric, false);
+
+                    return true;
+                } catch (SQLException e) {
+                    Rlog.e(LOG_TAG, "Can't store current operator", e);
+                }
+            }
+            return false;
+        } else { // isPhoneTypeCdmaLte()
+            if (DBG) logd("updateCurrentCarrierInProvider not updated X retVal=" + true);
+            return true;
+        }
+    }
+
+    private void handleCfuQueryResult(CallForwardInfo[] infos) {
+        IccRecords r = mIccRecords.get();
+        if (r != null) {
+            if (infos == null || infos.length == 0) {
+                // Assume the default is not active
+                // Set unconditional CFF in SIM to false
+                setVoiceCallForwardingFlag(1, false, null);
+            } else {
+                for (int i = 0, s = infos.length; i < s; i++) {
+                    if ((infos[i].serviceClass & SERVICE_CLASS_VOICE) != 0) {
+                        setVoiceCallForwardingFlag(1, (infos[i].status == 1),
+                            infos[i].number);
+                        // should only have the one
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Retrieves the IccPhoneBookInterfaceManager of the GsmCdmaPhone
+     */
+    @Override
+    public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(){
+        return mIccPhoneBookIntManager;
+    }
+
+    //CDMA
+    public void registerForEriFileLoaded(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mEriFileLoadedRegistrants.add(r);
+    }
+
+    //CDMA
+    public void unregisterForEriFileLoaded(Handler h) {
+        mEriFileLoadedRegistrants.remove(h);
+    }
+
+    //CDMA
+    public void prepareEri() {
+        if (mEriManager == null) {
+            Rlog.e(LOG_TAG, "PrepareEri: Trying to access stale objects");
+            return;
+        }
+        mEriManager.loadEriFile();
+        if(mEriManager.isEriFileLoaded()) {
+            // when the ERI file is loaded
+            logd("ERI read, notify registrants");
+            mEriFileLoadedRegistrants.notifyRegistrants();
+        }
+    }
+
+    //CDMA
+    public boolean isEriFileLoaded() {
+        return mEriManager.isEriFileLoaded();
+    }
+
+
+    /**
+     * Activate or deactivate cell broadcast SMS.
+     *
+     * @param activate 0 = activate, 1 = deactivate
+     * @param response Callback message is empty on completion
+     */
+    @Override
+    public void activateCellBroadcastSms(int activate, Message response) {
+        loge("[GsmCdmaPhone] activateCellBroadcastSms() is obsolete; use SmsManager");
+        response.sendToTarget();
+    }
+
+    /**
+     * Query the current configuration of cdma cell broadcast SMS.
+     *
+     * @param response Callback message is empty on completion
+     */
+    @Override
+    public void getCellBroadcastSmsConfig(Message response) {
+        loge("[GsmCdmaPhone] getCellBroadcastSmsConfig() is obsolete; use SmsManager");
+        response.sendToTarget();
+    }
+
+    /**
+     * Configure cdma cell broadcast SMS.
+     *
+     * @param response Callback message is empty on completion
+     */
+    @Override
+    public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response) {
+        loge("[GsmCdmaPhone] setCellBroadcastSmsConfig() is obsolete; use SmsManager");
+        response.sendToTarget();
+    }
+
+    /**
+     * Returns true if OTA Service Provisioning needs to be performed.
+     */
+    @Override
+    public boolean needsOtaServiceProvisioning() {
+        if (isPhoneTypeGsm()) {
+            return false;
+        } else {
+            return mSST.getOtasp() != TelephonyManager.OTASP_NOT_NEEDED;
+        }
+    }
+
+    @Override
+    public boolean isCspPlmnEnabled() {
+        IccRecords r = mIccRecords.get();
+        return (r != null) ? r.isCspPlmnEnabled() : false;
+    }
+
+    /**
+     * Whether manual select is now allowed and we should set
+     * to auto network select mode.
+     */
+    public boolean shouldForceAutoNetworkSelect() {
+
+        int nwMode = Phone.PREFERRED_NT_MODE;
+        int subId = getSubId();
+
+        // If it's invalid subId, we shouldn't force to auto network select mode.
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            return false;
+        }
+
+        nwMode = android.provider.Settings.Global.getInt(mContext.getContentResolver(),
+                    android.provider.Settings.Global.PREFERRED_NETWORK_MODE + subId, nwMode);
+
+        logd("shouldForceAutoNetworkSelect in mode = " + nwMode);
+        /*
+         *  For multimode targets in global mode manual network
+         *  selection is disallowed. So we should force auto select mode.
+         */
+        if (isManualSelProhibitedInGlobalMode()
+                && ((nwMode == Phone.NT_MODE_LTE_CDMA_EVDO_GSM_WCDMA)
+                        || (nwMode == Phone.NT_MODE_GLOBAL)) ){
+            logd("Should force auto network select mode = " + nwMode);
+            return true;
+        } else {
+            logd("Should not force auto network select mode = " + nwMode);
+        }
+
+        /*
+         *  Single mode phone with - GSM network modes/global mode
+         *  LTE only for 3GPP
+         *  LTE centric + 3GPP Legacy
+         *  Note: the actual enabling/disabling manual selection for these
+         *  cases will be controlled by csp
+         */
+        return false;
+    }
+
+    private boolean isManualSelProhibitedInGlobalMode() {
+        boolean isProhibited = false;
+        final String configString = getContext().getResources().getString(com.android.internal.
+                R.string.prohibit_manual_network_selection_in_gobal_mode);
+
+        if (!TextUtils.isEmpty(configString)) {
+            String[] configArray = configString.split(";");
+
+            if (configArray != null &&
+                    ((configArray.length == 1 && configArray[0].equalsIgnoreCase("true")) ||
+                        (configArray.length == 2 && !TextUtils.isEmpty(configArray[1]) &&
+                            configArray[0].equalsIgnoreCase("true") &&
+                            isMatchGid(configArray[1])))) {
+                            isProhibited = true;
+            }
+        }
+        logd("isManualNetSelAllowedInGlobal in current carrier is " + isProhibited);
+        return isProhibited;
+    }
+
+    private void registerForIccRecordEvents() {
+        IccRecords r = mIccRecords.get();
+        if (r == null) {
+            return;
+        }
+        if (isPhoneTypeGsm()) {
+            r.registerForNetworkSelectionModeAutomatic(
+                    this, EVENT_SET_NETWORK_AUTOMATIC, null);
+            r.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null);
+            r.registerForRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
+        } else {
+            r.registerForRecordsLoaded(this, EVENT_RUIM_RECORDS_LOADED, null);
+            if (isPhoneTypeCdmaLte()) {
+                // notify simRecordsLoaded registrants for cdmaLte phone
+                r.registerForRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
+            }
+        }
+    }
+
+    private void unregisterForIccRecordEvents() {
+        IccRecords r = mIccRecords.get();
+        if (r == null) {
+            return;
+        }
+        r.unregisterForNetworkSelectionModeAutomatic(this);
+        r.unregisterForRecordsEvents(this);
+        r.unregisterForRecordsLoaded(this);
+    }
+
+    @Override
+    public void exitEmergencyCallbackMode() {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "exitEmergencyCallbackMode: mImsPhone=" + mImsPhone
+                    + " isPhoneTypeGsm=" + isPhoneTypeGsm());
+        }
+        if (isPhoneTypeGsm()) {
+            if (mImsPhone != null) {
+                mImsPhone.exitEmergencyCallbackMode();
+            }
+        } else {
+            if (mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+            // Send a message which will invoke handleExitEmergencyCallbackMode
+            mCi.exitEmergencyCallbackMode(obtainMessage(EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE));
+        }
+    }
+
+    //CDMA
+    private void handleEnterEmergencyCallbackMode(Message msg) {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleEnterEmergencyCallbackMode, isInEcm()="
+                    + isInEcm());
+        }
+        // if phone is not in Ecm mode, and it's changed to Ecm mode
+        if (!isInEcm()) {
+            setIsInEcm(true);
+
+            // notify change
+            sendEmergencyCallbackModeChange();
+
+            // Post this runnable so we will automatically exit
+            // if no one invokes exitEmergencyCallbackMode() directly.
+            long delayInMillis = SystemProperties.getLong(
+                    TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE);
+            postDelayed(mExitEcmRunnable, delayInMillis);
+            // We don't want to go to sleep while in Ecm
+            mWakeLock.acquire();
+        }
+    }
+
+    //CDMA
+    private void handleExitEmergencyCallbackMode(Message msg) {
+        AsyncResult ar = (AsyncResult)msg.obj;
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleExitEmergencyCallbackMode,ar.exception , isInEcm="
+                    + ar.exception + isInEcm());
+        }
+        // Remove pending exit Ecm runnable, if any
+        removeCallbacks(mExitEcmRunnable);
+
+        if (mEcmExitRespRegistrant != null) {
+            mEcmExitRespRegistrant.notifyRegistrant(ar);
+        }
+        // if exiting ecm success
+        if (ar.exception == null) {
+            if (isInEcm()) {
+                setIsInEcm(false);
+            }
+
+            // release wakeLock
+            if (mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+
+            // send an Intent
+            sendEmergencyCallbackModeChange();
+            // Re-initiate data connection
+            mDcTracker.setInternalDataEnabled(true);
+            notifyEmergencyCallRegistrants(false);
+        }
+    }
+
+    //CDMA
+    public void notifyEmergencyCallRegistrants(boolean started) {
+        mEmergencyCallToggledRegistrants.notifyResult(started ? 1 : 0);
+    }
+
+    //CDMA
+    /**
+     * Handle to cancel or restart Ecm timer in emergency call back mode
+     * if action is CANCEL_ECM_TIMER, cancel Ecm timer and notify apps the timer is canceled;
+     * otherwise, restart Ecm timer and notify apps the timer is restarted.
+     */
+    public void handleTimerInEmergencyCallbackMode(int action) {
+        switch(action) {
+            case CANCEL_ECM_TIMER:
+                removeCallbacks(mExitEcmRunnable);
+                mEcmTimerResetRegistrants.notifyResult(Boolean.TRUE);
+                break;
+            case RESTART_ECM_TIMER:
+                long delayInMillis = SystemProperties.getLong(
+                        TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE);
+                postDelayed(mExitEcmRunnable, delayInMillis);
+                mEcmTimerResetRegistrants.notifyResult(Boolean.FALSE);
+                break;
+            default:
+                Rlog.e(LOG_TAG, "handleTimerInEmergencyCallbackMode, unsupported action " + action);
+        }
+    }
+
+    //CDMA
+    private static final String IS683A_FEATURE_CODE = "*228";
+    private static final int IS683A_FEATURE_CODE_NUM_DIGITS = 4;
+    private static final int IS683A_SYS_SEL_CODE_NUM_DIGITS = 2;
+    private static final int IS683A_SYS_SEL_CODE_OFFSET = 4;
+
+    private static final int IS683_CONST_800MHZ_A_BAND = 0;
+    private static final int IS683_CONST_800MHZ_B_BAND = 1;
+    private static final int IS683_CONST_1900MHZ_A_BLOCK = 2;
+    private static final int IS683_CONST_1900MHZ_B_BLOCK = 3;
+    private static final int IS683_CONST_1900MHZ_C_BLOCK = 4;
+    private static final int IS683_CONST_1900MHZ_D_BLOCK = 5;
+    private static final int IS683_CONST_1900MHZ_E_BLOCK = 6;
+    private static final int IS683_CONST_1900MHZ_F_BLOCK = 7;
+    private static final int INVALID_SYSTEM_SELECTION_CODE = -1;
+
+    // Define the pattern/format for carrier specified OTASP number schema.
+    // It separates by comma and/or whitespace.
+    private static Pattern pOtaSpNumSchema = Pattern.compile("[,\\s]+");
+
+    //CDMA
+    private static boolean isIs683OtaSpDialStr(String dialStr) {
+        int sysSelCodeInt;
+        boolean isOtaspDialString = false;
+        int dialStrLen = dialStr.length();
+
+        if (dialStrLen == IS683A_FEATURE_CODE_NUM_DIGITS) {
+            if (dialStr.equals(IS683A_FEATURE_CODE)) {
+                isOtaspDialString = true;
+            }
+        } else {
+            sysSelCodeInt = extractSelCodeFromOtaSpNum(dialStr);
+            switch (sysSelCodeInt) {
+                case IS683_CONST_800MHZ_A_BAND:
+                case IS683_CONST_800MHZ_B_BAND:
+                case IS683_CONST_1900MHZ_A_BLOCK:
+                case IS683_CONST_1900MHZ_B_BLOCK:
+                case IS683_CONST_1900MHZ_C_BLOCK:
+                case IS683_CONST_1900MHZ_D_BLOCK:
+                case IS683_CONST_1900MHZ_E_BLOCK:
+                case IS683_CONST_1900MHZ_F_BLOCK:
+                    isOtaspDialString = true;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return isOtaspDialString;
+    }
+
+    //CDMA
+    /**
+     * This function extracts the system selection code from the dial string.
+     */
+    private static int extractSelCodeFromOtaSpNum(String dialStr) {
+        int dialStrLen = dialStr.length();
+        int sysSelCodeInt = INVALID_SYSTEM_SELECTION_CODE;
+
+        if ((dialStr.regionMatches(0, IS683A_FEATURE_CODE,
+                0, IS683A_FEATURE_CODE_NUM_DIGITS)) &&
+                (dialStrLen >= (IS683A_FEATURE_CODE_NUM_DIGITS +
+                        IS683A_SYS_SEL_CODE_NUM_DIGITS))) {
+            // Since we checked the condition above, the system selection code
+            // extracted from dialStr will not cause any exception
+            sysSelCodeInt = Integer.parseInt (
+                    dialStr.substring (IS683A_FEATURE_CODE_NUM_DIGITS,
+                            IS683A_FEATURE_CODE_NUM_DIGITS + IS683A_SYS_SEL_CODE_NUM_DIGITS));
+        }
+        if (DBG) Rlog.d(LOG_TAG, "extractSelCodeFromOtaSpNum " + sysSelCodeInt);
+        return sysSelCodeInt;
+    }
+
+    //CDMA
+    /**
+     * This function checks if the system selection code extracted from
+     * the dial string "sysSelCodeInt' is the system selection code specified
+     * in the carrier ota sp number schema "sch".
+     */
+    private static boolean checkOtaSpNumBasedOnSysSelCode(int sysSelCodeInt, String sch[]) {
+        boolean isOtaSpNum = false;
+        try {
+            // Get how many number of system selection code ranges
+            int selRc = Integer.parseInt(sch[1]);
+            for (int i = 0; i < selRc; i++) {
+                if (!TextUtils.isEmpty(sch[i+2]) && !TextUtils.isEmpty(sch[i+3])) {
+                    int selMin = Integer.parseInt(sch[i+2]);
+                    int selMax = Integer.parseInt(sch[i+3]);
+                    // Check if the selection code extracted from the dial string falls
+                    // within any of the range pairs specified in the schema.
+                    if ((sysSelCodeInt >= selMin) && (sysSelCodeInt <= selMax)) {
+                        isOtaSpNum = true;
+                        break;
+                    }
+                }
+            }
+        } catch (NumberFormatException ex) {
+            // If the carrier ota sp number schema is not correct, we still allow dial
+            // and only log the error:
+            Rlog.e(LOG_TAG, "checkOtaSpNumBasedOnSysSelCode, error", ex);
+        }
+        return isOtaSpNum;
+    }
+
+    //CDMA
+    /**
+     * The following function checks if a dial string is a carrier specified
+     * OTASP number or not by checking against the OTASP number schema stored
+     * in PROPERTY_OTASP_NUM_SCHEMA.
+     *
+     * Currently, there are 2 schemas for carriers to specify the OTASP number:
+     * 1) Use system selection code:
+     *    The schema is:
+     *    SELC,the # of code pairs,min1,max1,min2,max2,...
+     *    e.g "SELC,3,10,20,30,40,60,70" indicates that there are 3 pairs of
+     *    selection codes, and they are {10,20}, {30,40} and {60,70} respectively.
+     *
+     * 2) Use feature code:
+     *    The schema is:
+     *    "FC,length of feature code,feature code".
+     *     e.g "FC,2,*2" indicates that the length of the feature code is 2,
+     *     and the code itself is "*2".
+     */
+    private boolean isCarrierOtaSpNum(String dialStr) {
+        boolean isOtaSpNum = false;
+        int sysSelCodeInt = extractSelCodeFromOtaSpNum(dialStr);
+        if (sysSelCodeInt == INVALID_SYSTEM_SELECTION_CODE) {
+            return isOtaSpNum;
+        }
+        // mCarrierOtaSpNumSchema is retrieved from PROPERTY_OTASP_NUM_SCHEMA:
+        if (!TextUtils.isEmpty(mCarrierOtaSpNumSchema)) {
+            Matcher m = pOtaSpNumSchema.matcher(mCarrierOtaSpNumSchema);
+            if (DBG) {
+                Rlog.d(LOG_TAG, "isCarrierOtaSpNum,schema" + mCarrierOtaSpNumSchema);
+            }
+
+            if (m.find()) {
+                String sch[] = pOtaSpNumSchema.split(mCarrierOtaSpNumSchema);
+                // If carrier uses system selection code mechanism
+                if (!TextUtils.isEmpty(sch[0]) && sch[0].equals("SELC")) {
+                    if (sysSelCodeInt!=INVALID_SYSTEM_SELECTION_CODE) {
+                        isOtaSpNum=checkOtaSpNumBasedOnSysSelCode(sysSelCodeInt,sch);
+                    } else {
+                        if (DBG) {
+                            Rlog.d(LOG_TAG, "isCarrierOtaSpNum,sysSelCodeInt is invalid");
+                        }
+                    }
+                } else if (!TextUtils.isEmpty(sch[0]) && sch[0].equals("FC")) {
+                    int fcLen =  Integer.parseInt(sch[1]);
+                    String fc = sch[2];
+                    if (dialStr.regionMatches(0,fc,0,fcLen)) {
+                        isOtaSpNum = true;
+                    } else {
+                        if (DBG) Rlog.d(LOG_TAG, "isCarrierOtaSpNum,not otasp number");
+                    }
+                } else {
+                    if (DBG) {
+                        Rlog.d(LOG_TAG, "isCarrierOtaSpNum,ota schema not supported" + sch[0]);
+                    }
+                }
+            } else {
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "isCarrierOtaSpNum,ota schema pattern not right" +
+                            mCarrierOtaSpNumSchema);
+                }
+            }
+        } else {
+            if (DBG) Rlog.d(LOG_TAG, "isCarrierOtaSpNum,ota schema pattern empty");
+        }
+        return isOtaSpNum;
+    }
+
+    /**
+     * isOTASPNumber: checks a given number against the IS-683A OTASP dial string and carrier
+     * OTASP dial string.
+     *
+     * @param dialStr the number to look up.
+     * @return true if the number is in IS-683A OTASP dial string or carrier OTASP dial string
+     */
+    @Override
+    public  boolean isOtaSpNumber(String dialStr) {
+        if (isPhoneTypeGsm()) {
+            return super.isOtaSpNumber(dialStr);
+        } else {
+            boolean isOtaSpNum = false;
+            String dialableStr = PhoneNumberUtils.extractNetworkPortionAlt(dialStr);
+            if (dialableStr != null) {
+                isOtaSpNum = isIs683OtaSpDialStr(dialableStr);
+                if (isOtaSpNum == false) {
+                    isOtaSpNum = isCarrierOtaSpNum(dialableStr);
+                }
+            }
+            if (DBG) Rlog.d(LOG_TAG, "isOtaSpNumber " + isOtaSpNum);
+            return isOtaSpNum;
+        }
+    }
+
+    @Override
+    public int getCdmaEriIconIndex() {
+        if (isPhoneTypeGsm()) {
+            return super.getCdmaEriIconIndex();
+        } else {
+            return getServiceState().getCdmaEriIconIndex();
+        }
+    }
+
+    /**
+     * Returns the CDMA ERI icon mode,
+     * 0 - ON
+     * 1 - FLASHING
+     */
+    @Override
+    public int getCdmaEriIconMode() {
+        if (isPhoneTypeGsm()) {
+            return super.getCdmaEriIconMode();
+        } else {
+            return getServiceState().getCdmaEriIconMode();
+        }
+    }
+
+    /**
+     * Returns the CDMA ERI text,
+     */
+    @Override
+    public String getCdmaEriText() {
+        if (isPhoneTypeGsm()) {
+            return super.getCdmaEriText();
+        } else {
+            int roamInd = getServiceState().getCdmaRoamingIndicator();
+            int defRoamInd = getServiceState().getCdmaDefaultRoamingIndicator();
+            return mEriManager.getCdmaEriText(roamInd, defRoamInd);
+        }
+    }
+
+    private void phoneObjectUpdater(int newVoiceRadioTech) {
+        logd("phoneObjectUpdater: newVoiceRadioTech=" + newVoiceRadioTech);
+
+        // Check for a voice over lte replacement
+        if (ServiceState.isLte(newVoiceRadioTech)
+                || (newVoiceRadioTech == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN)) {
+            CarrierConfigManager configMgr = (CarrierConfigManager)
+                    getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            PersistableBundle b = configMgr.getConfigForSubId(getSubId());
+            if (b != null) {
+                int volteReplacementRat =
+                        b.getInt(CarrierConfigManager.KEY_VOLTE_REPLACEMENT_RAT_INT);
+                logd("phoneObjectUpdater: volteReplacementRat=" + volteReplacementRat);
+                if (volteReplacementRat != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN) {
+                    newVoiceRadioTech = volteReplacementRat;
+                }
+            } else {
+                loge("phoneObjectUpdater: didn't get volteReplacementRat from carrier config");
+            }
+        }
+
+        if(mRilVersion == 6 && getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE) {
+            /*
+             * On v6 RIL, when LTE_ON_CDMA is TRUE, always create CDMALTEPhone
+             * irrespective of the voice radio tech reported.
+             */
+            if (getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+                logd("phoneObjectUpdater: LTE ON CDMA property is set. Use CDMA Phone" +
+                        " newVoiceRadioTech=" + newVoiceRadioTech +
+                        " mActivePhone=" + getPhoneName());
+                return;
+            } else {
+                logd("phoneObjectUpdater: LTE ON CDMA property is set. Switch to CDMALTEPhone" +
+                        " newVoiceRadioTech=" + newVoiceRadioTech +
+                        " mActivePhone=" + getPhoneName());
+                newVoiceRadioTech = ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT;
+            }
+        } else {
+
+            // If the device is shutting down, then there is no need to switch to the new phone
+            // which might send unnecessary attach request to the modem.
+            if (isShuttingDown()) {
+                logd("Device is shutting down. No need to switch phone now.");
+                return;
+            }
+
+            boolean matchCdma = ServiceState.isCdma(newVoiceRadioTech);
+            boolean matchGsm = ServiceState.isGsm(newVoiceRadioTech);
+            if ((matchCdma && getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) ||
+                    (matchGsm && getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)) {
+                // Nothing changed. Keep phone as it is.
+                logd("phoneObjectUpdater: No change ignore," +
+                        " newVoiceRadioTech=" + newVoiceRadioTech +
+                        " mActivePhone=" + getPhoneName());
+                return;
+            }
+            if (!matchCdma && !matchGsm) {
+                loge("phoneObjectUpdater: newVoiceRadioTech=" + newVoiceRadioTech +
+                        " doesn't match either CDMA or GSM - error! No phone change");
+                return;
+            }
+        }
+
+        if (newVoiceRadioTech == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN) {
+            // We need some voice phone object to be active always, so never
+            // delete the phone without anything to replace it with!
+            logd("phoneObjectUpdater: Unknown rat ignore, "
+                    + " newVoiceRadioTech=Unknown. mActivePhone=" + getPhoneName());
+            return;
+        }
+
+        boolean oldPowerState = false; // old power state to off
+        if (mResetModemOnRadioTechnologyChange) {
+            if (mCi.getRadioState().isOn()) {
+                oldPowerState = true;
+                logd("phoneObjectUpdater: Setting Radio Power to Off");
+                mCi.setRadioPower(false, null);
+            }
+        }
+
+        switchVoiceRadioTech(newVoiceRadioTech);
+
+        if (mResetModemOnRadioTechnologyChange && oldPowerState) { // restore power state
+            logd("phoneObjectUpdater: Resetting Radio");
+            mCi.setRadioPower(oldPowerState, null);
+        }
+
+        // update voice radio tech in icc card proxy
+        mIccCardProxy.setVoiceRadioTech(newVoiceRadioTech);
+
+        // Send an Intent to the PhoneApp that we had a radio technology change
+        Intent intent = new Intent(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED);
+        intent.putExtra(PhoneConstants.PHONE_NAME_KEY, getPhoneName());
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhoneId);
+        ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+    }
+
+    private void switchVoiceRadioTech(int newVoiceRadioTech) {
+
+        String outgoingPhoneName = getPhoneName();
+
+        logd("Switching Voice Phone : " + outgoingPhoneName + " >>> "
+                + (ServiceState.isGsm(newVoiceRadioTech) ? "GSM" : "CDMA"));
+
+        if (ServiceState.isCdma(newVoiceRadioTech)) {
+            switchPhoneType(PhoneConstants.PHONE_TYPE_CDMA_LTE);
+        } else if (ServiceState.isGsm(newVoiceRadioTech)) {
+            switchPhoneType(PhoneConstants.PHONE_TYPE_GSM);
+        } else {
+            loge("deleteAndCreatePhone: newVoiceRadioTech=" + newVoiceRadioTech +
+                    " is not CDMA or GSM (error) - aborting!");
+            return;
+        }
+    }
+
+    @Override
+    public IccSmsInterfaceManager getIccSmsInterfaceManager(){
+        return mIccSmsInterfaceManager;
+    }
+
+    @Override
+    public void updatePhoneObject(int voiceRadioTech) {
+        logd("updatePhoneObject: radioTechnology=" + voiceRadioTech);
+        sendMessage(obtainMessage(EVENT_UPDATE_PHONE_OBJECT, voiceRadioTech, 0, null));
+    }
+
+    @Override
+    public void setImsRegistrationState(boolean registered) {
+        mSST.setImsRegistrationState(registered);
+    }
+
+    @Override
+    public boolean getIccRecordsLoaded() {
+        return mIccCardProxy.getIccRecordsLoaded();
+    }
+
+    @Override
+    public IccCard getIccCard() {
+        return mIccCardProxy;
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("GsmCdmaPhone extends:");
+        super.dump(fd, pw, args);
+        pw.println(" mPrecisePhoneType=" + mPrecisePhoneType);
+        pw.println(" mCT=" + mCT);
+        pw.println(" mSST=" + mSST);
+        pw.println(" mPendingMMIs=" + mPendingMMIs);
+        pw.println(" mIccPhoneBookIntManager=" + mIccPhoneBookIntManager);
+        if (VDBG) pw.println(" mImei=" + mImei);
+        if (VDBG) pw.println(" mImeiSv=" + mImeiSv);
+        if (VDBG) pw.println(" mVmNumber=" + mVmNumber);
+        pw.println(" mCdmaSSM=" + mCdmaSSM);
+        pw.println(" mCdmaSubscriptionSource=" + mCdmaSubscriptionSource);
+        pw.println(" mEriManager=" + mEriManager);
+        pw.println(" mWakeLock=" + mWakeLock);
+        pw.println(" isInEcm()=" + isInEcm());
+        if (VDBG) pw.println(" mEsn=" + mEsn);
+        if (VDBG) pw.println(" mMeid=" + mMeid);
+        pw.println(" mCarrierOtaSpNumSchema=" + mCarrierOtaSpNumSchema);
+        if (!isPhoneTypeGsm()) {
+            pw.println(" getCdmaEriIconIndex()=" + getCdmaEriIconIndex());
+            pw.println(" getCdmaEriIconMode()=" + getCdmaEriIconMode());
+            pw.println(" getCdmaEriText()=" + getCdmaEriText());
+            pw.println(" isMinInfoReady()=" + isMinInfoReady());
+        }
+        pw.println(" isCspPlmnEnabled()=" + isCspPlmnEnabled());
+        pw.flush();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        try {
+            mIccCardProxy.dump(fd, pw, args);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.flush();
+        pw.println("++++++++++++++++++++++++++++++++");
+        pw.println("DeviceStateMonitor:");
+        mDeviceStateMonitor.dump(fd, pw, args);
+        pw.println("++++++++++++++++++++++++++++++++");
+    }
+
+    @Override
+    public boolean setOperatorBrandOverride(String brand) {
+        if (mUiccController == null) {
+            return false;
+        }
+
+        UiccCard card = mUiccController.getUiccCard(getPhoneId());
+        if (card == null) {
+            return false;
+        }
+
+        boolean status = card.setOperatorBrandOverride(brand);
+
+        // Refresh.
+        if (status) {
+            IccRecords iccRecords = mIccRecords.get();
+            if (iccRecords != null) {
+                TelephonyManager.from(mContext).setSimOperatorNameForPhone(
+                        getPhoneId(), iccRecords.getServiceProviderName());
+            }
+            if (mSST != null) {
+                mSST.pollState();
+            }
+        }
+        return status;
+    }
+
+    /**
+     * @return operator numeric.
+     */
+    private String getOperatorNumeric() {
+        String operatorNumeric = null;
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            if (r != null) {
+                operatorNumeric = r.getOperatorNumeric();
+            }
+        } else { //isPhoneTypeCdmaLte()
+            IccRecords curIccRecords = null;
+            if (mCdmaSubscriptionSource == CDMA_SUBSCRIPTION_NV) {
+                operatorNumeric = SystemProperties.get("ro.cdma.home.operator.numeric");
+            } else if (mCdmaSubscriptionSource == CDMA_SUBSCRIPTION_RUIM_SIM) {
+                curIccRecords = mSimRecords;
+                if (curIccRecords != null) {
+                    operatorNumeric = curIccRecords.getOperatorNumeric();
+                } else {
+                    curIccRecords = mIccRecords.get();
+                    if (curIccRecords != null && (curIccRecords instanceof RuimRecords)) {
+                        RuimRecords csim = (RuimRecords) curIccRecords;
+                        operatorNumeric = csim.getRUIMOperatorNumeric();
+                    }
+                }
+            }
+            if (operatorNumeric == null) {
+                loge("getOperatorNumeric: Cannot retrieve operatorNumeric:"
+                        + " mCdmaSubscriptionSource = " + mCdmaSubscriptionSource +
+                        " mIccRecords = " + ((curIccRecords != null) ?
+                        curIccRecords.getRecordsLoaded() : null));
+            }
+
+            logd("getOperatorNumeric: mCdmaSubscriptionSource = " + mCdmaSubscriptionSource
+                    + " operatorNumeric = " + operatorNumeric);
+
+        }
+        return operatorNumeric;
+    }
+
+    /**
+     * @return The country ISO for the subscription associated with this phone.
+     */
+    public String getCountryIso() {
+        int subId = getSubId();
+        SubscriptionInfo subInfo = SubscriptionManager.from(getContext())
+                .getActiveSubscriptionInfo(subId);
+        if (subInfo == null) {
+            return null;
+        }
+        return subInfo.getCountryIso().toUpperCase();
+    }
+
+    public void notifyEcbmTimerReset(Boolean flag) {
+        mEcmTimerResetRegistrants.notifyResult(flag);
+    }
+
+    /**
+     * Registration point for Ecm timer reset
+     *
+     * @param h handler to notify
+     * @param what User-defined message code
+     * @param obj placed in Message.obj
+     */
+    @Override
+    public void registerForEcmTimerReset(Handler h, int what, Object obj) {
+        mEcmTimerResetRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForEcmTimerReset(Handler h) {
+        mEcmTimerResetRegistrants.remove(h);
+    }
+
+    /**
+     * Sets the SIM voice message waiting indicator records.
+     * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported
+     * @param countWaiting The number of messages waiting, if known. Use
+     *                     -1 to indicate that an unknown number of
+     *                      messages are waiting
+     */
+    @Override
+    public void setVoiceMessageWaiting(int line, int countWaiting) {
+        if (isPhoneTypeGsm()) {
+            IccRecords r = mIccRecords.get();
+            if (r != null) {
+                r.setVoiceMessageWaiting(line, countWaiting);
+            } else {
+                logd("SIM Records not found, MWI not updated");
+            }
+        } else {
+            setVoiceMessageCount(countWaiting);
+        }
+    }
+
+    private void logd(String s) {
+        Rlog.d(LOG_TAG, "[GsmCdmaPhone] " + s);
+    }
+
+    private void logi(String s) {
+        Rlog.i(LOG_TAG, "[GsmCdmaPhone] " + s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, "[GsmCdmaPhone] " + s);
+    }
+
+    @Override
+    public boolean isUtEnabled() {
+        Phone imsPhone = mImsPhone;
+        if (imsPhone != null) {
+            return imsPhone.isUtEnabled();
+        } else {
+            logd("isUtEnabled: called for GsmCdma");
+            return false;
+        }
+    }
+
+    public String getDtmfToneDelayKey() {
+        return isPhoneTypeGsm() ?
+                CarrierConfigManager.KEY_GSM_DTMF_TONE_DELAY_INT :
+                CarrierConfigManager.KEY_CDMA_DTMF_TONE_DELAY_INT;
+    }
+
+    @VisibleForTesting
+    public PowerManager.WakeLock getWakeLock() {
+        return mWakeLock;
+    }
+
+}
diff --git a/com/android/internal/telephony/HardwareConfig.java b/com/android/internal/telephony/HardwareConfig.java
new file mode 100644
index 0000000..8623354
--- /dev/null
+++ b/com/android/internal/telephony/HardwareConfig.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+import android.telephony.Rlog;
+import java.util.BitSet;
+import android.telephony.ServiceState;
+
+/**
+ * {@hide}
+ *
+ * hardware configuration information reported by the ril layer and for
+ * use by the telephone framework.
+ *
+ * the hardware configuration is managed by the TelephonyDevController
+ * (aka: the 'TDC').
+ *
+ * the hardware resources are:
+ *    - modem: physical entity providing acces technology.
+ *    - sim: physicaly entity providing a slot interface.
+ */
+public class HardwareConfig {
+    static final String LOG_TAG = "HardwareConfig";
+
+    /**
+     * hardware configuration kind.
+     */
+    public static final int DEV_HARDWARE_TYPE_MODEM = 0;
+    public static final int DEV_HARDWARE_TYPE_SIM   = 1;
+    /**
+     * ril attachment model.  if single, there is a one-to-one
+     * relationship between a modem hardware and a ril daemon.
+     * if multiple, there is a one-to-many relatioship between a
+     * modem hardware and several ril simultaneous ril daemons.
+     */
+    public static final int DEV_MODEM_RIL_MODEL_SINGLE   = 0;
+    public static final int DEV_MODEM_RIL_MODEL_MULTIPLE = 1;
+    /**
+     * hardware state of the resource.
+     *
+     *   enabled: the resource can be used by the msim-framework,
+     *            call activity can be handled on it.
+     *   standby: the resource can be used by the msim-framework but
+     *            only for non call related activity.  as example:
+     *            reading the address book from a sim device. attempt
+     *            to use this resource for call activity leads to
+     *            undetermined results.
+     *   disabled: the resource  cannot be used and attempt to use
+     *             it leads to undetermined results.
+     *
+     * by default, all resources are 'enabled', what causes a resource
+     * to be marked otherwise is a property of the underlying hardware
+     * knowledge and implementation and it is out of scope of the TDC.
+     */
+    public static final int DEV_HARDWARE_STATE_ENABLED  = 0;
+    public static final int DEV_HARDWARE_STATE_STANDBY  = 1;
+    public static final int DEV_HARDWARE_STATE_DISABLED = 2;
+
+    /**
+     * common hardware configuration.
+     *
+     * type - see DEV_HARDWARE_TYPE_
+     * uuid - unique identifier for this hardware.
+     * state - see DEV_HARDWARE_STATE_
+     */
+    public int type;
+    public String uuid;
+    public int state;
+    /**
+     * following is some specific hardware configuration based on the hardware type.
+     */
+    /**
+     * DEV_HARDWARE_TYPE_MODEM.
+     *
+     * rilModel - see DEV_MODEM_RIL_MODEL_
+     * rat - BitSet value, based on android.telephony.ServiceState
+     * maxActiveVoiceCall - maximum number of concurent active voice calls.
+     * maxActiveDataCall - maximum number of concurent active data calls.
+     * maxStandby - maximum number of concurent standby connections.
+     *
+     * note: the maxStandby is not necessarily an equal sum of the maxActiveVoiceCall
+     * and maxActiveDataCall (nor a derivative of it) since it really depends on the
+     * modem capability, hence it is left for the hardware to define.
+     */
+    public int rilModel;
+    public BitSet rat;
+    public int maxActiveVoiceCall;
+    public int maxActiveDataCall;
+    public int maxStandby;
+    /**
+     * DEV_HARDWARE_TYPE_SIM.
+     *
+     * modemUuid - unique association to a modem for a sim.
+     */
+    public String modemUuid;
+
+    /**
+     * default constructor.
+     */
+    public HardwareConfig(int type) {
+        this.type = type;
+    }
+
+    /**
+     * create from a resource string format.
+     */
+    public HardwareConfig(String res) {
+        String split[] = res.split(",");
+
+        type = Integer.parseInt(split[0]);
+
+        switch (type) {
+            case DEV_HARDWARE_TYPE_MODEM: {
+                assignModem(
+                    split[1].trim(),            /* uuid */
+                    Integer.parseInt(split[2]), /* state */
+                    Integer.parseInt(split[3]), /* ril-model */
+                    Integer.parseInt(split[4]), /* rat */
+                    Integer.parseInt(split[5]), /* max-voice */
+                    Integer.parseInt(split[6]), /* max-data */
+                    Integer.parseInt(split[7])  /* max-standby */
+                );
+                break;
+            }
+            case DEV_HARDWARE_TYPE_SIM: {
+                assignSim(
+                    split[1].trim(),            /* uuid */
+                    Integer.parseInt(split[2]), /* state */
+                    split[3].trim()             /* modem-uuid */
+                );
+                break;
+            }
+        }
+    }
+
+    public void assignModem(String id, int state, int model, int ratBits,
+        int maxV, int maxD, int maxS) {
+        if (type == DEV_HARDWARE_TYPE_MODEM) {
+            char[] bits = Integer.toBinaryString(ratBits).toCharArray();
+            uuid = id;
+            this.state = state;
+            rilModel = model;
+            rat = new BitSet(bits.length);
+            for (int i = 0 ; i < bits.length ; i++) {
+                rat.set(i, (bits[i] == '1' ? true : false));
+            }
+            maxActiveVoiceCall = maxV;
+            maxActiveDataCall = maxD;
+            maxStandby = maxS;
+        }
+    }
+
+    public void assignSim(String id, int state, String link) {
+        if (type == DEV_HARDWARE_TYPE_SIM) {
+            uuid = id;
+            modemUuid = link;
+            this.state = state;
+        }
+    }
+
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        if (type == DEV_HARDWARE_TYPE_MODEM) {
+            builder.append("Modem ");
+            builder.append("{ uuid=" + uuid);
+            builder.append(", state=" + state);
+            builder.append(", rilModel=" + rilModel);
+            builder.append(", rat=" + rat.toString());
+            builder.append(", maxActiveVoiceCall=" + maxActiveVoiceCall);
+            builder.append(", maxActiveDataCall=" + maxActiveDataCall);
+            builder.append(", maxStandby=" + maxStandby);
+            builder.append(" }");
+        } else if (type == DEV_HARDWARE_TYPE_SIM) {
+            builder.append("Sim ");
+            builder.append("{ uuid=" + uuid);
+            builder.append(", modemUuid=" + modemUuid);
+            builder.append(", state=" + state);
+            builder.append(" }");
+        } else {
+            builder.append("Invalid Configration");
+        }
+        return builder.toString();
+    }
+
+    public int compareTo(HardwareConfig hw) {
+        String one = this.toString();
+        String two = hw.toString();
+
+        return (one.compareTo(two));
+    }
+}
diff --git a/com/android/internal/telephony/HbpcdLookup.java b/com/android/internal/telephony/HbpcdLookup.java
new file mode 100644
index 0000000..d9a3e72
--- /dev/null
+++ b/com/android/internal/telephony/HbpcdLookup.java
@@ -0,0 +1,124 @@
+/*
+**
+** Copyright 2014, 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.internal.telephony;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * @hide
+ */
+public class HbpcdLookup {
+    public static final String AUTHORITY = "hbpcd_lookup";
+
+    public static final Uri CONTENT_URI =
+        Uri.parse("content://" + AUTHORITY);
+
+    public static final String PATH_MCC_IDD = "idd";
+    public static final String PATH_MCC_LOOKUP_TABLE = "lookup";
+    public static final String PATH_MCC_SID_CONFLICT = "conflict";
+    public static final String PATH_MCC_SID_RANGE = "range";
+    public static final String PATH_NANP_AREA_CODE = "nanp";
+    public static final String PATH_ARBITRARY_MCC_SID_MATCH = "arbitrary";
+    public static final String PATH_USERADD_COUNTRY = "useradd";
+
+    public static final String ID = "_id";
+    public static final int IDINDEX = 0;
+
+    /**
+     * @hide
+     */
+    public static class MccIdd implements BaseColumns {
+        public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTHORITY + "/" + PATH_MCC_IDD);
+        public static final String DEFAULT_SORT_ORDER = "MCC ASC";
+
+        public static final String MCC = "MCC";
+        public static final String IDD = "IDD";
+
+    }
+
+    /**
+     * @hide
+     */
+    public static class MccLookup implements BaseColumns {
+        public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTHORITY + "/" + PATH_MCC_LOOKUP_TABLE);
+        public static final String DEFAULT_SORT_ORDER = "MCC ASC";
+
+        public static final String MCC = "MCC";
+        public static final String COUNTRY_CODE = "Country_Code";
+        public static final String COUNTRY_NAME = "Country_Name";
+        public static final String NDD = "NDD";
+        public static final String NANPS = "NANPS";
+        public static final String GMT_OFFSET_LOW = "GMT_Offset_Low";
+        public static final String GMT_OFFSET_HIGH = "GMT_Offset_High";
+        public static final String GMT_DST_LOW = "GMT_DST_Low";
+        public static final String GMT_DST_HIGH = "GMT_DST_High";
+
+    }
+
+    /**
+     * @hide
+     */
+    public static class MccSidConflicts implements BaseColumns {
+        public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTHORITY + "/" + PATH_MCC_SID_CONFLICT);
+        public static final String DEFAULT_SORT_ORDER = "MCC ASC";
+
+        public static final String MCC = "MCC";
+        public static final String SID_CONFLICT = "SID_Conflict";
+
+    }
+
+    /**
+     * @hide
+     */
+    public static class MccSidRange implements BaseColumns {
+        public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTHORITY + "/" + PATH_MCC_SID_RANGE);
+        public static final String DEFAULT_SORT_ORDER = "MCC ASC";
+
+        public static final String MCC = "MCC";
+        public static final String RANGE_LOW = "SID_Range_Low";
+        public static final String RANGE_HIGH = "SID_Range_High";
+    }
+
+    /**
+     * @hide
+     */
+    public static class ArbitraryMccSidMatch implements BaseColumns {
+        public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTHORITY + "/" + PATH_ARBITRARY_MCC_SID_MATCH);
+        public static final String DEFAULT_SORT_ORDER = "MCC ASC";
+
+        public static final String MCC = "MCC";
+        public static final String SID = "SID";
+
+    }
+
+    /**
+     * @hide
+     */
+    public static class NanpAreaCode implements BaseColumns {
+        public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTHORITY + "/" + PATH_NANP_AREA_CODE);
+        public static final String DEFAULT_SORT_ORDER = "Area_Code ASC";
+
+        public static final String AREA_CODE = "Area_Code";
+    }
+}
diff --git a/com/android/internal/telephony/HbpcdUtils.java b/com/android/internal/telephony/HbpcdUtils.java
new file mode 100644
index 0000000..2f31942
--- /dev/null
+++ b/com/android/internal/telephony/HbpcdUtils.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.HbpcdLookup.ArbitraryMccSidMatch;
+import com.android.internal.telephony.HbpcdLookup.MccIdd;
+import com.android.internal.telephony.HbpcdLookup.MccLookup;
+import com.android.internal.telephony.HbpcdLookup.MccSidConflicts;
+import com.android.internal.telephony.HbpcdLookup.MccSidRange;
+
+public final class HbpcdUtils {
+    private static final String LOG_TAG = "HbpcdUtils";
+    private static final boolean DBG = false;
+    private ContentResolver resolver = null;
+
+    public HbpcdUtils(Context context) {
+        resolver = context.getContentResolver();
+    }
+
+    /**
+     *  Resolves the unknown MCC with SID and Timezone information.
+    */
+    public int getMcc(int sid, int tz, int DSTflag, boolean isNitzTimeZone) {
+        int tmpMcc = 0;
+
+        // check if SID exists in arbitrary_mcc_sid_match table.
+        // these SIDs are assigned to more than 1 operators, but they are known to
+        // be used by a specific operator, other operators having the same SID are
+        // not using it currently, if that SID is in this table, we don't need to
+        // check other tables.
+        String projection2[] = {ArbitraryMccSidMatch.MCC};
+        Cursor c2 = resolver.query(ArbitraryMccSidMatch.CONTENT_URI, projection2,
+                            ArbitraryMccSidMatch.SID + "=" + sid, null, null);
+
+        if (c2 != null) {
+            int c2Counter = c2.getCount();
+            if (DBG) {
+                Rlog.d(LOG_TAG, "Query unresolved arbitrary table, entries are " + c2Counter);
+            }
+            if (c2Counter == 1) {
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "Query Unresolved arbitrary returned the cursor " + c2);
+                }
+                c2.moveToFirst();
+                tmpMcc = c2.getInt(0);
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "MCC found in arbitrary_mcc_sid_match: " + tmpMcc);
+                }
+                c2.close();
+                return tmpMcc;
+            }
+            c2.close();
+        }
+
+        // Then check if SID exists in mcc_sid_conflict table.
+        // and use the timezone in mcc_lookup table to check which MCC matches.
+        String projection3[] = {MccSidConflicts.MCC};
+        Cursor c3 = resolver.query(MccSidConflicts.CONTENT_URI, projection3,
+                MccSidConflicts.SID_CONFLICT + "=" + sid + " and (((" +
+                MccLookup.GMT_OFFSET_LOW + "<=" + tz + ") and (" + tz + "<=" +
+                MccLookup.GMT_OFFSET_HIGH + ") and (" + "0=" + DSTflag + ")) or ((" +
+                MccLookup.GMT_DST_LOW + "<=" + tz + ") and (" + tz + "<=" +
+                MccLookup.GMT_DST_HIGH + ") and (" + "1=" + DSTflag + ")))",
+                        null, null);
+        if (c3 != null) {
+            int c3Counter = c3.getCount();
+            if (c3Counter > 0) {
+                if (c3Counter > 1) {
+                    Rlog.w(LOG_TAG, "something wrong, get more results for 1 conflict SID: " + c3);
+                }
+                if (DBG) Rlog.d(LOG_TAG, "Query conflict sid returned the cursor " + c3);
+                c3.moveToFirst();
+                tmpMcc = c3.getInt(0);
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "MCC found in mcc_lookup_table. Return tmpMcc = " + tmpMcc);
+                }
+                if (!isNitzTimeZone) {
+                    // time zone is not accurate, it may get wrong mcc, ignore it.
+                    if (DBG) {
+                        Rlog.d(LOG_TAG, "time zone is not accurate, mcc may be " + tmpMcc);
+                    }
+                    tmpMcc = 0;
+                }
+                c3.close();
+                return tmpMcc;
+            } else {
+                c3.close();
+            }
+        }
+
+        // if there is no conflict, then check if SID is in mcc_sid_range.
+        String projection5[] = {MccSidRange.MCC};
+        Cursor c5 = resolver.query(MccSidRange.CONTENT_URI, projection5,
+                MccSidRange.RANGE_LOW + "<=" + sid + " and " +
+                MccSidRange.RANGE_HIGH + ">=" + sid,
+                null, null);
+        if (c5 != null) {
+            if (c5.getCount() > 0) {
+                if (DBG) Rlog.d(LOG_TAG, "Query Range returned the cursor " + c5);
+                c5.moveToFirst();
+                tmpMcc = c5.getInt(0);
+                if (DBG) Rlog.d(LOG_TAG, "SID found in mcc_sid_range. Return tmpMcc = " + tmpMcc);
+                c5.close();
+                return tmpMcc;
+            }
+            c5.close();
+        }
+        if (DBG) Rlog.d(LOG_TAG, "SID NOT found in mcc_sid_range.");
+
+        if (DBG) Rlog.d(LOG_TAG, "Exit getMccByOtherFactors. Return tmpMcc =  " + tmpMcc);
+        // If unknown MCC still could not be resolved,
+        return tmpMcc;
+    }
+
+    /**
+     *  Gets country information with given MCC.
+    */
+    public String getIddByMcc(int mcc) {
+        if (DBG) Rlog.d(LOG_TAG, "Enter getHbpcdInfoByMCC.");
+        String idd = "";
+
+        Cursor c = null;
+
+        String projection[] = {MccIdd.IDD};
+        Cursor cur = resolver.query(MccIdd.CONTENT_URI, projection,
+                MccIdd.MCC + "=" + mcc, null, null);
+        if (cur != null) {
+            if (cur.getCount() > 0) {
+                if (DBG) Rlog.d(LOG_TAG, "Query Idd returned the cursor " + cur);
+                // TODO: for those country having more than 1 IDDs, need more information
+                // to decide which IDD would be used. currently just use the first 1.
+                cur.moveToFirst();
+                idd = cur.getString(0);
+                if (DBG) Rlog.d(LOG_TAG, "IDD = " + idd);
+
+            }
+            cur.close();
+        }
+        if (c != null) c.close();
+
+        if (DBG) Rlog.d(LOG_TAG, "Exit getHbpcdInfoByMCC.");
+        return idd;
+    }
+}
diff --git a/com/android/internal/telephony/IccCard.java b/com/android/internal/telephony/IccCard.java
new file mode 100644
index 0000000..e5b34e2
--- /dev/null
+++ b/com/android/internal/telephony/IccCard.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.telephony.IccCardConstants.State;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccRecords;
+
+/**
+ * {@hide}
+ * @Deprecated use UiccController.getUiccCard instead.
+ *
+ * Integrated Circuit Card (ICC) interface
+ * An object of a class implementing this interface is used by external
+ * apps (specifically PhoneApp) to perform icc card related functionality.
+ *
+ * Apps (those that have access to Phone object) can retrieve this object
+ * by calling phone.getIccCard()
+ *
+ * This interface is implemented by IccCardProxy and the object PhoneApp
+ * gets when it calls getIccCard is IccCardProxy.
+ */
+public interface IccCard {
+    /**
+     * @return combined Card and current App state
+     */
+    public State getState();
+
+    /**
+     * @return IccRecords object belonging to current UiccCardApplication
+     */
+    public IccRecords getIccRecords();
+
+    /**
+     * @return IccFileHandler object belonging to current UiccCardApplication
+     */
+    public IccFileHandler getIccFileHandler();
+
+    /**
+     * Notifies handler of any transition into IccCardConstants.State.ABSENT
+     */
+    public void registerForAbsent(Handler h, int what, Object obj);
+    public void unregisterForAbsent(Handler h);
+
+    /**
+     * Notifies handler of any transition into IccCardConstants.State.NETWORK_LOCKED
+     */
+    public void registerForNetworkLocked(Handler h, int what, Object obj);
+    public void unregisterForNetworkLocked(Handler h);
+
+    /**
+     * Notifies handler of any transition into IccCardConstants.State.isPinLocked()
+     */
+    public void registerForLocked(Handler h, int what, Object obj);
+    public void unregisterForLocked(Handler h);
+
+    /**
+     * Supply the ICC PIN to the ICC
+     *
+     * When the operation is complete, onComplete will be sent to its
+     * Handler.
+     *
+     * onComplete.obj will be an AsyncResult
+     *
+     * ((AsyncResult)onComplete.obj).exception == null on success
+     * ((AsyncResult)onComplete.obj).exception != null on fail
+     *
+     * If the supplied PIN is incorrect:
+     * ((AsyncResult)onComplete.obj).exception != null
+     * && ((AsyncResult)onComplete.obj).exception
+     *       instanceof com.android.internal.telephony.gsm.CommandException)
+     * && ((CommandException)(((AsyncResult)onComplete.obj).exception))
+     *          .getCommandError() == CommandException.Error.PASSWORD_INCORRECT
+     */
+    public void supplyPin (String pin, Message onComplete);
+
+    /**
+     * Supply the ICC PUK to the ICC
+     */
+    public void supplyPuk (String puk, String newPin, Message onComplete);
+
+    /**
+     * Supply the ICC PIN2 to the ICC
+     */
+    public void supplyPin2 (String pin2, Message onComplete);
+
+    /**
+     * Supply the ICC PUK2 to the ICC
+     */
+    public void supplyPuk2 (String puk2, String newPin2, Message onComplete);
+
+    /**
+     * Check whether fdn (fixed dialing number) service is available.
+     * @return true if ICC fdn service available
+     *         false if ICC fdn service not available
+    */
+    public boolean getIccFdnAvailable();
+
+    /**
+     * Supply Network depersonalization code to the RIL
+     */
+    public void supplyNetworkDepersonalization (String pin, Message onComplete);
+
+    /**
+     * Check whether ICC pin lock is enabled
+     * This is a sync call which returns the cached pin enabled state
+     *
+     * @return true for ICC locked enabled
+     *         false for ICC locked disabled
+     */
+    public boolean getIccLockEnabled();
+
+    /**
+     * Check whether ICC fdn (fixed dialing number) is enabled
+     * This is a sync call which returns the cached pin enabled state
+     *
+     * @return true for ICC fdn enabled
+     *         false for ICC fdn disabled
+     */
+    public boolean getIccFdnEnabled();
+
+     /**
+      * Set the ICC pin lock enabled or disabled
+      * When the operation is complete, onComplete will be sent to its handler
+      *
+      * @param enabled "true" for locked "false" for unlocked.
+      * @param password needed to change the ICC pin state, aka. Pin1
+      * @param onComplete
+      *        onComplete.obj will be an AsyncResult
+      *        ((AsyncResult)onComplete.obj).exception == null on success
+      *        ((AsyncResult)onComplete.obj).exception != null on fail
+      */
+     public void setIccLockEnabled (boolean enabled,
+             String password, Message onComplete);
+
+     /**
+      * Set the ICC fdn enabled or disabled
+      * When the operation is complete, onComplete will be sent to its handler
+      *
+      * @param enabled "true" for locked "false" for unlocked.
+      * @param password needed to change the ICC fdn enable, aka Pin2
+      * @param onComplete
+      *        onComplete.obj will be an AsyncResult
+      *        ((AsyncResult)onComplete.obj).exception == null on success
+      *        ((AsyncResult)onComplete.obj).exception != null on fail
+      */
+     public void setIccFdnEnabled (boolean enabled,
+             String password, Message onComplete);
+
+     /**
+      * Change the ICC password used in ICC pin lock
+      * When the operation is complete, onComplete will be sent to its handler
+      *
+      * @param oldPassword is the old password
+      * @param newPassword is the new password
+      * @param onComplete
+      *        onComplete.obj will be an AsyncResult
+      *        ((AsyncResult)onComplete.obj).exception == null on success
+      *        ((AsyncResult)onComplete.obj).exception != null on fail
+      */
+     public void changeIccLockPassword(String oldPassword, String newPassword,
+             Message onComplete);
+
+     /**
+      * Change the ICC password used in ICC fdn enable
+      * When the operation is complete, onComplete will be sent to its handler
+      *
+      * @param oldPassword is the old password
+      * @param newPassword is the new password
+      * @param onComplete
+      *        onComplete.obj will be an AsyncResult
+      *        ((AsyncResult)onComplete.obj).exception == null on success
+      *        ((AsyncResult)onComplete.obj).exception != null on fail
+      */
+     public void changeIccFdnPassword(String oldPassword, String newPassword,
+             Message onComplete);
+
+    /**
+     * Returns service provider name stored in ICC card.
+     * If there is no service provider name associated or the record is not
+     * yet available, null will be returned <p>
+     *
+     * Please use this value when display Service Provider Name in idle mode <p>
+     *
+     * Usage of this provider name in the UI is a common carrier requirement.
+     *
+     * Also available via Android property "gsm.sim.operator.alpha"
+     *
+     * @return Service Provider Name stored in ICC card
+     *         null if no service provider name associated or the record is not
+     *         yet available
+     *
+     */
+    public String getServiceProviderName ();
+
+    /**
+     * Checks if an Application of specified type present on the card
+     * @param type is AppType to look for
+     */
+    public boolean isApplicationOnIcc(IccCardApplicationStatus.AppType type);
+
+    /**
+     * @return true if a ICC card is present
+     */
+    public boolean hasIccCard();
+
+    /**
+     * @return true if ICC card is PIN2 blocked
+     */
+    public boolean getIccPin2Blocked();
+
+    /**
+     * @return true if ICC card is PUK2 blocked
+     */
+    public boolean getIccPuk2Blocked();
+}
diff --git a/com/android/internal/telephony/IccCardConstants.java b/com/android/internal/telephony/IccCardConstants.java
new file mode 100644
index 0000000..f3d9335
--- /dev/null
+++ b/com/android/internal/telephony/IccCardConstants.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony;
+
+import android.telephony.TelephonyManager;
+
+/**
+ * {@hide}
+ */
+public class IccCardConstants {
+
+    /* The extra data for broadcasting intent INTENT_ICC_STATE_CHANGE */
+    public static final String INTENT_KEY_ICC_STATE = "ss";
+    /* UNKNOWN means the ICC state is unknown */
+    public static final String INTENT_VALUE_ICC_UNKNOWN = "UNKNOWN";
+    /* NOT_READY means the ICC interface is not ready (eg, radio is off or powering on) */
+    public static final String INTENT_VALUE_ICC_NOT_READY = "NOT_READY";
+    /* ABSENT means ICC is missing */
+    public static final String INTENT_VALUE_ICC_ABSENT = "ABSENT";
+    /* CARD_IO_ERROR means for three consecutive times there was SIM IO error */
+    static public final String INTENT_VALUE_ICC_CARD_IO_ERROR = "CARD_IO_ERROR";
+    /* CARD_RESTRICTED means card is present but not usable due to carrier restrictions */
+    static public final String INTENT_VALUE_ICC_CARD_RESTRICTED = "CARD_RESTRICTED";
+    /* LOCKED means ICC is locked by pin or by network */
+    public static final String INTENT_VALUE_ICC_LOCKED = "LOCKED";
+    //TODO: we can remove this state in the future if Bug 18489776 analysis
+    //#42's first race condition is resolved
+    /* INTERNAL LOCKED means ICC is locked by pin or by network */
+    public static final String INTENT_VALUE_ICC_INTERNAL_LOCKED = "INTERNAL_LOCKED";
+    /* READY means ICC is ready to access */
+    public static final String INTENT_VALUE_ICC_READY = "READY";
+    /* IMSI means ICC IMSI is ready in property */
+    public static final String INTENT_VALUE_ICC_IMSI = "IMSI";
+    /* LOADED means all ICC records, including IMSI, are loaded */
+    public static final String INTENT_VALUE_ICC_LOADED = "LOADED";
+    /* The extra data for broadcasting intent INTENT_ICC_STATE_CHANGE */
+    public static final String INTENT_KEY_LOCKED_REASON = "reason";
+    /* PIN means ICC is locked on PIN1 */
+    public static final String INTENT_VALUE_LOCKED_ON_PIN = "PIN";
+    /* PUK means ICC is locked on PUK1 */
+    public static final String INTENT_VALUE_LOCKED_ON_PUK = "PUK";
+    /* NETWORK means ICC is locked on NETWORK PERSONALIZATION */
+    public static final String INTENT_VALUE_LOCKED_NETWORK = "NETWORK";
+    /* PERM_DISABLED means ICC is permanently disabled due to puk fails */
+    public static final String INTENT_VALUE_ABSENT_ON_PERM_DISABLED = "PERM_DISABLED";
+
+    /**
+     * This is combination of IccCardStatus.CardState and IccCardApplicationStatus.AppState
+     * for external apps (like PhoneApp) to use
+     *
+     * UNKNOWN is a transient state, for example, after user inputs ICC pin under
+     * PIN_REQUIRED state, the query for ICC status returns UNKNOWN before it
+     * turns to READY
+     *
+     * The ordinal values much match {@link TelephonyManager#SIM_STATE_UNKNOWN} ...
+     */
+    public enum State {
+        UNKNOWN,        /** ordinal(0) == {@See TelephonyManager#SIM_STATE_UNKNOWN} */
+        ABSENT,         /** ordinal(1) == {@See TelephonyManager#SIM_STATE_ABSENT} */
+        PIN_REQUIRED,   /** ordinal(2) == {@See TelephonyManager#SIM_STATE_PIN_REQUIRED} */
+        PUK_REQUIRED,   /** ordinal(3) == {@See TelephonyManager#SIM_STATE_PUK_REQUIRED} */
+        NETWORK_LOCKED, /** ordinal(4) == {@See TelephonyManager#SIM_STATE_NETWORK_LOCKED} */
+        READY,          /** ordinal(5) == {@See TelephonyManager#SIM_STATE_READY} */
+        NOT_READY,      /** ordinal(6) == {@See TelephonyManager#SIM_STATE_NOT_READY} */
+        PERM_DISABLED,  /** ordinal(7) == {@See TelephonyManager#SIM_STATE_PERM_DISABLED} */
+        CARD_IO_ERROR,  /** ordinal(8) == {@See TelephonyManager#SIM_STATE_CARD_IO_ERROR} */
+        CARD_RESTRICTED;/** ordinal(9) == {@See TelephonyManager#SIM_STATE_CARD_RESTRICTED} */
+
+        public boolean isPinLocked() {
+            return ((this == PIN_REQUIRED) || (this == PUK_REQUIRED));
+        }
+
+        public boolean iccCardExist() {
+            return ((this == PIN_REQUIRED) || (this == PUK_REQUIRED)
+                    || (this == NETWORK_LOCKED) || (this == READY)
+                    || (this == PERM_DISABLED) || (this == CARD_IO_ERROR)
+                    || (this == CARD_RESTRICTED));
+        }
+
+        public static State intToState(int state) throws IllegalArgumentException {
+            switch(state) {
+                case 0: return UNKNOWN;
+                case 1: return ABSENT;
+                case 2: return PIN_REQUIRED;
+                case 3: return PUK_REQUIRED;
+                case 4: return NETWORK_LOCKED;
+                case 5: return READY;
+                case 6: return NOT_READY;
+                case 7: return PERM_DISABLED;
+                case 8: return CARD_IO_ERROR;
+                case 9: return CARD_RESTRICTED;
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/IccPhoneBookInterfaceManager.java b/com/android/internal/telephony/IccPhoneBookInterfaceManager.java
new file mode 100644
index 0000000..2a35370
--- /dev/null
+++ b/com/android/internal/telephony/IccPhoneBookInterfaceManager.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.pm.PackageManager;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.uicc.AdnRecord;
+import com.android.internal.telephony.uicc.AdnRecordCache;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+import com.android.internal.telephony.uicc.IccConstants;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * SimPhoneBookInterfaceManager to provide an inter-process communication to
+ * access ADN-like SIM records.
+ */
+public class IccPhoneBookInterfaceManager {
+    static final String LOG_TAG = "IccPhoneBookIM";
+    protected static final boolean DBG = true;
+
+    protected Phone mPhone;
+    private   UiccCardApplication mCurrentApp = null;
+    protected AdnRecordCache mAdnCache;
+    protected final Object mLock = new Object();
+    protected int mRecordSize[];
+    protected boolean mSuccess;
+    private   boolean mIs3gCard = false;  // flag to determine if card is 3G or 2G
+    protected List<AdnRecord> mRecords;
+
+
+    protected static final boolean ALLOW_SIM_OP_IN_UI_THREAD = false;
+
+    protected static final int EVENT_GET_SIZE_DONE = 1;
+    protected static final int EVENT_LOAD_DONE = 2;
+    protected static final int EVENT_UPDATE_DONE = 3;
+
+    protected Handler mBaseHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            AsyncResult ar;
+
+            switch (msg.what) {
+                case EVENT_GET_SIZE_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    synchronized (mLock) {
+                        if (ar.exception == null) {
+                            mRecordSize = (int[])ar.result;
+                            // recordSize[0]  is the record length
+                            // recordSize[1]  is the total length of the EF file
+                            // recordSize[2]  is the number of records in the EF file
+                            logd("GET_RECORD_SIZE Size " + mRecordSize[0] +
+                                    " total " + mRecordSize[1] +
+                                    " #record " + mRecordSize[2]);
+                        }
+                        notifyPending(ar);
+                    }
+                    break;
+                case EVENT_UPDATE_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    synchronized (mLock) {
+                        mSuccess = (ar.exception == null);
+                        notifyPending(ar);
+                    }
+                    break;
+                case EVENT_LOAD_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    synchronized (mLock) {
+                        if (ar.exception == null) {
+                            mRecords = (List<AdnRecord>) ar.result;
+                        } else {
+                            if(DBG) logd("Cannot load ADN records");
+                            mRecords = null;
+                        }
+                        notifyPending(ar);
+                    }
+                    break;
+            }
+        }
+
+        private void notifyPending(AsyncResult ar) {
+            if (ar.userObj != null) {
+                AtomicBoolean status = (AtomicBoolean) ar.userObj;
+                status.set(true);
+            }
+            mLock.notifyAll();
+        }
+    };
+
+    public IccPhoneBookInterfaceManager(Phone phone) {
+        this.mPhone = phone;
+        IccRecords r = phone.getIccRecords();
+        if (r != null) {
+            mAdnCache = r.getAdnCache();
+        }
+    }
+
+    public void dispose() {
+    }
+
+    public void updateIccRecords(IccRecords iccRecords) {
+        if (iccRecords != null) {
+            mAdnCache = iccRecords.getAdnCache();
+        } else {
+            mAdnCache = null;
+        }
+    }
+
+    protected void logd(String msg) {
+        Rlog.d(LOG_TAG, "[IccPbInterfaceManager] " + msg);
+    }
+
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, "[IccPbInterfaceManager] " + msg);
+    }
+
+    /**
+     * Replace oldAdn with newAdn in ADN-like record in EF
+     *
+     * getAdnRecordsInEf must be called at least once before this function,
+     * otherwise an error will be returned. Currently the email field
+     * if set in the ADN record is ignored.
+     * throws SecurityException if no WRITE_CONTACTS permission
+     *
+     * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN
+     * @param oldTag adn tag to be replaced
+     * @param oldPhoneNumber adn number to be replaced
+     *        Set both oldTag and oldPhoneNubmer to "" means to replace an
+     *        empty record, aka, insert new record
+     * @param newTag adn tag to be stored
+     * @param newPhoneNumber adn number ot be stored
+     *        Set both newTag and newPhoneNubmer to "" means to replace the old
+     *        record with empty one, aka, delete old record
+     * @param pin2 required to update EF_FDN, otherwise must be null
+     * @return true for success
+     */
+    public boolean
+    updateAdnRecordsInEfBySearch (int efid,
+            String oldTag, String oldPhoneNumber,
+            String newTag, String newPhoneNumber, String pin2) {
+
+
+        if (mPhone.getContext().checkCallingOrSelfPermission(
+                android.Manifest.permission.WRITE_CONTACTS)
+            != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "Requires android.permission.WRITE_CONTACTS permission");
+        }
+
+
+        if (DBG) logd("updateAdnRecordsInEfBySearch: efid=0x" +
+                Integer.toHexString(efid).toUpperCase() + " ("+ Rlog.pii(LOG_TAG, oldTag) + "," +
+                Rlog.pii(LOG_TAG, oldPhoneNumber) + ")" + "==>" + " ("+ Rlog.pii(LOG_TAG, newTag) +
+                "," + Rlog.pii(LOG_TAG, newPhoneNumber) + ")"+ " pin2=" + Rlog.pii(LOG_TAG, pin2));
+
+        efid = updateEfForIccType(efid);
+
+        synchronized(mLock) {
+            checkThread();
+            mSuccess = false;
+            AtomicBoolean status = new AtomicBoolean(false);
+            Message response = mBaseHandler.obtainMessage(EVENT_UPDATE_DONE, status);
+            AdnRecord oldAdn = new AdnRecord(oldTag, oldPhoneNumber);
+            AdnRecord newAdn = new AdnRecord(newTag, newPhoneNumber);
+            if (mAdnCache != null) {
+                mAdnCache.updateAdnBySearch(efid, oldAdn, newAdn, pin2, response);
+                waitForResult(status);
+            } else {
+                loge("Failure while trying to update by search due to uninitialised adncache");
+            }
+        }
+        return mSuccess;
+    }
+
+    /**
+     * Update an ADN-like EF record by record index
+     *
+     * This is useful for iteration the whole ADN file, such as write the whole
+     * phone book or erase/format the whole phonebook. Currently the email field
+     * if set in the ADN record is ignored.
+     * throws SecurityException if no WRITE_CONTACTS permission
+     *
+     * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN
+     * @param newTag adn tag to be stored
+     * @param newPhoneNumber adn number to be stored
+     *        Set both newTag and newPhoneNubmer to "" means to replace the old
+     *        record with empty one, aka, delete old record
+     * @param index is 1-based adn record index to be updated
+     * @param pin2 required to update EF_FDN, otherwise must be null
+     * @return true for success
+     */
+    public boolean
+    updateAdnRecordsInEfByIndex(int efid, String newTag,
+            String newPhoneNumber, int index, String pin2) {
+
+        if (mPhone.getContext().checkCallingOrSelfPermission(
+                android.Manifest.permission.WRITE_CONTACTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "Requires android.permission.WRITE_CONTACTS permission");
+        }
+
+        if (DBG) logd("updateAdnRecordsInEfByIndex: efid=0x" +
+                Integer.toHexString(efid).toUpperCase() + " Index=" + index + " ==> " + "(" +
+                Rlog.pii(LOG_TAG, newTag) + "," + Rlog.pii(LOG_TAG, newPhoneNumber) + ")" +
+                " pin2=" + Rlog.pii(LOG_TAG, pin2));
+        synchronized(mLock) {
+            checkThread();
+            mSuccess = false;
+            AtomicBoolean status = new AtomicBoolean(false);
+            Message response = mBaseHandler.obtainMessage(EVENT_UPDATE_DONE, status);
+            AdnRecord newAdn = new AdnRecord(newTag, newPhoneNumber);
+            if (mAdnCache != null) {
+                mAdnCache.updateAdnByIndex(efid, newAdn, index, pin2, response);
+                waitForResult(status);
+            } else {
+                loge("Failure while trying to update by index due to uninitialised adncache");
+            }
+        }
+        return mSuccess;
+    }
+
+    /**
+     * Get the capacity of records in efid
+     *
+     * @param efid the EF id of a ADN-like ICC
+     * @return  int[3] array
+     *            recordSizes[0]  is the single record length
+     *            recordSizes[1]  is the total length of the EF file
+     *            recordSizes[2]  is the number of records in the EF file
+     */
+    public int[] getAdnRecordsSize(int efid) {
+        if (DBG) logd("getAdnRecordsSize: efid=" + efid);
+        synchronized(mLock) {
+            checkThread();
+            mRecordSize = new int[3];
+
+            //Using mBaseHandler, no difference in EVENT_GET_SIZE_DONE handling
+            AtomicBoolean status = new AtomicBoolean(false);
+            Message response = mBaseHandler.obtainMessage(EVENT_GET_SIZE_DONE, status);
+
+            IccFileHandler fh = mPhone.getIccFileHandler();
+            if (fh != null) {
+                fh.getEFLinearRecordSize(efid, response);
+                waitForResult(status);
+            }
+        }
+
+        return mRecordSize;
+    }
+
+
+    /**
+     * Loads the AdnRecords in efid and returns them as a
+     * List of AdnRecords
+     *
+     * throws SecurityException if no READ_CONTACTS permission
+     *
+     * @param efid the EF id of a ADN-like ICC
+     * @return List of AdnRecord
+     */
+    public List<AdnRecord> getAdnRecordsInEf(int efid) {
+
+        if (mPhone.getContext().checkCallingOrSelfPermission(
+                android.Manifest.permission.READ_CONTACTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "Requires android.permission.READ_CONTACTS permission");
+        }
+
+        efid = updateEfForIccType(efid);
+        if (DBG) logd("getAdnRecordsInEF: efid=0x" + Integer.toHexString(efid).toUpperCase());
+
+        synchronized(mLock) {
+            checkThread();
+            AtomicBoolean status = new AtomicBoolean(false);
+            Message response = mBaseHandler.obtainMessage(EVENT_LOAD_DONE, status);
+            if (mAdnCache != null) {
+                mAdnCache.requestLoadAllAdnLike(efid, mAdnCache.extensionEfForEf(efid), response);
+                waitForResult(status);
+            } else {
+                loge("Failure while trying to load from SIM due to uninitialised adncache");
+            }
+        }
+        return mRecords;
+    }
+
+    protected void checkThread() {
+        if (!ALLOW_SIM_OP_IN_UI_THREAD) {
+            // Make sure this isn't the UI thread, since it will block
+            if (mBaseHandler.getLooper().equals(Looper.myLooper())) {
+                loge("query() called on the main UI thread!");
+                throw new IllegalStateException(
+                        "You cannot call query on this provder from the main UI thread.");
+            }
+        }
+    }
+
+    protected void waitForResult(AtomicBoolean status) {
+        while (!status.get()) {
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                logd("interrupted while trying to update by search");
+            }
+        }
+    }
+
+    private int updateEfForIccType(int efid) {
+        // Check if we are trying to read ADN records
+        if (efid == IccConstants.EF_ADN) {
+            if (mPhone.getCurrentUiccAppType() == AppType.APPTYPE_USIM) {
+                return IccConstants.EF_PBR;
+            }
+        }
+        return efid;
+    }
+}
+
diff --git a/com/android/internal/telephony/IccProvider.java b/com/android/internal/telephony/IccProvider.java
new file mode 100644
index 0000000..8feec94
--- /dev/null
+++ b/com/android/internal/telephony/IccProvider.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.ContentProvider;
+import android.content.UriMatcher;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.text.TextUtils;
+import android.telephony.Rlog;
+
+import java.util.List;
+
+import com.android.internal.telephony.uicc.AdnRecord;
+import com.android.internal.telephony.uicc.IccConstants;
+
+
+/**
+ * {@hide}
+ */
+public class IccProvider extends ContentProvider {
+    private static final String TAG = "IccProvider";
+    private static final boolean DBG = true;
+
+
+    private static final String[] ADDRESS_BOOK_COLUMN_NAMES = new String[] {
+        "name",
+        "number",
+        "emails",
+        "_id"
+    };
+
+    protected static final int ADN = 1;
+    protected static final int ADN_SUB = 2;
+    protected static final int FDN = 3;
+    protected static final int FDN_SUB = 4;
+    protected static final int SDN = 5;
+    protected static final int SDN_SUB = 6;
+    protected static final int ADN_ALL = 7;
+
+    protected static final String STR_TAG = "tag";
+    protected static final String STR_NUMBER = "number";
+    protected static final String STR_EMAILS = "emails";
+    protected static final String STR_PIN2 = "pin2";
+
+    private static final UriMatcher URL_MATCHER =
+                            new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        URL_MATCHER.addURI("icc", "adn", ADN);
+        URL_MATCHER.addURI("icc", "adn/subId/#", ADN_SUB);
+        URL_MATCHER.addURI("icc", "fdn", FDN);
+        URL_MATCHER.addURI("icc", "fdn/subId/#", FDN_SUB);
+        URL_MATCHER.addURI("icc", "sdn", SDN);
+        URL_MATCHER.addURI("icc", "sdn/subId/#", SDN_SUB);
+    }
+
+    private SubscriptionManager mSubscriptionManager;
+
+    @Override
+    public boolean onCreate() {
+        mSubscriptionManager = SubscriptionManager.from(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri url, String[] projection, String selection,
+            String[] selectionArgs, String sort) {
+        if (DBG) log("query");
+
+        switch (URL_MATCHER.match(url)) {
+            case ADN:
+                return loadFromEf(IccConstants.EF_ADN,
+                        SubscriptionManager.getDefaultSubscriptionId());
+
+            case ADN_SUB:
+                return loadFromEf(IccConstants.EF_ADN, getRequestSubId(url));
+
+            case FDN:
+                return loadFromEf(IccConstants.EF_FDN,
+                        SubscriptionManager.getDefaultSubscriptionId());
+
+            case FDN_SUB:
+                return loadFromEf(IccConstants.EF_FDN, getRequestSubId(url));
+
+            case SDN:
+                return loadFromEf(IccConstants.EF_SDN,
+                        SubscriptionManager.getDefaultSubscriptionId());
+
+            case SDN_SUB:
+                return loadFromEf(IccConstants.EF_SDN, getRequestSubId(url));
+
+            case ADN_ALL:
+                return loadAllSimContacts(IccConstants.EF_ADN);
+
+            default:
+                throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+
+    private Cursor loadAllSimContacts(int efType) {
+        Cursor [] result;
+        List<SubscriptionInfo> subInfoList = mSubscriptionManager.getActiveSubscriptionInfoList();
+
+        if ((subInfoList == null) || (subInfoList.size() == 0)) {
+            result = new Cursor[0];
+        } else {
+            int subIdCount = subInfoList.size();
+            result = new Cursor[subIdCount];
+            int subId;
+
+            for (int i = 0; i < subIdCount; i++) {
+                subId = subInfoList.get(i).getSubscriptionId();
+                result[i] = loadFromEf(efType, subId);
+                Rlog.i(TAG,"ADN Records loaded for Subscription ::" + subId);
+            }
+        }
+
+        return new MergeCursor(result);
+    }
+
+    @Override
+    public String getType(Uri url) {
+        switch (URL_MATCHER.match(url)) {
+            case ADN:
+            case ADN_SUB:
+            case FDN:
+            case FDN_SUB:
+            case SDN:
+            case SDN_SUB:
+            case ADN_ALL:
+                return "vnd.android.cursor.dir/sim-contact";
+
+            default:
+                throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri url, ContentValues initialValues) {
+        Uri resultUri;
+        int efType;
+        String pin2 = null;
+        int subId;
+
+        if (DBG) log("insert");
+
+        int match = URL_MATCHER.match(url);
+        switch (match) {
+            case ADN:
+                efType = IccConstants.EF_ADN;
+                subId = SubscriptionManager.getDefaultSubscriptionId();
+                break;
+
+            case ADN_SUB:
+                efType = IccConstants.EF_ADN;
+                subId = getRequestSubId(url);
+                break;
+
+            case FDN:
+                efType = IccConstants.EF_FDN;
+                subId = SubscriptionManager.getDefaultSubscriptionId();
+                pin2 = initialValues.getAsString("pin2");
+                break;
+
+            case FDN_SUB:
+                efType = IccConstants.EF_FDN;
+                subId = getRequestSubId(url);
+                pin2 = initialValues.getAsString("pin2");
+                break;
+
+            default:
+                throw new UnsupportedOperationException(
+                        "Cannot insert into URL: " + url);
+        }
+
+        String tag = initialValues.getAsString("tag");
+        String number = initialValues.getAsString("number");
+        // TODO(): Read email instead of sending null.
+        boolean success = addIccRecordToEf(efType, tag, number, null, pin2, subId);
+
+        if (!success) {
+            return null;
+        }
+
+        StringBuilder buf = new StringBuilder("content://icc/");
+        switch (match) {
+            case ADN:
+                buf.append("adn/");
+                break;
+
+            case ADN_SUB:
+                buf.append("adn/subId/");
+                break;
+
+            case FDN:
+                buf.append("fdn/");
+                break;
+
+            case FDN_SUB:
+                buf.append("fdn/subId/");
+                break;
+        }
+
+        // TODO: we need to find out the rowId for the newly added record
+        buf.append(0);
+
+        resultUri = Uri.parse(buf.toString());
+
+        getContext().getContentResolver().notifyChange(url, null);
+        /*
+        // notify interested parties that an insertion happened
+        getContext().getContentResolver().notifyInsert(
+                resultUri, rowID, null);
+        */
+
+        return resultUri;
+    }
+
+    private String normalizeValue(String inVal) {
+        int len = inVal.length();
+        // If name is empty in contact return null to avoid crash.
+        if (len == 0) {
+            if (DBG) log("len of input String is 0");
+            return inVal;
+        }
+        String retVal = inVal;
+
+        if (inVal.charAt(0) == '\'' && inVal.charAt(len-1) == '\'') {
+            retVal = inVal.substring(1, len-1);
+        }
+
+        return retVal;
+    }
+
+    @Override
+    public int delete(Uri url, String where, String[] whereArgs) {
+        int efType;
+        int subId;
+
+        int match = URL_MATCHER.match(url);
+        switch (match) {
+            case ADN:
+                efType = IccConstants.EF_ADN;
+                subId = SubscriptionManager.getDefaultSubscriptionId();
+                break;
+
+            case ADN_SUB:
+                efType = IccConstants.EF_ADN;
+                subId = getRequestSubId(url);
+                break;
+
+            case FDN:
+                efType = IccConstants.EF_FDN;
+                subId = SubscriptionManager.getDefaultSubscriptionId();
+                break;
+
+            case FDN_SUB:
+                efType = IccConstants.EF_FDN;
+                subId = getRequestSubId(url);
+                break;
+
+            default:
+                throw new UnsupportedOperationException(
+                        "Cannot insert into URL: " + url);
+        }
+
+        if (DBG) log("delete");
+
+        // parse where clause
+        String tag = null;
+        String number = null;
+        String[] emails = null;
+        String pin2 = null;
+
+        String[] tokens = where.split("AND");
+        int n = tokens.length;
+
+        while (--n >= 0) {
+            String param = tokens[n];
+            if (DBG) log("parsing '" + param + "'");
+
+            String[] pair = param.split("=", 2);
+
+            if (pair.length != 2) {
+                Rlog.e(TAG, "resolve: bad whereClause parameter: " + param);
+                continue;
+            }
+            String key = pair[0].trim();
+            String val = pair[1].trim();
+
+            if (STR_TAG.equals(key)) {
+                tag = normalizeValue(val);
+            } else if (STR_NUMBER.equals(key)) {
+                number = normalizeValue(val);
+            } else if (STR_EMAILS.equals(key)) {
+                //TODO(): Email is null.
+                emails = null;
+            } else if (STR_PIN2.equals(key)) {
+                pin2 = normalizeValue(val);
+            }
+        }
+
+        if (efType == FDN && TextUtils.isEmpty(pin2)) {
+            return 0;
+        }
+
+        boolean success = deleteIccRecordFromEf(efType, tag, number, emails, pin2, subId);
+        if (!success) {
+            return 0;
+        }
+
+        getContext().getContentResolver().notifyChange(url, null);
+        return 1;
+    }
+
+    @Override
+    public int update(Uri url, ContentValues values, String where, String[] whereArgs) {
+        String pin2 = null;
+        int efType;
+        int subId;
+
+        if (DBG) log("update");
+
+        int match = URL_MATCHER.match(url);
+        switch (match) {
+            case ADN:
+                efType = IccConstants.EF_ADN;
+                subId = SubscriptionManager.getDefaultSubscriptionId();
+                break;
+
+            case ADN_SUB:
+                efType = IccConstants.EF_ADN;
+                subId = getRequestSubId(url);
+                break;
+
+            case FDN:
+                efType = IccConstants.EF_FDN;
+                subId = SubscriptionManager.getDefaultSubscriptionId();
+                pin2 = values.getAsString("pin2");
+                break;
+
+            case FDN_SUB:
+                efType = IccConstants.EF_FDN;
+                subId = getRequestSubId(url);
+                pin2 = values.getAsString("pin2");
+                break;
+
+            default:
+                throw new UnsupportedOperationException(
+                        "Cannot insert into URL: " + url);
+        }
+
+        String tag = values.getAsString("tag");
+        String number = values.getAsString("number");
+        String[] emails = null;
+        String newTag = values.getAsString("newTag");
+        String newNumber = values.getAsString("newNumber");
+        String[] newEmails = null;
+        // TODO(): Update for email.
+        boolean success = updateIccRecordInEf(efType, tag, number,
+                newTag, newNumber, pin2, subId);
+
+        if (!success) {
+            return 0;
+        }
+
+        getContext().getContentResolver().notifyChange(url, null);
+        return 1;
+    }
+
+    private MatrixCursor loadFromEf(int efType, int subId) {
+        if (DBG) log("loadFromEf: efType=0x" +
+                Integer.toHexString(efType).toUpperCase() + ", subscription=" + subId);
+
+        List<AdnRecord> adnRecords = null;
+        try {
+            IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface(
+                    ServiceManager.getService("simphonebook"));
+            if (iccIpb != null) {
+                adnRecords = iccIpb.getAdnRecordsInEfForSubscriber(subId, efType);
+            }
+        } catch (RemoteException ex) {
+            // ignore it
+        } catch (SecurityException ex) {
+            if (DBG) log(ex.toString());
+        }
+
+        if (adnRecords != null) {
+            // Load the results
+            final int N = adnRecords.size();
+            final MatrixCursor cursor = new MatrixCursor(ADDRESS_BOOK_COLUMN_NAMES, N);
+            if (DBG) log("adnRecords.size=" + N);
+            for (int i = 0; i < N ; i++) {
+                loadRecord(adnRecords.get(i), cursor, i);
+            }
+            return cursor;
+        } else {
+            // No results to load
+            Rlog.w(TAG, "Cannot load ADN records");
+            return new MatrixCursor(ADDRESS_BOOK_COLUMN_NAMES);
+        }
+    }
+
+    private boolean
+    addIccRecordToEf(int efType, String name, String number, String[] emails,
+            String pin2, int subId) {
+        if (DBG) log("addIccRecordToEf: efType=0x" + Integer.toHexString(efType).toUpperCase() +
+                ", name=" + Rlog.pii(TAG, name) + ", number=" + Rlog.pii(TAG, number) +
+                ", emails=" + Rlog.pii(TAG, emails) + ", subscription=" + subId);
+
+        boolean success = false;
+
+        // TODO: do we need to call getAdnRecordsInEf() before calling
+        // updateAdnRecordsInEfBySearch()? In any case, we will leave
+        // the UI level logic to fill that prereq if necessary. But
+        // hopefully, we can remove this requirement.
+
+        try {
+            IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface(
+                    ServiceManager.getService("simphonebook"));
+            if (iccIpb != null) {
+                success = iccIpb.updateAdnRecordsInEfBySearchForSubscriber(subId, efType,
+                        "", "", name, number, pin2);
+            }
+        } catch (RemoteException ex) {
+            // ignore it
+        } catch (SecurityException ex) {
+            if (DBG) log(ex.toString());
+        }
+        if (DBG) log("addIccRecordToEf: " + success);
+        return success;
+    }
+
+    private boolean
+    updateIccRecordInEf(int efType, String oldName, String oldNumber,
+            String newName, String newNumber, String pin2, int subId) {
+        if (DBG) log("updateIccRecordInEf: efType=0x" + Integer.toHexString(efType).toUpperCase() +
+                ", oldname=" + Rlog.pii(TAG, oldName) + ", oldnumber=" + Rlog.pii(TAG, oldNumber) +
+                ", newname=" + Rlog.pii(TAG, newName) + ", newnumber=" + Rlog.pii(TAG, newName) +
+                ", subscription=" + subId);
+
+        boolean success = false;
+
+        try {
+            IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface(
+                    ServiceManager.getService("simphonebook"));
+            if (iccIpb != null) {
+                success = iccIpb.updateAdnRecordsInEfBySearchForSubscriber(subId, efType, oldName,
+                        oldNumber, newName, newNumber, pin2);
+            }
+        } catch (RemoteException ex) {
+            // ignore it
+        } catch (SecurityException ex) {
+            if (DBG) log(ex.toString());
+        }
+        if (DBG) log("updateIccRecordInEf: " + success);
+        return success;
+    }
+
+
+    private boolean deleteIccRecordFromEf(int efType, String name, String number, String[] emails,
+            String pin2, int subId) {
+        if (DBG) log("deleteIccRecordFromEf: efType=0x" +
+                Integer.toHexString(efType).toUpperCase() + ", name=" + Rlog.pii(TAG, name) +
+                ", number=" + Rlog.pii(TAG, number) + ", emails=" + Rlog.pii(TAG, emails) +
+                ", pin2=" + Rlog.pii(TAG, pin2) + ", subscription=" + subId);
+
+        boolean success = false;
+
+        try {
+            IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface(
+                    ServiceManager.getService("simphonebook"));
+            if (iccIpb != null) {
+                success = iccIpb.updateAdnRecordsInEfBySearchForSubscriber(subId, efType,
+                          name, number, "", "", pin2);
+            }
+        } catch (RemoteException ex) {
+            // ignore it
+        } catch (SecurityException ex) {
+            if (DBG) log(ex.toString());
+        }
+        if (DBG) log("deleteIccRecordFromEf: " + success);
+        return success;
+    }
+
+    /**
+     * Loads an AdnRecord into a MatrixCursor. Must be called with mLock held.
+     *
+     * @param record the ADN record to load from
+     * @param cursor the cursor to receive the results
+     */
+    private void loadRecord(AdnRecord record, MatrixCursor cursor, int id) {
+        if (!record.isEmpty()) {
+            Object[] contact = new Object[4];
+            String alphaTag = record.getAlphaTag();
+            String number = record.getNumber();
+
+            if (DBG) log("loadRecord: " + alphaTag + ", " + Rlog.pii(TAG, number));
+            contact[0] = alphaTag;
+            contact[1] = number;
+
+            String[] emails = record.getEmails();
+            if (emails != null) {
+                StringBuilder emailString = new StringBuilder();
+                for (String email: emails) {
+                    log("Adding email:" + Rlog.pii(TAG, email));
+                    emailString.append(email);
+                    emailString.append(",");
+                }
+                contact[2] = emailString.toString();
+            }
+            contact[3] = id;
+            cursor.addRow(contact);
+        }
+    }
+
+    private void log(String msg) {
+        Rlog.d(TAG, "[IccProvider] " + msg);
+    }
+
+    private int getRequestSubId(Uri url) {
+        if (DBG) log("getRequestSubId url: " + url);
+
+        try {
+            return Integer.parseInt(url.getLastPathSegment());
+        } catch (NumberFormatException ex) {
+            throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/IccSmsInterfaceManager.java b/com/android/internal/telephony/IccSmsInterfaceManager.java
new file mode 100644
index 0000000..0fc08c6
--- /dev/null
+++ b/com/android/internal/telephony/IccSmsInterfaceManager.java
@@ -0,0 +1,1157 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony;
+
+import static android.telephony.SmsManager.STATUS_ON_ICC_FREE;
+import static android.telephony.SmsManager.STATUS_ON_ICC_READ;
+import static android.telephony.SmsManager.STATUS_ON_ICC_UNREAD;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserManager;
+import android.provider.Telephony;
+import android.service.carrier.CarrierMessagingService;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+import com.android.internal.telephony.uicc.IccConstants;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.util.HexDump;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * IccSmsInterfaceManager to provide an inter-process communication to
+ * access Sms in Icc.
+ */
+public class IccSmsInterfaceManager {
+    static final String LOG_TAG = "IccSmsInterfaceManager";
+    static final boolean DBG = true;
+
+    protected final Object mLock = new Object();
+    protected boolean mSuccess;
+    private List<SmsRawData> mSms;
+
+    private CellBroadcastRangeManager mCellBroadcastRangeManager =
+            new CellBroadcastRangeManager();
+    private CdmaBroadcastRangeManager mCdmaBroadcastRangeManager =
+            new CdmaBroadcastRangeManager();
+
+    private static final int EVENT_LOAD_DONE = 1;
+    private static final int EVENT_UPDATE_DONE = 2;
+    protected static final int EVENT_SET_BROADCAST_ACTIVATION_DONE = 3;
+    protected static final int EVENT_SET_BROADCAST_CONFIG_DONE = 4;
+    private static final int SMS_CB_CODE_SCHEME_MIN = 0;
+    private static final int SMS_CB_CODE_SCHEME_MAX = 255;
+
+    protected Phone mPhone;
+    final protected Context mContext;
+    final protected AppOpsManager mAppOps;
+    final private UserManager mUserManager;
+    protected SMSDispatcher mDispatcher;
+
+    protected Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            AsyncResult ar;
+
+            switch (msg.what) {
+                case EVENT_UPDATE_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    synchronized (mLock) {
+                        mSuccess = (ar.exception == null);
+                        mLock.notifyAll();
+                    }
+                    break;
+                case EVENT_LOAD_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    synchronized (mLock) {
+                        if (ar.exception == null) {
+                            mSms = buildValidRawData((ArrayList<byte[]>) ar.result);
+                            //Mark SMS as read after importing it from card.
+                            markMessagesAsRead((ArrayList<byte[]>) ar.result);
+                        } else {
+                            if (Rlog.isLoggable("SMS", Log.DEBUG)) {
+                                log("Cannot load Sms records");
+                            }
+                            mSms = null;
+                        }
+                        mLock.notifyAll();
+                    }
+                    break;
+                case EVENT_SET_BROADCAST_ACTIVATION_DONE:
+                case EVENT_SET_BROADCAST_CONFIG_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    synchronized (mLock) {
+                        mSuccess = (ar.exception == null);
+                        mLock.notifyAll();
+                    }
+                    break;
+            }
+        }
+    };
+
+    protected IccSmsInterfaceManager(Phone phone) {
+        mPhone = phone;
+        mContext = phone.getContext();
+        mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mDispatcher = new ImsSMSDispatcher(phone,
+                phone.mSmsStorageMonitor, phone.mSmsUsageMonitor);
+    }
+
+    protected void markMessagesAsRead(ArrayList<byte[]> messages) {
+        if (messages == null) {
+            return;
+        }
+
+        //IccFileHandler can be null, if icc card is absent.
+        IccFileHandler fh = mPhone.getIccFileHandler();
+        if (fh == null) {
+            //shouldn't really happen, as messages are marked as read, only
+            //after importing it from icc.
+            if (Rlog.isLoggable("SMS", Log.DEBUG)) {
+                log("markMessagesAsRead - aborting, no icc card present.");
+            }
+            return;
+        }
+
+        int count = messages.size();
+
+        for (int i = 0; i < count; i++) {
+             byte[] ba = messages.get(i);
+             if (ba[0] == STATUS_ON_ICC_UNREAD) {
+                 int n = ba.length;
+                 byte[] nba = new byte[n - 1];
+                 System.arraycopy(ba, 1, nba, 0, n - 1);
+                 byte[] record = makeSmsRecordData(STATUS_ON_ICC_READ, nba);
+                 fh.updateEFLinearFixed(IccConstants.EF_SMS, i + 1, record, null, null);
+                 if (Rlog.isLoggable("SMS", Log.DEBUG)) {
+                     log("SMS " + (i + 1) + " marked as read");
+                 }
+             }
+        }
+    }
+
+    protected void updatePhoneObject(Phone phone) {
+        mPhone = phone;
+        mDispatcher.updatePhoneObject(phone);
+    }
+
+    protected void enforceReceiveAndSend(String message) {
+        mContext.enforceCallingOrSelfPermission(
+                Manifest.permission.RECEIVE_SMS, message);
+        mContext.enforceCallingOrSelfPermission(
+                Manifest.permission.SEND_SMS, message);
+    }
+
+    /**
+     * Update the specified message on the Icc.
+     *
+     * @param index record index of message to update
+     * @param status new message status (STATUS_ON_ICC_READ,
+     *                  STATUS_ON_ICC_UNREAD, STATUS_ON_ICC_SENT,
+     *                  STATUS_ON_ICC_UNSENT, STATUS_ON_ICC_FREE)
+     * @param pdu the raw PDU to store
+     * @return success or not
+     *
+     */
+
+    public boolean
+    updateMessageOnIccEf(String callingPackage, int index, int status, byte[] pdu) {
+        if (DBG) log("updateMessageOnIccEf: index=" + index +
+                " status=" + status + " ==> " +
+                "("+ Arrays.toString(pdu) + ")");
+        enforceReceiveAndSend("Updating message on Icc");
+        if (mAppOps.noteOp(AppOpsManager.OP_WRITE_ICC_SMS, Binder.getCallingUid(),
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            return false;
+        }
+        synchronized(mLock) {
+            mSuccess = false;
+            Message response = mHandler.obtainMessage(EVENT_UPDATE_DONE);
+
+            if (status == STATUS_ON_ICC_FREE) {
+                // RIL_REQUEST_DELETE_SMS_ON_SIM vs RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM
+                // Special case FREE: call deleteSmsOnSim/Ruim instead of
+                // manipulating the record
+                // Will eventually fail if icc card is not present.
+                if (PhoneConstants.PHONE_TYPE_GSM == mPhone.getPhoneType()) {
+                    mPhone.mCi.deleteSmsOnSim(index, response);
+                } else {
+                    mPhone.mCi.deleteSmsOnRuim(index, response);
+                }
+            } else {
+                //IccFilehandler can be null if ICC card is not present.
+                IccFileHandler fh = mPhone.getIccFileHandler();
+                if (fh == null) {
+                    response.recycle();
+                    return mSuccess; /* is false */
+                }
+                byte[] record = makeSmsRecordData(status, pdu);
+                fh.updateEFLinearFixed(
+                        IccConstants.EF_SMS,
+                        index, record, null, response);
+            }
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to update by index");
+            }
+        }
+        return mSuccess;
+    }
+
+    /**
+     * Copy a raw SMS PDU to the Icc.
+     *
+     * @param pdu the raw PDU to store
+     * @param status message status (STATUS_ON_ICC_READ, STATUS_ON_ICC_UNREAD,
+     *               STATUS_ON_ICC_SENT, STATUS_ON_ICC_UNSENT)
+     * @return success or not
+     *
+     */
+    public boolean copyMessageToIccEf(String callingPackage, int status, byte[] pdu, byte[] smsc) {
+        //NOTE smsc not used in RUIM
+        if (DBG) log("copyMessageToIccEf: status=" + status + " ==> " +
+                "pdu=("+ Arrays.toString(pdu) +
+                "), smsc=(" + Arrays.toString(smsc) +")");
+        enforceReceiveAndSend("Copying message to Icc");
+        if (mAppOps.noteOp(AppOpsManager.OP_WRITE_ICC_SMS, Binder.getCallingUid(),
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            return false;
+        }
+        synchronized(mLock) {
+            mSuccess = false;
+            Message response = mHandler.obtainMessage(EVENT_UPDATE_DONE);
+
+            //RIL_REQUEST_WRITE_SMS_TO_SIM vs RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM
+            if (PhoneConstants.PHONE_TYPE_GSM == mPhone.getPhoneType()) {
+                mPhone.mCi.writeSmsToSim(status, IccUtils.bytesToHexString(smsc),
+                        IccUtils.bytesToHexString(pdu), response);
+            } else {
+                mPhone.mCi.writeSmsToRuim(status, IccUtils.bytesToHexString(pdu),
+                        response);
+            }
+
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to update by index");
+            }
+        }
+        return mSuccess;
+    }
+
+    /**
+     * Retrieves all messages currently stored on Icc.
+     *
+     * @return list of SmsRawData of all sms on Icc
+     */
+
+    public List<SmsRawData> getAllMessagesFromIccEf(String callingPackage) {
+        if (DBG) log("getAllMessagesFromEF");
+
+        mContext.enforceCallingOrSelfPermission(
+                Manifest.permission.RECEIVE_SMS,
+                "Reading messages from Icc");
+        if (mAppOps.noteOp(AppOpsManager.OP_READ_ICC_SMS, Binder.getCallingUid(),
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            return new ArrayList<SmsRawData>();
+        }
+        synchronized(mLock) {
+
+            IccFileHandler fh = mPhone.getIccFileHandler();
+            if (fh == null) {
+                Rlog.e(LOG_TAG, "Cannot load Sms records. No icc card?");
+                mSms = null;
+                return mSms;
+            }
+
+            Message response = mHandler.obtainMessage(EVENT_LOAD_DONE);
+            fh.loadEFLinearFixedAll(IccConstants.EF_SMS, response);
+
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to load from the Icc");
+            }
+        }
+        return mSms;
+    }
+
+    /**
+     * A permissions check before passing to {@link IccSmsInterfaceManager#sendDataInternal}.
+     * This method checks if the calling package or itself has the permission to send the data sms.
+     */
+    public void sendDataWithSelfPermissions(String callingPackage, String destAddr, String scAddr,
+            int destPort, byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        mPhone.getContext().enforceCallingOrSelfPermission(
+                Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        sendDataInternal(callingPackage, destAddr, scAddr, destPort, data, sentIntent,
+                deliveryIntent);
+    }
+
+    /**
+     * A permissions check before passing to {@link IccSmsInterfaceManager#sendDataInternal}.
+     * This method checks only if the calling package has the permission to send the data sms.
+     */
+    public void sendData(String callingPackage, String destAddr, String scAddr, int destPort,
+            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        mPhone.getContext().enforceCallingPermission(
+                Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        sendDataInternal(callingPackage, destAddr, scAddr, destPort, data, sentIntent,
+                deliveryIntent);
+    }
+
+    /**
+     * Send a data based SMS to a specific application port.
+     *
+     * @param destAddr the address to send the message to
+     * @param scAddr is the service center address or null to use
+     *  the current default SMSC
+     * @param destPort the port to deliver the message to
+     * @param data the body of the message to send
+     * @param sentIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is successfully sent, or failed.
+     *  The result code will be <code>Activity.RESULT_OK<code> for success,
+     *  or one of these errors:<br>
+     *  <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
+     *  <code>RESULT_ERROR_RADIO_OFF</code><br>
+     *  <code>RESULT_ERROR_NULL_PDU</code><br>
+     *  For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include
+     *  the extra "errorCode" containing a radio technology specific value,
+     *  generally only useful for troubleshooting.<br>
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * @param deliveryIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is delivered to the recipient.  The
+     *  raw pdu of the status report is in the extended data ("pdu").
+     */
+
+    private void sendDataInternal(String callingPackage, String destAddr, String scAddr,
+            int destPort, byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
+            log("sendData: destAddr=" + destAddr + " scAddr=" + scAddr + " destPort=" +
+                destPort + " data='"+ HexDump.toHexString(data)  + "' sentIntent=" +
+                sentIntent + " deliveryIntent=" + deliveryIntent);
+        }
+        if (mAppOps.noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+        destAddr = filterDestAddress(destAddr);
+        mDispatcher.sendData(destAddr, scAddr, destPort, data, sentIntent, deliveryIntent);
+    }
+
+    /**
+     * A permissions check before passing to {@link IccSmsInterfaceManager#sendTextInternal}.
+     * This method checks only if the calling package has the permission to send the sms.
+     */
+    public void sendText(String callingPackage, String destAddr, String scAddr,
+            String text, PendingIntent sentIntent, PendingIntent deliveryIntent,
+            boolean persistMessageForNonDefaultSmsApp) {
+        mPhone.getContext().enforceCallingPermission(
+                Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        sendTextInternal(callingPackage, destAddr, scAddr, text, sentIntent, deliveryIntent,
+            persistMessageForNonDefaultSmsApp);
+    }
+
+    /**
+     * A permissions check before passing to {@link IccSmsInterfaceManager#sendTextInternal}.
+     * This method checks if the calling package or itself has the permission to send the sms.
+     */
+    public void sendTextWithSelfPermissions(String callingPackage, String destAddr, String scAddr,
+            String text, PendingIntent sentIntent, PendingIntent deliveryIntent,
+            boolean persistMessage) {
+        mPhone.getContext().enforceCallingOrSelfPermission(
+                Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        sendTextInternal(callingPackage, destAddr, scAddr, text, sentIntent, deliveryIntent,
+            persistMessage);
+    }
+
+    /**
+     * Send a text based SMS.
+     *
+     * @param destAddr the address to send the message to
+     * @param scAddr is the service center address or null to use
+     *  the current default SMSC
+     * @param text the body of the message to send
+     * @param sentIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is successfully sent, or failed.
+     *  The result code will be <code>Activity.RESULT_OK<code> for success,
+     *  or one of these errors:<br>
+     *  <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
+     *  <code>RESULT_ERROR_RADIO_OFF</code><br>
+     *  <code>RESULT_ERROR_NULL_PDU</code><br>
+     *  For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include
+     *  the extra "errorCode" containing a radio technology specific value,
+     *  generally only useful for troubleshooting.<br>
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * @param deliveryIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is delivered to the recipient.  The
+     *  raw pdu of the status report is in the extended data ("pdu").
+     */
+
+    private void sendTextInternal(String callingPackage, String destAddr, String scAddr,
+            String text, PendingIntent sentIntent, PendingIntent deliveryIntent,
+            boolean persistMessageForNonDefaultSmsApp) {
+        if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
+            log("sendText: destAddr=" + destAddr + " scAddr=" + scAddr +
+                " text='"+ text + "' sentIntent=" +
+                sentIntent + " deliveryIntent=" + deliveryIntent);
+        }
+        if (mAppOps.noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+        if (!persistMessageForNonDefaultSmsApp) {
+            enforcePrivilegedAppPermissions();
+        }
+        destAddr = filterDestAddress(destAddr);
+        mDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent,
+                null/*messageUri*/, callingPackage, persistMessageForNonDefaultSmsApp);
+    }
+
+    /**
+     * Inject an SMS PDU into the android application framework.
+     *
+     * @param pdu is the byte array of pdu to be injected into android application framework
+     * @param format is the format of SMS pdu (3gpp or 3gpp2)
+     * @param receivedIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is successfully received by the
+     *  android application framework. This intent is broadcasted at
+     *  the same time an SMS received from radio is acknowledged back.
+     */
+    public void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
+        enforcePrivilegedAppPermissions();
+        if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
+            log("pdu: " + pdu +
+                "\n format=" + format +
+                "\n receivedIntent=" + receivedIntent);
+        }
+        mDispatcher.injectSmsPdu(pdu, format, receivedIntent);
+    }
+
+    /**
+     * Send a multi-part text based SMS.
+     *
+     * @param destAddr the address to send the message to
+     * @param scAddr is the service center address or null to use
+     *   the current default SMSC
+     * @param parts an <code>ArrayList</code> of strings that, in order,
+     *   comprise the original message
+     * @param sentIntents if not null, an <code>ArrayList</code> of
+     *   <code>PendingIntent</code>s (one for each message part) that is
+     *   broadcast when the corresponding message part has been sent.
+     *   The result code will be <code>Activity.RESULT_OK<code> for success,
+     *   or one of these errors:
+     *   <code>RESULT_ERROR_GENERIC_FAILURE</code>
+     *   <code>RESULT_ERROR_RADIO_OFF</code>
+     *   <code>RESULT_ERROR_NULL_PDU</code>.
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * @param deliveryIntents if not null, an <code>ArrayList</code> of
+     *   <code>PendingIntent</code>s (one for each message part) that is
+     *   broadcast when the corresponding message part has been delivered
+     *   to the recipient.  The raw pdu of the status report is in the
+     *   extended data ("pdu").
+     */
+
+    public void sendMultipartText(String callingPackage, String destAddr, String scAddr,
+            List<String> parts, List<PendingIntent> sentIntents,
+            List<PendingIntent> deliveryIntents, boolean persistMessageForNonDefaultSmsApp) {
+        mPhone.getContext().enforceCallingPermission(
+                Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        if (!persistMessageForNonDefaultSmsApp) {
+            // Only allow carrier app or carrier ims to skip auto message persistence.
+            enforcePrivilegedAppPermissions();
+        }
+        if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
+            int i = 0;
+            for (String part : parts) {
+                log("sendMultipartText: destAddr=" + destAddr + ", srAddr=" + scAddr +
+                        ", part[" + (i++) + "]=" + part);
+            }
+        }
+        if (mAppOps.noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+
+        destAddr = filterDestAddress(destAddr);
+
+        if (parts.size() > 1 && parts.size() < 10 && !SmsMessage.hasEmsSupport()) {
+            for (int i = 0; i < parts.size(); i++) {
+                // If EMS is not supported, we have to break down EMS into single segment SMS
+                // and add page info " x/y".
+                String singlePart = parts.get(i);
+                if (SmsMessage.shouldAppendPageNumberAsPrefix()) {
+                    singlePart = String.valueOf(i + 1) + '/' + parts.size() + ' ' + singlePart;
+                } else {
+                    singlePart = singlePart.concat(' ' + String.valueOf(i + 1) + '/' + parts.size());
+                }
+
+                PendingIntent singleSentIntent = null;
+                if (sentIntents != null && sentIntents.size() > i) {
+                    singleSentIntent = sentIntents.get(i);
+                }
+
+                PendingIntent singleDeliveryIntent = null;
+                if (deliveryIntents != null && deliveryIntents.size() > i) {
+                    singleDeliveryIntent = deliveryIntents.get(i);
+                }
+
+                mDispatcher.sendText(destAddr, scAddr, singlePart,
+                        singleSentIntent, singleDeliveryIntent,
+                        null/*messageUri*/, callingPackage,
+                        persistMessageForNonDefaultSmsApp);
+            }
+            return;
+        }
+
+        mDispatcher.sendMultipartText(destAddr, scAddr, (ArrayList<String>) parts,
+                (ArrayList<PendingIntent>) sentIntents, (ArrayList<PendingIntent>) deliveryIntents,
+                null/*messageUri*/, callingPackage, persistMessageForNonDefaultSmsApp);
+    }
+
+
+    public int getPremiumSmsPermission(String packageName) {
+        return mDispatcher.getPremiumSmsPermission(packageName);
+    }
+
+
+    public void setPremiumSmsPermission(String packageName, int permission) {
+        mDispatcher.setPremiumSmsPermission(packageName, permission);
+    }
+
+    /**
+     * create SmsRawData lists from all sms record byte[]
+     * Use null to indicate "free" record
+     *
+     * @param messages List of message records from EF_SMS.
+     * @return SmsRawData list of all in-used records
+     */
+    protected ArrayList<SmsRawData> buildValidRawData(ArrayList<byte[]> messages) {
+        int count = messages.size();
+        ArrayList<SmsRawData> ret;
+
+        ret = new ArrayList<SmsRawData>(count);
+
+        for (int i = 0; i < count; i++) {
+            byte[] ba = messages.get(i);
+            if (ba[0] == STATUS_ON_ICC_FREE) {
+                ret.add(null);
+            } else {
+                ret.add(new SmsRawData(messages.get(i)));
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Generates an EF_SMS record from status and raw PDU.
+     *
+     * @param status Message status.  See TS 51.011 10.5.3.
+     * @param pdu Raw message PDU.
+     * @return byte array for the record.
+     */
+    protected byte[] makeSmsRecordData(int status, byte[] pdu) {
+        byte[] data;
+        if (PhoneConstants.PHONE_TYPE_GSM == mPhone.getPhoneType()) {
+            data = new byte[SmsManager.SMS_RECORD_LENGTH];
+        } else {
+            data = new byte[SmsManager.CDMA_SMS_RECORD_LENGTH];
+        }
+
+        // Status bits for this record.  See TS 51.011 10.5.3
+        data[0] = (byte)(status & 7);
+
+        System.arraycopy(pdu, 0, data, 1, pdu.length);
+
+        // Pad out with 0xFF's.
+        for (int j = pdu.length+1; j < data.length; j++) {
+            data[j] = -1;
+        }
+
+        return data;
+    }
+
+    public boolean enableCellBroadcast(int messageIdentifier, int ranType) {
+        return enableCellBroadcastRange(messageIdentifier, messageIdentifier, ranType);
+    }
+
+    public boolean disableCellBroadcast(int messageIdentifier, int ranType) {
+        return disableCellBroadcastRange(messageIdentifier, messageIdentifier, ranType);
+    }
+
+    public boolean enableCellBroadcastRange(int startMessageId, int endMessageId, int ranType) {
+        if (ranType == SmsManager.CELL_BROADCAST_RAN_TYPE_GSM) {
+            return enableGsmBroadcastRange(startMessageId, endMessageId);
+        } else if (ranType == SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA) {
+            return enableCdmaBroadcastRange(startMessageId, endMessageId);
+        } else {
+            throw new IllegalArgumentException("Not a supportted RAN Type");
+        }
+    }
+
+    public boolean disableCellBroadcastRange(int startMessageId, int endMessageId, int ranType) {
+        if (ranType == SmsManager.CELL_BROADCAST_RAN_TYPE_GSM ) {
+            return disableGsmBroadcastRange(startMessageId, endMessageId);
+        } else if (ranType == SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA)  {
+            return disableCdmaBroadcastRange(startMessageId, endMessageId);
+        } else {
+            throw new IllegalArgumentException("Not a supportted RAN Type");
+        }
+    }
+
+    synchronized public boolean enableGsmBroadcastRange(int startMessageId, int endMessageId) {
+
+        Context context = mPhone.getContext();
+
+        context.enforceCallingPermission(
+                "android.permission.RECEIVE_SMS",
+                "Enabling cell broadcast SMS");
+
+        String client = context.getPackageManager().getNameForUid(
+                Binder.getCallingUid());
+
+        if (!mCellBroadcastRangeManager.enableRange(startMessageId, endMessageId, client)) {
+            log("Failed to add GSM cell broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+            return false;
+        }
+
+        if (DBG)
+            log("Added GSM cell broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+
+        setCellBroadcastActivation(!mCellBroadcastRangeManager.isEmpty());
+
+        return true;
+    }
+
+    synchronized public boolean disableGsmBroadcastRange(int startMessageId, int endMessageId) {
+
+        Context context = mPhone.getContext();
+
+        context.enforceCallingPermission(
+                "android.permission.RECEIVE_SMS",
+                "Disabling cell broadcast SMS");
+
+        String client = context.getPackageManager().getNameForUid(
+                Binder.getCallingUid());
+
+        if (!mCellBroadcastRangeManager.disableRange(startMessageId, endMessageId, client)) {
+            log("Failed to remove GSM cell broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+            return false;
+        }
+
+        if (DBG)
+            log("Removed GSM cell broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+
+        setCellBroadcastActivation(!mCellBroadcastRangeManager.isEmpty());
+
+        return true;
+    }
+
+    synchronized public boolean enableCdmaBroadcastRange(int startMessageId, int endMessageId) {
+
+        Context context = mPhone.getContext();
+
+        context.enforceCallingPermission(
+                "android.permission.RECEIVE_SMS",
+                "Enabling cdma broadcast SMS");
+
+        String client = context.getPackageManager().getNameForUid(
+                Binder.getCallingUid());
+
+        if (!mCdmaBroadcastRangeManager.enableRange(startMessageId, endMessageId, client)) {
+            log("Failed to add cdma broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+            return false;
+        }
+
+        if (DBG)
+            log("Added cdma broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+
+        setCdmaBroadcastActivation(!mCdmaBroadcastRangeManager.isEmpty());
+
+        return true;
+    }
+
+    synchronized public boolean disableCdmaBroadcastRange(int startMessageId, int endMessageId) {
+
+        Context context = mPhone.getContext();
+
+        context.enforceCallingPermission(
+                "android.permission.RECEIVE_SMS",
+                "Disabling cell broadcast SMS");
+
+        String client = context.getPackageManager().getNameForUid(
+                Binder.getCallingUid());
+
+        if (!mCdmaBroadcastRangeManager.disableRange(startMessageId, endMessageId, client)) {
+            log("Failed to remove cdma broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+            return false;
+        }
+
+        if (DBG)
+            log("Removed cdma broadcast subscription for MID range " + startMessageId
+                    + " to " + endMessageId + " from client " + client);
+
+        setCdmaBroadcastActivation(!mCdmaBroadcastRangeManager.isEmpty());
+
+        return true;
+    }
+
+    class CellBroadcastRangeManager extends IntRangeManager {
+        private ArrayList<SmsBroadcastConfigInfo> mConfigList =
+                new ArrayList<SmsBroadcastConfigInfo>();
+
+        /**
+         * Called when the list of enabled ranges has changed. This will be
+         * followed by zero or more calls to {@link #addRange} followed by
+         * a call to {@link #finishUpdate}.
+         */
+        protected void startUpdate() {
+            mConfigList.clear();
+        }
+
+        /**
+         * Called after {@link #startUpdate} to indicate a range of enabled
+         * values.
+         * @param startId the first id included in the range
+         * @param endId the last id included in the range
+         */
+        protected void addRange(int startId, int endId, boolean selected) {
+            mConfigList.add(new SmsBroadcastConfigInfo(startId, endId,
+                        SMS_CB_CODE_SCHEME_MIN, SMS_CB_CODE_SCHEME_MAX, selected));
+        }
+
+        /**
+         * Called to indicate the end of a range update started by the
+         * previous call to {@link #startUpdate}.
+         * @return true if successful, false otherwise
+         */
+        protected boolean finishUpdate() {
+            if (mConfigList.isEmpty()) {
+                return true;
+            } else {
+                SmsBroadcastConfigInfo[] configs =
+                        mConfigList.toArray(new SmsBroadcastConfigInfo[mConfigList.size()]);
+                return setCellBroadcastConfig(configs);
+            }
+        }
+    }
+
+    class CdmaBroadcastRangeManager extends IntRangeManager {
+        private ArrayList<CdmaSmsBroadcastConfigInfo> mConfigList =
+                new ArrayList<CdmaSmsBroadcastConfigInfo>();
+
+        /**
+         * Called when the list of enabled ranges has changed. This will be
+         * followed by zero or more calls to {@link #addRange} followed by a
+         * call to {@link #finishUpdate}.
+         */
+        protected void startUpdate() {
+            mConfigList.clear();
+        }
+
+        /**
+         * Called after {@link #startUpdate} to indicate a range of enabled
+         * values.
+         * @param startId the first id included in the range
+         * @param endId the last id included in the range
+         */
+        protected void addRange(int startId, int endId, boolean selected) {
+            mConfigList.add(new CdmaSmsBroadcastConfigInfo(startId, endId,
+                    1, selected));
+        }
+
+        /**
+         * Called to indicate the end of a range update started by the previous
+         * call to {@link #startUpdate}.
+         * @return true if successful, false otherwise
+         */
+        protected boolean finishUpdate() {
+            if (mConfigList.isEmpty()) {
+                return true;
+            } else {
+                CdmaSmsBroadcastConfigInfo[] configs =
+                        mConfigList.toArray(new CdmaSmsBroadcastConfigInfo[mConfigList.size()]);
+                return setCdmaBroadcastConfig(configs);
+            }
+        }
+    }
+
+    private boolean setCellBroadcastConfig(SmsBroadcastConfigInfo[] configs) {
+        if (DBG)
+            log("Calling setGsmBroadcastConfig with " + configs.length + " configurations");
+
+        synchronized (mLock) {
+            Message response = mHandler.obtainMessage(EVENT_SET_BROADCAST_CONFIG_DONE);
+
+            mSuccess = false;
+            mPhone.mCi.setGsmBroadcastConfig(configs, response);
+
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to set cell broadcast config");
+            }
+        }
+
+        return mSuccess;
+    }
+
+    private boolean setCellBroadcastActivation(boolean activate) {
+        if (DBG)
+            log("Calling setCellBroadcastActivation(" + activate + ')');
+
+        synchronized (mLock) {
+            Message response = mHandler.obtainMessage(EVENT_SET_BROADCAST_ACTIVATION_DONE);
+
+            mSuccess = false;
+            mPhone.mCi.setGsmBroadcastActivation(activate, response);
+
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to set cell broadcast activation");
+            }
+        }
+
+        return mSuccess;
+    }
+
+    private boolean setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs) {
+        if (DBG)
+            log("Calling setCdmaBroadcastConfig with " + configs.length + " configurations");
+
+        synchronized (mLock) {
+            Message response = mHandler.obtainMessage(EVENT_SET_BROADCAST_CONFIG_DONE);
+
+            mSuccess = false;
+            mPhone.mCi.setCdmaBroadcastConfig(configs, response);
+
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to set cdma broadcast config");
+            }
+        }
+
+        return mSuccess;
+    }
+
+    private boolean setCdmaBroadcastActivation(boolean activate) {
+        if (DBG)
+            log("Calling setCdmaBroadcastActivation(" + activate + ")");
+
+        synchronized (mLock) {
+            Message response = mHandler.obtainMessage(EVENT_SET_BROADCAST_ACTIVATION_DONE);
+
+            mSuccess = false;
+            mPhone.mCi.setCdmaBroadcastActivation(activate, response);
+
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                log("interrupted while trying to set cdma broadcast activation");
+            }
+        }
+
+        return mSuccess;
+    }
+
+    protected void log(String msg) {
+        Log.d(LOG_TAG, "[IccSmsInterfaceManager] " + msg);
+    }
+
+    public boolean isImsSmsSupported() {
+        return mDispatcher.isIms();
+    }
+
+    public String getImsSmsFormat() {
+        return mDispatcher.getImsSmsFormat();
+    }
+
+    public void sendStoredText(String callingPkg, Uri messageUri, String scAddress,
+            PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        mPhone.getContext().enforceCallingPermission(Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
+            log("sendStoredText: scAddr=" + scAddress + " messageUri=" + messageUri
+                    + " sentIntent=" + sentIntent + " deliveryIntent=" + deliveryIntent);
+        }
+        if (mAppOps.noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(), callingPkg)
+                != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+        final ContentResolver resolver = mPhone.getContext().getContentResolver();
+        if (!isFailedOrDraft(resolver, messageUri)) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]sendStoredText: not FAILED or DRAFT message");
+            returnUnspecifiedFailure(sentIntent);
+            return;
+        }
+        final String[] textAndAddress = loadTextAndAddress(resolver, messageUri);
+        if (textAndAddress == null) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]sendStoredText: can not load text");
+            returnUnspecifiedFailure(sentIntent);
+            return;
+        }
+        textAndAddress[1] = filterDestAddress(textAndAddress[1]);
+        mDispatcher.sendText(textAndAddress[1], scAddress, textAndAddress[0],
+                sentIntent, deliveryIntent, messageUri, callingPkg,
+                true /* persistMessageForNonDefaultSmsApp */);
+    }
+
+    public void sendStoredMultipartText(String callingPkg, Uri messageUri, String scAddress,
+            List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents) {
+        mPhone.getContext().enforceCallingPermission(Manifest.permission.SEND_SMS,
+                "Sending SMS message");
+        if (mAppOps.noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(), callingPkg)
+                != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+        final ContentResolver resolver = mPhone.getContext().getContentResolver();
+        if (!isFailedOrDraft(resolver, messageUri)) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]sendStoredMultipartText: "
+                    + "not FAILED or DRAFT message");
+            returnUnspecifiedFailure(sentIntents);
+            return;
+        }
+        final String[] textAndAddress = loadTextAndAddress(resolver, messageUri);
+        if (textAndAddress == null) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]sendStoredMultipartText: can not load text");
+            returnUnspecifiedFailure(sentIntents);
+            return;
+        }
+        final ArrayList<String> parts = SmsManager.getDefault().divideMessage(textAndAddress[0]);
+        if (parts == null || parts.size() < 1) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]sendStoredMultipartText: can not divide text");
+            returnUnspecifiedFailure(sentIntents);
+            return;
+        }
+
+        textAndAddress[1] = filterDestAddress(textAndAddress[1]);
+
+        if (parts.size() > 1 && parts.size() < 10 && !SmsMessage.hasEmsSupport()) {
+            for (int i = 0; i < parts.size(); i++) {
+                // If EMS is not supported, we have to break down EMS into single segment SMS
+                // and add page info " x/y".
+                String singlePart = parts.get(i);
+                if (SmsMessage.shouldAppendPageNumberAsPrefix()) {
+                    singlePart = String.valueOf(i + 1) + '/' + parts.size() + ' ' + singlePart;
+                } else {
+                    singlePart = singlePart.concat(' ' + String.valueOf(i + 1) + '/' + parts.size());
+                }
+
+                PendingIntent singleSentIntent = null;
+                if (sentIntents != null && sentIntents.size() > i) {
+                    singleSentIntent = sentIntents.get(i);
+                }
+
+                PendingIntent singleDeliveryIntent = null;
+                if (deliveryIntents != null && deliveryIntents.size() > i) {
+                    singleDeliveryIntent = deliveryIntents.get(i);
+                }
+
+                mDispatcher.sendText(textAndAddress[1], scAddress, singlePart,
+                        singleSentIntent, singleDeliveryIntent, messageUri, callingPkg,
+                        true  /* persistMessageForNonDefaultSmsApp */);
+            }
+            return;
+        }
+
+        mDispatcher.sendMultipartText(
+                textAndAddress[1], // destAddress
+                scAddress,
+                parts,
+                (ArrayList<PendingIntent>) sentIntents,
+                (ArrayList<PendingIntent>) deliveryIntents,
+                messageUri,
+                callingPkg,
+                true  /* persistMessageForNonDefaultSmsApp */);
+    }
+
+    private boolean isFailedOrDraft(ContentResolver resolver, Uri messageUri) {
+        // Clear the calling identity and query the database using the phone user id
+        // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
+        // between the calling uid and the package uid
+        final long identity = Binder.clearCallingIdentity();
+        Cursor cursor = null;
+        try {
+            cursor = resolver.query(
+                    messageUri,
+                    new String[]{ Telephony.Sms.TYPE },
+                    null/*selection*/,
+                    null/*selectionArgs*/,
+                    null/*sortOrder*/);
+            if (cursor != null && cursor.moveToFirst()) {
+                final int type = cursor.getInt(0);
+                return type == Telephony.Sms.MESSAGE_TYPE_DRAFT
+                        || type == Telephony.Sms.MESSAGE_TYPE_FAILED;
+            }
+        } catch (SQLiteException e) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]isFailedOrDraft: query message type failed", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+            Binder.restoreCallingIdentity(identity);
+        }
+        return false;
+    }
+
+    // Return an array including both the SMS text (0) and address (1)
+    private String[] loadTextAndAddress(ContentResolver resolver, Uri messageUri) {
+        // Clear the calling identity and query the database using the phone user id
+        // Otherwise the AppOps check in TelephonyProvider would complain about mismatch
+        // between the calling uid and the package uid
+        final long identity = Binder.clearCallingIdentity();
+        Cursor cursor = null;
+        try {
+            cursor = resolver.query(
+                    messageUri,
+                    new String[]{
+                            Telephony.Sms.BODY,
+                            Telephony.Sms.ADDRESS
+                    },
+                    null/*selection*/,
+                    null/*selectionArgs*/,
+                    null/*sortOrder*/);
+            if (cursor != null && cursor.moveToFirst()) {
+                return new String[]{ cursor.getString(0), cursor.getString(1) };
+            }
+        } catch (SQLiteException e) {
+            Log.e(LOG_TAG, "[IccSmsInterfaceManager]loadText: query message text failed", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+            Binder.restoreCallingIdentity(identity);
+        }
+        return null;
+    }
+
+    private void returnUnspecifiedFailure(PendingIntent pi) {
+        if (pi != null) {
+            try {
+                pi.send(SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+            } catch (PendingIntent.CanceledException e) {
+                // ignore
+            }
+        }
+    }
+
+    private void returnUnspecifiedFailure(List<PendingIntent> pis) {
+        if (pis == null) {
+            return;
+        }
+        for (PendingIntent pi : pis) {
+            returnUnspecifiedFailure(pi);
+        }
+    }
+
+    private void enforceCarrierPrivilege() {
+        UiccController controller = UiccController.getInstance();
+        if (controller == null || controller.getUiccCard(mPhone.getPhoneId()) == null) {
+            throw new SecurityException("No Carrier Privilege: No UICC");
+        }
+        if (controller.getUiccCard(mPhone.getPhoneId()).getCarrierPrivilegeStatusForCurrentTransaction(
+                mContext.getPackageManager()) !=
+                    TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+            throw new SecurityException("No Carrier Privilege.");
+        }
+    }
+
+    /**
+     * Enforces that the caller has {@link android.Manifest.permission#MODIFY_PHONE_STATE}
+     * permission or is one of the following apps:
+     * <ul>
+     *     <li> IMS App
+     *     <li> Carrier App
+     * </ul>
+     */
+    private void enforcePrivilegedAppPermissions() {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+                == PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+
+        int callingUid = Binder.getCallingUid();
+        String carrierImsPackage = CarrierSmsUtils.getCarrierImsPackageForIntent(mContext, mPhone,
+                new Intent(CarrierMessagingService.SERVICE_INTERFACE));
+        try {
+            if (carrierImsPackage != null
+                    && callingUid == mContext.getPackageManager().getPackageUid(
+                            carrierImsPackage, 0)) {
+              return;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            if (Rlog.isLoggable("SMS", Log.DEBUG)) {
+                log("Cannot find configured carrier ims package");
+            }
+        }
+
+        enforceCarrierPrivilege();
+    }
+
+    private String filterDestAddress(String destAddr) {
+        String result  = null;
+        result = SmsNumberUtils.filterDestAddr(mPhone, destAddr);
+        return result != null ? result : destAddr;
+    }
+
+}
diff --git a/com/android/internal/telephony/ImsSMSDispatcher.java b/com/android/internal/telephony/ImsSMSDispatcher.java
new file mode 100644
index 0000000..4d8f62c
--- /dev/null
+++ b/com/android/internal/telephony/ImsSMSDispatcher.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
+import com.android.internal.telephony.cdma.CdmaSMSDispatcher;
+import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
+import com.android.internal.telephony.gsm.GsmSMSDispatcher;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ImsSMSDispatcher extends SMSDispatcher {
+    private static final String TAG = "RIL_ImsSms";
+
+    private SMSDispatcher mCdmaDispatcher;
+    private SMSDispatcher mGsmDispatcher;
+
+    private GsmInboundSmsHandler mGsmInboundSmsHandler;
+    private CdmaInboundSmsHandler mCdmaInboundSmsHandler;
+
+
+    /** true if IMS is registered and sms is supported, false otherwise.*/
+    private boolean mIms = false;
+    private String mImsSmsFormat = SmsConstants.FORMAT_UNKNOWN;
+
+    public ImsSMSDispatcher(Phone phone, SmsStorageMonitor storageMonitor,
+            SmsUsageMonitor usageMonitor) {
+        super(phone, usageMonitor, null);
+        Rlog.d(TAG, "ImsSMSDispatcher created");
+
+        // Create dispatchers, inbound SMS handlers and
+        // broadcast undelivered messages in raw table.
+        mCdmaDispatcher = new CdmaSMSDispatcher(phone, usageMonitor, this);
+        mGsmInboundSmsHandler = GsmInboundSmsHandler.makeInboundSmsHandler(phone.getContext(),
+                storageMonitor, phone);
+        mCdmaInboundSmsHandler = CdmaInboundSmsHandler.makeInboundSmsHandler(phone.getContext(),
+                storageMonitor, phone, (CdmaSMSDispatcher) mCdmaDispatcher);
+        mGsmDispatcher = new GsmSMSDispatcher(phone, usageMonitor, this, mGsmInboundSmsHandler);
+        SmsBroadcastUndelivered.initialize(phone.getContext(),
+            mGsmInboundSmsHandler, mCdmaInboundSmsHandler);
+        InboundSmsHandler.registerNewMessageNotificationActionHandler(phone.getContext());
+
+        mCi.registerForOn(this, EVENT_RADIO_ON, null);
+        mCi.registerForImsNetworkStateChanged(this, EVENT_IMS_STATE_CHANGED, null);
+    }
+
+    /* Updates the phone object when there is a change */
+    @Override
+    protected void updatePhoneObject(Phone phone) {
+        Rlog.d(TAG, "In IMS updatePhoneObject ");
+        super.updatePhoneObject(phone);
+        mCdmaDispatcher.updatePhoneObject(phone);
+        mGsmDispatcher.updatePhoneObject(phone);
+        mGsmInboundSmsHandler.updatePhoneObject(phone);
+        mCdmaInboundSmsHandler.updatePhoneObject(phone);
+    }
+
+    public void dispose() {
+        mCi.unregisterForOn(this);
+        mCi.unregisterForImsNetworkStateChanged(this);
+        mGsmDispatcher.dispose();
+        mCdmaDispatcher.dispose();
+        mGsmInboundSmsHandler.dispose();
+        mCdmaInboundSmsHandler.dispose();
+    }
+
+    /**
+     * Handles events coming from the phone stack. Overridden from handler.
+     *
+     * @param msg the message to handle
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+        case EVENT_RADIO_ON:
+        case EVENT_IMS_STATE_CHANGED: // received unsol
+            mCi.getImsRegistrationState(this.obtainMessage(EVENT_IMS_STATE_DONE));
+            break;
+
+        case EVENT_IMS_STATE_DONE:
+            ar = (AsyncResult) msg.obj;
+
+            if (ar.exception == null) {
+                updateImsInfo(ar);
+            } else {
+                Rlog.e(TAG, "IMS State query failed with exp "
+                        + ar.exception);
+            }
+            break;
+
+        default:
+            super.handleMessage(msg);
+        }
+    }
+
+    private void setImsSmsFormat(int format) {
+        // valid format?
+        switch (format) {
+            case PhoneConstants.PHONE_TYPE_GSM:
+                mImsSmsFormat = "3gpp";
+                break;
+            case PhoneConstants.PHONE_TYPE_CDMA:
+                mImsSmsFormat = "3gpp2";
+                break;
+            default:
+                mImsSmsFormat = "unknown";
+                break;
+        }
+    }
+
+    private void updateImsInfo(AsyncResult ar) {
+        int[] responseArray = (int[])ar.result;
+
+        mIms = false;
+        if (responseArray[0] == 1) {  // IMS is registered
+            Rlog.d(TAG, "IMS is registered!");
+            mIms = true;
+        } else {
+            Rlog.d(TAG, "IMS is NOT registered!");
+        }
+
+        setImsSmsFormat(responseArray[1]);
+
+        if (("unknown".equals(mImsSmsFormat))) {
+            Rlog.e(TAG, "IMS format was unknown!");
+            // failed to retrieve valid IMS SMS format info, set IMS to unregistered
+            mIms = false;
+        }
+    }
+
+    @Override
+    public void sendData(String destAddr, String scAddr, int destPort,
+            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        if (isCdmaMo()) {
+            mCdmaDispatcher.sendData(destAddr, scAddr, destPort,
+                    data, sentIntent, deliveryIntent);
+        } else {
+            mGsmDispatcher.sendData(destAddr, scAddr, destPort,
+                    data, sentIntent, deliveryIntent);
+        }
+    }
+
+    @Override
+    public void sendMultipartText(String destAddr, String scAddr,
+            ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
+            ArrayList<PendingIntent> deliveryIntents, Uri messageUri, String callingPkg,
+            boolean persistMessage) {
+        if (isCdmaMo()) {
+            mCdmaDispatcher.sendMultipartText(destAddr, scAddr,
+                    parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage);
+        } else {
+            mGsmDispatcher.sendMultipartText(destAddr, scAddr,
+                    parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage);
+        }
+    }
+
+    @Override
+    protected void sendSms(SmsTracker tracker) {
+        //  sendSms is a helper function to other send functions, sendText/Data...
+        //  it is not part of ISms.stub
+        Rlog.e(TAG, "sendSms should never be called from here!");
+    }
+
+    @Override
+    protected void sendSmsByPstn(SmsTracker tracker) {
+        // This function should be defined in Gsm/CdmaDispatcher.
+        Rlog.e(TAG, "sendSmsByPstn should never be called from here!");
+    }
+
+    @Override
+    public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
+            boolean persistMessage) {
+        Rlog.d(TAG, "sendText");
+        if (isCdmaMo()) {
+            mCdmaDispatcher.sendText(destAddr, scAddr,
+                    text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage);
+        } else {
+            mGsmDispatcher.sendText(destAddr, scAddr,
+                    text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage);
+        }
+    }
+
+    @VisibleForTesting
+    @Override
+    public void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
+        Rlog.d(TAG, "ImsSMSDispatcher:injectSmsPdu");
+        try {
+            // TODO We need to decide whether we should allow injecting GSM(3gpp)
+            // SMS pdus when the phone is camping on CDMA(3gpp2) network and vice versa.
+            android.telephony.SmsMessage msg =
+                    android.telephony.SmsMessage.createFromPdu(pdu, format);
+
+            // Only class 1 SMS are allowed to be injected.
+            if (msg == null ||
+                    msg.getMessageClass() != android.telephony.SmsMessage.MessageClass.CLASS_1) {
+                if (msg == null) {
+                    Rlog.e(TAG, "injectSmsPdu: createFromPdu returned null");
+                }
+                if (receivedIntent != null) {
+                    receivedIntent.send(Intents.RESULT_SMS_GENERIC_ERROR);
+                }
+                return;
+            }
+
+            AsyncResult ar = new AsyncResult(receivedIntent, msg, null);
+
+            if (format.equals(SmsConstants.FORMAT_3GPP)) {
+                Rlog.i(TAG, "ImsSMSDispatcher:injectSmsText Sending msg=" + msg +
+                        ", format=" + format + "to mGsmInboundSmsHandler");
+                mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
+            } else if (format.equals(SmsConstants.FORMAT_3GPP2)) {
+                Rlog.i(TAG, "ImsSMSDispatcher:injectSmsText Sending msg=" + msg +
+                        ", format=" + format + "to mCdmaInboundSmsHandler");
+                mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
+            } else {
+                // Invalid pdu format.
+                Rlog.e(TAG, "Invalid pdu format: " + format);
+                if (receivedIntent != null)
+                    receivedIntent.send(Intents.RESULT_SMS_GENERIC_ERROR);
+            }
+        } catch (Exception e) {
+            Rlog.e(TAG, "injectSmsPdu failed: ", e);
+            try {
+                if (receivedIntent != null)
+                    receivedIntent.send(Intents.RESULT_SMS_GENERIC_ERROR);
+            } catch (CanceledException ex) {}
+        }
+    }
+
+    @Override
+    public void sendRetrySms(SmsTracker tracker) {
+        String oldFormat = tracker.mFormat;
+
+        // newFormat will be based on voice technology
+        String newFormat =
+            (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType()) ?
+                    mCdmaDispatcher.getFormat() :
+                        mGsmDispatcher.getFormat();
+
+        // was previously sent sms format match with voice tech?
+        if (oldFormat.equals(newFormat)) {
+            if (isCdmaFormat(newFormat)) {
+                Rlog.d(TAG, "old format matched new format (cdma)");
+                mCdmaDispatcher.sendSms(tracker);
+                return;
+            } else {
+                Rlog.d(TAG, "old format matched new format (gsm)");
+                mGsmDispatcher.sendSms(tracker);
+                return;
+            }
+        }
+
+        // format didn't match, need to re-encode.
+        HashMap map = tracker.getData();
+
+        // to re-encode, fields needed are:  scAddr, destAddr, and
+        //   text if originally sent as sendText or
+        //   data and destPort if originally sent as sendData.
+        if (!( map.containsKey("scAddr") && map.containsKey("destAddr") &&
+               ( map.containsKey("text") ||
+                       (map.containsKey("data") && map.containsKey("destPort"))))) {
+            // should never come here...
+            Rlog.e(TAG, "sendRetrySms failed to re-encode per missing fields!");
+            tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/*errorCode*/);
+            return;
+        }
+        String scAddr = (String)map.get("scAddr");
+        String destAddr = (String)map.get("destAddr");
+
+        SmsMessageBase.SubmitPduBase pdu = null;
+        //    figure out from tracker if this was sendText/Data
+        if (map.containsKey("text")) {
+            Rlog.d(TAG, "sms failed was text");
+            String text = (String)map.get("text");
+
+            if (isCdmaFormat(newFormat)) {
+                Rlog.d(TAG, "old format (gsm) ==> new format (cdma)");
+                pdu = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(
+                        scAddr, destAddr, text, (tracker.mDeliveryIntent != null), null);
+            } else {
+                Rlog.d(TAG, "old format (cdma) ==> new format (gsm)");
+                pdu = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(
+                        scAddr, destAddr, text, (tracker.mDeliveryIntent != null), null);
+            }
+        } else if (map.containsKey("data")) {
+            Rlog.d(TAG, "sms failed was data");
+            byte[] data = (byte[])map.get("data");
+            Integer destPort = (Integer)map.get("destPort");
+
+            if (isCdmaFormat(newFormat)) {
+                Rlog.d(TAG, "old format (gsm) ==> new format (cdma)");
+                pdu = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(
+                            scAddr, destAddr, destPort.intValue(), data,
+                            (tracker.mDeliveryIntent != null));
+            } else {
+                Rlog.d(TAG, "old format (cdma) ==> new format (gsm)");
+                pdu = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(
+                            scAddr, destAddr, destPort.intValue(), data,
+                            (tracker.mDeliveryIntent != null));
+            }
+        }
+
+        // replace old smsc and pdu with newly encoded ones
+        map.put("smsc", pdu.encodedScAddress);
+        map.put("pdu", pdu.encodedMessage);
+
+        SMSDispatcher dispatcher = (isCdmaFormat(newFormat)) ?
+                mCdmaDispatcher : mGsmDispatcher;
+
+        tracker.mFormat = dispatcher.getFormat();
+        dispatcher.sendSms(tracker);
+    }
+
+    @Override
+    protected void sendSubmitPdu(SmsTracker tracker) {
+        sendRawPdu(tracker);
+    }
+
+    @Override
+    protected String getFormat() {
+        // this function should be defined in Gsm/CdmaDispatcher.
+        Rlog.e(TAG, "getFormat should never be called from here!");
+        return "unknown";
+    }
+
+    @Override
+    protected GsmAlphabet.TextEncodingDetails calculateLength(
+            CharSequence messageBody, boolean use7bitOnly) {
+        Rlog.e(TAG, "Error! Not implemented for IMS.");
+        return null;
+    }
+
+    @Override
+    protected SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
+            String message, SmsHeader smsHeader, int format, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, boolean lastPart,
+            AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
+            String fullMessageText) {
+        Rlog.e(TAG, "Error! Not implemented for IMS.");
+        return null;
+    }
+
+    @Override
+    public boolean isIms() {
+        return mIms;
+    }
+
+    @Override
+    public String getImsSmsFormat() {
+        return mImsSmsFormat;
+    }
+
+    /**
+     * Determines whether or not to use CDMA format for MO SMS.
+     * If SMS over IMS is supported, then format is based on IMS SMS format,
+     * otherwise format is based on current phone type.
+     *
+     * @return true if Cdma format should be used for MO SMS, false otherwise.
+     */
+    private boolean isCdmaMo() {
+        if (!isIms()) {
+            // IMS is not registered, use Voice technology to determine SMS format.
+            return (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType());
+        }
+        // IMS is registered with SMS support
+        return isCdmaFormat(mImsSmsFormat);
+    }
+
+    /**
+     * Determines whether or not format given is CDMA format.
+     *
+     * @param format
+     * @return true if format given is CDMA format, false otherwise.
+     */
+    private boolean isCdmaFormat(String format) {
+        return (mCdmaDispatcher.getFormat().equals(format));
+    }
+}
diff --git a/com/android/internal/telephony/InboundSmsHandler.java b/com/android/internal/telephony/InboundSmsHandler.java
new file mode 100644
index 0000000..59195f8
--- /dev/null
+++ b/com/android/internal/telephony/InboundSmsHandler.java
@@ -0,0 +1,1574 @@
+/*
+ * 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.internal.telephony;
+
+import static android.service.carrier.CarrierMessagingService.RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE;
+import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageManager;
+import android.content.pm.UserInfo;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IDeviceIdleController;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.provider.Telephony;
+import android.provider.Telephony.Sms.Intents;
+import android.service.carrier.CarrierMessagingService;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.util.NotificationChannelController;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class broadcasts incoming SMS messages to interested apps after storing them in
+ * the SmsProvider "raw" table and ACKing them to the SMSC. After each message has been
+ * broadcast, its parts are removed from the raw table. If the device crashes after ACKing
+ * but before the broadcast completes, the pending messages will be rebroadcast on the next boot.
+ *
+ * <p>The state machine starts in {@link IdleState} state. When the {@link SMSDispatcher} receives a
+ * new SMS from the radio, it calls {@link #dispatchNormalMessage},
+ * which sends a message to the state machine, causing the wakelock to be acquired in
+ * {@link #haltedProcessMessage}, which transitions to {@link DeliveringState} state, where the message
+ * is saved to the raw table, then acknowledged via the {@link SMSDispatcher} which called us.
+ *
+ * <p>After saving the SMS, if the message is complete (either single-part or the final segment
+ * of a multi-part SMS), we broadcast the completed PDUs as an ordered broadcast, then transition to
+ * {@link WaitingState} state to wait for the broadcast to complete. When the local
+ * {@link BroadcastReceiver} is called with the result, it sends {@link #EVENT_BROADCAST_COMPLETE}
+ * to the state machine, causing us to either broadcast the next pending message (if one has
+ * arrived while waiting for the broadcast to complete), or to transition back to the halted state
+ * after all messages are processed. Then the wakelock is released and we wait for the next SMS.
+ */
+public abstract class InboundSmsHandler extends StateMachine {
+    protected static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true, logs user data
+
+    /** Query projection for checking for duplicate message segments. */
+    private static final String[] PDU_PROJECTION = {
+            "pdu"
+    };
+
+    /** Query projection for combining concatenated message segments. */
+    private static final String[] PDU_SEQUENCE_PORT_PROJECTION = {
+            "pdu",
+            "sequence",
+            "destination_port",
+            "display_originating_addr"
+    };
+
+    /** Mapping from DB COLUMN to PDU_SEQUENCE_PORT PROJECTION index */
+    private static final Map<Integer, Integer> PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING =
+            new HashMap<Integer, Integer>() {{
+                put(PDU_COLUMN, 0);
+                put(SEQUENCE_COLUMN, 1);
+                put(DESTINATION_PORT_COLUMN, 2);
+                put(DISPLAY_ADDRESS_COLUMN, 3);
+    }};
+
+    public static final int PDU_COLUMN = 0;
+    public static final int SEQUENCE_COLUMN = 1;
+    public static final int DESTINATION_PORT_COLUMN = 2;
+    public static final int DATE_COLUMN = 3;
+    public static final int REFERENCE_NUMBER_COLUMN = 4;
+    public static final int COUNT_COLUMN = 5;
+    public static final int ADDRESS_COLUMN = 6;
+    public static final int ID_COLUMN = 7;
+    public static final int MESSAGE_BODY_COLUMN = 8;
+    public static final int DISPLAY_ADDRESS_COLUMN = 9;
+
+    public static final String SELECT_BY_ID = "_id=?";
+
+    /** New SMS received as an AsyncResult. */
+    public static final int EVENT_NEW_SMS = 1;
+
+    /** Message type containing a {@link InboundSmsTracker} ready to broadcast to listeners. */
+    public static final int EVENT_BROADCAST_SMS = 2;
+
+    /** Message from resultReceiver notifying {@link WaitingState} of a completed broadcast. */
+    private static final int EVENT_BROADCAST_COMPLETE = 3;
+
+    /** Sent on exit from {@link WaitingState} to return to idle after sending all broadcasts. */
+    private static final int EVENT_RETURN_TO_IDLE = 4;
+
+    /** Release wakelock after {@link mWakeLockTimeout} when returning to idle state. */
+    private static final int EVENT_RELEASE_WAKELOCK = 5;
+
+    /** Sent by {@link SmsBroadcastUndelivered} after cleaning the raw table. */
+    public static final int EVENT_START_ACCEPTING_SMS = 6;
+
+    /** Update phone object */
+    private static final int EVENT_UPDATE_PHONE_OBJECT = 7;
+
+    /** New SMS received as an AsyncResult. */
+    public static final int EVENT_INJECT_SMS = 8;
+
+    /** Wakelock release delay when returning to idle state. */
+    private static final int WAKELOCK_TIMEOUT = 3000;
+
+    // The notitfication tag used when showing a notification. The combination of notification tag
+    // and notification id should be unique within the phone app.
+    private static final String NOTIFICATION_TAG = "InboundSmsHandler";
+    private static final int NOTIFICATION_ID_NEW_MESSAGE = 1;
+
+    /** URI for raw table of SMS provider. */
+    protected static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw");
+    protected static final Uri sRawUriPermanentDelete =
+            Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw/permanentDelete");
+
+    protected final Context mContext;
+    private final ContentResolver mResolver;
+
+    /** Special handler for WAP push messages. */
+    private final WapPushOverSms mWapPush;
+
+    /** Wake lock to ensure device stays awake while dispatching the SMS intents. */
+    private final PowerManager.WakeLock mWakeLock;
+
+    /** DefaultState throws an exception or logs an error for unhandled message types. */
+    private final DefaultState mDefaultState = new DefaultState();
+
+    /** Startup state. Waiting for {@link SmsBroadcastUndelivered} to complete. */
+    private final StartupState mStartupState = new StartupState();
+
+    /** Idle state. Waiting for messages to process. */
+    private final IdleState mIdleState = new IdleState();
+
+    /** Delivering state. Saves the PDU in the raw table and acknowledges to SMSC. */
+    private final DeliveringState mDeliveringState = new DeliveringState();
+
+    /** Broadcasting state. Waits for current broadcast to complete before delivering next. */
+    private final WaitingState mWaitingState = new WaitingState();
+
+    /** Helper class to check whether storage is available for incoming messages. */
+    protected SmsStorageMonitor mStorageMonitor;
+
+    private final boolean mSmsReceiveDisabled;
+
+    protected Phone mPhone;
+
+    protected CellBroadcastHandler mCellBroadcastHandler;
+
+    private UserManager mUserManager;
+
+    IDeviceIdleController mDeviceIdleController;
+
+    // Delete permanently from raw table
+    private final int DELETE_PERMANENTLY = 1;
+    // Only mark deleted, but keep in db for message de-duping
+    private final int MARK_DELETED = 2;
+
+    private static String ACTION_OPEN_SMS_APP =
+        "com.android.internal.telephony.OPEN_DEFAULT_SMS_APP";
+
+    /** Timeout for releasing wakelock */
+    private int mWakeLockTimeout;
+
+    /**
+     * Create a new SMS broadcast helper.
+     * @param name the class name for logging
+     * @param context the context of the phone app
+     * @param storageMonitor the SmsStorageMonitor to check for storage availability
+     */
+    protected InboundSmsHandler(String name, Context context, SmsStorageMonitor storageMonitor,
+            Phone phone, CellBroadcastHandler cellBroadcastHandler) {
+        super(name);
+
+        mContext = context;
+        mStorageMonitor = storageMonitor;
+        mPhone = phone;
+        mCellBroadcastHandler = cellBroadcastHandler;
+        mResolver = context.getContentResolver();
+        mWapPush = new WapPushOverSms(context);
+
+        boolean smsCapable = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_sms_capable);
+        mSmsReceiveDisabled = !TelephonyManager.from(mContext).getSmsReceiveCapableForPhone(
+                mPhone.getPhoneId(), smsCapable);
+
+        PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
+        mWakeLock.acquire();    // wake lock released after we enter idle state
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        mDeviceIdleController = TelephonyComponentFactory.getInstance().getIDeviceIdleController();
+
+        addState(mDefaultState);
+        addState(mStartupState, mDefaultState);
+        addState(mIdleState, mDefaultState);
+        addState(mDeliveringState, mDefaultState);
+            addState(mWaitingState, mDeliveringState);
+
+        setInitialState(mStartupState);
+        if (DBG) log("created InboundSmsHandler");
+    }
+
+    /**
+     * Tell the state machine to quit after processing all messages.
+     */
+    public void dispose() {
+        quit();
+    }
+
+    /**
+     * Update the phone object when it changes.
+     */
+    public void updatePhoneObject(Phone phone) {
+        sendMessage(EVENT_UPDATE_PHONE_OBJECT, phone);
+    }
+
+    /**
+     * Dispose of the WAP push object and release the wakelock.
+     */
+    @Override
+    protected void onQuitting() {
+        mWapPush.dispose();
+
+        while (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+    }
+
+    // CAF_MSIM Is this used anywhere ? if not remove it
+    public Phone getPhone() {
+        return mPhone;
+    }
+
+    /**
+     * This parent state throws an exception (for debug builds) or prints an error for unhandled
+     * message types.
+     */
+    private class DefaultState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_UPDATE_PHONE_OBJECT: {
+                    onUpdatePhoneObject((Phone) msg.obj);
+                    break;
+                }
+                default: {
+                    String errorText = "processMessage: unhandled message type " + msg.what +
+                        " currState=" + getCurrentState().getName();
+                    if (Build.IS_DEBUGGABLE) {
+                        loge("---- Dumping InboundSmsHandler ----");
+                        loge("Total records=" + getLogRecCount());
+                        for (int i = Math.max(getLogRecSize() - 20, 0); i < getLogRecSize(); i++) {
+                            loge("Rec[%d]: %s\n" + i + getLogRec(i).toString());
+                        }
+                        loge("---- Dumped InboundSmsHandler ----");
+
+                        throw new RuntimeException(errorText);
+                    } else {
+                        loge(errorText);
+                    }
+                    break;
+                }
+            }
+            return HANDLED;
+        }
+    }
+
+    /**
+     * The Startup state waits for {@link SmsBroadcastUndelivered} to process the raw table and
+     * notify the state machine to broadcast any complete PDUs that might not have been broadcast.
+     */
+    private class StartupState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log("entering Startup state");
+            // Set wakelock timeout to 0 during startup, this will ensure that the wakelock is not
+            // held if there are no pending messages to be handled.
+            setWakeLockTimeout(0);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            log("StartupState.processMessage:" + msg.what);
+            switch (msg.what) {
+                case EVENT_NEW_SMS:
+                case EVENT_INJECT_SMS:
+                case EVENT_BROADCAST_SMS:
+                    deferMessage(msg);
+                    return HANDLED;
+
+                case EVENT_START_ACCEPTING_SMS:
+                    transitionTo(mIdleState);
+                    return HANDLED;
+
+                case EVENT_BROADCAST_COMPLETE:
+                case EVENT_RETURN_TO_IDLE:
+                case EVENT_RELEASE_WAKELOCK:
+                default:
+                    // let DefaultState handle these unexpected message types
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * In the idle state the wakelock is released until a new SM arrives, then we transition
+     * to Delivering mode to handle it, acquiring the wakelock on exit.
+     */
+    private class IdleState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log("entering Idle state");
+            sendMessageDelayed(EVENT_RELEASE_WAKELOCK, getWakeLockTimeout());
+        }
+
+        @Override
+        public void exit() {
+            mWakeLock.acquire();
+            if (DBG) log("acquired wakelock, leaving Idle state");
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            log("IdleState.processMessage:" + msg.what);
+            if (DBG) log("Idle state processing message type " + msg.what);
+            switch (msg.what) {
+                case EVENT_NEW_SMS:
+                case EVENT_INJECT_SMS:
+                case EVENT_BROADCAST_SMS:
+                    deferMessage(msg);
+                    transitionTo(mDeliveringState);
+                    return HANDLED;
+
+                case EVENT_RELEASE_WAKELOCK:
+                    mWakeLock.release();
+                    if (DBG) {
+                        if (mWakeLock.isHeld()) {
+                            // this is okay as long as we call release() for every acquire()
+                            log("mWakeLock is still held after release");
+                        } else {
+                            log("mWakeLock released");
+                        }
+                    }
+                    return HANDLED;
+
+                case EVENT_RETURN_TO_IDLE:
+                    // already in idle state; ignore
+                    return HANDLED;
+
+                case EVENT_BROADCAST_COMPLETE:
+                case EVENT_START_ACCEPTING_SMS:
+                default:
+                    // let DefaultState handle these unexpected message types
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * In the delivering state, the inbound SMS is processed and stored in the raw table.
+     * The message is acknowledged before we exit this state. If there is a message to broadcast,
+     * transition to {@link WaitingState} state to send the ordered broadcast and wait for the
+     * results. When all messages have been processed, the halting state will release the wakelock.
+     */
+    private class DeliveringState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log("entering Delivering state");
+        }
+
+        @Override
+        public void exit() {
+            if (DBG) log("leaving Delivering state");
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            log("DeliveringState.processMessage:" + msg.what);
+            switch (msg.what) {
+                case EVENT_NEW_SMS:
+                    // handle new SMS from RIL
+                    handleNewSms((AsyncResult) msg.obj);
+                    sendMessage(EVENT_RETURN_TO_IDLE);
+                    return HANDLED;
+
+                case EVENT_INJECT_SMS:
+                    // handle new injected SMS
+                    handleInjectSms((AsyncResult) msg.obj);
+                    sendMessage(EVENT_RETURN_TO_IDLE);
+                    return HANDLED;
+
+                case EVENT_BROADCAST_SMS:
+                    // if any broadcasts were sent, transition to waiting state
+                    InboundSmsTracker inboundSmsTracker = (InboundSmsTracker) msg.obj;
+                    if (processMessagePart(inboundSmsTracker)) {
+                        transitionTo(mWaitingState);
+                    } else {
+                        // if event is sent from SmsBroadcastUndelivered.broadcastSms(), and
+                        // processMessagePart() returns false, the state machine will be stuck in
+                        // DeliveringState until next message is received. Send message to
+                        // transition to idle to avoid that so that wakelock can be released
+                        log("No broadcast sent on processing EVENT_BROADCAST_SMS in Delivering " +
+                                "state. Return to Idle state");
+                        sendMessage(EVENT_RETURN_TO_IDLE);
+                    }
+                    return HANDLED;
+
+                case EVENT_RETURN_TO_IDLE:
+                    // return to idle after processing all other messages
+                    transitionTo(mIdleState);
+                    return HANDLED;
+
+                case EVENT_RELEASE_WAKELOCK:
+                    mWakeLock.release();    // decrement wakelock from previous entry to Idle
+                    if (!mWakeLock.isHeld()) {
+                        // wakelock should still be held until 3 seconds after we enter Idle
+                        loge("mWakeLock released while delivering/broadcasting!");
+                    }
+                    return HANDLED;
+
+                // we shouldn't get this message type in this state, log error and halt.
+                case EVENT_BROADCAST_COMPLETE:
+                case EVENT_START_ACCEPTING_SMS:
+                default:
+                    // let DefaultState handle these unexpected message types
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * The waiting state delegates handling of new SMS to parent {@link DeliveringState}, but
+     * defers handling of the {@link #EVENT_BROADCAST_SMS} phase until after the current
+     * result receiver sends {@link #EVENT_BROADCAST_COMPLETE}. Before transitioning to
+     * {@link DeliveringState}, {@link #EVENT_RETURN_TO_IDLE} is sent to transition to
+     * {@link IdleState} after any deferred {@link #EVENT_BROADCAST_SMS} messages are handled.
+     */
+    private class WaitingState extends State {
+        @Override
+        public void exit() {
+            if (DBG) log("exiting Waiting state");
+            // Before moving to idle state, set wakelock timeout to WAKE_LOCK_TIMEOUT milliseconds
+            // to give any receivers time to take their own wake locks
+            setWakeLockTimeout(WAKELOCK_TIMEOUT);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            log("WaitingState.processMessage:" + msg.what);
+            switch (msg.what) {
+                case EVENT_BROADCAST_SMS:
+                    // defer until the current broadcast completes
+                    deferMessage(msg);
+                    return HANDLED;
+
+                case EVENT_BROADCAST_COMPLETE:
+                    // return to idle after handling all deferred messages
+                    sendMessage(EVENT_RETURN_TO_IDLE);
+                    transitionTo(mDeliveringState);
+                    return HANDLED;
+
+                case EVENT_RETURN_TO_IDLE:
+                    // not ready to return to idle; ignore
+                    return HANDLED;
+
+                default:
+                    // parent state handles the other message types
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    private void handleNewSms(AsyncResult ar) {
+        if (ar.exception != null) {
+            loge("Exception processing incoming SMS: " + ar.exception);
+            return;
+        }
+
+        int result;
+        try {
+            SmsMessage sms = (SmsMessage) ar.result;
+            result = dispatchMessage(sms.mWrappedSmsMessage);
+        } catch (RuntimeException ex) {
+            loge("Exception dispatching message", ex);
+            result = Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+
+        // RESULT_OK means that the SMS will be acknowledged by special handling,
+        // e.g. for SMS-PP data download. Any other result, we should ack here.
+        if (result != Activity.RESULT_OK) {
+            boolean handled = (result == Intents.RESULT_SMS_HANDLED);
+            notifyAndAcknowledgeLastIncomingSms(handled, result, null);
+        }
+    }
+
+    /**
+     * This method is called when a new SMS PDU is injected into application framework.
+     * @param ar is the AsyncResult that has the SMS PDU to be injected.
+     */
+    private void handleInjectSms(AsyncResult ar) {
+        int result;
+        PendingIntent receivedIntent = null;
+        try {
+            receivedIntent = (PendingIntent) ar.userObj;
+            SmsMessage sms = (SmsMessage) ar.result;
+            if (sms == null) {
+              result = Intents.RESULT_SMS_GENERIC_ERROR;
+            } else {
+              result = dispatchMessage(sms.mWrappedSmsMessage);
+            }
+        } catch (RuntimeException ex) {
+            loge("Exception dispatching message", ex);
+            result = Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+
+        if (receivedIntent != null) {
+            try {
+                receivedIntent.send(result);
+            } catch (CanceledException e) { }
+        }
+    }
+
+    /**
+     * Process an SMS message from the RIL, calling subclass methods to handle 3GPP and
+     * 3GPP2-specific message types.
+     *
+     * @param smsb the SmsMessageBase object from the RIL
+     * @return a result code from {@link android.provider.Telephony.Sms.Intents},
+     *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
+     */
+    private int dispatchMessage(SmsMessageBase smsb) {
+        // If sms is null, there was a parsing error.
+        if (smsb == null) {
+            loge("dispatchSmsMessage: message is null");
+            return Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+
+        if (mSmsReceiveDisabled) {
+            // Device doesn't support receiving SMS,
+            log("Received short message on device which doesn't support "
+                    + "receiving SMS. Ignored.");
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        // onlyCore indicates if the device is in cryptkeeper
+        boolean onlyCore = false;
+        try {
+            onlyCore = IPackageManager.Stub.asInterface(ServiceManager.getService("package")).
+                    isOnlyCoreApps();
+        } catch (RemoteException e) {
+        }
+        if (onlyCore) {
+            // Device is unable to receive SMS in encrypted state
+            log("Received a short message in encrypted state. Rejecting.");
+            return Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+
+        return dispatchMessageRadioSpecific(smsb);
+    }
+
+    /**
+     * Process voicemail notification, SMS-PP data download, CDMA CMAS, CDMA WAP push, and other
+     * 3GPP/3GPP2-specific messages. Regular SMS messages are handled by calling the shared
+     * {@link #dispatchNormalMessage} from this class.
+     *
+     * @param smsb the SmsMessageBase object from the RIL
+     * @return a result code from {@link android.provider.Telephony.Sms.Intents},
+     *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
+     */
+    protected abstract int dispatchMessageRadioSpecific(SmsMessageBase smsb);
+
+    /**
+     * Send an acknowledge message to the SMSC.
+     * @param success indicates that last message was successfully received.
+     * @param result result code indicating any error
+     * @param response callback message sent when operation completes.
+     */
+    protected abstract void acknowledgeLastIncomingSms(boolean success,
+            int result, Message response);
+
+    /**
+     * Called when the phone changes the default method updates mPhone
+     * mStorageMonitor and mCellBroadcastHandler.updatePhoneObject.
+     * Override if different or other behavior is desired.
+     *
+     * @param phone
+     */
+    protected void onUpdatePhoneObject(Phone phone) {
+        mPhone = phone;
+        mStorageMonitor = mPhone.mSmsStorageMonitor;
+        log("onUpdatePhoneObject: phone=" + mPhone.getClass().getSimpleName());
+    }
+
+    /**
+     * Notify interested apps if the framework has rejected an incoming SMS,
+     * and send an acknowledge message to the network.
+     * @param success indicates that last message was successfully received.
+     * @param result result code indicating any error
+     * @param response callback message sent when operation completes.
+     */
+    private void notifyAndAcknowledgeLastIncomingSms(boolean success,
+            int result, Message response) {
+        if (!success) {
+            // broadcast SMS_REJECTED_ACTION intent
+            Intent intent = new Intent(Intents.SMS_REJECTED_ACTION);
+            intent.putExtra("result", result);
+            // Allow registered broadcast receivers to get this intent even
+            // when they are in the background.
+            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+            mContext.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS);
+        }
+        acknowledgeLastIncomingSms(success, result, response);
+    }
+
+    /**
+     * Return true if this handler is for 3GPP2 messages; false for 3GPP format.
+     * @return true for the 3GPP2 handler; false for the 3GPP handler
+     */
+    protected abstract boolean is3gpp2();
+
+    /**
+     * Dispatch a normal incoming SMS. This is called from {@link #dispatchMessageRadioSpecific}
+     * if no format-specific handling was required. Saves the PDU to the SMS provider raw table,
+     * creates an {@link InboundSmsTracker}, then sends it to the state machine as an
+     * {@link #EVENT_BROADCAST_SMS}. Returns {@link Intents#RESULT_SMS_HANDLED} or an error value.
+     *
+     * @param sms the message to dispatch
+     * @return {@link Intents#RESULT_SMS_HANDLED} if the message was accepted, or an error status
+     */
+    protected int dispatchNormalMessage(SmsMessageBase sms) {
+        SmsHeader smsHeader = sms.getUserDataHeader();
+        InboundSmsTracker tracker;
+
+        if ((smsHeader == null) || (smsHeader.concatRef == null)) {
+            // Message is not concatenated.
+            int destPort = -1;
+            if (smsHeader != null && smsHeader.portAddrs != null) {
+                // The message was sent to a port.
+                destPort = smsHeader.portAddrs.destPort;
+                if (DBG) log("destination port: " + destPort);
+            }
+
+            tracker = TelephonyComponentFactory.getInstance().makeInboundSmsTracker(sms.getPdu(),
+                    sms.getTimestampMillis(), destPort, is3gpp2(), false,
+                    sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
+                    sms.getMessageBody());
+        } else {
+            // Create a tracker for this message segment.
+            SmsHeader.ConcatRef concatRef = smsHeader.concatRef;
+            SmsHeader.PortAddrs portAddrs = smsHeader.portAddrs;
+            int destPort = (portAddrs != null ? portAddrs.destPort : -1);
+
+            tracker = TelephonyComponentFactory.getInstance().makeInboundSmsTracker(sms.getPdu(),
+                    sms.getTimestampMillis(), destPort, is3gpp2(), sms.getOriginatingAddress(),
+                    sms.getDisplayOriginatingAddress(), concatRef.refNumber, concatRef.seqNumber,
+                    concatRef.msgCount, false, sms.getMessageBody());
+        }
+
+        if (VDBG) log("created tracker: " + tracker);
+
+        // de-duping is done only for text messages
+        // destPort = -1 indicates text messages, otherwise it's a data sms
+        return addTrackerToRawTableAndSendMessage(tracker,
+                tracker.getDestPort() == -1 /* de-dup if text message */);
+    }
+
+    /**
+     * Helper to add the tracker to the raw table and then send a message to broadcast it, if
+     * successful. Returns the SMS intent status to return to the SMSC.
+     * @param tracker the tracker to save to the raw table and then deliver
+     * @return {@link Intents#RESULT_SMS_HANDLED} or {@link Intents#RESULT_SMS_GENERIC_ERROR}
+     * or {@link Intents#RESULT_SMS_DUPLICATED}
+     */
+    protected int addTrackerToRawTableAndSendMessage(InboundSmsTracker tracker, boolean deDup) {
+        switch(addTrackerToRawTable(tracker, deDup)) {
+        case Intents.RESULT_SMS_HANDLED:
+            sendMessage(EVENT_BROADCAST_SMS, tracker);
+            return Intents.RESULT_SMS_HANDLED;
+
+        case Intents.RESULT_SMS_DUPLICATED:
+            return Intents.RESULT_SMS_HANDLED;
+
+        case Intents.RESULT_SMS_GENERIC_ERROR:
+        default:
+            return Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+    }
+
+    /**
+     * Process the inbound SMS segment. If the message is complete, send it as an ordered
+     * broadcast to interested receivers and return true. If the message is a segment of an
+     * incomplete multi-part SMS, return false.
+     * @param tracker the tracker containing the message segment to process
+     * @return true if an ordered broadcast was sent; false if waiting for more message segments
+     */
+    private boolean processMessagePart(InboundSmsTracker tracker) {
+        int messageCount = tracker.getMessageCount();
+        byte[][] pdus;
+        int destPort = tracker.getDestPort();
+        boolean block = false;
+
+        if (messageCount == 1) {
+            // single-part message
+            pdus = new byte[][]{tracker.getPdu()};
+            block = BlockChecker.isBlocked(mContext, tracker.getDisplayAddress());
+        } else {
+            // multi-part message
+            Cursor cursor = null;
+            try {
+                // used by several query selection arguments
+                String address = tracker.getAddress();
+                String refNumber = Integer.toString(tracker.getReferenceNumber());
+                String count = Integer.toString(tracker.getMessageCount());
+
+                // query for all segments and broadcast message if we have all the parts
+                String[] whereArgs = {address, refNumber, count};
+                cursor = mResolver.query(sRawUri, PDU_SEQUENCE_PORT_PROJECTION,
+                        tracker.getQueryForSegments(), whereArgs, null);
+
+                int cursorCount = cursor.getCount();
+                if (cursorCount < messageCount) {
+                    // Wait for the other message parts to arrive. It's also possible for the last
+                    // segment to arrive before processing the EVENT_BROADCAST_SMS for one of the
+                    // earlier segments. In that case, the broadcast will be sent as soon as all
+                    // segments are in the table, and any later EVENT_BROADCAST_SMS messages will
+                    // get a row count of 0 and return.
+                    return false;
+                }
+
+                // All the parts are in place, deal with them
+                pdus = new byte[messageCount][];
+                while (cursor.moveToNext()) {
+                    // subtract offset to convert sequence to 0-based array index
+                    int index = cursor.getInt(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
+                            .get(SEQUENCE_COLUMN)) - tracker.getIndexOffset();
+
+                    pdus[index] = HexDump.hexStringToByteArray(cursor.getString(
+                            PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING.get(PDU_COLUMN)));
+
+                    // Read the destination port from the first segment (needed for CDMA WAP PDU).
+                    // It's not a bad idea to prefer the port from the first segment in other cases.
+                    if (index == 0 && !cursor.isNull(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
+                            .get(DESTINATION_PORT_COLUMN))) {
+                        int port = cursor.getInt(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
+                                .get(DESTINATION_PORT_COLUMN));
+                        // strip format flags and convert to real port number, or -1
+                        port = InboundSmsTracker.getRealDestPort(port);
+                        if (port != -1) {
+                            destPort = port;
+                        }
+                    }
+                    // check if display address should be blocked or not
+                    if (!block) {
+                        // Depending on the nature of the gateway, the display origination address
+                        // is either derived from the content of the SMS TP-OA field, or the TP-OA
+                        // field contains a generic gateway address and the from address is added
+                        // at the beginning in the message body. In that case only the first SMS
+                        // (part of Multi-SMS) comes with the display originating address which
+                        // could be used for block checking purpose.
+                        block = BlockChecker.isBlocked(mContext,
+                                cursor.getString(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
+                                        .get(DISPLAY_ADDRESS_COLUMN)));
+                    }
+                }
+            } catch (SQLException e) {
+                loge("Can't access multipart SMS database", e);
+                return false;
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+
+        // Do not process null pdu(s). Check for that and return false in that case.
+        List<byte[]> pduList = Arrays.asList(pdus);
+        if (pduList.size() == 0 || pduList.contains(null)) {
+            loge("processMessagePart: returning false due to " +
+                    (pduList.size() == 0 ? "pduList.size() == 0" : "pduList.contains(null)"));
+            return false;
+        }
+
+        SmsBroadcastReceiver resultReceiver = new SmsBroadcastReceiver(tracker);
+
+        if (!mUserManager.isUserUnlocked()) {
+            return processMessagePartWithUserLocked(tracker, pdus, destPort, resultReceiver);
+        }
+
+        if (destPort == SmsHeader.PORT_WAP_PUSH) {
+            // Build up the data stream
+            ByteArrayOutputStream output = new ByteArrayOutputStream();
+            for (byte[] pdu : pdus) {
+                // 3GPP needs to extract the User Data from the PDU; 3GPP2 has already done this
+                if (!tracker.is3gpp2()) {
+                    SmsMessage msg = SmsMessage.createFromPdu(pdu, SmsConstants.FORMAT_3GPP);
+                    if (msg != null) {
+                        pdu = msg.getUserData();
+                    } else {
+                        loge("processMessagePart: SmsMessage.createFromPdu returned null");
+                        return false;
+                    }
+                }
+                output.write(pdu, 0, pdu.length);
+            }
+            int result = mWapPush.dispatchWapPdu(output.toByteArray(), resultReceiver, this);
+            if (DBG) log("dispatchWapPdu() returned " + result);
+            // result is Activity.RESULT_OK if an ordered broadcast was sent
+            if (result == Activity.RESULT_OK) {
+                return true;
+            } else {
+                deleteFromRawTable(tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(),
+                        MARK_DELETED);
+                return false;
+            }
+        }
+
+        if (block) {
+            deleteFromRawTable(tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(),
+                    DELETE_PERMANENTLY);
+            return false;
+        }
+
+        boolean filterInvoked = filterSms(
+            pdus, destPort, tracker, resultReceiver, true /* userUnlocked */);
+
+        if (!filterInvoked) {
+            dispatchSmsDeliveryIntent(pdus, tracker.getFormat(), destPort, resultReceiver);
+        }
+
+        return true;
+    }
+
+    /**
+     * Processes the message part while the credential-encrypted storage is still locked.
+     *
+     * <p>If the message is a regular MMS, show a new message notification. If the message is a
+     * SMS, ask the carrier app to filter it and show the new message notification if the carrier
+     * app asks to keep the message.
+     *
+     * @return true if an ordered broadcast was sent to the carrier app; false otherwise.
+     */
+    private boolean processMessagePartWithUserLocked(InboundSmsTracker tracker,
+            byte[][] pdus, int destPort, SmsBroadcastReceiver resultReceiver) {
+        log("Credential-encrypted storage not available. Port: " + destPort);
+        if (destPort == SmsHeader.PORT_WAP_PUSH && mWapPush.isWapPushForMms(pdus[0], this)) {
+            showNewMessageNotification();
+            return false;
+        }
+        if (destPort == -1) {
+            // This is a regular SMS - hand it to the carrier or system app for filtering.
+            boolean filterInvoked = filterSms(
+                pdus, destPort, tracker, resultReceiver, false /* userUnlocked */);
+            if (filterInvoked) {
+                // filter invoked, wait for it to return the result.
+                return true;
+            } else {
+                // filter not invoked, show the notification and do nothing further.
+                showNewMessageNotification();
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private void showNewMessageNotification() {
+        // Do not show the notification on non-FBE devices.
+        if (!StorageManager.isFileEncryptedNativeOrEmulated()) {
+            return;
+        }
+        log("Show new message notification.");
+        PendingIntent intent = PendingIntent.getBroadcast(
+            mContext,
+            0,
+            new Intent(ACTION_OPEN_SMS_APP),
+            PendingIntent.FLAG_ONE_SHOT);
+        Notification.Builder mBuilder = new Notification.Builder(mContext)
+                .setSmallIcon(com.android.internal.R.drawable.sym_action_chat)
+                .setAutoCancel(true)
+                .setVisibility(Notification.VISIBILITY_PUBLIC)
+                .setDefaults(Notification.DEFAULT_ALL)
+                .setContentTitle(mContext.getString(R.string.new_sms_notification_title))
+                .setContentText(mContext.getString(R.string.new_sms_notification_content))
+                .setContentIntent(intent)
+                .setChannelId(NotificationChannelController.CHANNEL_ID_SMS);
+        NotificationManager mNotificationManager =
+            (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mNotificationManager.notify(
+                NOTIFICATION_TAG, NOTIFICATION_ID_NEW_MESSAGE, mBuilder.build());
+    }
+
+    static void cancelNewMessageNotification(Context context) {
+        NotificationManager mNotificationManager =
+            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+        mNotificationManager.cancel(InboundSmsHandler.NOTIFICATION_TAG,
+            InboundSmsHandler.NOTIFICATION_ID_NEW_MESSAGE);
+    }
+
+    /**
+     * Filters the SMS.
+     *
+     * <p>currently 3 filters exists: the carrier package, the system package, and the
+     * VisualVoicemailSmsFilter.
+     *
+     * <p>The filtering process is:
+     *
+     * <p>If the carrier package exists, the SMS will be filtered with it first. If the carrier
+     * package did not drop the SMS, then the VisualVoicemailSmsFilter will filter it in the
+     * callback.
+     *
+     * <p>If the carrier package does not exists, we will let the VisualVoicemailSmsFilter filter
+     * it. If the SMS passed the filter, then we will try to find the system package to do the
+     * filtering.
+     *
+     * @return true if a filter is invoked and the SMS processing flow is diverted, false otherwise.
+     */
+    private boolean filterSms(byte[][] pdus, int destPort,
+        InboundSmsTracker tracker, SmsBroadcastReceiver resultReceiver, boolean userUnlocked) {
+        CarrierServicesSmsFilterCallback filterCallback =
+                new CarrierServicesSmsFilterCallback(
+                        pdus, destPort, tracker.getFormat(), resultReceiver, userUnlocked);
+        CarrierServicesSmsFilter carrierServicesFilter = new CarrierServicesSmsFilter(
+                mContext, mPhone, pdus, destPort, tracker.getFormat(), filterCallback, getName());
+        if (carrierServicesFilter.filter()) {
+            return true;
+        }
+
+        if (VisualVoicemailSmsFilter.filter(
+                mContext, pdus, tracker.getFormat(), destPort, mPhone.getSubId())) {
+            log("Visual voicemail SMS dropped");
+            dropSms(resultReceiver);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Dispatch the intent with the specified permission, appOp, and result receiver, using
+     * this state machine's handler thread to run the result receiver.
+     *
+     * @param intent the intent to broadcast
+     * @param permission receivers are required to have this permission
+     * @param appOp app op that is being performed when dispatching to a receiver
+     * @param user user to deliver the intent to
+     */
+    public void dispatchIntent(Intent intent, String permission, int appOp,
+            Bundle opts, BroadcastReceiver resultReceiver, UserHandle user) {
+        intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT);
+        final String action = intent.getAction();
+        if (Intents.SMS_DELIVER_ACTION.equals(action)
+                || Intents.SMS_RECEIVED_ACTION.equals(action)
+                || Intents.WAP_PUSH_DELIVER_ACTION.equals(action)
+                || Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) {
+            // Some intents need to be delivered with high priority:
+            // SMS_DELIVER, SMS_RECEIVED, WAP_PUSH_DELIVER, WAP_PUSH_RECEIVED
+            // In some situations, like after boot up or system under load, normal
+            // intent delivery could take a long time.
+            // This flag should only be set for intents for visible, timely operations
+            // which is true for the intents above.
+            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        }
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
+        if (user.equals(UserHandle.ALL)) {
+            // Get a list of currently started users.
+            int[] users = null;
+            try {
+                users = ActivityManager.getService().getRunningUserIds();
+            } catch (RemoteException re) {
+            }
+            if (users == null) {
+                users = new int[] {user.getIdentifier()};
+            }
+            // Deliver the broadcast only to those running users that are permitted
+            // by user policy.
+            for (int i = users.length - 1; i >= 0; i--) {
+                UserHandle targetUser = new UserHandle(users[i]);
+                if (users[i] != UserHandle.USER_SYSTEM) {
+                    // Is the user not allowed to use SMS?
+                    if (mUserManager.hasUserRestriction(UserManager.DISALLOW_SMS, targetUser)) {
+                        continue;
+                    }
+                    // Skip unknown users and managed profiles as well
+                    UserInfo info = mUserManager.getUserInfo(users[i]);
+                    if (info == null || info.isManagedProfile()) {
+                        continue;
+                    }
+                }
+                // Only pass in the resultReceiver when the USER_SYSTEM is processed.
+                mContext.sendOrderedBroadcastAsUser(intent, targetUser, permission, appOp, opts,
+                        users[i] == UserHandle.USER_SYSTEM ? resultReceiver : null,
+                        getHandler(), Activity.RESULT_OK, null, null);
+            }
+        } else {
+            mContext.sendOrderedBroadcastAsUser(intent, user, permission, appOp, opts,
+                    resultReceiver, getHandler(), Activity.RESULT_OK, null, null);
+        }
+    }
+
+    /**
+     * Helper for {@link SmsBroadcastUndelivered} to delete an old message in the raw table.
+     */
+    private void deleteFromRawTable(String deleteWhere, String[] deleteWhereArgs,
+                                    int deleteType) {
+        Uri uri = deleteType == DELETE_PERMANENTLY ? sRawUriPermanentDelete : sRawUri;
+        int rows = mResolver.delete(uri, deleteWhere, deleteWhereArgs);
+        if (rows == 0) {
+            loge("No rows were deleted from raw table!");
+        } else if (DBG) {
+            log("Deleted " + rows + " rows from raw table.");
+        }
+    }
+
+    private Bundle handleSmsWhitelisting(ComponentName target) {
+        String pkgName;
+        String reason;
+        if (target != null) {
+            pkgName = target.getPackageName();
+            reason = "sms-app";
+        } else {
+            pkgName = mContext.getPackageName();
+            reason = "sms-broadcast";
+        }
+        try {
+            long duration = mDeviceIdleController.addPowerSaveTempWhitelistAppForSms(
+                    pkgName, 0, reason);
+            BroadcastOptions bopts = BroadcastOptions.makeBasic();
+            bopts.setTemporaryAppWhitelistDuration(duration);
+            return bopts.toBundle();
+        } catch (RemoteException e) {
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates and dispatches the intent to the default SMS app, appropriate port or via the {@link
+     * AppSmsManager}.
+     *
+     * @param pdus message pdus
+     * @param format the message format, typically "3gpp" or "3gpp2"
+     * @param destPort the destination port
+     * @param resultReceiver the receiver handling the delivery result
+     */
+    private void dispatchSmsDeliveryIntent(byte[][] pdus, String format, int destPort,
+            SmsBroadcastReceiver resultReceiver) {
+        Intent intent = new Intent();
+        intent.putExtra("pdus", pdus);
+        intent.putExtra("format", format);
+
+        if (destPort == -1) {
+            intent.setAction(Intents.SMS_DELIVER_ACTION);
+            // Direct the intent to only the default SMS app. If we can't find a default SMS app
+            // then sent it to all broadcast receivers.
+            // We are deliberately delivering to the primary user's default SMS App.
+            ComponentName componentName = SmsApplication.getDefaultSmsApplication(mContext, true);
+            if (componentName != null) {
+                // Deliver SMS message only to this receiver.
+                intent.setComponent(componentName);
+                log("Delivering SMS to: " + componentName.getPackageName() +
+                    " " + componentName.getClassName());
+            } else {
+                intent.setComponent(null);
+            }
+
+            // TODO: Validate that this is the right place to store the SMS.
+            if (SmsManager.getDefault().getAutoPersisting()) {
+                final Uri uri = writeInboxMessage(intent);
+                if (uri != null) {
+                    // Pass this to SMS apps so that they know where it is stored
+                    intent.putExtra("uri", uri.toString());
+                }
+            }
+
+            // Handle app specific sms messages.
+            AppSmsManager appManager = mPhone.getAppSmsManager();
+            if (appManager.handleSmsReceivedIntent(intent)) {
+                // The AppSmsManager handled this intent, we're done.
+                dropSms(resultReceiver);
+                return;
+            }
+        } else {
+            intent.setAction(Intents.DATA_SMS_RECEIVED_ACTION);
+            Uri uri = Uri.parse("sms://localhost:" + destPort);
+            intent.setData(uri);
+            intent.setComponent(null);
+            // Allow registered broadcast receivers to get this intent even
+            // when they are in the background.
+            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        }
+
+        Bundle options = handleSmsWhitelisting(intent.getComponent());
+        dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
+                AppOpsManager.OP_RECEIVE_SMS, options, resultReceiver, UserHandle.SYSTEM);
+    }
+
+    /**
+     * Function to check if message should be dropped because same message has already been
+     * received. In certain cases it checks for similar messages instead of exact same (cases where
+     * keeping both messages in db can cause ambiguity)
+     * @return true if duplicate exists, false otherwise
+     */
+    private boolean duplicateExists(InboundSmsTracker tracker) throws SQLException {
+        String address = tracker.getAddress();
+        // convert to strings for query
+        String refNumber = Integer.toString(tracker.getReferenceNumber());
+        String count = Integer.toString(tracker.getMessageCount());
+        // sequence numbers are 1-based except for CDMA WAP, which is 0-based
+        int sequence = tracker.getSequenceNumber();
+        String seqNumber = Integer.toString(sequence);
+        String date = Long.toString(tracker.getTimestamp());
+        String messageBody = tracker.getMessageBody();
+        String where;
+        if (tracker.getMessageCount() == 1) {
+            where = "address=? AND reference_number=? AND count=? AND sequence=? AND " +
+                    "date=? AND message_body=?";
+        } else {
+            // for multi-part messages, deduping should also be done against undeleted
+            // segments that can cause ambiguity when contacenating the segments, that is,
+            // segments with same address, reference_number, count, sequence and message type.
+            where = tracker.getQueryForMultiPartDuplicates();
+        }
+
+        Cursor cursor = null;
+        try {
+            // Check for duplicate message segments
+            cursor = mResolver.query(sRawUri, PDU_PROJECTION, where,
+                    new String[]{address, refNumber, count, seqNumber, date, messageBody},
+                    null);
+
+            // moveToNext() returns false if no duplicates were found
+            if (cursor != null && cursor.moveToNext()) {
+                loge("Discarding duplicate message segment, refNumber=" + refNumber
+                        + " seqNumber=" + seqNumber + " count=" + count);
+                if (VDBG) {
+                    loge("address=" + address + " date=" + date + " messageBody=" +
+                            messageBody);
+                }
+                String oldPduString = cursor.getString(PDU_COLUMN);
+                byte[] pdu = tracker.getPdu();
+                byte[] oldPdu = HexDump.hexStringToByteArray(oldPduString);
+                if (!Arrays.equals(oldPdu, tracker.getPdu())) {
+                    loge("Warning: dup message segment PDU of length " + pdu.length
+                            + " is different from existing PDU of length " + oldPdu.length);
+                }
+                return true;   // reject message
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Insert a message PDU into the raw table so we can acknowledge it immediately.
+     * If the device crashes before the broadcast to listeners completes, it will be delivered
+     * from the raw table on the next device boot. For single-part messages, the deleteWhere
+     * and deleteWhereArgs fields of the tracker will be set to delete the correct row after
+     * the ordered broadcast completes.
+     *
+     * @param tracker the tracker to add to the raw table
+     * @return true on success; false on failure to write to database
+     */
+    private int addTrackerToRawTable(InboundSmsTracker tracker, boolean deDup) {
+        if (deDup) {
+            try {
+                if (duplicateExists(tracker)) {
+                    return Intents.RESULT_SMS_DUPLICATED;   // reject message
+                }
+            } catch (SQLException e) {
+                loge("Can't access SMS database", e);
+                return Intents.RESULT_SMS_GENERIC_ERROR;    // reject message
+            }
+        } else {
+            logd("Skipped message de-duping logic");
+        }
+
+        String address = tracker.getAddress();
+        String refNumber = Integer.toString(tracker.getReferenceNumber());
+        String count = Integer.toString(tracker.getMessageCount());
+        ContentValues values = tracker.getContentValues();
+
+        if (VDBG) log("adding content values to raw table: " + values.toString());
+        Uri newUri = mResolver.insert(sRawUri, values);
+        if (DBG) log("URI of new row -> " + newUri);
+
+        try {
+            long rowId = ContentUris.parseId(newUri);
+            if (tracker.getMessageCount() == 1) {
+                // set the delete selection args for single-part message
+                tracker.setDeleteWhere(SELECT_BY_ID, new String[]{Long.toString(rowId)});
+            } else {
+                // set the delete selection args for multi-part message
+                String[] deleteWhereArgs = {address, refNumber, count};
+                tracker.setDeleteWhere(tracker.getQueryForSegments(), deleteWhereArgs);
+            }
+            return Intents.RESULT_SMS_HANDLED;
+        } catch (Exception e) {
+            loge("error parsing URI for new row: " + newUri, e);
+            return Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+    }
+
+    /**
+     * Returns whether the default message format for the current radio technology is 3GPP2.
+     * @return true if the radio technology uses 3GPP2 format by default, false for 3GPP format
+     */
+    static boolean isCurrentFormat3gpp2() {
+        int activePhone = TelephonyManager.getDefault().getCurrentPhoneType();
+        return (PHONE_TYPE_CDMA == activePhone);
+    }
+
+    /**
+     * Handler for an {@link InboundSmsTracker} broadcast. Deletes PDUs from the raw table and
+     * logs the broadcast duration (as an error if the other receivers were especially slow).
+     */
+    private final class SmsBroadcastReceiver extends BroadcastReceiver {
+        private final String mDeleteWhere;
+        private final String[] mDeleteWhereArgs;
+        private long mBroadcastTimeNano;
+
+        SmsBroadcastReceiver(InboundSmsTracker tracker) {
+            mDeleteWhere = tracker.getDeleteWhere();
+            mDeleteWhereArgs = tracker.getDeleteWhereArgs();
+            mBroadcastTimeNano = System.nanoTime();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intents.SMS_DELIVER_ACTION)) {
+                // Now dispatch the notification only intent
+                intent.setAction(Intents.SMS_RECEIVED_ACTION);
+                // Allow registered broadcast receivers to get this intent even
+                // when they are in the background.
+                intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+                intent.setComponent(null);
+                // All running users will be notified of the received sms.
+                Bundle options = handleSmsWhitelisting(null);
+
+                dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
+                        AppOpsManager.OP_RECEIVE_SMS,
+                        options, this, UserHandle.ALL);
+            } else if (action.equals(Intents.WAP_PUSH_DELIVER_ACTION)) {
+                // Now dispatch the notification only intent
+                intent.setAction(Intents.WAP_PUSH_RECEIVED_ACTION);
+                intent.setComponent(null);
+                // Allow registered broadcast receivers to get this intent even
+                // when they are in the background.
+                intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+                // Only the primary user will receive notification of incoming mms.
+                // That app will do the actual downloading of the mms.
+                Bundle options = null;
+                try {
+                    long duration = mDeviceIdleController.addPowerSaveTempWhitelistAppForMms(
+                            mContext.getPackageName(), 0, "mms-broadcast");
+                    BroadcastOptions bopts = BroadcastOptions.makeBasic();
+                    bopts.setTemporaryAppWhitelistDuration(duration);
+                    options = bopts.toBundle();
+                } catch (RemoteException e) {
+                }
+
+                String mimeType = intent.getType();
+                dispatchIntent(intent, WapPushOverSms.getPermissionForType(mimeType),
+                        WapPushOverSms.getAppOpsPermissionForIntent(mimeType), options, this,
+                        UserHandle.SYSTEM);
+            } else {
+                // Now that the intents have been deleted we can clean up the PDU data.
+                if (!Intents.DATA_SMS_RECEIVED_ACTION.equals(action)
+                        && !Intents.SMS_RECEIVED_ACTION.equals(action)
+                        && !Intents.DATA_SMS_RECEIVED_ACTION.equals(action)
+                        && !Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) {
+                    loge("unexpected BroadcastReceiver action: " + action);
+                }
+
+                int rc = getResultCode();
+                if ((rc != Activity.RESULT_OK) && (rc != Intents.RESULT_SMS_HANDLED)) {
+                    loge("a broadcast receiver set the result code to " + rc
+                            + ", deleting from raw table anyway!");
+                } else if (DBG) {
+                    log("successful broadcast, deleting from raw table.");
+                }
+
+                deleteFromRawTable(mDeleteWhere, mDeleteWhereArgs, MARK_DELETED);
+                sendMessage(EVENT_BROADCAST_COMPLETE);
+
+                int durationMillis = (int) ((System.nanoTime() - mBroadcastTimeNano) / 1000000);
+                if (durationMillis >= 5000) {
+                    loge("Slow ordered broadcast completion time: " + durationMillis + " ms");
+                } else if (DBG) {
+                    log("ordered broadcast completed in: " + durationMillis + " ms");
+                }
+            }
+        }
+    }
+
+    /**
+     * Callback that handles filtering results by carrier services.
+     */
+    private final class CarrierServicesSmsFilterCallback implements
+            CarrierServicesSmsFilter.CarrierServicesSmsFilterCallbackInterface {
+        private final byte[][] mPdus;
+        private final int mDestPort;
+        private final String mSmsFormat;
+        private final SmsBroadcastReceiver mSmsBroadcastReceiver;
+        private final boolean mUserUnlocked;
+
+        CarrierServicesSmsFilterCallback(byte[][] pdus, int destPort, String smsFormat,
+                         SmsBroadcastReceiver smsBroadcastReceiver,  boolean userUnlocked) {
+            mPdus = pdus;
+            mDestPort = destPort;
+            mSmsFormat = smsFormat;
+            mSmsBroadcastReceiver = smsBroadcastReceiver;
+            mUserUnlocked = userUnlocked;
+        }
+
+        @Override
+        public void onFilterComplete(int result) {
+            logv("onFilterComplete: result is " + result);
+            if ((result & CarrierMessagingService.RECEIVE_OPTIONS_DROP) == 0) {
+                if (VisualVoicemailSmsFilter.filter(mContext, mPdus,
+                        mSmsFormat, mDestPort, mPhone.getSubId())) {
+                    log("Visual voicemail SMS dropped");
+                    dropSms(mSmsBroadcastReceiver);
+                    return;
+                }
+
+                if (mUserUnlocked) {
+                    dispatchSmsDeliveryIntent(
+                            mPdus, mSmsFormat, mDestPort, mSmsBroadcastReceiver);
+                } else {
+                    // Don't do anything further, leave the message in the raw table if the
+                    // credential-encrypted storage is still locked and show the new message
+                    // notification if the message is visible to the user.
+                    if (!isSkipNotifyFlagSet(result)) {
+                        showNewMessageNotification();
+                    }
+                    sendMessage(EVENT_BROADCAST_COMPLETE);
+                }
+            } else {
+                // Drop this SMS.
+                dropSms(mSmsBroadcastReceiver);
+            }
+        }
+    }
+
+    private void dropSms(SmsBroadcastReceiver receiver) {
+        // Needs phone package permissions.
+        deleteFromRawTable(receiver.mDeleteWhere, receiver.mDeleteWhereArgs, MARK_DELETED);
+        sendMessage(EVENT_BROADCAST_COMPLETE);
+    }
+
+    /** Checks whether the flag to skip new message notification is set in the bitmask returned
+     *  from the carrier app.
+     */
+    private boolean isSkipNotifyFlagSet(int callbackResult) {
+        return (callbackResult
+            & RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE) > 0;
+    }
+
+    /**
+     * Log with debug level.
+     * @param s the string to log
+     */
+    @Override
+    protected void log(String s) {
+        Rlog.d(getName(), s);
+    }
+
+    /**
+     * Log with error level.
+     * @param s the string to log
+     */
+    @Override
+    protected void loge(String s) {
+        Rlog.e(getName(), s);
+    }
+
+    /**
+     * Log with error level.
+     * @param s the string to log
+     * @param e is a Throwable which logs additional information.
+     */
+    @Override
+    protected void loge(String s, Throwable e) {
+        Rlog.e(getName(), s, e);
+    }
+
+    /**
+     * Store a received SMS into Telephony provider
+     *
+     * @param intent The intent containing the received SMS
+     * @return The URI of written message
+     */
+    private Uri writeInboxMessage(Intent intent) {
+        final SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
+        if (messages == null || messages.length < 1) {
+            loge("Failed to parse SMS pdu");
+            return null;
+        }
+        // Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access
+        // the methods on it although the SmsMessage itself is not null. So do this check
+        // before we do anything on the parsed SmsMessages.
+        for (final SmsMessage sms : messages) {
+            try {
+                sms.getDisplayMessageBody();
+            } catch (NullPointerException e) {
+                loge("NPE inside SmsMessage");
+                return null;
+            }
+        }
+        final ContentValues values = parseSmsMessage(messages);
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            return mContext.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, values);
+        } catch (Exception e) {
+            loge("Failed to persist inbox message", e);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        return null;
+    }
+
+    /**
+     * Convert SmsMessage[] into SMS database schema columns
+     *
+     * @param msgs The SmsMessage array of the received SMS
+     * @return ContentValues representing the columns of parsed SMS
+     */
+    private static ContentValues parseSmsMessage(SmsMessage[] msgs) {
+        final SmsMessage sms = msgs[0];
+        final ContentValues values = new ContentValues();
+        values.put(Telephony.Sms.Inbox.ADDRESS, sms.getDisplayOriginatingAddress());
+        values.put(Telephony.Sms.Inbox.BODY, buildMessageBodyFromPdus(msgs));
+        values.put(Telephony.Sms.Inbox.DATE_SENT, sms.getTimestampMillis());
+        values.put(Telephony.Sms.Inbox.DATE, System.currentTimeMillis());
+        values.put(Telephony.Sms.Inbox.PROTOCOL, sms.getProtocolIdentifier());
+        values.put(Telephony.Sms.Inbox.SEEN, 0);
+        values.put(Telephony.Sms.Inbox.READ, 0);
+        final String subject = sms.getPseudoSubject();
+        if (!TextUtils.isEmpty(subject)) {
+            values.put(Telephony.Sms.Inbox.SUBJECT, subject);
+        }
+        values.put(Telephony.Sms.Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
+        values.put(Telephony.Sms.Inbox.SERVICE_CENTER, sms.getServiceCenterAddress());
+        return values;
+    }
+
+    /**
+     * Build up the SMS message body from the SmsMessage array of received SMS
+     *
+     * @param msgs The SmsMessage array of the received SMS
+     * @return The text message body
+     */
+    private static String buildMessageBodyFromPdus(SmsMessage[] msgs) {
+        if (msgs.length == 1) {
+            // There is only one part, so grab the body directly.
+            return replaceFormFeeds(msgs[0].getDisplayMessageBody());
+        } else {
+            // Build up the body from the parts.
+            StringBuilder body = new StringBuilder();
+            for (SmsMessage msg: msgs) {
+                // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
+                body.append(msg.getDisplayMessageBody());
+            }
+            return replaceFormFeeds(body.toString());
+        }
+    }
+
+    // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
+    private static String replaceFormFeeds(String s) {
+        return s == null ? "" : s.replace('\f', '\n');
+    }
+
+    @VisibleForTesting
+    public PowerManager.WakeLock getWakeLock() {
+        return mWakeLock;
+    }
+
+    @VisibleForTesting
+    public int getWakeLockTimeout() {
+        return mWakeLockTimeout;
+    }
+
+    /**
+    * Sets the wakelock timeout to {@link timeOut} milliseconds
+    */
+    private void setWakeLockTimeout(int timeOut) {
+        mWakeLockTimeout = timeOut;
+    }
+
+    /**
+     * Handler for the broadcast sent when the new message notification is clicked. It launches the
+     * default SMS app.
+     */
+    private static class NewMessageNotificationActionReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_OPEN_SMS_APP.equals(intent.getAction())) {
+                context.startActivity(context.getPackageManager().getLaunchIntentForPackage(
+                    Telephony.Sms.getDefaultSmsPackage(context)));
+            }
+        }
+    }
+
+    /**
+     * Registers the broadcast receiver to launch the default SMS app when the user clicks the
+     * new message notification.
+     */
+    static void registerNewMessageNotificationActionHandler(Context context) {
+        IntentFilter userFilter = new IntentFilter();
+        userFilter.addAction(ACTION_OPEN_SMS_APP);
+        context.registerReceiver(new NewMessageNotificationActionReceiver(), userFilter);
+    }
+}
diff --git a/com/android/internal/telephony/InboundSmsTracker.java b/com/android/internal/telephony/InboundSmsTracker.java
new file mode 100644
index 0000000..c63ccc8
--- /dev/null
+++ b/com/android/internal/telephony/InboundSmsTracker.java
@@ -0,0 +1,372 @@
+/*
+ * 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.internal.telephony;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * Tracker for an incoming SMS message ready to broadcast to listeners.
+ * This is similar to {@link com.android.internal.telephony.SMSDispatcher.SmsTracker} used for
+ * outgoing messages.
+ */
+public class InboundSmsTracker {
+
+    // Fields for single and multi-part messages
+    private final byte[] mPdu;
+    private final long mTimestamp;
+    private final int mDestPort;
+    private final boolean mIs3gpp2;
+    private final boolean mIs3gpp2WapPdu;
+    private final String mMessageBody;
+
+    // Fields for concatenating multi-part SMS messages
+    private final String mAddress;
+    private final int mReferenceNumber;
+    private final int mSequenceNumber;
+    private final int mMessageCount;
+
+    // Fields for deleting this message after delivery
+    private String mDeleteWhere;
+    private String[] mDeleteWhereArgs;
+
+    /**
+     * Copied from SmsMessageBase#getDisplayOriginatingAddress used for blocking messages.
+     * DisplayAddress could be email address if this message was from an email gateway, otherwise
+     * same as mAddress. Email gateway might set a generic gateway address as the mAddress which
+     * could not be used for blocking check and append the display email address at the beginning
+     * of the message body. In that case, display email address is only available for the first SMS
+     * in the Multi-part SMS.
+     */
+    private final String mDisplayAddress;
+
+    @VisibleForTesting
+    /** Destination port flag bit for no destination port. */
+    public static final int DEST_PORT_FLAG_NO_PORT = (1 << 16);
+
+    /** Destination port flag bit to indicate 3GPP format message. */
+    private static final int DEST_PORT_FLAG_3GPP = (1 << 17);
+
+    @VisibleForTesting
+    /** Destination port flag bit to indicate 3GPP2 format message. */
+    public static final int DEST_PORT_FLAG_3GPP2 = (1 << 18);
+
+    @VisibleForTesting
+    /** Destination port flag bit to indicate 3GPP2 format WAP message. */
+    public static final int DEST_PORT_FLAG_3GPP2_WAP_PDU = (1 << 19);
+
+    /** Destination port mask (16-bit unsigned value on GSM and CDMA). */
+    private static final int DEST_PORT_MASK = 0xffff;
+
+    @VisibleForTesting
+    public static final String SELECT_BY_REFERENCE = "address=? AND reference_number=? AND "
+            + "count=? AND (destination_port & " + DEST_PORT_FLAG_3GPP2_WAP_PDU
+            + "=0) AND deleted=0";
+
+    @VisibleForTesting
+    public static final String SELECT_BY_REFERENCE_3GPP2WAP = "address=? AND reference_number=? "
+            + "AND count=? AND (destination_port & "
+            + DEST_PORT_FLAG_3GPP2_WAP_PDU + "=" + DEST_PORT_FLAG_3GPP2_WAP_PDU + ") AND deleted=0";
+
+    @VisibleForTesting
+    public static final String SELECT_BY_DUPLICATE_REFERENCE = "address=? AND "
+            + "reference_number=? AND count=? AND sequence=? AND "
+            + "((date=? AND message_body=?) OR deleted=0) AND (destination_port & "
+            + DEST_PORT_FLAG_3GPP2_WAP_PDU + "=0)";
+
+    @VisibleForTesting
+    public static final String SELECT_BY_DUPLICATE_REFERENCE_3GPP2WAP = "address=? AND "
+            + "reference_number=? " + "AND count=? AND sequence=? AND "
+            + "((date=? AND message_body=?) OR deleted=0) AND "
+            + "(destination_port & " + DEST_PORT_FLAG_3GPP2_WAP_PDU + "="
+            + DEST_PORT_FLAG_3GPP2_WAP_PDU + ")";
+
+    /**
+     * Create a tracker for a single-part SMS.
+     *
+     * @param pdu the message PDU
+     * @param timestamp the message timestamp
+     * @param destPort the destination port
+     * @param is3gpp2 true for 3GPP2 format; false for 3GPP format
+     * @param is3gpp2WapPdu true for 3GPP2 format WAP PDU; false otherwise
+     * @param address originating address
+     * @param displayAddress email address if this message was from an email gateway, otherwise same
+     *                       as originating address
+     */
+    public InboundSmsTracker(byte[] pdu, long timestamp, int destPort, boolean is3gpp2,
+            boolean is3gpp2WapPdu, String address, String displayAddress, String messageBody) {
+        mPdu = pdu;
+        mTimestamp = timestamp;
+        mDestPort = destPort;
+        mIs3gpp2 = is3gpp2;
+        mIs3gpp2WapPdu = is3gpp2WapPdu;
+        mMessageBody = messageBody;
+        mAddress = address;
+        mDisplayAddress = displayAddress;
+        // fields for multi-part SMS
+        mReferenceNumber = -1;
+        mSequenceNumber = getIndexOffset();     // 0 or 1, depending on type
+        mMessageCount = 1;
+    }
+
+    /**
+     * Create a tracker for a multi-part SMS. Sequence numbers start at 1 for 3GPP and regular
+     * concatenated 3GPP2 messages, but CDMA WAP push sequence numbers start at 0. The caller will
+     * subtract 1 if necessary so that the sequence number is always 0-based. When loading and
+     * saving to the raw table, the sequence number is adjusted if necessary for backwards
+     * compatibility.
+     *
+     * @param pdu the message PDU
+     * @param timestamp the message timestamp
+     * @param destPort the destination port
+     * @param is3gpp2 true for 3GPP2 format; false for 3GPP format
+     * @param address originating address, or email if this message was from an email gateway
+     * @param displayAddress email address if this message was from an email gateway, otherwise same
+     *                       as originating address
+     * @param referenceNumber the concatenated reference number
+     * @param sequenceNumber the sequence number of this segment (0-based)
+     * @param messageCount the total number of segments
+     * @param is3gpp2WapPdu true for 3GPP2 format WAP PDU; false otherwise
+     */
+    public InboundSmsTracker(byte[] pdu, long timestamp, int destPort, boolean is3gpp2,
+            String address, String displayAddress, int referenceNumber, int sequenceNumber,
+            int messageCount, boolean is3gpp2WapPdu, String messageBody) {
+        mPdu = pdu;
+        mTimestamp = timestamp;
+        mDestPort = destPort;
+        mIs3gpp2 = is3gpp2;
+        mIs3gpp2WapPdu = is3gpp2WapPdu;
+        mMessageBody = messageBody;
+        // fields used for check blocking message
+        mDisplayAddress = displayAddress;
+        // fields for multi-part SMS
+        mAddress = address;
+        mReferenceNumber = referenceNumber;
+        mSequenceNumber = sequenceNumber;
+        mMessageCount = messageCount;
+    }
+
+    /**
+     * Create a new tracker from the row of the raw table pointed to by Cursor.
+     * Since this constructor is used only for recovery during startup, the Dispatcher is null.
+     * @param cursor a Cursor pointing to the row to construct this SmsTracker for
+     */
+    public InboundSmsTracker(Cursor cursor, boolean isCurrentFormat3gpp2) {
+        mPdu = HexDump.hexStringToByteArray(cursor.getString(InboundSmsHandler.PDU_COLUMN));
+
+        if (cursor.isNull(InboundSmsHandler.DESTINATION_PORT_COLUMN)) {
+            mDestPort = -1;
+            mIs3gpp2 = isCurrentFormat3gpp2;
+            mIs3gpp2WapPdu = false;
+        } else {
+            int destPort = cursor.getInt(InboundSmsHandler.DESTINATION_PORT_COLUMN);
+            if ((destPort & DEST_PORT_FLAG_3GPP) != 0) {
+                mIs3gpp2 = false;
+            } else if ((destPort & DEST_PORT_FLAG_3GPP2) != 0) {
+                mIs3gpp2 = true;
+            } else {
+                mIs3gpp2 = isCurrentFormat3gpp2;
+            }
+            mIs3gpp2WapPdu = ((destPort & DEST_PORT_FLAG_3GPP2_WAP_PDU) != 0);
+            mDestPort = getRealDestPort(destPort);
+        }
+
+        mTimestamp = cursor.getLong(InboundSmsHandler.DATE_COLUMN);
+        mAddress = cursor.getString(InboundSmsHandler.ADDRESS_COLUMN);
+        mDisplayAddress = cursor.getString(InboundSmsHandler.DISPLAY_ADDRESS_COLUMN);
+
+        if (cursor.isNull(InboundSmsHandler.COUNT_COLUMN)) {
+            // single-part message
+            long rowId = cursor.getLong(InboundSmsHandler.ID_COLUMN);
+            mReferenceNumber = -1;
+            mSequenceNumber = getIndexOffset();     // 0 or 1, depending on type
+            mMessageCount = 1;
+            mDeleteWhere = InboundSmsHandler.SELECT_BY_ID;
+            mDeleteWhereArgs = new String[]{Long.toString(rowId)};
+        } else {
+            // multi-part message
+            mReferenceNumber = cursor.getInt(InboundSmsHandler.REFERENCE_NUMBER_COLUMN);
+            mMessageCount = cursor.getInt(InboundSmsHandler.COUNT_COLUMN);
+
+            // GSM sequence numbers start at 1; CDMA WDP datagram sequence numbers start at 0
+            mSequenceNumber = cursor.getInt(InboundSmsHandler.SEQUENCE_COLUMN);
+            int index = mSequenceNumber - getIndexOffset();
+
+            if (index < 0 || index >= mMessageCount) {
+                throw new IllegalArgumentException("invalid PDU sequence " + mSequenceNumber
+                        + " of " + mMessageCount);
+            }
+
+            mDeleteWhere = getQueryForSegments();
+            mDeleteWhereArgs = new String[]{mAddress,
+                    Integer.toString(mReferenceNumber), Integer.toString(mMessageCount)};
+        }
+        mMessageBody = cursor.getString(InboundSmsHandler.MESSAGE_BODY_COLUMN);
+    }
+
+    public ContentValues getContentValues() {
+        ContentValues values = new ContentValues();
+        values.put("pdu", HexDump.toHexString(mPdu));
+        values.put("date", mTimestamp);
+        // Always set the destination port, since it now contains message format flags.
+        // Port is a 16-bit value, or -1, so clear the upper bits before setting flags.
+        int destPort;
+        if (mDestPort == -1) {
+            destPort = DEST_PORT_FLAG_NO_PORT;
+        } else {
+            destPort = mDestPort & DEST_PORT_MASK;
+        }
+        if (mIs3gpp2) {
+            destPort |= DEST_PORT_FLAG_3GPP2;
+        } else {
+            destPort |= DEST_PORT_FLAG_3GPP;
+        }
+        if (mIs3gpp2WapPdu) {
+            destPort |= DEST_PORT_FLAG_3GPP2_WAP_PDU;
+        }
+        values.put("destination_port", destPort);
+        if (mAddress != null) {
+            values.put("address", mAddress);
+            values.put("display_originating_addr", mDisplayAddress);
+            values.put("reference_number", mReferenceNumber);
+            values.put("sequence", mSequenceNumber);
+            values.put("count", mMessageCount);
+        }
+        values.put("message_body", mMessageBody);
+        return values;
+    }
+
+    /**
+     * Get the port number, or -1 if there is no destination port.
+     * @param destPort the destination port value, with flags
+     * @return the real destination port, or -1 for no port
+     */
+    public static int getRealDestPort(int destPort) {
+        if ((destPort & DEST_PORT_FLAG_NO_PORT) != 0) {
+            return -1;
+        } else {
+           return destPort & DEST_PORT_MASK;
+        }
+    }
+
+    /**
+     * Update the values to delete all rows of the message from raw table.
+     * @param deleteWhere the selection to use
+     * @param deleteWhereArgs the selection args to use
+     */
+    public void setDeleteWhere(String deleteWhere, String[] deleteWhereArgs) {
+        mDeleteWhere = deleteWhere;
+        mDeleteWhereArgs = deleteWhereArgs;
+    }
+
+    public String toString() {
+        StringBuilder builder = new StringBuilder("SmsTracker{timestamp=");
+        builder.append(new Date(mTimestamp));
+        builder.append(" destPort=").append(mDestPort);
+        builder.append(" is3gpp2=").append(mIs3gpp2);
+        if (mAddress != null) {
+            builder.append(" address=").append(mAddress);
+            builder.append(" display_originating_addr=").append(mDisplayAddress);
+            builder.append(" refNumber=").append(mReferenceNumber);
+            builder.append(" seqNumber=").append(mSequenceNumber);
+            builder.append(" msgCount=").append(mMessageCount);
+        }
+        if (mDeleteWhere != null) {
+            builder.append(" deleteWhere(").append(mDeleteWhere);
+            builder.append(") deleteArgs=(").append(Arrays.toString(mDeleteWhereArgs));
+            builder.append(')');
+        }
+        builder.append('}');
+        return builder.toString();
+    }
+
+    public byte[] getPdu() {
+        return mPdu;
+    }
+
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    public int getDestPort() {
+        return mDestPort;
+    }
+
+    public boolean is3gpp2() {
+        return mIs3gpp2;
+    }
+
+    public String getFormat() {
+        return mIs3gpp2 ? SmsConstants.FORMAT_3GPP2 : SmsConstants.FORMAT_3GPP;
+    }
+
+    public String getQueryForSegments() {
+        return mIs3gpp2WapPdu ? SELECT_BY_REFERENCE_3GPP2WAP : SELECT_BY_REFERENCE;
+    }
+
+    public String getQueryForMultiPartDuplicates() {
+        return mIs3gpp2WapPdu ? SELECT_BY_DUPLICATE_REFERENCE_3GPP2WAP :
+                SELECT_BY_DUPLICATE_REFERENCE;
+    }
+
+    /**
+     * Sequence numbers for concatenated messages start at 1. The exception is CDMA WAP PDU
+     * messages, which use a 0-based index.
+     * @return the offset to use to convert between mIndex and the sequence number
+     */
+    public int getIndexOffset() {
+        return (mIs3gpp2 && mIs3gpp2WapPdu) ? 0 : 1;
+    }
+
+    public String getAddress() {
+        return mAddress;
+    }
+
+    public String getDisplayAddress() {
+        return mDisplayAddress;
+    }
+
+    public String getMessageBody() {
+        return mMessageBody;
+    }
+
+    public int getReferenceNumber() {
+        return mReferenceNumber;
+    }
+
+    public int getSequenceNumber() {
+        return mSequenceNumber;
+    }
+
+    public int getMessageCount() {
+        return mMessageCount;
+    }
+
+    public String getDeleteWhere() {
+        return mDeleteWhere;
+    }
+
+    public String[] getDeleteWhereArgs() {
+        return mDeleteWhereArgs;
+    }
+}
diff --git a/com/android/internal/telephony/IntRangeManager.java b/com/android/internal/telephony/IntRangeManager.java
new file mode 100644
index 0000000..3da2cc5
--- /dev/null
+++ b/com/android/internal/telephony/IntRangeManager.java
@@ -0,0 +1,669 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Clients can enable reception of SMS-CB messages for specific ranges of
+ * message identifiers (channels). This class keeps track of the currently
+ * enabled message identifiers and calls abstract methods to update the
+ * radio when the range of enabled message identifiers changes.
+ *
+ * An update is a call to {@link #startUpdate} followed by zero or more
+ * calls to {@link #addRange} followed by a call to {@link #finishUpdate}.
+ * Calls to {@link #enableRange} and {@link #disableRange} will perform
+ * an incremental update operation if the enabled ranges have changed.
+ * A full update operation (i.e. after a radio reset) can be performed
+ * by a call to {@link #updateRanges}.
+ *
+ * Clients are identified by String (the name associated with the User ID
+ * of the caller) so that a call to remove a range can be mapped to the
+ * client that enabled that range (or else rejected).
+ */
+public abstract class IntRangeManager {
+
+    /**
+     * Initial capacity for IntRange clients array list. There will be
+     * few cell broadcast listeners on a typical device, so this can be small.
+     */
+    private static final int INITIAL_CLIENTS_ARRAY_SIZE = 4;
+
+    /**
+     * One or more clients forming the continuous range [startId, endId].
+     * <p>When a client is added, the IntRange may merge with one or more
+     * adjacent IntRanges to form a single combined IntRange.
+     * <p>When a client is removed, the IntRange may divide into several
+     * non-contiguous IntRanges.
+     */
+    private class IntRange {
+        int mStartId;
+        int mEndId;
+        // sorted by earliest start id
+        final ArrayList<ClientRange> mClients;
+
+        /**
+         * Create a new IntRange with a single client.
+         * @param startId the first id included in the range
+         * @param endId the last id included in the range
+         * @param client the client requesting the enabled range
+         */
+        IntRange(int startId, int endId, String client) {
+            mStartId = startId;
+            mEndId = endId;
+            mClients = new ArrayList<ClientRange>(INITIAL_CLIENTS_ARRAY_SIZE);
+            mClients.add(new ClientRange(startId, endId, client));
+        }
+
+        /**
+         * Create a new IntRange for an existing ClientRange.
+         * @param clientRange the initial ClientRange to add
+         */
+        IntRange(ClientRange clientRange) {
+            mStartId = clientRange.mStartId;
+            mEndId = clientRange.mEndId;
+            mClients = new ArrayList<ClientRange>(INITIAL_CLIENTS_ARRAY_SIZE);
+            mClients.add(clientRange);
+        }
+
+        /**
+         * Create a new IntRange from an existing IntRange. This is used for
+         * removing a ClientRange, because new IntRanges may need to be created
+         * for any gaps that open up after the ClientRange is removed. A copy
+         * is made of the elements of the original IntRange preceding the element
+         * that is being removed. The following elements will be added to this
+         * IntRange or to a new IntRange when a gap is found.
+         * @param intRange the original IntRange to copy elements from
+         * @param numElements the number of elements to copy from the original
+         */
+        IntRange(IntRange intRange, int numElements) {
+            mStartId = intRange.mStartId;
+            mEndId = intRange.mEndId;
+            mClients = new ArrayList<ClientRange>(intRange.mClients.size());
+            for (int i=0; i < numElements; i++) {
+                mClients.add(intRange.mClients.get(i));
+            }
+        }
+
+        /**
+         * Insert new ClientRange in order by start id, then by end id
+         * <p>If the new ClientRange is known to be sorted before or after the
+         * existing ClientRanges, or at a particular index, it can be added
+         * to the clients array list directly, instead of via this method.
+         * <p>Note that this can be changed from linear to binary search if the
+         * number of clients grows large enough that it would make a difference.
+         * @param range the new ClientRange to insert
+         */
+        void insert(ClientRange range) {
+            int len = mClients.size();
+            int insert = -1;
+            for (int i=0; i < len; i++) {
+                ClientRange nextRange = mClients.get(i);
+                if (range.mStartId <= nextRange.mStartId) {
+                    // ignore duplicate ranges from the same client
+                    if (!range.equals(nextRange)) {
+                        // check if same startId, then order by endId
+                        if (range.mStartId == nextRange.mStartId
+                                && range.mEndId > nextRange.mEndId) {
+                            insert = i + 1;
+                            if (insert < len) {
+                                // there may be more client following with same startId
+                                // new [1, 5] existing [1, 2] [1, 4] [1, 7]
+                                continue;
+                            }
+                            break;
+                        }
+                        mClients.add(i, range);
+                    }
+                    return;
+                }
+            }
+            if (insert != -1 && insert < len) {
+                mClients.add(insert, range);
+                return;
+            }
+            mClients.add(range);    // append to end of list
+        }
+    }
+
+    /**
+     * The message id range for a single client.
+     */
+    private class ClientRange {
+        final int mStartId;
+        final int mEndId;
+        final String mClient;
+
+        ClientRange(int startId, int endId, String client) {
+            mStartId = startId;
+            mEndId = endId;
+            mClient = client;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o != null && o instanceof ClientRange) {
+                ClientRange other = (ClientRange) o;
+                return mStartId == other.mStartId &&
+                        mEndId == other.mEndId &&
+                        mClient.equals(other.mClient);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return (mStartId * 31 + mEndId) * 31 + mClient.hashCode();
+        }
+    }
+
+    /**
+     * List of integer ranges, one per client, sorted by start id.
+     */
+    private ArrayList<IntRange> mRanges = new ArrayList<IntRange>();
+
+    protected IntRangeManager() {}
+
+    /**
+     * Enable a range for the specified client and update ranges
+     * if necessary. If {@link #finishUpdate} returns failure,
+     * false is returned and the range is not added.
+     *
+     * @param startId the first id included in the range
+     * @param endId the last id included in the range
+     * @param client the client requesting the enabled range
+     * @return true if successful, false otherwise
+     */
+    public synchronized boolean enableRange(int startId, int endId, String client) {
+        int len = mRanges.size();
+
+        // empty range list: add the initial IntRange
+        if (len == 0) {
+            if (tryAddRanges(startId, endId, true)) {
+                mRanges.add(new IntRange(startId, endId, client));
+                return true;
+            } else {
+                return false;   // failed to update radio
+            }
+        }
+
+        for (int startIndex = 0; startIndex < len; startIndex++) {
+            IntRange range = mRanges.get(startIndex);
+            if ((startId) >= range.mStartId && (endId) <= range.mEndId) {
+                // exact same range:  new [1, 1] existing [1, 1]
+                // range already enclosed in existing: new [3, 3], [1,3]
+                // no radio update necessary.
+                // duplicate "client" check is done in insert, attempt to insert.
+                range.insert(new ClientRange(startId, endId, client));
+                return true;
+            } else if ((startId - 1) == range.mEndId) {
+                // new [3, x] existing [1, 2]  OR new [2, 2] existing [1, 1]
+                // found missing link? check if next range can be joined
+                int newRangeEndId = endId;
+                IntRange nextRange = null;
+                if ((startIndex + 1) < len) {
+                    nextRange = mRanges.get(startIndex + 1);
+                    if ((nextRange.mStartId - 1) <= endId) {
+                        // new [3, x] existing [1, 2] [5, 7] OR  new [2 , 2] existing [1, 1] [3, 5]
+                        if (endId <= nextRange.mEndId) {
+                            // new [3, 6] existing [1, 2] [5, 7]
+                            newRangeEndId = nextRange.mStartId - 1; // need to enable [3, 4]
+                        }
+                    } else {
+                        // mark nextRange to be joined as null.
+                        nextRange = null;
+                    }
+                }
+                if (tryAddRanges(startId, newRangeEndId, true)) {
+                    range.mEndId = endId;
+                    range.insert(new ClientRange(startId, endId, client));
+
+                    // found missing link? check if next range can be joined
+                    if (nextRange != null) {
+                        if (range.mEndId < nextRange.mEndId) {
+                            // new [3, 6] existing [1, 2] [5, 10]
+                            range.mEndId = nextRange.mEndId;
+                        }
+                        range.mClients.addAll(nextRange.mClients);
+                        mRanges.remove(nextRange);
+                    }
+                    return true;
+                } else {
+                    return false;   // failed to update radio
+                }
+            } else if (startId < range.mStartId) {
+                // new [1, x] , existing [5, y]
+                // test if new range completely precedes this range
+                // note that [1, 4] and [5, 6] coalesce to [1, 6]
+                if ((endId + 1) < range.mStartId) {
+                    // new [1, 3] existing [5, 6] non contiguous case
+                    // insert new int range before previous first range
+                    if (tryAddRanges(startId, endId, true)) {
+                        mRanges.add(startIndex, new IntRange(startId, endId, client));
+                        return true;
+                    } else {
+                        return false;   // failed to update radio
+                    }
+                } else if (endId <= range.mEndId) {
+                    // new [1, 4] existing [5, 6]  or  new [1, 1] existing [2, 2]
+                    // extend the start of this range
+                    if (tryAddRanges(startId, range.mStartId - 1, true)) {
+                        range.mStartId = startId;
+                        range.mClients.add(0, new ClientRange(startId, endId, client));
+                        return true;
+                    } else {
+                        return false;   // failed to update radio
+                    }
+                } else {
+                    // find last range that can coalesce into the new combined range
+                    for (int endIndex = startIndex+1; endIndex < len; endIndex++) {
+                        IntRange endRange = mRanges.get(endIndex);
+                        if ((endId + 1) < endRange.mStartId) {
+                            // new [1, 10] existing [2, 3] [14, 15]
+                            // try to add entire new range
+                            if (tryAddRanges(startId, endId, true)) {
+                                range.mStartId = startId;
+                                range.mEndId = endId;
+                                // insert new ClientRange before existing ranges
+                                range.mClients.add(0, new ClientRange(startId, endId, client));
+                                // coalesce range with following ranges up to endIndex-1
+                                // remove each range after adding its elements, so the index
+                                // of the next range to join is always startIndex+1.
+                                // i is the index if no elements were removed: we only care
+                                // about the number of loop iterations, not the value of i.
+                                int joinIndex = startIndex + 1;
+                                for (int i = joinIndex; i < endIndex; i++) {
+                                    // new [1, 10] existing [2, 3] [5, 6] [14, 15]
+                                    IntRange joinRange = mRanges.get(joinIndex);
+                                    range.mClients.addAll(joinRange.mClients);
+                                    mRanges.remove(joinRange);
+                                }
+                                return true;
+                            } else {
+                                return false;   // failed to update radio
+                            }
+                        } else if (endId <= endRange.mEndId) {
+                            // new [1, 10] existing [2, 3] [5, 15]
+                            // add range from start id to start of last overlapping range,
+                            // values from endRange.startId to endId are already enabled
+                            if (tryAddRanges(startId, endRange.mStartId - 1, true)) {
+                                range.mStartId = startId;
+                                range.mEndId = endRange.mEndId;
+                                // insert new ClientRange before existing ranges
+                                range.mClients.add(0, new ClientRange(startId, endId, client));
+                                // coalesce range with following ranges up to endIndex
+                                // remove each range after adding its elements, so the index
+                                // of the next range to join is always startIndex+1.
+                                // i is the index if no elements were removed: we only care
+                                // about the number of loop iterations, not the value of i.
+                                int joinIndex = startIndex + 1;
+                                for (int i = joinIndex; i <= endIndex; i++) {
+                                    IntRange joinRange = mRanges.get(joinIndex);
+                                    range.mClients.addAll(joinRange.mClients);
+                                    mRanges.remove(joinRange);
+                                }
+                                return true;
+                            } else {
+                                return false;   // failed to update radio
+                            }
+                        }
+                    }
+
+                    // new [1, 10] existing [2, 3]
+                    // endId extends past all existing IntRanges: combine them all together
+                    if (tryAddRanges(startId, endId, true)) {
+                        range.mStartId = startId;
+                        range.mEndId = endId;
+                        // insert new ClientRange before existing ranges
+                        range.mClients.add(0, new ClientRange(startId, endId, client));
+                        // coalesce range with following ranges up to len-1
+                        // remove each range after adding its elements, so the index
+                        // of the next range to join is always startIndex+1.
+                        // i is the index if no elements were removed: we only care
+                        // about the number of loop iterations, not the value of i.
+                        int joinIndex = startIndex + 1;
+                        for (int i = joinIndex; i < len; i++) {
+                            // new [1, 10] existing [2, 3] [5, 6]
+                            IntRange joinRange = mRanges.get(joinIndex);
+                            range.mClients.addAll(joinRange.mClients);
+                            mRanges.remove(joinRange);
+                        }
+                        return true;
+                    } else {
+                        return false;   // failed to update radio
+                    }
+                }
+            } else if ((startId + 1) <= range.mEndId) {
+                // new [2, x] existing [1, 4]
+                if (endId <= range.mEndId) {
+                    // new [2, 3] existing [1, 4]
+                    // completely contained in existing range; no radio changes
+                    range.insert(new ClientRange(startId, endId, client));
+                    return true;
+                } else {
+                    // new [2, 5] existing [1, 4]
+                    // find last range that can coalesce into the new combined range
+                    int endIndex = startIndex;
+                    for (int testIndex = startIndex+1; testIndex < len; testIndex++) {
+                        IntRange testRange = mRanges.get(testIndex);
+                        if ((endId + 1) < testRange.mStartId) {
+                            break;
+                        } else {
+                            endIndex = testIndex;
+                        }
+                    }
+                    // no adjacent IntRanges to combine
+                    if (endIndex == startIndex) {
+                        // new [2, 5] existing [1, 4]
+                        // add range from range.endId+1 to endId,
+                        // values from startId to range.endId are already enabled
+                        if (tryAddRanges(range.mEndId + 1, endId, true)) {
+                            range.mEndId = endId;
+                            range.insert(new ClientRange(startId, endId, client));
+                            return true;
+                        } else {
+                            return false;   // failed to update radio
+                        }
+                    }
+                    // get last range to coalesce into start range
+                    IntRange endRange = mRanges.get(endIndex);
+                    // Values from startId to range.endId have already been enabled.
+                    // if endId > endRange.endId, then enable range from range.endId+1 to endId,
+                    // else enable range from range.endId+1 to endRange.startId-1, because
+                    // values from endRange.startId to endId have already been added.
+                    int newRangeEndId = (endId <= endRange.mEndId) ? endRange.mStartId - 1 : endId;
+                    // new [2, 10] existing [1, 4] [7, 8] OR
+                    // new [2, 10] existing [1, 4] [7, 15]
+                    if (tryAddRanges(range.mEndId + 1, newRangeEndId, true)) {
+                        newRangeEndId = (endId <= endRange.mEndId) ? endRange.mEndId : endId;
+                        range.mEndId = newRangeEndId;
+                        // insert new ClientRange in place
+                        range.insert(new ClientRange(startId, endId, client));
+                        // coalesce range with following ranges up to endIndex
+                        // remove each range after adding its elements, so the index
+                        // of the next range to join is always startIndex+1 (joinIndex).
+                        // i is the index if no elements had been removed: we only care
+                        // about the number of loop iterations, not the value of i.
+                        int joinIndex = startIndex + 1;
+                        for (int i = joinIndex; i <= endIndex; i++) {
+                            IntRange joinRange = mRanges.get(joinIndex);
+                            range.mClients.addAll(joinRange.mClients);
+                            mRanges.remove(joinRange);
+                        }
+                        return true;
+                    } else {
+                        return false;   // failed to update radio
+                    }
+                }
+            }
+        }
+
+        // new [5, 6], existing [1, 3]
+        // append new range after existing IntRanges
+        if (tryAddRanges(startId, endId, true)) {
+            mRanges.add(new IntRange(startId, endId, client));
+            return true;
+        } else {
+            return false;   // failed to update radio
+        }
+    }
+
+    /**
+     * Disable a range for the specified client and update ranges
+     * if necessary. If {@link #finishUpdate} returns failure,
+     * false is returned and the range is not removed.
+     *
+     * @param startId the first id included in the range
+     * @param endId the last id included in the range
+     * @param client the client requesting to disable the range
+     * @return true if successful, false otherwise
+     */
+    public synchronized boolean disableRange(int startId, int endId, String client) {
+        int len = mRanges.size();
+
+        for (int i=0; i < len; i++) {
+            IntRange range = mRanges.get(i);
+            if (startId < range.mStartId) {
+                return false;   // not found
+            } else if (endId <= range.mEndId) {
+                // found the IntRange that encloses the client range, if any
+                // search for it in the clients list
+                ArrayList<ClientRange> clients = range.mClients;
+
+                // handle common case of IntRange containing one ClientRange
+                int crLength = clients.size();
+                if (crLength == 1) {
+                    ClientRange cr = clients.get(0);
+                    if (cr.mStartId == startId && cr.mEndId == endId && cr.mClient.equals(client)) {
+                        // mRange contains only what's enabled.
+                        // remove the range from mRange then update the radio
+                        mRanges.remove(i);
+                        if (updateRanges()) {
+                            return true;
+                        } else {
+                            // failed to update radio.  insert back the range
+                            mRanges.add(i, range);
+                            return false;
+                        }
+                    } else {
+                        return false;   // not found
+                    }
+                }
+
+                // several ClientRanges: remove one, potentially splitting into many IntRanges.
+                // Save the original start and end id for the original IntRange
+                // in case the radio update fails and we have to revert it. If the
+                // update succeeds, we remove the client range and insert the new IntRanges.
+                // clients are ordered by startId then by endId, so client with largest endId
+                // can be anywhere.  Need to loop thru to find largestEndId.
+                int largestEndId = Integer.MIN_VALUE;  // largest end identifier found
+                boolean updateStarted = false;
+
+                // crlength >= 2
+                for (int crIndex=0; crIndex < crLength; crIndex++) {
+                    ClientRange cr = clients.get(crIndex);
+                    if (cr.mStartId == startId && cr.mEndId == endId && cr.mClient.equals(client)) {
+                        // found the ClientRange to remove, check if it's the last in the list
+                        if (crIndex == crLength - 1) {
+                            if (range.mEndId == largestEndId) {
+                                // remove [2, 5] from [1, 7] [2, 5]
+                                // no channels to remove from radio; return success
+                                clients.remove(crIndex);
+                                return true;
+                            } else {
+                                // disable the channels at the end and lower the end id
+                                clients.remove(crIndex);
+                                range.mEndId = largestEndId;
+                                if (updateRanges()) {
+                                    return true;
+                                } else {
+                                    clients.add(crIndex, cr);
+                                    range.mEndId = cr.mEndId;
+                                    return false;
+                                }
+                            }
+                        }
+
+                        // copy the IntRange so that we can remove elements and modify the
+                        // start and end id's in the copy, leaving the original unmodified
+                        // until after the radio update succeeds
+                        IntRange rangeCopy = new IntRange(range, crIndex);
+
+                        if (crIndex == 0) {
+                            // removing the first ClientRange, so we may need to increase
+                            // the start id of the IntRange.
+                            // We know there are at least two ClientRanges in the list,
+                            // because check for just one ClientRanges case is already handled
+                            // so clients.get(1) should always succeed.
+                            int nextStartId = clients.get(1).mStartId;
+                            if (nextStartId != range.mStartId) {
+                                updateStarted = true;
+                                rangeCopy.mStartId = nextStartId;
+                            }
+                            // init largestEndId
+                            largestEndId = clients.get(1).mEndId;
+                        }
+
+                        // go through remaining ClientRanges, creating new IntRanges when
+                        // there is a gap in the sequence. After radio update succeeds,
+                        // remove the original IntRange and append newRanges to mRanges.
+                        // Otherwise, leave the original IntRange in mRanges and return false.
+                        ArrayList<IntRange> newRanges = new ArrayList<IntRange>();
+
+                        IntRange currentRange = rangeCopy;
+                        for (int nextIndex = crIndex + 1; nextIndex < crLength; nextIndex++) {
+                            ClientRange nextCr = clients.get(nextIndex);
+                            if (nextCr.mStartId > largestEndId + 1) {
+                                updateStarted = true;
+                                currentRange.mEndId = largestEndId;
+                                newRanges.add(currentRange);
+                                currentRange = new IntRange(nextCr);
+                            } else {
+                                if (currentRange.mEndId < nextCr.mEndId) {
+                                    currentRange.mEndId = nextCr.mEndId;
+                                }
+                                currentRange.mClients.add(nextCr);
+                            }
+                            if (nextCr.mEndId > largestEndId) {
+                                largestEndId = nextCr.mEndId;
+                            }
+                        }
+
+                        // remove any channels between largestEndId and endId
+                        if (largestEndId < endId) {
+                            updateStarted = true;
+                            currentRange.mEndId = largestEndId;
+                        }
+                        newRanges.add(currentRange);
+
+                        // replace the original IntRange with newRanges
+                        mRanges.remove(i);
+                        mRanges.addAll(i, newRanges);
+                        if (updateStarted && !updateRanges()) {
+                            // failed to update radio.  revert back mRange.
+                            mRanges.removeAll(newRanges);
+                            mRanges.add(i, range);
+                            return false;
+                        }
+
+                        return true;
+                    } else {
+                        // not the ClientRange to remove; save highest end ID seen so far
+                        if (cr.mEndId > largestEndId) {
+                            largestEndId = cr.mEndId;
+                        }
+                    }
+                }
+            }
+        }
+
+        return false;   // not found
+    }
+
+    /**
+     * Perform a complete update operation (enable all ranges). Useful
+     * after a radio reset. Calls {@link #startUpdate}, followed by zero or
+     * more calls to {@link #addRange}, followed by {@link #finishUpdate}.
+     * @return true if successful, false otherwise
+     */
+    public boolean updateRanges() {
+        startUpdate();
+
+        populateAllRanges();
+        return finishUpdate();
+    }
+
+    /**
+     * Enable or disable a single range of message identifiers.
+     * @param startId the first id included in the range
+     * @param endId the last id included in the range
+     * @param selected true to enable range, false to disable range
+     * @return true if successful, false otherwise
+     */
+    protected boolean tryAddRanges(int startId, int endId, boolean selected) {
+
+        startUpdate();
+        populateAllRanges();
+        // This is the new range to be enabled
+        addRange(startId, endId, selected); // adds to mConfigList
+        return finishUpdate();
+    }
+
+    /**
+     * Returns whether the list of ranges is completely empty.
+     * @return true if there are no enabled ranges
+     */
+    public boolean isEmpty() {
+        return mRanges.isEmpty();
+    }
+
+    /**
+     * Called when attempting to add a single range of message identifiers
+     * Populate all ranges of message identifiers.
+     */
+    private void populateAllRanges() {
+        Iterator<IntRange> itr = mRanges.iterator();
+        // Populate all ranges from mRanges
+        while (itr.hasNext()) {
+            IntRange currRange = (IntRange) itr.next();
+            addRange(currRange.mStartId, currRange.mEndId, true);
+        }
+    }
+
+    /**
+     * Called when attempting to add a single range of message identifiers
+     * Populate all ranges of message identifiers using clients' ranges.
+     */
+    private void populateAllClientRanges() {
+        int len = mRanges.size();
+        for (int i = 0; i < len; i++) {
+            IntRange range = mRanges.get(i);
+
+            int clientLen = range.mClients.size();
+            for (int j=0; j < clientLen; j++) {
+                ClientRange nextRange = range.mClients.get(j);
+                addRange(nextRange.mStartId, nextRange.mEndId, true);
+            }
+        }
+    }
+
+    /**
+     * Called when the list of enabled ranges has changed. This will be
+     * followed by zero or more calls to {@link #addRange} followed by
+     * a call to {@link #finishUpdate}.
+     */
+    protected abstract void startUpdate();
+
+    /**
+     * Called after {@link #startUpdate} to indicate a range of enabled
+     * or disabled values.
+     *
+     * @param startId the first id included in the range
+     * @param endId the last id included in the range
+     * @param selected true to enable range, false to disable range
+     */
+    protected abstract void addRange(int startId, int endId, boolean selected);
+
+    /**
+     * Called to indicate the end of a range update started by the
+     * previous call to {@link #startUpdate}.
+     * @return true if successful, false otherwise
+     */
+    protected abstract boolean finishUpdate();
+}
diff --git a/com/android/internal/telephony/IntentBroadcaster.java b/com/android/internal/telephony/IntentBroadcaster.java
new file mode 100644
index 0000000..7528819
--- /dev/null
+++ b/com/android/internal/telephony/IntentBroadcaster.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * This class is used to broadcast intents that need to be rebroadcast after the device is unlocked.
+ * NOTE: Currently this is used only for SIM_STATE_CHANGED so logic is hardcoded for that;
+ * for example broadcasts are always sticky, only the last intent for the slotId is rebroadcast,
+ * etc.
+ */
+public class IntentBroadcaster {
+    private static final String TAG = "IntentBroadcaster";
+
+    private Map<Integer, Intent> mRebroadcastIntents = new HashMap<>();
+    private static IntentBroadcaster sIntentBroadcaster;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
+                synchronized (mRebroadcastIntents) {
+                    // rebroadcast intents
+                    Iterator iterator = mRebroadcastIntents.entrySet().iterator();
+                    while (iterator.hasNext()) {
+                        Map.Entry pair = (Map.Entry) iterator.next();
+                        Intent i = (Intent) pair.getValue();
+                        i.putExtra(TelephonyIntents.EXTRA_REBROADCAST_ON_UNLOCK, true);
+                        iterator.remove();
+                        logd("Rebroadcasting intent " + i.getAction() + " "
+                                + i.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE)
+                                + " for slotId " + pair.getKey());
+                        ActivityManager.broadcastStickyIntent(i, UserHandle.USER_ALL);
+                    }
+                }
+            }
+        }
+    };
+
+    private IntentBroadcaster(Context context) {
+        context.registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
+    }
+
+    /**
+     * Method to get an instance of IntentBroadcaster after creating one if needed.
+     * @return IntentBroadcaster instance
+     */
+    public static IntentBroadcaster getInstance(Context context) {
+        if (sIntentBroadcaster == null) {
+            sIntentBroadcaster = new IntentBroadcaster(context);
+        }
+        return sIntentBroadcaster;
+    }
+
+    public static IntentBroadcaster getInstance() {
+        return sIntentBroadcaster;
+    }
+
+    /**
+     * Wrapper for ActivityManager.broadcastStickyIntent() that also stores intent to be rebroadcast
+     * on USER_UNLOCKED
+     */
+    public void broadcastStickyIntent(Intent intent, int slotId) {
+        logd("Broadcasting and adding intent for rebroadcast: " + intent.getAction() + " "
+                + intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE)
+                + " for slotId " + slotId);
+        synchronized (mRebroadcastIntents) {
+            ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+            mRebroadcastIntents.put(slotId, intent);
+        }
+    }
+
+    private void logd(String s) {
+        Log.d(TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/LastCallFailCause.java b/com/android/internal/telephony/LastCallFailCause.java
new file mode 100644
index 0000000..d06eee6
--- /dev/null
+++ b/com/android/internal/telephony/LastCallFailCause.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+public class LastCallFailCause {
+  public int causeCode;
+  public String vendorCause;
+
+    @Override
+    public String toString() {
+        return super.toString()
+            + " causeCode: " + causeCode
+            + " vendorCause: " + vendorCause;
+    }
+}
diff --git a/com/android/internal/telephony/MccTable.java b/com/android/internal/telephony/MccTable.java
new file mode 100644
index 0000000..5714b29
--- /dev/null
+++ b/com/android/internal/telephony/MccTable.java
@@ -0,0 +1,701 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.app.LocaleStore;
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import libcore.icu.ICU;
+import libcore.icu.TimeZoneNames;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Mobile Country Code
+ *
+ * {@hide}
+ */
+public final class MccTable {
+    static final String LOG_TAG = "MccTable";
+
+    static ArrayList<MccEntry> sTable;
+
+    static class MccEntry implements Comparable<MccEntry> {
+        final int mMcc;
+        final String mIso;
+        final int mSmallestDigitsMnc;
+
+        MccEntry(int mnc, String iso, int smallestDigitsMCC) {
+            if (iso == null) {
+                throw new NullPointerException();
+            }
+            mMcc = mnc;
+            mIso = iso;
+            mSmallestDigitsMnc = smallestDigitsMCC;
+        }
+
+        @Override
+        public int compareTo(MccEntry o) {
+            return mMcc - o.mMcc;
+        }
+    }
+
+    private static MccEntry entryForMcc(int mcc) {
+        MccEntry m = new MccEntry(mcc, "", 0);
+
+        int index = Collections.binarySearch(sTable, m);
+
+        if (index < 0) {
+            return null;
+        } else {
+            return sTable.get(index);
+        }
+    }
+
+    /**
+     * Returns a default time zone ID for the given MCC.
+     * @param mcc Mobile Country Code
+     * @return default TimeZone ID, or null if not specified
+     */
+    public static String defaultTimeZoneForMcc(int mcc) {
+        MccEntry entry = entryForMcc(mcc);
+        if (entry == null) {
+            return null;
+        }
+        Locale locale = new Locale("", entry.mIso);
+        String[] tz = TimeZoneNames.forLocale(locale);
+        if (tz.length == 0) return null;
+
+        String zoneName = tz[0];
+
+        /* Use Australia/Sydney instead of Australia/Lord_Howe for Australia.
+         * http://b/33228250
+         * Todo: remove the code, see b/62418027
+         */
+        if (mcc == 505  /* Australia / Norfolk Island */) {
+            for (String zone : tz) {
+                if (zone.contains("Sydney")) {
+                    zoneName = zone;
+                }
+            }
+        }
+        return zoneName;
+    }
+
+    /**
+     * Given a GSM Mobile Country Code, returns
+     * an ISO two-character country code if available.
+     * Returns "" if unavailable.
+     */
+    public static String countryCodeForMcc(int mcc) {
+        MccEntry entry = entryForMcc(mcc);
+
+        if (entry == null) {
+            return "";
+        } else {
+            return entry.mIso;
+        }
+    }
+
+    /**
+     * Given a GSM Mobile Country Code, returns
+     * an ISO 2-3 character language code if available.
+     * Returns null if unavailable.
+     */
+    public static String defaultLanguageForMcc(int mcc) {
+        MccEntry entry = entryForMcc(mcc);
+        if (entry == null) {
+            Slog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): no country for mcc");
+            return null;
+        }
+
+        final String country = entry.mIso;
+
+        // Choose English as the default language for India.
+        if ("in".equals(country)) {
+            return "en";
+        }
+
+        // Ask CLDR for the language this country uses...
+        Locale likelyLocale = ICU.addLikelySubtags(new Locale("und", country));
+        String likelyLanguage = likelyLocale.getLanguage();
+        Slog.d(LOG_TAG, "defaultLanguageForMcc(" + mcc + "): country " + country + " uses " +
+               likelyLanguage);
+        return likelyLanguage;
+    }
+
+    /**
+     * Given a GSM Mobile Country Code, returns
+     * the smallest number of digits that M if available.
+     * Returns 2 if unavailable.
+     */
+    public static int smallestDigitsMccForMnc(int mcc) {
+        MccEntry entry = entryForMcc(mcc);
+
+        if (entry == null) {
+            return 2;
+        } else {
+            return entry.mSmallestDigitsMnc;
+        }
+    }
+
+    /**
+     * Updates MCC and MNC device configuration information for application retrieving
+     * correct version of resources.  If MCC is 0, MCC and MNC will be ignored (not set).
+     * @param context Context to act on.
+     * @param mccmnc truncated imsi with just the MCC and MNC - MNC assumed to be from 4th to end
+     * @param fromServiceState true if coming from the radio service state, false if from SIM
+     */
+    public static void updateMccMncConfiguration(Context context, String mccmnc,
+            boolean fromServiceState) {
+        Slog.d(LOG_TAG, "updateMccMncConfiguration mccmnc='" + mccmnc + "' fromServiceState=" + fromServiceState);
+
+        if (Build.IS_DEBUGGABLE) {
+            String overrideMcc = SystemProperties.get("persist.sys.override_mcc");
+            if (!TextUtils.isEmpty(overrideMcc)) {
+                mccmnc = overrideMcc;
+                Slog.d(LOG_TAG, "updateMccMncConfiguration overriding mccmnc='" + mccmnc + "'");
+            }
+        }
+
+        if (!TextUtils.isEmpty(mccmnc)) {
+            int mcc, mnc;
+
+            String defaultMccMnc = TelephonyManager.getDefault().getSimOperatorNumeric();
+            Slog.d(LOG_TAG, "updateMccMncConfiguration defaultMccMnc=" + defaultMccMnc);
+            //Update mccmnc only for default subscription in case of MultiSim.
+//            if (!defaultMccMnc.equals(mccmnc)) {
+//                Slog.d(LOG_TAG, "Not a Default subscription, ignoring mccmnc config update.");
+//                return;
+//            }
+
+            try {
+                mcc = Integer.parseInt(mccmnc.substring(0,3));
+                mnc = Integer.parseInt(mccmnc.substring(3));
+            } catch (NumberFormatException e) {
+                Slog.e(LOG_TAG, "Error parsing IMSI: " + mccmnc);
+                return;
+            }
+
+            Slog.d(LOG_TAG, "updateMccMncConfiguration: mcc=" + mcc + ", mnc=" + mnc);
+            if (mcc != 0) {
+                setTimezoneFromMccIfNeeded(context, mcc);
+            }
+            if (fromServiceState) {
+                setWifiCountryCodeFromMcc(context, mcc);
+            } else {
+                // from SIM
+                try {
+                    Configuration config = new Configuration();
+                    boolean updateConfig = false;
+                    if (mcc != 0) {
+                        config.mcc = mcc;
+                        config.mnc = mnc == 0 ? Configuration.MNC_ZERO : mnc;
+                        updateConfig = true;
+                    }
+
+                    if (updateConfig) {
+                        Slog.d(LOG_TAG, "updateMccMncConfiguration updateConfig config=" + config);
+                        ActivityManager.getService().updateConfiguration(config);
+                    } else {
+                        Slog.d(LOG_TAG, "updateMccMncConfiguration nothing to update");
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(LOG_TAG, "Can't update configuration", e);
+                }
+            }
+        } else {
+            if (fromServiceState) {
+                // an empty mccmnc means no signal - tell wifi we don't know
+                setWifiCountryCodeFromMcc(context, 0);
+            }
+        }
+    }
+
+    /**
+     * Maps a given locale to a fallback locale that approximates it. This is a hack.
+     */
+    private static final Map<Locale, Locale> FALLBACKS = new HashMap<Locale, Locale>();
+
+    static {
+        // If we have English (without a country) explicitly prioritize en_US. http://b/28998094
+        FALLBACKS.put(Locale.ENGLISH, Locale.US);
+    }
+
+    /**
+     * Finds a suitable locale among {@code candidates} to use as the fallback locale for
+     * {@code target}. This looks through the list of {@link #FALLBACKS}, and follows the chain
+     * until a locale in {@code candidates} is found.
+     * This function assumes that {@code target} is not in {@code candidates}.
+     *
+     * TODO: This should really follow the CLDR chain of parent locales! That might be a bit
+     * of a problem because we don't really have an en-001 locale on android.
+     *
+     * @return The fallback locale or {@code null} if there is no suitable fallback defined in the
+     *         lookup.
+     */
+    private static Locale lookupFallback(Locale target, List<Locale> candidates) {
+        Locale fallback = target;
+        while ((fallback = FALLBACKS.get(fallback)) != null) {
+            if (candidates.contains(fallback)) {
+                return fallback;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return Locale for the language and country or null if no good match.
+     *
+     * @param context Context to act on.
+     * @param language Two character language code desired
+     * @param country Two character country code desired
+     *
+     * @return Locale or null if no appropriate value
+     */
+    private static Locale getLocaleForLanguageCountry(Context context, String language,
+            String country) {
+        if (language == null) {
+            Slog.d(LOG_TAG, "getLocaleForLanguageCountry: skipping no language");
+            return null; // no match possible
+        }
+        if (country == null) {
+            country = ""; // The Locale constructor throws if passed null.
+        }
+
+        final Locale target = new Locale(language, country);
+        try {
+            String[] localeArray = context.getAssets().getLocales();
+            List<String> locales = new ArrayList<>(Arrays.asList(localeArray));
+
+            // Even in developer mode, you don't want the pseudolocales.
+            locales.remove("ar-XB");
+            locales.remove("en-XA");
+
+            List<Locale> languageMatches = new ArrayList<>();
+            for (String locale : locales) {
+                final Locale l = Locale.forLanguageTag(locale.replace('_', '-'));
+
+                // Only consider locales with both language and country.
+                if (l == null || "und".equals(l.getLanguage()) ||
+                        l.getLanguage().isEmpty() || l.getCountry().isEmpty()) {
+                    continue;
+                }
+                if (l.getLanguage().equals(target.getLanguage())) {
+                    // If we got a perfect match, we're done.
+                    if (l.getCountry().equals(target.getCountry())) {
+                        Slog.d(LOG_TAG, "getLocaleForLanguageCountry: got perfect match: " +
+                               l.toLanguageTag());
+                        return l;
+                    }
+
+                    // We've only matched the language, not the country.
+                    languageMatches.add(l);
+                }
+            }
+
+            if (languageMatches.isEmpty()) {
+                Slog.d(LOG_TAG, "getLocaleForLanguageCountry: no locales for language " + language);
+                return null;
+            }
+
+            Locale bestMatch = lookupFallback(target, languageMatches);
+            if (bestMatch != null) {
+                Slog.d(LOG_TAG, "getLocaleForLanguageCountry: got a fallback match: "
+                        + bestMatch.toLanguageTag());
+                return bestMatch;
+            } else {
+                // Ask {@link LocaleStore} whether this locale is considered "translated".
+                // LocaleStore has a broader definition of translated than just the asset locales
+                // above: a locale is "translated" if it has translation assets, or another locale
+                // with the same language and script has translation assets.
+                // If a locale is "translated", it is selectable in setup wizard, and can therefore
+                // be considerd a valid result for this method.
+                if (!TextUtils.isEmpty(target.getCountry())) {
+                    LocaleStore.fillCache(context);
+                    LocaleInfo targetInfo = LocaleStore.getLocaleInfo(target);
+                    if (targetInfo.isTranslated()) {
+                        Slog.d(LOG_TAG, "getLocaleForLanguageCountry: "
+                                + "target locale is translated: " + target);
+                        return target;
+                    }
+                }
+
+                // Somewhat arbitrarily take the first locale for the language,
+                // unless we get a perfect match later. Note that these come back in no
+                // particular order, so there's no reason to think the first match is
+                // a particularly good match.
+                Slog.d(LOG_TAG, "getLocaleForLanguageCountry: got language-only match: "
+                        + language);
+                return languageMatches.get(0);
+            }
+        } catch (Exception e) {
+            Slog.d(LOG_TAG, "getLocaleForLanguageCountry: exception", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * If the timezone is not already set, set it based on the MCC of the SIM.
+     * @param context Context to act on.
+     * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA)
+     */
+    private static void setTimezoneFromMccIfNeeded(Context context, int mcc) {
+        String timezone = SystemProperties.get(ServiceStateTracker.TIMEZONE_PROPERTY);
+        // timezone.equals("GMT") will be true and only true if the timezone was
+        // set to a default value by the system server (when starting, system server.
+        // sets the persist.sys.timezone to "GMT" if it's not set)."GMT" is not used by
+        // any code that sets it explicitly (in case where something sets GMT explicitly,
+        // "Etc/GMT" Olsen ID would be used).
+        // TODO(b/64056758): Remove "timezone.equals("GMT")" hack when there's a
+        // better way of telling if the value has been defaulted.
+        if (timezone == null || timezone.length() == 0 || timezone.equals("GMT")) {
+            String zoneId = defaultTimeZoneForMcc(mcc);
+            if (zoneId != null && zoneId.length() > 0) {
+                // Set time zone based on MCC
+                AlarmManager alarm =
+                        (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+                alarm.setTimeZone(zoneId);
+                Slog.d(LOG_TAG, "timezone set to " + zoneId);
+            }
+        }
+    }
+
+    /**
+     * Get Locale based on the MCC of the SIM.
+     *
+     * @param context Context to act on.
+     * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA)
+     * @param simLanguage (nullable) the language from the SIM records (if present).
+     *
+     * @return locale for the mcc or null if none
+     */
+    public static Locale getLocaleFromMcc(Context context, int mcc, String simLanguage) {
+        boolean hasSimLanguage = !TextUtils.isEmpty(simLanguage);
+        String language = hasSimLanguage ? simLanguage : MccTable.defaultLanguageForMcc(mcc);
+        String country = MccTable.countryCodeForMcc(mcc);
+
+        Slog.d(LOG_TAG, "getLocaleFromMcc(" + language + ", " + country + ", " + mcc);
+        final Locale locale = getLocaleForLanguageCountry(context, language, country);
+
+        // If we couldn't find a locale that matches the SIM language, give it a go again
+        // with the "likely" language for the given country.
+        if (locale == null && hasSimLanguage) {
+            language = MccTable.defaultLanguageForMcc(mcc);
+            Slog.d(LOG_TAG, "[retry ] getLocaleFromMcc(" + language + ", " + country + ", " + mcc);
+            return getLocaleForLanguageCountry(context, language, country);
+        }
+
+        return locale;
+    }
+
+    /**
+     * Set the country code for wifi.  This sets allowed wifi channels based on the
+     * country of the carrier we see.  If we can't see any, reset to 0 so we don't
+     * broadcast on forbidden channels.
+     * @param context Context to act on.
+     * @param mcc Mobile Country Code of the operator.  0 if not known
+     */
+    private static void setWifiCountryCodeFromMcc(Context context, int mcc) {
+        String country = MccTable.countryCodeForMcc(mcc);
+        Slog.d(LOG_TAG, "WIFI_COUNTRY_CODE set to " + country);
+        WifiManager wM = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        wM.setCountryCode(country, false);
+    }
+
+    static {
+        sTable = new ArrayList<MccEntry>(240);
+
+
+        /*
+         * The table below is built from two resources:
+         *
+         * 1) ITU "Mobile Network Code (MNC) for the international
+         *   identification plan for mobile terminals and mobile users"
+         *   which is available as an annex to the ITU operational bulletin
+         *   available here: http://www.itu.int/itu-t/bulletin/annex.html
+         *
+         * 2) The ISO 3166 country codes list, available here:
+         *    http://www.iso.org/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/index.html
+         *
+         * This table has not been verified.
+         */
+
+		sTable.add(new MccEntry(202,"gr",2));	//Greece
+		sTable.add(new MccEntry(204,"nl",2));	//Netherlands (Kingdom of the)
+		sTable.add(new MccEntry(206,"be",2));	//Belgium
+		sTable.add(new MccEntry(208,"fr",2));	//France
+		sTable.add(new MccEntry(212,"mc",2));	//Monaco (Principality of)
+		sTable.add(new MccEntry(213,"ad",2));	//Andorra (Principality of)
+		sTable.add(new MccEntry(214,"es",2));	//Spain
+		sTable.add(new MccEntry(216,"hu",2));	//Hungary (Republic of)
+		sTable.add(new MccEntry(218,"ba",2));	//Bosnia and Herzegovina
+		sTable.add(new MccEntry(219,"hr",2));	//Croatia (Republic of)
+		sTable.add(new MccEntry(220,"rs",2));	//Serbia and Montenegro
+		sTable.add(new MccEntry(222,"it",2));	//Italy
+		sTable.add(new MccEntry(225,"va",2));	//Vatican City State
+		sTable.add(new MccEntry(226,"ro",2));	//Romania
+		sTable.add(new MccEntry(228,"ch",2));	//Switzerland (Confederation of)
+		sTable.add(new MccEntry(230,"cz",2));	//Czechia
+		sTable.add(new MccEntry(231,"sk",2));	//Slovak Republic
+		sTable.add(new MccEntry(232,"at",2));	//Austria
+		sTable.add(new MccEntry(234,"gb",2));	//United Kingdom of Great Britain and Northern Ireland
+		sTable.add(new MccEntry(235,"gb",2));	//United Kingdom of Great Britain and Northern Ireland
+		sTable.add(new MccEntry(238,"dk",2));	//Denmark
+		sTable.add(new MccEntry(240,"se",2));	//Sweden
+		sTable.add(new MccEntry(242,"no",2));	//Norway
+		sTable.add(new MccEntry(244,"fi",2));	//Finland
+		sTable.add(new MccEntry(246,"lt",2));	//Lithuania (Republic of)
+		sTable.add(new MccEntry(247,"lv",2));	//Latvia (Republic of)
+		sTable.add(new MccEntry(248,"ee",2));	//Estonia (Republic of)
+		sTable.add(new MccEntry(250,"ru",2));	//Russian Federation
+		sTable.add(new MccEntry(255,"ua",2));	//Ukraine
+		sTable.add(new MccEntry(257,"by",2));	//Belarus (Republic of)
+		sTable.add(new MccEntry(259,"md",2));	//Moldova (Republic of)
+		sTable.add(new MccEntry(260,"pl",2));	//Poland (Republic of)
+		sTable.add(new MccEntry(262,"de",2));	//Germany (Federal Republic of)
+		sTable.add(new MccEntry(266,"gi",2));	//Gibraltar
+		sTable.add(new MccEntry(268,"pt",2));	//Portugal
+		sTable.add(new MccEntry(270,"lu",2));	//Luxembourg
+		sTable.add(new MccEntry(272,"ie",2));	//Ireland
+		sTable.add(new MccEntry(274,"is",2));	//Iceland
+		sTable.add(new MccEntry(276,"al",2));	//Albania (Republic of)
+		sTable.add(new MccEntry(278,"mt",2));	//Malta
+		sTable.add(new MccEntry(280,"cy",2));	//Cyprus (Republic of)
+		sTable.add(new MccEntry(282,"ge",2));	//Georgia
+		sTable.add(new MccEntry(283,"am",2));	//Armenia (Republic of)
+		sTable.add(new MccEntry(284,"bg",2));	//Bulgaria (Republic of)
+		sTable.add(new MccEntry(286,"tr",2));	//Turkey
+		sTable.add(new MccEntry(288,"fo",2));	//Faroe Islands
+                sTable.add(new MccEntry(289,"ge",2));    //Abkhazia (Georgia)
+		sTable.add(new MccEntry(290,"gl",2));	//Greenland (Denmark)
+		sTable.add(new MccEntry(292,"sm",2));	//San Marino (Republic of)
+		sTable.add(new MccEntry(293,"si",2));	//Slovenia (Republic of)
+                sTable.add(new MccEntry(294,"mk",2));   //The Former Yugoslav Republic of Macedonia
+		sTable.add(new MccEntry(295,"li",2));	//Liechtenstein (Principality of)
+                sTable.add(new MccEntry(297,"me",2));    //Montenegro (Republic of)
+		sTable.add(new MccEntry(302,"ca",3));	//Canada
+		sTable.add(new MccEntry(308,"pm",2));	//Saint Pierre and Miquelon (Collectivit territoriale de la Rpublique franaise)
+		sTable.add(new MccEntry(310,"us",3));	//United States of America
+		sTable.add(new MccEntry(311,"us",3));	//United States of America
+		sTable.add(new MccEntry(312,"us",3));	//United States of America
+		sTable.add(new MccEntry(313,"us",3));	//United States of America
+		sTable.add(new MccEntry(314,"us",3));	//United States of America
+		sTable.add(new MccEntry(315,"us",3));	//United States of America
+		sTable.add(new MccEntry(316,"us",3));	//United States of America
+		sTable.add(new MccEntry(330,"pr",2));	//Puerto Rico
+		sTable.add(new MccEntry(332,"vi",2));	//United States Virgin Islands
+		sTable.add(new MccEntry(334,"mx",3));	//Mexico
+		sTable.add(new MccEntry(338,"jm",3));	//Jamaica
+		sTable.add(new MccEntry(340,"gp",2));	//Guadeloupe (French Department of)
+		sTable.add(new MccEntry(342,"bb",3));	//Barbados
+		sTable.add(new MccEntry(344,"ag",3));	//Antigua and Barbuda
+		sTable.add(new MccEntry(346,"ky",3));	//Cayman Islands
+		sTable.add(new MccEntry(348,"vg",3));	//British Virgin Islands
+		sTable.add(new MccEntry(350,"bm",2));	//Bermuda
+		sTable.add(new MccEntry(352,"gd",2));	//Grenada
+		sTable.add(new MccEntry(354,"ms",2));	//Montserrat
+		sTable.add(new MccEntry(356,"kn",2));	//Saint Kitts and Nevis
+		sTable.add(new MccEntry(358,"lc",2));	//Saint Lucia
+		sTable.add(new MccEntry(360,"vc",2));	//Saint Vincent and the Grenadines
+		sTable.add(new MccEntry(362,"ai",2));	//Netherlands Antilles
+		sTable.add(new MccEntry(363,"aw",2));	//Aruba
+		sTable.add(new MccEntry(364,"bs",2));	//Bahamas (Commonwealth of the)
+		sTable.add(new MccEntry(365,"ai",3));	//Anguilla
+		sTable.add(new MccEntry(366,"dm",2));	//Dominica (Commonwealth of)
+		sTable.add(new MccEntry(368,"cu",2));	//Cuba
+		sTable.add(new MccEntry(370,"do",2));	//Dominican Republic
+		sTable.add(new MccEntry(372,"ht",2));	//Haiti (Republic of)
+		sTable.add(new MccEntry(374,"tt",2));	//Trinidad and Tobago
+		sTable.add(new MccEntry(376,"tc",2));	//Turks and Caicos Islands
+		sTable.add(new MccEntry(400,"az",2));	//Azerbaijani Republic
+		sTable.add(new MccEntry(401,"kz",2));	//Kazakhstan (Republic of)
+		sTable.add(new MccEntry(402,"bt",2));	//Bhutan (Kingdom of)
+		sTable.add(new MccEntry(404,"in",2));	//India (Republic of)
+		sTable.add(new MccEntry(405,"in",2));	//India (Republic of)
+		sTable.add(new MccEntry(406,"in",2));	//India (Republic of)
+		sTable.add(new MccEntry(410,"pk",2));	//Pakistan (Islamic Republic of)
+		sTable.add(new MccEntry(412,"af",2));	//Afghanistan
+		sTable.add(new MccEntry(413,"lk",2));	//Sri Lanka (Democratic Socialist Republic of)
+		sTable.add(new MccEntry(414,"mm",2));	//Myanmar (Union of)
+		sTable.add(new MccEntry(415,"lb",2));	//Lebanon
+		sTable.add(new MccEntry(416,"jo",2));	//Jordan (Hashemite Kingdom of)
+		sTable.add(new MccEntry(417,"sy",2));	//Syrian Arab Republic
+		sTable.add(new MccEntry(418,"iq",2));	//Iraq (Republic of)
+		sTable.add(new MccEntry(419,"kw",2));	//Kuwait (State of)
+		sTable.add(new MccEntry(420,"sa",2));	//Saudi Arabia (Kingdom of)
+		sTable.add(new MccEntry(421,"ye",2));	//Yemen (Republic of)
+		sTable.add(new MccEntry(422,"om",2));	//Oman (Sultanate of)
+                sTable.add(new MccEntry(423,"ps",2));    //Palestine
+		sTable.add(new MccEntry(424,"ae",2));	//United Arab Emirates
+		sTable.add(new MccEntry(425,"il",2));	//Israel (State of)
+		sTable.add(new MccEntry(426,"bh",2));	//Bahrain (Kingdom of)
+		sTable.add(new MccEntry(427,"qa",2));	//Qatar (State of)
+		sTable.add(new MccEntry(428,"mn",2));	//Mongolia
+		sTable.add(new MccEntry(429,"np",2));	//Nepal
+		sTable.add(new MccEntry(430,"ae",2));	//United Arab Emirates
+		sTable.add(new MccEntry(431,"ae",2));	//United Arab Emirates
+		sTable.add(new MccEntry(432,"ir",2));	//Iran (Islamic Republic of)
+		sTable.add(new MccEntry(434,"uz",2));	//Uzbekistan (Republic of)
+		sTable.add(new MccEntry(436,"tj",2));	//Tajikistan (Republic of)
+		sTable.add(new MccEntry(437,"kg",2));	//Kyrgyz Republic
+		sTable.add(new MccEntry(438,"tm",2));	//Turkmenistan
+		sTable.add(new MccEntry(440,"jp",2));	//Japan
+		sTable.add(new MccEntry(441,"jp",2));	//Japan
+		sTable.add(new MccEntry(450,"kr",2));	//Korea (Republic of)
+		sTable.add(new MccEntry(452,"vn",2));	//Viet Nam (Socialist Republic of)
+		sTable.add(new MccEntry(454,"hk",2));	//"Hong Kong, China"
+		sTable.add(new MccEntry(455,"mo",2));	//"Macao, China"
+		sTable.add(new MccEntry(456,"kh",2));	//Cambodia (Kingdom of)
+		sTable.add(new MccEntry(457,"la",2));	//Lao People's Democratic Republic
+		sTable.add(new MccEntry(460,"cn",2));	//China (People's Republic of)
+		sTable.add(new MccEntry(461,"cn",2));	//China (People's Republic of)
+		sTable.add(new MccEntry(466,"tw",2));	//Taiwan
+		sTable.add(new MccEntry(467,"kp",2));	//Democratic People's Republic of Korea
+		sTable.add(new MccEntry(470,"bd",2));	//Bangladesh (People's Republic of)
+		sTable.add(new MccEntry(472,"mv",2));	//Maldives (Republic of)
+		sTable.add(new MccEntry(502,"my",2));	//Malaysia
+		sTable.add(new MccEntry(505,"au",2));	//Australia
+		sTable.add(new MccEntry(510,"id",2));	//Indonesia (Republic of)
+		sTable.add(new MccEntry(514,"tl",2));	//Democratic Republic of Timor-Leste
+		sTable.add(new MccEntry(515,"ph",2));	//Philippines (Republic of the)
+		sTable.add(new MccEntry(520,"th",2));	//Thailand
+		sTable.add(new MccEntry(525,"sg",2));	//Singapore (Republic of)
+		sTable.add(new MccEntry(528,"bn",2));	//Brunei Darussalam
+		sTable.add(new MccEntry(530,"nz",2));	//New Zealand
+		sTable.add(new MccEntry(534,"mp",2));	//Northern Mariana Islands (Commonwealth of the)
+		sTable.add(new MccEntry(535,"gu",2));	//Guam
+		sTable.add(new MccEntry(536,"nr",2));	//Nauru (Republic of)
+		sTable.add(new MccEntry(537,"pg",2));	//Papua New Guinea
+		sTable.add(new MccEntry(539,"to",2));	//Tonga (Kingdom of)
+		sTable.add(new MccEntry(540,"sb",2));	//Solomon Islands
+		sTable.add(new MccEntry(541,"vu",2));	//Vanuatu (Republic of)
+		sTable.add(new MccEntry(542,"fj",2));	//Fiji (Republic of)
+		sTable.add(new MccEntry(543,"wf",2));	//Wallis and Futuna (Territoire franais d'outre-mer)
+		sTable.add(new MccEntry(544,"as",2));	//American Samoa
+		sTable.add(new MccEntry(545,"ki",2));	//Kiribati (Republic of)
+		sTable.add(new MccEntry(546,"nc",2));	//New Caledonia (Territoire franais d'outre-mer)
+		sTable.add(new MccEntry(547,"pf",2));	//French Polynesia (Territoire franais d'outre-mer)
+		sTable.add(new MccEntry(548,"ck",2));	//Cook Islands
+		sTable.add(new MccEntry(549,"ws",2));	//Samoa (Independent State of)
+		sTable.add(new MccEntry(550,"fm",2));	//Micronesia (Federated States of)
+		sTable.add(new MccEntry(551,"mh",2));	//Marshall Islands (Republic of the)
+		sTable.add(new MccEntry(552,"pw",2));	//Palau (Republic of)
+		sTable.add(new MccEntry(553,"tv",2));	//Tuvalu
+		sTable.add(new MccEntry(555,"nu",2));	//Niue
+		sTable.add(new MccEntry(602,"eg",2));	//Egypt (Arab Republic of)
+		sTable.add(new MccEntry(603,"dz",2));	//Algeria (People's Democratic Republic of)
+		sTable.add(new MccEntry(604,"ma",2));	//Morocco (Kingdom of)
+		sTable.add(new MccEntry(605,"tn",2));	//Tunisia
+		sTable.add(new MccEntry(606,"ly",2));	//Libya (Socialist People's Libyan Arab Jamahiriya)
+		sTable.add(new MccEntry(607,"gm",2));	//Gambia (Republic of the)
+		sTable.add(new MccEntry(608,"sn",2));	//Senegal (Republic of)
+		sTable.add(new MccEntry(609,"mr",2));	//Mauritania (Islamic Republic of)
+		sTable.add(new MccEntry(610,"ml",2));	//Mali (Republic of)
+		sTable.add(new MccEntry(611,"gn",2));	//Guinea (Republic of)
+		sTable.add(new MccEntry(612,"ci",2));	//Côte d'Ivoire (Republic of)
+		sTable.add(new MccEntry(613,"bf",2));	//Burkina Faso
+		sTable.add(new MccEntry(614,"ne",2));	//Niger (Republic of the)
+		sTable.add(new MccEntry(615,"tg",2));	//Togolese Republic
+		sTable.add(new MccEntry(616,"bj",2));	//Benin (Republic of)
+		sTable.add(new MccEntry(617,"mu",2));	//Mauritius (Republic of)
+		sTable.add(new MccEntry(618,"lr",2));	//Liberia (Republic of)
+		sTable.add(new MccEntry(619,"sl",2));	//Sierra Leone
+		sTable.add(new MccEntry(620,"gh",2));	//Ghana
+		sTable.add(new MccEntry(621,"ng",2));	//Nigeria (Federal Republic of)
+		sTable.add(new MccEntry(622,"td",2));	//Chad (Republic of)
+		sTable.add(new MccEntry(623,"cf",2));	//Central African Republic
+		sTable.add(new MccEntry(624,"cm",2));	//Cameroon (Republic of)
+		sTable.add(new MccEntry(625,"cv",2));	//Cape Verde (Republic of)
+		sTable.add(new MccEntry(626,"st",2));	//Sao Tome and Principe (Democratic Republic of)
+		sTable.add(new MccEntry(627,"gq",2));	//Equatorial Guinea (Republic of)
+		sTable.add(new MccEntry(628,"ga",2));	//Gabonese Republic
+		sTable.add(new MccEntry(629,"cg",2));	//Congo (Republic of the)
+		sTable.add(new MccEntry(630,"cd",2));	//Democratic Republic of the Congo
+		sTable.add(new MccEntry(631,"ao",2));	//Angola (Republic of)
+		sTable.add(new MccEntry(632,"gw",2));	//Guinea-Bissau (Republic of)
+		sTable.add(new MccEntry(633,"sc",2));	//Seychelles (Republic of)
+		sTable.add(new MccEntry(634,"sd",2));	//Sudan (Republic of the)
+		sTable.add(new MccEntry(635,"rw",2));	//Rwanda (Republic of)
+		sTable.add(new MccEntry(636,"et",2));	//Ethiopia (Federal Democratic Republic of)
+		sTable.add(new MccEntry(637,"so",2));	//Somali Democratic Republic
+		sTable.add(new MccEntry(638,"dj",2));	//Djibouti (Republic of)
+		sTable.add(new MccEntry(639,"ke",2));	//Kenya (Republic of)
+		sTable.add(new MccEntry(640,"tz",2));	//Tanzania (United Republic of)
+		sTable.add(new MccEntry(641,"ug",2));	//Uganda (Republic of)
+		sTable.add(new MccEntry(642,"bi",2));	//Burundi (Republic of)
+		sTable.add(new MccEntry(643,"mz",2));	//Mozambique (Republic of)
+		sTable.add(new MccEntry(645,"zm",2));	//Zambia (Republic of)
+		sTable.add(new MccEntry(646,"mg",2));	//Madagascar (Republic of)
+		sTable.add(new MccEntry(647,"re",2));	//Reunion (French Department of)
+		sTable.add(new MccEntry(648,"zw",2));	//Zimbabwe (Republic of)
+		sTable.add(new MccEntry(649,"na",2));	//Namibia (Republic of)
+		sTable.add(new MccEntry(650,"mw",2));	//Malawi
+		sTable.add(new MccEntry(651,"ls",2));	//Lesotho (Kingdom of)
+		sTable.add(new MccEntry(652,"bw",2));	//Botswana (Republic of)
+		sTable.add(new MccEntry(653,"sz",2));	//Swaziland (Kingdom of)
+		sTable.add(new MccEntry(654,"km",2));	//Comoros (Union of the)
+		sTable.add(new MccEntry(655,"za",2));	//South Africa (Republic of)
+		sTable.add(new MccEntry(657,"er",2));	//Eritrea
+		sTable.add(new MccEntry(658,"sh",2));	//Saint Helena, Ascension and Tristan da Cunha
+		sTable.add(new MccEntry(659,"ss",2));	//South Sudan (Republic of)
+		sTable.add(new MccEntry(702,"bz",2));	//Belize
+		sTable.add(new MccEntry(704,"gt",2));	//Guatemala (Republic of)
+		sTable.add(new MccEntry(706,"sv",2));	//El Salvador (Republic of)
+		sTable.add(new MccEntry(708,"hn",3));	//Honduras (Republic of)
+		sTable.add(new MccEntry(710,"ni",2));	//Nicaragua
+		sTable.add(new MccEntry(712,"cr",2));	//Costa Rica
+		sTable.add(new MccEntry(714,"pa",2));	//Panama (Republic of)
+		sTable.add(new MccEntry(716,"pe",2));	//Peru
+		sTable.add(new MccEntry(722,"ar",3));	//Argentine Republic
+		sTable.add(new MccEntry(724,"br",2));	//Brazil (Federative Republic of)
+		sTable.add(new MccEntry(730,"cl",2));	//Chile
+		sTable.add(new MccEntry(732,"co",3));	//Colombia (Republic of)
+		sTable.add(new MccEntry(734,"ve",2));	//Venezuela (Bolivarian Republic of)
+		sTable.add(new MccEntry(736,"bo",2));	//Bolivia (Republic of)
+		sTable.add(new MccEntry(738,"gy",2));	//Guyana
+		sTable.add(new MccEntry(740,"ec",2));	//Ecuador
+		sTable.add(new MccEntry(742,"gf",2));	//French Guiana (French Department of)
+		sTable.add(new MccEntry(744,"py",2));	//Paraguay (Republic of)
+		sTable.add(new MccEntry(746,"sr",2));	//Suriname (Republic of)
+		sTable.add(new MccEntry(748,"uy",2));	//Uruguay (Eastern Republic of)
+		sTable.add(new MccEntry(750,"fk",2));	//Falkland Islands (Malvinas)
+        //table.add(new MccEntry(901,"",2));	//"International Mobile, shared code"
+
+        Collections.sort(sTable);
+    }
+}
diff --git a/com/android/internal/telephony/MmiCode.java b/com/android/internal/telephony/MmiCode.java
new file mode 100644
index 0000000..da5321a
--- /dev/null
+++ b/com/android/internal/telephony/MmiCode.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.os.ResultReceiver;
+
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ */
+public interface MmiCode
+{
+    /**
+     * {@hide}
+     */
+    public enum State {
+        PENDING,
+        CANCELLED,
+        COMPLETE,
+        FAILED
+    }
+
+    /**
+     * @return Current state of MmiCode request
+     */
+    public State getState();
+
+    /**
+     * @return Localized message for UI display, valid only in COMPLETE
+     * or FAILED states. null otherwise
+     */
+
+    public CharSequence getMessage();
+
+    /**
+     * @return Phone associated with the MMI/USSD message
+     */
+    public Phone getPhone();
+
+    /**
+     * Cancels pending MMI request.
+     * State becomes CANCELLED unless already COMPLETE or FAILED
+     */
+    public void cancel();
+
+    /**
+     * @return true if the network response is a REQUEST for more user input.
+     */
+    public boolean isUssdRequest();
+
+    /**
+     * @return true if an outstanding request can be canceled.
+     */
+    public boolean isCancelable();
+
+    /**
+     * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related
+     */
+    public boolean isPinPukCommand();
+
+    /**
+     * Process a MMI code or short code...anything that isn't a dialing number
+     */
+    void processCode() throws CallStateException;
+
+    /**
+     * @return the Receiver for the Ussd Callback.
+     */
+    public ResultReceiver getUssdCallbackReceiver();
+
+    /**
+     * @return the dialString.
+     */
+    public String getDialString();
+
+    Pattern sPatternCdmaMmiCodeWhileRoaming = Pattern.compile(
+            "\\*(\\d{2})(\\+{0,1})(\\d{0,})");
+    /*           1        2         3
+           1 = service code
+           2 = prefix
+           3 = number
+    */
+    int MATCH_GROUP_CDMA_MMI_CODE_SERVICE_CODE = 1;
+    int MATCH_GROUP_CDMA_MMI_CODE_NUMBER_PREFIX = 2;
+    int MATCH_GROUP_CDMA_MMI_CODE_NUMBER = 3;
+}
diff --git a/com/android/internal/telephony/NetworkScanRequestTracker.java b/com/android/internal/telephony/NetworkScanRequestTracker.java
new file mode 100644
index 0000000..14c6810
--- /dev/null
+++ b/com/android/internal/telephony/NetworkScanRequestTracker.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import static android.telephony.RadioNetworkConstants.RadioAccessNetworks.EUTRAN;
+import static android.telephony.RadioNetworkConstants.RadioAccessNetworks.GERAN;
+import static android.telephony.RadioNetworkConstants.RadioAccessNetworks.UTRAN;
+
+import android.hardware.radio.V1_0.RadioError;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Process;
+import android.os.RemoteException;
+import android.telephony.CellInfo;
+import android.telephony.NetworkScan;
+import android.telephony.NetworkScanRequest;
+import android.telephony.RadioAccessSpecifier;
+import android.telephony.TelephonyScanManager;
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Manages radio access network scan requests.
+ *
+ * Provides methods to start and stop network scan requests, and keeps track of all the live scans.
+ *
+ * {@hide}
+ */
+public final class NetworkScanRequestTracker {
+
+    private static final String TAG = "ScanRequestTracker";
+
+    private static final int CMD_START_NETWORK_SCAN = 1;
+    private static final int EVENT_START_NETWORK_SCAN_DONE = 2;
+    private static final int EVENT_RECEIVE_NETWORK_SCAN_RESULT = 3;
+    private static final int CMD_STOP_NETWORK_SCAN = 4;
+    private static final int EVENT_STOP_NETWORK_SCAN_DONE = 5;
+    private static final int CMD_INTERRUPT_NETWORK_SCAN = 6;
+    private static final int EVENT_INTERRUPT_NETWORK_SCAN_DONE = 7;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_START_NETWORK_SCAN:
+                    mScheduler.doStartScan((NetworkScanRequestInfo) msg.obj);
+                    break;
+
+                case EVENT_START_NETWORK_SCAN_DONE:
+                    mScheduler.startScanDone((AsyncResult) msg.obj);
+                    break;
+
+                case EVENT_RECEIVE_NETWORK_SCAN_RESULT:
+                    mScheduler.receiveResult((AsyncResult) msg.obj);
+                    break;
+
+                case CMD_STOP_NETWORK_SCAN:
+                    mScheduler.doStopScan(msg.arg1);
+                    break;
+
+                case EVENT_STOP_NETWORK_SCAN_DONE:
+                    mScheduler.stopScanDone((AsyncResult) msg.obj);
+                    break;
+
+                case CMD_INTERRUPT_NETWORK_SCAN:
+                    mScheduler.doInterruptScan(msg.arg1);
+                    break;
+
+                case EVENT_INTERRUPT_NETWORK_SCAN_DONE:
+                    mScheduler.interruptScanDone((AsyncResult) msg.obj);
+                    break;
+            }
+        }
+    };
+
+    // The sequence number of NetworkScanRequests
+    private final AtomicInteger mNextNetworkScanRequestId = new AtomicInteger(1);
+    private final NetworkScanRequestScheduler mScheduler = new NetworkScanRequestScheduler();
+
+    private void logEmptyResultOrException(AsyncResult ar) {
+        if (ar.result == null) {
+            Log.e(TAG, "NetworkScanResult: Empty result");
+        } else {
+            Log.e(TAG, "NetworkScanResult: Exception: " + ar.exception);
+        }
+    }
+
+    private boolean isValidScan(NetworkScanRequestInfo nsri) {
+        if (nsri.mRequest.specifiers == null) {
+            return false;
+        }
+        if (nsri.mRequest.specifiers.length > NetworkScanRequest.MAX_RADIO_ACCESS_NETWORKS) {
+            return false;
+        }
+        for (RadioAccessSpecifier ras : nsri.mRequest.specifiers) {
+            if (ras.radioAccessNetwork != GERAN && ras.radioAccessNetwork != UTRAN
+                    && ras.radioAccessNetwork != EUTRAN) {
+                return false;
+            }
+            if (ras.bands != null && ras.bands.length > NetworkScanRequest.MAX_BANDS) {
+                return false;
+            }
+            if (ras.channels != null && ras.channels.length > NetworkScanRequest.MAX_CHANNELS) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** Sends a message back to the application via its callback. */
+    private void notifyMessenger(NetworkScanRequestInfo nsri, int what, int err,
+            List<CellInfo> result) {
+        Messenger messenger = nsri.mMessenger;
+        Message message = Message.obtain();
+        message.what = what;
+        message.arg1 = err;
+        message.arg2 = nsri.mScanId;
+        if (result != null) {
+            CellInfo[] ci = result.toArray(new CellInfo[result.size()]);
+            Bundle b = new Bundle();
+            b.putParcelableArray(TelephonyScanManager.SCAN_RESULT_KEY, ci);
+            message.setData(b);
+        } else {
+            message.obj = null;
+        }
+        try {
+            messenger.send(message);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Exception in notifyMessenger: " + e);
+        }
+    }
+
+    /**
+    * Tracks info about the radio network scan.
+     *
+    * Also used to notice when the calling process dies so we can self-expire.
+    */
+    class NetworkScanRequestInfo implements IBinder.DeathRecipient {
+        private final NetworkScanRequest mRequest;
+        private final Messenger mMessenger;
+        private final IBinder mBinder;
+        private final Phone mPhone;
+        private final int mScanId;
+        private final int mUid;
+        private final int mPid;
+        private boolean mIsBinderDead;
+
+        NetworkScanRequestInfo(NetworkScanRequest r, Messenger m, IBinder b, int id, Phone phone) {
+            super();
+            mRequest = r;
+            mMessenger = m;
+            mBinder = b;
+            mScanId = id;
+            mPhone = phone;
+            mUid = Binder.getCallingUid();
+            mPid = Binder.getCallingPid();
+            mIsBinderDead = false;
+
+            try {
+                mBinder.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                binderDied();
+            }
+        }
+
+        synchronized void setIsBinderDead(boolean val) {
+            mIsBinderDead = val;
+        }
+
+        synchronized boolean getIsBinderDead() {
+            return mIsBinderDead;
+        }
+
+        NetworkScanRequest getRequest() {
+            return mRequest;
+        }
+
+        void unlinkDeathRecipient() {
+            if (mBinder != null) {
+                mBinder.unlinkToDeath(this, 0);
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            Log.e(TAG, "PhoneInterfaceManager NetworkScanRequestInfo binderDied("
+                    + mRequest + ", " + mBinder + ")");
+            setIsBinderDead(true);
+            interruptNetworkScan(mScanId);
+        }
+    }
+
+    /**
+     * Handles multiplexing and scheduling for multiple requests.
+     */
+    private class NetworkScanRequestScheduler {
+
+        private NetworkScanRequestInfo mLiveRequestInfo;
+        private NetworkScanRequestInfo mPendingRequestInfo;
+
+        private int rilErrorToScanError(int rilError) {
+            switch (rilError) {
+                case RadioError.NONE:
+                    return NetworkScan.SUCCESS;
+                case RadioError.RADIO_NOT_AVAILABLE:
+                    Log.e(TAG, "rilErrorToScanError: RADIO_NOT_AVAILABLE");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case RadioError.REQUEST_NOT_SUPPORTED:
+                    Log.e(TAG, "rilErrorToScanError: REQUEST_NOT_SUPPORTED");
+                    return NetworkScan.ERROR_UNSUPPORTED;
+                case RadioError.NO_MEMORY:
+                    Log.e(TAG, "rilErrorToScanError: NO_MEMORY");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case RadioError.INTERNAL_ERR:
+                    Log.e(TAG, "rilErrorToScanError: INTERNAL_ERR");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case RadioError.MODEM_ERR:
+                    Log.e(TAG, "rilErrorToScanError: MODEM_ERR");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case RadioError.OPERATION_NOT_ALLOWED:
+                    Log.e(TAG, "rilErrorToScanError: OPERATION_NOT_ALLOWED");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case RadioError.INVALID_ARGUMENTS:
+                    Log.e(TAG, "rilErrorToScanError: INVALID_ARGUMENTS");
+                    return NetworkScan.ERROR_INVALID_SCAN;
+                case RadioError.DEVICE_IN_USE:
+                    Log.e(TAG, "rilErrorToScanError: DEVICE_IN_USE");
+                    return NetworkScan.ERROR_MODEM_BUSY;
+                default:
+                    Log.e(TAG, "rilErrorToScanError: Unexpected RadioError " +  rilError);
+                    return NetworkScan.ERROR_RIL_ERROR;
+            }
+        }
+
+        private int commandExceptionErrorToScanError(CommandException.Error error) {
+            switch (error) {
+                case RADIO_NOT_AVAILABLE:
+                    Log.e(TAG, "commandExceptionErrorToScanError: RADIO_NOT_AVAILABLE");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case REQUEST_NOT_SUPPORTED:
+                    Log.e(TAG, "commandExceptionErrorToScanError: REQUEST_NOT_SUPPORTED");
+                    return NetworkScan.ERROR_UNSUPPORTED;
+                case NO_MEMORY:
+                    Log.e(TAG, "commandExceptionErrorToScanError: NO_MEMORY");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case INTERNAL_ERR:
+                    Log.e(TAG, "commandExceptionErrorToScanError: INTERNAL_ERR");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case MODEM_ERR:
+                    Log.e(TAG, "commandExceptionErrorToScanError: MODEM_ERR");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case OPERATION_NOT_ALLOWED:
+                    Log.e(TAG, "commandExceptionErrorToScanError: OPERATION_NOT_ALLOWED");
+                    return NetworkScan.ERROR_MODEM_ERROR;
+                case INVALID_ARGUMENTS:
+                    Log.e(TAG, "commandExceptionErrorToScanError: INVALID_ARGUMENTS");
+                    return NetworkScan.ERROR_INVALID_SCAN;
+                case DEVICE_IN_USE:
+                    Log.e(TAG, "commandExceptionErrorToScanError: DEVICE_IN_USE");
+                    return NetworkScan.ERROR_MODEM_BUSY;
+                default:
+                    Log.e(TAG, "commandExceptionErrorToScanError: Unexpected CommandExceptionError "
+                            +  error);
+                    return NetworkScan.ERROR_RIL_ERROR;
+            }
+        }
+
+        private void doStartScan(NetworkScanRequestInfo nsri) {
+            if (nsri == null) {
+                Log.e(TAG, "CMD_START_NETWORK_SCAN: nsri is null");
+                return;
+            }
+            if (!isValidScan(nsri)) {
+                notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_ERROR,
+                        NetworkScan.ERROR_INVALID_SCAN, null);
+                return;
+            }
+            if (nsri.getIsBinderDead()) {
+                Log.e(TAG, "CMD_START_NETWORK_SCAN: Binder has died");
+                return;
+            }
+            if (!startNewScan(nsri)) {
+                if (!interruptLiveScan(nsri)) {
+                    if (!cacheScan(nsri)) {
+                        notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_ERROR,
+                                NetworkScan.ERROR_MODEM_BUSY, null);
+                    }
+                }
+            }
+        }
+
+        private synchronized void startScanDone(AsyncResult ar) {
+            NetworkScanRequestInfo nsri = (NetworkScanRequestInfo) ar.userObj;
+            if (nsri == null) {
+                Log.e(TAG, "EVENT_START_NETWORK_SCAN_DONE: nsri is null");
+                return;
+            }
+            if (mLiveRequestInfo == null || nsri.mScanId != mLiveRequestInfo.mScanId) {
+                Log.e(TAG, "EVENT_START_NETWORK_SCAN_DONE: nsri does not match mLiveRequestInfo");
+                return;
+            }
+            if (ar.exception == null && ar.result != null) {
+                // Register for the scan results if the scan started successfully.
+                nsri.mPhone.mCi.registerForNetworkScanResult(mHandler,
+                        EVENT_RECEIVE_NETWORK_SCAN_RESULT, nsri);
+            } else {
+                logEmptyResultOrException(ar);
+                if (ar.exception != null) {
+                    CommandException.Error error =
+                            ((CommandException) (ar.exception)).getCommandError();
+                    deleteScanAndMayNotify(nsri, commandExceptionErrorToScanError(error), true);
+                } else {
+                    Log.wtf(TAG, "EVENT_START_NETWORK_SCAN_DONE: ar.exception can not be null!");
+                }
+            }
+        }
+
+        private void receiveResult(AsyncResult ar) {
+            NetworkScanRequestInfo nsri = (NetworkScanRequestInfo) ar.userObj;
+            if (nsri == null) {
+                Log.e(TAG, "EVENT_RECEIVE_NETWORK_SCAN_RESULT: nsri is null");
+                return;
+            }
+            if (ar.exception == null && ar.result != null) {
+                NetworkScanResult nsr = (NetworkScanResult) ar.result;
+                if (nsr.scanError == NetworkScan.SUCCESS) {
+                    notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_RESULTS,
+                            rilErrorToScanError(nsr.scanError), nsr.networkInfos);
+                    if (nsr.scanStatus == NetworkScanResult.SCAN_STATUS_COMPLETE) {
+                        deleteScanAndMayNotify(nsri, NetworkScan.SUCCESS, true);
+                        nsri.mPhone.mCi.unregisterForNetworkScanResult(mHandler);
+                    }
+                } else {
+                    if (nsr.networkInfos != null) {
+                        notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_RESULTS,
+                                NetworkScan.SUCCESS, nsr.networkInfos);
+                    }
+                    deleteScanAndMayNotify(nsri, rilErrorToScanError(nsr.scanError), true);
+                    nsri.mPhone.mCi.unregisterForNetworkScanResult(mHandler);
+                }
+            } else {
+                logEmptyResultOrException(ar);
+                deleteScanAndMayNotify(nsri, NetworkScan.ERROR_RIL_ERROR, true);
+                nsri.mPhone.mCi.unregisterForNetworkScanResult(mHandler);
+            }
+        }
+
+
+        // Stops the scan if the scanId and uid match the mScanId and mUid.
+        // If the scan to be stopped is the live scan, we only send the request to RIL, while the
+        // mLiveRequestInfo will not be cleared and the user will not be notified either.
+        // If the scan to be stopped is the pending scan, we will clear mPendingRequestInfo and
+        // notify the user.
+        private synchronized void doStopScan(int scanId) {
+            if (mLiveRequestInfo != null && scanId == mLiveRequestInfo.mScanId) {
+                mLiveRequestInfo.mPhone.stopNetworkScan(
+                        mHandler.obtainMessage(EVENT_STOP_NETWORK_SCAN_DONE, mLiveRequestInfo));
+            } else if (mPendingRequestInfo != null && scanId == mPendingRequestInfo.mScanId) {
+                notifyMessenger(mPendingRequestInfo,
+                        TelephonyScanManager.CALLBACK_SCAN_COMPLETE, NetworkScan.SUCCESS, null);
+                mPendingRequestInfo = null;
+            } else {
+                Log.e(TAG, "stopScan: scan " + scanId + " does not exist!");
+            }
+        }
+
+        private void stopScanDone(AsyncResult ar) {
+            NetworkScanRequestInfo nsri = (NetworkScanRequestInfo) ar.userObj;
+            if (nsri == null) {
+                Log.e(TAG, "EVENT_STOP_NETWORK_SCAN_DONE: nsri is null");
+                return;
+            }
+            if (ar.exception == null && ar.result != null) {
+                deleteScanAndMayNotify(nsri, NetworkScan.SUCCESS, true);
+            } else {
+                logEmptyResultOrException(ar);
+                if (ar.exception != null) {
+                    CommandException.Error error =
+                            ((CommandException) (ar.exception)).getCommandError();
+                    deleteScanAndMayNotify(nsri, commandExceptionErrorToScanError(error), true);
+                } else {
+                    Log.wtf(TAG, "EVENT_STOP_NETWORK_SCAN_DONE: ar.exception can not be null!");
+                }
+            }
+            nsri.mPhone.mCi.unregisterForNetworkScanResult(mHandler);
+        }
+
+        // Interrupts the live scan is the scanId matches the mScanId of the mLiveRequestInfo.
+        private synchronized void doInterruptScan(int scanId) {
+            if (mLiveRequestInfo != null && scanId == mLiveRequestInfo.mScanId) {
+                mLiveRequestInfo.mPhone.stopNetworkScan(mHandler.obtainMessage(
+                        EVENT_INTERRUPT_NETWORK_SCAN_DONE, mLiveRequestInfo));
+            } else {
+                Log.e(TAG, "doInterruptScan: scan " + scanId + " does not exist!");
+            }
+        }
+
+        private void interruptScanDone(AsyncResult ar) {
+            NetworkScanRequestInfo nsri = (NetworkScanRequestInfo) ar.userObj;
+            if (nsri == null) {
+                Log.e(TAG, "EVENT_INTERRUPT_NETWORK_SCAN_DONE: nsri is null");
+                return;
+            }
+            nsri.mPhone.mCi.unregisterForNetworkScanResult(mHandler);
+            deleteScanAndMayNotify(nsri, 0, false);
+        }
+
+        // Interrupts the live scan and caches nsri in mPendingRequestInfo. Once the live scan is
+        // stopped, a new scan will automatically start with nsri.
+        // The new scan can interrupt the live scan only when all the below requirements are met:
+        //   1. There is 1 live scan and no other pending scan
+        //   2. The new scan is requested by system process
+        //   3. The live scan is not requested by system process
+        private synchronized boolean interruptLiveScan(NetworkScanRequestInfo nsri) {
+            if (mLiveRequestInfo != null && mPendingRequestInfo == null
+                    && nsri.mUid == Process.SYSTEM_UID
+                            && mLiveRequestInfo.mUid != Process.SYSTEM_UID) {
+                doInterruptScan(mLiveRequestInfo.mScanId);
+                mPendingRequestInfo = nsri;
+                notifyMessenger(mLiveRequestInfo, TelephonyScanManager.CALLBACK_SCAN_ERROR,
+                        NetworkScan.ERROR_INTERRUPTED, null);
+                return true;
+            }
+            return false;
+        }
+
+        private boolean cacheScan(NetworkScanRequestInfo nsri) {
+            // TODO(30954762): Cache periodic scan for OC-MR1.
+            return false;
+        }
+
+        // Starts a new scan with nsri if there is no live scan running.
+        private synchronized boolean startNewScan(NetworkScanRequestInfo nsri) {
+            if (mLiveRequestInfo == null) {
+                mLiveRequestInfo = nsri;
+                nsri.mPhone.startNetworkScan(nsri.getRequest(),
+                        mHandler.obtainMessage(EVENT_START_NETWORK_SCAN_DONE, nsri));
+                return true;
+            }
+            return false;
+        }
+
+
+        // Deletes the mLiveRequestInfo and notify the user if it matches nsri.
+        private synchronized void deleteScanAndMayNotify(NetworkScanRequestInfo nsri, int error,
+                boolean notify) {
+            if (mLiveRequestInfo != null && nsri.mScanId == mLiveRequestInfo.mScanId) {
+                if (notify) {
+                    if (error == NetworkScan.SUCCESS) {
+                        notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_COMPLETE, error,
+                                null);
+                    } else {
+                        notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_ERROR, error,
+                                null);
+                    }
+                }
+                mLiveRequestInfo = null;
+                if (mPendingRequestInfo != null) {
+                    startNewScan(mPendingRequestInfo);
+                    mPendingRequestInfo = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * Interrupts an ongoing network scan
+     *
+     * This method is similar to stopNetworkScan, since they both stops an ongoing scan. The
+     * difference is that stopNetworkScan is only used by the callers to stop their own scans, so
+     * sanity check will be done to make sure the request is valid; while this method is only
+     * internally used by NetworkScanRequestTracker so sanity check is not needed.
+     */
+    private void interruptNetworkScan(int scanId) {
+        // scanId will be stored at Message.arg1
+        mHandler.obtainMessage(CMD_INTERRUPT_NETWORK_SCAN, scanId, 0).sendToTarget();
+    }
+
+    /**
+     * Starts a new network scan
+     *
+     * This function only wraps all the incoming information and delegate then to the handler thread
+     * which will actually handles the scan request. So a new scanId will always be generated and
+     * returned to the user, no matter how this scan will be actually handled.
+     */
+    public int startNetworkScan(
+            NetworkScanRequest request, Messenger messenger, IBinder binder, Phone phone) {
+        int scanId = mNextNetworkScanRequestId.getAndIncrement();
+        NetworkScanRequestInfo nsri =
+                new NetworkScanRequestInfo(request, messenger, binder, scanId, phone);
+        // nsri will be stored as Message.obj
+        mHandler.obtainMessage(CMD_START_NETWORK_SCAN, nsri).sendToTarget();
+        return scanId;
+    }
+
+    /**
+     * Stops an ongoing network scan
+     *
+     * The ongoing scan will be stopped only when the input scanId and caller's uid matches the
+     * corresponding information associated with it.
+     */
+    public void stopNetworkScan(int scanId) {
+        synchronized (mScheduler) {
+            if ((mScheduler.mLiveRequestInfo != null
+                    && scanId == mScheduler.mLiveRequestInfo.mScanId
+                    && Binder.getCallingUid() == mScheduler.mLiveRequestInfo.mUid)
+                    || (mScheduler.mPendingRequestInfo != null
+                    && scanId == mScheduler.mPendingRequestInfo.mScanId
+                    && Binder.getCallingUid() == mScheduler.mPendingRequestInfo.mUid)) {
+                // scanId will be stored at Message.arg1
+                mHandler.obtainMessage(CMD_STOP_NETWORK_SCAN, scanId, 0).sendToTarget();
+            } else {
+                throw new IllegalArgumentException("Scan with id: " + scanId + " does not exist!");
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/NetworkScanResult.java b/com/android/internal/telephony/NetworkScanResult.java
new file mode 100644
index 0000000..95f39d7
--- /dev/null
+++ b/com/android/internal/telephony/NetworkScanResult.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.CellInfo;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Defines the incremental network scan result.
+ *
+ * This class contains the network scan results. When the user starts a new scan, multiple
+ * NetworkScanResult may be returned, containing either the scan result or error. When the user
+ * stops an ongoing scan, only one NetworkScanResult will be returned to indicate either the scan
+ * is now complete or there is some error stopping it.
+ * @hide
+ */
+public final class NetworkScanResult implements Parcelable {
+
+    // Contains only part of the scan result and more are coming.
+    public static final int SCAN_STATUS_PARTIAL = 1;
+
+    // Contains the last part of the scan result and the scan is now complete.
+    public static final int SCAN_STATUS_COMPLETE = 2;
+
+    // The status of the scan, only valid when scanError = SUCCESS.
+    public int scanStatus;
+
+    /**
+     * The error code of the scan
+     *
+     * This is the error code returned from the RIL, see {@link RILConstants} for more details
+     */
+    public int scanError;
+
+    // The scan results, only valid when scanError = SUCCESS.
+    public List<CellInfo> networkInfos;
+
+    /**
+     * Creates a new NetworkScanResult with scanStatus, scanError and networkInfos
+     *
+     * @param scanStatus The status of the scan.
+     * @param scanError The error code of the scan.
+     * @param networkInfos List of the CellInfo.
+     */
+    public NetworkScanResult(int scanStatus, int scanError, List<CellInfo> networkInfos) {
+        this.scanStatus = scanStatus;
+        this.scanError = scanError;
+        this.networkInfos = networkInfos;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(scanStatus);
+        dest.writeInt(scanError);
+        dest.writeParcelableList(networkInfos, flags);
+    }
+
+    private NetworkScanResult(Parcel in) {
+        scanStatus = in.readInt();
+        scanError = in.readInt();
+        List<CellInfo> ni = new ArrayList<>();
+        in.readParcelableList(ni, Object.class.getClassLoader());
+        networkInfos = ni;
+    }
+
+    @Override
+    public boolean equals (Object o) {
+        NetworkScanResult nsr;
+
+        try {
+            nsr = (NetworkScanResult) o;
+        } catch (ClassCastException ex) {
+            return false;
+        }
+
+        if (o == null) {
+            return false;
+        }
+
+        return (scanStatus == nsr.scanStatus
+                && scanError == nsr.scanError
+                && networkInfos.equals(nsr.networkInfos));
+    }
+
+    @Override
+    public int hashCode () {
+        return ((scanStatus * 31)
+                + (scanError * 23)
+                + (Objects.hashCode(networkInfos) * 37));
+    }
+
+    public static final Creator<NetworkScanResult> CREATOR =
+        new Creator<NetworkScanResult>() {
+            @Override
+            public NetworkScanResult createFromParcel(Parcel in) {
+                return new NetworkScanResult(in);
+            }
+
+            @Override
+            public NetworkScanResult[] newArray(int size) {
+                return new NetworkScanResult[size];
+            }
+        };
+}
diff --git a/com/android/internal/telephony/OemHookIndication.java b/com/android/internal/telephony/OemHookIndication.java
new file mode 100644
index 0000000..122a70e
--- /dev/null
+++ b/com/android/internal/telephony/OemHookIndication.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.hardware.radio.deprecated.V1_0.IOemHookIndication;
+import android.os.AsyncResult;
+
+import java.util.ArrayList;
+
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_OEM_HOOK_RAW;
+
+/**
+ * Class containing oem hook indication callbacks
+ */
+public class OemHookIndication extends IOemHookIndication.Stub {
+    RIL mRil;
+
+    public OemHookIndication(RIL ril) {
+        mRil = ril;
+    }
+
+    /**
+     * @param indicationType RadioIndicationType
+     * @param data Data sent by oem
+     */
+    public void oemHookRaw(int indicationType, ArrayList<Byte> data) {
+        mRil.processIndication(indicationType);
+
+        byte[] response = RIL.arrayListToPrimitiveArray(data);
+        if (RIL.RILJ_LOGD) {
+            mRil.unsljLogvRet(RIL_UNSOL_OEM_HOOK_RAW,
+                    com.android.internal.telephony.uicc.IccUtils.bytesToHexString(response));
+        }
+
+        if (mRil.mUnsolOemHookRawRegistrant != null) {
+            mRil.mUnsolOemHookRawRegistrant.notifyRegistrant(new AsyncResult(null, response, null));
+        }
+    }
+}
diff --git a/com/android/internal/telephony/OemHookResponse.java b/com/android/internal/telephony/OemHookResponse.java
new file mode 100644
index 0000000..0afeac8
--- /dev/null
+++ b/com/android/internal/telephony/OemHookResponse.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.hardware.radio.deprecated.V1_0.IOemHookResponse;
+import android.hardware.radio.V1_0.RadioError;
+import android.hardware.radio.V1_0.RadioResponseInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Class containing oem hook response callbacks
+ */
+public class OemHookResponse extends IOemHookResponse.Stub {
+    RIL mRil;
+
+    public OemHookResponse(RIL ril) {
+        mRil = ril;
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param data Data returned by oem
+     */
+    public void sendRequestRawResponse(RadioResponseInfo responseInfo, ArrayList<Byte> data) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            byte[] ret = null;
+            if (responseInfo.error == RadioError.NONE) {
+                ret = RIL.arrayListToPrimitiveArray(data);
+                RadioResponse.sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param data Data returned by oem
+     */
+    public void sendRequestStringsResponse(RadioResponseInfo responseInfo, ArrayList<String> data) {
+        RadioResponse.responseStringArrayList(mRil, responseInfo, data);
+    }
+}
diff --git a/com/android/internal/telephony/OperatorInfo.java b/com/android/internal/telephony/OperatorInfo.java
new file mode 100644
index 0000000..a29d7c1
--- /dev/null
+++ b/com/android/internal/telephony/OperatorInfo.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * {@hide}
+ */
+public class OperatorInfo implements Parcelable {
+    public enum State {
+        UNKNOWN,
+        AVAILABLE,
+        CURRENT,
+        FORBIDDEN;
+    }
+
+    private String mOperatorAlphaLong;
+    private String mOperatorAlphaShort;
+    private String mOperatorNumeric;
+
+    private State mState = State.UNKNOWN;
+
+
+    public String
+    getOperatorAlphaLong() {
+        return mOperatorAlphaLong;
+    }
+
+    public String
+    getOperatorAlphaShort() {
+        return mOperatorAlphaShort;
+    }
+
+    public String
+    getOperatorNumeric() {
+        return mOperatorNumeric;
+    }
+
+    public State
+    getState() {
+        return mState;
+    }
+
+    OperatorInfo(String operatorAlphaLong,
+                String operatorAlphaShort,
+                String operatorNumeric,
+                State state) {
+
+        mOperatorAlphaLong = operatorAlphaLong;
+        mOperatorAlphaShort = operatorAlphaShort;
+        mOperatorNumeric = operatorNumeric;
+
+        mState = state;
+    }
+
+
+    public OperatorInfo(String operatorAlphaLong,
+                String operatorAlphaShort,
+                String operatorNumeric,
+                String stateString) {
+        this (operatorAlphaLong, operatorAlphaShort,
+                operatorNumeric, rilStateToState(stateString));
+    }
+
+    public OperatorInfo(String operatorAlphaLong,
+            String operatorAlphaShort,
+            String operatorNumeric) {
+        this(operatorAlphaLong, operatorAlphaShort, operatorNumeric, State.UNKNOWN);
+    }
+
+    /**
+     * See state strings defined in ril.h RIL_REQUEST_QUERY_AVAILABLE_NETWORKS
+     */
+    private static State rilStateToState(String s) {
+        if (s.equals("unknown")) {
+            return State.UNKNOWN;
+        } else if (s.equals("available")) {
+            return State.AVAILABLE;
+        } else if (s.equals("current")) {
+            return State.CURRENT;
+        } else if (s.equals("forbidden")) {
+            return State.FORBIDDEN;
+        } else {
+            throw new RuntimeException(
+                "RIL impl error: Invalid network state '" + s + "'");
+        }
+    }
+
+
+    @Override
+    public String toString() {
+        return "OperatorInfo " + mOperatorAlphaLong
+                + "/" + mOperatorAlphaShort
+                + "/" + mOperatorNumeric
+                + "/" + mState;
+    }
+
+    /**
+     * Parcelable interface implemented below.
+     * This is a simple effort to make OperatorInfo parcelable rather than
+     * trying to make the conventional containing object (AsyncResult),
+     * implement parcelable.  This functionality is needed for the
+     * NetworkQueryService to fix 1128695.
+     */
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * Method to serialize a OperatorInfo object.
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mOperatorAlphaLong);
+        dest.writeString(mOperatorAlphaShort);
+        dest.writeString(mOperatorNumeric);
+        dest.writeSerializable(mState);
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * Method to deserialize a OperatorInfo object, or an array thereof.
+     */
+    public static final Creator<OperatorInfo> CREATOR =
+        new Creator<OperatorInfo>() {
+            @Override
+            public OperatorInfo createFromParcel(Parcel in) {
+                OperatorInfo opInfo = new OperatorInfo(
+                        in.readString(), /*operatorAlphaLong*/
+                        in.readString(), /*operatorAlphaShort*/
+                        in.readString(), /*operatorNumeric*/
+                        (State) in.readSerializable()); /*state*/
+                return opInfo;
+            }
+
+            @Override
+            public OperatorInfo[] newArray(int size) {
+                return new OperatorInfo[size];
+            }
+        };
+}
diff --git a/com/android/internal/telephony/Phone.java b/com/android/internal/telephony/Phone.java
new file mode 100644
index 0000000..28e4556
--- /dev/null
+++ b/com/android/internal/telephony/Phone.java
@@ -0,0 +1,3698 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.NetworkStats;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.AsyncResult;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.SystemProperties;
+import android.os.WorkSource;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.service.carrier.CarrierIdentifier;
+import android.telecom.VideoProfile;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellIdentityCdma;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoCdma;
+import android.telephony.CellLocation;
+import android.telephony.ClientRequestStats;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.PhoneStateListener;
+import android.telephony.RadioAccessFamily;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionManager;
+import android.telephony.VoLteServiceState;
+import android.text.TextUtils;
+
+import com.android.ims.ImsCall;
+import com.android.ims.ImsConfig;
+import com.android.ims.ImsManager;
+import com.android.internal.R;
+import com.android.internal.telephony.dataconnection.DataConnectionReasons;
+import com.android.internal.telephony.dataconnection.DcTracker;
+import com.android.internal.telephony.imsphone.ImsPhoneCall;
+import com.android.internal.telephony.test.SimulatedRadioControl;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.IsimRecords;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.uicc.UsimServiceTable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * (<em>Not for SDK use</em>)
+ * A base implementation for the com.android.internal.telephony.Phone interface.
+ *
+ * Note that implementations of Phone.java are expected to be used
+ * from a single application thread. This should be the same thread that
+ * originally called PhoneFactory to obtain the interface.
+ *
+ *  {@hide}
+ *
+ */
+
+public abstract class Phone extends Handler implements PhoneInternalInterface {
+    private static final String LOG_TAG = "Phone";
+
+    protected final static Object lockForRadioTechnologyChange = new Object();
+
+    protected final int USSD_MAX_QUEUE = 10;
+
+    private BroadcastReceiver mImsIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Rlog.d(LOG_TAG, "mImsIntentReceiver: action " + intent.getAction());
+            if (intent.hasExtra(ImsManager.EXTRA_PHONE_ID)) {
+                int extraPhoneId = intent.getIntExtra(ImsManager.EXTRA_PHONE_ID,
+                        SubscriptionManager.INVALID_PHONE_INDEX);
+                Rlog.d(LOG_TAG, "mImsIntentReceiver: extraPhoneId = " + extraPhoneId);
+                if (extraPhoneId == SubscriptionManager.INVALID_PHONE_INDEX ||
+                        extraPhoneId != getPhoneId()) {
+                    return;
+                }
+            }
+
+            synchronized (Phone.lockForRadioTechnologyChange) {
+                if (intent.getAction().equals(ImsManager.ACTION_IMS_SERVICE_UP)) {
+                    mImsServiceReady = true;
+                    updateImsPhone();
+                    ImsManager.updateImsServiceConfig(mContext, mPhoneId, false);
+                } else if (intent.getAction().equals(ImsManager.ACTION_IMS_SERVICE_DOWN)) {
+                    mImsServiceReady = false;
+                    updateImsPhone();
+                } else if (intent.getAction().equals(ImsConfig.ACTION_IMS_CONFIG_CHANGED)) {
+                    int item = intent.getIntExtra(ImsConfig.EXTRA_CHANGED_ITEM, -1);
+                    String value = intent.getStringExtra(ImsConfig.EXTRA_NEW_VALUE);
+                    ImsManager.onProvisionedValueChanged(context, item, value);
+                }
+            }
+        }
+    };
+
+    // Key used to read and write the saved network selection numeric value
+    public static final String NETWORK_SELECTION_KEY = "network_selection_key";
+    // Key used to read and write the saved network selection operator name
+    public static final String NETWORK_SELECTION_NAME_KEY = "network_selection_name_key";
+    // Key used to read and write the saved network selection operator short name
+    public static final String NETWORK_SELECTION_SHORT_KEY = "network_selection_short_key";
+
+
+    // Key used to read/write "disable data connection on boot" pref (used for testing)
+    public static final String DATA_DISABLED_ON_BOOT_KEY = "disabled_on_boot_key";
+
+    // Key used to read/write data_roaming_is_user_setting pref
+    public static final String DATA_ROAMING_IS_USER_SETTING_KEY = "data_roaming_is_user_setting_key";
+
+    /* Event Constants */
+    protected static final int EVENT_RADIO_AVAILABLE             = 1;
+    /** Supplementary Service Notification received. */
+    protected static final int EVENT_SSN                         = 2;
+    protected static final int EVENT_SIM_RECORDS_LOADED          = 3;
+    private static final int EVENT_MMI_DONE                      = 4;
+    protected static final int EVENT_RADIO_ON                    = 5;
+    protected static final int EVENT_GET_BASEBAND_VERSION_DONE   = 6;
+    protected static final int EVENT_USSD                        = 7;
+    protected static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE  = 8;
+    protected static final int EVENT_GET_IMEI_DONE               = 9;
+    protected static final int EVENT_GET_IMEISV_DONE             = 10;
+    private static final int EVENT_GET_SIM_STATUS_DONE           = 11;
+    protected static final int EVENT_SET_CALL_FORWARD_DONE       = 12;
+    protected static final int EVENT_GET_CALL_FORWARD_DONE       = 13;
+    protected static final int EVENT_CALL_RING                   = 14;
+    private static final int EVENT_CALL_RING_CONTINUE            = 15;
+
+    // Used to intercept the carrier selection calls so that
+    // we can save the values.
+    private static final int EVENT_SET_NETWORK_MANUAL_COMPLETE      = 16;
+    private static final int EVENT_SET_NETWORK_AUTOMATIC_COMPLETE   = 17;
+    protected static final int EVENT_SET_CLIR_COMPLETE              = 18;
+    protected static final int EVENT_REGISTERED_TO_NETWORK          = 19;
+    protected static final int EVENT_SET_VM_NUMBER_DONE             = 20;
+    // Events for CDMA support
+    protected static final int EVENT_GET_DEVICE_IDENTITY_DONE       = 21;
+    protected static final int EVENT_RUIM_RECORDS_LOADED            = 22;
+    protected static final int EVENT_NV_READY                       = 23;
+    private static final int EVENT_SET_ENHANCED_VP                  = 24;
+    protected static final int EVENT_EMERGENCY_CALLBACK_MODE_ENTER  = 25;
+    protected static final int EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE = 26;
+    protected static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 27;
+    // other
+    protected static final int EVENT_SET_NETWORK_AUTOMATIC          = 28;
+    protected static final int EVENT_ICC_RECORD_EVENTS              = 29;
+    private static final int EVENT_ICC_CHANGED                      = 30;
+    // Single Radio Voice Call Continuity
+    private static final int EVENT_SRVCC_STATE_CHANGED              = 31;
+    private static final int EVENT_INITIATE_SILENT_REDIAL           = 32;
+    private static final int EVENT_RADIO_NOT_AVAILABLE              = 33;
+    private static final int EVENT_UNSOL_OEM_HOOK_RAW               = 34;
+    protected static final int EVENT_GET_RADIO_CAPABILITY           = 35;
+    protected static final int EVENT_SS                             = 36;
+    private static final int EVENT_CONFIG_LCE                       = 37;
+    private static final int EVENT_CHECK_FOR_NETWORK_AUTOMATIC      = 38;
+    protected static final int EVENT_VOICE_RADIO_TECH_CHANGED       = 39;
+    protected static final int EVENT_REQUEST_VOICE_RADIO_TECH_DONE  = 40;
+    protected static final int EVENT_RIL_CONNECTED                  = 41;
+    protected static final int EVENT_UPDATE_PHONE_OBJECT            = 42;
+    protected static final int EVENT_CARRIER_CONFIG_CHANGED         = 43;
+    // Carrier's CDMA prefer mode setting
+    protected static final int EVENT_SET_ROAMING_PREFERENCE_DONE    = 44;
+    protected static final int EVENT_MODEM_RESET                    = 45;
+
+    protected static final int EVENT_LAST                       = EVENT_MODEM_RESET;
+
+    // For shared prefs.
+    private static final String GSM_ROAMING_LIST_OVERRIDE_PREFIX = "gsm_roaming_list_";
+    private static final String GSM_NON_ROAMING_LIST_OVERRIDE_PREFIX = "gsm_non_roaming_list_";
+    private static final String CDMA_ROAMING_LIST_OVERRIDE_PREFIX = "cdma_roaming_list_";
+    private static final String CDMA_NON_ROAMING_LIST_OVERRIDE_PREFIX = "cdma_non_roaming_list_";
+
+    // Key used to read/write current CLIR setting
+    public static final String CLIR_KEY = "clir_key";
+
+    // Key used for storing voice mail count
+    private static final String VM_COUNT = "vm_count_key";
+    // Key used to read/write the ID for storing the voice mail
+    private static final String VM_ID = "vm_id_key";
+
+    // Key used for storing call forwarding status
+    public static final String CF_STATUS = "cf_status_key";
+    // Key used to read/write the ID for storing the call forwarding status
+    public static final String CF_ID = "cf_id_key";
+
+    // Key used to read/write "disable DNS server check" pref (used for testing)
+    private static final String DNS_SERVER_CHECK_DISABLED_KEY = "dns_server_check_disabled_key";
+
+    /**
+     * This method is invoked when the Phone exits Emergency Callback Mode.
+     */
+    protected void handleExitEmergencyCallbackMode() {
+    }
+
+    /**
+     * Small container class used to hold information relevant to
+     * the carrier selection process. operatorNumeric can be ""
+     * if we are looking for automatic selection. operatorAlphaLong is the
+     * corresponding operator name.
+     */
+    private static class NetworkSelectMessage {
+        public Message message;
+        public String operatorNumeric;
+        public String operatorAlphaLong;
+        public String operatorAlphaShort;
+    }
+
+    /* Instance Variables */
+    public CommandsInterface mCi;
+    protected int mVmCount = 0;
+    private boolean mDnsCheckDisabled;
+    public DcTracker mDcTracker;
+    /* Used for dispatching signals to configured carrier apps */
+    protected CarrierSignalAgent mCarrierSignalAgent;
+    /* Used for dispatching carrier action from carrier apps */
+    protected CarrierActionAgent mCarrierActionAgent;
+    private boolean mDoesRilSendMultipleCallRing;
+    private int mCallRingContinueToken;
+    private int mCallRingDelay;
+    private boolean mIsVoiceCapable = true;
+    private final AppSmsManager mAppSmsManager;
+    private SimActivationTracker mSimActivationTracker;
+    // Keep track of whether or not the phone is in Emergency Callback Mode for Phone and
+    // subclasses
+    protected boolean mIsPhoneInEcmState = false;
+
+    // Variable to cache the video capability. When RAT changes, we lose this info and are unable
+    // to recover from the state. We cache it and notify listeners when they register.
+    protected boolean mIsVideoCapable = false;
+    protected UiccController mUiccController = null;
+    protected final AtomicReference<IccRecords> mIccRecords = new AtomicReference<IccRecords>();
+    public SmsStorageMonitor mSmsStorageMonitor;
+    public SmsUsageMonitor mSmsUsageMonitor;
+    protected AtomicReference<UiccCardApplication> mUiccApplication =
+            new AtomicReference<UiccCardApplication>();
+
+    private TelephonyTester mTelephonyTester;
+    private String mName;
+    private final String mActionDetached;
+    private final String mActionAttached;
+
+    protected int mPhoneId;
+
+    private boolean mImsServiceReady = false;
+    protected Phone mImsPhone = null;
+
+    private final AtomicReference<RadioCapability> mRadioCapability =
+            new AtomicReference<RadioCapability>();
+
+    private static final int DEFAULT_REPORT_INTERVAL_MS = 200;
+    private static final boolean LCE_PULL_MODE = true;
+    private int mLceStatus = RILConstants.LCE_NOT_AVAILABLE;
+    protected TelephonyComponentFactory mTelephonyComponentFactory;
+
+    //IMS
+    /**
+     * {@link CallStateException} message text used to indicate that an IMS call has failed because
+     * it needs to be retried using GSM or CDMA (e.g. CS fallback).
+     * TODO: Replace this with a proper exception; {@link CallStateException} doesn't make sense.
+     */
+    public static final String CS_FALLBACK = "cs_fallback";
+    public static final String EXTRA_KEY_ALERT_TITLE = "alertTitle";
+    public static final String EXTRA_KEY_ALERT_MESSAGE = "alertMessage";
+    public static final String EXTRA_KEY_ALERT_SHOW = "alertShow";
+    public static final String EXTRA_KEY_NOTIFICATION_MESSAGE = "notificationMessage";
+
+    private final RegistrantList mPreciseCallStateRegistrants
+            = new RegistrantList();
+
+    private final RegistrantList mHandoverRegistrants
+            = new RegistrantList();
+
+    private final RegistrantList mNewRingingConnectionRegistrants
+            = new RegistrantList();
+
+    private final RegistrantList mIncomingRingRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mDisconnectRegistrants
+            = new RegistrantList();
+
+    private final RegistrantList mServiceStateRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mMmiCompleteRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mMmiRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mUnknownConnectionRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mSuppServiceFailedRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mRadioOffOrNotAvailableRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mSimRecordsLoadedRegistrants
+            = new RegistrantList();
+
+    private final RegistrantList mVideoCapabilityChangedRegistrants
+            = new RegistrantList();
+
+    protected final RegistrantList mEmergencyCallToggledRegistrants
+            = new RegistrantList();
+
+    protected Registrant mPostDialHandler;
+
+    private Looper mLooper; /* to insure registrants are in correct thread*/
+
+    protected final Context mContext;
+
+    /**
+     * PhoneNotifier is an abstraction for all system-wide
+     * state change notification. DefaultPhoneNotifier is
+     * used here unless running we're inside a unit test.
+     */
+    protected PhoneNotifier mNotifier;
+
+    protected SimulatedRadioControl mSimulatedRadioControl;
+
+    private boolean mUnitTestMode;
+
+    public IccRecords getIccRecords() {
+        return mIccRecords.get();
+    }
+
+    /**
+     * Returns a string identifier for this phone interface for parties
+     *  outside the phone app process.
+     *  @return The string name.
+     */
+    public String getPhoneName() {
+        return mName;
+    }
+
+    protected void setPhoneName(String name) {
+        mName = name;
+    }
+
+    /**
+     * Retrieves Nai for phones. Returns null if Nai is not set.
+     */
+    public String getNai(){
+         return null;
+    }
+
+    /**
+     * Return the ActionDetached string. When this action is received by components
+     * they are to simulate detaching from the network.
+     *
+     * @return com.android.internal.telephony.{mName}.action_detached
+     *          {mName} is GSM, CDMA ...
+     */
+    public String getActionDetached() {
+        return mActionDetached;
+    }
+
+    /**
+     * Return the ActionAttached string. When this action is received by components
+     * they are to simulate attaching to the network.
+     *
+     * @return com.android.internal.telephony.{mName}.action_detached
+     *          {mName} is GSM, CDMA ...
+     */
+    public String getActionAttached() {
+        return mActionAttached;
+    }
+
+    /**
+     * Set a system property, unless we're in unit test mode
+     */
+    // CAF_MSIM TODO this need to be replated with TelephonyManager API ?
+    public void setSystemProperty(String property, String value) {
+        if(getUnitTestMode()) {
+            return;
+        }
+        SystemProperties.set(property, value);
+    }
+
+    /**
+     * Set a system property, unless we're in unit test mode
+     */
+    // CAF_MSIM TODO this need to be replated with TelephonyManager API ?
+    public String getSystemProperty(String property, String defValue) {
+        if(getUnitTestMode()) {
+            return null;
+        }
+        return SystemProperties.get(property, defValue);
+    }
+
+    /**
+     * Constructs a Phone in normal (non-unit test) mode.
+     *
+     * @param notifier An instance of DefaultPhoneNotifier,
+     * @param context Context object from hosting application
+     * unless unit testing.
+     * @param ci is CommandsInterface
+     * @param unitTestMode when true, prevents notifications
+     * of state change events
+     */
+    protected Phone(String name, PhoneNotifier notifier, Context context, CommandsInterface ci,
+                    boolean unitTestMode) {
+        this(name, notifier, context, ci, unitTestMode, SubscriptionManager.DEFAULT_PHONE_INDEX,
+                TelephonyComponentFactory.getInstance());
+    }
+
+    /**
+     * Constructs a Phone in normal (non-unit test) mode.
+     *
+     * @param notifier An instance of DefaultPhoneNotifier,
+     * @param context Context object from hosting application
+     * unless unit testing.
+     * @param ci is CommandsInterface
+     * @param unitTestMode when true, prevents notifications
+     * of state change events
+     * @param phoneId the phone-id of this phone.
+     */
+    protected Phone(String name, PhoneNotifier notifier, Context context, CommandsInterface ci,
+                    boolean unitTestMode, int phoneId,
+                    TelephonyComponentFactory telephonyComponentFactory) {
+        mPhoneId = phoneId;
+        mName = name;
+        mNotifier = notifier;
+        mContext = context;
+        mLooper = Looper.myLooper();
+        mCi = ci;
+        mActionDetached = this.getClass().getPackage().getName() + ".action_detached";
+        mActionAttached = this.getClass().getPackage().getName() + ".action_attached";
+        mAppSmsManager = telephonyComponentFactory.makeAppSmsManager(context);
+
+        if (Build.IS_DEBUGGABLE) {
+            mTelephonyTester = new TelephonyTester(this);
+        }
+
+        setUnitTestMode(unitTestMode);
+
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
+        mDnsCheckDisabled = sp.getBoolean(DNS_SERVER_CHECK_DISABLED_KEY, false);
+        mCi.setOnCallRing(this, EVENT_CALL_RING, null);
+
+        /* "Voice capable" means that this device supports circuit-switched
+        * (i.e. voice) phone calls over the telephony network, and is allowed
+        * to display the in-call UI while a cellular voice call is active.
+        * This will be false on "data only" devices which can't make voice
+        * calls and don't support any in-call UI.
+        */
+        mIsVoiceCapable = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_voice_capable);
+
+        /**
+         *  Some RIL's don't always send RIL_UNSOL_CALL_RING so it needs
+         *  to be generated locally. Ideally all ring tones should be loops
+         * and this wouldn't be necessary. But to minimize changes to upper
+         * layers it is requested that it be generated by lower layers.
+         *
+         * By default old phones won't have the property set but do generate
+         * the RIL_UNSOL_CALL_RING so the default if there is no property is
+         * true.
+         */
+        mDoesRilSendMultipleCallRing = SystemProperties.getBoolean(
+                TelephonyProperties.PROPERTY_RIL_SENDS_MULTIPLE_CALL_RING, true);
+        Rlog.d(LOG_TAG, "mDoesRilSendMultipleCallRing=" + mDoesRilSendMultipleCallRing);
+
+        mCallRingDelay = SystemProperties.getInt(
+                TelephonyProperties.PROPERTY_CALL_RING_DELAY, 3000);
+        Rlog.d(LOG_TAG, "mCallRingDelay=" + mCallRingDelay);
+
+        if (getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
+            return;
+        }
+
+        // The locale from the "ro.carrier" system property or R.array.carrier_properties.
+        // This will be overwritten by the Locale from the SIM language settings (EF-PL, EF-LI)
+        // if applicable.
+        final Locale carrierLocale = getLocaleFromCarrierProperties(mContext);
+        if (carrierLocale != null && !TextUtils.isEmpty(carrierLocale.getCountry())) {
+            final String country = carrierLocale.getCountry();
+            try {
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.WIFI_COUNTRY_CODE);
+            } catch (Settings.SettingNotFoundException e) {
+                // note this is not persisting
+                WifiManager wM = (WifiManager)
+                        mContext.getSystemService(Context.WIFI_SERVICE);
+                wM.setCountryCode(country, false);
+            }
+        }
+
+        // Initialize device storage and outgoing SMS usage monitors for SMSDispatchers.
+        mTelephonyComponentFactory = telephonyComponentFactory;
+        mSmsStorageMonitor = mTelephonyComponentFactory.makeSmsStorageMonitor(this);
+        mSmsUsageMonitor = mTelephonyComponentFactory.makeSmsUsageMonitor(context);
+        mUiccController = UiccController.getInstance();
+        mUiccController.registerForIccChanged(this, EVENT_ICC_CHANGED, null);
+        mSimActivationTracker = mTelephonyComponentFactory.makeSimActivationTracker(this);
+        if (getPhoneType() != PhoneConstants.PHONE_TYPE_SIP) {
+            mCi.registerForSrvccStateChanged(this, EVENT_SRVCC_STATE_CHANGED, null);
+        }
+        mCi.setOnUnsolOemHookRaw(this, EVENT_UNSOL_OEM_HOOK_RAW, null);
+        mCi.startLceService(DEFAULT_REPORT_INTERVAL_MS, LCE_PULL_MODE,
+                obtainMessage(EVENT_CONFIG_LCE));
+    }
+
+    /**
+     * Start listening for IMS service UP/DOWN events. If using the new ImsResolver APIs, we should
+     * always be setting up ImsPhones.
+     */
+    public void startMonitoringImsService() {
+        if (getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) {
+            return;
+        }
+
+        synchronized(Phone.lockForRadioTechnologyChange) {
+            IntentFilter filter = new IntentFilter();
+            ImsManager imsManager = ImsManager.getInstance(mContext, getPhoneId());
+            // Don't listen to deprecated intents using the new dynamic binding.
+            if (imsManager != null && !imsManager.isDynamicBinding()) {
+                filter.addAction(ImsManager.ACTION_IMS_SERVICE_UP);
+                filter.addAction(ImsManager.ACTION_IMS_SERVICE_DOWN);
+            }
+            filter.addAction(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
+            mContext.registerReceiver(mImsIntentReceiver, filter);
+
+            // Monitor IMS service - but first poll to see if already up (could miss
+            // intent). Also, when using new ImsResolver APIs, the service will be available soon,
+            // so start trying to bind.
+            if (imsManager != null) {
+                // If it is dynamic binding, kick off ImsPhone creation now instead of waiting for
+                // the service to be available.
+                if (imsManager.isDynamicBinding() || imsManager.isServiceAvailable()) {
+                    mImsServiceReady = true;
+                    updateImsPhone();
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if device should convert CDMA Caller ID restriction related MMI codes to
+     * equivalent 3GPP MMI Codes that provide same functionality when device is roaming.
+     * This method should only return true on multi-mode devices when carrier requires this
+     * conversion to be done on the device.
+     *
+     * @return true when carrier config
+     * "KEY_CONVERT_CDMA_CALLER_ID_MMI_CODES_WHILE_ROAMING_ON_3GPP_BOOL" is set to true
+     */
+    public boolean supportsConversionOfCdmaCallerIdMmiCodesWhileRoaming() {
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle b = configManager.getConfig();
+        if (b != null) {
+            return b.getBoolean(
+                    CarrierConfigManager
+                            .KEY_CONVERT_CDMA_CALLER_ID_MMI_CODES_WHILE_ROAMING_ON_3GPP_BOOL,
+                    false);
+        } else {
+            // Default value set in CarrierConfigManager
+            return false;
+        }
+    }
+
+    /**
+     * When overridden the derived class needs to call
+     * super.handleMessage(msg) so this method has a
+     * a chance to process the message.
+     *
+     * @param msg
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        // messages to be handled whether or not the phone is being destroyed
+        // should only include messages which are being re-directed and do not use
+        // resources of the phone being destroyed
+        switch (msg.what) {
+            // handle the select network completion callbacks.
+            case EVENT_SET_NETWORK_MANUAL_COMPLETE:
+            case EVENT_SET_NETWORK_AUTOMATIC_COMPLETE:
+                handleSetSelectNetwork((AsyncResult) msg.obj);
+                return;
+        }
+
+        switch(msg.what) {
+            case EVENT_CALL_RING:
+                Rlog.d(LOG_TAG, "Event EVENT_CALL_RING Received state=" + getState());
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    PhoneConstants.State state = getState();
+                    if ((!mDoesRilSendMultipleCallRing)
+                            && ((state == PhoneConstants.State.RINGING) ||
+                                    (state == PhoneConstants.State.IDLE))) {
+                        mCallRingContinueToken += 1;
+                        sendIncomingCallRingNotification(mCallRingContinueToken);
+                    } else {
+                        notifyIncomingRing();
+                    }
+                }
+                break;
+
+            case EVENT_CALL_RING_CONTINUE:
+                Rlog.d(LOG_TAG, "Event EVENT_CALL_RING_CONTINUE Received state=" + getState());
+                if (getState() == PhoneConstants.State.RINGING) {
+                    sendIncomingCallRingNotification(msg.arg1);
+                }
+                break;
+
+            case EVENT_ICC_CHANGED:
+                onUpdateIccAvailability();
+                break;
+
+            case EVENT_INITIATE_SILENT_REDIAL:
+                Rlog.d(LOG_TAG, "Event EVENT_INITIATE_SILENT_REDIAL Received");
+                ar = (AsyncResult) msg.obj;
+                if ((ar.exception == null) && (ar.result != null)) {
+                    String dialString = (String) ar.result;
+                    if (TextUtils.isEmpty(dialString)) return;
+                    try {
+                        dialInternal(dialString, null, VideoProfile.STATE_AUDIO_ONLY, null);
+                    } catch (CallStateException e) {
+                        Rlog.e(LOG_TAG, "silent redial failed: " + e);
+                    }
+                }
+                break;
+
+            case EVENT_SRVCC_STATE_CHANGED:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    handleSrvccStateChanged((int[]) ar.result);
+                } else {
+                    Rlog.e(LOG_TAG, "Srvcc exception: " + ar.exception);
+                }
+                break;
+
+            case EVENT_UNSOL_OEM_HOOK_RAW:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    byte[] data = (byte[])ar.result;
+                    mNotifier.notifyOemHookRawEventForSubscriber(getSubId(), data);
+                } else {
+                    Rlog.e(LOG_TAG, "OEM hook raw exception: " + ar.exception);
+                }
+                break;
+
+            case EVENT_CONFIG_LCE:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception != null) {
+                    Rlog.d(LOG_TAG, "config LCE service failed: " + ar.exception);
+                } else {
+                    final ArrayList<Integer> statusInfo = (ArrayList<Integer>)ar.result;
+                    mLceStatus = statusInfo.get(0);
+                }
+                break;
+
+            case EVENT_CHECK_FOR_NETWORK_AUTOMATIC: {
+                onCheckForNetworkSelectionModeAutomatic(msg);
+                break;
+            }
+            default:
+                throw new RuntimeException("unexpected event not handled");
+        }
+    }
+
+    public ArrayList<Connection> getHandoverConnection() {
+        return null;
+    }
+
+    public void notifySrvccState(Call.SrvccState state) {
+    }
+
+    public void registerForSilentRedial(Handler h, int what, Object obj) {
+    }
+
+    public void unregisterForSilentRedial(Handler h) {
+    }
+
+    private void handleSrvccStateChanged(int[] ret) {
+        Rlog.d(LOG_TAG, "handleSrvccStateChanged");
+
+        ArrayList<Connection> conn = null;
+        Phone imsPhone = mImsPhone;
+        Call.SrvccState srvccState = Call.SrvccState.NONE;
+        if (ret != null && ret.length != 0) {
+            int state = ret[0];
+            switch(state) {
+                case VoLteServiceState.HANDOVER_STARTED:
+                    srvccState = Call.SrvccState.STARTED;
+                    if (imsPhone != null) {
+                        conn = imsPhone.getHandoverConnection();
+                        migrateFrom(imsPhone);
+                    } else {
+                        Rlog.d(LOG_TAG, "HANDOVER_STARTED: mImsPhone null");
+                    }
+                    break;
+                case VoLteServiceState.HANDOVER_COMPLETED:
+                    srvccState = Call.SrvccState.COMPLETED;
+                    if (imsPhone != null) {
+                        imsPhone.notifySrvccState(srvccState);
+                    } else {
+                        Rlog.d(LOG_TAG, "HANDOVER_COMPLETED: mImsPhone null");
+                    }
+                    break;
+                case VoLteServiceState.HANDOVER_FAILED:
+                case VoLteServiceState.HANDOVER_CANCELED:
+                    srvccState = Call.SrvccState.FAILED;
+                    break;
+
+                default:
+                    //ignore invalid state
+                    return;
+            }
+
+            getCallTracker().notifySrvccState(srvccState, conn);
+
+            VoLteServiceState lteState = new VoLteServiceState(state);
+            notifyVoLteServiceStateChanged(lteState);
+        }
+    }
+
+    /**
+     * Gets the context for the phone, as set at initialization time.
+     */
+    public Context getContext() {
+        return mContext;
+    }
+
+    // Will be called when icc changed
+    protected abstract void onUpdateIccAvailability();
+
+    /**
+     * Disables the DNS check (i.e., allows "0.0.0.0").
+     * Useful for lab testing environment.
+     * @param b true disables the check, false enables.
+     */
+    public void disableDnsCheck(boolean b) {
+        mDnsCheckDisabled = b;
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putBoolean(DNS_SERVER_CHECK_DISABLED_KEY, b);
+        editor.apply();
+    }
+
+    /**
+     * Returns true if the DNS check is currently disabled.
+     */
+    public boolean isDnsCheckDisabled() {
+        return mDnsCheckDisabled;
+    }
+
+    /**
+     * Register for getting notifications for change in the Call State {@link Call.State}
+     * This is called PreciseCallState because the call state is more precise than the
+     * {@link PhoneConstants.State} which can be obtained using the {@link PhoneStateListener}
+     *
+     * Resulting events will have an AsyncResult in <code>Message.obj</code>.
+     * AsyncResult.userData will be set to the obj argument here.
+     * The <em>h</em> parameter is held only by a weak reference.
+     */
+    public void registerForPreciseCallStateChanged(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mPreciseCallStateRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for voice call state change notifications.
+     * Extraneous calls are tolerated silently.
+     */
+    public void unregisterForPreciseCallStateChanged(Handler h) {
+        mPreciseCallStateRegistrants.remove(h);
+    }
+
+    /**
+     * Subclasses of Phone probably want to replace this with a
+     * version scoped to their packages
+     */
+    protected void notifyPreciseCallStateChangedP() {
+        AsyncResult ar = new AsyncResult(null, this, null);
+        mPreciseCallStateRegistrants.notifyRegistrants(ar);
+
+        mNotifier.notifyPreciseCallState(this);
+    }
+
+    /**
+     * Notifies when a Handover happens due to SRVCC or Silent Redial
+     */
+    public void registerForHandoverStateChanged(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+        mHandoverRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for handover state notifications
+     */
+    public void unregisterForHandoverStateChanged(Handler h) {
+        mHandoverRegistrants.remove(h);
+    }
+
+    /**
+     * Subclasses of Phone probably want to replace this with a
+     * version scoped to their packages
+     */
+    public void notifyHandoverStateChanged(Connection cn) {
+       AsyncResult ar = new AsyncResult(null, cn, null);
+       mHandoverRegistrants.notifyRegistrants(ar);
+    }
+
+    protected void setIsInEmergencyCall() {
+    }
+
+    protected void migrateFrom(Phone from) {
+        migrate(mHandoverRegistrants, from.mHandoverRegistrants);
+        migrate(mPreciseCallStateRegistrants, from.mPreciseCallStateRegistrants);
+        migrate(mNewRingingConnectionRegistrants, from.mNewRingingConnectionRegistrants);
+        migrate(mIncomingRingRegistrants, from.mIncomingRingRegistrants);
+        migrate(mDisconnectRegistrants, from.mDisconnectRegistrants);
+        migrate(mServiceStateRegistrants, from.mServiceStateRegistrants);
+        migrate(mMmiCompleteRegistrants, from.mMmiCompleteRegistrants);
+        migrate(mMmiRegistrants, from.mMmiRegistrants);
+        migrate(mUnknownConnectionRegistrants, from.mUnknownConnectionRegistrants);
+        migrate(mSuppServiceFailedRegistrants, from.mSuppServiceFailedRegistrants);
+        if (from.isInEmergencyCall()) {
+            setIsInEmergencyCall();
+        }
+    }
+
+    protected void migrate(RegistrantList to, RegistrantList from) {
+        from.removeCleared();
+        for (int i = 0, n = from.size(); i < n; i++) {
+            Registrant r = (Registrant) from.get(i);
+            Message msg = r.messageForRegistrant();
+            // Since CallManager has already registered with both CS and IMS phones,
+            // the migrate should happen only for those registrants which are not
+            // registered with CallManager.Hence the below check is needed to add
+            // only those registrants to the registrant list which are not
+            // coming from the CallManager.
+            if (msg != null) {
+                if (msg.obj == CallManager.getInstance().getRegistrantIdentifier()) {
+                    continue;
+                } else {
+                    to.add((Registrant) from.get(i));
+                }
+            } else {
+                Rlog.d(LOG_TAG, "msg is null");
+            }
+        }
+    }
+
+    /**
+     * Notifies when a previously untracked non-ringing/waiting connection has appeared.
+     * This is likely due to some other entity (eg, SIM card application) initiating a call.
+     */
+    public void registerForUnknownConnection(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mUnknownConnectionRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for unknown connection notifications.
+     */
+    public void unregisterForUnknownConnection(Handler h) {
+        mUnknownConnectionRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when a new ringing or waiting connection has appeared.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = a Connection. <p>
+     *  Please check Connection.isRinging() to make sure the Connection
+     *  has not dropped since this message was posted.
+     *  If Connection.isRinging() is true, then
+     *   Connection.getCall() == Phone.getRingingCall()
+     */
+    public void registerForNewRingingConnection(
+            Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mNewRingingConnectionRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for new ringing connection notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForNewRingingConnection(Handler h) {
+        mNewRingingConnectionRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when phone's video capabilities changes <p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = true if phone supports video calling <p>
+     */
+    public void registerForVideoCapabilityChanged(
+            Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mVideoCapabilityChangedRegistrants.addUnique(h, what, obj);
+
+        // Notify any registrants of the cached video capability as soon as they register.
+        notifyForVideoCapabilityChanged(mIsVideoCapable);
+    }
+
+    /**
+     * Unregisters for video capability changed notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForVideoCapabilityChanged(Handler h) {
+        mVideoCapabilityChangedRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when a sInCall VoicePrivacy is enabled
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj){
+        mCi.registerForInCallVoicePrivacyOn(h, what, obj);
+    }
+
+    /**
+     * Unegister for notifications when a sInCall VoicePrivacy is enabled
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForInCallVoicePrivacyOn(Handler h){
+        mCi.unregisterForInCallVoicePrivacyOn(h);
+    }
+
+    /**
+     * Register for notifications when a sInCall VoicePrivacy is disabled
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj){
+        mCi.registerForInCallVoicePrivacyOff(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when a sInCall VoicePrivacy is disabled
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForInCallVoicePrivacyOff(Handler h){
+        mCi.unregisterForInCallVoicePrivacyOff(h);
+    }
+
+    /**
+     * Notifies when an incoming call rings.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = a Connection. <p>
+     */
+    public void registerForIncomingRing(
+            Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mIncomingRingRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for ring notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForIncomingRing(Handler h) {
+        mIncomingRingRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when a voice connection has disconnected, either due to local
+     * or remote hangup or error.
+     *
+     *  Messages received from this will have the following members:<p>
+     *  <ul><li>Message.obj will be an AsyncResult</li>
+     *  <li>AsyncResult.userObj = obj</li>
+     *  <li>AsyncResult.result = a Connection object that is
+     *  no longer connected.</li></ul>
+     */
+    public void registerForDisconnect(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mDisconnectRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for voice disconnection notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForDisconnect(Handler h) {
+        mDisconnectRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications when a supplementary service attempt fails.
+     * Message.obj will contain an AsyncResult.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForSuppServiceFailed(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mSuppServiceFailedRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when a supplementary service attempt fails.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSuppServiceFailed(Handler h) {
+        mSuppServiceFailedRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications of initiation of a new MMI code request.
+     * MMI codes for GSM are discussed in 3GPP TS 22.030.<p>
+     *
+     * Example: If Phone.dial is called with "*#31#", then the app will
+     * be notified here.<p>
+     *
+     * The returned <code>Message.obj</code> will contain an AsyncResult.
+     *
+     * <code>obj.result</code> will be an "MmiCode" object.
+     */
+    public void registerForMmiInitiate(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mMmiRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for new MMI initiate notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForMmiInitiate(Handler h) {
+        mMmiRegistrants.remove(h);
+    }
+
+    /**
+     * Register for notifications that an MMI request has completed
+     * its network activity and is in its final state. This may mean a state
+     * of COMPLETE, FAILED, or CANCELLED.
+     *
+     * <code>Message.obj</code> will contain an AsyncResult.
+     * <code>obj.result</code> will be an "MmiCode" object
+     */
+    public void registerForMmiComplete(Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mMmiCompleteRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for MMI complete notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForMmiComplete(Handler h) {
+        checkCorrectThread(h);
+
+        mMmiCompleteRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for Sim records loaded
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForSimRecordsLoaded(Handler h, int what, Object obj) {
+    }
+
+    /**
+     * Unregister for notifications for Sim records loaded
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSimRecordsLoaded(Handler h) {
+    }
+
+    /**
+     * Register for TTY mode change notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be an Integer containing new mode.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForTtyModeReceived(Handler h, int what, Object obj) {
+    }
+
+    /**
+     * Unregisters for TTY mode change notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForTtyModeReceived(Handler h) {
+    }
+
+    /**
+     * Switches network selection mode to "automatic", re-scanning and
+     * re-selecting a network if appropriate.
+     *
+     * @param response The message to dispatch when the network selection
+     * is complete.
+     *
+     * @see #selectNetworkManually(OperatorInfo, boolean, android.os.Message)
+     */
+    public void setNetworkSelectionModeAutomatic(Message response) {
+        Rlog.d(LOG_TAG, "setNetworkSelectionModeAutomatic, querying current mode");
+        // we don't want to do this unecesarily - it acutally causes
+        // the radio to repeate network selection and is costly
+        // first check if we're already in automatic mode
+        Message msg = obtainMessage(EVENT_CHECK_FOR_NETWORK_AUTOMATIC);
+        msg.obj = response;
+        mCi.getNetworkSelectionMode(msg);
+    }
+
+    private void onCheckForNetworkSelectionModeAutomatic(Message fromRil) {
+        AsyncResult ar = (AsyncResult)fromRil.obj;
+        Message response = (Message)ar.userObj;
+        boolean doAutomatic = true;
+        if (ar.exception == null && ar.result != null) {
+            try {
+                int[] modes = (int[])ar.result;
+                if (modes[0] == 0) {
+                    // already confirmed to be in automatic mode - don't resend
+                    doAutomatic = false;
+                }
+            } catch (Exception e) {
+                // send the setting on error
+            }
+        }
+
+        // wrap the response message in our own message along with
+        // an empty string (to indicate automatic selection) for the
+        // operator's id.
+        NetworkSelectMessage nsm = new NetworkSelectMessage();
+        nsm.message = response;
+        nsm.operatorNumeric = "";
+        nsm.operatorAlphaLong = "";
+        nsm.operatorAlphaShort = "";
+
+        if (doAutomatic) {
+            Message msg = obtainMessage(EVENT_SET_NETWORK_AUTOMATIC_COMPLETE, nsm);
+            mCi.setNetworkSelectionModeAutomatic(msg);
+        } else {
+            Rlog.d(LOG_TAG, "setNetworkSelectionModeAutomatic - already auto, ignoring");
+            ar.userObj = nsm;
+            handleSetSelectNetwork(ar);
+        }
+
+        updateSavedNetworkOperator(nsm);
+    }
+
+    /**
+     * Query the radio for the current network selection mode.
+     *
+     * Return values:
+     *     0 - automatic.
+     *     1 - manual.
+     */
+    public void getNetworkSelectionMode(Message message) {
+        mCi.getNetworkSelectionMode(message);
+    }
+
+    public List<ClientRequestStats> getClientRequestStats() {
+        return mCi.getClientRequestStats();
+    }
+
+    /**
+     * Manually selects a network. <code>response</code> is
+     * dispatched when this is complete.  <code>response.obj</code> will be
+     * an AsyncResult, and <code>response.obj.exception</code> will be non-null
+     * on failure.
+     *
+     * @see #setNetworkSelectionModeAutomatic(Message)
+     */
+    public void selectNetworkManually(OperatorInfo network, boolean persistSelection,
+            Message response) {
+        // wrap the response message in our own message along with
+        // the operator's id.
+        NetworkSelectMessage nsm = new NetworkSelectMessage();
+        nsm.message = response;
+        nsm.operatorNumeric = network.getOperatorNumeric();
+        nsm.operatorAlphaLong = network.getOperatorAlphaLong();
+        nsm.operatorAlphaShort = network.getOperatorAlphaShort();
+
+        Message msg = obtainMessage(EVENT_SET_NETWORK_MANUAL_COMPLETE, nsm);
+        mCi.setNetworkSelectionModeManual(network.getOperatorNumeric(), msg);
+
+        if (persistSelection) {
+            updateSavedNetworkOperator(nsm);
+        } else {
+            clearSavedNetworkSelection();
+        }
+    }
+
+    /**
+     * Registration point for emergency call/callback mode start. Message.obj is AsyncResult and
+     * Message.obj.result will be Integer indicating start of call by value 1 or end of call by
+     * value 0
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj.userObj
+     */
+    public void registerForEmergencyCallToggle(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mEmergencyCallToggledRegistrants.add(r);
+    }
+
+    public void unregisterForEmergencyCallToggle(Handler h) {
+        mEmergencyCallToggledRegistrants.remove(h);
+    }
+
+    private void updateSavedNetworkOperator(NetworkSelectMessage nsm) {
+        int subId = getSubId();
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            // open the shared preferences editor, and write the value.
+            // nsm.operatorNumeric is "" if we're in automatic.selection.
+            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+            SharedPreferences.Editor editor = sp.edit();
+            editor.putString(NETWORK_SELECTION_KEY + subId, nsm.operatorNumeric);
+            editor.putString(NETWORK_SELECTION_NAME_KEY + subId, nsm.operatorAlphaLong);
+            editor.putString(NETWORK_SELECTION_SHORT_KEY + subId, nsm.operatorAlphaShort);
+
+            // commit and log the result.
+            if (!editor.commit()) {
+                Rlog.e(LOG_TAG, "failed to commit network selection preference");
+            }
+        } else {
+            Rlog.e(LOG_TAG, "Cannot update network selection preference due to invalid subId " +
+                    subId);
+        }
+    }
+
+    /**
+     * Used to track the settings upon completion of the network change.
+     */
+    private void handleSetSelectNetwork(AsyncResult ar) {
+        // look for our wrapper within the asyncresult, skip the rest if it
+        // is null.
+        if (!(ar.userObj instanceof NetworkSelectMessage)) {
+            Rlog.e(LOG_TAG, "unexpected result from user object.");
+            return;
+        }
+
+        NetworkSelectMessage nsm = (NetworkSelectMessage) ar.userObj;
+
+        // found the object, now we send off the message we had originally
+        // attached to the request.
+        if (nsm.message != null) {
+            AsyncResult.forMessage(nsm.message, ar.result, ar.exception);
+            nsm.message.sendToTarget();
+        }
+    }
+
+    /**
+     * Method to retrieve the saved operator from the Shared Preferences
+     */
+    private OperatorInfo getSavedNetworkSelection() {
+        // open the shared preferences and search with our key.
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        String numeric = sp.getString(NETWORK_SELECTION_KEY + getSubId(), "");
+        String name = sp.getString(NETWORK_SELECTION_NAME_KEY + getSubId(), "");
+        String shrt = sp.getString(NETWORK_SELECTION_SHORT_KEY + getSubId(), "");
+        return new OperatorInfo(name, shrt, numeric);
+    }
+
+    /**
+     * Clears the saved network selection.
+     */
+    private void clearSavedNetworkSelection() {
+        // open the shared preferences and search with our key.
+        PreferenceManager.getDefaultSharedPreferences(getContext()).edit().
+                remove(NETWORK_SELECTION_KEY + getSubId()).
+                remove(NETWORK_SELECTION_NAME_KEY + getSubId()).
+                remove(NETWORK_SELECTION_SHORT_KEY + getSubId()).commit();
+    }
+
+    /**
+     * Method to restore the previously saved operator id, or reset to
+     * automatic selection, all depending upon the value in the shared
+     * preferences.
+     */
+    private void restoreSavedNetworkSelection(Message response) {
+        // retrieve the operator
+        OperatorInfo networkSelection = getSavedNetworkSelection();
+
+        // set to auto if the id is empty, otherwise select the network.
+        if (networkSelection == null || TextUtils.isEmpty(networkSelection.getOperatorNumeric())) {
+            setNetworkSelectionModeAutomatic(response);
+        } else {
+            selectNetworkManually(networkSelection, true, response);
+        }
+    }
+
+    /**
+     * Saves CLIR setting so that we can re-apply it as necessary
+     * (in case the RIL resets it across reboots).
+     */
+    public void saveClirSetting(int commandInterfaceCLIRMode) {
+        // Open the shared preferences editor, and write the value.
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putInt(CLIR_KEY + getPhoneId(), commandInterfaceCLIRMode);
+        Rlog.i(LOG_TAG, "saveClirSetting: " + CLIR_KEY + getPhoneId() + "=" +
+                commandInterfaceCLIRMode);
+
+        // Commit and log the result.
+        if (!editor.commit()) {
+            Rlog.e(LOG_TAG, "Failed to commit CLIR preference");
+        }
+    }
+
+    /**
+     * For unit tests; don't send notifications to "Phone"
+     * mailbox registrants if true.
+     */
+    private void setUnitTestMode(boolean f) {
+        mUnitTestMode = f;
+    }
+
+    /**
+     * @return true If unit test mode is enabled
+     */
+    public boolean getUnitTestMode() {
+        return mUnitTestMode;
+    }
+
+    /**
+     * To be invoked when a voice call Connection disconnects.
+     *
+     * Subclasses of Phone probably want to replace this with a
+     * version scoped to their packages
+     */
+    protected void notifyDisconnectP(Connection cn) {
+        AsyncResult ar = new AsyncResult(null, cn, null);
+        mDisconnectRegistrants.notifyRegistrants(ar);
+    }
+
+    /**
+     * Register for ServiceState changed.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a ServiceState instance
+     */
+    public void registerForServiceStateChanged(
+            Handler h, int what, Object obj) {
+        checkCorrectThread(h);
+
+        mServiceStateRegistrants.add(h, what, obj);
+    }
+
+    /**
+     * Unregisters for ServiceStateChange notification.
+     * Extraneous calls are tolerated silently
+     */
+    public void unregisterForServiceStateChanged(Handler h) {
+        mServiceStateRegistrants.remove(h);
+    }
+
+    /**
+     * Notifies when out-band ringback tone is needed.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = boolean, true to start play ringback tone
+     *                       and false to stop. <p>
+     */
+    public void registerForRingbackTone(Handler h, int what, Object obj) {
+        mCi.registerForRingbackTone(h, what, obj);
+    }
+
+    /**
+     * Unregisters for ringback tone notification.
+     */
+    public void unregisterForRingbackTone(Handler h) {
+        mCi.unregisterForRingbackTone(h);
+    }
+
+    /**
+     * Notifies when out-band on-hold tone is needed.<p>
+     *
+     *  Messages received from this:
+     *  Message.obj will be an AsyncResult
+     *  AsyncResult.userObj = obj
+     *  AsyncResult.result = boolean, true to start play on-hold tone
+     *                       and false to stop. <p>
+     */
+    public void registerForOnHoldTone(Handler h, int what, Object obj) {
+    }
+
+    /**
+     * Unregisters for on-hold tone notification.
+     */
+    public void unregisterForOnHoldTone(Handler h) {
+    }
+
+    /**
+     * Registers the handler to reset the uplink mute state to get
+     * uplink audio.
+     */
+    public void registerForResendIncallMute(Handler h, int what, Object obj) {
+        mCi.registerForResendIncallMute(h, what, obj);
+    }
+
+    /**
+     * Unregisters for resend incall mute notifications.
+     */
+    public void unregisterForResendIncallMute(Handler h) {
+        mCi.unregisterForResendIncallMute(h);
+    }
+
+    /**
+     * Enables or disables echo suppression.
+     */
+    public void setEchoSuppressionEnabled() {
+        // no need for regular phone
+    }
+
+    /**
+     * Subclasses of Phone probably want to replace this with a
+     * version scoped to their packages
+     */
+    protected void notifyServiceStateChangedP(ServiceState ss) {
+        AsyncResult ar = new AsyncResult(null, ss, null);
+        mServiceStateRegistrants.notifyRegistrants(ar);
+
+        mNotifier.notifyServiceState(this);
+    }
+
+    /**
+     * If this is a simulated phone interface, returns a SimulatedRadioControl.
+     * @return SimulatedRadioControl if this is a simulated interface;
+     * otherwise, null.
+     */
+    public SimulatedRadioControl getSimulatedRadioControl() {
+        return mSimulatedRadioControl;
+    }
+
+    /**
+     * Verifies the current thread is the same as the thread originally
+     * used in the initialization of this instance. Throws RuntimeException
+     * if not.
+     *
+     * @exception RuntimeException if the current thread is not
+     * the thread that originally obtained this Phone instance.
+     */
+    private void checkCorrectThread(Handler h) {
+        if (h.getLooper() != mLooper) {
+            throw new RuntimeException(
+                    "com.android.internal.telephony.Phone must be used from within one thread");
+        }
+    }
+
+    /**
+     * Set the properties by matching the carrier string in
+     * a string-array resource
+     */
+    private static Locale getLocaleFromCarrierProperties(Context ctx) {
+        String carrier = SystemProperties.get("ro.carrier");
+
+        if (null == carrier || 0 == carrier.length() || "unknown".equals(carrier)) {
+            return null;
+        }
+
+        CharSequence[] carrierLocales = ctx.getResources().getTextArray(R.array.carrier_properties);
+
+        for (int i = 0; i < carrierLocales.length; i+=3) {
+            String c = carrierLocales[i].toString();
+            if (carrier.equals(c)) {
+                return Locale.forLanguageTag(carrierLocales[i + 1].toString().replace('_', '-'));
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Get current coarse-grained voice call state.
+     * Use {@link #registerForPreciseCallStateChanged(Handler, int, Object)
+     * registerForPreciseCallStateChanged()} for change notification. <p>
+     * If the phone has an active call and call waiting occurs,
+     * then the phone state is RINGING not OFFHOOK
+     * <strong>Note:</strong>
+     * This registration point provides notification of finer-grained
+     * changes.<p>
+     */
+    public abstract PhoneConstants.State getState();
+
+    /**
+     * Retrieves the IccFileHandler of the Phone instance
+     */
+    public IccFileHandler getIccFileHandler(){
+        UiccCardApplication uiccApplication = mUiccApplication.get();
+        IccFileHandler fh;
+
+        if (uiccApplication == null) {
+            Rlog.d(LOG_TAG, "getIccFileHandler: uiccApplication == null, return null");
+            fh = null;
+        } else {
+            fh = uiccApplication.getIccFileHandler();
+        }
+
+        Rlog.d(LOG_TAG, "getIccFileHandler: fh=" + fh);
+        return fh;
+    }
+
+    /*
+     * Retrieves the Handler of the Phone instance
+     */
+    public Handler getHandler() {
+        return this;
+    }
+
+    /**
+     * Update the phone object if the voice radio technology has changed
+     *
+     * @param voiceRadioTech The new voice radio technology
+     */
+    public void updatePhoneObject(int voiceRadioTech) {
+    }
+
+    /**
+    * Retrieves the ServiceStateTracker of the phone instance.
+    */
+    public ServiceStateTracker getServiceStateTracker() {
+        return null;
+    }
+
+    /**
+    * Get call tracker
+    */
+    public CallTracker getCallTracker() {
+        return null;
+    }
+
+    /**
+     * Update voice activation state
+     */
+    public void setVoiceActivationState(int state) {
+        mSimActivationTracker.setVoiceActivationState(state);
+    }
+    /**
+     * Update data activation state
+     */
+    public void setDataActivationState(int state) {
+        mSimActivationTracker.setDataActivationState(state);
+    }
+
+    /**
+     * Returns voice activation state
+     */
+    public int getVoiceActivationState() {
+        return mSimActivationTracker.getVoiceActivationState();
+    }
+    /**
+     * Returns data activation state
+     */
+    public int getDataActivationState() {
+        return mSimActivationTracker.getDataActivationState();
+    }
+
+    /**
+     * Update voice mail count related fields and notify listeners
+     */
+    public void updateVoiceMail() {
+        Rlog.e(LOG_TAG, "updateVoiceMail() should be overridden");
+    }
+
+    public AppType getCurrentUiccAppType() {
+        UiccCardApplication currentApp = mUiccApplication.get();
+        if (currentApp != null) {
+            return currentApp.getType();
+        }
+        return AppType.APPTYPE_UNKNOWN;
+    }
+
+    /**
+     * Returns the ICC card interface for this phone, or null
+     * if not applicable to underlying technology.
+     */
+    public IccCard getIccCard() {
+        return null;
+        //throw new Exception("getIccCard Shouldn't be called from Phone");
+    }
+
+    /**
+     * Retrieves the serial number of the ICC, if applicable. Returns only the decimal digits before
+     * the first hex digit in the ICC ID.
+     */
+    public String getIccSerialNumber() {
+        IccRecords r = mIccRecords.get();
+        return (r != null) ? r.getIccId() : null;
+    }
+
+    /**
+     * Retrieves the full serial number of the ICC (including hex digits), if applicable.
+     */
+    public String getFullIccSerialNumber() {
+        IccRecords r = mIccRecords.get();
+        return (r != null) ? r.getFullIccId() : null;
+    }
+
+    /**
+     * Returns SIM record load state. Use
+     * <code>getSimCard().registerForReady()</code> for change notification.
+     *
+     * @return true if records from the SIM have been loaded and are
+     * available (if applicable). If not applicable to the underlying
+     * technology, returns true as well.
+     */
+    public boolean getIccRecordsLoaded() {
+        IccRecords r = mIccRecords.get();
+        return (r != null) ? r.getRecordsLoaded() : false;
+    }
+
+    /**
+     * @param workSource calling WorkSource
+     * @return all available cell information or null if none.
+     */
+    public List<CellInfo> getAllCellInfo(WorkSource workSource) {
+        List<CellInfo> cellInfoList = getServiceStateTracker().getAllCellInfo(workSource);
+        return privatizeCellInfoList(cellInfoList);
+    }
+
+    public CellLocation getCellLocation() {
+        return getCellLocation(null);
+    }
+
+    /**
+     * Clear CDMA base station lat/long values if location setting is disabled.
+     * @param cellInfoList the original cell info list from the RIL
+     * @return the original list with CDMA lat/long cleared if necessary
+     */
+    private List<CellInfo> privatizeCellInfoList(List<CellInfo> cellInfoList) {
+        if (cellInfoList == null) return null;
+        int mode = Settings.Secure.getInt(getContext().getContentResolver(),
+                Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+        if (mode == Settings.Secure.LOCATION_MODE_OFF) {
+            ArrayList<CellInfo> privateCellInfoList = new ArrayList<CellInfo>(cellInfoList.size());
+            // clear lat/lon values for location privacy
+            for (CellInfo c : cellInfoList) {
+                if (c instanceof CellInfoCdma) {
+                    CellInfoCdma cellInfoCdma = (CellInfoCdma) c;
+                    CellIdentityCdma cellIdentity = cellInfoCdma.getCellIdentity();
+                    CellIdentityCdma maskedCellIdentity = new CellIdentityCdma(
+                            cellIdentity.getNetworkId(),
+                            cellIdentity.getSystemId(),
+                            cellIdentity.getBasestationId(),
+                            Integer.MAX_VALUE, Integer.MAX_VALUE);
+                    CellInfoCdma privateCellInfoCdma = new CellInfoCdma(cellInfoCdma);
+                    privateCellInfoCdma.setCellIdentity(maskedCellIdentity);
+                    privateCellInfoList.add(privateCellInfoCdma);
+                } else {
+                    privateCellInfoList.add(c);
+                }
+            }
+            cellInfoList = privateCellInfoList;
+        }
+        return cellInfoList;
+    }
+
+    /**
+     * Sets the minimum time in milli-seconds between {@link PhoneStateListener#onCellInfoChanged
+     * PhoneStateListener.onCellInfoChanged} will be invoked.
+     *
+     * The default, 0, means invoke onCellInfoChanged when any of the reported
+     * information changes. Setting the value to INT_MAX(0x7fffffff) means never issue
+     * A onCellInfoChanged.
+     *
+     * @param rateInMillis the rate
+     * @param workSource calling WorkSource
+     */
+    public void setCellInfoListRate(int rateInMillis, WorkSource workSource) {
+        mCi.setCellInfoListRate(rateInMillis, null, workSource);
+    }
+
+    /**
+     * Get voice message waiting indicator status. No change notification
+     * available on this interface. Use PhoneStateNotifier or similar instead.
+     *
+     * @return true if there is a voice message waiting
+     */
+    public boolean getMessageWaitingIndicator() {
+        return mVmCount != 0;
+    }
+
+    private int getCallForwardingIndicatorFromSharedPref() {
+        int status = IccRecords.CALL_FORWARDING_STATUS_DISABLED;
+        int subId = getSubId();
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+            status = sp.getInt(CF_STATUS + subId, IccRecords.CALL_FORWARDING_STATUS_UNKNOWN);
+            Rlog.d(LOG_TAG, "getCallForwardingIndicatorFromSharedPref: for subId " + subId + "= " +
+                    status);
+            // Check for old preference if status is UNKNOWN for current subId. This part of the
+            // code is needed only when upgrading from M to N.
+            if (status == IccRecords.CALL_FORWARDING_STATUS_UNKNOWN) {
+                String subscriberId = sp.getString(CF_ID, null);
+                if (subscriberId != null) {
+                    String currentSubscriberId = getSubscriberId();
+
+                    if (subscriberId.equals(currentSubscriberId)) {
+                        // get call forwarding status from preferences
+                        status = sp.getInt(CF_STATUS, IccRecords.CALL_FORWARDING_STATUS_DISABLED);
+                        setCallForwardingIndicatorInSharedPref(
+                                status == IccRecords.CALL_FORWARDING_STATUS_ENABLED ? true : false);
+                        Rlog.d(LOG_TAG, "getCallForwardingIndicatorFromSharedPref: " + status);
+                    } else {
+                        Rlog.d(LOG_TAG, "getCallForwardingIndicatorFromSharedPref: returning " +
+                                "DISABLED as status for matching subscriberId not found");
+                    }
+
+                    // get rid of old preferences.
+                    SharedPreferences.Editor editor = sp.edit();
+                    editor.remove(CF_ID);
+                    editor.remove(CF_STATUS);
+                    editor.apply();
+                }
+            }
+        } else {
+            Rlog.e(LOG_TAG, "getCallForwardingIndicatorFromSharedPref: invalid subId " + subId);
+        }
+        return status;
+    }
+
+    private void setCallForwardingIndicatorInSharedPref(boolean enable) {
+        int status = enable ? IccRecords.CALL_FORWARDING_STATUS_ENABLED :
+                IccRecords.CALL_FORWARDING_STATUS_DISABLED;
+        int subId = getSubId();
+        Rlog.d(LOG_TAG, "setCallForwardingIndicatorInSharedPref: Storing status = " + status +
+                " in pref " + CF_STATUS + subId);
+
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putInt(CF_STATUS + subId, status);
+        editor.apply();
+    }
+
+    public void setVoiceCallForwardingFlag(int line, boolean enable, String number) {
+        setCallForwardingIndicatorInSharedPref(enable);
+        IccRecords r = mIccRecords.get();
+        if (r != null) {
+            r.setVoiceCallForwardingFlag(line, enable, number);
+        }
+    }
+
+    protected void setVoiceCallForwardingFlag(IccRecords r, int line, boolean enable,
+                                              String number) {
+        setCallForwardingIndicatorInSharedPref(enable);
+        r.setVoiceCallForwardingFlag(line, enable, number);
+    }
+
+    /**
+     * Get voice call forwarding indicator status. No change notification
+     * available on this interface. Use PhoneStateNotifier or similar instead.
+     *
+     * @return true if there is a voice call forwarding
+     */
+    public boolean getCallForwardingIndicator() {
+        if (getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+            Rlog.e(LOG_TAG, "getCallForwardingIndicator: not possible in CDMA");
+            return false;
+        }
+        IccRecords r = mIccRecords.get();
+        int callForwardingIndicator = IccRecords.CALL_FORWARDING_STATUS_UNKNOWN;
+        if (r != null) {
+            callForwardingIndicator = r.getVoiceCallForwardingFlag();
+        }
+        if (callForwardingIndicator == IccRecords.CALL_FORWARDING_STATUS_UNKNOWN) {
+            callForwardingIndicator = getCallForwardingIndicatorFromSharedPref();
+        }
+        return (callForwardingIndicator == IccRecords.CALL_FORWARDING_STATUS_ENABLED);
+    }
+
+    public CarrierSignalAgent getCarrierSignalAgent() {
+        return mCarrierSignalAgent;
+    }
+
+    public CarrierActionAgent getCarrierActionAgent() {
+        return mCarrierActionAgent;
+    }
+
+    /**
+     *  Query the CDMA roaming preference setting
+     *
+     * @param response is callback message to report one of  CDMA_RM_*
+     */
+    public void queryCdmaRoamingPreference(Message response) {
+        mCi.queryCdmaRoamingPreference(response);
+    }
+
+    /**
+     * Get current signal strength. No change notification available on this
+     * interface. Use <code>PhoneStateNotifier</code> or an equivalent.
+     * An ASU is 0-31 or -1 if unknown (for GSM, dBm = -113 - 2 * asu).
+     * The following special values are defined:</p>
+     * <ul><li>0 means "-113 dBm or less".</li>
+     * <li>31 means "-51 dBm or greater".</li></ul>
+     *
+     * @return Current signal strength as SignalStrength
+     */
+    public SignalStrength getSignalStrength() {
+        ServiceStateTracker sst = getServiceStateTracker();
+        if (sst == null) {
+            return new SignalStrength();
+        } else {
+            return sst.getSignalStrength();
+        }
+    }
+
+    /**
+     * @return true, if the device is in a state where both voice and data
+     * are supported simultaneously. This can change based on location or network condition.
+     */
+    public boolean isConcurrentVoiceAndDataAllowed() {
+        ServiceStateTracker sst = getServiceStateTracker();
+        return sst == null ? false : sst.isConcurrentVoiceAndDataAllowed();
+    }
+
+    /**
+     *  Requests to set the CDMA roaming preference
+     * @param cdmaRoamingType one of  CDMA_RM_*
+     * @param response is callback message
+     */
+    public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) {
+        mCi.setCdmaRoamingPreference(cdmaRoamingType, response);
+    }
+
+    /**
+     *  Requests to set the CDMA subscription mode
+     * @param cdmaSubscriptionType one of  CDMA_SUBSCRIPTION_*
+     * @param response is callback message
+     */
+    public void setCdmaSubscription(int cdmaSubscriptionType, Message response) {
+        mCi.setCdmaSubscriptionSource(cdmaSubscriptionType, response);
+    }
+
+    /**
+     *  Requests to set the preferred network type for searching and registering
+     * (CS/PS domain, RAT, and operation mode)
+     * @param networkType one of  NT_*_TYPE
+     * @param response is callback message
+     */
+    public void setPreferredNetworkType(int networkType, Message response) {
+        // Only set preferred network types to that which the modem supports
+        int modemRaf = getRadioAccessFamily();
+        int rafFromType = RadioAccessFamily.getRafFromNetworkType(networkType);
+
+        if (modemRaf == RadioAccessFamily.RAF_UNKNOWN
+                || rafFromType == RadioAccessFamily.RAF_UNKNOWN) {
+            Rlog.d(LOG_TAG, "setPreferredNetworkType: Abort, unknown RAF: "
+                    + modemRaf + " " + rafFromType);
+            if (response != null) {
+                CommandException ex;
+
+                ex = new CommandException(CommandException.Error.GENERIC_FAILURE);
+                AsyncResult.forMessage(response, null, ex);
+                response.sendToTarget();
+            }
+            return;
+        }
+
+        int filteredRaf = (rafFromType & modemRaf);
+        int filteredType = RadioAccessFamily.getNetworkTypeFromRaf(filteredRaf);
+
+        Rlog.d(LOG_TAG, "setPreferredNetworkType: networkType = " + networkType
+                + " modemRaf = " + modemRaf
+                + " rafFromType = " + rafFromType
+                + " filteredType = " + filteredType);
+
+        mCi.setPreferredNetworkType(filteredType, response);
+    }
+
+    /**
+     *  Query the preferred network type setting
+     *
+     * @param response is callback message to report one of  NT_*_TYPE
+     */
+    public void getPreferredNetworkType(Message response) {
+        mCi.getPreferredNetworkType(response);
+    }
+
+    /**
+     * Gets the default SMSC address.
+     *
+     * @param result Callback message contains the SMSC address.
+     */
+    public void getSmscAddress(Message result) {
+        mCi.getSmscAddress(result);
+    }
+
+    /**
+     * Sets the default SMSC address.
+     *
+     * @param address new SMSC address
+     * @param result Callback message is empty on completion
+     */
+    public void setSmscAddress(String address, Message result) {
+        mCi.setSmscAddress(address, result);
+    }
+
+    /**
+     * setTTYMode
+     * sets a TTY mode option.
+     * @param ttyMode is a one of the following:
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+     * @param onComplete a callback message when the action is completed
+     */
+    public void setTTYMode(int ttyMode, Message onComplete) {
+        mCi.setTTYMode(ttyMode, onComplete);
+    }
+
+    /**
+     * setUiTTYMode
+     * sets a TTY mode option.
+     * @param ttyMode is a one of the following:
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+     * @param onComplete a callback message when the action is completed
+     */
+    public void setUiTTYMode(int uiTtyMode, Message onComplete) {
+        Rlog.d(LOG_TAG, "unexpected setUiTTYMode method call");
+    }
+
+    /**
+     * queryTTYMode
+     * query the status of the TTY mode
+     *
+     * @param onComplete a callback message when the action is completed.
+     */
+    public void queryTTYMode(Message onComplete) {
+        mCi.queryTTYMode(onComplete);
+    }
+
+    /**
+     * Enable or disable enhanced Voice Privacy (VP). If enhanced VP is
+     * disabled, normal VP is enabled.
+     *
+     * @param enable whether true or false to enable or disable.
+     * @param onComplete a callback message when the action is completed.
+     */
+    public void enableEnhancedVoicePrivacy(boolean enable, Message onComplete) {
+    }
+
+    /**
+     * Get the currently set Voice Privacy (VP) mode.
+     *
+     * @param onComplete a callback message when the action is completed.
+     */
+    public void getEnhancedVoicePrivacy(Message onComplete) {
+    }
+
+    /**
+     * Assign a specified band for RF configuration.
+     *
+     * @param bandMode one of BM_*_BAND
+     * @param response is callback message
+     */
+    public void setBandMode(int bandMode, Message response) {
+        mCi.setBandMode(bandMode, response);
+    }
+
+    /**
+     * Query the list of band mode supported by RF.
+     *
+     * @param response is callback message
+     *        ((AsyncResult)response.obj).result  is an int[] where int[0] is
+     *        the size of the array and the rest of each element representing
+     *        one available BM_*_BAND
+     */
+    public void queryAvailableBandMode(Message response) {
+        mCi.queryAvailableBandMode(response);
+    }
+
+    /**
+     * Invokes RIL_REQUEST_OEM_HOOK_RAW on RIL implementation.
+     *
+     * @param data The data for the request.
+     * @param response <strong>On success</strong>,
+     * (byte[])(((AsyncResult)response.obj).result)
+     * <strong>On failure</strong>,
+     * (((AsyncResult)response.obj).result) == null and
+     * (((AsyncResult)response.obj).exception) being an instance of
+     * com.android.internal.telephony.gsm.CommandException
+     *
+     * @see #invokeOemRilRequestRaw(byte[], android.os.Message)
+     * @deprecated OEM needs a vendor-extension hal and their apps should use that instead
+     */
+    @Deprecated
+    public void invokeOemRilRequestRaw(byte[] data, Message response) {
+        mCi.invokeOemRilRequestRaw(data, response);
+    }
+
+    /**
+     * Invokes RIL_REQUEST_OEM_HOOK_Strings on RIL implementation.
+     *
+     * @param strings The strings to make available as the request data.
+     * @param response <strong>On success</strong>, "response" bytes is
+     * made available as:
+     * (String[])(((AsyncResult)response.obj).result).
+     * <strong>On failure</strong>,
+     * (((AsyncResult)response.obj).result) == null and
+     * (((AsyncResult)response.obj).exception) being an instance of
+     * com.android.internal.telephony.gsm.CommandException
+     *
+     * @see #invokeOemRilRequestStrings(java.lang.String[], android.os.Message)
+     * @deprecated OEM needs a vendor-extension hal and their apps should use that instead
+     */
+    @Deprecated
+    public void invokeOemRilRequestStrings(String[] strings, Message response) {
+        mCi.invokeOemRilRequestStrings(strings, response);
+    }
+
+    /**
+     * Read one of the NV items defined in {@link RadioNVItems} / {@code ril_nv_items.h}.
+     * Used for device configuration by some CDMA operators.
+     *
+     * @param itemID the ID of the item to read
+     * @param response callback message with the String response in the obj field
+     */
+    public void nvReadItem(int itemID, Message response) {
+        mCi.nvReadItem(itemID, response);
+    }
+
+    /**
+     * Write one of the NV items defined in {@link RadioNVItems} / {@code ril_nv_items.h}.
+     * Used for device configuration by some CDMA operators.
+     *
+     * @param itemID the ID of the item to read
+     * @param itemValue the value to write, as a String
+     * @param response Callback message.
+     */
+    public void nvWriteItem(int itemID, String itemValue, Message response) {
+        mCi.nvWriteItem(itemID, itemValue, response);
+    }
+
+    /**
+     * Update the CDMA Preferred Roaming List (PRL) in the radio NV storage.
+     * Used for device configuration by some CDMA operators.
+     *
+     * @param preferredRoamingList byte array containing the new PRL
+     * @param response Callback message.
+     */
+    public void nvWriteCdmaPrl(byte[] preferredRoamingList, Message response) {
+        mCi.nvWriteCdmaPrl(preferredRoamingList, response);
+    }
+
+    /**
+     * Perform the specified type of NV config reset. The radio will be taken offline
+     * and the device must be rebooted after erasing the NV. Used for device
+     * configuration by some CDMA operators.
+     *
+     * @param resetType reset type: 1: reload NV reset, 2: erase NV reset, 3: factory NV reset
+     * @param response Callback message.
+     */
+    public void nvResetConfig(int resetType, Message response) {
+        mCi.nvResetConfig(resetType, response);
+    }
+
+    public void notifyDataActivity() {
+        mNotifier.notifyDataActivity(this);
+    }
+
+    private void notifyMessageWaitingIndicator() {
+        // Do not notify voice mail waiting if device doesn't support voice
+        if (!mIsVoiceCapable)
+            return;
+
+        // This function is added to send the notification to DefaultPhoneNotifier.
+        mNotifier.notifyMessageWaitingChanged(this);
+    }
+
+    public void notifyDataConnection(String reason, String apnType,
+            PhoneConstants.DataState state) {
+        mNotifier.notifyDataConnection(this, reason, apnType, state);
+    }
+
+    public void notifyDataConnection(String reason, String apnType) {
+        mNotifier.notifyDataConnection(this, reason, apnType, getDataConnectionState(apnType));
+    }
+
+    public void notifyDataConnection(String reason) {
+        String types[] = getActiveApnTypes();
+        for (String apnType : types) {
+            mNotifier.notifyDataConnection(this, reason, apnType, getDataConnectionState(apnType));
+        }
+    }
+
+    public void notifyOtaspChanged(int otaspMode) {
+        mNotifier.notifyOtaspChanged(this, otaspMode);
+    }
+
+    public void notifyVoiceActivationStateChanged(int state) {
+        mNotifier.notifyVoiceActivationStateChanged(this, state);
+    }
+
+    public void notifyDataActivationStateChanged(int state) {
+        mNotifier.notifyDataActivationStateChanged(this, state);
+    }
+
+    public void notifySignalStrength() {
+        mNotifier.notifySignalStrength(this);
+    }
+
+    public void notifyCellInfo(List<CellInfo> cellInfo) {
+        mNotifier.notifyCellInfo(this, privatizeCellInfoList(cellInfo));
+    }
+
+    public void notifyVoLteServiceStateChanged(VoLteServiceState lteState) {
+        mNotifier.notifyVoLteServiceStateChanged(this, lteState);
+    }
+
+    /**
+     * @return true if a mobile originating emergency call is active
+     */
+    public boolean isInEmergencyCall() {
+        return false;
+    }
+
+    // This property is used to handle phone process crashes, and is the same for CDMA and IMS
+    // phones
+    protected static boolean getInEcmMode() {
+        return SystemProperties.getBoolean(TelephonyProperties.PROPERTY_INECM_MODE, false);
+    }
+
+    /**
+     * @return {@code true} if we are in emergency call back mode. This is a period where the phone
+     * should be using as little power as possible and be ready to receive an incoming call from the
+     * emergency operator.
+     */
+    public boolean isInEcm() {
+        return mIsPhoneInEcmState;
+    }
+
+    public void setIsInEcm(boolean isInEcm) {
+        setSystemProperty(TelephonyProperties.PROPERTY_INECM_MODE, String.valueOf(isInEcm));
+        mIsPhoneInEcmState = isInEcm;
+    }
+
+    private static int getVideoState(Call call) {
+        int videoState = VideoProfile.STATE_AUDIO_ONLY;
+        Connection conn = call.getEarliestConnection();
+        if (conn != null) {
+            videoState = conn.getVideoState();
+        }
+        return videoState;
+    }
+
+    /**
+     * Determines if the specified call currently is or was at some point a video call, or if it is
+     * a conference call.
+     * @param call The call.
+     * @return {@code true} if the call is or was a video call or is a conference call,
+     *      {@code false} otherwise.
+     */
+    private boolean isVideoCallOrConference(Call call) {
+        if (call.isMultiparty()) {
+            return true;
+        }
+
+        boolean isDowngradedVideoCall = false;
+        if (call instanceof ImsPhoneCall) {
+            ImsPhoneCall imsPhoneCall = (ImsPhoneCall) call;
+            ImsCall imsCall = imsPhoneCall.getImsCall();
+            return imsCall != null && (imsCall.isVideoCall() ||
+                    imsCall.wasVideoCall());
+        }
+        return isDowngradedVideoCall;
+    }
+
+    /**
+     * @return {@code true} if an IMS video call or IMS conference is present, false otherwise.
+     */
+    public boolean isImsVideoCallOrConferencePresent() {
+        boolean isPresent = false;
+        if (mImsPhone != null) {
+            isPresent = isVideoCallOrConference(mImsPhone.getForegroundCall()) ||
+                    isVideoCallOrConference(mImsPhone.getBackgroundCall()) ||
+                    isVideoCallOrConference(mImsPhone.getRingingCall());
+        }
+        Rlog.d(LOG_TAG, "isImsVideoCallOrConferencePresent: " + isPresent);
+        return isPresent;
+    }
+
+    /**
+     * Return a numerical identifier for the phone radio interface.
+     * @return PHONE_TYPE_XXX as defined above.
+     */
+    public abstract int getPhoneType();
+
+    /**
+     * Returns unread voicemail count. This count is shown when the  voicemail
+     * notification is expanded.<p>
+     */
+    public int getVoiceMessageCount(){
+        return mVmCount;
+    }
+
+    /** sets the voice mail count of the phone and notifies listeners. */
+    public void setVoiceMessageCount(int countWaiting) {
+        mVmCount = countWaiting;
+        int subId = getSubId();
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+
+            Rlog.d(LOG_TAG, "setVoiceMessageCount: Storing Voice Mail Count = " + countWaiting +
+                    " for mVmCountKey = " + VM_COUNT + subId + " in preferences.");
+
+            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+            SharedPreferences.Editor editor = sp.edit();
+            editor.putInt(VM_COUNT + subId, countWaiting);
+            editor.apply();
+        } else {
+            Rlog.e(LOG_TAG, "setVoiceMessageCount in sharedPreference: invalid subId " + subId);
+        }
+        // notify listeners of voice mail
+        notifyMessageWaitingIndicator();
+    }
+
+    /** gets the voice mail count from preferences */
+    protected int getStoredVoiceMessageCount() {
+        int countVoiceMessages = 0;
+        int subId = getSubId();
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            int invalidCount = -2;  //-1 is not really invalid. It is used for unknown number of vm
+            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+            int countFromSP = sp.getInt(VM_COUNT + subId, invalidCount);
+            if (countFromSP != invalidCount) {
+                countVoiceMessages = countFromSP;
+                Rlog.d(LOG_TAG, "getStoredVoiceMessageCount: from preference for subId " + subId +
+                        "= " + countVoiceMessages);
+            } else {
+                // Check for old preference if count not found for current subId. This part of the
+                // code is needed only when upgrading from M to N.
+                String subscriberId = sp.getString(VM_ID, null);
+                if (subscriberId != null) {
+                    String currentSubscriberId = getSubscriberId();
+
+                    if (currentSubscriberId != null && currentSubscriberId.equals(subscriberId)) {
+                        // get voice mail count from preferences
+                        countVoiceMessages = sp.getInt(VM_COUNT, 0);
+                        setVoiceMessageCount(countVoiceMessages);
+                        Rlog.d(LOG_TAG, "getStoredVoiceMessageCount: from preference = " +
+                                countVoiceMessages);
+                    } else {
+                        Rlog.d(LOG_TAG, "getStoredVoiceMessageCount: returning 0 as count for " +
+                                "matching subscriberId not found");
+
+                    }
+                    // get rid of old preferences.
+                    SharedPreferences.Editor editor = sp.edit();
+                    editor.remove(VM_ID);
+                    editor.remove(VM_COUNT);
+                    editor.apply();
+                }
+            }
+        } else {
+            Rlog.e(LOG_TAG, "getStoredVoiceMessageCount: invalid subId " + subId);
+        }
+        return countVoiceMessages;
+    }
+
+    /**
+     * send secret dialer codes to launch arbitrary activities.
+     * an Intent is started with the android_secret_code://<code> URI.
+     *
+     * @param code stripped version of secret code without *#*# prefix and #*#* suffix
+     */
+    public void sendDialerSpecialCode(String code) {
+        if (!TextUtils.isEmpty(code)) {
+            Intent intent = new Intent(TelephonyIntents.SECRET_CODE_ACTION,
+                    Uri.parse("android_secret_code://" + code));
+            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+            mContext.sendBroadcast(intent);
+        }
+    }
+
+    /**
+     * Returns the CDMA ERI icon index to display
+     */
+    public int getCdmaEriIconIndex() {
+        return -1;
+    }
+
+    /**
+     * Returns the CDMA ERI icon mode,
+     * 0 - ON
+     * 1 - FLASHING
+     */
+    public int getCdmaEriIconMode() {
+        return -1;
+    }
+
+    /**
+     * Returns the CDMA ERI text,
+     */
+    public String getCdmaEriText() {
+        return "GSM nw, no ERI";
+    }
+
+    /**
+     * Retrieves the MIN for CDMA phones.
+     */
+    public String getCdmaMin() {
+        return null;
+    }
+
+    /**
+     * Check if subscription data has been assigned to mMin
+     *
+     * return true if MIN info is ready; false otherwise.
+     */
+    public boolean isMinInfoReady() {
+        return false;
+    }
+
+    /**
+     *  Retrieves PRL Version for CDMA phones
+     */
+    public String getCdmaPrlVersion(){
+        return null;
+    }
+
+    /**
+     * send burst DTMF tone, it can send the string as single character or multiple character
+     * ignore if there is no active call or not valid digits string.
+     * Valid digit means only includes characters ISO-LATIN characters 0-9, *, #
+     * The difference between sendDtmf and sendBurstDtmf is sendDtmf only sends one character,
+     * this api can send single character and multiple character, also, this api has response
+     * back to caller.
+     *
+     * @param dtmfString is string representing the dialing digit(s) in the active call
+     * @param on the DTMF ON length in milliseconds, or 0 for default
+     * @param off the DTMF OFF length in milliseconds, or 0 for default
+     * @param onComplete is the callback message when the action is processed by BP
+     *
+     */
+    public void sendBurstDtmf(String dtmfString, int on, int off, Message onComplete) {
+    }
+
+    /**
+     * Sets an event to be fired when the telephony system processes
+     * a post-dial character on an outgoing call.<p>
+     *
+     * Messages of type <code>what</code> will be sent to <code>h</code>.
+     * The <code>obj</code> field of these Message's will be instances of
+     * <code>AsyncResult</code>. <code>Message.obj.result</code> will be
+     * a Connection object.<p>
+     *
+     * Message.arg1 will be the post dial character being processed,
+     * or 0 ('\0') if end of string.<p>
+     *
+     * If Connection.getPostDialState() == WAIT,
+     * the application must call
+     * {@link com.android.internal.telephony.Connection#proceedAfterWaitChar()
+     * Connection.proceedAfterWaitChar()} or
+     * {@link com.android.internal.telephony.Connection#cancelPostDial()
+     * Connection.cancelPostDial()}
+     * for the telephony system to continue playing the post-dial
+     * DTMF sequence.<p>
+     *
+     * If Connection.getPostDialState() == WILD,
+     * the application must call
+     * {@link com.android.internal.telephony.Connection#proceedAfterWildChar
+     * Connection.proceedAfterWildChar()}
+     * or
+     * {@link com.android.internal.telephony.Connection#cancelPostDial()
+     * Connection.cancelPostDial()}
+     * for the telephony system to continue playing the
+     * post-dial DTMF sequence.<p>
+     *
+     * Only one post dial character handler may be set. <p>
+     * Calling this method with "h" equal to null unsets this handler.<p>
+     */
+    public void setOnPostDialCharacter(Handler h, int what, Object obj) {
+        mPostDialHandler = new Registrant(h, what, obj);
+    }
+
+    public Registrant getPostDialHandler() {
+        return mPostDialHandler;
+    }
+
+    /**
+     * request to exit emergency call back mode
+     * the caller should use setOnECMModeExitResponse
+     * to receive the emergency callback mode exit response
+     */
+    public void exitEmergencyCallbackMode() {
+    }
+
+    /**
+     * Register for notifications when CDMA OTA Provision status change
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj) {
+    }
+
+    /**
+     * Unregister for notifications when CDMA OTA Provision status change
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForCdmaOtaStatusChange(Handler h) {
+    }
+
+    /**
+     * Registration point for subscription info ready
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) {
+    }
+
+    /**
+     * Unregister for notifications for subscription info
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSubscriptionInfoReady(Handler h) {
+    }
+
+    /**
+     * Returns true if OTA Service Provisioning needs to be performed.
+     */
+    public boolean needsOtaServiceProvisioning() {
+        return false;
+    }
+
+    /**
+     * this decides if the dial number is OTA(Over the air provision) number or not
+     * @param dialStr is string representing the dialing digit(s)
+     * @return  true means the dialStr is OTA number, and false means the dialStr is not OTA number
+     */
+    public  boolean isOtaSpNumber(String dialStr) {
+        return false;
+    }
+
+    /**
+     * Register for notifications when CDMA call waiting comes
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForCallWaiting(Handler h, int what, Object obj){
+    }
+
+    /**
+     * Unegister for notifications when CDMA Call waiting comes
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForCallWaiting(Handler h){
+    }
+
+    /**
+     * Registration point for Ecm timer reset
+     * @param h handler to notify
+     * @param what user-defined message code
+     * @param obj placed in Message.obj
+     */
+    public void registerForEcmTimerReset(Handler h, int what, Object obj) {
+    }
+
+    /**
+     * Unregister for notification for Ecm timer reset
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForEcmTimerReset(Handler h) {
+    }
+
+    /**
+     * Register for signal information notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a SuppServiceNotification instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForSignalInfo(Handler h, int what, Object obj) {
+        mCi.registerForSignalInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for signal information notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForSignalInfo(Handler h) {
+        mCi.unregisterForSignalInfo(h);
+    }
+
+    /**
+     * Register for display information notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a SuppServiceNotification instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForDisplayInfo(Handler h, int what, Object obj) {
+        mCi.registerForDisplayInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for display information notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForDisplayInfo(Handler h) {
+         mCi.unregisterForDisplayInfo(h);
+    }
+
+    /**
+     * Register for CDMA number information record notification from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a CdmaInformationRecords.CdmaNumberInfoRec
+     * instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForNumberInfo(Handler h, int what, Object obj) {
+        mCi.registerForNumberInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for number information record notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForNumberInfo(Handler h) {
+        mCi.unregisterForNumberInfo(h);
+    }
+
+    /**
+     * Register for CDMA redirected number information record notification
+     * from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a CdmaInformationRecords.CdmaRedirectingNumberInfoRec
+     * instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForRedirectedNumberInfo(Handler h, int what, Object obj) {
+        mCi.registerForRedirectedNumberInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for redirected number information record notification.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForRedirectedNumberInfo(Handler h) {
+        mCi.unregisterForRedirectedNumberInfo(h);
+    }
+
+    /**
+     * Register for CDMA line control information record notification
+     * from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a CdmaInformationRecords.CdmaLineControlInfoRec
+     * instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForLineControlInfo(Handler h, int what, Object obj) {
+        mCi.registerForLineControlInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for line control information notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForLineControlInfo(Handler h) {
+        mCi.unregisterForLineControlInfo(h);
+    }
+
+    /**
+     * Register for CDMA T53 CLIR information record notifications
+     * from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a CdmaInformationRecords.CdmaT53ClirInfoRec
+     * instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerFoT53ClirlInfo(Handler h, int what, Object obj) {
+        mCi.registerFoT53ClirlInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for T53 CLIR information record notification
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForT53ClirInfo(Handler h) {
+        mCi.unregisterForT53ClirInfo(h);
+    }
+
+    /**
+     * Register for CDMA T53 audio control information record notifications
+     * from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a CdmaInformationRecords.CdmaT53AudioControlInfoRec
+     * instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForT53AudioControlInfo(Handler h, int what, Object obj) {
+        mCi.registerForT53AudioControlInfo(h, what, obj);
+    }
+
+    /**
+     * Unregisters for T53 audio control information record notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForT53AudioControlInfo(Handler h) {
+        mCi.unregisterForT53AudioControlInfo(h);
+    }
+
+    /**
+     * registers for exit emergency call back mode request response
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void setOnEcbModeExitResponse(Handler h, int what, Object obj){
+    }
+
+    /**
+     * Unregisters for exit emergency call back mode request response
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unsetOnEcbModeExitResponse(Handler h){
+    }
+
+    /**
+     * Register for radio off or not available
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForRadioOffOrNotAvailable(Handler h, int what, Object obj) {
+        mRadioOffOrNotAvailableRegistrants.addUnique(h, what, obj);
+    }
+
+    /**
+     * Unregisters for radio off or not available
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForRadioOffOrNotAvailable(Handler h) {
+        mRadioOffOrNotAvailableRegistrants.remove(h);
+    }
+
+    /**
+     * Returns an array of string identifiers for the APN types serviced by the
+     * currently active.
+     *  @return The string array will always return at least one entry, Phone.APN_TYPE_DEFAULT.
+     * TODO: Revisit if we always should return at least one entry.
+     */
+    public String[] getActiveApnTypes() {
+        if (mDcTracker == null) {
+            return null;
+        }
+
+        return mDcTracker.getActiveApnTypes();
+    }
+
+    /**
+     * Check if TETHER_DUN_APN setting or config_tether_apndata includes APN that matches
+     * current operator.
+     * @return true if there is a matching DUN APN.
+     */
+    public boolean hasMatchedTetherApnSetting() {
+        return mDcTracker.hasMatchedTetherApnSetting();
+    }
+
+    /**
+     * Returns string for the active APN host.
+     *  @return type as a string or null if none.
+     */
+    public String getActiveApnHost(String apnType) {
+        return mDcTracker.getActiveApnString(apnType);
+    }
+
+    /**
+     * Return the LinkProperties for the named apn or null if not available
+     */
+    public LinkProperties getLinkProperties(String apnType) {
+        return mDcTracker.getLinkProperties(apnType);
+    }
+
+    /**
+     * Return the NetworkCapabilities
+     */
+    public NetworkCapabilities getNetworkCapabilities(String apnType) {
+        return mDcTracker.getNetworkCapabilities(apnType);
+    }
+
+    /**
+     * Report on whether data connectivity is allowed.
+     *
+     * @return True if data is allowed to be established.
+     */
+    public boolean isDataAllowed() {
+        return ((mDcTracker != null) && (mDcTracker.isDataAllowed(null)));
+    }
+
+    /**
+     * Report on whether data connectivity is allowed.
+     *
+     * @param reasons The reasons that data can/can't be established. This is an output param.
+     * @return True if data is allowed to be established
+     */
+    public boolean isDataAllowed(DataConnectionReasons reasons) {
+        return ((mDcTracker != null) && (mDcTracker.isDataAllowed(reasons)));
+    }
+
+
+    /**
+     * Action set from carrier signalling broadcast receivers to enable/disable metered apns.
+     */
+    public void carrierActionSetMeteredApnsEnabled(boolean enabled) {
+        mCarrierActionAgent.carrierActionSetMeteredApnsEnabled(enabled);
+    }
+
+    /**
+     * Action set from carrier signalling broadcast receivers to enable/disable radio
+     */
+    public void carrierActionSetRadioEnabled(boolean enabled) {
+        mCarrierActionAgent.carrierActionSetRadioEnabled(enabled);
+    }
+
+    /**
+     * Action set from carrier app to start/stop reporting default network condition.
+     */
+    public void carrierActionReportDefaultNetworkStatus(boolean report) {
+        mCarrierActionAgent.carrierActionReportDefaultNetworkStatus(report);
+    }
+
+    /**
+     * Notify registrants of a new ringing Connection.
+     * Subclasses of Phone probably want to replace this with a
+     * version scoped to their packages
+     */
+    public void notifyNewRingingConnectionP(Connection cn) {
+        if (!mIsVoiceCapable)
+            return;
+        AsyncResult ar = new AsyncResult(null, cn, null);
+        mNewRingingConnectionRegistrants.notifyRegistrants(ar);
+    }
+
+    /**
+     * Notify registrants of a new unknown connection.
+     */
+    public void notifyUnknownConnectionP(Connection cn) {
+        mUnknownConnectionRegistrants.notifyResult(cn);
+    }
+
+    /**
+     * Notify registrants if phone is video capable.
+     */
+    public void notifyForVideoCapabilityChanged(boolean isVideoCallCapable) {
+        // Cache the current video capability so that we don't lose the information.
+        mIsVideoCapable = isVideoCallCapable;
+
+        AsyncResult ar = new AsyncResult(null, isVideoCallCapable, null);
+        mVideoCapabilityChangedRegistrants.notifyRegistrants(ar);
+    }
+
+    /**
+     * Notify registrants of a RING event.
+     */
+    private void notifyIncomingRing() {
+        if (!mIsVoiceCapable)
+            return;
+        AsyncResult ar = new AsyncResult(null, this, null);
+        mIncomingRingRegistrants.notifyRegistrants(ar);
+    }
+
+    /**
+     * Send the incoming call Ring notification if conditions are right.
+     */
+    private void sendIncomingCallRingNotification(int token) {
+        if (mIsVoiceCapable && !mDoesRilSendMultipleCallRing &&
+                (token == mCallRingContinueToken)) {
+            Rlog.d(LOG_TAG, "Sending notifyIncomingRing");
+            notifyIncomingRing();
+            sendMessageDelayed(
+                    obtainMessage(EVENT_CALL_RING_CONTINUE, token, 0), mCallRingDelay);
+        } else {
+            Rlog.d(LOG_TAG, "Ignoring ring notification request,"
+                    + " mDoesRilSendMultipleCallRing=" + mDoesRilSendMultipleCallRing
+                    + " token=" + token
+                    + " mCallRingContinueToken=" + mCallRingContinueToken
+                    + " mIsVoiceCapable=" + mIsVoiceCapable);
+        }
+    }
+
+    /**
+     * TODO: Adding a function for each property is not good.
+     * A fucntion of type getPhoneProp(propType) where propType is an
+     * enum of GSM+CDMA+LTE props would be a better approach.
+     *
+     * Get "Restriction of menu options for manual PLMN selection" bit
+     * status from EF_CSP data, this belongs to "Value Added Services Group".
+     * @return true if this bit is set or EF_CSP data is unavailable,
+     * false otherwise
+     */
+    public boolean isCspPlmnEnabled() {
+        return false;
+    }
+
+    /**
+     * Return an interface to retrieve the ISIM records for IMS, if available.
+     * @return the interface to retrieve the ISIM records, or null if not supported
+     */
+    public IsimRecords getIsimRecords() {
+        Rlog.e(LOG_TAG, "getIsimRecords() is only supported on LTE devices");
+        return null;
+    }
+
+    /**
+     * Retrieves the MSISDN from the UICC. For GSM/UMTS phones, this is equivalent to
+     * {@link #getLine1Number()}. For CDMA phones, {@link #getLine1Number()} returns
+     * the MDN, so this method is provided to return the MSISDN on CDMA/LTE phones.
+     */
+    public String getMsisdn() {
+        return null;
+    }
+
+    /**
+     * Get the current for the default apn DataState. No change notification
+     * exists at this interface -- use
+     * {@link android.telephony.PhoneStateListener} instead.
+     */
+    public PhoneConstants.DataState getDataConnectionState() {
+        return getDataConnectionState(PhoneConstants.APN_TYPE_DEFAULT);
+    }
+
+    public void notifyCallForwardingIndicator() {
+    }
+
+    public void notifyDataConnectionFailed(String reason, String apnType) {
+        mNotifier.notifyDataConnectionFailed(this, reason, apnType);
+    }
+
+    public void notifyPreciseDataConnectionFailed(String reason, String apnType, String apn,
+            String failCause) {
+        mNotifier.notifyPreciseDataConnectionFailed(this, reason, apnType, apn, failCause);
+    }
+
+    /**
+     * Return if the current radio is LTE on CDMA. This
+     * is a tri-state return value as for a period of time
+     * the mode may be unknown.
+     *
+     * @return {@link PhoneConstants#LTE_ON_CDMA_UNKNOWN}, {@link PhoneConstants#LTE_ON_CDMA_FALSE}
+     * or {@link PhoneConstants#LTE_ON_CDMA_TRUE}
+     */
+    public int getLteOnCdmaMode() {
+        return mCi.getLteOnCdmaMode();
+    }
+
+    /**
+     * Sets the SIM voice message waiting indicator records.
+     * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported
+     * @param countWaiting The number of messages waiting, if known. Use
+     *                     -1 to indicate that an unknown number of
+     *                      messages are waiting
+     */
+    public void setVoiceMessageWaiting(int line, int countWaiting) {
+        // This function should be overridden by class GsmCdmaPhone.
+        Rlog.e(LOG_TAG, "Error! This function should never be executed, inactive Phone.");
+    }
+
+    /**
+     * Gets the USIM service table from the UICC, if present and available.
+     * @return an interface to the UsimServiceTable record, or null if not available
+     */
+    public UsimServiceTable getUsimServiceTable() {
+        IccRecords r = mIccRecords.get();
+        return (r != null) ? r.getUsimServiceTable() : null;
+    }
+
+    /**
+     * Gets the Uicc card corresponding to this phone.
+     * @return the UiccCard object corresponding to the phone ID.
+     */
+    public UiccCard getUiccCard() {
+        return mUiccController.getUiccCard(mPhoneId);
+    }
+
+    /**
+     * Get P-CSCF address from PCO after data connection is established or modified.
+     * @param apnType the apnType, "ims" for IMS APN, "emergency" for EMERGENCY APN
+     */
+    public String[] getPcscfAddress(String apnType) {
+        return mDcTracker.getPcscfAddress(apnType);
+    }
+
+    /**
+     * Set IMS registration state
+     */
+    public void setImsRegistrationState(boolean registered) {
+    }
+
+    /**
+     * Return an instance of a IMS phone
+     */
+    public Phone getImsPhone() {
+        return mImsPhone;
+    }
+
+    /**
+     * Returns Carrier specific information that will be used to encrypt the IMSI and IMPI.
+     * @param keyType whether the key is being used for WLAN or ePDG.
+     * @return ImsiEncryptionInfo which includes the Key Type, the Public Key
+     *        {@link java.security.PublicKey} and the Key Identifier.
+     *        The keyIdentifier This is used by the server to help it locate the private key to
+     *        decrypt the permanent identity.
+     */
+    public ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int keyType) {
+        return null;
+    }
+
+    /**
+     * Sets the carrier information needed to encrypt the IMSI and IMPI.
+     * @param imsiEncryptionInfo Carrier specific information that will be used to encrypt the
+     *        IMSI and IMPI. This includes the Key type, the Public key
+     *        {@link java.security.PublicKey} and the Key identifier.
+     */
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo) {
+        return;
+    }
+
+    /**
+     * Return if UT capability of ImsPhone is enabled or not
+     */
+    public boolean isUtEnabled() {
+        if (mImsPhone != null) {
+            return mImsPhone.isUtEnabled();
+        }
+        return false;
+    }
+
+    public void dispose() {
+    }
+
+    private void updateImsPhone() {
+        Rlog.d(LOG_TAG, "updateImsPhone"
+                + " mImsServiceReady=" + mImsServiceReady);
+
+        if (mImsServiceReady && (mImsPhone == null)) {
+            mImsPhone = PhoneFactory.makeImsPhone(mNotifier, this);
+            CallManager.getInstance().registerPhone(mImsPhone);
+            mImsPhone.registerForSilentRedial(
+                    this, EVENT_INITIATE_SILENT_REDIAL, null);
+        } else if (!mImsServiceReady && (mImsPhone != null)) {
+            CallManager.getInstance().unregisterPhone(mImsPhone);
+            mImsPhone.unregisterForSilentRedial(this);
+
+            mImsPhone.dispose();
+            // Potential GC issue if someone keeps a reference to ImsPhone.
+            // However: this change will make sure that such a reference does
+            // not access functions through NULL pointer.
+            //mImsPhone.removeReferences();
+            mImsPhone = null;
+        }
+    }
+
+    /**
+     * Dials a number.
+     *
+     * @param dialString The number to dial.
+     * @param uusInfo The UUSInfo.
+     * @param videoState The video state for the call.
+     * @param intentExtras Extras from the original CALL intent.
+     * @return The Connection.
+     * @throws CallStateException
+     */
+    protected Connection dialInternal(
+            String dialString, UUSInfo uusInfo, int videoState, Bundle intentExtras)
+            throws CallStateException {
+        // dialInternal shall be overriden by GsmCdmaPhone
+        return null;
+    }
+
+    /*
+     * Returns the subscription id.
+     */
+    public int getSubId() {
+        return SubscriptionController.getInstance().getSubIdUsingPhoneId(mPhoneId);
+    }
+
+    /**
+     * Returns the phone id.
+     */
+    public int getPhoneId() {
+        return mPhoneId;
+    }
+
+    /**
+     * Return the service state of mImsPhone if it is STATE_IN_SERVICE
+     * otherwise return the current voice service state
+     */
+    public int getVoicePhoneServiceState() {
+        Phone imsPhone = mImsPhone;
+        if (imsPhone != null
+                && imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE) {
+            return ServiceState.STATE_IN_SERVICE;
+        }
+        return getServiceState().getState();
+    }
+
+    /**
+     * Override the service provider name and the operator name for the current ICCID.
+     */
+    public boolean setOperatorBrandOverride(String brand) {
+        return false;
+    }
+
+    /**
+     * Override the roaming indicator for the current ICCID.
+     */
+    public boolean setRoamingOverride(List<String> gsmRoamingList,
+            List<String> gsmNonRoamingList, List<String> cdmaRoamingList,
+            List<String> cdmaNonRoamingList) {
+        String iccId = getIccSerialNumber();
+        if (TextUtils.isEmpty(iccId)) {
+            return false;
+        }
+
+        setRoamingOverrideHelper(gsmRoamingList, GSM_ROAMING_LIST_OVERRIDE_PREFIX, iccId);
+        setRoamingOverrideHelper(gsmNonRoamingList, GSM_NON_ROAMING_LIST_OVERRIDE_PREFIX, iccId);
+        setRoamingOverrideHelper(cdmaRoamingList, CDMA_ROAMING_LIST_OVERRIDE_PREFIX, iccId);
+        setRoamingOverrideHelper(cdmaNonRoamingList, CDMA_NON_ROAMING_LIST_OVERRIDE_PREFIX, iccId);
+
+        // Refresh.
+        ServiceStateTracker tracker = getServiceStateTracker();
+        if (tracker != null) {
+            tracker.pollState();
+        }
+        return true;
+    }
+
+    private void setRoamingOverrideHelper(List<String> list, String prefix, String iccId) {
+        SharedPreferences.Editor spEditor =
+                PreferenceManager.getDefaultSharedPreferences(mContext).edit();
+        String key = prefix + iccId;
+        if (list == null || list.isEmpty()) {
+            spEditor.remove(key).commit();
+        } else {
+            spEditor.putStringSet(key, new HashSet<String>(list)).commit();
+        }
+    }
+
+    public boolean isMccMncMarkedAsRoaming(String mccMnc) {
+        return getRoamingOverrideHelper(GSM_ROAMING_LIST_OVERRIDE_PREFIX, mccMnc);
+    }
+
+    public boolean isMccMncMarkedAsNonRoaming(String mccMnc) {
+        return getRoamingOverrideHelper(GSM_NON_ROAMING_LIST_OVERRIDE_PREFIX, mccMnc);
+    }
+
+    public boolean isSidMarkedAsRoaming(int SID) {
+        return getRoamingOverrideHelper(CDMA_ROAMING_LIST_OVERRIDE_PREFIX,
+                Integer.toString(SID));
+    }
+
+    public boolean isSidMarkedAsNonRoaming(int SID) {
+        return getRoamingOverrideHelper(CDMA_NON_ROAMING_LIST_OVERRIDE_PREFIX,
+                Integer.toString(SID));
+    }
+
+    /**
+     * Query the IMS Registration Status.
+     *
+     * @return true if IMS is Registered
+     */
+    public boolean isImsRegistered() {
+        Phone imsPhone = mImsPhone;
+        boolean isImsRegistered = false;
+        if (imsPhone != null) {
+            isImsRegistered = imsPhone.isImsRegistered();
+        } else {
+            ServiceStateTracker sst = getServiceStateTracker();
+            if (sst != null) {
+                isImsRegistered = sst.isImsRegistered();
+            }
+        }
+        Rlog.d(LOG_TAG, "isImsRegistered =" + isImsRegistered);
+        return isImsRegistered;
+    }
+
+    /**
+     * Get Wifi Calling Feature Availability
+     */
+    public boolean isWifiCallingEnabled() {
+        Phone imsPhone = mImsPhone;
+        boolean isWifiCallingEnabled = false;
+        if (imsPhone != null) {
+            isWifiCallingEnabled = imsPhone.isWifiCallingEnabled();
+        }
+        Rlog.d(LOG_TAG, "isWifiCallingEnabled =" + isWifiCallingEnabled);
+        return isWifiCallingEnabled;
+    }
+
+    /**
+     * Get Volte Feature Availability
+     */
+    public boolean isVolteEnabled() {
+        Phone imsPhone = mImsPhone;
+        boolean isVolteEnabled = false;
+        if (imsPhone != null) {
+            isVolteEnabled = imsPhone.isVolteEnabled();
+        }
+        Rlog.d(LOG_TAG, "isImsRegistered =" + isVolteEnabled);
+        return isVolteEnabled;
+    }
+
+    private boolean getRoamingOverrideHelper(String prefix, String key) {
+        String iccId = getIccSerialNumber();
+        if (TextUtils.isEmpty(iccId) || TextUtils.isEmpty(key)) {
+            return false;
+        }
+
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+        Set<String> value = sp.getStringSet(prefix + iccId, null);
+        if (value == null) {
+            return false;
+        }
+        return value.contains(key);
+    }
+
+    /**
+     * Is Radio Present on the device and is it accessible
+     */
+    public boolean isRadioAvailable() {
+        return mCi.getRadioState().isAvailable();
+    }
+
+    /**
+     * Is Radio turned on
+     */
+    public boolean isRadioOn() {
+        return mCi.getRadioState().isOn();
+    }
+
+    /**
+     * shutdown Radio gracefully
+     */
+    public void shutdownRadio() {
+        getServiceStateTracker().requestShutdown();
+    }
+
+    /**
+     * Return true if the device is shutting down.
+     */
+    public boolean isShuttingDown() {
+        return getServiceStateTracker().isDeviceShuttingDown();
+    }
+
+    /**
+     *  Set phone radio capability
+     *
+     *  @param rc the phone radio capability defined in
+     *         RadioCapability. It's a input object used to transfer parameter to logic modem
+     *  @param response Callback message.
+     */
+    public void setRadioCapability(RadioCapability rc, Message response) {
+        mCi.setRadioCapability(rc, response);
+    }
+
+    /**
+     *  Get phone radio access family
+     *
+     *  @return a bit mask to identify the radio access family.
+     */
+    public int getRadioAccessFamily() {
+        final RadioCapability rc = getRadioCapability();
+        return (rc == null ? RadioAccessFamily.RAF_UNKNOWN : rc.getRadioAccessFamily());
+    }
+
+    /**
+     *  Get the associated data modems Id.
+     *
+     *  @return a String containing the id of the data modem
+     */
+    public String getModemUuId() {
+        final RadioCapability rc = getRadioCapability();
+        return (rc == null ? "" : rc.getLogicalModemUuid());
+    }
+
+    /**
+     *  Get phone radio capability
+     *
+     *  @return the capability of the radio defined in RadioCapability
+     */
+    public RadioCapability getRadioCapability() {
+        return mRadioCapability.get();
+    }
+
+    /**
+     *  The RadioCapability has changed. This comes up from the RIL and is called when radios first
+     *  become available or after a capability switch.  The flow is we use setRadioCapability to
+     *  request a change with the RIL and get an UNSOL response with the new data which gets set
+     *  here.
+     *
+     *  @param rc the phone radio capability currently in effect for this phone.
+     */
+    public void radioCapabilityUpdated(RadioCapability rc) {
+        // Called when radios first become available or after a capability switch
+        // Update the cached value
+        mRadioCapability.set(rc);
+
+        if (SubscriptionManager.isValidSubscriptionId(getSubId())) {
+            sendSubscriptionSettings(true);
+        }
+    }
+
+    public void sendSubscriptionSettings(boolean restoreNetworkSelection) {
+        // Send settings down
+        int type = PhoneFactory.calculatePreferredNetworkType(mContext, getSubId());
+        setPreferredNetworkType(type, null);
+
+        if (restoreNetworkSelection) {
+            restoreSavedNetworkSelection(null);
+        }
+    }
+
+    protected void setPreferredNetworkTypeIfSimLoaded() {
+        int subId = getSubId();
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            int type = PhoneFactory.calculatePreferredNetworkType(mContext, getSubId());
+            setPreferredNetworkType(type, null);
+        }
+    }
+
+    /**
+     * Registers the handler when phone radio  capability is changed.
+     *
+     * @param h Handler for notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    public void registerForRadioCapabilityChanged(Handler h, int what, Object obj) {
+        mCi.registerForRadioCapabilityChanged(h, what, obj);
+    }
+
+    /**
+     * Unregister for notifications when phone radio type and access technology is changed.
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    public void unregisterForRadioCapabilityChanged(Handler h) {
+        mCi.unregisterForRadioCapabilityChanged(this);
+    }
+
+    /**
+     * Determines if  IMS is enabled for call.
+     *
+     * @return {@code true} if IMS calling is enabled.
+     */
+    public boolean isImsUseEnabled() {
+        boolean imsUseEnabled =
+                ((ImsManager.isVolteEnabledByPlatform(mContext) &&
+                ImsManager.isEnhanced4gLteModeSettingEnabledByUser(mContext)) ||
+                (ImsManager.isWfcEnabledByPlatform(mContext) &&
+                ImsManager.isWfcEnabledByUser(mContext)) &&
+                ImsManager.isNonTtyOrTtyOnVolteEnabled(mContext));
+        return imsUseEnabled;
+    }
+
+    /**
+     * Determines if the connection to IMS services are available yet.
+     * @return {@code true} if the connection to IMS services are available.
+     */
+    public boolean isImsAvailable() {
+        if (mImsPhone == null) {
+            return false;
+        }
+
+        return mImsPhone.isImsAvailable();
+    }
+
+    /**
+     * Determines if video calling is enabled for the phone.
+     *
+     * @return {@code true} if video calling is enabled, {@code false} otherwise.
+     */
+    public boolean isVideoEnabled() {
+        Phone imsPhone = mImsPhone;
+        if (imsPhone != null) {
+            return imsPhone.isVideoEnabled();
+        }
+        return false;
+    }
+
+    /**
+     * Returns the status of Link Capacity Estimation (LCE) service.
+     */
+    public int getLceStatus() {
+        return mLceStatus;
+    }
+
+    /**
+     * Returns the modem activity information
+     */
+    public void getModemActivityInfo(Message response)  {
+        mCi.getModemActivityInfo(response);
+    }
+
+    /**
+     * Starts LCE service after radio becomes available.
+     * LCE service state may get destroyed on the modem when radio becomes unavailable.
+     */
+    public void startLceAfterRadioIsAvailable() {
+        mCi.startLceService(DEFAULT_REPORT_INTERVAL_MS, LCE_PULL_MODE,
+                obtainMessage(EVENT_CONFIG_LCE));
+    }
+
+    /**
+     * Set allowed carriers
+     */
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message response) {
+        mCi.setAllowedCarriers(carriers, response);
+    }
+
+    /**
+     * Get allowed carriers
+     */
+    public void getAllowedCarriers(Message response) {
+        mCi.getAllowedCarriers(response);
+    }
+
+    /**
+     * Returns the locale based on the carrier properties (such as {@code ro.carrier}) and
+     * SIM preferences.
+     */
+    public Locale getLocaleFromSimAndCarrierPrefs() {
+        final IccRecords records = mIccRecords.get();
+        if (records != null && records.getSimLanguage() != null) {
+            return new Locale(records.getSimLanguage());
+        }
+
+        return getLocaleFromCarrierProperties(mContext);
+    }
+
+    public void updateDataConnectionTracker() {
+        mDcTracker.update();
+    }
+
+    public void setInternalDataEnabled(boolean enable, Message onCompleteMsg) {
+        mDcTracker.setInternalDataEnabled(enable, onCompleteMsg);
+    }
+
+    public boolean updateCurrentCarrierInProvider() {
+        return false;
+    }
+
+    public void registerForAllDataDisconnected(Handler h, int what, Object obj) {
+        mDcTracker.registerForAllDataDisconnected(h, what, obj);
+    }
+
+    public void unregisterForAllDataDisconnected(Handler h) {
+        mDcTracker.unregisterForAllDataDisconnected(h);
+    }
+
+    public void registerForDataEnabledChanged(Handler h, int what, Object obj) {
+        mDcTracker.registerForDataEnabledChanged(h, what, obj);
+    }
+
+    public void unregisterForDataEnabledChanged(Handler h) {
+        mDcTracker.unregisterForDataEnabledChanged(h);
+    }
+
+    public IccSmsInterfaceManager getIccSmsInterfaceManager(){
+        return null;
+    }
+
+    protected boolean isMatchGid(String gid) {
+        String gid1 = getGroupIdLevel1();
+        int gidLength = gid.length();
+        if (!TextUtils.isEmpty(gid1) && (gid1.length() >= gidLength)
+                && gid1.substring(0, gidLength).equalsIgnoreCase(gid)) {
+            return true;
+        }
+        return false;
+    }
+
+    public static void checkWfcWifiOnlyModeBeforeDial(Phone imsPhone, Context context)
+            throws CallStateException {
+        if (imsPhone == null || !imsPhone.isWifiCallingEnabled()) {
+            boolean wfcWiFiOnly = (ImsManager.isWfcEnabledByPlatform(context) &&
+                    ImsManager.isWfcEnabledByUser(context) &&
+                    (ImsManager.getWfcMode(context) ==
+                            ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY));
+            if (wfcWiFiOnly) {
+                throw new CallStateException(
+                        CallStateException.ERROR_OUT_OF_SERVICE,
+                        "WFC Wi-Fi Only Mode: IMS not registered");
+            }
+        }
+    }
+
+    public void startRingbackTone() {
+    }
+
+    public void stopRingbackTone() {
+    }
+
+    public void callEndCleanupHandOverCallIfAny() {
+    }
+
+    public void cancelUSSD() {
+    }
+
+    /**
+     * Set boolean broadcastEmergencyCallStateChanges
+     */
+    public abstract void setBroadcastEmergencyCallStateChanges(boolean broadcast);
+
+    public abstract void sendEmergencyCallStateChange(boolean callActive);
+
+    /**
+     * This function returns the parent phone of the current phone. It is applicable
+     * only for IMS phone (function is overridden by ImsPhone). For others the phone
+     * object itself is returned.
+     * @return
+     */
+    public Phone getDefaultPhone() {
+        return this;
+    }
+
+    /**
+     * Get aggregated video call data usage since boot.
+     * Permissions android.Manifest.permission.READ_NETWORK_USAGE_HISTORY is required.
+     *
+     * @param perUidStats True if requesting data usage per uid, otherwise overall usage.
+     * @return Snapshot of video call data usage
+     */
+    public NetworkStats getVtDataUsage(boolean perUidStats) {
+        if (mImsPhone == null) return null;
+        return mImsPhone.getVtDataUsage(perUidStats);
+    }
+
+    /**
+     * Policy control of data connection. Usually used when we hit data limit.
+     * @param enabled True if enabling the data, otherwise disabling.
+     */
+    public void setPolicyDataEnabled(boolean enabled) {
+        mDcTracker.setPolicyDataEnabled(enabled);
+    }
+
+    /**
+     * SIP URIs aliased to the current subscriber given by the IMS implementation.
+     * Applicable only on IMS; used in absence of line1number.
+     * @return array of SIP URIs aliased to the current subscriber
+     */
+    public Uri[] getCurrentSubscriberUris() {
+        return null;
+    }
+
+    public AppSmsManager getAppSmsManager() {
+        return mAppSmsManager;
+    }
+
+    /**
+     * Set SIM card power state.
+     * @param state State of SIM (power down, power up, pass through)
+     * - {@link android.telephony.TelephonyManager#CARD_POWER_DOWN}
+     * - {@link android.telephony.TelephonyManager#CARD_POWER_UP}
+     * - {@link android.telephony.TelephonyManager#CARD_POWER_UP_PASS_THROUGH}
+     **/
+    public void setSimPowerState(int state) {
+        mCi.setSimCardPower(state, null);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("Phone: subId=" + getSubId());
+        pw.println(" mPhoneId=" + mPhoneId);
+        pw.println(" mCi=" + mCi);
+        pw.println(" mDnsCheckDisabled=" + mDnsCheckDisabled);
+        pw.println(" mDcTracker=" + mDcTracker);
+        pw.println(" mDoesRilSendMultipleCallRing=" + mDoesRilSendMultipleCallRing);
+        pw.println(" mCallRingContinueToken=" + mCallRingContinueToken);
+        pw.println(" mCallRingDelay=" + mCallRingDelay);
+        pw.println(" mIsVoiceCapable=" + mIsVoiceCapable);
+        pw.println(" mIccRecords=" + mIccRecords.get());
+        pw.println(" mUiccApplication=" + mUiccApplication.get());
+        pw.println(" mSmsStorageMonitor=" + mSmsStorageMonitor);
+        pw.println(" mSmsUsageMonitor=" + mSmsUsageMonitor);
+        pw.flush();
+        pw.println(" mLooper=" + mLooper);
+        pw.println(" mContext=" + mContext);
+        pw.println(" mNotifier=" + mNotifier);
+        pw.println(" mSimulatedRadioControl=" + mSimulatedRadioControl);
+        pw.println(" mUnitTestMode=" + mUnitTestMode);
+        pw.println(" isDnsCheckDisabled()=" + isDnsCheckDisabled());
+        pw.println(" getUnitTestMode()=" + getUnitTestMode());
+        pw.println(" getState()=" + getState());
+        pw.println(" getIccSerialNumber()=" + getIccSerialNumber());
+        pw.println(" getIccRecordsLoaded()=" + getIccRecordsLoaded());
+        pw.println(" getMessageWaitingIndicator()=" + getMessageWaitingIndicator());
+        pw.println(" getCallForwardingIndicator()=" + getCallForwardingIndicator());
+        pw.println(" isInEmergencyCall()=" + isInEmergencyCall());
+        pw.flush();
+        pw.println(" isInEcm()=" + isInEcm());
+        pw.println(" getPhoneName()=" + getPhoneName());
+        pw.println(" getPhoneType()=" + getPhoneType());
+        pw.println(" getVoiceMessageCount()=" + getVoiceMessageCount());
+        pw.println(" getActiveApnTypes()=" + getActiveApnTypes());
+        pw.println(" needsOtaServiceProvisioning=" + needsOtaServiceProvisioning());
+        pw.flush();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        if (mImsPhone != null) {
+            try {
+                mImsPhone.dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (mDcTracker != null) {
+            try {
+                mDcTracker.dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (getServiceStateTracker() != null) {
+            try {
+                getServiceStateTracker().dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (mCarrierActionAgent != null) {
+            try {
+                mCarrierActionAgent.dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (mCarrierSignalAgent != null) {
+            try {
+                mCarrierSignalAgent.dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (getCallTracker() != null) {
+            try {
+                getCallTracker().dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (mSimActivationTracker != null) {
+            try {
+                mSimActivationTracker.dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        if (mCi != null && mCi instanceof RIL) {
+            try {
+                ((RIL)mCi).dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+    }
+}
diff --git a/com/android/internal/telephony/PhoneConstantConversions.java b/com/android/internal/telephony/PhoneConstantConversions.java
new file mode 100644
index 0000000..f7f0f29
--- /dev/null
+++ b/com/android/internal/telephony/PhoneConstantConversions.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.PreciseCallState;
+
+import com.android.internal.telephony.PhoneConstants;
+
+import java.util.List;
+
+public class PhoneConstantConversions {
+    /**
+     * Convert the {@link PhoneConstants.State} enum into the TelephonyManager.CALL_STATE_*
+     * constants for the public API.
+     */
+    public static int convertCallState(PhoneConstants.State state) {
+        switch (state) {
+            case RINGING:
+                return TelephonyManager.CALL_STATE_RINGING;
+            case OFFHOOK:
+                return TelephonyManager.CALL_STATE_OFFHOOK;
+            default:
+                return TelephonyManager.CALL_STATE_IDLE;
+        }
+    }
+
+    /**
+     * Convert the TelephonyManager.CALL_STATE_* constants into the
+     * {@link PhoneConstants.State} enum for the public API.
+     */
+    public static PhoneConstants.State convertCallState(int state) {
+        switch (state) {
+            case TelephonyManager.CALL_STATE_RINGING:
+                return PhoneConstants.State.RINGING;
+            case TelephonyManager.CALL_STATE_OFFHOOK:
+                return PhoneConstants.State.OFFHOOK;
+            default:
+                return PhoneConstants.State.IDLE;
+        }
+    }
+
+    /**
+     * Convert the {@link PhoneConstants.DataState} enum into the TelephonyManager.DATA_* constants
+     * for the public API.
+     */
+    public static int convertDataState(PhoneConstants.DataState state) {
+        switch (state) {
+            case CONNECTING:
+                return TelephonyManager.DATA_CONNECTING;
+            case CONNECTED:
+                return TelephonyManager.DATA_CONNECTED;
+            case SUSPENDED:
+                return TelephonyManager.DATA_SUSPENDED;
+            default:
+                return TelephonyManager.DATA_DISCONNECTED;
+        }
+    }
+
+    /**
+     * Convert the TelephonyManager.DATA_* constants into {@link PhoneConstants.DataState} enum
+     * for the public API.
+     */
+    public static PhoneConstants.DataState convertDataState(int state) {
+        switch (state) {
+            case TelephonyManager.DATA_CONNECTING:
+                return PhoneConstants.DataState.CONNECTING;
+            case TelephonyManager.DATA_CONNECTED:
+                return PhoneConstants.DataState.CONNECTED;
+            case TelephonyManager.DATA_SUSPENDED:
+                return PhoneConstants.DataState.SUSPENDED;
+            default:
+                return PhoneConstants.DataState.DISCONNECTED;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/PhoneConstants.java b/com/android/internal/telephony/PhoneConstants.java
new file mode 100644
index 0000000..f9de776
--- /dev/null
+++ b/com/android/internal/telephony/PhoneConstants.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony;
+
+/**
+ * @hide
+ */
+public class PhoneConstants {
+
+    /**
+     * The phone state. One of the following:<p>
+     * <ul>
+     * <li>IDLE = no phone activity</li>
+     * <li>RINGING = a phone call is ringing or call waiting.
+     *  In the latter case, another call is active as well</li>
+     * <li>OFFHOOK = The phone is off hook. At least one call
+     * exists that is dialing, active or holding and no calls are
+     * ringing or waiting.</li>
+     * </ul>
+     */
+    public enum State {
+        IDLE, RINGING, OFFHOOK;
+    };
+
+    /**
+      * The state of a data connection.
+      * <ul>
+      * <li>CONNECTED = IP traffic should be available</li>
+      * <li>CONNECTING = Currently setting up data connection</li>
+      * <li>DISCONNECTED = IP not available</li>
+      * <li>SUSPENDED = connection is created but IP traffic is
+      *                 temperately not available. i.e. voice call is in place
+      *                 in 2G network</li>
+      * </ul>
+      */
+    public enum DataState {
+        CONNECTED, CONNECTING, DISCONNECTED, SUSPENDED;
+    };
+
+    public static final String STATE_KEY = "state";
+
+    // Radio Type
+    public static final int PHONE_TYPE_NONE = RILConstants.NO_PHONE;
+    public static final int PHONE_TYPE_GSM = RILConstants.GSM_PHONE;
+    public static final int PHONE_TYPE_CDMA = RILConstants.CDMA_PHONE;
+    public static final int PHONE_TYPE_SIP = RILConstants.SIP_PHONE;
+    public static final int PHONE_TYPE_THIRD_PARTY = RILConstants.THIRD_PARTY_PHONE;
+    public static final int PHONE_TYPE_IMS = RILConstants.IMS_PHONE;
+    // Currently this is used only to differentiate CDMA and CDMALTE Phone in GsmCdma* files. For
+    // anything outside of that, a cdma + lte phone is still CDMA_PHONE
+    public static final int PHONE_TYPE_CDMA_LTE = RILConstants.CDMA_LTE_PHONE;
+
+    // Modes for LTE_ON_CDMA
+    public static final int LTE_ON_CDMA_UNKNOWN = RILConstants.LTE_ON_CDMA_UNKNOWN;
+    public static final int LTE_ON_CDMA_FALSE = RILConstants.LTE_ON_CDMA_FALSE;
+    public static final int LTE_ON_CDMA_TRUE = RILConstants.LTE_ON_CDMA_TRUE;
+
+    // Number presentation type for caller id display (From internal/Connection.java)
+    public static final int PRESENTATION_ALLOWED = 1;    // normal
+    public static final int PRESENTATION_RESTRICTED = 2; // block by user
+    public static final int PRESENTATION_UNKNOWN = 3;    // no specified or unknown by network
+    public static final int PRESENTATION_PAYPHONE = 4;   // show pay phone info
+
+    // Sim activation type
+    public static final int SIM_ACTIVATION_TYPE_VOICE = 0;
+    public static final int SIM_ACTIVATION_TYPE_DATA = 1;
+
+    public static final String PHONE_NAME_KEY = "phoneName";
+    public static final String FAILURE_REASON_KEY = "reason";
+    public static final String STATE_CHANGE_REASON_KEY = "reason";
+    public static final String DATA_NETWORK_TYPE_KEY = "networkType";
+    public static final String DATA_FAILURE_CAUSE_KEY = "failCause";
+    public static final String DATA_APN_TYPE_KEY = "apnType";
+    public static final String DATA_APN_KEY = "apn";
+    public static final String DATA_LINK_PROPERTIES_KEY = "linkProperties";
+    public static final String DATA_NETWORK_CAPABILITIES_KEY = "networkCapabilities";
+
+    public static final String DATA_IFACE_NAME_KEY = "iface";
+    public static final String NETWORK_UNAVAILABLE_KEY = "networkUnvailable";
+    public static final String DATA_NETWORK_ROAMING_KEY = "networkRoaming";
+    public static final String PHONE_IN_ECM_STATE = "phoneinECMState";
+    public static final String PHONE_IN_EMERGENCY_CALL = "phoneInEmergencyCall";
+
+    public static final String REASON_LINK_PROPERTIES_CHANGED = "linkPropertiesChanged";
+
+    /**
+     * Return codes for supplyPinReturnResult and
+     * supplyPukReturnResult APIs
+     */
+    public static final int PIN_RESULT_SUCCESS = 0;
+    public static final int PIN_PASSWORD_INCORRECT = 1;
+    public static final int PIN_GENERAL_FAILURE = 2;
+
+    /**
+     * Return codes for <code>enableApnType()</code>
+     */
+    public static final int APN_ALREADY_ACTIVE     = 0;
+    public static final int APN_REQUEST_STARTED    = 1;
+    public static final int APN_TYPE_NOT_AVAILABLE = 2;
+    public static final int APN_REQUEST_FAILED     = 3;
+    public static final int APN_ALREADY_INACTIVE   = 4;
+
+    /**
+     * APN types for data connections.  These are usage categories for an APN
+     * entry.  One APN entry may support multiple APN types, eg, a single APN
+     * may service regular internet traffic ("default") as well as MMS-specific
+     * connections.<br/>
+     * APN_TYPE_ALL is a special type to indicate that this APN entry can
+     * service all data connections.
+     */
+    public static final String APN_TYPE_ALL = "*";
+    /** APN type for default data traffic */
+    public static final String APN_TYPE_DEFAULT = "default";
+    /** APN type for MMS traffic */
+    public static final String APN_TYPE_MMS = "mms";
+    /** APN type for SUPL assisted GPS */
+    public static final String APN_TYPE_SUPL = "supl";
+    /** APN type for DUN traffic */
+    public static final String APN_TYPE_DUN = "dun";
+    /** APN type for HiPri traffic */
+    public static final String APN_TYPE_HIPRI = "hipri";
+    /** APN type for FOTA */
+    public static final String APN_TYPE_FOTA = "fota";
+    /** APN type for IMS */
+    public static final String APN_TYPE_IMS = "ims";
+    /** APN type for CBS */
+    public static final String APN_TYPE_CBS = "cbs";
+    /** APN type for IA Initial Attach APN */
+    public static final String APN_TYPE_IA = "ia";
+    /** APN type for Emergency PDN. This is not an IA apn, but is used
+     * for access to carrier services in an emergency call situation. */
+    public static final String APN_TYPE_EMERGENCY = "emergency";
+    /** Array of all APN types */
+    public static final String[] APN_TYPES = {APN_TYPE_DEFAULT,
+            APN_TYPE_MMS,
+            APN_TYPE_SUPL,
+            APN_TYPE_DUN,
+            APN_TYPE_HIPRI,
+            APN_TYPE_FOTA,
+            APN_TYPE_IMS,
+            APN_TYPE_CBS,
+            APN_TYPE_IA,
+            APN_TYPE_EMERGENCY
+    };
+
+    public static final int RIL_CARD_MAX_APPS    = 8;
+
+    public static final int DEFAULT_CARD_INDEX   = 0;
+
+    public static final int MAX_PHONE_COUNT_SINGLE_SIM = 1;
+
+    public static final int MAX_PHONE_COUNT_DUAL_SIM = 2;
+
+    public static final int MAX_PHONE_COUNT_TRI_SIM = 3;
+
+    public static final String PHONE_KEY = "phone";
+
+    public static final String SLOT_KEY  = "slot";
+
+    /** Fired when a subscriptions phone state changes. */
+    public static final String ACTION_SUBSCRIPTION_PHONE_STATE_CHANGED =
+        "android.intent.action.SUBSCRIPTION_PHONE_STATE";
+
+    // FIXME: This is used to pass a subId via intents, we need to look at its usage, which is
+    // FIXME: extensive, and see if this should be an array of all active subId's or ...?
+    public static final String SUBSCRIPTION_KEY  = "subscription";
+
+    public static final String SUB_SETTING  = "subSettings";
+
+    public static final int SUB1 = 0;
+    public static final int SUB2 = 1;
+    public static final int SUB3 = 2;
+
+    // TODO: Remove these constants and use an int instead.
+    public static final int SIM_ID_1 = 0;
+    public static final int SIM_ID_2 = 1;
+    public static final int SIM_ID_3 = 2;
+    public static final int SIM_ID_4 = 3;
+
+    // ICC SIM Application Types
+    // TODO: Replace the IccCardApplicationStatus.AppType enums with these constants
+    public static final int APPTYPE_UNKNOWN = 0;
+    public static final int APPTYPE_SIM = 1;
+    public static final int APPTYPE_USIM = 2;
+    public static final int APPTYPE_RUIM = 3;
+    public static final int APPTYPE_CSIM = 4;
+    public static final int APPTYPE_ISIM = 5;
+
+    public enum CardUnavailableReason {
+        REASON_CARD_REMOVED,
+        REASON_RADIO_UNAVAILABLE,
+        REASON_SIM_REFRESH_RESET
+    };
+
+    // Initial MTU value.
+    public static final int UNSET_MTU = 0;
+
+    //FIXME maybe this shouldn't be here - sprout only
+    public static final int CAPABILITY_3G   = 1;
+
+    /**
+     * Values for the adb property "persist.radio.videocall.audio.output"
+     */
+    public static final int AUDIO_OUTPUT_ENABLE_SPEAKER = 0;
+    public static final int AUDIO_OUTPUT_DISABLE_SPEAKER = 1;
+    public static final int AUDIO_OUTPUT_DEFAULT = AUDIO_OUTPUT_ENABLE_SPEAKER;
+
+    // authContext (parameter P2) when doing SIM challenge,
+    // per 3GPP TS 31.102 (Section 7.1.2)
+    public static final int AUTH_CONTEXT_EAP_SIM = 128;
+    public static final int AUTH_CONTEXT_EAP_AKA = 129;
+    public static final int AUTH_CONTEXT_UNDEFINED = -1;
+
+    /**
+     * Value for the global property CELL_ON
+     *  0: Cell radio is off
+     *  1: Cell radio is on
+     *  2: Cell radio is off because airplane mode is enabled
+     */
+    public static final int CELL_OFF_FLAG = 0;
+    public static final int CELL_ON_FLAG = 1;
+    public static final int CELL_OFF_DUE_TO_AIRPLANE_MODE_FLAG = 2;
+}
diff --git a/com/android/internal/telephony/PhoneFactory.java b/com/android/internal/telephony/PhoneFactory.java
new file mode 100644
index 0000000..d2a65d9
--- /dev/null
+++ b/com/android/internal/telephony/PhoneFactory.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.LocalServerSocket;
+import android.os.Looper;
+import android.os.ServiceManager;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.LocalLog;
+
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.dataconnection.TelephonyNetworkFactory;
+import com.android.internal.telephony.euicc.EuiccController;
+import com.android.internal.telephony.ims.ImsResolver;
+import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.imsphone.ImsPhoneFactory;
+import com.android.internal.telephony.sip.SipPhone;
+import com.android.internal.telephony.sip.SipPhoneFactory;
+import com.android.internal.telephony.uicc.IccCardProxy;
+import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.util.NotificationChannelController;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@hide}
+ */
+public class PhoneFactory {
+    static final String LOG_TAG = "PhoneFactory";
+    static final int SOCKET_OPEN_RETRY_MILLIS = 2 * 1000;
+    static final int SOCKET_OPEN_MAX_RETRY = 3;
+    static final boolean DBG = false;
+
+    //***** Class Variables
+
+    // lock sLockProxyPhones protects both sPhones and sPhone
+    final static Object sLockProxyPhones = new Object();
+    static private Phone[] sPhones = null;
+    static private Phone sPhone = null;
+
+    static private CommandsInterface[] sCommandsInterfaces = null;
+
+    static private ProxyController sProxyController;
+    static private UiccController sUiccController;
+    private static IntentBroadcaster sIntentBroadcaster;
+    private static @Nullable EuiccController sEuiccController;
+
+    static private CommandsInterface sCommandsInterface = null;
+    static private SubscriptionInfoUpdater sSubInfoRecordUpdater = null;
+
+    static private boolean sMadeDefaults = false;
+    static private PhoneNotifier sPhoneNotifier;
+    static private Context sContext;
+    static private PhoneSwitcher sPhoneSwitcher;
+    static private SubscriptionMonitor sSubscriptionMonitor;
+    static private TelephonyNetworkFactory[] sTelephonyNetworkFactories;
+    static private ImsResolver sImsResolver;
+    static private NotificationChannelController sNotificationChannelController;
+
+    static private final HashMap<String, LocalLog>sLocalLogs = new HashMap<String, LocalLog>();
+
+    // TODO - make this a dynamic property read from the modem
+    public static final int MAX_ACTIVE_PHONES = 1;
+
+    //***** Class Methods
+
+    public static void makeDefaultPhones(Context context) {
+        makeDefaultPhone(context);
+    }
+
+    /**
+     * FIXME replace this with some other way of making these
+     * instances
+     */
+    public static void makeDefaultPhone(Context context) {
+        synchronized (sLockProxyPhones) {
+            if (!sMadeDefaults) {
+                sContext = context;
+                // create the telephony device controller.
+                TelephonyDevController.create();
+
+                int retryCount = 0;
+                for(;;) {
+                    boolean hasException = false;
+                    retryCount ++;
+
+                    try {
+                        // use UNIX domain socket to
+                        // prevent subsequent initialization
+                        new LocalServerSocket("com.android.internal.telephony");
+                    } catch (java.io.IOException ex) {
+                        hasException = true;
+                    }
+
+                    if ( !hasException ) {
+                        break;
+                    } else if (retryCount > SOCKET_OPEN_MAX_RETRY) {
+                        throw new RuntimeException("PhoneFactory probably already running");
+                    } else {
+                        try {
+                            Thread.sleep(SOCKET_OPEN_RETRY_MILLIS);
+                        } catch (InterruptedException er) {
+                        }
+                    }
+                }
+
+                sPhoneNotifier = new DefaultPhoneNotifier();
+
+                int cdmaSubscription = CdmaSubscriptionSourceManager.getDefault(context);
+                Rlog.i(LOG_TAG, "Cdma Subscription set to " + cdmaSubscription);
+
+                if (context.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_TELEPHONY_EUICC)) {
+                    sEuiccController = EuiccController.init(context);
+                }
+
+                /* In case of multi SIM mode two instances of Phone, RIL are created,
+                   where as in single SIM mode only instance. isMultiSimEnabled() function checks
+                   whether it is single SIM or multi SIM mode */
+                int numPhones = TelephonyManager.getDefault().getPhoneCount();
+                // Start ImsResolver and bind to ImsServices.
+                String defaultImsPackage = sContext.getResources().getString(
+                        com.android.internal.R.string.config_ims_package);
+                Rlog.i(LOG_TAG, "ImsResolver: defaultImsPackage: " + defaultImsPackage);
+                sImsResolver = new ImsResolver(sContext, defaultImsPackage, numPhones);
+                sImsResolver.populateCacheAndStartBind();
+
+                int[] networkModes = new int[numPhones];
+                sPhones = new Phone[numPhones];
+                sCommandsInterfaces = new RIL[numPhones];
+                sTelephonyNetworkFactories = new TelephonyNetworkFactory[numPhones];
+
+                for (int i = 0; i < numPhones; i++) {
+                    // reads the system properties and makes commandsinterface
+                    // Get preferred network type.
+                    networkModes[i] = RILConstants.PREFERRED_NETWORK_MODE;
+
+                    Rlog.i(LOG_TAG, "Network Mode set to " + Integer.toString(networkModes[i]));
+                    sCommandsInterfaces[i] = new RIL(context, networkModes[i],
+                            cdmaSubscription, i);
+                }
+                Rlog.i(LOG_TAG, "Creating SubscriptionController");
+                SubscriptionController.init(context, sCommandsInterfaces);
+
+                // Instantiate UiccController so that all other classes can just
+                // call getInstance()
+                sUiccController = UiccController.make(context, sCommandsInterfaces);
+
+                for (int i = 0; i < numPhones; i++) {
+                    Phone phone = null;
+                    int phoneType = TelephonyManager.getPhoneType(networkModes[i]);
+                    if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+                        phone = new GsmCdmaPhone(context,
+                                sCommandsInterfaces[i], sPhoneNotifier, i,
+                                PhoneConstants.PHONE_TYPE_GSM,
+                                TelephonyComponentFactory.getInstance());
+                    } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+                        phone = new GsmCdmaPhone(context,
+                                sCommandsInterfaces[i], sPhoneNotifier, i,
+                                PhoneConstants.PHONE_TYPE_CDMA_LTE,
+                                TelephonyComponentFactory.getInstance());
+                    }
+                    Rlog.i(LOG_TAG, "Creating Phone with type = " + phoneType + " sub = " + i);
+
+                    sPhones[i] = phone;
+                }
+
+                // Set the default phone in base class.
+                // FIXME: This is a first best guess at what the defaults will be. It
+                // FIXME: needs to be done in a more controlled manner in the future.
+                sPhone = sPhones[0];
+                sCommandsInterface = sCommandsInterfaces[0];
+
+                // Ensure that we have a default SMS app. Requesting the app with
+                // updateIfNeeded set to true is enough to configure a default SMS app.
+                ComponentName componentName =
+                        SmsApplication.getDefaultSmsApplication(context, true /* updateIfNeeded */);
+                String packageName = "NONE";
+                if (componentName != null) {
+                    packageName = componentName.getPackageName();
+                }
+                Rlog.i(LOG_TAG, "defaultSmsApplication: " + packageName);
+
+                // Set up monitor to watch for changes to SMS packages
+                SmsApplication.initSmsPackageMonitor(context);
+
+                sMadeDefaults = true;
+
+                Rlog.i(LOG_TAG, "Creating SubInfoRecordUpdater ");
+                sSubInfoRecordUpdater = new SubscriptionInfoUpdater(
+                        BackgroundThread.get().getLooper(), context, sPhones, sCommandsInterfaces);
+                SubscriptionController.getInstance().updatePhonesAvailability(sPhones);
+
+                // Start monitoring after defaults have been made.
+                // Default phone must be ready before ImsPhone is created because ImsService might
+                // need it when it is being opened. This should initialize multiple ImsPhones for
+                // ImsResolver implementations of ImsService.
+                for (int i = 0; i < numPhones; i++) {
+                    sPhones[i].startMonitoringImsService();
+                }
+
+                ITelephonyRegistry tr = ITelephonyRegistry.Stub.asInterface(
+                        ServiceManager.getService("telephony.registry"));
+                SubscriptionController sc = SubscriptionController.getInstance();
+
+                sSubscriptionMonitor = new SubscriptionMonitor(tr, sContext, sc, numPhones);
+
+                sPhoneSwitcher = new PhoneSwitcher(MAX_ACTIVE_PHONES, numPhones,
+                        sContext, sc, Looper.myLooper(), tr, sCommandsInterfaces,
+                        sPhones);
+
+                sProxyController = ProxyController.getInstance(context, sPhones,
+                        sUiccController, sCommandsInterfaces, sPhoneSwitcher);
+
+                sIntentBroadcaster = IntentBroadcaster.getInstance(context);
+
+                sNotificationChannelController = new NotificationChannelController(context);
+
+                sTelephonyNetworkFactories = new TelephonyNetworkFactory[numPhones];
+                for (int i = 0; i < numPhones; i++) {
+                    sTelephonyNetworkFactories[i] = new TelephonyNetworkFactory(
+                            sPhoneSwitcher, sc, sSubscriptionMonitor, Looper.myLooper(),
+                            sContext, i, sPhones[i].mDcTracker);
+                }
+            }
+        }
+    }
+
+    public static Phone getDefaultPhone() {
+        synchronized (sLockProxyPhones) {
+            if (!sMadeDefaults) {
+                throw new IllegalStateException("Default phones haven't been made yet!");
+            }
+            return sPhone;
+        }
+    }
+
+    public static Phone getPhone(int phoneId) {
+        Phone phone;
+        String dbgInfo = "";
+
+        synchronized (sLockProxyPhones) {
+            if (!sMadeDefaults) {
+                throw new IllegalStateException("Default phones haven't been made yet!");
+                // CAF_MSIM FIXME need to introduce default phone id ?
+            } else if (phoneId == SubscriptionManager.DEFAULT_PHONE_INDEX) {
+                if (DBG) dbgInfo = "phoneId == DEFAULT_PHONE_ID return sPhone";
+                phone = sPhone;
+            } else {
+                if (DBG) dbgInfo = "phoneId != DEFAULT_PHONE_ID return sPhones[phoneId]";
+                phone = (((phoneId >= 0)
+                                && (phoneId < TelephonyManager.getDefault().getPhoneCount()))
+                        ? sPhones[phoneId] : null);
+            }
+            if (DBG) {
+                Rlog.d(LOG_TAG, "getPhone:- " + dbgInfo + " phoneId=" + phoneId +
+                        " phone=" + phone);
+            }
+            return phone;
+        }
+    }
+
+    public static Phone[] getPhones() {
+        synchronized (sLockProxyPhones) {
+            if (!sMadeDefaults) {
+                throw new IllegalStateException("Default phones haven't been made yet!");
+            }
+            return sPhones;
+        }
+    }
+
+    public static ImsResolver getImsResolver() {
+        return sImsResolver;
+    }
+
+    /**
+     * Makes a {@link SipPhone} object.
+     * @param sipUri the local SIP URI the phone runs on
+     * @return the {@code SipPhone} object or null if the SIP URI is not valid
+     */
+    public static SipPhone makeSipPhone(String sipUri) {
+        return SipPhoneFactory.makePhone(sipUri, sContext, sPhoneNotifier);
+    }
+
+    /**
+     * Returns the preferred network type that should be set in the modem.
+     *
+     * @param context The current {@link Context}.
+     * @return the preferred network mode that should be set.
+     */
+    // TODO: Fix when we "properly" have TelephonyDevController/SubscriptionController ..
+    public static int calculatePreferredNetworkType(Context context, int phoneSubId) {
+        int networkType = android.provider.Settings.Global.getInt(context.getContentResolver(),
+                android.provider.Settings.Global.PREFERRED_NETWORK_MODE + phoneSubId,
+                RILConstants.PREFERRED_NETWORK_MODE);
+        Rlog.d(LOG_TAG, "calculatePreferredNetworkType: phoneSubId = " + phoneSubId +
+                " networkType = " + networkType);
+        return networkType;
+    }
+
+    /* Gets the default subscription */
+    public static int getDefaultSubscription() {
+        return SubscriptionController.getInstance().getDefaultSubId();
+    }
+
+    /* Returns User SMS Prompt property,  enabled or not */
+    public static boolean isSMSPromptEnabled() {
+        boolean prompt = false;
+        int value = 0;
+        try {
+            value = Settings.Global.getInt(sContext.getContentResolver(),
+                    Settings.Global.MULTI_SIM_SMS_PROMPT);
+        } catch (SettingNotFoundException snfe) {
+            Rlog.e(LOG_TAG, "Settings Exception Reading Dual Sim SMS Prompt Values");
+        }
+        prompt = (value == 0) ? false : true ;
+        Rlog.d(LOG_TAG, "SMS Prompt option:" + prompt);
+
+       return prompt;
+    }
+
+    /**
+     * Makes a {@link ImsPhone} object.
+     * @return the {@code ImsPhone} object or null if the exception occured
+     */
+    public static Phone makeImsPhone(PhoneNotifier phoneNotifier, Phone defaultPhone) {
+        return ImsPhoneFactory.makePhone(sContext, phoneNotifier, defaultPhone);
+    }
+
+    /**
+     * Request a refresh of the embedded subscription list.
+     *
+     * @param callback Optional callback to execute after the refresh completes. Must terminate
+     *     quickly as it will be called from SubscriptionInfoUpdater's handler thread.
+     */
+    public static void requestEmbeddedSubscriptionInfoListRefresh(@Nullable Runnable callback) {
+        sSubInfoRecordUpdater.requestEmbeddedSubscriptionInfoListRefresh(callback);
+    }
+
+    /**
+     * Adds a local log category.
+     *
+     * Only used within the telephony process.  Use localLog to add log entries.
+     *
+     * TODO - is there a better way to do this?  Think about design when we have a minute.
+     *
+     * @param key the name of the category - will be the header in the service dump.
+     * @param size the number of lines to maintain in this category
+     */
+    public static void addLocalLog(String key, int size) {
+        synchronized(sLocalLogs) {
+            if (sLocalLogs.containsKey(key)) {
+                throw new IllegalArgumentException("key " + key + " already present");
+            }
+            sLocalLogs.put(key, new LocalLog(size));
+        }
+    }
+
+    /**
+     * Add a line to the named Local Log.
+     *
+     * This will appear in the TelephonyDebugService dump.
+     *
+     * @param key the name of the log category to put this in.  Must be created
+     *            via addLocalLog.
+     * @param log the string to add to the log.
+     */
+    public static void localLog(String key, String log) {
+        synchronized(sLocalLogs) {
+            if (sLocalLogs.containsKey(key) == false) {
+                throw new IllegalArgumentException("key " + key + " not found");
+            }
+            sLocalLogs.get(key).log(log);
+        }
+    }
+
+    public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printwriter, "  ");
+        pw.println("PhoneFactory:");
+        pw.println(" sMadeDefaults=" + sMadeDefaults);
+
+        sPhoneSwitcher.dump(fd, pw, args);
+        pw.println();
+
+        Phone[] phones = (Phone[])PhoneFactory.getPhones();
+        for (int i = 0; i < phones.length; i++) {
+            pw.increaseIndent();
+            Phone phone = phones[i];
+
+            try {
+                phone.dump(fd, pw, args);
+            } catch (Exception e) {
+                pw.println("Telephony DebugService: Could not get Phone[" + i + "] e=" + e);
+                continue;
+            }
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+
+            sTelephonyNetworkFactories[i].dump(fd, pw, args);
+
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+
+            try {
+                ((IccCardProxy)phone.getIccCard()).dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            pw.flush();
+            pw.decreaseIndent();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        pw.println("SubscriptionMonitor:");
+        pw.increaseIndent();
+        try {
+            sSubscriptionMonitor.dump(fd, pw, args);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.decreaseIndent();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        pw.println("UiccController:");
+        pw.increaseIndent();
+        try {
+            sUiccController.dump(fd, pw, args);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.flush();
+        pw.decreaseIndent();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        if (sEuiccController != null) {
+            pw.println("EuiccController:");
+            pw.increaseIndent();
+            try {
+                sEuiccController.dump(fd, pw, args);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            pw.flush();
+            pw.decreaseIndent();
+            pw.println("++++++++++++++++++++++++++++++++");
+        }
+
+        pw.println("SubscriptionController:");
+        pw.increaseIndent();
+        try {
+            SubscriptionController.getInstance().dump(fd, pw, args);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.flush();
+        pw.decreaseIndent();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        pw.println("SubInfoRecordUpdater:");
+        pw.increaseIndent();
+        try {
+            sSubInfoRecordUpdater.dump(fd, pw, args);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.flush();
+        pw.decreaseIndent();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        pw.println("LocalLogs:");
+        pw.increaseIndent();
+        synchronized (sLocalLogs) {
+            for (String key : sLocalLogs.keySet()) {
+                pw.println(key);
+                pw.increaseIndent();
+                sLocalLogs.get(key).dump(fd, pw, args);
+                pw.decreaseIndent();
+            }
+            pw.flush();
+        }
+        pw.decreaseIndent();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        pw.println("SharedPreferences:");
+        pw.increaseIndent();
+        try {
+            if (sContext != null) {
+                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(sContext);
+                Map spValues = sp.getAll();
+                for (Object key : spValues.keySet()) {
+                    pw.println(key + " : " + spValues.get(key));
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        pw.flush();
+        pw.decreaseIndent();
+    }
+}
diff --git a/com/android/internal/telephony/PhoneInternalInterface.java b/com/android/internal/telephony/PhoneInternalInterface.java
new file mode 100644
index 0000000..7dbd8d3
--- /dev/null
+++ b/com/android/internal/telephony/PhoneInternalInterface.java
@@ -0,0 +1,850 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.os.WorkSource;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellLocation;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+import android.telephony.ServiceState;
+
+import com.android.internal.telephony.PhoneConstants.*; // ????
+
+import java.util.List;
+
+/**
+ * Internal interface used to control the phone; SDK developers cannot
+ * obtain this interface.
+ *
+ * {@hide}
+ *
+ */
+public interface PhoneInternalInterface {
+
+    /** used to enable additional debug messages */
+    static final boolean DEBUG_PHONE = true;
+
+    public enum DataActivityState {
+        /**
+         * The state of a data activity.
+         * <ul>
+         * <li>NONE = No traffic</li>
+         * <li>DATAIN = Receiving IP ppp traffic</li>
+         * <li>DATAOUT = Sending IP ppp traffic</li>
+         * <li>DATAINANDOUT = Both receiving and sending IP ppp traffic</li>
+         * <li>DORMANT = The data connection is still active,
+                                     but physical link is down</li>
+         * </ul>
+         */
+        NONE, DATAIN, DATAOUT, DATAINANDOUT, DORMANT;
+    }
+
+    enum SuppService {
+      UNKNOWN, SWITCH, SEPARATE, TRANSFER, CONFERENCE, REJECT, HANGUP, RESUME, HOLD;
+    }
+
+    // "Features" accessible through the connectivity manager
+    static final String FEATURE_ENABLE_MMS = "enableMMS";
+    static final String FEATURE_ENABLE_SUPL = "enableSUPL";
+    static final String FEATURE_ENABLE_DUN = "enableDUN";
+    static final String FEATURE_ENABLE_HIPRI = "enableHIPRI";
+    static final String FEATURE_ENABLE_DUN_ALWAYS = "enableDUNAlways";
+    static final String FEATURE_ENABLE_FOTA = "enableFOTA";
+    static final String FEATURE_ENABLE_IMS = "enableIMS";
+    static final String FEATURE_ENABLE_CBS = "enableCBS";
+    static final String FEATURE_ENABLE_EMERGENCY = "enableEmergency";
+
+    /**
+     * Optional reasons for disconnect and connect
+     */
+    static final String REASON_ROAMING_ON = "roamingOn";
+    static final String REASON_ROAMING_OFF = "roamingOff";
+    static final String REASON_DATA_DISABLED = "dataDisabled";
+    static final String REASON_DATA_ENABLED = "dataEnabled";
+    static final String REASON_DATA_ATTACHED = "dataAttached";
+    static final String REASON_DATA_DETACHED = "dataDetached";
+    static final String REASON_CDMA_DATA_ATTACHED = "cdmaDataAttached";
+    static final String REASON_CDMA_DATA_DETACHED = "cdmaDataDetached";
+    static final String REASON_APN_CHANGED = "apnChanged";
+    static final String REASON_APN_SWITCHED = "apnSwitched";
+    static final String REASON_APN_FAILED = "apnFailed";
+    static final String REASON_RESTORE_DEFAULT_APN = "restoreDefaultApn";
+    static final String REASON_RADIO_TURNED_OFF = "radioTurnedOff";
+    static final String REASON_PDP_RESET = "pdpReset";
+    static final String REASON_VOICE_CALL_ENDED = "2GVoiceCallEnded";
+    static final String REASON_VOICE_CALL_STARTED = "2GVoiceCallStarted";
+    static final String REASON_PS_RESTRICT_ENABLED = "psRestrictEnabled";
+    static final String REASON_PS_RESTRICT_DISABLED = "psRestrictDisabled";
+    static final String REASON_SIM_LOADED = "simLoaded";
+    static final String REASON_NW_TYPE_CHANGED = "nwTypeChanged";
+    static final String REASON_DATA_DEPENDENCY_MET = "dependencyMet";
+    static final String REASON_DATA_DEPENDENCY_UNMET = "dependencyUnmet";
+    static final String REASON_LOST_DATA_CONNECTION = "lostDataConnection";
+    static final String REASON_CONNECTED = "connected";
+    static final String REASON_SINGLE_PDN_ARBITRATION = "SinglePdnArbitration";
+    static final String REASON_DATA_SPECIFIC_DISABLED = "specificDisabled";
+    static final String REASON_SIM_NOT_READY = "simNotReady";
+    static final String REASON_IWLAN_AVAILABLE = "iwlanAvailable";
+    static final String REASON_CARRIER_CHANGE = "carrierChange";
+    static final String REASON_CARRIER_ACTION_DISABLE_METERED_APN =
+            "carrierActionDisableMeteredApn";
+    static final String REASON_CSS_INDICATOR_CHANGED = "cssIndicatorChanged";
+
+    // Used for band mode selection methods
+    static final int BM_UNSPECIFIED = RILConstants.BAND_MODE_UNSPECIFIED; // automatic
+    static final int BM_EURO_BAND   = RILConstants.BAND_MODE_EURO;
+    static final int BM_US_BAND     = RILConstants.BAND_MODE_USA;
+    static final int BM_JPN_BAND    = RILConstants.BAND_MODE_JPN;
+    static final int BM_AUS_BAND    = RILConstants.BAND_MODE_AUS;
+    static final int BM_AUS2_BAND   = RILConstants.BAND_MODE_AUS_2;
+    static final int BM_CELL_800    = RILConstants.BAND_MODE_CELL_800;
+    static final int BM_PCS         = RILConstants.BAND_MODE_PCS;
+    static final int BM_JTACS       = RILConstants.BAND_MODE_JTACS;
+    static final int BM_KOREA_PCS   = RILConstants.BAND_MODE_KOREA_PCS;
+    static final int BM_4_450M      = RILConstants.BAND_MODE_5_450M;
+    static final int BM_IMT2000     = RILConstants.BAND_MODE_IMT2000;
+    static final int BM_7_700M2     = RILConstants.BAND_MODE_7_700M_2;
+    static final int BM_8_1800M     = RILConstants.BAND_MODE_8_1800M;
+    static final int BM_9_900M      = RILConstants.BAND_MODE_9_900M;
+    static final int BM_10_800M_2   = RILConstants.BAND_MODE_10_800M_2;
+    static final int BM_EURO_PAMR   = RILConstants.BAND_MODE_EURO_PAMR_400M;
+    static final int BM_AWS         = RILConstants.BAND_MODE_AWS;
+    static final int BM_US_2500M    = RILConstants.BAND_MODE_USA_2500M;
+    static final int BM_NUM_BAND_MODES = 19; //Total number of band modes
+
+    // Used for preferred network type
+    // Note NT_* substitute RILConstants.NETWORK_MODE_* above the Phone
+    int NT_MODE_WCDMA_PREF   = RILConstants.NETWORK_MODE_WCDMA_PREF;
+    int NT_MODE_GSM_ONLY     = RILConstants.NETWORK_MODE_GSM_ONLY;
+    int NT_MODE_WCDMA_ONLY   = RILConstants.NETWORK_MODE_WCDMA_ONLY;
+    int NT_MODE_GSM_UMTS     = RILConstants.NETWORK_MODE_GSM_UMTS;
+
+    int NT_MODE_CDMA         = RILConstants.NETWORK_MODE_CDMA;
+
+    int NT_MODE_CDMA_NO_EVDO = RILConstants.NETWORK_MODE_CDMA_NO_EVDO;
+    int NT_MODE_EVDO_NO_CDMA = RILConstants.NETWORK_MODE_EVDO_NO_CDMA;
+    int NT_MODE_GLOBAL       = RILConstants.NETWORK_MODE_GLOBAL;
+
+    int NT_MODE_LTE_CDMA_AND_EVDO        = RILConstants.NETWORK_MODE_LTE_CDMA_EVDO;
+    int NT_MODE_LTE_GSM_WCDMA            = RILConstants.NETWORK_MODE_LTE_GSM_WCDMA;
+    int NT_MODE_LTE_CDMA_EVDO_GSM_WCDMA  = RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA;
+    int NT_MODE_LTE_ONLY                 = RILConstants.NETWORK_MODE_LTE_ONLY;
+    int NT_MODE_LTE_WCDMA                = RILConstants.NETWORK_MODE_LTE_WCDMA;
+
+    int NT_MODE_TDSCDMA_ONLY            = RILConstants.NETWORK_MODE_TDSCDMA_ONLY;
+    int NT_MODE_TDSCDMA_WCDMA           = RILConstants.NETWORK_MODE_TDSCDMA_WCDMA;
+    int NT_MODE_LTE_TDSCDMA             = RILConstants.NETWORK_MODE_LTE_TDSCDMA;
+    int NT_MODE_TDSCDMA_GSM             = RILConstants.NETWORK_MODE_TDSCDMA_GSM;
+    int NT_MODE_LTE_TDSCDMA_GSM         = RILConstants.NETWORK_MODE_LTE_TDSCDMA_GSM;
+    int NT_MODE_TDSCDMA_GSM_WCDMA       = RILConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA;
+    int NT_MODE_LTE_TDSCDMA_WCDMA       = RILConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA;
+    int NT_MODE_LTE_TDSCDMA_GSM_WCDMA   = RILConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA;
+    int NT_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = RILConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
+    int NT_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = RILConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
+
+    int PREFERRED_NT_MODE                = RILConstants.PREFERRED_NETWORK_MODE;
+
+    // Used for CDMA roaming mode
+    // Home Networks only, as defined in PRL
+    static final int CDMA_RM_HOME        = CarrierConfigManager.CDMA_ROAMING_MODE_HOME;
+    // Roaming an Affiliated networks, as defined in PRL
+    static final int CDMA_RM_AFFILIATED  = CarrierConfigManager.CDMA_ROAMING_MODE_AFFILIATED;
+    // Roaming on Any Network, as defined in PRL
+    static final int CDMA_RM_ANY         = CarrierConfigManager.CDMA_ROAMING_MODE_ANY;
+
+    // Used for CDMA subscription mode
+    static final int CDMA_SUBSCRIPTION_UNKNOWN  =-1; // Unknown
+    static final int CDMA_SUBSCRIPTION_RUIM_SIM = 0; // RUIM/SIM (default)
+    static final int CDMA_SUBSCRIPTION_NV       = 1; // NV -> non-volatile memory
+
+    static final int PREFERRED_CDMA_SUBSCRIPTION = CDMA_SUBSCRIPTION_RUIM_SIM;
+
+    static final int TTY_MODE_OFF = 0;
+    static final int TTY_MODE_FULL = 1;
+    static final int TTY_MODE_HCO = 2;
+    static final int TTY_MODE_VCO = 3;
+
+     /**
+     * CDMA OTA PROVISION STATUS, the same as RIL_CDMA_OTA_Status in ril.h
+     */
+
+    public static final int CDMA_OTA_PROVISION_STATUS_SPL_UNLOCKED = 0;
+    public static final int CDMA_OTA_PROVISION_STATUS_SPC_RETRIES_EXCEEDED = 1;
+    public static final int CDMA_OTA_PROVISION_STATUS_A_KEY_EXCHANGED = 2;
+    public static final int CDMA_OTA_PROVISION_STATUS_SSD_UPDATED = 3;
+    public static final int CDMA_OTA_PROVISION_STATUS_NAM_DOWNLOADED = 4;
+    public static final int CDMA_OTA_PROVISION_STATUS_MDN_DOWNLOADED = 5;
+    public static final int CDMA_OTA_PROVISION_STATUS_IMSI_DOWNLOADED = 6;
+    public static final int CDMA_OTA_PROVISION_STATUS_PRL_DOWNLOADED = 7;
+    public static final int CDMA_OTA_PROVISION_STATUS_COMMITTED = 8;
+    public static final int CDMA_OTA_PROVISION_STATUS_OTAPA_STARTED = 9;
+    public static final int CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED = 10;
+    public static final int CDMA_OTA_PROVISION_STATUS_OTAPA_ABORTED = 11;
+
+
+    /**
+     * Get the current ServiceState. Use
+     * <code>registerForServiceStateChanged</code> to be informed of
+     * updates.
+     */
+    ServiceState getServiceState();
+
+    /**
+     * Get the current CellLocation.
+     * @param workSource calling WorkSource
+     */
+    CellLocation getCellLocation(WorkSource workSource);
+
+    /**
+     * Get the current DataState. No change notification exists at this
+     * interface -- use
+     * {@link android.telephony.PhoneStateListener} instead.
+     * @param apnType specify for which apn to get connection state info.
+     */
+    DataState getDataConnectionState(String apnType);
+
+    /**
+     * Get the current DataActivityState. No change notification exists at this
+     * interface -- use
+     * {@link android.telephony.TelephonyManager} instead.
+     */
+    DataActivityState getDataActivityState();
+
+    /**
+     * Returns a list of MMI codes that are pending. (They have initiated
+     * but have not yet completed).
+     * Presently there is only ever one.
+     * Use <code>registerForMmiInitiate</code>
+     * and <code>registerForMmiComplete</code> for change notification.
+     */
+    public List<? extends MmiCode> getPendingMmiCodes();
+
+    /**
+     * Sends user response to a USSD REQUEST message.  An MmiCode instance
+     * representing this response is sent to handlers registered with
+     * registerForMmiInitiate.
+     *
+     * @param ussdMessge    Message to send in the response.
+     */
+    public void sendUssdResponse(String ussdMessge);
+
+    /**
+     * Register for Supplementary Service notifications from the network.
+     * Message.obj will contain an AsyncResult.
+     * AsyncResult.result will be a SuppServiceNotification instance.
+     *
+     * @param h Handler that receives the notification message.
+     * @param what User-defined message code.
+     * @param obj User object.
+     */
+    void registerForSuppServiceNotification(Handler h, int what, Object obj);
+
+    /**
+     * Unregisters for Supplementary Service notifications.
+     * Extraneous calls are tolerated silently
+     *
+     * @param h Handler to be removed from the registrant list.
+     */
+    void unregisterForSuppServiceNotification(Handler h);
+
+    /**
+     * Answers a ringing or waiting call. Active calls, if any, go on hold.
+     * Answering occurs asynchronously, and final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @param videoState The video state in which to answer the call.
+     * @exception CallStateException when no call is ringing or waiting
+     */
+    void acceptCall(int videoState) throws CallStateException;
+
+    /**
+     * Reject (ignore) a ringing call. In GSM, this means UDUB
+     * (User Determined User Busy). Reject occurs asynchronously,
+     * and final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException when no call is ringing or waiting
+     */
+    void rejectCall() throws CallStateException;
+
+    /**
+     * Places any active calls on hold, and makes any held calls
+     *  active. Switch occurs asynchronously and may fail.
+     * Final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException if a call is ringing, waiting, or
+     * dialing/alerting. In these cases, this operation may not be performed.
+     */
+    void switchHoldingAndActive() throws CallStateException;
+
+    /**
+     * Whether or not the phone can conference in the current phone
+     * state--that is, one call holding and one call active.
+     * @return true if the phone can conference; false otherwise.
+     */
+    boolean canConference();
+
+    /**
+     * Conferences holding and active. Conference occurs asynchronously
+     * and may fail. Final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException if canConference() would return false.
+     * In these cases, this operation may not be performed.
+     */
+    void conference() throws CallStateException;
+
+    /**
+     * Whether or not the phone can do explicit call transfer in the current
+     * phone state--that is, one call holding and one call active.
+     * @return true if the phone can do explicit call transfer; false otherwise.
+     */
+    boolean canTransfer();
+
+    /**
+     * Connects the two calls and disconnects the subscriber from both calls
+     * Explicit Call Transfer occurs asynchronously
+     * and may fail. Final notification occurs via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     *
+     * @exception CallStateException if canTransfer() would return false.
+     * In these cases, this operation may not be performed.
+     */
+    void explicitCallTransfer() throws CallStateException;
+
+    /**
+     * Clears all DISCONNECTED connections from Call connection lists.
+     * Calls that were in the DISCONNECTED state become idle. This occurs
+     * synchronously.
+     */
+    void clearDisconnected();
+
+    /**
+     * Gets the foreground call object, which represents all connections that
+     * are dialing or active (all connections
+     * that have their audio path connected).<p>
+     *
+     * The foreground call is a singleton object. It is constant for the life
+     * of this phone. It is never null.<p>
+     *
+     * The foreground call will only ever be in one of these states:
+     * IDLE, ACTIVE, DIALING, ALERTING, or DISCONNECTED.
+     *
+     * State change notification is available via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     */
+    Call getForegroundCall();
+
+    /**
+     * Gets the background call object, which represents all connections that
+     * are holding (all connections that have been accepted or connected, but
+     * do not have their audio path connected). <p>
+     *
+     * The background call is a singleton object. It is constant for the life
+     * of this phone object . It is never null.<p>
+     *
+     * The background call will only ever be in one of these states:
+     * IDLE, HOLDING or DISCONNECTED.
+     *
+     * State change notification is available via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     */
+    Call getBackgroundCall();
+
+    /**
+     * Gets the ringing call object, which represents an incoming
+     * connection (if present) that is pending answer/accept. (This connection
+     * may be RINGING or WAITING, and there may be only one.)<p>
+
+     * The ringing call is a singleton object. It is constant for the life
+     * of this phone. It is never null.<p>
+     *
+     * The ringing call will only ever be in one of these states:
+     * IDLE, INCOMING, WAITING or DISCONNECTED.
+     *
+     * State change notification is available via
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}.
+     */
+    Call getRingingCall();
+
+    /**
+     * Initiate a new voice connection. This happens asynchronously, so you
+     * cannot assume the audio path is connected (or a call index has been
+     * assigned) until PhoneStateChanged notification has occurred.
+     *
+     * @param dialString The dial string.
+     * @param videoState The desired video state for the connection.
+     * @exception CallStateException if a new outgoing call is not currently
+     * possible because no more call slots exist or a call exists that is
+     * dialing, alerting, ringing, or waiting.  Other errors are
+     * handled asynchronously.
+     */
+    Connection dial(String dialString, int videoState) throws CallStateException;
+
+    /**
+     * Initiate a new voice connection with supplementary User to User
+     * Information. This happens asynchronously, so you cannot assume the audio
+     * path is connected (or a call index has been assigned) until
+     * PhoneStateChanged notification has occurred.
+     *
+     * NOTE: If adding another parameter, consider creating a DialArgs parameter instead to
+     * encapsulate all dial arguments and decrease scaffolding headache.
+     *
+     * @param dialString The dial string.
+     * @param uusInfo The UUSInfo.
+     * @param videoState The desired video state for the connection.
+     * @param intentExtras The extras from the original CALL intent.
+     * @exception CallStateException if a new outgoing call is not currently
+     *                possible because no more call slots exist or a call exists
+     *                that is dialing, alerting, ringing, or waiting. Other
+     *                errors are handled asynchronously.
+     */
+    Connection dial(String dialString, UUSInfo uusInfo, int videoState, Bundle intentExtras)
+            throws CallStateException;
+
+    /**
+     * Handles PIN MMI commands (PIN/PIN2/PUK/PUK2), which are initiated
+     * without SEND (so <code>dial</code> is not appropriate).
+     *
+     * @param dialString the MMI command to be executed.
+     * @return true if MMI command is executed.
+     */
+    boolean handlePinMmi(String dialString);
+
+    /**
+     * Handles USSD commands
+     *
+     * @param ussdRequest the USSD command to be executed.
+     * @param wrappedCallback receives the callback result.
+     */
+    boolean handleUssdRequest(String ussdRequest, ResultReceiver wrappedCallback)
+            throws CallStateException;
+
+    /**
+     * Handles in-call MMI commands. While in a call, or while receiving a
+     * call, use this to execute MMI commands.
+     * see 3GPP 20.030, section 6.5.5.1 for specs on the allowed MMI commands.
+     *
+     * @param command the MMI command to be executed.
+     * @return true if the MMI command is executed.
+     * @throws CallStateException
+     */
+    boolean handleInCallMmiCommands(String command) throws CallStateException;
+
+    /**
+     * Play a DTMF tone on the active call. Ignored if there is no active call.
+     * @param c should be one of 0-9, '*' or '#'. Other values will be
+     * silently ignored.
+     */
+    void sendDtmf(char c);
+
+    /**
+     * Start to paly a DTMF tone on the active call. Ignored if there is no active call
+     * or there is a playing DTMF tone.
+     * @param c should be one of 0-9, '*' or '#'. Other values will be
+     * silently ignored.
+     */
+    void startDtmf(char c);
+
+    /**
+     * Stop the playing DTMF tone. Ignored if there is no playing DTMF
+     * tone or no active call.
+     */
+    void stopDtmf();
+
+    /**
+     * Sets the radio power on/off state (off is sometimes
+     * called "airplane mode"). Current state can be gotten via
+     * {@link #getServiceState()}.{@link
+     * android.telephony.ServiceState#getState() getState()}.
+     * <strong>Note: </strong>This request is asynchronous.
+     * getServiceState().getState() will not change immediately after this call.
+     * registerForServiceStateChanged() to find out when the
+     * request is complete.
+     *
+     * @param power true means "on", false means "off".
+     */
+    void setRadioPower(boolean power);
+
+    /**
+     * Get the line 1 phone number (MSISDN). For CDMA phones, the MDN is returned
+     * and {@link #getMsisdn()} will return the MSISDN on CDMA/LTE phones.<p>
+     *
+     * @return phone number. May return null if not
+     * available or the SIM is not ready
+     */
+    String getLine1Number();
+
+    /**
+     * Returns the alpha tag associated with the msisdn number.
+     * If there is no alpha tag associated or the record is not yet available,
+     * returns a default localized string. <p>
+     */
+    String getLine1AlphaTag();
+
+    /**
+     * Sets the MSISDN phone number in the SIM card.
+     *
+     * @param alphaTag the alpha tag associated with the MSISDN phone number
+     *        (see getMsisdnAlphaTag)
+     * @param number the new MSISDN phone number to be set on the SIM.
+     * @param onComplete a callback message when the action is completed.
+     *
+     * @return true if req is sent, false otherwise. If req is not sent there will be no response,
+     * that is, onComplete will never be sent.
+     */
+    boolean setLine1Number(String alphaTag, String number, Message onComplete);
+
+    /**
+     * Get the voice mail access phone number. Typically dialed when the
+     * user holds the "1" key in the phone app. May return null if not
+     * available or the SIM is not ready.<p>
+     */
+    String getVoiceMailNumber();
+
+    /**
+     * Returns the alpha tag associated with the voice mail number.
+     * If there is no alpha tag associated or the record is not yet available,
+     * returns a default localized string. <p>
+     *
+     * Please use this value instead of some other localized string when
+     * showing a name for this number in the UI. For example, call log
+     * entries should show this alpha tag. <p>
+     *
+     * Usage of this alpha tag in the UI is a common carrier requirement.
+     */
+    String getVoiceMailAlphaTag();
+
+    /**
+     * setVoiceMailNumber
+     * sets the voicemail number in the SIM card.
+     *
+     * @param alphaTag the alpha tag associated with the voice mail number
+     *        (see getVoiceMailAlphaTag)
+     * @param voiceMailNumber the new voicemail number to be set on the SIM.
+     * @param onComplete a callback message when the action is completed.
+     */
+    void setVoiceMailNumber(String alphaTag,
+                            String voiceMailNumber,
+                            Message onComplete);
+
+    /**
+     * getCallForwardingOptions
+     * gets a call forwarding option. The return value of
+     * ((AsyncResult)onComplete.obj) is an array of CallForwardInfo.
+     *
+     * @param commandInterfaceCFReason is one of the valid call forwarding
+     *        CF_REASONS, as defined in
+     *        <code>com.android.internal.telephony.CommandsInterface.</code>
+     * @param onComplete a callback message when the action is completed.
+     *        @see com.android.internal.telephony.CallForwardInfo for details.
+     */
+    void getCallForwardingOption(int commandInterfaceCFReason,
+                                  Message onComplete);
+
+    /**
+     * setCallForwardingOptions
+     * sets a call forwarding option.
+     *
+     * @param commandInterfaceCFReason is one of the valid call forwarding
+     *        CF_REASONS, as defined in
+     *        <code>com.android.internal.telephony.CommandsInterface.</code>
+     * @param commandInterfaceCFAction is one of the valid call forwarding
+     *        CF_ACTIONS, as defined in
+     *        <code>com.android.internal.telephony.CommandsInterface.</code>
+     * @param dialingNumber is the target phone number to forward calls to
+     * @param timerSeconds is used by CFNRy to indicate the timeout before
+     *        forwarding is attempted.
+     * @param onComplete a callback message when the action is completed.
+     */
+    void setCallForwardingOption(int commandInterfaceCFReason,
+                                 int commandInterfaceCFAction,
+                                 String dialingNumber,
+                                 int timerSeconds,
+                                 Message onComplete);
+
+    /**
+     * getOutgoingCallerIdDisplay
+     * gets outgoing caller id display. The return value of
+     * ((AsyncResult)onComplete.obj) is an array of int, with a length of 2.
+     *
+     * @param onComplete a callback message when the action is completed.
+     *        @see com.android.internal.telephony.CommandsInterface#getCLIR for details.
+     */
+    void getOutgoingCallerIdDisplay(Message onComplete);
+
+    /**
+     * setOutgoingCallerIdDisplay
+     * sets a call forwarding option.
+     *
+     * @param commandInterfaceCLIRMode is one of the valid call CLIR
+     *        modes, as defined in
+     *        <code>com.android.internal.telephony.CommandsInterface./code>
+     * @param onComplete a callback message when the action is completed.
+     */
+    void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode,
+                                    Message onComplete);
+
+    /**
+     * getCallWaiting
+     * gets call waiting activation state. The return value of
+     * ((AsyncResult)onComplete.obj) is an array of int, with a length of 1.
+     *
+     * @param onComplete a callback message when the action is completed.
+     *        @see com.android.internal.telephony.CommandsInterface#queryCallWaiting for details.
+     */
+    void getCallWaiting(Message onComplete);
+
+    /**
+     * setCallWaiting
+     * sets a call forwarding option.
+     *
+     * @param enable is a boolean representing the state that you are
+     *        requesting, true for enabled, false for disabled.
+     * @param onComplete a callback message when the action is completed.
+     */
+    void setCallWaiting(boolean enable, Message onComplete);
+
+    /**
+     * Scan available networks. This method is asynchronous; .
+     * On completion, <code>response.obj</code> is set to an AsyncResult with
+     * one of the following members:.<p>
+     *<ul>
+     * <li><code>response.obj.result</code> will be a <code>List</code> of
+     * <code>OperatorInfo</code> objects, or</li>
+     * <li><code>response.obj.exception</code> will be set with an exception
+     * on failure.</li>
+     * </ul>
+     */
+    void getAvailableNetworks(Message response);
+
+    /**
+     * Start a network scan. This method is asynchronous; .
+     * On completion, <code>response.obj</code> is set to an AsyncResult with
+     * one of the following members:.<p>
+     * <ul>
+     * <li><code>response.obj.result</code> will be a <code>NetworkScanResult</code> object, or</li>
+     * <li><code>response.obj.exception</code> will be set with an exception
+     * on failure.</li>
+     * </ul>
+     */
+    void startNetworkScan(NetworkScanRequest nsr, Message response);
+
+    /**
+     * Stop ongoing network scan. This method is asynchronous; .
+     * On completion, <code>response.obj</code> is set to an AsyncResult with
+     * one of the following members:.<p>
+     * <ul>
+     * <li><code>response.obj.result</code> will be a <code>NetworkScanResult</code> object, or</li>
+     * <li><code>response.obj.exception</code> will be set with an exception
+     * on failure.</li>
+     * </ul>
+     */
+    void stopNetworkScan(Message response);
+
+    /**
+     * Query neighboring cell IDs.  <code>response</code> is dispatched when
+     * this is complete.  <code>response.obj</code> will be an AsyncResult,
+     * and <code>response.obj.exception</code> will be non-null on failure.
+     * On success, <code>AsyncResult.result</code> will be a <code>String[]</code>
+     * containing the neighboring cell IDs.  Index 0 will contain the count
+     * of available cell IDs.  Cell IDs are in hexadecimal format.
+     *
+     * @param response callback message that is dispatched when the query
+     * completes.
+     * @param workSource calling WorkSource
+     */
+    default void getNeighboringCids(Message response, WorkSource workSource){}
+
+    /**
+     * Mutes or unmutes the microphone for the active call. The microphone
+     * is automatically unmuted if a call is answered, dialed, or resumed
+     * from a holding state.
+     *
+     * @param muted true to mute the microphone,
+     * false to activate the microphone.
+     */
+
+    void setMute(boolean muted);
+
+    /**
+     * Gets current mute status. Use
+     * {@link #registerForPreciseCallStateChanged(android.os.Handler, int,
+     * java.lang.Object) registerForPreciseCallStateChanged()}
+     * as a change notifcation, although presently phone state changed is not
+     * fired when setMute() is called.
+     *
+     * @return true is muting, false is unmuting
+     */
+    boolean getMute();
+
+    /**
+     * Get the current active Data Call list
+     *
+     * @param response <strong>On success</strong>, "response" bytes is
+     * made available as:
+     * (String[])(((AsyncResult)response.obj).result).
+     * <strong>On failure</strong>,
+     * (((AsyncResult)response.obj).result) == null and
+     * (((AsyncResult)response.obj).exception) being an instance of
+     * com.android.internal.telephony.gsm.CommandException
+     */
+    void getDataCallList(Message response);
+
+    /**
+     * Update the ServiceState CellLocation for current network registration.
+     */
+    void updateServiceLocation();
+
+    /**
+     * Enable location update notifications.
+     */
+    void enableLocationUpdates();
+
+    /**
+     * Disable location update notifications.
+     */
+    void disableLocationUpdates();
+
+    /**
+     * @return true if enable data connection on roaming
+     */
+    boolean getDataRoamingEnabled();
+
+    /**
+     * @param enable set true if enable data connection on roaming
+     */
+    void setDataRoamingEnabled(boolean enable);
+
+    /**
+     * @return true if user has enabled data
+     */
+    boolean getDataEnabled();
+
+    /**
+     * @param @enable set {@code true} if enable data connection
+     */
+    void setDataEnabled(boolean enable);
+
+    /**
+     * Retrieves the unique device ID, e.g., IMEI for GSM phones and MEID for CDMA phones.
+     */
+    String getDeviceId();
+
+    /**
+     * Retrieves the software version number for the device, e.g., IMEI/SV
+     * for GSM phones.
+     */
+    String getDeviceSvn();
+
+    /**
+     * Retrieves the unique subscriber ID, e.g., IMSI for GSM phones.
+     */
+    String getSubscriberId();
+
+    /**
+     * Retrieves the Group Identifier Level1 for GSM phones.
+     */
+    String getGroupIdLevel1();
+
+    /**
+     * Retrieves the Group Identifier Level2 for phones.
+     */
+    String getGroupIdLevel2();
+
+    /* CDMA support methods */
+
+    /**
+     * Retrieves the ESN for CDMA phones.
+     */
+    String getEsn();
+
+    /**
+     * Retrieves MEID for CDMA phones.
+     */
+    String getMeid();
+
+    /**
+     * Retrieves IMEI for phones. Returns null if IMEI is not set.
+     */
+    String getImei();
+
+    /**
+     * Retrieves the IccPhoneBookInterfaceManager of the Phone
+     */
+    public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager();
+
+    /**
+     * Activate or deactivate cell broadcast SMS.
+     *
+     * @param activate
+     *            0 = activate, 1 = deactivate
+     * @param response
+     *            Callback message is empty on completion
+     */
+    void activateCellBroadcastSms(int activate, Message response);
+
+    /**
+     * Query the current configuration of cdma cell broadcast SMS.
+     *
+     * @param response
+     *            Callback message is empty on completion
+     */
+    void getCellBroadcastSmsConfig(Message response);
+
+    /**
+     * Configure cell broadcast SMS.
+     *
+     * TODO: Change the configValuesArray to a RIL_BroadcastSMSConfig
+     *
+     * @param response
+     *            Callback message is empty on completion
+     */
+    public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response);
+
+    /*
+    * Sets the carrier information needed to encrypt the IMSI and IMPI.
+    * @param imsiEncryptionInfo Carrier specific information that will be used to encrypt the
+    *        IMSI and IMPI. This includes the Key type, the Public key
+    *        {@link java.security.PublicKey} and the Key identifier.
+    */
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo);
+
+    /**
+     * Returns Carrier specific information that will be used to encrypt the IMSI and IMPI.
+     * @param keyType whether the key is being used for WLAN or ePDG.
+     * @return ImsiEncryptionInfo which includes the Key Type, the Public Key
+     *        {@link java.security.PublicKey} and the Key Identifier.
+     *        The keyIdentifier This is used by the server to help it locate the private key to
+     *        decrypt the permanent identity.
+     */
+    public ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int keyType);
+}
diff --git a/com/android/internal/telephony/PhoneNotifier.java b/com/android/internal/telephony/PhoneNotifier.java
new file mode 100644
index 0000000..2caf5e2
--- /dev/null
+++ b/com/android/internal/telephony/PhoneNotifier.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.telephony.CellInfo;
+import android.telephony.VoLteServiceState;
+
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public interface PhoneNotifier {
+
+    public void notifyPhoneState(Phone sender);
+
+    public void notifyServiceState(Phone sender);
+
+    public void notifyCellLocation(Phone sender);
+
+    public void notifySignalStrength(Phone sender);
+
+    public void notifyMessageWaitingChanged(Phone sender);
+
+    public void notifyCallForwardingChanged(Phone sender);
+
+    /** TODO - reason should never be null */
+    public void notifyDataConnection(Phone sender, String reason, String apnType,
+            PhoneConstants.DataState state);
+
+    public void notifyDataConnectionFailed(Phone sender, String reason, String apnType);
+
+    public void notifyDataActivity(Phone sender);
+
+    public void notifyOtaspChanged(Phone sender, int otaspMode);
+
+    public void notifyCellInfo(Phone sender, List<CellInfo> cellInfo);
+
+    public void notifyPreciseCallState(Phone sender);
+
+    public void notifyDisconnectCause(int cause, int preciseCause);
+
+    public void notifyPreciseDataConnectionFailed(Phone sender, String reason, String apnType,
+            String apn, String failCause);
+
+    public void notifyVoLteServiceStateChanged(Phone sender, VoLteServiceState lteState);
+
+    public void notifyVoiceActivationStateChanged(Phone sender, int activationState);
+
+    public void notifyDataActivationStateChanged(Phone sender, int activationState);
+
+    public void notifyOemHookRawEventForSubscriber(int subId, byte[] rawData);
+}
diff --git a/com/android/internal/telephony/PhoneStateIntentReceiver.java b/com/android/internal/telephony/PhoneStateIntentReceiver.java
new file mode 100644
index 0000000..9dad3cc
--- /dev/null
+++ b/com/android/internal/telephony/PhoneStateIntentReceiver.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.TelephonyManager;
+import android.telephony.Rlog;
+
+/**
+ *
+ *                            DO NOT USE THIS CLASS:
+ *
+ *      Use android.telephony.TelephonyManager and PhoneStateListener instead.
+ *
+ *
+ */
+@Deprecated
+public final class PhoneStateIntentReceiver extends BroadcastReceiver {
+    private static final String LOG_TAG = "PhoneStatIntentReceiver";
+    private static final boolean DBG = false;
+
+    private static final int NOTIF_PHONE    = 1 << 0;
+    private static final int NOTIF_SERVICE  = 1 << 1;
+    private static final int NOTIF_SIGNAL   = 1 << 2;
+
+    PhoneConstants.State mPhoneState = PhoneConstants.State.IDLE;
+    ServiceState mServiceState = new ServiceState();
+    SignalStrength mSignalStrength = new SignalStrength();
+
+    private Context mContext;
+    private Handler mTarget;
+    private IntentFilter mFilter;
+    private int mWants;
+    private int mPhoneStateEventWhat;
+    private int mServiceStateEventWhat;
+    private int mAsuEventWhat;
+
+    public PhoneStateIntentReceiver() {
+        super();
+        mFilter = new IntentFilter();
+    }
+
+    public PhoneStateIntentReceiver(Context context, Handler target) {
+        this();
+        setContext(context);
+        setTarget(target);
+    }
+
+    public void setContext(Context c) {
+        mContext = c;
+    }
+
+    public void setTarget(Handler h) {
+        mTarget = h;
+    }
+
+    public PhoneConstants.State getPhoneState() {
+        if ((mWants & NOTIF_PHONE) == 0) {
+            throw new RuntimeException
+                ("client must call notifyPhoneCallState(int)");
+        }
+        return mPhoneState;
+    }
+
+    public ServiceState getServiceState() {
+        if ((mWants & NOTIF_SERVICE) == 0) {
+            throw new RuntimeException
+                ("client must call notifyServiceState(int)");
+        }
+        return mServiceState;
+    }
+
+    /**
+     * Returns current signal strength in as an asu 0..31
+     *
+     * Throws RuntimeException if client has not called notifySignalStrength()
+     */
+    public int getSignalStrengthLevelAsu() {
+        // TODO: use new SignalStrength instead of asu
+        if ((mWants & NOTIF_SIGNAL) == 0) {
+            throw new RuntimeException
+                ("client must call notifySignalStrength(int)");
+        }
+        return mSignalStrength.getAsuLevel();
+    }
+
+    /**
+     * Return current signal strength in "dBm", ranging from -113 - -51dBm
+     * or -1 if unknown
+     *
+     * @return signal strength in dBm, -1 if not yet updated
+     * Throws RuntimeException if client has not called notifySignalStrength()
+     */
+    public int getSignalStrengthDbm() {
+        if ((mWants & NOTIF_SIGNAL) == 0) {
+            throw new RuntimeException
+                ("client must call notifySignalStrength(int)");
+        }
+        return mSignalStrength.getDbm();
+    }
+
+    public void notifyPhoneCallState(int eventWhat) {
+        mWants |= NOTIF_PHONE;
+        mPhoneStateEventWhat = eventWhat;
+        mFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+    }
+
+    public boolean getNotifyPhoneCallState() {
+        return ((mWants & NOTIF_PHONE) != 0);
+    }
+
+    public void notifyServiceState(int eventWhat) {
+        mWants |= NOTIF_SERVICE;
+        mServiceStateEventWhat = eventWhat;
+        mFilter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED);
+    }
+
+    public boolean getNotifyServiceState() {
+        return ((mWants & NOTIF_SERVICE) != 0);
+    }
+
+    public void notifySignalStrength (int eventWhat) {
+        mWants |= NOTIF_SIGNAL;
+        mAsuEventWhat = eventWhat;
+        mFilter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED);
+    }
+
+    public boolean getNotifySignalStrength() {
+        return ((mWants & NOTIF_SIGNAL) != 0);
+    }
+
+    public void registerIntent() {
+        mContext.registerReceiver(this, mFilter);
+    }
+
+    public void unregisterIntent() {
+        mContext.unregisterReceiver(this);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        try {
+            if (TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED.equals(action)) {
+                mSignalStrength = SignalStrength.newFromBundle(intent.getExtras());
+
+                if (mTarget != null && getNotifySignalStrength()) {
+                    Message message = Message.obtain(mTarget, mAsuEventWhat);
+                    mTarget.sendMessage(message);
+                }
+            } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) {
+                if (DBG) Rlog.d(LOG_TAG, "onReceiveIntent: ACTION_PHONE_STATE_CHANGED, state="
+                               + intent.getStringExtra(PhoneConstants.STATE_KEY));
+                String phoneState = intent.getStringExtra(PhoneConstants.STATE_KEY);
+                mPhoneState = Enum.valueOf(
+                        PhoneConstants.State.class, phoneState);
+
+                if (mTarget != null && getNotifyPhoneCallState()) {
+                    Message message = Message.obtain(mTarget,
+                            mPhoneStateEventWhat);
+                    mTarget.sendMessage(message);
+                }
+            } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) {
+                mServiceState = ServiceState.newFromBundle(intent.getExtras());
+
+                if (mTarget != null && getNotifyServiceState()) {
+                    Message message = Message.obtain(mTarget,
+                            mServiceStateEventWhat);
+                    mTarget.sendMessage(message);
+                }
+            }
+        } catch (Exception ex) {
+            Rlog.e(LOG_TAG, "[PhoneStateIntentRecv] caught " + ex);
+            ex.printStackTrace();
+        }
+    }
+
+}
diff --git a/com/android/internal/telephony/PhoneSubInfoController.java b/com/android/internal/telephony/PhoneSubInfoController.java
new file mode 100644
index 0000000..9cd088f
--- /dev/null
+++ b/com/android/internal/telephony/PhoneSubInfoController.java
@@ -0,0 +1,559 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ * Copyright (c) 2011-2013, The Linux Foundation. All rights reserved.
+ * Not a Contribution.
+ *
+ * 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.internal.telephony;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionManager;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.uicc.IsimRecords;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+
+import static android.Manifest.permission.CALL_PRIVILEGED;
+import static android.Manifest.permission.READ_PHONE_NUMBERS;
+import static android.Manifest.permission.READ_PHONE_STATE;
+import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
+import static android.Manifest.permission.READ_SMS;
+import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+
+public class PhoneSubInfoController extends IPhoneSubInfo.Stub {
+    private static final String TAG = "PhoneSubInfoController";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    private final Phone[] mPhone;
+    private final Context mContext;
+    private final AppOpsManager mAppOps;
+
+    public PhoneSubInfoController(Context context, Phone[] phone) {
+        mPhone = phone;
+        if (ServiceManager.getService("iphonesubinfo") == null) {
+            ServiceManager.addService("iphonesubinfo", this);
+        }
+        mContext = context;
+        mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+    }
+
+    public String getDeviceId(String callingPackage) {
+        return getDeviceIdForPhone(SubscriptionManager.getPhoneId(getDefaultSubscription()),
+                callingPackage);
+    }
+
+    public String getDeviceIdForPhone(int phoneId, String callingPackage) {
+        if (!checkReadPhoneState(callingPackage, "getDeviceId")) {
+            return null;
+        }
+        if (!SubscriptionManager.isValidPhoneId(phoneId)) {
+            phoneId = 0;
+        }
+        final Phone phone = mPhone[phoneId];
+        if (phone != null) {
+            return phone.getDeviceId();
+        } else {
+            loge("getDeviceIdForPhone phone " + phoneId + " is null");
+            return null;
+        }
+    }
+
+    public String getNaiForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getNai")) {
+                return null;
+            }
+            return phone.getNai();
+        } else {
+            loge("getNai phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getImeiForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getImei")) {
+                return null;
+            }
+            return phone.getImei();
+        } else {
+            loge("getDeviceId phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int subId, int keyType,
+            String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getCarrierInfoForImsiEncryption")) {
+                return null;
+            }
+            return phone.getCarrierInfoForImsiEncryption(keyType);
+        } else {
+            loge("getCarrierInfoForImsiEncryption phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public void setCarrierInfoForImsiEncryption(int subId, String callingPackage,
+                                                ImsiEncryptionInfo imsiEncryptionInfo) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "setCarrierInfoForImsiEncryption")) {
+                return;
+            }
+            phone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo);
+        } else {
+            loge("setCarrierInfoForImsiEncryption phone is null for Subscription:" + subId);
+            return;
+        }
+    }
+
+
+    public String getDeviceSvn(String callingPackage) {
+        return getDeviceSvnUsingSubId(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getDeviceSvnUsingSubId(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getDeviceSvn")) {
+                return null;
+            }
+            return phone.getDeviceSvn();
+        } else {
+            loge("getDeviceSvn phone is null");
+            return null;
+        }
+    }
+
+    public String getSubscriberId(String callingPackage) {
+        return getSubscriberIdForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getSubscriberIdForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getSubscriberId")) {
+                return null;
+            }
+            return phone.getSubscriberId();
+        } else {
+            loge("getSubscriberId phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    /**
+     * Retrieves the serial number of the ICC, if applicable.
+     */
+    public String getIccSerialNumber(String callingPackage) {
+        return getIccSerialNumberForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getIccSerialNumberForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getIccSerialNumber")) {
+                return null;
+            }
+            return phone.getIccSerialNumber();
+        } else {
+            loge("getIccSerialNumber phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getLine1Number(String callingPackage) {
+        return getLine1NumberForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getLine1NumberForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            // This is open to apps with WRITE_SMS.
+            if (!checkReadPhoneNumber(callingPackage, "getLine1Number")) {
+                return null;
+            }
+            return phone.getLine1Number();
+        } else {
+            loge("getLine1Number phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getLine1AlphaTag(String callingPackage) {
+        return getLine1AlphaTagForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getLine1AlphaTagForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getLine1AlphaTag")) {
+                return null;
+            }
+            return phone.getLine1AlphaTag();
+        } else {
+            loge("getLine1AlphaTag phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getMsisdn(String callingPackage) {
+        return getMsisdnForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getMsisdnForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getMsisdn")) {
+                return null;
+            }
+            return phone.getMsisdn();
+        } else {
+            loge("getMsisdn phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getVoiceMailNumber(String callingPackage) {
+        return getVoiceMailNumberForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getVoiceMailNumberForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getVoiceMailNumber")) {
+                return null;
+            }
+            String number = PhoneNumberUtils.extractNetworkPortion(phone.getVoiceMailNumber());
+            if (VDBG) log("VM: getVoiceMailNUmber: " + number);
+            return number;
+        } else {
+            loge("getVoiceMailNumber phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    // TODO: change getCompleteVoiceMailNumber() to require READ_PRIVILEGED_PHONE_STATE
+    public String getCompleteVoiceMailNumber() {
+        return getCompleteVoiceMailNumberForSubscriber(getDefaultSubscription());
+    }
+
+    public String getCompleteVoiceMailNumberForSubscriber(int subId) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            mContext.enforceCallingOrSelfPermission(CALL_PRIVILEGED, "Requires CALL_PRIVILEGED");
+            String number = phone.getVoiceMailNumber();
+            if (VDBG) log("VM: getCompleteVoiceMailNUmber: " + number);
+            return number;
+        } else {
+            loge("getCompleteVoiceMailNumber phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getVoiceMailAlphaTag(String callingPackage) {
+        return getVoiceMailAlphaTagForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getVoiceMailAlphaTagForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getVoiceMailAlphaTag")) {
+                return null;
+            }
+            return phone.getVoiceMailAlphaTag();
+        } else {
+            loge("getVoiceMailAlphaTag phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    /**
+     * get Phone object based on subId.
+     **/
+    private Phone getPhone(int subId) {
+        int phoneId = SubscriptionManager.getPhoneId(subId);
+        if (!SubscriptionManager.isValidPhoneId(phoneId)) {
+            phoneId = 0;
+        }
+        return mPhone[phoneId];
+    }
+
+    /**
+     * Make sure caller has either read privileged phone permission or carrier privilege.
+     *
+     * @throws SecurityException if the caller does not have the required permission/privilege
+     */
+    private void enforcePrivilegedPermissionOrCarrierPrivilege(Phone phone) {
+        int permissionResult = mContext.checkCallingOrSelfPermission(
+                READ_PRIVILEGED_PHONE_STATE);
+        if (permissionResult == PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        log("No read privileged phone permission, check carrier privilege next.");
+        UiccCard uiccCard = phone.getUiccCard();
+        if (uiccCard == null) {
+            throw new SecurityException("No Carrier Privilege: No UICC");
+        }
+        if (uiccCard.getCarrierPrivilegeStatusForCurrentTransaction(
+                mContext.getPackageManager()) != CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+            throw new SecurityException("No Carrier Privilege.");
+        }
+    }
+
+    private int getDefaultSubscription() {
+        return  PhoneFactory.getDefaultSubscription();
+    }
+
+
+    /**
+    * get the Isim Impi based on subId
+    */
+    public String getIsimImpi(int subId) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
+                    "Requires READ_PRIVILEGED_PHONE_STATE");
+            IsimRecords isim = phone.getIsimRecords();
+            if (isim != null) {
+                return isim.getIsimImpi();
+            } else {
+                return null;
+            }
+        } else {
+            loge("getIsimImpi phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    /**
+    * get the Isim Domain based on subId
+    */
+    public String getIsimDomain(int subId) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
+                    "Requires READ_PRIVILEGED_PHONE_STATE");
+            IsimRecords isim = phone.getIsimRecords();
+            if (isim != null) {
+                return isim.getIsimDomain();
+            } else {
+                return null;
+            }
+        } else {
+            loge("getIsimDomain phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    /**
+    * get the Isim Impu based on subId
+    */
+    public String[] getIsimImpu(int subId) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
+                    "Requires READ_PRIVILEGED_PHONE_STATE");
+            IsimRecords isim = phone.getIsimRecords();
+            if (isim != null) {
+                return isim.getIsimImpu();
+            } else {
+                return null;
+            }
+        } else {
+            loge("getIsimImpu phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    /**
+    * get the Isim Ist based on subId
+    */
+    public String getIsimIst(int subId) throws RemoteException {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
+                    "Requires READ_PRIVILEGED_PHONE_STATE");
+            IsimRecords isim = phone.getIsimRecords();
+            if (isim != null) {
+                return isim.getIsimIst();
+            } else {
+                return null;
+            }
+        } else {
+            loge("getIsimIst phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    /**
+    * get the Isim Pcscf based on subId
+    */
+    public String[] getIsimPcscf(int subId) throws RemoteException {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
+                    "Requires READ_PRIVILEGED_PHONE_STATE");
+            IsimRecords isim = phone.getIsimRecords();
+            if (isim != null) {
+                return isim.getIsimPcscf();
+            } else {
+                return null;
+            }
+        } else {
+            loge("getIsimPcscf phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    public String getIsimChallengeResponse(String nonce) throws RemoteException {
+        Phone phone = getPhone(getDefaultSubscription());
+        mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
+                "Requires READ_PRIVILEGED_PHONE_STATE");
+        IsimRecords isim = phone.getIsimRecords();
+        if (isim != null) {
+            return isim.getIsimChallengeResponse(nonce);
+        } else {
+            return null;
+        }
+    }
+
+    public String getIccSimChallengeResponse(int subId, int appType, int authType, String data)
+            throws RemoteException {
+        Phone phone = getPhone(subId);
+        enforcePrivilegedPermissionOrCarrierPrivilege(phone);
+        UiccCard uiccCard = phone.getUiccCard();
+        if (uiccCard == null) {
+            loge("getIccSimChallengeResponse() UiccCard is null");
+            return null;
+        }
+
+        UiccCardApplication uiccApp = uiccCard.getApplicationByType(appType);
+        if (uiccApp == null) {
+            loge("getIccSimChallengeResponse() no app with specified type -- " +
+                    appType);
+            return null;
+        } else {
+            loge("getIccSimChallengeResponse() found app " + uiccApp.getAid()
+                    + " specified type -- " + appType);
+        }
+
+        if(authType != UiccCardApplication.AUTH_CONTEXT_EAP_SIM &&
+                authType != UiccCardApplication.AUTH_CONTEXT_EAP_AKA) {
+            loge("getIccSimChallengeResponse() unsupported authType: " + authType);
+            return null;
+        }
+
+        return uiccApp.getIccRecords().getIccSimChallengeResponse(authType, data);
+    }
+
+    public String getGroupIdLevel1(String callingPackage) {
+        return getGroupIdLevel1ForSubscriber(getDefaultSubscription(), callingPackage);
+    }
+
+    public String getGroupIdLevel1ForSubscriber(int subId, String callingPackage) {
+        Phone phone = getPhone(subId);
+        if (phone != null) {
+            if (!checkReadPhoneState(callingPackage, "getGroupIdLevel1")) {
+                return null;
+            }
+            return phone.getGroupIdLevel1();
+        } else {
+            loge("getGroupIdLevel1 phone is null for Subscription:" + subId);
+            return null;
+        }
+    }
+
+    private boolean checkReadPhoneState(String callingPackage, String message) {
+        try {
+            mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE, message);
+
+            // SKIP checking run-time OP_READ_PHONE_STATE since self or using PRIVILEGED
+            return true;
+        } catch (SecurityException e) {
+            mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, message);
+        }
+
+        return mAppOps.noteOp(AppOpsManager.OP_READ_PHONE_STATE, Binder.getCallingUid(),
+                callingPackage) == AppOpsManager.MODE_ALLOWED;
+    }
+
+    /**
+     * Besides READ_PHONE_STATE, READ_PHONE_NUMBERS, WRITE_SMS and READ_SMS also allow apps to get
+     * phone numbers.
+     */
+    private boolean checkReadPhoneNumber(String callingPackage, String message) {
+        // Default SMS app can always read it.
+        if (mAppOps.noteOp(AppOpsManager.OP_WRITE_SMS,
+                Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) {
+            return true;
+        }
+        try {
+            return checkReadPhoneState(callingPackage, message);
+        } catch (SecurityException readPhoneStateSecurityException) {
+        }
+        try {
+            // Can be read with READ_SMS too.
+            mContext.enforceCallingOrSelfPermission(READ_SMS, message);
+            int opCode = mAppOps.permissionToOpCode(READ_SMS);
+            if (opCode != AppOpsManager.OP_NONE) {
+                return mAppOps.noteOp(opCode, Binder.getCallingUid(), callingPackage)
+                        == AppOpsManager.MODE_ALLOWED;
+            } else {
+                return true;
+            }
+        } catch (SecurityException readSmsSecurityException) {
+        }
+        try {
+            // Can be read with READ_PHONE_NUMBERS too.
+            mContext.enforceCallingOrSelfPermission(READ_PHONE_NUMBERS, message);
+            int opCode = mAppOps.permissionToOpCode(READ_PHONE_NUMBERS);
+            if (opCode != AppOpsManager.OP_NONE) {
+                return mAppOps.noteOp(opCode, Binder.getCallingUid(), callingPackage)
+                        == AppOpsManager.MODE_ALLOWED;
+            } else {
+                return true;
+            }
+        } catch (SecurityException readPhoneNumberSecurityException) {
+        }
+        // Throw exception with message including READ_PHONE_STATE, READ_SMS, and READ_PHONE_NUMBERS
+        // permissions
+        throw new SecurityException(message + ": Neither user " + Binder.getCallingUid() +
+                " nor current process has " + READ_PHONE_STATE + ", " +
+                READ_SMS + ", or " + READ_PHONE_STATE + ".");
+    }
+
+    private void log(String s) {
+        Rlog.d(TAG, s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/PhoneSwitcher.java b/com/android/internal/telephony/PhoneSwitcher.java
new file mode 100644
index 0000000..1c6e023
--- /dev/null
+++ b/com/android/internal/telephony/PhoneSwitcher.java
@@ -0,0 +1,449 @@
+/*
+* Copyright (C) 2015 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.internal.telephony;
+
+import static android.telephony.SubscriptionManager.INVALID_PHONE_INDEX;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkFactory;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.StringNetworkSpecifier;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.RemoteException;
+import android.telephony.Rlog;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.dataconnection.DcRequest;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility singleton to monitor subscription changes and incoming NetworkRequests
+ * and determine which phone/phones are active.
+ *
+ * Manages the ALLOW_DATA calls to modems and notifies phones about changes to
+ * the active phones.  Note we don't wait for data attach (which may not happen anyway).
+ */
+public class PhoneSwitcher extends Handler {
+    private final static String LOG_TAG = "PhoneSwitcher";
+    private final static boolean VDBG = false;
+
+    private final int mMaxActivePhones;
+    private final List<DcRequest> mPrioritizedDcRequests = new ArrayList<DcRequest>();
+    private final RegistrantList[] mActivePhoneRegistrants;
+    private final SubscriptionController mSubscriptionController;
+    private final int[] mPhoneSubscriptions;
+    private final CommandsInterface[] mCommandsInterfaces;
+    private final Context mContext;
+    private final PhoneState[] mPhoneStates;
+    private final int mNumPhones;
+    private final Phone[] mPhones;
+    private final LocalLog mLocalLog;
+
+    private int mDefaultDataSubscription;
+
+    private final static int EVENT_DEFAULT_SUBSCRIPTION_CHANGED = 101;
+    private final static int EVENT_SUBSCRIPTION_CHANGED         = 102;
+    private final static int EVENT_REQUEST_NETWORK              = 103;
+    private final static int EVENT_RELEASE_NETWORK              = 104;
+    private final static int EVENT_EMERGENCY_TOGGLE             = 105;
+    private final static int EVENT_RESEND_DATA_ALLOWED          = 106;
+
+    private final static int MAX_LOCAL_LOG_LINES = 30;
+
+    @VisibleForTesting
+    public PhoneSwitcher(Looper looper) {
+        super(looper);
+        mMaxActivePhones = 0;
+        mSubscriptionController = null;
+        mPhoneSubscriptions = null;
+        mCommandsInterfaces = null;
+        mContext = null;
+        mPhoneStates = null;
+        mPhones = null;
+        mLocalLog = null;
+        mActivePhoneRegistrants = null;
+        mNumPhones = 0;
+    }
+
+    public PhoneSwitcher(int maxActivePhones, int numPhones, Context context,
+            SubscriptionController subscriptionController, Looper looper, ITelephonyRegistry tr,
+            CommandsInterface[] cis, Phone[] phones) {
+        super(looper);
+        mContext = context;
+        mNumPhones = numPhones;
+        mPhones = phones;
+        mPhoneSubscriptions = new int[numPhones];
+        mMaxActivePhones = maxActivePhones;
+        mLocalLog = new LocalLog(MAX_LOCAL_LOG_LINES);
+
+        mSubscriptionController = subscriptionController;
+
+        mActivePhoneRegistrants = new RegistrantList[numPhones];
+        mPhoneStates = new PhoneState[numPhones];
+        for (int i = 0; i < numPhones; i++) {
+            mActivePhoneRegistrants[i] = new RegistrantList();
+            mPhoneStates[i] = new PhoneState();
+            if (mPhones[i] != null) {
+                mPhones[i].registerForEmergencyCallToggle(this, EVENT_EMERGENCY_TOGGLE, null);
+            }
+        }
+
+        mCommandsInterfaces = cis;
+
+        try {
+            tr.addOnSubscriptionsChangedListener("PhoneSwitcher", mSubscriptionsChangedListener);
+        } catch (RemoteException e) {
+        }
+
+        mContext.registerReceiver(mDefaultDataChangedReceiver,
+                new IntentFilter(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED));
+
+        NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_SUPL);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_FOTA);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_CBS);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_IA);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_RCS);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_XCAP);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_EIMS);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        netCap.setNetworkSpecifier(new MatchAllNetworkSpecifier());
+
+        NetworkFactory networkFactory = new PhoneSwitcherNetworkRequestListener(looper, context,
+                netCap, this);
+        // we want to see all requests
+        networkFactory.setScoreFilter(101);
+        networkFactory.register();
+
+        log("PhoneSwitcher started");
+    }
+
+    private final BroadcastReceiver mDefaultDataChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Message msg = PhoneSwitcher.this.obtainMessage(EVENT_DEFAULT_SUBSCRIPTION_CHANGED);
+            msg.sendToTarget();
+        }
+    };
+
+    private final IOnSubscriptionsChangedListener mSubscriptionsChangedListener =
+            new IOnSubscriptionsChangedListener.Stub() {
+        @Override
+        public void onSubscriptionsChanged() {
+            Message msg = PhoneSwitcher.this.obtainMessage(EVENT_SUBSCRIPTION_CHANGED);
+            msg.sendToTarget();
+        }
+    };
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case EVENT_SUBSCRIPTION_CHANGED: {
+                onEvaluate(REQUESTS_UNCHANGED, "subChanged");
+                break;
+            }
+            case EVENT_DEFAULT_SUBSCRIPTION_CHANGED: {
+                onEvaluate(REQUESTS_UNCHANGED, "defaultChanged");
+                break;
+            }
+            case EVENT_REQUEST_NETWORK: {
+                onRequestNetwork((NetworkRequest)msg.obj);
+                break;
+            }
+            case EVENT_RELEASE_NETWORK: {
+                onReleaseNetwork((NetworkRequest)msg.obj);
+                break;
+            }
+            case EVENT_EMERGENCY_TOGGLE: {
+                onEvaluate(REQUESTS_CHANGED, "emergencyToggle");
+                break;
+            }
+            case EVENT_RESEND_DATA_ALLOWED: {
+                onResendDataAllowed(msg);
+                break;
+            }
+        }
+    }
+
+    private boolean isEmergency() {
+        for (Phone p : mPhones) {
+            if (p == null) continue;
+            if (p.isInEcm() || p.isInEmergencyCall()) return true;
+        }
+        return false;
+    }
+
+    private static class PhoneSwitcherNetworkRequestListener extends NetworkFactory {
+        private final PhoneSwitcher mPhoneSwitcher;
+        public PhoneSwitcherNetworkRequestListener (Looper l, Context c,
+                NetworkCapabilities nc, PhoneSwitcher ps) {
+            super(l, c, "PhoneSwitcherNetworkRequstListener", nc);
+            mPhoneSwitcher = ps;
+        }
+
+        @Override
+        protected void needNetworkFor(NetworkRequest networkRequest, int score) {
+            if (VDBG) log("needNetworkFor " + networkRequest + ", " + score);
+            Message msg = mPhoneSwitcher.obtainMessage(EVENT_REQUEST_NETWORK);
+            msg.obj = networkRequest;
+            msg.sendToTarget();
+        }
+
+        @Override
+        protected void releaseNetworkFor(NetworkRequest networkRequest) {
+            if (VDBG) log("releaseNetworkFor " + networkRequest);
+            Message msg = mPhoneSwitcher.obtainMessage(EVENT_RELEASE_NETWORK);
+            msg.obj = networkRequest;
+            msg.sendToTarget();
+        }
+    }
+
+    private void onRequestNetwork(NetworkRequest networkRequest) {
+        final DcRequest dcRequest = new DcRequest(networkRequest, mContext);
+        if (mPrioritizedDcRequests.contains(dcRequest) == false) {
+            mPrioritizedDcRequests.add(dcRequest);
+            Collections.sort(mPrioritizedDcRequests);
+            onEvaluate(REQUESTS_CHANGED, "netRequest");
+        }
+    }
+
+    private void onReleaseNetwork(NetworkRequest networkRequest) {
+        final DcRequest dcRequest = new DcRequest(networkRequest, mContext);
+
+        if (mPrioritizedDcRequests.remove(dcRequest)) {
+            onEvaluate(REQUESTS_CHANGED, "netReleased");
+        }
+    }
+
+    private static final boolean REQUESTS_CHANGED   = true;
+    private static final boolean REQUESTS_UNCHANGED = false;
+    /**
+     * Re-evaluate things.
+     * Do nothing if nothing's changed.
+     *
+     * Otherwise, go through the requests in priority order adding their phone
+     * until we've added up to the max allowed.  Then go through shutting down
+     * phones that aren't in the active phone list.  Finally, activate all
+     * phones in the active phone list.
+     */
+    private void onEvaluate(boolean requestsChanged, String reason) {
+        StringBuilder sb = new StringBuilder(reason);
+        if (isEmergency()) {
+            log("onEvalute aborted due to Emergency");
+            return;
+        }
+
+        boolean diffDetected = requestsChanged;
+        final int dataSub = mSubscriptionController.getDefaultDataSubId();
+        if (dataSub != mDefaultDataSubscription) {
+            sb.append(" default ").append(mDefaultDataSubscription).append("->").append(dataSub);
+            mDefaultDataSubscription = dataSub;
+            diffDetected = true;
+
+        }
+
+        for (int i = 0; i < mNumPhones; i++) {
+            int sub = mSubscriptionController.getSubIdUsingPhoneId(i);
+            if (sub != mPhoneSubscriptions[i]) {
+                sb.append(" phone[").append(i).append("] ").append(mPhoneSubscriptions[i]);
+                sb.append("->").append(sub);
+                mPhoneSubscriptions[i] = sub;
+                diffDetected = true;
+            }
+        }
+
+        if (diffDetected) {
+            log("evaluating due to " + sb.toString());
+
+            List<Integer> newActivePhones = new ArrayList<Integer>();
+
+            for (DcRequest dcRequest : mPrioritizedDcRequests) {
+                int phoneIdForRequest = phoneIdForRequest(dcRequest.networkRequest);
+                if (phoneIdForRequest == INVALID_PHONE_INDEX) continue;
+                if (newActivePhones.contains(phoneIdForRequest)) continue;
+                newActivePhones.add(phoneIdForRequest);
+                if (newActivePhones.size() >= mMaxActivePhones) break;
+            }
+
+            if (VDBG) {
+                log("default subId = " + mDefaultDataSubscription);
+                for (int i = 0; i < mNumPhones; i++) {
+                    log(" phone[" + i + "] using sub[" + mPhoneSubscriptions[i] + "]");
+                }
+                log(" newActivePhones:");
+                for (Integer i : newActivePhones) log("  " + i);
+            }
+
+            for (int phoneId = 0; phoneId < mNumPhones; phoneId++) {
+                if (newActivePhones.contains(phoneId) == false) {
+                    deactivate(phoneId);
+                }
+            }
+
+            // only activate phones up to the limit
+            for (int phoneId : newActivePhones) {
+                activate(phoneId);
+            }
+        }
+    }
+
+    private static class PhoneState {
+        public volatile boolean active = false;
+        public long lastRequested = 0;
+    }
+
+    private void deactivate(int phoneId) {
+        PhoneState state = mPhoneStates[phoneId];
+        if (state.active == false) return;
+        state.active = false;
+        log("deactivate " + phoneId);
+        state.lastRequested = System.currentTimeMillis();
+        // Skip ALLOW_DATA for single SIM device
+        if (mNumPhones > 1) {
+            mCommandsInterfaces[phoneId].setDataAllowed(false, null);
+        }
+        mActivePhoneRegistrants[phoneId].notifyRegistrants();
+    }
+
+    private void activate(int phoneId) {
+        PhoneState state = mPhoneStates[phoneId];
+        if (state.active == true) return;
+        state.active = true;
+        log("activate " + phoneId);
+        state.lastRequested = System.currentTimeMillis();
+        // Skip ALLOW_DATA for single SIM device
+        if (mNumPhones > 1) {
+            mCommandsInterfaces[phoneId].setDataAllowed(true, null);
+        }
+        mActivePhoneRegistrants[phoneId].notifyRegistrants();
+    }
+
+    // used when the modem may have been rebooted and we want to resend
+    // setDataAllowed
+    public void resendDataAllowed(int phoneId) {
+        validatePhoneId(phoneId);
+        Message msg = obtainMessage(EVENT_RESEND_DATA_ALLOWED);
+        msg.arg1 = phoneId;
+        msg.sendToTarget();
+    }
+
+    private void onResendDataAllowed(Message msg) {
+        final int phoneId = msg.arg1;
+        // Skip ALLOW_DATA for single SIM device
+        if (mNumPhones > 1) {
+            mCommandsInterfaces[phoneId].setDataAllowed(mPhoneStates[phoneId].active, null);
+        }
+    }
+
+    private int phoneIdForRequest(NetworkRequest netRequest) {
+        NetworkSpecifier specifier = netRequest.networkCapabilities.getNetworkSpecifier();
+        int subId;
+
+        if (specifier == null) {
+            subId = mDefaultDataSubscription;
+        } else if (specifier instanceof StringNetworkSpecifier) {
+            try {
+                subId = Integer.parseInt(((StringNetworkSpecifier) specifier).specifier);
+            } catch (NumberFormatException e) {
+                Rlog.e(LOG_TAG, "NumberFormatException on "
+                        + ((StringNetworkSpecifier) specifier).specifier);
+                subId = INVALID_SUBSCRIPTION_ID;
+            }
+        } else {
+            subId = INVALID_SUBSCRIPTION_ID;
+        }
+
+        int phoneId = INVALID_PHONE_INDEX;
+        if (subId == INVALID_SUBSCRIPTION_ID) return phoneId;
+
+        for (int i = 0 ; i < mNumPhones; i++) {
+            if (mPhoneSubscriptions[i] == subId) {
+                phoneId = i;
+                break;
+            }
+        }
+        return phoneId;
+    }
+
+    public boolean isPhoneActive(int phoneId) {
+        validatePhoneId(phoneId);
+        return mPhoneStates[phoneId].active;
+    }
+
+    public void registerForActivePhoneSwitch(int phoneId, Handler h, int what, Object o) {
+        validatePhoneId(phoneId);
+        Registrant r = new Registrant(h, what, o);
+        mActivePhoneRegistrants[phoneId].add(r);
+        r.notifyRegistrant();
+    }
+
+    public void unregisterForActivePhoneSwitch(int phoneId, Handler h) {
+        validatePhoneId(phoneId);
+        mActivePhoneRegistrants[phoneId].remove(h);
+    }
+
+    private void validatePhoneId(int phoneId) {
+        if (phoneId < 0 || phoneId >= mNumPhones) {
+            throw new IllegalArgumentException("Invalid PhoneId");
+        }
+    }
+
+    private void log(String l) {
+        Rlog.d(LOG_TAG, l);
+        mLocalLog.log(l);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        pw.println("PhoneSwitcher:");
+        Calendar c = Calendar.getInstance();
+        for (int i = 0; i < mNumPhones; i++) {
+            PhoneState ps = mPhoneStates[i];
+            c.setTimeInMillis(ps.lastRequested);
+            pw.println("PhoneId(" + i + ") active=" + ps.active + ", lastRequest=" +
+                    (ps.lastRequested == 0 ? "never" :
+                     String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c)));
+        }
+        pw.increaseIndent();
+        mLocalLog.dump(fd, pw, args);
+        pw.decreaseIndent();
+    }
+}
diff --git a/com/android/internal/telephony/ProxyController.java b/com/android/internal/telephony/ProxyController.java
new file mode 100644
index 0000000..f6d42af
--- /dev/null
+++ b/com/android/internal/telephony/ProxyController.java
@@ -0,0 +1,659 @@
+/*
+ * 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.internal.telephony;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.telephony.RadioAccessFamily;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.telephony.uicc.UiccController;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ProxyController {
+    static final String LOG_TAG = "ProxyController";
+
+    private static final int EVENT_NOTIFICATION_RC_CHANGED        = 1;
+    private static final int EVENT_START_RC_RESPONSE        = 2;
+    private static final int EVENT_APPLY_RC_RESPONSE        = 3;
+    private static final int EVENT_FINISH_RC_RESPONSE       = 4;
+    private static final int EVENT_TIMEOUT                  = 5;
+
+    private static final int SET_RC_STATUS_IDLE             = 0;
+    private static final int SET_RC_STATUS_STARTING         = 1;
+    private static final int SET_RC_STATUS_STARTED          = 2;
+    private static final int SET_RC_STATUS_APPLYING         = 3;
+    private static final int SET_RC_STATUS_SUCCESS          = 4;
+    private static final int SET_RC_STATUS_FAIL             = 5;
+
+    // The entire transaction must complete within this amount of time
+    // or a FINISH will be issued to each Logical Modem with the old
+    // Radio Access Family.
+    private static final int SET_RC_TIMEOUT_WAITING_MSEC    = (45 * 1000);
+
+    //***** Class Variables
+    private static ProxyController sProxyController;
+
+    private Phone[] mPhones;
+
+    private UiccController mUiccController;
+
+    private CommandsInterface[] mCi;
+
+    private Context mContext;
+
+    private PhoneSwitcher mPhoneSwitcher;
+
+    //UiccPhoneBookController to use proper IccPhoneBookInterfaceManagerProxy object
+    private UiccPhoneBookController mUiccPhoneBookController;
+
+    //PhoneSubInfoController to use proper PhoneSubInfoProxy object
+    private PhoneSubInfoController mPhoneSubInfoController;
+
+    //UiccSmsController to use proper IccSmsInterfaceManager object
+    private UiccSmsController mUiccSmsController;
+
+    WakeLock mWakeLock;
+
+    // record each phone's set radio capability status
+    private int[] mSetRadioAccessFamilyStatus;
+    private int mRadioAccessFamilyStatusCounter;
+    private boolean mTransactionFailed = false;
+
+    private String[] mCurrentLogicalModemIds;
+    private String[] mNewLogicalModemIds;
+
+    // Allows the generation of unique Id's for radio capability request session  id
+    private AtomicInteger mUniqueIdGenerator = new AtomicInteger(new Random().nextInt());
+
+    // on-going radio capability request session id
+    private int mRadioCapabilitySessionId;
+
+    // Record new and old Radio Access Family (raf) configuration.
+    // The old raf configuration is used to restore each logical modem raf when FINISH is
+    // issued if any requests fail.
+    private int[] mNewRadioAccessFamily;
+    private int[] mOldRadioAccessFamily;
+
+
+    //***** Class Methods
+    public static ProxyController getInstance(Context context, Phone[] phone,
+            UiccController uiccController, CommandsInterface[] ci, PhoneSwitcher ps) {
+        if (sProxyController == null) {
+            sProxyController = new ProxyController(context, phone, uiccController, ci, ps);
+        }
+        return sProxyController;
+    }
+
+    public static ProxyController getInstance() {
+        return sProxyController;
+    }
+
+    private ProxyController(Context context, Phone[] phone, UiccController uiccController,
+            CommandsInterface[] ci, PhoneSwitcher phoneSwitcher) {
+        logd("Constructor - Enter");
+
+        mContext = context;
+        mPhones = phone;
+        mUiccController = uiccController;
+        mCi = ci;
+        mPhoneSwitcher = phoneSwitcher;
+
+        mUiccPhoneBookController = new UiccPhoneBookController(mPhones);
+        mPhoneSubInfoController = new PhoneSubInfoController(mContext, mPhones);
+        mUiccSmsController = new UiccSmsController();
+        mSetRadioAccessFamilyStatus = new int[mPhones.length];
+        mNewRadioAccessFamily = new int[mPhones.length];
+        mOldRadioAccessFamily = new int[mPhones.length];
+        mCurrentLogicalModemIds = new String[mPhones.length];
+        mNewLogicalModemIds = new String[mPhones.length];
+
+        // wake lock for set radio capability
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+        mWakeLock.setReferenceCounted(false);
+
+        // Clear to be sure we're in the initial state
+        clearTransaction();
+        for (int i = 0; i < mPhones.length; i++) {
+            mPhones[i].registerForRadioCapabilityChanged(
+                    mHandler, EVENT_NOTIFICATION_RC_CHANGED, null);
+        }
+        logd("Constructor - Exit");
+    }
+
+    public void updateDataConnectionTracker(int sub) {
+        mPhones[sub].updateDataConnectionTracker();
+    }
+
+    public void enableDataConnectivity(int sub) {
+        mPhones[sub].setInternalDataEnabled(true, null);
+    }
+
+    public void disableDataConnectivity(int sub,
+            Message dataCleanedUpMsg) {
+        mPhones[sub].setInternalDataEnabled(false, dataCleanedUpMsg);
+    }
+
+    public void updateCurrentCarrierInProvider(int sub) {
+        mPhones[sub].updateCurrentCarrierInProvider();
+    }
+
+    public void registerForAllDataDisconnected(int subId, Handler h, int what, Object obj) {
+        int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
+
+        if (phoneId >= 0 && phoneId < TelephonyManager.getDefault().getPhoneCount()) {
+            mPhones[phoneId].registerForAllDataDisconnected(h, what, obj);
+        }
+    }
+
+    public void unregisterForAllDataDisconnected(int subId, Handler h) {
+        int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
+
+        if (phoneId >= 0 && phoneId < TelephonyManager.getDefault().getPhoneCount()) {
+            mPhones[phoneId].unregisterForAllDataDisconnected(h);
+        }
+    }
+
+    public boolean isDataDisconnected(int subId) {
+        int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
+
+        if (phoneId >= 0 && phoneId < TelephonyManager.getDefault().getPhoneCount()) {
+            return mPhones[phoneId].mDcTracker.isDisconnected();
+        } else {
+            // if we can't find a phone for the given subId, it is disconnected.
+            return true;
+        }
+    }
+
+    /**
+     * Get phone radio type and access technology.
+     *
+     * @param phoneId which phone you want to get
+     * @return phone radio type and access technology for input phone ID
+     */
+    public int getRadioAccessFamily(int phoneId) {
+        if (phoneId >= mPhones.length) {
+            return RadioAccessFamily.RAF_UNKNOWN;
+        } else {
+            return mPhones[phoneId].getRadioAccessFamily();
+        }
+    }
+
+    /**
+     * Set phone radio type and access technology for each phone.
+     *
+     * @param rafs an RadioAccessFamily array to indicate all phone's
+     *        new radio access family. The length of RadioAccessFamily
+     *        must equal to phone count.
+     * @return false if another session is already active and the request is rejected.
+     */
+    public boolean setRadioCapability(RadioAccessFamily[] rafs) {
+        if (rafs.length != mPhones.length) {
+            throw new RuntimeException("Length of input rafs must equal to total phone count");
+        }
+        // Check if there is any ongoing transaction and throw an exception if there
+        // is one as this is a programming error.
+        synchronized (mSetRadioAccessFamilyStatus) {
+            for (int i = 0; i < mPhones.length; i++) {
+                if (mSetRadioAccessFamilyStatus[i] != SET_RC_STATUS_IDLE) {
+                    // TODO: The right behaviour is to cancel previous request and send this.
+                    loge("setRadioCapability: Phone[" + i + "] is not idle. Rejecting request.");
+                    return false;
+                }
+            }
+        }
+
+        // Check we actually need to do anything
+        boolean same = true;
+        for (int i = 0; i < mPhones.length; i++) {
+            if (mPhones[i].getRadioAccessFamily() != rafs[i].getRadioAccessFamily()) {
+                same = false;
+            }
+        }
+        if (same) {
+            // All phones are already set to the requested raf
+            logd("setRadioCapability: Already in requested configuration, nothing to do.");
+            // It isn't really an error, so return true - everything is OK.
+            return true;
+        }
+
+        // Clear to be sure we're in the initial state
+        clearTransaction();
+
+        // Keep a wake lock until we finish radio capability changed
+        mWakeLock.acquire();
+
+        return doSetRadioCapabilities(rafs);
+    }
+
+    private boolean doSetRadioCapabilities(RadioAccessFamily[] rafs) {
+        // A new sessionId for this transaction
+        mRadioCapabilitySessionId = mUniqueIdGenerator.getAndIncrement();
+
+        // Start timer to make sure all phones respond within a specific time interval.
+        // Will send FINISH if a timeout occurs.
+        Message msg = mHandler.obtainMessage(EVENT_TIMEOUT, mRadioCapabilitySessionId, 0);
+        mHandler.sendMessageDelayed(msg, SET_RC_TIMEOUT_WAITING_MSEC);
+
+        synchronized (mSetRadioAccessFamilyStatus) {
+            logd("setRadioCapability: new request session id=" + mRadioCapabilitySessionId);
+            resetRadioAccessFamilyStatusCounter();
+            for (int i = 0; i < rafs.length; i++) {
+                int phoneId = rafs[i].getPhoneId();
+                logd("setRadioCapability: phoneId=" + phoneId + " status=STARTING");
+                mSetRadioAccessFamilyStatus[phoneId] = SET_RC_STATUS_STARTING;
+                mOldRadioAccessFamily[phoneId] = mPhones[phoneId].getRadioAccessFamily();
+                int requestedRaf = rafs[i].getRadioAccessFamily();
+                // TODO Set the new radio access family to the maximum of the requested & supported
+                // int supportedRaf = mPhones[i].getRadioAccessFamily();
+                // mNewRadioAccessFamily[phoneId] = requestedRaf & supportedRaf;
+                mNewRadioAccessFamily[phoneId] = requestedRaf;
+
+                mCurrentLogicalModemIds[phoneId] = mPhones[phoneId].getModemUuId();
+                // get the logical mode corresponds to new raf requested and pass the
+                // same as part of SET_RADIO_CAP APPLY phase
+                mNewLogicalModemIds[phoneId] = getLogicalModemIdFromRaf(requestedRaf);
+                logd("setRadioCapability: mOldRadioAccessFamily[" + phoneId + "]="
+                        + mOldRadioAccessFamily[phoneId]);
+                logd("setRadioCapability: mNewRadioAccessFamily[" + phoneId + "]="
+                        + mNewRadioAccessFamily[phoneId]);
+                sendRadioCapabilityRequest(
+                        phoneId,
+                        mRadioCapabilitySessionId,
+                        RadioCapability.RC_PHASE_START,
+                        mOldRadioAccessFamily[phoneId],
+                        mCurrentLogicalModemIds[phoneId],
+                        RadioCapability.RC_STATUS_NONE,
+                        EVENT_START_RC_RESPONSE);
+            }
+        }
+
+        return true;
+    }
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            logd("handleMessage msg.what=" + msg.what);
+            switch (msg.what) {
+                case EVENT_START_RC_RESPONSE:
+                    onStartRadioCapabilityResponse(msg);
+                    break;
+
+                case EVENT_APPLY_RC_RESPONSE:
+                    onApplyRadioCapabilityResponse(msg);
+                    break;
+
+                case EVENT_NOTIFICATION_RC_CHANGED:
+                    onNotificationRadioCapabilityChanged(msg);
+                    break;
+
+                case EVENT_FINISH_RC_RESPONSE:
+                    onFinishRadioCapabilityResponse(msg);
+                    break;
+
+                case EVENT_TIMEOUT:
+                    onTimeoutRadioCapability(msg);
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    };
+
+    /**
+     * Handle START response
+     * @param msg obj field isa RadioCapability
+     */
+    private void onStartRadioCapabilityResponse(Message msg) {
+        synchronized (mSetRadioAccessFamilyStatus) {
+            AsyncResult ar = (AsyncResult)msg.obj;
+            if (ar.exception != null) {
+                // just abort now.  They didn't take our start so we don't have to revert
+                logd("onStartRadioCapabilityResponse got exception=" + ar.exception);
+                mRadioCapabilitySessionId = mUniqueIdGenerator.getAndIncrement();
+                Intent intent = new Intent(TelephonyIntents.ACTION_SET_RADIO_CAPABILITY_FAILED);
+                mContext.sendBroadcast(intent);
+                clearTransaction();
+                return;
+            }
+            RadioCapability rc = (RadioCapability) ((AsyncResult) msg.obj).result;
+            if ((rc == null) || (rc.getSession() != mRadioCapabilitySessionId)) {
+                logd("onStartRadioCapabilityResponse: Ignore session=" + mRadioCapabilitySessionId
+                        + " rc=" + rc);
+                return;
+            }
+            mRadioAccessFamilyStatusCounter--;
+            int id = rc.getPhoneId();
+            if (((AsyncResult) msg.obj).exception != null) {
+                logd("onStartRadioCapabilityResponse: Error response session=" + rc.getSession());
+                logd("onStartRadioCapabilityResponse: phoneId=" + id + " status=FAIL");
+                mSetRadioAccessFamilyStatus[id] = SET_RC_STATUS_FAIL;
+                mTransactionFailed = true;
+            } else {
+                logd("onStartRadioCapabilityResponse: phoneId=" + id + " status=STARTED");
+                mSetRadioAccessFamilyStatus[id] = SET_RC_STATUS_STARTED;
+            }
+
+            if (mRadioAccessFamilyStatusCounter == 0) {
+                HashSet<String> modemsInUse = new HashSet<String>(mNewLogicalModemIds.length);
+                for (String modemId : mNewLogicalModemIds) {
+                    if (!modemsInUse.add(modemId)) {
+                        mTransactionFailed = true;
+                        Log.wtf(LOG_TAG, "ERROR: sending down the same id for different phones");
+                    }
+                }
+                logd("onStartRadioCapabilityResponse: success=" + !mTransactionFailed);
+                if (mTransactionFailed) {
+                    // Sends a variable number of requests, so don't resetRadioAccessFamilyCounter
+                    // here.
+                    issueFinish(mRadioCapabilitySessionId);
+                } else {
+                    // All logical modem accepted the new radio access family, issue the APPLY
+                    resetRadioAccessFamilyStatusCounter();
+                    for (int i = 0; i < mPhones.length; i++) {
+                        sendRadioCapabilityRequest(
+                            i,
+                            mRadioCapabilitySessionId,
+                            RadioCapability.RC_PHASE_APPLY,
+                            mNewRadioAccessFamily[i],
+                            mNewLogicalModemIds[i],
+                            RadioCapability.RC_STATUS_NONE,
+                            EVENT_APPLY_RC_RESPONSE);
+
+                        logd("onStartRadioCapabilityResponse: phoneId=" + i + " status=APPLYING");
+                        mSetRadioAccessFamilyStatus[i] = SET_RC_STATUS_APPLYING;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Handle APPLY response
+     * @param msg obj field isa RadioCapability
+     */
+    private void onApplyRadioCapabilityResponse(Message msg) {
+        RadioCapability rc = (RadioCapability) ((AsyncResult) msg.obj).result;
+        if ((rc == null) || (rc.getSession() != mRadioCapabilitySessionId)) {
+            logd("onApplyRadioCapabilityResponse: Ignore session=" + mRadioCapabilitySessionId
+                    + " rc=" + rc);
+            return;
+        }
+        logd("onApplyRadioCapabilityResponse: rc=" + rc);
+        if (((AsyncResult) msg.obj).exception != null) {
+            synchronized (mSetRadioAccessFamilyStatus) {
+                logd("onApplyRadioCapabilityResponse: Error response session=" + rc.getSession());
+                int id = rc.getPhoneId();
+                logd("onApplyRadioCapabilityResponse: phoneId=" + id + " status=FAIL");
+                mSetRadioAccessFamilyStatus[id] = SET_RC_STATUS_FAIL;
+                mTransactionFailed = true;
+            }
+        } else {
+            logd("onApplyRadioCapabilityResponse: Valid start expecting notification rc=" + rc);
+        }
+    }
+
+    /**
+     * Handle the notification unsolicited response associated with the APPLY
+     * @param msg obj field isa RadioCapability
+     */
+    private void onNotificationRadioCapabilityChanged(Message msg) {
+        RadioCapability rc = (RadioCapability) ((AsyncResult) msg.obj).result;
+        if ((rc == null) || (rc.getSession() != mRadioCapabilitySessionId)) {
+            logd("onNotificationRadioCapabilityChanged: Ignore session=" + mRadioCapabilitySessionId
+                    + " rc=" + rc);
+            return;
+        }
+        synchronized (mSetRadioAccessFamilyStatus) {
+            logd("onNotificationRadioCapabilityChanged: rc=" + rc);
+            // skip the overdue response by checking sessionId
+            if (rc.getSession() != mRadioCapabilitySessionId) {
+                logd("onNotificationRadioCapabilityChanged: Ignore session="
+                        + mRadioCapabilitySessionId + " rc=" + rc);
+                return;
+            }
+
+            int id = rc.getPhoneId();
+            if ((((AsyncResult) msg.obj).exception != null) ||
+                    (rc.getStatus() == RadioCapability.RC_STATUS_FAIL)) {
+                logd("onNotificationRadioCapabilityChanged: phoneId=" + id + " status=FAIL");
+                mSetRadioAccessFamilyStatus[id] = SET_RC_STATUS_FAIL;
+                mTransactionFailed = true;
+            } else {
+                logd("onNotificationRadioCapabilityChanged: phoneId=" + id + " status=SUCCESS");
+                mSetRadioAccessFamilyStatus[id] = SET_RC_STATUS_SUCCESS;
+                // The modems may have been restarted and forgotten this
+                mPhoneSwitcher.resendDataAllowed(id);
+                mPhones[id].radioCapabilityUpdated(rc);
+            }
+
+            mRadioAccessFamilyStatusCounter--;
+            if (mRadioAccessFamilyStatusCounter == 0) {
+                logd("onNotificationRadioCapabilityChanged: APPLY URC success=" +
+                        mTransactionFailed);
+                issueFinish(mRadioCapabilitySessionId);
+            }
+        }
+    }
+
+    /**
+     * Handle the FINISH Phase response
+     * @param msg obj field isa RadioCapability
+     */
+    void onFinishRadioCapabilityResponse(Message msg) {
+        RadioCapability rc = (RadioCapability) ((AsyncResult) msg.obj).result;
+        if ((rc == null) || (rc.getSession() != mRadioCapabilitySessionId)) {
+            logd("onFinishRadioCapabilityResponse: Ignore session=" + mRadioCapabilitySessionId
+                    + " rc=" + rc);
+            return;
+        }
+        synchronized (mSetRadioAccessFamilyStatus) {
+            logd(" onFinishRadioCapabilityResponse mRadioAccessFamilyStatusCounter="
+                    + mRadioAccessFamilyStatusCounter);
+            mRadioAccessFamilyStatusCounter--;
+            if (mRadioAccessFamilyStatusCounter == 0) {
+                completeRadioCapabilityTransaction();
+            }
+        }
+    }
+
+    private void onTimeoutRadioCapability(Message msg) {
+        if (msg.arg1 != mRadioCapabilitySessionId) {
+           logd("RadioCapability timeout: Ignore msg.arg1=" + msg.arg1 +
+                   "!= mRadioCapabilitySessionId=" + mRadioCapabilitySessionId);
+            return;
+        }
+
+        synchronized(mSetRadioAccessFamilyStatus) {
+            // timed-out.  Clean up as best we can
+            for (int i = 0; i < mPhones.length; i++) {
+                logd("RadioCapability timeout: mSetRadioAccessFamilyStatus[" + i + "]=" +
+                        mSetRadioAccessFamilyStatus[i]);
+            }
+
+            // Increment the sessionId as we are completing the transaction below
+            // so we don't want it completed when the FINISH phase is done.
+            int uniqueDifferentId = mUniqueIdGenerator.getAndIncrement();
+            // send FINISH request with fail status and then uniqueDifferentId
+            mTransactionFailed = true;
+            issueFinish(uniqueDifferentId);
+        }
+    }
+
+    private void issueFinish(int sessionId) {
+        // Issue FINISH
+        synchronized(mSetRadioAccessFamilyStatus) {
+            for (int i = 0; i < mPhones.length; i++) {
+                logd("issueFinish: phoneId=" + i + " sessionId=" + sessionId
+                        + " mTransactionFailed=" + mTransactionFailed);
+                mRadioAccessFamilyStatusCounter++;
+                sendRadioCapabilityRequest(
+                        i,
+                        sessionId,
+                        RadioCapability.RC_PHASE_FINISH,
+                        mOldRadioAccessFamily[i],
+                        mCurrentLogicalModemIds[i],
+                        (mTransactionFailed ? RadioCapability.RC_STATUS_FAIL :
+                        RadioCapability.RC_STATUS_SUCCESS),
+                        EVENT_FINISH_RC_RESPONSE);
+                if (mTransactionFailed) {
+                    logd("issueFinish: phoneId: " + i + " status: FAIL");
+                    // At least one failed, mark them all failed.
+                    mSetRadioAccessFamilyStatus[i] = SET_RC_STATUS_FAIL;
+                }
+            }
+        }
+    }
+
+    private void completeRadioCapabilityTransaction() {
+        // Create the intent to broadcast
+        Intent intent;
+        logd("onFinishRadioCapabilityResponse: success=" + !mTransactionFailed);
+        if (!mTransactionFailed) {
+            ArrayList<RadioAccessFamily> phoneRAFList = new ArrayList<RadioAccessFamily>();
+            for (int i = 0; i < mPhones.length; i++) {
+                int raf = mPhones[i].getRadioAccessFamily();
+                logd("radioAccessFamily[" + i + "]=" + raf);
+                RadioAccessFamily phoneRC = new RadioAccessFamily(i, raf);
+                phoneRAFList.add(phoneRC);
+            }
+            intent = new Intent(TelephonyIntents.ACTION_SET_RADIO_CAPABILITY_DONE);
+            intent.putParcelableArrayListExtra(TelephonyIntents.EXTRA_RADIO_ACCESS_FAMILY,
+                    phoneRAFList);
+
+            // make messages about the old transaction obsolete (specifically the timeout)
+            mRadioCapabilitySessionId = mUniqueIdGenerator.getAndIncrement();
+
+            // Reinitialize
+            clearTransaction();
+        } else {
+            intent = new Intent(TelephonyIntents.ACTION_SET_RADIO_CAPABILITY_FAILED);
+
+            // now revert.
+            mTransactionFailed = false;
+            RadioAccessFamily[] rafs = new RadioAccessFamily[mPhones.length];
+            for (int phoneId = 0; phoneId < mPhones.length; phoneId++) {
+                rafs[phoneId] = new RadioAccessFamily(phoneId, mOldRadioAccessFamily[phoneId]);
+            }
+            doSetRadioCapabilities(rafs);
+        }
+
+        // Broadcast that we're done
+        mContext.sendBroadcast(intent, android.Manifest.permission.READ_PHONE_STATE);
+    }
+
+    // Clear this transaction
+    private void clearTransaction() {
+        logd("clearTransaction");
+        synchronized(mSetRadioAccessFamilyStatus) {
+            for (int i = 0; i < mPhones.length; i++) {
+                logd("clearTransaction: phoneId=" + i + " status=IDLE");
+                mSetRadioAccessFamilyStatus[i] = SET_RC_STATUS_IDLE;
+                mOldRadioAccessFamily[i] = 0;
+                mNewRadioAccessFamily[i] = 0;
+                mTransactionFailed = false;
+            }
+
+            if (mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+        }
+    }
+
+    private void resetRadioAccessFamilyStatusCounter() {
+        mRadioAccessFamilyStatusCounter = mPhones.length;
+    }
+
+    private void sendRadioCapabilityRequest(int phoneId, int sessionId, int rcPhase,
+            int radioFamily, String logicalModemId, int status, int eventId) {
+        RadioCapability requestRC = new RadioCapability(
+                phoneId, sessionId, rcPhase, radioFamily, logicalModemId, status);
+        mPhones[phoneId].setRadioCapability(
+                requestRC, mHandler.obtainMessage(eventId));
+    }
+
+    // This method will return max number of raf bits supported from the raf
+    // values currently stored in all phone objects
+    public int getMaxRafSupported() {
+        int[] numRafSupported = new int[mPhones.length];
+        int maxNumRafBit = 0;
+        int maxRaf = RadioAccessFamily.RAF_UNKNOWN;
+
+        for (int len = 0; len < mPhones.length; len++) {
+            numRafSupported[len] = Integer.bitCount(mPhones[len].getRadioAccessFamily());
+            if (maxNumRafBit < numRafSupported[len]) {
+                maxNumRafBit = numRafSupported[len];
+                maxRaf = mPhones[len].getRadioAccessFamily();
+            }
+        }
+
+        return maxRaf;
+    }
+
+    // This method will return minimum number of raf bits supported from the raf
+    // values currently stored in all phone objects
+    public int getMinRafSupported() {
+        int[] numRafSupported = new int[mPhones.length];
+        int minNumRafBit = 0;
+        int minRaf = RadioAccessFamily.RAF_UNKNOWN;
+
+        for (int len = 0; len < mPhones.length; len++) {
+            numRafSupported[len] = Integer.bitCount(mPhones[len].getRadioAccessFamily());
+            if ((minNumRafBit == 0) || (minNumRafBit > numRafSupported[len])) {
+                minNumRafBit = numRafSupported[len];
+                minRaf = mPhones[len].getRadioAccessFamily();
+            }
+        }
+        return minRaf;
+    }
+
+    // This method checks current raf values stored in all phones and
+    // whicheve phone raf matches with input raf, returns modemId from that phone
+    private String getLogicalModemIdFromRaf(int raf) {
+        String modemUuid = null;
+
+        for (int phoneId = 0; phoneId < mPhones.length; phoneId++) {
+            if (mPhones[phoneId].getRadioAccessFamily() == raf) {
+                modemUuid = mPhones[phoneId].getModemUuId();
+                break;
+            }
+        }
+        return modemUuid;
+    }
+
+    private void logd(String string) {
+        Rlog.d(LOG_TAG, string);
+    }
+
+    private void loge(String string) {
+        Rlog.e(LOG_TAG, string);
+    }
+}
diff --git a/com/android/internal/telephony/RIL.java b/com/android/internal/telephony/RIL.java
new file mode 100644
index 0000000..8bb2125
--- /dev/null
+++ b/com/android/internal/telephony/RIL.java
@@ -0,0 +1,4976 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import static com.android.internal.telephony.RILConstants.*;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.hardware.radio.V1_0.Carrier;
+import android.hardware.radio.V1_0.CarrierRestrictions;
+import android.hardware.radio.V1_0.CdmaBroadcastSmsConfigInfo;
+import android.hardware.radio.V1_0.CdmaSmsAck;
+import android.hardware.radio.V1_0.CdmaSmsMessage;
+import android.hardware.radio.V1_0.CdmaSmsWriteArgs;
+import android.hardware.radio.V1_0.CellInfoCdma;
+import android.hardware.radio.V1_0.CellInfoGsm;
+import android.hardware.radio.V1_0.CellInfoLte;
+import android.hardware.radio.V1_0.CellInfoType;
+import android.hardware.radio.V1_0.CellInfoWcdma;
+import android.hardware.radio.V1_0.DataProfileInfo;
+import android.hardware.radio.V1_0.Dial;
+import android.hardware.radio.V1_0.GsmBroadcastSmsConfigInfo;
+import android.hardware.radio.V1_0.GsmSmsMessage;
+import android.hardware.radio.V1_0.HardwareConfigModem;
+import android.hardware.radio.V1_0.IRadio;
+import android.hardware.radio.V1_0.IccIo;
+import android.hardware.radio.V1_0.ImsSmsMessage;
+import android.hardware.radio.V1_0.LceDataInfo;
+import android.hardware.radio.V1_0.MvnoType;
+import android.hardware.radio.V1_0.NvWriteItem;
+import android.hardware.radio.V1_0.RadioError;
+import android.hardware.radio.V1_0.RadioIndicationType;
+import android.hardware.radio.V1_0.RadioResponseInfo;
+import android.hardware.radio.V1_0.RadioResponseType;
+import android.hardware.radio.V1_0.ResetNvType;
+import android.hardware.radio.V1_0.SelectUiccSub;
+import android.hardware.radio.V1_0.SetupDataCallResult;
+import android.hardware.radio.V1_0.SimApdu;
+import android.hardware.radio.V1_0.SmsWriteArgs;
+import android.hardware.radio.V1_0.UusInfo;
+import android.hardware.radio.deprecated.V1_0.IOemHook;
+import android.net.ConnectivityManager;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.HwBinder;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.WorkSource;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.CellInfo;
+import android.telephony.ClientRequestStats;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.ModemActivityInfo;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.NetworkScanRequest;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.RadioAccessFamily;
+import android.telephony.RadioAccessSpecifier;
+import android.telephony.RadioNetworkConstants.RadioAccessNetworks;
+import android.telephony.Rlog;
+import android.telephony.SignalStrength;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyHistogram;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.CdmaInformationRecords;
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.dataconnection.DataCallResponse;
+import com.android.internal.telephony.dataconnection.DataProfile;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * RIL implementation of the CommandsInterface.
+ *
+ * {@hide}
+ */
+public class RIL extends BaseCommands implements CommandsInterface {
+    static final String RILJ_LOG_TAG = "RILJ";
+    // Have a separate wakelock instance for Ack
+    static final String RILJ_ACK_WAKELOCK_NAME = "RILJ_ACK_WL";
+    static final boolean RILJ_LOGD = true;
+    static final boolean RILJ_LOGV = false; // STOPSHIP if true
+    static final int RIL_HISTOGRAM_BUCKET_COUNT = 5;
+
+    /**
+     * Wake lock timeout should be longer than the longest timeout in
+     * the vendor ril.
+     */
+    private static final int DEFAULT_WAKE_LOCK_TIMEOUT_MS = 60000;
+
+    // Wake lock default timeout associated with ack
+    private static final int DEFAULT_ACK_WAKE_LOCK_TIMEOUT_MS = 200;
+
+    private static final int DEFAULT_BLOCKING_MESSAGE_RESPONSE_TIMEOUT_MS = 2000;
+
+    // Variables used to differentiate ack messages from request while calling clearWakeLock()
+    public static final int INVALID_WAKELOCK = -1;
+    public static final int FOR_WAKELOCK = 0;
+    public static final int FOR_ACK_WAKELOCK = 1;
+    private final ClientWakelockTracker mClientWakelockTracker = new ClientWakelockTracker();
+
+    //***** Instance Variables
+
+    final WakeLock mWakeLock;           // Wake lock associated with request/response
+    final WakeLock mAckWakeLock;        // Wake lock associated with ack sent
+    final int mWakeLockTimeout;         // Timeout associated with request/response
+    final int mAckWakeLockTimeout;      // Timeout associated with ack sent
+    // The number of wakelock requests currently active.  Don't release the lock
+    // until dec'd to 0
+    int mWakeLockCount;
+
+    // Variables used to identify releasing of WL on wakelock timeouts
+    volatile int mWlSequenceNum = 0;
+    volatile int mAckWlSequenceNum = 0;
+
+    SparseArray<RILRequest> mRequestList = new SparseArray<RILRequest>();
+    static SparseArray<TelephonyHistogram> mRilTimeHistograms = new
+            SparseArray<TelephonyHistogram>();
+
+    Object[]     mLastNITZTimeInfo;
+
+    // When we are testing emergency calls
+    AtomicBoolean mTestingEmergencyCall = new AtomicBoolean(false);
+
+    final Integer mPhoneId;
+
+    /* default work source which will blame phone process */
+    private WorkSource mRILDefaultWorkSource;
+
+    /* Worksource containing all applications causing wakelock to be held */
+    private WorkSource mActiveWakelockWorkSource;
+
+    /** Telephony metrics instance for logging metrics event */
+    private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();
+
+    boolean mIsMobileNetworkSupported;
+    RadioResponse mRadioResponse;
+    RadioIndication mRadioIndication;
+    volatile IRadio mRadioProxy = null;
+    OemHookResponse mOemHookResponse;
+    OemHookIndication mOemHookIndication;
+    volatile IOemHook mOemHookProxy = null;
+    final AtomicLong mRadioProxyCookie = new AtomicLong(0);
+    final RadioProxyDeathRecipient mRadioProxyDeathRecipient;
+    final RilHandler mRilHandler;
+
+    //***** Events
+    static final int EVENT_WAKE_LOCK_TIMEOUT    = 2;
+    static final int EVENT_ACK_WAKE_LOCK_TIMEOUT    = 4;
+    static final int EVENT_BLOCKING_RESPONSE_TIMEOUT = 5;
+    static final int EVENT_RADIO_PROXY_DEAD     = 6;
+
+    //***** Constants
+
+    static final String[] HIDL_SERVICE_NAME = {"slot1", "slot2", "slot3"};
+
+    static final int IRADIO_GET_SERVICE_DELAY_MILLIS = 4 * 1000;
+
+    public static List<TelephonyHistogram> getTelephonyRILTimingHistograms() {
+        List<TelephonyHistogram> list;
+        synchronized (mRilTimeHistograms) {
+            list = new ArrayList<>(mRilTimeHistograms.size());
+            for (int i = 0; i < mRilTimeHistograms.size(); i++) {
+                TelephonyHistogram entry = new TelephonyHistogram(mRilTimeHistograms.valueAt(i));
+                list.add(entry);
+            }
+        }
+        return list;
+    }
+
+    /** The handler used to handle the internal event of RIL. */
+    @VisibleForTesting
+    public class RilHandler extends Handler {
+
+        //***** Handler implementation
+        @Override
+        public void handleMessage(Message msg) {
+            RILRequest rr;
+
+            switch (msg.what) {
+                case EVENT_WAKE_LOCK_TIMEOUT:
+                    // Haven't heard back from the last request.  Assume we're
+                    // not getting a response and  release the wake lock.
+
+                    // The timer of WAKE_LOCK_TIMEOUT is reset with each
+                    // new send request. So when WAKE_LOCK_TIMEOUT occurs
+                    // all requests in mRequestList already waited at
+                    // least DEFAULT_WAKE_LOCK_TIMEOUT_MS but no response.
+                    //
+                    // Note: Keep mRequestList so that delayed response
+                    // can still be handled when response finally comes.
+
+                    synchronized (mRequestList) {
+                        if (msg.arg1 == mWlSequenceNum && clearWakeLock(FOR_WAKELOCK)) {
+                            if (RILJ_LOGD) {
+                                int count = mRequestList.size();
+                                Rlog.d(RILJ_LOG_TAG, "WAKE_LOCK_TIMEOUT " +
+                                        " mRequestList=" + count);
+                                for (int i = 0; i < count; i++) {
+                                    rr = mRequestList.valueAt(i);
+                                    Rlog.d(RILJ_LOG_TAG, i + ": [" + rr.mSerial + "] "
+                                            + requestToString(rr.mRequest));
+                                }
+                            }
+                        }
+                    }
+                    break;
+
+                case EVENT_ACK_WAKE_LOCK_TIMEOUT:
+                    if (msg.arg1 == mAckWlSequenceNum && clearWakeLock(FOR_ACK_WAKELOCK)) {
+                        if (RILJ_LOGV) {
+                            Rlog.d(RILJ_LOG_TAG, "ACK_WAKE_LOCK_TIMEOUT");
+                        }
+                    }
+                    break;
+
+                case EVENT_BLOCKING_RESPONSE_TIMEOUT:
+                    int serial = msg.arg1;
+                    rr = findAndRemoveRequestFromList(serial);
+                    // If the request has already been processed, do nothing
+                    if(rr == null) {
+                        break;
+                    }
+
+                    //build a response if expected
+                    if (rr.mResult != null) {
+                        Object timeoutResponse = getResponseForTimedOutRILRequest(rr);
+                        AsyncResult.forMessage( rr.mResult, timeoutResponse, null);
+                        rr.mResult.sendToTarget();
+                        mMetrics.writeOnRilTimeoutResponse(mPhoneId, rr.mSerial, rr.mRequest);
+                    }
+
+                    decrementWakeLock(rr);
+                    rr.release();
+                    break;
+
+                case EVENT_RADIO_PROXY_DEAD:
+                    riljLog("handleMessage: EVENT_RADIO_PROXY_DEAD cookie = " + msg.obj +
+                            " mRadioProxyCookie = " + mRadioProxyCookie.get());
+                    if ((long) msg.obj == mRadioProxyCookie.get()) {
+                        resetProxyAndRequestList();
+
+                        // todo: rild should be back up since message was sent with a delay. this is
+                        // a hack.
+                        getRadioProxy(null);
+                        getOemHookProxy(null);
+                    }
+                    break;
+            }
+        }
+    }
+
+    /**
+     * In order to prevent calls to Telephony from waiting indefinitely
+     * low-latency blocking calls will eventually time out. In the event of
+     * a timeout, this function generates a response that is returned to the
+     * higher layers to unblock the call. This is in lieu of a meaningful
+     * response.
+     * @param rr The RIL Request that has timed out.
+     * @return A default object, such as the one generated by a normal response
+     * that is returned to the higher layers.
+     **/
+    private static Object getResponseForTimedOutRILRequest(RILRequest rr) {
+        if (rr == null ) return null;
+
+        Object timeoutResponse = null;
+        switch(rr.mRequest) {
+            case RIL_REQUEST_GET_ACTIVITY_INFO:
+                timeoutResponse = new ModemActivityInfo(
+                        0, 0, 0, new int [ModemActivityInfo.TX_POWER_LEVELS], 0, 0);
+                break;
+        };
+        return timeoutResponse;
+    }
+
+    final class RadioProxyDeathRecipient implements HwBinder.DeathRecipient {
+        @Override
+        public void serviceDied(long cookie) {
+            // Deal with service going away
+            riljLog("serviceDied");
+            // todo: temp hack to send delayed message so that rild is back up by then
+            //mRilHandler.sendMessage(mRilHandler.obtainMessage(EVENT_RADIO_PROXY_DEAD, cookie));
+            mRilHandler.sendMessageDelayed(
+                    mRilHandler.obtainMessage(EVENT_RADIO_PROXY_DEAD, cookie),
+                    IRADIO_GET_SERVICE_DELAY_MILLIS);
+        }
+    }
+
+    private void resetProxyAndRequestList() {
+        mRadioProxy = null;
+        mOemHookProxy = null;
+
+        // increment the cookie so that death notification can be ignored
+        mRadioProxyCookie.incrementAndGet();
+
+        setRadioState(RadioState.RADIO_UNAVAILABLE);
+
+        RILRequest.resetSerial();
+        // Clear request list on close
+        clearRequestList(RADIO_NOT_AVAILABLE, false);
+
+        // todo: need to get service right away so setResponseFunctions() can be called for
+        // unsolicited indications. getService() is not a blocking call, so it doesn't help to call
+        // it here. Current hack is to call getService() on death notification after a delay.
+    }
+
+    /** Returns a {@link IRadio} instance or null if the service is not available. */
+    @VisibleForTesting
+    public IRadio getRadioProxy(Message result) {
+        if (!mIsMobileNetworkSupported) {
+            if (RILJ_LOGV) riljLog("getRadioProxy: Not calling getService(): wifi-only");
+            if (result != null) {
+                AsyncResult.forMessage(result, null,
+                        CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
+                result.sendToTarget();
+            }
+            return null;
+        }
+
+        if (mRadioProxy != null) {
+            return mRadioProxy;
+        }
+
+        try {
+            mRadioProxy = IRadio.getService(HIDL_SERVICE_NAME[mPhoneId == null ? 0 : mPhoneId]);
+            if (mRadioProxy != null) {
+                mRadioProxy.linkToDeath(mRadioProxyDeathRecipient,
+                        mRadioProxyCookie.incrementAndGet());
+                mRadioProxy.setResponseFunctions(mRadioResponse, mRadioIndication);
+            } else {
+                riljLoge("getRadioProxy: mRadioProxy == null");
+            }
+        } catch (RemoteException | RuntimeException e) {
+            mRadioProxy = null;
+            riljLoge("RadioProxy getService/setResponseFunctions: " + e);
+        }
+
+        if (mRadioProxy == null) {
+            if (result != null) {
+                AsyncResult.forMessage(result, null,
+                        CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
+                result.sendToTarget();
+            }
+
+            // if service is not up, treat it like death notification to try to get service again
+            mRilHandler.sendMessageDelayed(
+                    mRilHandler.obtainMessage(EVENT_RADIO_PROXY_DEAD,
+                            mRadioProxyCookie.incrementAndGet()),
+                    IRADIO_GET_SERVICE_DELAY_MILLIS);
+        }
+
+        return mRadioProxy;
+    }
+
+    /** Returns an {@link IOemHook} instance or null if the service is not available. */
+    @VisibleForTesting
+    public IOemHook getOemHookProxy(Message result) {
+        if (!mIsMobileNetworkSupported) {
+            if (RILJ_LOGV) riljLog("getOemHookProxy: Not calling getService(): wifi-only");
+            if (result != null) {
+                AsyncResult.forMessage(result, null,
+                        CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
+                result.sendToTarget();
+            }
+            return null;
+        }
+
+        if (mOemHookProxy != null) {
+            return mOemHookProxy;
+        }
+
+        try {
+            mOemHookProxy = IOemHook.getService(
+                    HIDL_SERVICE_NAME[mPhoneId == null ? 0 : mPhoneId]);
+            if (mOemHookProxy != null) {
+                // not calling linkToDeath() as ril service runs in the same process and death
+                // notification for that should be sufficient
+                mOemHookProxy.setResponseFunctions(mOemHookResponse, mOemHookIndication);
+            } else {
+                riljLoge("getOemHookProxy: mOemHookProxy == null");
+            }
+        } catch (RemoteException | RuntimeException e) {
+            mOemHookProxy = null;
+            riljLoge("OemHookProxy getService/setResponseFunctions: " + e);
+        }
+
+        if (mOemHookProxy == null) {
+            if (result != null) {
+                AsyncResult.forMessage(result, null,
+                        CommandException.fromRilErrno(RADIO_NOT_AVAILABLE));
+                result.sendToTarget();
+            }
+
+            // if service is not up, treat it like death notification to try to get service again
+            mRilHandler.sendMessageDelayed(
+                    mRilHandler.obtainMessage(EVENT_RADIO_PROXY_DEAD,
+                            mRadioProxyCookie.incrementAndGet()),
+                    IRADIO_GET_SERVICE_DELAY_MILLIS);
+        }
+
+        return mOemHookProxy;
+    }
+
+    //***** Constructors
+
+    public RIL(Context context, int preferredNetworkType, int cdmaSubscription) {
+        this(context, preferredNetworkType, cdmaSubscription, null);
+    }
+
+    public RIL(Context context, int preferredNetworkType,
+            int cdmaSubscription, Integer instanceId) {
+        super(context);
+        if (RILJ_LOGD) {
+            riljLog("RIL: init preferredNetworkType=" + preferredNetworkType
+                    + " cdmaSubscription=" + cdmaSubscription + ")");
+        }
+
+        mContext = context;
+        mCdmaSubscription  = cdmaSubscription;
+        mPreferredNetworkType = preferredNetworkType;
+        mPhoneType = RILConstants.NO_PHONE;
+        mPhoneId = instanceId;
+
+        ConnectivityManager cm = (ConnectivityManager)context.getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        mIsMobileNetworkSupported = cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+
+        mRadioResponse = new RadioResponse(this);
+        mRadioIndication = new RadioIndication(this);
+        mOemHookResponse = new OemHookResponse(this);
+        mOemHookIndication = new OemHookIndication(this);
+        mRilHandler = new RilHandler();
+        mRadioProxyDeathRecipient = new RadioProxyDeathRecipient();
+
+        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, RILJ_LOG_TAG);
+        mWakeLock.setReferenceCounted(false);
+        mAckWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, RILJ_ACK_WAKELOCK_NAME);
+        mAckWakeLock.setReferenceCounted(false);
+        mWakeLockTimeout = SystemProperties.getInt(TelephonyProperties.PROPERTY_WAKE_LOCK_TIMEOUT,
+                DEFAULT_WAKE_LOCK_TIMEOUT_MS);
+        mAckWakeLockTimeout = SystemProperties.getInt(
+                TelephonyProperties.PROPERTY_WAKE_LOCK_TIMEOUT, DEFAULT_ACK_WAKE_LOCK_TIMEOUT_MS);
+        mWakeLockCount = 0;
+        mRILDefaultWorkSource = new WorkSource(context.getApplicationInfo().uid,
+                context.getPackageName());
+
+        TelephonyDevController tdc = TelephonyDevController.getInstance();
+        tdc.registerRIL(this);
+
+        // set radio callback; needed to set RadioIndication callback (should be done after
+        // wakelock stuff is initialized above as callbacks are received on separate binder threads)
+        getRadioProxy(null);
+        getOemHookProxy(null);
+    }
+
+    @Override
+    public void setOnNITZTime(Handler h, int what, Object obj) {
+        super.setOnNITZTime(h, what, obj);
+
+        // Send the last NITZ time if we have it
+        if (mLastNITZTimeInfo != null) {
+            mNITZTimeRegistrant
+                .notifyRegistrant(
+                    new AsyncResult (null, mLastNITZTimeInfo, null));
+        }
+    }
+
+    private void addRequest(RILRequest rr) {
+        acquireWakeLock(rr, FOR_WAKELOCK);
+        synchronized (mRequestList) {
+            rr.mStartTimeMs = SystemClock.elapsedRealtime();
+            mRequestList.append(rr.mSerial, rr);
+        }
+    }
+
+    private RILRequest obtainRequest(int request, Message result, WorkSource workSource) {
+        RILRequest rr = RILRequest.obtain(request, result, workSource);
+        addRequest(rr);
+        return rr;
+    }
+
+    private void handleRadioProxyExceptionForRR(RILRequest rr, String caller, Exception e) {
+        riljLoge(caller + ": " + e);
+        resetProxyAndRequestList();
+
+        // service most likely died, handle exception like death notification to try to get service
+        // again
+        mRilHandler.sendMessageDelayed(
+                mRilHandler.obtainMessage(EVENT_RADIO_PROXY_DEAD,
+                        mRadioProxyCookie.incrementAndGet()),
+                IRADIO_GET_SERVICE_DELAY_MILLIS);
+    }
+
+    private String convertNullToEmptyString(String string) {
+        return string != null ? string : "";
+    }
+
+    @Override
+    public void getIccCardStatus(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_SIM_STATUS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getIccCardStatus(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getIccCardStatus", e);
+            }
+        }
+    }
+
+    @Override
+    public void supplyIccPin(String pin, Message result) {
+        supplyIccPinForApp(pin, null, result);
+    }
+
+    @Override
+    public void supplyIccPinForApp(String pin, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ENTER_SIM_PIN, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " aid = " + aid);
+            }
+
+            try {
+                radioProxy.supplyIccPinForApp(rr.mSerial,
+                        convertNullToEmptyString(pin),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "supplyIccPinForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void supplyIccPuk(String puk, String newPin, Message result) {
+        supplyIccPukForApp(puk, newPin, null, result);
+    }
+
+    @Override
+    public void supplyIccPukForApp(String puk, String newPin, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ENTER_SIM_PUK, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " aid = " + aid);
+            }
+
+            try {
+                radioProxy.supplyIccPukForApp(rr.mSerial,
+                        convertNullToEmptyString(puk),
+                        convertNullToEmptyString(newPin),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "supplyIccPukForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void supplyIccPin2(String pin, Message result) {
+        supplyIccPin2ForApp(pin, null, result);
+    }
+
+    @Override
+    public void supplyIccPin2ForApp(String pin, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ENTER_SIM_PIN2, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " aid = " + aid);
+            }
+
+            try {
+                radioProxy.supplyIccPin2ForApp(rr.mSerial,
+                        convertNullToEmptyString(pin),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "supplyIccPin2ForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void supplyIccPuk2(String puk2, String newPin2, Message result) {
+        supplyIccPuk2ForApp(puk2, newPin2, null, result);
+    }
+
+    @Override
+    public void supplyIccPuk2ForApp(String puk, String newPin2, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ENTER_SIM_PUK2, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " aid = " + aid);
+            }
+
+            try {
+                radioProxy.supplyIccPuk2ForApp(rr.mSerial,
+                        convertNullToEmptyString(puk),
+                        convertNullToEmptyString(newPin2),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "supplyIccPuk2ForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void changeIccPin(String oldPin, String newPin, Message result) {
+        changeIccPinForApp(oldPin, newPin, null, result);
+    }
+
+    @Override
+    public void changeIccPinForApp(String oldPin, String newPin, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CHANGE_SIM_PIN, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " oldPin = "
+                        + oldPin + " newPin = " + newPin + " aid = " + aid);
+            }
+
+            try {
+                radioProxy.changeIccPinForApp(rr.mSerial,
+                        convertNullToEmptyString(oldPin),
+                        convertNullToEmptyString(newPin),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "changeIccPinForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void changeIccPin2(String oldPin2, String newPin2, Message result) {
+        changeIccPin2ForApp(oldPin2, newPin2, null, result);
+    }
+
+    @Override
+    public void changeIccPin2ForApp(String oldPin2, String newPin2, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CHANGE_SIM_PIN2, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " oldPin = "
+                        + oldPin2 + " newPin = " + newPin2 + " aid = " + aid);
+            }
+
+            try {
+                radioProxy.changeIccPin2ForApp(rr.mSerial,
+                        convertNullToEmptyString(oldPin2),
+                        convertNullToEmptyString(newPin2),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "changeIccPin2ForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void supplyNetworkDepersonalization(String netpin, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ENTER_NETWORK_DEPERSONALIZATION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " netpin = "
+                        + netpin);
+            }
+
+            try {
+                radioProxy.supplyNetworkDepersonalization(rr.mSerial,
+                        convertNullToEmptyString(netpin));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "supplyNetworkDepersonalization", e);
+            }
+        }
+    }
+
+    @Override
+    public void getCurrentCalls(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_CURRENT_CALLS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.getCurrentCalls(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getCurrentCalls", e);
+            }
+        }
+    }
+
+    @Override
+    public void dial(String address, int clirMode, Message result) {
+        dial(address, clirMode, null, result);
+    }
+
+    @Override
+    public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DIAL, result,
+                    mRILDefaultWorkSource);
+
+            Dial dialInfo = new Dial();
+            dialInfo.address = convertNullToEmptyString(address);
+            dialInfo.clir = clirMode;
+            if (uusInfo != null) {
+                UusInfo info = new UusInfo();
+                info.uusType = uusInfo.getType();
+                info.uusDcs = uusInfo.getDcs();
+                info.uusData = new String(uusInfo.getUserData());
+                dialInfo.uusInfo.add(info);
+            }
+
+            if (RILJ_LOGD) {
+                // Do not log function arg for privacy
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.dial(rr.mSerial, dialInfo);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "dial", e);
+            }
+        }
+    }
+
+    @Override
+    public void getIMSI(Message result) {
+        getIMSIForApp(null, result);
+    }
+
+    @Override
+    public void getIMSIForApp(String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_IMSI, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString()
+                        + ">  " + requestToString(rr.mRequest) + " aid = " + aid);
+            }
+            try {
+                radioProxy.getImsiForApp(rr.mSerial, convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getIMSIForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void hangupConnection(int gsmIndex, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_HANGUP, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " gsmIndex = "
+                        + gsmIndex);
+            }
+
+            try {
+                radioProxy.hangup(rr.mSerial, gsmIndex);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "hangupConnection", e);
+            }
+        }
+    }
+
+    @Override
+    public void hangupWaitingOrBackground(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.hangupWaitingOrBackground(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "hangupWaitingOrBackground", e);
+            }
+        }
+    }
+
+    @Override
+    public void hangupForegroundResumeBackground(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.hangupForegroundResumeBackground(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "hangupForegroundResumeBackground", e);
+            }
+        }
+    }
+
+    @Override
+    public void switchWaitingOrHoldingAndActive(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.switchWaitingOrHoldingAndActive(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "switchWaitingOrHoldingAndActive", e);
+            }
+        }
+    }
+
+    @Override
+    public void conference(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CONFERENCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.conference(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "conference", e);
+            }
+        }
+    }
+
+    @Override
+    public void rejectCall(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_UDUB, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.rejectCall(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "rejectCall", e);
+            }
+        }
+    }
+
+    @Override
+    public void getLastCallFailCause(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_LAST_CALL_FAIL_CAUSE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getLastCallFailCause(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getLastCallFailCause", e);
+            }
+        }
+    }
+
+    @Override
+    public void getSignalStrength(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIGNAL_STRENGTH, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getSignalStrength(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getSignalStrength", e);
+            }
+        }
+    }
+
+    @Override
+    public void getVoiceRegistrationState(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_VOICE_REGISTRATION_STATE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getVoiceRegistrationState(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getVoiceRegistrationState", e);
+            }
+        }
+    }
+
+    @Override
+    public void getDataRegistrationState(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DATA_REGISTRATION_STATE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getDataRegistrationState(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getDataRegistrationState", e);
+            }
+        }
+    }
+
+    @Override
+    public void getOperator(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_OPERATOR, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getOperator(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getOperator", e);
+            }
+        }
+    }
+
+    @Override
+    public void setRadioPower(boolean on, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_RADIO_POWER, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " on = " + on);
+            }
+
+            try {
+                radioProxy.setRadioPower(rr.mSerial, on);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setRadioPower", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendDtmf(char c, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DTMF, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                // Do not log function arg for privacy
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.sendDtmf(rr.mSerial, c + "");
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendDtmf", e);
+            }
+        }
+    }
+
+    private GsmSmsMessage constructGsmSendSmsRilRequest(String smscPdu, String pdu) {
+        GsmSmsMessage msg = new GsmSmsMessage();
+        msg.smscPdu = smscPdu == null ? "" : smscPdu;
+        msg.pdu = pdu == null ? "" : pdu;
+        return msg;
+    }
+
+    @Override
+    public void sendSMS(String smscPdu, String pdu, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SEND_SMS, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function args for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            GsmSmsMessage msg = constructGsmSendSmsRilRequest(smscPdu, pdu);
+
+            try {
+                radioProxy.sendSms(rr.mSerial, msg);
+                mMetrics.writeRilSendSms(mPhoneId, rr.mSerial, SmsSession.Event.Tech.SMS_GSM,
+                        SmsSession.Event.Format.SMS_FORMAT_3GPP);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendSMS", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendSMSExpectMore(String smscPdu, String pdu, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SEND_SMS_EXPECT_MORE, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function arg for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            GsmSmsMessage msg = constructGsmSendSmsRilRequest(smscPdu, pdu);
+
+            try {
+                radioProxy.sendSMSExpectMore(rr.mSerial, msg);
+                mMetrics.writeRilSendSms(mPhoneId, rr.mSerial, SmsSession.Event.Tech.SMS_GSM,
+                        SmsSession.Event.Format.SMS_FORMAT_3GPP);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendSMSExpectMore", e);
+            }
+        }
+    }
+
+    /**
+     * Convert MVNO type string into MvnoType defined in types.hal.
+     * @param mvnoType MVNO type
+     * @return MVNO type in integer
+     */
+    private static int convertToHalMvnoType(String mvnoType) {
+        switch (mvnoType) {
+            case "imsi" : return MvnoType.IMSI;
+            case "gid" : return MvnoType.GID;
+            case "spn" : return MvnoType.SPN;
+            default: return MvnoType.NONE;
+        }
+    }
+
+    /**
+     * Convert to DataProfileInfo defined in types.hal
+     * @param dp Data profile
+     * @return A converted data profile
+     */
+    private static DataProfileInfo convertToHalDataProfile(DataProfile dp) {
+        DataProfileInfo dpi = new DataProfileInfo();
+
+        dpi.profileId = dp.profileId;
+        dpi.apn = dp.apn;
+        dpi.protocol = dp.protocol;
+        dpi.roamingProtocol = dp.roamingProtocol;
+        dpi.authType = dp.authType;
+        dpi.user = dp.user;
+        dpi.password = dp.password;
+        dpi.type = dp.type;
+        dpi.maxConnsTime = dp.maxConnsTime;
+        dpi.maxConns = dp.maxConns;
+        dpi.waitTime = dp.waitTime;
+        dpi.enabled = dp.enabled;
+        dpi.supportedApnTypesBitmap = dp.supportedApnTypesBitmap;
+        dpi.bearerBitmap = dp.bearerBitmap;
+        dpi.mtu = dp.mtu;
+        dpi.mvnoType = convertToHalMvnoType(dp.mvnoType);
+        dpi.mvnoMatchData = dp.mvnoMatchData;
+
+        return dpi;
+    }
+
+    /**
+     * Convert NV reset type into ResetNvType defined in types.hal.
+     * @param resetType NV reset type.
+     * @return Converted reset type in integer or -1 if param is invalid.
+     */
+    private static int convertToHalResetNvType(int resetType) {
+        /**
+         * resetType values
+         * 1 - reload all NV items
+         * 2 - erase NV reset (SCRTN)
+         * 3 - factory reset (RTN)
+         */
+        switch (resetType) {
+            case 1: return ResetNvType.RELOAD;
+            case 2: return ResetNvType.ERASE;
+            case 3: return ResetNvType.FACTORY_RESET;
+        }
+        return -1;
+    }
+
+    /**
+     * Convert SetupDataCallResult defined in types.hal into DataCallResponse
+     * @param dcResult setup data call result
+     * @return converted DataCallResponse object
+     */
+    static DataCallResponse convertDataCallResult(SetupDataCallResult dcResult) {
+        return new DataCallResponse(dcResult.status,
+                dcResult.suggestedRetryTime,
+                dcResult.cid,
+                dcResult.active,
+                dcResult.type,
+                dcResult.ifname,
+                dcResult.addresses,
+                dcResult.dnses,
+                dcResult.gateways,
+                dcResult.pcscf,
+                dcResult.mtu
+        );
+    }
+
+    @Override
+    public void setupDataCall(int radioTechnology, DataProfile dataProfile, boolean isRoaming,
+                              boolean allowRoaming, Message result) {
+
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+
+            RILRequest rr = obtainRequest(RIL_REQUEST_SETUP_DATA_CALL, result,
+                    mRILDefaultWorkSource);
+
+            // Convert to HAL data profile
+            DataProfileInfo dpi = convertToHalDataProfile(dataProfile);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + ",radioTechnology=" + radioTechnology + ",isRoaming="
+                        + isRoaming + ",allowRoaming=" + allowRoaming + "," + dataProfile);
+            }
+
+            try {
+                radioProxy.setupDataCall(rr.mSerial, radioTechnology, dpi,
+                        dataProfile.modemCognitive, allowRoaming, isRoaming);
+                mMetrics.writeRilSetupDataCall(mPhoneId, rr.mSerial, radioTechnology, dpi.profileId,
+                        dpi.apn, dpi.authType, dpi.protocol);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setupDataCall", e);
+            }
+        }
+    }
+
+    @Override
+    public void iccIO(int command, int fileId, String path, int p1, int p2, int p3,
+                      String data, String pin2, Message result) {
+        iccIOForApp(command, fileId, path, p1, p2, p3, data, pin2, null, result);
+    }
+
+    @Override
+    public void iccIOForApp(int command, int fileId, String path, int p1, int p2, int p3,
+                 String data, String pin2, String aid, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_IO, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> iccIO: "
+                        + requestToString(rr.mRequest) + " command = 0x"
+                        + Integer.toHexString(command) + " fileId = 0x"
+                        + Integer.toHexString(fileId) + " path = " + path + " p1 = "
+                        + p1 + " p2 = " + p2 + " p3 = " + " data = " + data
+                        + " aid = " + aid);
+            }
+
+            IccIo iccIo = new IccIo();
+            iccIo.command = command;
+            iccIo.fileId = fileId;
+            iccIo.path = convertNullToEmptyString(path);
+            iccIo.p1 = p1;
+            iccIo.p2 = p2;
+            iccIo.p3 = p3;
+            iccIo.data = convertNullToEmptyString(data);
+            iccIo.pin2 = convertNullToEmptyString(pin2);
+            iccIo.aid = convertNullToEmptyString(aid);
+
+            try {
+                radioProxy.iccIOForApp(rr.mSerial, iccIo);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "iccIOForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendUSSD(String ussd, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SEND_USSD, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                String logUssd = "*******";
+                if (RILJ_LOGV) logUssd = ussd;
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " ussd = " + logUssd);
+            }
+
+            try {
+                radioProxy.sendUssd(rr.mSerial, convertNullToEmptyString(ussd));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendUSSD", e);
+            }
+        }
+    }
+
+    @Override
+    public void cancelPendingUssd(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CANCEL_USSD, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString()
+                        + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.cancelPendingUssd(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "cancelPendingUssd", e);
+            }
+        }
+    }
+
+    @Override
+    public void getCLIR(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_CLIR, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getClir(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getCLIR", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCLIR(int clirMode, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_CLIR, result, mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " clirMode = " + clirMode);
+            }
+
+            try {
+                radioProxy.setClir(rr.mSerial, clirMode);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCLIR", e);
+            }
+        }
+    }
+
+    @Override
+    public void queryCallForwardStatus(int cfReason, int serviceClass,
+                           String number, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_CALL_FORWARD_STATUS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " cfreason = " + cfReason + " serviceClass = " + serviceClass);
+            }
+
+            android.hardware.radio.V1_0.CallForwardInfo cfInfo =
+                    new android.hardware.radio.V1_0.CallForwardInfo();
+            cfInfo.reason = cfReason;
+            cfInfo.serviceClass = serviceClass;
+            cfInfo.toa = PhoneNumberUtils.toaFromString(number);
+            cfInfo.number = convertNullToEmptyString(number);
+            cfInfo.timeSeconds = 0;
+
+            try {
+                radioProxy.getCallForwardStatus(rr.mSerial, cfInfo);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "queryCallForwardStatus", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCallForward(int action, int cfReason, int serviceClass,
+                   String number, int timeSeconds, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_CALL_FORWARD, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " action = " + action + " cfReason = " + cfReason + " serviceClass = "
+                        + serviceClass + " timeSeconds = " + timeSeconds);
+            }
+
+            android.hardware.radio.V1_0.CallForwardInfo cfInfo =
+                    new android.hardware.radio.V1_0.CallForwardInfo();
+            cfInfo.status = action;
+            cfInfo.reason = cfReason;
+            cfInfo.serviceClass = serviceClass;
+            cfInfo.toa = PhoneNumberUtils.toaFromString(number);
+            cfInfo.number = convertNullToEmptyString(number);
+            cfInfo.timeSeconds = timeSeconds;
+
+            try {
+                radioProxy.setCallForward(rr.mSerial, cfInfo);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCallForward", e);
+
+            }
+        }
+    }
+
+    @Override
+    public void queryCallWaiting(int serviceClass, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_CALL_WAITING, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " serviceClass = " + serviceClass);
+            }
+
+            try {
+                radioProxy.getCallWaiting(rr.mSerial, serviceClass);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "queryCallWaiting", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, int serviceClass, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_CALL_WAITING, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " enable = " + enable + " serviceClass = " + serviceClass);
+            }
+
+            try {
+                radioProxy.setCallWaiting(rr.mSerial, enable, serviceClass);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCallWaiting", e);
+            }
+        }
+    }
+
+    @Override
+    public void acknowledgeLastIncomingGsmSms(boolean success, int cause, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SMS_ACKNOWLEDGE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " success = " + success + " cause = " + cause);
+            }
+
+            try {
+                radioProxy.acknowledgeLastIncomingGsmSms(rr.mSerial, success, cause);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "acknowledgeLastIncomingGsmSms", e);
+            }
+        }
+    }
+
+    @Override
+    public void acceptCall(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ANSWER, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.acceptCall(rr.mSerial);
+                mMetrics.writeRilAnswer(mPhoneId, rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "acceptCall", e);
+            }
+        }
+    }
+
+    @Override
+    public void deactivateDataCall(int cid, int reason, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DEACTIVATE_DATA_CALL, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest) + " cid = " + cid + " reason = " + reason);
+            }
+
+            try {
+                radioProxy.deactivateDataCall(rr.mSerial, cid, (reason == 0) ? false : true);
+                mMetrics.writeRilDeactivateDataCall(mPhoneId, rr.mSerial,
+                        cid, reason);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "deactivateDataCall", e);
+            }
+        }
+    }
+
+    @Override
+    public void queryFacilityLock(String facility, String password, int serviceClass,
+                                  Message result) {
+        queryFacilityLockForApp(facility, password, serviceClass, null, result);
+    }
+
+    @Override
+    public void queryFacilityLockForApp(String facility, String password, int serviceClass,
+                                        String appId, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_FACILITY_LOCK, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " facility = " + facility + " serviceClass = " + serviceClass
+                        + " appId = " + appId);
+            }
+
+            try {
+                radioProxy.getFacilityLockForApp(rr.mSerial,
+                        convertNullToEmptyString(facility),
+                        convertNullToEmptyString(password),
+                        serviceClass,
+                        convertNullToEmptyString(appId));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getFacilityLockForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void setFacilityLock(String facility, boolean lockState, String password,
+                                int serviceClass, Message result) {
+        setFacilityLockForApp(facility, lockState, password, serviceClass, null, result);
+    }
+
+    @Override
+    public void setFacilityLockForApp(String facility, boolean lockState, String password,
+                                      int serviceClass, String appId, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_FACILITY_LOCK, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " facility = " + facility + " lockstate = " + lockState
+                        + " serviceClass = " + serviceClass + " appId = " + appId);
+            }
+
+            try {
+                radioProxy.setFacilityLockForApp(rr.mSerial,
+                        convertNullToEmptyString(facility),
+                        lockState,
+                        convertNullToEmptyString(password),
+                        serviceClass,
+                        convertNullToEmptyString(appId));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setFacilityLockForApp", e);
+            }
+        }
+    }
+
+    @Override
+    public void changeBarringPassword(String facility, String oldPwd, String newPwd,
+                                      Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CHANGE_BARRING_PASSWORD, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log all function args for privacy
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + "facility = " + facility);
+            }
+
+            try {
+                radioProxy.setBarringPassword(rr.mSerial,
+                        convertNullToEmptyString(facility),
+                        convertNullToEmptyString(oldPwd),
+                        convertNullToEmptyString(newPwd));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "changeBarringPassword", e);
+            }
+        }
+    }
+
+    @Override
+    public void getNetworkSelectionMode(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_NETWORK_SELECTION_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getNetworkSelectionMode(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getNetworkSelectionMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_NETWORK_SELECTION_AUTOMATIC, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.setNetworkSelectionModeAutomatic(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setNetworkSelectionModeAutomatic", e);
+            }
+        }
+    }
+
+    @Override
+    public void setNetworkSelectionModeManual(String operatorNumeric, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_NETWORK_SELECTION_MANUAL, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " operatorNumeric = " + operatorNumeric);
+            }
+
+            try {
+                radioProxy.setNetworkSelectionModeManual(rr.mSerial,
+                        convertNullToEmptyString(operatorNumeric));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setNetworkSelectionModeManual", e);
+            }
+        }
+    }
+
+    @Override
+    public void getAvailableNetworks(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_AVAILABLE_NETWORKS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getAvailableNetworks(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getAvailableNetworks", e);
+            }
+        }
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            android.hardware.radio.V1_1.IRadio radioProxy11 =
+                    android.hardware.radio.V1_1.IRadio.castFrom(radioProxy);
+            if (radioProxy11 == null) {
+                if (result != null) {
+                    AsyncResult.forMessage(result, null,
+                            CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+                    result.sendToTarget();
+                }
+            } else {
+                android.hardware.radio.V1_1.NetworkScanRequest request =
+                        new android.hardware.radio.V1_1.NetworkScanRequest();
+                request.type = nsr.scanType;
+                request.interval = 60;
+                for (RadioAccessSpecifier ras : nsr.specifiers) {
+                    android.hardware.radio.V1_1.RadioAccessSpecifier s =
+                            new android.hardware.radio.V1_1.RadioAccessSpecifier();
+                    s.radioAccessNetwork = ras.radioAccessNetwork;
+                    List<Integer> bands = null;
+                    switch (ras.radioAccessNetwork) {
+                        case RadioAccessNetworks.GERAN:
+                            bands = s.geranBands;
+                            break;
+                        case RadioAccessNetworks.UTRAN:
+                            bands = s.utranBands;
+                            break;
+                        case RadioAccessNetworks.EUTRAN:
+                            bands = s.eutranBands;
+                            break;
+                        default:
+                            Log.wtf(RILJ_LOG_TAG, "radioAccessNetwork " + ras.radioAccessNetwork
+                                    + " not supported!");
+                            return;
+                    }
+                    if (ras.bands != null) {
+                        for (int band : ras.bands) {
+                            bands.add(band);
+                        }
+                    }
+                    if (ras.channels != null) {
+                        for (int channel : ras.channels) {
+                            s.channels.add(channel);
+                        }
+                    }
+                    request.specifiers.add(s);
+                }
+
+                RILRequest rr = obtainRequest(RIL_REQUEST_START_NETWORK_SCAN, result,
+                        mRILDefaultWorkSource);
+
+                if (RILJ_LOGD) {
+                    riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+                }
+
+                try {
+                    radioProxy11.startNetworkScan(rr.mSerial, request);
+                } catch (RemoteException | RuntimeException e) {
+                    handleRadioProxyExceptionForRR(rr, "startNetworkScan", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void stopNetworkScan(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            android.hardware.radio.V1_1.IRadio radioProxy11 =
+                    android.hardware.radio.V1_1.IRadio.castFrom(radioProxy);
+            if (radioProxy11 == null) {
+                if (result != null) {
+                    AsyncResult.forMessage(result, null,
+                            CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+                    result.sendToTarget();
+                }
+            } else {
+                RILRequest rr = obtainRequest(RIL_REQUEST_STOP_NETWORK_SCAN, result,
+                        mRILDefaultWorkSource);
+
+                if (RILJ_LOGD) {
+                    riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+                }
+
+                try {
+                    radioProxy11.stopNetworkScan(rr.mSerial);
+                } catch (RemoteException | RuntimeException e) {
+                    handleRadioProxyExceptionForRR(rr, "stopNetworkScan", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void startDtmf(char c, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DTMF_START, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function arg for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.startDtmf(rr.mSerial, c + "");
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "startDtmf", e);
+            }
+        }
+    }
+
+    @Override
+    public void stopDtmf(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DTMF_STOP, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.stopDtmf(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "stopDtmf", e);
+            }
+        }
+    }
+
+    @Override
+    public void separateConnection(int gsmIndex, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SEPARATE_CONNECTION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " gsmIndex = " + gsmIndex);
+            }
+
+            try {
+                radioProxy.separateConnection(rr.mSerial, gsmIndex);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "separateConnection", e);
+            }
+        }
+    }
+
+    @Override
+    public void getBasebandVersion(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_BASEBAND_VERSION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getBasebandVersion(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getBasebandVersion", e);
+            }
+        }
+    }
+
+    @Override
+    public void setMute(boolean enableMute, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_MUTE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " enableMute = " + enableMute);
+            }
+
+            try {
+                radioProxy.setMute(rr.mSerial, enableMute);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setMute", e);
+            }
+        }
+    }
+
+    @Override
+    public void getMute(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_MUTE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getMute(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getMute", e);
+            }
+        }
+    }
+
+    @Override
+    public void queryCLIP(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_CLIP, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getClip(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "queryCLIP", e);
+            }
+        }
+    }
+
+    /**
+     * @deprecated
+     */
+    @Override
+    @Deprecated
+    public void getPDPContextList(Message result) {
+        getDataCallList(result);
+    }
+
+    @Override
+    public void getDataCallList(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DATA_CALL_LIST, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getDataCallList(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getDataCallList", e);
+            }
+        }
+    }
+
+    @Override
+    public void invokeOemRilRequestRaw(byte[] data, Message response) {
+        IOemHook oemHookProxy = getOemHookProxy(response);
+        if (oemHookProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_OEM_HOOK_RAW, response,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + "[" + IccUtils.bytesToHexString(data) + "]");
+            }
+
+            try {
+                oemHookProxy.sendRequestRaw(rr.mSerial, primitiveArrayToArrayList(data));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "invokeOemRilRequestRaw", e);
+            }
+        }
+    }
+
+    @Override
+    public void invokeOemRilRequestStrings(String[] strings, Message result) {
+        IOemHook oemHookProxy = getOemHookProxy(result);
+        if (oemHookProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_OEM_HOOK_STRINGS, result,
+                    mRILDefaultWorkSource);
+
+            String logStr = "";
+            for (int i = 0; i < strings.length; i++) {
+                logStr = logStr + strings[i] + " ";
+            }
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " strings = "
+                        + logStr);
+            }
+
+            try {
+                oemHookProxy.sendRequestStrings(rr.mSerial,
+                        new ArrayList<String>(Arrays.asList(strings)));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "invokeOemRilRequestStrings", e);
+            }
+        }
+    }
+
+    @Override
+    public void setSuppServiceNotifications(boolean enable, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_SUPP_SVC_NOTIFICATION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " enable = "
+                        + enable);
+            }
+
+            try {
+                radioProxy.setSuppServiceNotifications(rr.mSerial, enable);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setSuppServiceNotifications", e);
+            }
+        }
+    }
+
+    @Override
+    public void writeSmsToSim(int status, String smsc, String pdu, Message result) {
+        status = translateStatus(status);
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_WRITE_SMS_TO_SIM, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGV) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest)
+                        + " " + status);
+            }
+
+            SmsWriteArgs args = new SmsWriteArgs();
+            args.status = status;
+            args.smsc = convertNullToEmptyString(smsc);
+            args.pdu = convertNullToEmptyString(pdu);
+
+            try {
+                radioProxy.writeSmsToSim(rr.mSerial, args);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "writeSmsToSim", e);
+            }
+        }
+    }
+
+    @Override
+    public void deleteSmsOnSim(int index, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DELETE_SMS_ON_SIM, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGV) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest) + " index = " + index);
+            }
+
+            try {
+                radioProxy.deleteSmsOnSim(rr.mSerial, index);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "deleteSmsOnSim", e);
+            }
+        }
+    }
+
+    @Override
+    public void setBandMode(int bandMode, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_BAND_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " bandMode = " + bandMode);
+            }
+
+            try {
+                radioProxy.setBandMode(rr.mSerial, bandMode);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setBandMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void queryAvailableBandMode(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_AVAILABLE_BAND_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getAvailableBandModes(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "queryAvailableBandMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendEnvelope(String contents, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " contents = "
+                        + contents);
+            }
+
+            try {
+                radioProxy.sendEnvelope(rr.mSerial, convertNullToEmptyString(contents));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendEnvelope", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendTerminalResponse(String contents, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " contents = "
+                        + contents);
+            }
+
+            try {
+                radioProxy.sendTerminalResponseToSim(rr.mSerial,
+                        convertNullToEmptyString(contents));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendTerminalResponse", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendEnvelopeWithStatus(String contents, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " contents = "
+                        + contents);
+            }
+
+            try {
+                radioProxy.sendEnvelopeWithStatus(rr.mSerial, convertNullToEmptyString(contents));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendEnvelopeWithStatus", e);
+            }
+        }
+    }
+
+    @Override
+    public void explicitCallTransfer(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_EXPLICIT_CALL_TRANSFER, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.explicitCallTransfer(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "explicitCallTransfer", e);
+            }
+        }
+    }
+
+    @Override
+    public void setPreferredNetworkType(int networkType , Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " networkType = " + networkType);
+            }
+            mPreferredNetworkType = networkType;
+            mMetrics.writeSetPreferredNetworkType(mPhoneId, networkType);
+
+            try {
+                radioProxy.setPreferredNetworkType(rr.mSerial, networkType);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setPreferredNetworkType", e);
+            }
+        }
+    }
+
+    @Override
+    public void getPreferredNetworkType(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_PREFERRED_NETWORK_TYPE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getPreferredNetworkType(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getPreferredNetworkType", e);
+            }
+        }
+    }
+
+    @Override
+    public void getNeighboringCids(Message result, WorkSource workSource) {
+        workSource = getDeafultWorkSourceIfInvalid(workSource);
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_NEIGHBORING_CELL_IDS, result,
+                    workSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getNeighboringCids(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getNeighboringCids", e);
+            }
+        }
+    }
+
+    @Override
+    public void setLocationUpdates(boolean enable, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_LOCATION_UPDATES, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest) + " enable = " + enable);
+            }
+
+            try {
+                radioProxy.setLocationUpdates(rr.mSerial, enable);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setLocationUpdates", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCdmaSubscriptionSource(int cdmaSubscription , Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " cdmaSubscription = " + cdmaSubscription);
+            }
+
+            try {
+                radioProxy.setCdmaSubscriptionSource(rr.mSerial, cdmaSubscription);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCdmaSubscriptionSource", e);
+            }
+        }
+    }
+
+    @Override
+    public void queryCdmaRoamingPreference(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getCdmaRoamingPreference(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "queryCdmaRoamingPreference", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCdmaRoamingPreference(int cdmaRoamingType, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " cdmaRoamingType = " + cdmaRoamingType);
+            }
+
+            try {
+                radioProxy.setCdmaRoamingPreference(rr.mSerial, cdmaRoamingType);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCdmaRoamingPreference", e);
+            }
+        }
+    }
+
+    @Override
+    public void queryTTYMode(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_QUERY_TTY_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getTTYMode(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "queryTTYMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void setTTYMode(int ttyMode, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_TTY_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " ttyMode = " + ttyMode);
+            }
+
+            try {
+                radioProxy.setTTYMode(rr.mSerial, ttyMode);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setTTYMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void setPreferredVoicePrivacy(boolean enable, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " enable = " + enable);
+            }
+
+            try {
+                radioProxy.setPreferredVoicePrivacy(rr.mSerial, enable);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setPreferredVoicePrivacy", e);
+            }
+        }
+    }
+
+    @Override
+    public void getPreferredVoicePrivacy(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE,
+                    result, mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getPreferredVoicePrivacy(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getPreferredVoicePrivacy", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendCDMAFeatureCode(String featureCode, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_FLASH, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " featureCode = " + featureCode);
+            }
+
+            try {
+                radioProxy.sendCDMAFeatureCode(rr.mSerial, convertNullToEmptyString(featureCode));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendCDMAFeatureCode", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendBurstDtmf(String dtmfString, int on, int off, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_BURST_DTMF, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " dtmfString = " + dtmfString + " on = " + on + " off = " + off);
+            }
+
+            try {
+                radioProxy.sendBurstDtmf(rr.mSerial, convertNullToEmptyString(dtmfString), on, off);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendBurstDtmf", e);
+            }
+        }
+    }
+
+    private void constructCdmaSendSmsRilRequest(CdmaSmsMessage msg, byte[] pdu) {
+        int addrNbrOfDigits;
+        int subaddrNbrOfDigits;
+        int bearerDataLength;
+        ByteArrayInputStream bais = new ByteArrayInputStream(pdu);
+        DataInputStream dis = new DataInputStream(bais);
+
+        try {
+            msg.teleserviceId = dis.readInt(); // teleServiceId
+            msg.isServicePresent = (byte) dis.readInt() == 1 ? true : false; // servicePresent
+            msg.serviceCategory = dis.readInt(); // serviceCategory
+            msg.address.digitMode = dis.read();  // address digit mode
+            msg.address.numberMode = dis.read(); // address number mode
+            msg.address.numberType = dis.read(); // address number type
+            msg.address.numberPlan = dis.read(); // address number plan
+            addrNbrOfDigits = (byte) dis.read();
+            for (int i = 0; i < addrNbrOfDigits; i++) {
+                msg.address.digits.add(dis.readByte()); // address_orig_bytes[i]
+            }
+            msg.subAddress.subaddressType = dis.read(); //subaddressType
+            msg.subAddress.odd = (byte) dis.read() == 1 ? true : false; //subaddr odd
+            subaddrNbrOfDigits = (byte) dis.read();
+            for (int i = 0; i < subaddrNbrOfDigits; i++) {
+                msg.subAddress.digits.add(dis.readByte()); //subaddr_orig_bytes[i]
+            }
+
+            bearerDataLength = dis.read();
+            for (int i = 0; i < bearerDataLength; i++) {
+                msg.bearerData.add(dis.readByte()); //bearerData[i]
+            }
+        } catch (IOException ex) {
+            if (RILJ_LOGD) {
+                riljLog("sendSmsCdma: conversion from input stream to object failed: "
+                        + ex);
+            }
+        }
+    }
+
+    @Override
+    public void sendCdmaSms(byte[] pdu, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SEND_SMS, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function arg for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            CdmaSmsMessage msg = new CdmaSmsMessage();
+            constructCdmaSendSmsRilRequest(msg, pdu);
+
+            try {
+                radioProxy.sendCdmaSms(rr.mSerial, msg);
+                mMetrics.writeRilSendSms(mPhoneId, rr.mSerial, SmsSession.Event.Tech.SMS_CDMA,
+                        SmsSession.Event.Format.SMS_FORMAT_3GPP2);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendCdmaSms", e);
+            }
+        }
+    }
+
+    @Override
+    public void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " success = " + success + " cause = " + cause);
+            }
+
+            CdmaSmsAck msg = new CdmaSmsAck();
+            msg.errorClass = success ? 0 : 1;
+            msg.smsCauseCode = cause;
+
+            try {
+                radioProxy.acknowledgeLastIncomingCdmaSms(rr.mSerial, msg);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "acknowledgeLastIncomingCdmaSms", e);
+            }
+        }
+    }
+
+    @Override
+    public void getGsmBroadcastConfig(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GSM_GET_BROADCAST_CONFIG, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getGsmBroadcastConfig(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getGsmBroadcastConfig", e);
+            }
+        }
+    }
+
+    @Override
+    public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GSM_SET_BROADCAST_CONFIG, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " with " + config.length + " configs : ");
+                for (int i = 0; i < config.length; i++) {
+                    riljLog(config[i].toString());
+                }
+            }
+
+            ArrayList<GsmBroadcastSmsConfigInfo> configs = new ArrayList<>();
+
+            int numOfConfig = config.length;
+            GsmBroadcastSmsConfigInfo info;
+
+            for (int i = 0; i < numOfConfig; i++) {
+                info = new GsmBroadcastSmsConfigInfo();
+                info.fromServiceId = config[i].getFromServiceId();
+                info.toServiceId = config[i].getToServiceId();
+                info.fromCodeScheme = config[i].getFromCodeScheme();
+                info.toCodeScheme = config[i].getToCodeScheme();
+                info.selected = config[i].isSelected();
+                configs.add(info);
+            }
+
+            try {
+                radioProxy.setGsmBroadcastConfig(rr.mSerial, configs);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setGsmBroadcastConfig", e);
+            }
+        }
+    }
+
+    @Override
+    public void setGsmBroadcastActivation(boolean activate, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GSM_BROADCAST_ACTIVATION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " activate = " + activate);
+            }
+
+            try {
+                radioProxy.setGsmBroadcastActivation(rr.mSerial, activate);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setGsmBroadcastActivation", e);
+            }
+        }
+    }
+
+    @Override
+    public void getCdmaBroadcastConfig(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getCdmaBroadcastConfig(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getCdmaBroadcastConfig", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG, result,
+                    mRILDefaultWorkSource);
+
+            ArrayList<CdmaBroadcastSmsConfigInfo> halConfigs = new ArrayList<>();
+
+            for (CdmaSmsBroadcastConfigInfo config: configs) {
+                for (int i = config.getFromServiceCategory();
+                        i <= config.getToServiceCategory();
+                        i++) {
+                    CdmaBroadcastSmsConfigInfo info = new CdmaBroadcastSmsConfigInfo();
+                    info.serviceCategory = i;
+                    info.language = config.getLanguage();
+                    info.selected = config.isSelected();
+                    halConfigs.add(info);
+                }
+            }
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " with " + halConfigs.size() + " configs : ");
+                for (CdmaBroadcastSmsConfigInfo config : halConfigs) {
+                    riljLog(config.toString());
+                }
+            }
+
+            try {
+                radioProxy.setCdmaBroadcastConfig(rr.mSerial, halConfigs);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCdmaBroadcastConfig", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCdmaBroadcastActivation(boolean activate, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_BROADCAST_ACTIVATION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " activate = " + activate);
+            }
+
+            try {
+                radioProxy.setCdmaBroadcastActivation(rr.mSerial, activate);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCdmaBroadcastActivation", e);
+            }
+        }
+    }
+
+    @Override
+    public void getCDMASubscription(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_SUBSCRIPTION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getCDMASubscription(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getCDMASubscription", e);
+            }
+        }
+    }
+
+    @Override
+    public void writeSmsToRuim(int status, String pdu, Message result) {
+        status = translateStatus(status);
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGV) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest)
+                        + " status = " + status);
+            }
+
+            CdmaSmsWriteArgs args = new CdmaSmsWriteArgs();
+            args.status = status;
+            constructCdmaSendSmsRilRequest(args.message, pdu.getBytes());
+
+            try {
+                radioProxy.writeSmsToRuim(rr.mSerial, args);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "writeSmsToRuim", e);
+            }
+        }
+    }
+
+    @Override
+    public void deleteSmsOnRuim(int index, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGV) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest)
+                        + " index = " + index);
+            }
+
+            try {
+                radioProxy.deleteSmsOnRuim(rr.mSerial, index);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "deleteSmsOnRuim", e);
+            }
+        }
+    }
+
+    @Override
+    public void getDeviceIdentity(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_DEVICE_IDENTITY, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getDeviceIdentity(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getDeviceIdentity", e);
+            }
+        }
+    }
+
+    @Override
+    public void exitEmergencyCallbackMode(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_EXIT_EMERGENCY_CALLBACK_MODE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.exitEmergencyCallbackMode(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "exitEmergencyCallbackMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void getSmscAddress(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_SMSC_ADDRESS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getSmscAddress(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getSmscAddress", e);
+            }
+        }
+    }
+
+    @Override
+    public void setSmscAddress(String address, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_SMSC_ADDRESS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " address = " + address);
+            }
+
+            try {
+                radioProxy.setSmscAddress(rr.mSerial, convertNullToEmptyString(address));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setSmscAddress", e);
+            }
+        }
+    }
+
+    @Override
+    public void reportSmsMemoryStatus(boolean available, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_REPORT_SMS_MEMORY_STATUS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> "
+                        + requestToString(rr.mRequest) + " available = " + available);
+            }
+
+            try {
+                radioProxy.reportSmsMemoryStatus(rr.mSerial, available);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "reportSmsMemoryStatus", e);
+            }
+        }
+    }
+
+    @Override
+    public void reportStkServiceIsRunning(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.reportStkServiceIsRunning(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "reportStkServiceIsRunning", e);
+            }
+        }
+    }
+
+    @Override
+    public void getCdmaSubscriptionSource(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getCdmaSubscriptionSource(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getCdmaSubscriptionSource", e);
+            }
+        }
+    }
+
+    @Override
+    public void requestIsimAuthentication(String nonce, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ISIM_AUTHENTICATION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " nonce = " + nonce);
+            }
+
+            try {
+                radioProxy.requestIsimAuthentication(rr.mSerial, convertNullToEmptyString(nonce));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "requestIsimAuthentication", e);
+            }
+        }
+    }
+
+    @Override
+    public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " success = " + success);
+            }
+
+            try {
+                radioProxy.acknowledgeIncomingGsmSmsWithPdu(rr.mSerial, success,
+                        convertNullToEmptyString(ackPdu));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "acknowledgeIncomingGsmSmsWithPdu", e);
+            }
+        }
+    }
+
+    @Override
+    public void getVoiceRadioTechnology(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_VOICE_RADIO_TECH, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getVoiceRadioTechnology(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getVoiceRadioTechnology", e);
+            }
+        }
+    }
+
+    @Override
+    public void getCellInfoList(Message result, WorkSource workSource) {
+        workSource = getDeafultWorkSourceIfInvalid(workSource);
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_CELL_INFO_LIST, result,
+                    workSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.getCellInfoList(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getCellInfoList", e);
+            }
+        }
+    }
+
+    @Override
+    public void setCellInfoListRate(int rateInMillis, Message result, WorkSource workSource) {
+        workSource = getDeafultWorkSourceIfInvalid(workSource);
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE, result,
+                    workSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " rateInMillis = " + rateInMillis);
+            }
+
+            try {
+                radioProxy.setCellInfoListRate(rr.mSerial, rateInMillis);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setCellInfoListRate", e);
+            }
+        }
+    }
+
+    void setCellInfoListRate() {
+        setCellInfoListRate(Integer.MAX_VALUE, null, mRILDefaultWorkSource);
+    }
+
+    @Override
+    public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming, Message result) {
+
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_INITIAL_ATTACH_APN, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + dataProfile);
+            }
+
+            try {
+                radioProxy.setInitialAttachApn(rr.mSerial, convertToHalDataProfile(dataProfile),
+                        dataProfile.modemCognitive, isRoaming);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setInitialAttachApn", e);
+            }
+        }
+    }
+
+    @Override
+    public void getImsRegistrationState(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_IMS_REGISTRATION_STATE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.getImsRegistrationState(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getImsRegistrationState", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendImsGsmSms(String smscPdu, String pdu, int retry, int messageRef,
+                   Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_IMS_SEND_SMS, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function args for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            ImsSmsMessage msg = new ImsSmsMessage();
+            msg.tech = RILConstants.GSM_PHONE;
+            msg.retry = (byte) retry == 1 ? true : false;
+            msg.messageRef = messageRef;
+
+            GsmSmsMessage gsmMsg = constructGsmSendSmsRilRequest(smscPdu, pdu);
+            msg.gsmMessage.add(gsmMsg);
+            try {
+                radioProxy.sendImsSms(rr.mSerial, msg);
+                mMetrics.writeRilSendSms(mPhoneId, rr.mSerial, SmsSession.Event.Tech.SMS_IMS,
+                        SmsSession.Event.Format.SMS_FORMAT_3GPP);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendImsGsmSms", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendImsCdmaSms(byte[] pdu, int retry, int messageRef, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_IMS_SEND_SMS, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function args for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            ImsSmsMessage msg = new ImsSmsMessage();
+            msg.tech = RILConstants.CDMA_PHONE;
+            msg.retry = (byte) retry == 1 ? true : false;
+            msg.messageRef = messageRef;
+
+            CdmaSmsMessage cdmaMsg = new CdmaSmsMessage();
+            constructCdmaSendSmsRilRequest(cdmaMsg, pdu);
+            msg.cdmaMessage.add(cdmaMsg);
+
+            try {
+                radioProxy.sendImsSms(rr.mSerial, msg);
+                mMetrics.writeRilSendSms(mPhoneId, rr.mSerial, SmsSession.Event.Tech.SMS_IMS,
+                        SmsSession.Event.Format.SMS_FORMAT_3GPP2);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendImsCdmaSms", e);
+            }
+        }
+    }
+
+    private SimApdu createSimApdu(int channel, int cla, int instruction, int p1, int p2, int p3,
+                                  String data) {
+        SimApdu msg = new SimApdu();
+        msg.sessionId = channel;
+        msg.cla = cla;
+        msg.instruction = instruction;
+        msg.p1 = p1;
+        msg.p2 = p2;
+        msg.p3 = p3;
+        msg.data = convertNullToEmptyString(data);
+        return msg;
+    }
+
+    @Override
+    public void iccTransmitApduBasicChannel(int cla, int instruction, int p1, int p2,
+                                            int p3, String data, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_TRANSMIT_APDU_BASIC, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " cla = " + cla + " instruction = " + instruction
+                        + " p1 = " + p1 + " p2 = " + " p3 = " + p3 + " data = " + data);
+            }
+
+            SimApdu msg = createSimApdu(0, cla, instruction, p1, p2, p3, data);
+            try {
+                radioProxy.iccTransmitApduBasicChannel(rr.mSerial, msg);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "iccTransmitApduBasicChannel", e);
+            }
+        }
+    }
+
+    @Override
+    public void iccOpenLogicalChannel(String aid, int p2, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_OPEN_CHANNEL, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " aid = " + aid
+                        + " p2 = " + p2);
+            }
+
+            try {
+                radioProxy.iccOpenLogicalChannel(rr.mSerial, convertNullToEmptyString(aid), p2);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "iccOpenLogicalChannel", e);
+            }
+        }
+    }
+
+    @Override
+    public void iccCloseLogicalChannel(int channel, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_CLOSE_CHANNEL, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " channel = "
+                        + channel);
+            }
+
+            try {
+                radioProxy.iccCloseLogicalChannel(rr.mSerial, channel);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "iccCloseLogicalChannel", e);
+            }
+        }
+    }
+
+    @Override
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction,
+                                              int p1, int p2, int p3, String data,
+                                              Message result) {
+        if (channel <= 0) {
+            throw new RuntimeException(
+                    "Invalid channel in iccTransmitApduLogicalChannel: " + channel);
+        }
+
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_TRANSMIT_APDU_CHANNEL, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " channel = "
+                        + channel + " cla = " + cla + " instruction = " + instruction
+                        + " p1 = " + p1 + " p2 = " + " p3 = " + p3 + " data = " + data);
+            }
+
+            SimApdu msg = createSimApdu(channel, cla, instruction, p1, p2, p3, data);
+
+            try {
+                radioProxy.iccTransmitApduLogicalChannel(rr.mSerial, msg);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "iccTransmitApduLogicalChannel", e);
+            }
+        }
+    }
+
+    @Override
+    public void nvReadItem(int itemID, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_NV_READ_ITEM, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " itemId = " + itemID);
+            }
+
+            try {
+                radioProxy.nvReadItem(rr.mSerial, itemID);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "nvReadItem", e);
+            }
+        }
+    }
+
+    @Override
+    public void nvWriteItem(int itemId, String itemValue, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_NV_WRITE_ITEM, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " itemId = " + itemId + " itemValue = " + itemValue);
+            }
+
+            NvWriteItem item = new NvWriteItem();
+            item.itemId = itemId;
+            item.value = convertNullToEmptyString(itemValue);
+
+            try {
+                radioProxy.nvWriteItem(rr.mSerial, item);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "nvWriteItem", e);
+            }
+        }
+    }
+
+    @Override
+    public void nvWriteCdmaPrl(byte[] preferredRoamingList, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_NV_WRITE_CDMA_PRL, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " PreferredRoamingList = 0x"
+                        + IccUtils.bytesToHexString(preferredRoamingList));
+            }
+
+            ArrayList<Byte> arrList = new ArrayList<>();
+            for (int i = 0; i < preferredRoamingList.length; i++) {
+                arrList.add(preferredRoamingList[i]);
+            }
+
+            try {
+                radioProxy.nvWriteCdmaPrl(rr.mSerial, arrList);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "nvWriteCdmaPrl", e);
+            }
+        }
+    }
+
+    @Override
+    public void nvResetConfig(int resetType, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_NV_RESET_CONFIG, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " resetType = " + resetType);
+            }
+
+            try {
+                radioProxy.nvResetConfig(rr.mSerial, convertToHalResetNvType(resetType));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "nvResetConfig", e);
+            }
+        }
+    }
+
+    @Override
+    public void setUiccSubscription(int slotId, int appIndex, int subId,
+                                    int subStatus, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_UICC_SUBSCRIPTION, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " slot = " + slotId + " appIndex = " + appIndex
+                        + " subId = " + subId + " subStatus = " + subStatus);
+            }
+
+            SelectUiccSub info = new SelectUiccSub();
+            info.slot = slotId;
+            info.appIndex = appIndex;
+            info.subType = subId;
+            info.actStatus = subStatus;
+
+            try {
+                radioProxy.setUiccSubscription(rr.mSerial, info);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setUiccSubscription", e);
+            }
+        }
+    }
+
+    @Override
+    public void setDataAllowed(boolean allowed, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_ALLOW_DATA, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " allowed = " + allowed);
+            }
+
+            try {
+                radioProxy.setDataAllowed(rr.mSerial, allowed);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setDataAllowed", e);
+            }
+        }
+    }
+
+    @Override
+    public void getHardwareConfig(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_HARDWARE_CONFIG, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function args for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.getHardwareConfig(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getHardwareConfig", e);
+            }
+        }
+    }
+
+    @Override
+    public void requestIccSimAuthentication(int authContext, String data, String aid,
+                                            Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SIM_AUTHENTICATION, result,
+                    mRILDefaultWorkSource);
+
+            // Do not log function args for privacy
+            if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+            try {
+                radioProxy.requestIccSimAuthentication(rr.mSerial,
+                        authContext,
+                        convertNullToEmptyString(data),
+                        convertNullToEmptyString(aid));
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "requestIccSimAuthentication", e);
+            }
+        }
+    }
+
+    @Override
+    public void setDataProfile(DataProfile[] dps, boolean isRoaming, Message result) {
+
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_DATA_PROFILE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " with data profiles : ");
+                for (DataProfile profile : dps) {
+                    riljLog(profile.toString());
+                }
+            }
+
+            ArrayList<DataProfileInfo> dpis = new ArrayList<>();
+            for (DataProfile dp : dps) {
+                dpis.add(convertToHalDataProfile(dp));
+            }
+
+            try {
+                radioProxy.setDataProfile(rr.mSerial, dpis, isRoaming);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setDataProfile", e);
+            }
+        }
+    }
+
+    @Override
+    public void requestShutdown(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SHUTDOWN, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.requestShutdown(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "requestShutdown", e);
+            }
+        }
+    }
+
+    @Override
+    public void getRadioCapability(Message response) {
+        IRadio radioProxy = getRadioProxy(response);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_RADIO_CAPABILITY, response,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.getRadioCapability(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getRadioCapability", e);
+            }
+        }
+    }
+
+    @Override
+    public void setRadioCapability(RadioCapability rc, Message response) {
+        IRadio radioProxy = getRadioProxy(response);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_RADIO_CAPABILITY, response,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " RadioCapability = " + rc.toString());
+            }
+
+            android.hardware.radio.V1_0.RadioCapability halRc =
+                    new android.hardware.radio.V1_0.RadioCapability();
+
+            halRc.session = rc.getSession();
+            halRc.phase = rc.getPhase();
+            halRc.raf = rc.getRadioAccessFamily();
+            halRc.logicalModemUuid = convertNullToEmptyString(rc.getLogicalModemUuid());
+            halRc.status = rc.getStatus();
+
+            try {
+                radioProxy.setRadioCapability(rr.mSerial, halRc);
+            } catch (Exception e) {
+                handleRadioProxyExceptionForRR(rr, "setRadioCapability", e);
+            }
+        }
+    }
+
+    @Override
+    public void startLceService(int reportIntervalMs, boolean pullMode, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_START_LCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
+                        + " reportIntervalMs = " + reportIntervalMs + " pullMode = " + pullMode);
+            }
+
+            try {
+                radioProxy.startLceService(rr.mSerial, reportIntervalMs, pullMode);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "startLceService", e);
+            }
+        }
+    }
+
+    @Override
+    public void stopLceService(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_STOP_LCE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.stopLceService(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "stopLceService", e);
+            }
+        }
+    }
+
+    @Override
+    public void pullLceData(Message response) {
+        IRadio radioProxy = getRadioProxy(response);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_PULL_LCEDATA, response,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.pullLceData(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "pullLceData", e);
+            }
+        }
+    }
+
+    @Override
+    public void getModemActivityInfo(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_ACTIVITY_INFO, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.getModemActivityInfo(rr.mSerial);
+
+                Message msg = mRilHandler.obtainMessage(EVENT_BLOCKING_RESPONSE_TIMEOUT);
+                msg.obj = null;
+                msg.arg1 = rr.mSerial;
+                mRilHandler.sendMessageDelayed(msg, DEFAULT_BLOCKING_MESSAGE_RESPONSE_TIMEOUT_MS);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getModemActivityInfo", e);
+            }
+        }
+
+
+    }
+
+    @Override
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message result) {
+        checkNotNull(carriers, "Allowed carriers list cannot be null.");
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_ALLOWED_CARRIERS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                String logStr = "";
+                for (int i = 0; i < carriers.size(); i++) {
+                    logStr = logStr + carriers.get(i) + " ";
+                }
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " carriers = "
+                        + logStr);
+            }
+
+            boolean allAllowed;
+            if (carriers.size() == 0) {
+                allAllowed = true;
+            } else {
+                allAllowed = false;
+            }
+            CarrierRestrictions carrierList = new CarrierRestrictions();
+
+            for (CarrierIdentifier ci : carriers) { /* allowed carriers */
+                Carrier c = new Carrier();
+                c.mcc = convertNullToEmptyString(ci.getMcc());
+                c.mnc = convertNullToEmptyString(ci.getMnc());
+                int matchType = CarrierIdentifier.MatchType.ALL;
+                String matchData = null;
+                if (!TextUtils.isEmpty(ci.getSpn())) {
+                    matchType = CarrierIdentifier.MatchType.SPN;
+                    matchData = ci.getSpn();
+                } else if (!TextUtils.isEmpty(ci.getImsi())) {
+                    matchType = CarrierIdentifier.MatchType.IMSI_PREFIX;
+                    matchData = ci.getImsi();
+                } else if (!TextUtils.isEmpty(ci.getGid1())) {
+                    matchType = CarrierIdentifier.MatchType.GID1;
+                    matchData = ci.getGid1();
+                } else if (!TextUtils.isEmpty(ci.getGid2())) {
+                    matchType = CarrierIdentifier.MatchType.GID2;
+                    matchData = ci.getGid2();
+                }
+                c.matchType = matchType;
+                c.matchData = convertNullToEmptyString(matchData);
+                carrierList.allowedCarriers.add(c);
+            }
+
+            /* TODO: add excluded carriers */
+
+            try {
+                radioProxy.setAllowedCarriers(rr.mSerial, allAllowed, carrierList);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setAllowedCarriers", e);
+            }
+        }
+    }
+
+    @Override
+    public void getAllowedCarriers(Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_GET_ALLOWED_CARRIERS, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.getAllowedCarriers(rr.mSerial);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getAllowedCarriers", e);
+            }
+        }
+    }
+
+    @Override
+    public void sendDeviceState(int stateType, boolean state,
+                                Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SEND_DEVICE_STATE, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " "
+                        + stateType + ":" + state);
+            }
+
+            try {
+                radioProxy.sendDeviceState(rr.mSerial, stateType, state);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendDeviceState", e);
+            }
+        }
+    }
+
+    @Override
+    public void setUnsolResponseFilter(int filter, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_UNSOLICITED_RESPONSE_FILTER, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " " + filter);
+            }
+
+            try {
+                radioProxy.setIndicationFilter(rr.mSerial, filter);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "setIndicationFilter", e);
+            }
+        }
+    }
+
+    @Override
+    public void setSimCardPower(int state, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_SET_SIM_CARD_POWER, result,
+                    mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " " + state);
+            }
+            android.hardware.radio.V1_1.IRadio radioProxy11 =
+                    android.hardware.radio.V1_1.IRadio.castFrom(radioProxy);
+            if (radioProxy11 == null) {
+                try {
+                    switch (state) {
+                        case TelephonyManager.CARD_POWER_DOWN: {
+                            radioProxy.setSimCardPower(rr.mSerial, false);
+                            break;
+                        }
+                        case TelephonyManager.CARD_POWER_UP: {
+                            radioProxy.setSimCardPower(rr.mSerial, true);
+                            break;
+                        }
+                        default: {
+                            if (result != null) {
+                                AsyncResult.forMessage(result, null,
+                                        CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+                                result.sendToTarget();
+                            }
+                        }
+                    }
+                } catch (RemoteException | RuntimeException e) {
+                    handleRadioProxyExceptionForRR(rr, "setSimCardPower", e);
+                }
+            } else {
+                try {
+                    radioProxy11.setSimCardPower_1_1(rr.mSerial, state);
+                } catch (RemoteException | RuntimeException e) {
+                    handleRadioProxyExceptionForRR(rr, "setSimCardPower", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                Message result) {
+        checkNotNull(imsiEncryptionInfo, "ImsiEncryptionInfo cannot be null.");
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            android.hardware.radio.V1_1.IRadio radioProxy11 =
+                    android.hardware.radio.V1_1.IRadio.castFrom(radioProxy);
+            if (radioProxy11 == null) {
+                if (result != null) {
+                    AsyncResult.forMessage(result, null,
+                            CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+                    result.sendToTarget();
+                }
+            } else {
+                RILRequest rr = obtainRequest(RIL_REQUEST_SET_CARRIER_INFO_IMSI_ENCRYPTION, result,
+                        mRILDefaultWorkSource);
+                if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+                try {
+                    android.hardware.radio.V1_1.ImsiEncryptionInfo halImsiInfo =
+                            new android.hardware.radio.V1_1.ImsiEncryptionInfo();
+                    halImsiInfo.mnc = imsiEncryptionInfo.getMnc();
+                    halImsiInfo.mcc = imsiEncryptionInfo.getMcc();
+                    halImsiInfo.keyIdentifier = imsiEncryptionInfo.getKeyIdentifier();
+                    if (imsiEncryptionInfo.getExpirationTime() != null) {
+                        halImsiInfo.expirationTime =
+                                imsiEncryptionInfo.getExpirationTime().getTime();
+                    }
+                    for (byte b : imsiEncryptionInfo.getPublicKey().getEncoded()) {
+                        halImsiInfo.carrierKey.add(new Byte(b));
+                    }
+
+                    radioProxy11.setCarrierInfoForImsiEncryption(
+                            rr.mSerial, halImsiInfo);
+                } catch (RemoteException | RuntimeException e) {
+                    handleRadioProxyExceptionForRR(rr, "setCarrierInfoForImsiEncryption", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void getIMEI(Message result) {
+        throw new RuntimeException("getIMEI not expected to be called");
+    }
+
+    @Override
+    public void getIMEISV(Message result) {
+        throw new RuntimeException("getIMEISV not expected to be called");
+    }
+
+    /**
+     * @deprecated
+     */
+    @Deprecated
+    @Override
+    public void getLastPdpFailCause(Message result) {
+        throw new RuntimeException("getLastPdpFailCause not expected to be called");
+    }
+
+    /**
+     * The preferred new alternative to getLastPdpFailCause
+     */
+    @Override
+    public void getLastDataCallFailCause(Message result) {
+        throw new RuntimeException("getLastDataCallFailCause not expected to be called");
+    }
+
+    /**
+     *  Translates EF_SMS status bits to a status value compatible with
+     *  SMS AT commands.  See TS 27.005 3.1.
+     */
+    private int translateStatus(int status) {
+        switch(status & 0x7) {
+            case SmsManager.STATUS_ON_ICC_READ:
+                return 1;
+            case SmsManager.STATUS_ON_ICC_UNREAD:
+                return 0;
+            case SmsManager.STATUS_ON_ICC_SENT:
+                return 3;
+            case SmsManager.STATUS_ON_ICC_UNSENT:
+                return 2;
+        }
+
+        // Default to READ.
+        return 1;
+    }
+
+    @Override
+    public void resetRadio(Message result) {
+        throw new RuntimeException("resetRadio not expected to be called");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void handleCallSetupRequestFromSim(boolean accept, Message result) {
+        IRadio radioProxy = getRadioProxy(result);
+        if (radioProxy != null) {
+            RILRequest rr = obtainRequest(RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM,
+                    result, mRILDefaultWorkSource);
+
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+            }
+
+            try {
+                radioProxy.handleStkCallSetupRequestFromSim(rr.mSerial, accept);
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "getAllowedCarriers", e);
+            }
+        }
+    }
+
+    //***** Private Methods
+
+    /**
+     * This is a helper function to be called when a RadioIndication callback is called.
+     * It takes care of acquiring wakelock and sending ack if needed.
+     * @param indicationType RadioIndicationType received
+     */
+    void processIndication(int indicationType) {
+        if (indicationType == RadioIndicationType.UNSOLICITED_ACK_EXP) {
+            sendAck();
+            if (RILJ_LOGD) riljLog("Unsol response received; Sending ack to ril.cpp");
+        } else {
+            // ack is not expected to be sent back. Nothing is required to be done here.
+        }
+    }
+
+    void processRequestAck(int serial) {
+        RILRequest rr;
+        synchronized (mRequestList) {
+            rr = mRequestList.get(serial);
+        }
+        if (rr == null) {
+            Rlog.w(RIL.RILJ_LOG_TAG, "processRequestAck: Unexpected solicited ack response! "
+                    + "serial: " + serial);
+        } else {
+            decrementWakeLock(rr);
+            if (RIL.RILJ_LOGD) {
+                riljLog(rr.serialString() + " Ack < " + RIL.requestToString(rr.mRequest));
+            }
+        }
+    }
+
+    /**
+     * This is a helper function to be called when a RadioResponse callback is called.
+     * It takes care of acks, wakelocks, and finds and returns RILRequest corresponding to the
+     * response if one is found.
+     * @param responseInfo RadioResponseInfo received in response callback
+     * @return RILRequest corresponding to the response
+     */
+    @VisibleForTesting
+    public RILRequest processResponse(RadioResponseInfo responseInfo) {
+        int serial = responseInfo.serial;
+        int error = responseInfo.error;
+        int type = responseInfo.type;
+
+        RILRequest rr = null;
+
+        if (type == RadioResponseType.SOLICITED_ACK) {
+            synchronized (mRequestList) {
+                rr = mRequestList.get(serial);
+            }
+            if (rr == null) {
+                Rlog.w(RILJ_LOG_TAG, "Unexpected solicited ack response! sn: " + serial);
+            } else {
+                decrementWakeLock(rr);
+                if (RILJ_LOGD) {
+                    riljLog(rr.serialString() + " Ack < " + requestToString(rr.mRequest));
+                }
+            }
+            return rr;
+        }
+
+        rr = findAndRemoveRequestFromList(serial);
+        if (rr == null) {
+            Rlog.e(RIL.RILJ_LOG_TAG, "processResponse: Unexpected response! serial: " + serial
+                    + " error: " + error);
+            return null;
+        }
+
+        // Time logging for RIL command and storing it in TelephonyHistogram.
+        addToRilHistogram(rr);
+
+        if (type == RadioResponseType.SOLICITED_ACK_EXP) {
+            sendAck();
+            if (RIL.RILJ_LOGD) {
+                riljLog("Response received for " + rr.serialString() + " "
+                        + RIL.requestToString(rr.mRequest) + " Sending ack to ril.cpp");
+            }
+        } else {
+            // ack sent for SOLICITED_ACK_EXP above; nothing to do for SOLICITED response
+        }
+
+        // Here and below fake RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED, see b/7255789.
+        // This is needed otherwise we don't automatically transition to the main lock
+        // screen when the pin or puk is entered incorrectly.
+        switch (rr.mRequest) {
+            case RIL_REQUEST_ENTER_SIM_PUK:
+            case RIL_REQUEST_ENTER_SIM_PUK2:
+                if (mIccStatusChangedRegistrants != null) {
+                    if (RILJ_LOGD) {
+                        riljLog("ON enter sim puk fakeSimStatusChanged: reg count="
+                                + mIccStatusChangedRegistrants.size());
+                    }
+                    mIccStatusChangedRegistrants.notifyRegistrants();
+                }
+                break;
+            case RIL_REQUEST_SHUTDOWN:
+                setRadioState(RadioState.RADIO_UNAVAILABLE);
+                break;
+        }
+
+        if (error != RadioError.NONE) {
+            switch (rr.mRequest) {
+                case RIL_REQUEST_ENTER_SIM_PIN:
+                case RIL_REQUEST_ENTER_SIM_PIN2:
+                case RIL_REQUEST_CHANGE_SIM_PIN:
+                case RIL_REQUEST_CHANGE_SIM_PIN2:
+                case RIL_REQUEST_SET_FACILITY_LOCK:
+                    if (mIccStatusChangedRegistrants != null) {
+                        if (RILJ_LOGD) {
+                            riljLog("ON some errors fakeSimStatusChanged: reg count="
+                                    + mIccStatusChangedRegistrants.size());
+                        }
+                        mIccStatusChangedRegistrants.notifyRegistrants();
+                    }
+                    break;
+
+            }
+        } else {
+            switch (rr.mRequest) {
+                case RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND:
+                if (mTestingEmergencyCall.getAndSet(false)) {
+                    if (mEmergencyCallbackModeRegistrant != null) {
+                        riljLog("testing emergency call, notify ECM Registrants");
+                        mEmergencyCallbackModeRegistrant.notifyRegistrant();
+                    }
+                }
+            }
+        }
+        return rr;
+    }
+
+    /**
+     * This is a helper function to be called at the end of all RadioResponse callbacks.
+     * It takes care of sending error response, logging, decrementing wakelock if needed, and
+     * releases the request from memory pool.
+     * @param rr RILRequest for which response callback was called
+     * @param responseInfo RadioResponseInfo received in the callback
+     * @param ret object to be returned to request sender
+     */
+    @VisibleForTesting
+    public void processResponseDone(RILRequest rr, RadioResponseInfo responseInfo, Object ret) {
+        if (responseInfo.error == 0) {
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "< " + requestToString(rr.mRequest)
+                        + " " + retToString(rr.mRequest, ret));
+            }
+        } else {
+            if (RILJ_LOGD) {
+                riljLog(rr.serialString() + "< " + requestToString(rr.mRequest)
+                        + " error " + responseInfo.error);
+            }
+            rr.onError(responseInfo.error, ret);
+        }
+        mMetrics.writeOnRilSolicitedResponse(mPhoneId, rr.mSerial, responseInfo.error,
+                rr.mRequest, ret);
+        if (rr != null) {
+            if (responseInfo.type == RadioResponseType.SOLICITED) {
+                decrementWakeLock(rr);
+            }
+            rr.release();
+        }
+    }
+
+    /**
+     * Function to send ack and acquire related wakelock
+     */
+    private void sendAck() {
+        // TODO: Remove rr and clean up acquireWakelock for response and ack
+        RILRequest rr = RILRequest.obtain(RIL_RESPONSE_ACKNOWLEDGEMENT, null,
+                mRILDefaultWorkSource);
+        acquireWakeLock(rr, RIL.FOR_ACK_WAKELOCK);
+        IRadio radioProxy = getRadioProxy(null);
+        if (radioProxy != null) {
+            try {
+                radioProxy.responseAcknowledgement();
+            } catch (RemoteException | RuntimeException e) {
+                handleRadioProxyExceptionForRR(rr, "sendAck", e);
+                riljLoge("sendAck: " + e);
+            }
+        } else {
+            Rlog.e(RILJ_LOG_TAG, "Error trying to send ack, radioProxy = null");
+        }
+        rr.release();
+    }
+
+    private WorkSource getDeafultWorkSourceIfInvalid(WorkSource workSource) {
+        if (workSource == null) {
+            workSource = mRILDefaultWorkSource;
+        }
+
+        return workSource;
+    }
+
+    private String getWorkSourceClientId(WorkSource workSource) {
+        if (workSource != null) {
+            return String.valueOf(workSource.get(0)) + ":" + workSource.getName(0);
+        }
+
+        return null;
+    }
+
+    /**
+     * Holds a PARTIAL_WAKE_LOCK whenever
+     * a) There is outstanding RIL request sent to RIL deamon and no replied
+     * b) There is a request pending to be sent out.
+     *
+     * There is a WAKE_LOCK_TIMEOUT to release the lock, though it shouldn't
+     * happen often.
+     */
+    private void acquireWakeLock(RILRequest rr, int wakeLockType) {
+        synchronized (rr) {
+            if (rr.mWakeLockType != INVALID_WAKELOCK) {
+                Rlog.d(RILJ_LOG_TAG, "Failed to aquire wakelock for " + rr.serialString());
+                return;
+            }
+
+            switch(wakeLockType) {
+                case FOR_WAKELOCK:
+                    synchronized (mWakeLock) {
+                        mWakeLock.acquire();
+                        mWakeLockCount++;
+                        mWlSequenceNum++;
+
+                        String clientId = getWorkSourceClientId(rr.mWorkSource);
+                        if (!mClientWakelockTracker.isClientActive(clientId)) {
+                            if (mActiveWakelockWorkSource != null) {
+                                mActiveWakelockWorkSource.add(rr.mWorkSource);
+                            } else {
+                                mActiveWakelockWorkSource = rr.mWorkSource;
+                            }
+                            mWakeLock.setWorkSource(mActiveWakelockWorkSource);
+                        }
+
+                        mClientWakelockTracker.startTracking(rr.mClientId,
+                                rr.mRequest, rr.mSerial, mWakeLockCount);
+
+                        Message msg = mRilHandler.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT);
+                        msg.arg1 = mWlSequenceNum;
+                        mRilHandler.sendMessageDelayed(msg, mWakeLockTimeout);
+                    }
+                    break;
+                case FOR_ACK_WAKELOCK:
+                    synchronized (mAckWakeLock) {
+                        mAckWakeLock.acquire();
+                        mAckWlSequenceNum++;
+
+                        Message msg = mRilHandler.obtainMessage(EVENT_ACK_WAKE_LOCK_TIMEOUT);
+                        msg.arg1 = mAckWlSequenceNum;
+                        mRilHandler.sendMessageDelayed(msg, mAckWakeLockTimeout);
+                    }
+                    break;
+                default: //WTF
+                    Rlog.w(RILJ_LOG_TAG, "Acquiring Invalid Wakelock type " + wakeLockType);
+                    return;
+            }
+            rr.mWakeLockType = wakeLockType;
+        }
+    }
+
+    /** Returns the wake lock of the given type. */
+    @VisibleForTesting
+    public WakeLock getWakeLock(int wakeLockType) {
+        return wakeLockType == FOR_WAKELOCK ? mWakeLock : mAckWakeLock;
+    }
+
+    /** Returns the {@link RilHandler} instance. */
+    @VisibleForTesting
+    public RilHandler getRilHandler() {
+        return mRilHandler;
+    }
+
+    /** Returns the Ril request list. */
+    @VisibleForTesting
+    public SparseArray<RILRequest> getRilRequestList() {
+        return mRequestList;
+    }
+
+    private void decrementWakeLock(RILRequest rr) {
+        synchronized (rr) {
+            switch(rr.mWakeLockType) {
+                case FOR_WAKELOCK:
+                    synchronized (mWakeLock) {
+                        mClientWakelockTracker.stopTracking(rr.mClientId,
+                                rr.mRequest, rr.mSerial,
+                                (mWakeLockCount > 1) ? mWakeLockCount - 1 : 0);
+                        String clientId = getWorkSourceClientId(rr.mWorkSource);;
+                        if (!mClientWakelockTracker.isClientActive(clientId)
+                                && (mActiveWakelockWorkSource != null)) {
+                            mActiveWakelockWorkSource.remove(rr.mWorkSource);
+                            if (mActiveWakelockWorkSource.size() == 0) {
+                                mActiveWakelockWorkSource = null;
+                            }
+                            mWakeLock.setWorkSource(mActiveWakelockWorkSource);
+                        }
+
+                        if (mWakeLockCount > 1) {
+                            mWakeLockCount--;
+                        } else {
+                            mWakeLockCount = 0;
+                            mWakeLock.release();
+                        }
+                    }
+                    break;
+                case FOR_ACK_WAKELOCK:
+                    //We do not decrement the ACK wakelock
+                    break;
+                case INVALID_WAKELOCK:
+                    break;
+                default:
+                    Rlog.w(RILJ_LOG_TAG, "Decrementing Invalid Wakelock type " + rr.mWakeLockType);
+            }
+            rr.mWakeLockType = INVALID_WAKELOCK;
+        }
+    }
+
+    private boolean clearWakeLock(int wakeLockType) {
+        if (wakeLockType == FOR_WAKELOCK) {
+            synchronized (mWakeLock) {
+                if (mWakeLockCount == 0 && !mWakeLock.isHeld()) return false;
+                Rlog.d(RILJ_LOG_TAG, "NOTE: mWakeLockCount is " + mWakeLockCount
+                        + "at time of clearing");
+                mWakeLockCount = 0;
+                mWakeLock.release();
+                mClientWakelockTracker.stopTrackingAll();
+                mActiveWakelockWorkSource = null;
+                return true;
+            }
+        } else {
+            synchronized (mAckWakeLock) {
+                if (!mAckWakeLock.isHeld()) return false;
+                mAckWakeLock.release();
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Release each request in mRequestList then clear the list
+     * @param error is the RIL_Errno sent back
+     * @param loggable true means to print all requests in mRequestList
+     */
+    private void clearRequestList(int error, boolean loggable) {
+        RILRequest rr;
+        synchronized (mRequestList) {
+            int count = mRequestList.size();
+            if (RILJ_LOGD && loggable) {
+                Rlog.d(RILJ_LOG_TAG, "clearRequestList " + " mWakeLockCount="
+                        + mWakeLockCount + " mRequestList=" + count);
+            }
+
+            for (int i = 0; i < count; i++) {
+                rr = mRequestList.valueAt(i);
+                if (RILJ_LOGD && loggable) {
+                    Rlog.d(RILJ_LOG_TAG, i + ": [" + rr.mSerial + "] "
+                            + requestToString(rr.mRequest));
+                }
+                rr.onError(error, null);
+                decrementWakeLock(rr);
+                rr.release();
+            }
+            mRequestList.clear();
+        }
+    }
+
+    private RILRequest findAndRemoveRequestFromList(int serial) {
+        RILRequest rr = null;
+        synchronized (mRequestList) {
+            rr = mRequestList.get(serial);
+            if (rr != null) {
+                mRequestList.remove(serial);
+            }
+        }
+
+        return rr;
+    }
+
+    private void addToRilHistogram(RILRequest rr) {
+        long endTime = SystemClock.elapsedRealtime();
+        int totalTime = (int) (endTime - rr.mStartTimeMs);
+
+        synchronized (mRilTimeHistograms) {
+            TelephonyHistogram entry = mRilTimeHistograms.get(rr.mRequest);
+            if (entry == null) {
+                // We would have total #RIL_HISTOGRAM_BUCKET_COUNT range buckets for RIL commands
+                entry = new TelephonyHistogram(TelephonyHistogram.TELEPHONY_CATEGORY_RIL,
+                        rr.mRequest, RIL_HISTOGRAM_BUCKET_COUNT);
+                mRilTimeHistograms.put(rr.mRequest, entry);
+            }
+            entry.addTimeTaken(totalTime);
+        }
+    }
+
+    RadioCapability makeStaticRadioCapability() {
+        // default to UNKNOWN so we fail fast.
+        int raf = RadioAccessFamily.RAF_UNKNOWN;
+
+        String rafString = mContext.getResources().getString(
+                com.android.internal.R.string.config_radio_access_family);
+        if (!TextUtils.isEmpty(rafString)) {
+            raf = RadioAccessFamily.rafTypeFromString(rafString);
+        }
+        RadioCapability rc = new RadioCapability(mPhoneId.intValue(), 0, 0, raf,
+                "", RadioCapability.RC_STATUS_SUCCESS);
+        if (RILJ_LOGD) riljLog("Faking RIL_REQUEST_GET_RADIO_CAPABILITY response using " + raf);
+        return rc;
+    }
+
+    static String retToString(int req, Object ret) {
+        if (ret == null) return "";
+        switch (req) {
+            // Don't log these return values, for privacy's sake.
+            case RIL_REQUEST_GET_IMSI:
+            case RIL_REQUEST_GET_IMEI:
+            case RIL_REQUEST_GET_IMEISV:
+            case RIL_REQUEST_SIM_OPEN_CHANNEL:
+            case RIL_REQUEST_SIM_TRANSMIT_APDU_CHANNEL:
+
+                if (!RILJ_LOGV) {
+                    // If not versbose logging just return and don't display IMSI and IMEI, IMEISV
+                    return "";
+                }
+        }
+
+        StringBuilder sb;
+        String s;
+        int length;
+        if (ret instanceof int[]) {
+            int[] intArray = (int[]) ret;
+            length = intArray.length;
+            sb = new StringBuilder("{");
+            if (length > 0) {
+                int i = 0;
+                sb.append(intArray[i++]);
+                while (i < length) {
+                    sb.append(", ").append(intArray[i++]);
+                }
+            }
+            sb.append("}");
+            s = sb.toString();
+        } else if (ret instanceof String[]) {
+            String[] strings = (String[]) ret;
+            length = strings.length;
+            sb = new StringBuilder("{");
+            if (length > 0) {
+                int i = 0;
+                sb.append(strings[i++]);
+                while (i < length) {
+                    sb.append(", ").append(strings[i++]);
+                }
+            }
+            sb.append("}");
+            s = sb.toString();
+        } else if (req == RIL_REQUEST_GET_CURRENT_CALLS) {
+            ArrayList<DriverCall> calls = (ArrayList<DriverCall>) ret;
+            sb = new StringBuilder("{");
+            for (DriverCall dc : calls) {
+                sb.append("[").append(dc).append("] ");
+            }
+            sb.append("}");
+            s = sb.toString();
+        } else if (req == RIL_REQUEST_GET_NEIGHBORING_CELL_IDS) {
+            ArrayList<NeighboringCellInfo> cells = (ArrayList<NeighboringCellInfo>) ret;
+            sb = new StringBuilder("{");
+            for (NeighboringCellInfo cell : cells) {
+                sb.append("[").append(cell).append("] ");
+            }
+            sb.append("}");
+            s = sb.toString();
+        } else if (req == RIL_REQUEST_QUERY_CALL_FORWARD_STATUS) {
+            CallForwardInfo[] cinfo = (CallForwardInfo[]) ret;
+            length = cinfo.length;
+            sb = new StringBuilder("{");
+            for (int i = 0; i < length; i++) {
+                sb.append("[").append(cinfo[i]).append("] ");
+            }
+            sb.append("}");
+            s = sb.toString();
+        } else if (req == RIL_REQUEST_GET_HARDWARE_CONFIG) {
+            ArrayList<HardwareConfig> hwcfgs = (ArrayList<HardwareConfig>) ret;
+            sb = new StringBuilder(" ");
+            for (HardwareConfig hwcfg : hwcfgs) {
+                sb.append("[").append(hwcfg).append("] ");
+            }
+            s = sb.toString();
+        } else {
+            s = ret.toString();
+        }
+        return s;
+    }
+
+    void writeMetricsNewSms(int tech, int format) {
+        mMetrics.writeRilNewSms(mPhoneId, tech, format);
+    }
+
+    void writeMetricsCallRing(char[] response) {
+        mMetrics.writeRilCallRing(mPhoneId, response);
+    }
+
+    void writeMetricsSrvcc(int state) {
+        mMetrics.writeRilSrvcc(mPhoneId, state);
+    }
+
+    void writeMetricsModemRestartEvent(String reason) {
+        mMetrics.writeModemRestartEvent(mPhoneId, reason);
+    }
+
+    /**
+     * Notify all registrants that the ril has connected or disconnected.
+     *
+     * @param rilVer is the version of the ril or -1 if disconnected.
+     */
+    void notifyRegistrantsRilConnectionChanged(int rilVer) {
+        mRilVersion = rilVer;
+        if (mRilConnectedRegistrants != null) {
+            mRilConnectedRegistrants.notifyRegistrants(
+                    new AsyncResult(null, new Integer(rilVer), null));
+        }
+    }
+
+    void
+    notifyRegistrantsCdmaInfoRec(CdmaInformationRecords infoRec) {
+        int response = RIL_UNSOL_CDMA_INFO_REC;
+        if (infoRec.record instanceof CdmaInformationRecords.CdmaDisplayInfoRec) {
+            if (mDisplayInfoRegistrants != null) {
+                if (RILJ_LOGD) unsljLogRet(response, infoRec.record);
+                mDisplayInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        } else if (infoRec.record instanceof CdmaInformationRecords.CdmaSignalInfoRec) {
+            if (mSignalInfoRegistrants != null) {
+                if (RILJ_LOGD) unsljLogRet(response, infoRec.record);
+                mSignalInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        } else if (infoRec.record instanceof CdmaInformationRecords.CdmaNumberInfoRec) {
+            if (mNumberInfoRegistrants != null) {
+                if (RILJ_LOGD) unsljLogRet(response, infoRec.record);
+                mNumberInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        } else if (infoRec.record instanceof CdmaInformationRecords.CdmaRedirectingNumberInfoRec) {
+            if (mRedirNumInfoRegistrants != null) {
+                if (RILJ_LOGD) unsljLogRet(response, infoRec.record);
+                mRedirNumInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        } else if (infoRec.record instanceof CdmaInformationRecords.CdmaLineControlInfoRec) {
+            if (mLineControlInfoRegistrants != null) {
+                if (RILJ_LOGD) unsljLogRet(response, infoRec.record);
+                mLineControlInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        } else if (infoRec.record instanceof CdmaInformationRecords.CdmaT53ClirInfoRec) {
+            if (mT53ClirInfoRegistrants != null) {
+                if (RILJ_LOGD) unsljLogRet(response, infoRec.record);
+                mT53ClirInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        } else if (infoRec.record instanceof CdmaInformationRecords.CdmaT53AudioControlInfoRec) {
+            if (mT53AudCntrlInfoRegistrants != null) {
+                if (RILJ_LOGD) {
+                    unsljLogRet(response, infoRec.record);
+                }
+                mT53AudCntrlInfoRegistrants.notifyRegistrants(
+                        new AsyncResult(null, infoRec.record, null));
+            }
+        }
+    }
+
+    static String requestToString(int request) {
+        switch(request) {
+            case RIL_REQUEST_GET_SIM_STATUS:
+                return "GET_SIM_STATUS";
+            case RIL_REQUEST_ENTER_SIM_PIN:
+                return "ENTER_SIM_PIN";
+            case RIL_REQUEST_ENTER_SIM_PUK:
+                return "ENTER_SIM_PUK";
+            case RIL_REQUEST_ENTER_SIM_PIN2:
+                return "ENTER_SIM_PIN2";
+            case RIL_REQUEST_ENTER_SIM_PUK2:
+                return "ENTER_SIM_PUK2";
+            case RIL_REQUEST_CHANGE_SIM_PIN:
+                return "CHANGE_SIM_PIN";
+            case RIL_REQUEST_CHANGE_SIM_PIN2:
+                return "CHANGE_SIM_PIN2";
+            case RIL_REQUEST_ENTER_NETWORK_DEPERSONALIZATION:
+                return "ENTER_NETWORK_DEPERSONALIZATION";
+            case RIL_REQUEST_GET_CURRENT_CALLS:
+                return "GET_CURRENT_CALLS";
+            case RIL_REQUEST_DIAL:
+                return "DIAL";
+            case RIL_REQUEST_GET_IMSI:
+                return "GET_IMSI";
+            case RIL_REQUEST_HANGUP:
+                return "HANGUP";
+            case RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND:
+                return "HANGUP_WAITING_OR_BACKGROUND";
+            case RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND:
+                return "HANGUP_FOREGROUND_RESUME_BACKGROUND";
+            case RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE:
+                return "REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE";
+            case RIL_REQUEST_CONFERENCE:
+                return "CONFERENCE";
+            case RIL_REQUEST_UDUB:
+                return "UDUB";
+            case RIL_REQUEST_LAST_CALL_FAIL_CAUSE:
+                return "LAST_CALL_FAIL_CAUSE";
+            case RIL_REQUEST_SIGNAL_STRENGTH:
+                return "SIGNAL_STRENGTH";
+            case RIL_REQUEST_VOICE_REGISTRATION_STATE:
+                return "VOICE_REGISTRATION_STATE";
+            case RIL_REQUEST_DATA_REGISTRATION_STATE:
+                return "DATA_REGISTRATION_STATE";
+            case RIL_REQUEST_OPERATOR:
+                return "OPERATOR";
+            case RIL_REQUEST_RADIO_POWER:
+                return "RADIO_POWER";
+            case RIL_REQUEST_DTMF:
+                return "DTMF";
+            case RIL_REQUEST_SEND_SMS:
+                return "SEND_SMS";
+            case RIL_REQUEST_SEND_SMS_EXPECT_MORE:
+                return "SEND_SMS_EXPECT_MORE";
+            case RIL_REQUEST_SETUP_DATA_CALL:
+                return "SETUP_DATA_CALL";
+            case RIL_REQUEST_SIM_IO:
+                return "SIM_IO";
+            case RIL_REQUEST_SEND_USSD:
+                return "SEND_USSD";
+            case RIL_REQUEST_CANCEL_USSD:
+                return "CANCEL_USSD";
+            case RIL_REQUEST_GET_CLIR:
+                return "GET_CLIR";
+            case RIL_REQUEST_SET_CLIR:
+                return "SET_CLIR";
+            case RIL_REQUEST_QUERY_CALL_FORWARD_STATUS:
+                return "QUERY_CALL_FORWARD_STATUS";
+            case RIL_REQUEST_SET_CALL_FORWARD:
+                return "SET_CALL_FORWARD";
+            case RIL_REQUEST_QUERY_CALL_WAITING:
+                return "QUERY_CALL_WAITING";
+            case RIL_REQUEST_SET_CALL_WAITING:
+                return "SET_CALL_WAITING";
+            case RIL_REQUEST_SMS_ACKNOWLEDGE:
+                return "SMS_ACKNOWLEDGE";
+            case RIL_REQUEST_GET_IMEI:
+                return "GET_IMEI";
+            case RIL_REQUEST_GET_IMEISV:
+                return "GET_IMEISV";
+            case RIL_REQUEST_ANSWER:
+                return "ANSWER";
+            case RIL_REQUEST_DEACTIVATE_DATA_CALL:
+                return "DEACTIVATE_DATA_CALL";
+            case RIL_REQUEST_QUERY_FACILITY_LOCK:
+                return "QUERY_FACILITY_LOCK";
+            case RIL_REQUEST_SET_FACILITY_LOCK:
+                return "SET_FACILITY_LOCK";
+            case RIL_REQUEST_CHANGE_BARRING_PASSWORD:
+                return "CHANGE_BARRING_PASSWORD";
+            case RIL_REQUEST_QUERY_NETWORK_SELECTION_MODE:
+                return "QUERY_NETWORK_SELECTION_MODE";
+            case RIL_REQUEST_SET_NETWORK_SELECTION_AUTOMATIC:
+                return "SET_NETWORK_SELECTION_AUTOMATIC";
+            case RIL_REQUEST_SET_NETWORK_SELECTION_MANUAL:
+                return "SET_NETWORK_SELECTION_MANUAL";
+            case RIL_REQUEST_QUERY_AVAILABLE_NETWORKS :
+                return "QUERY_AVAILABLE_NETWORKS ";
+            case RIL_REQUEST_DTMF_START:
+                return "DTMF_START";
+            case RIL_REQUEST_DTMF_STOP:
+                return "DTMF_STOP";
+            case RIL_REQUEST_BASEBAND_VERSION:
+                return "BASEBAND_VERSION";
+            case RIL_REQUEST_SEPARATE_CONNECTION:
+                return "SEPARATE_CONNECTION";
+            case RIL_REQUEST_SET_MUTE:
+                return "SET_MUTE";
+            case RIL_REQUEST_GET_MUTE:
+                return "GET_MUTE";
+            case RIL_REQUEST_QUERY_CLIP:
+                return "QUERY_CLIP";
+            case RIL_REQUEST_LAST_DATA_CALL_FAIL_CAUSE:
+                return "LAST_DATA_CALL_FAIL_CAUSE";
+            case RIL_REQUEST_DATA_CALL_LIST:
+                return "DATA_CALL_LIST";
+            case RIL_REQUEST_RESET_RADIO:
+                return "RESET_RADIO";
+            case RIL_REQUEST_OEM_HOOK_RAW:
+                return "OEM_HOOK_RAW";
+            case RIL_REQUEST_OEM_HOOK_STRINGS:
+                return "OEM_HOOK_STRINGS";
+            case RIL_REQUEST_SCREEN_STATE:
+                return "SCREEN_STATE";
+            case RIL_REQUEST_SET_SUPP_SVC_NOTIFICATION:
+                return "SET_SUPP_SVC_NOTIFICATION";
+            case RIL_REQUEST_WRITE_SMS_TO_SIM:
+                return "WRITE_SMS_TO_SIM";
+            case RIL_REQUEST_DELETE_SMS_ON_SIM:
+                return "DELETE_SMS_ON_SIM";
+            case RIL_REQUEST_SET_BAND_MODE:
+                return "SET_BAND_MODE";
+            case RIL_REQUEST_QUERY_AVAILABLE_BAND_MODE:
+                return "QUERY_AVAILABLE_BAND_MODE";
+            case RIL_REQUEST_STK_GET_PROFILE:
+                return "REQUEST_STK_GET_PROFILE";
+            case RIL_REQUEST_STK_SET_PROFILE:
+                return "REQUEST_STK_SET_PROFILE";
+            case RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND:
+                return "REQUEST_STK_SEND_ENVELOPE_COMMAND";
+            case RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE:
+                return "REQUEST_STK_SEND_TERMINAL_RESPONSE";
+            case RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM:
+                return "REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM";
+            case RIL_REQUEST_EXPLICIT_CALL_TRANSFER: return "REQUEST_EXPLICIT_CALL_TRANSFER";
+            case RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE:
+                return "REQUEST_SET_PREFERRED_NETWORK_TYPE";
+            case RIL_REQUEST_GET_PREFERRED_NETWORK_TYPE:
+                return "REQUEST_GET_PREFERRED_NETWORK_TYPE";
+            case RIL_REQUEST_GET_NEIGHBORING_CELL_IDS:
+                return "REQUEST_GET_NEIGHBORING_CELL_IDS";
+            case RIL_REQUEST_SET_LOCATION_UPDATES:
+                return "REQUEST_SET_LOCATION_UPDATES";
+            case RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE:
+                return "RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE";
+            case RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE:
+                return "RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE";
+            case RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE:
+                return "RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE";
+            case RIL_REQUEST_SET_TTY_MODE:
+                return "RIL_REQUEST_SET_TTY_MODE";
+            case RIL_REQUEST_QUERY_TTY_MODE:
+                return "RIL_REQUEST_QUERY_TTY_MODE";
+            case RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE:
+                return "RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE";
+            case RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE:
+                return "RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE";
+            case RIL_REQUEST_CDMA_FLASH:
+                return "RIL_REQUEST_CDMA_FLASH";
+            case RIL_REQUEST_CDMA_BURST_DTMF:
+                return "RIL_REQUEST_CDMA_BURST_DTMF";
+            case RIL_REQUEST_CDMA_SEND_SMS:
+                return "RIL_REQUEST_CDMA_SEND_SMS";
+            case RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE:
+                return "RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE";
+            case RIL_REQUEST_GSM_GET_BROADCAST_CONFIG:
+                return "RIL_REQUEST_GSM_GET_BROADCAST_CONFIG";
+            case RIL_REQUEST_GSM_SET_BROADCAST_CONFIG:
+                return "RIL_REQUEST_GSM_SET_BROADCAST_CONFIG";
+            case RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG:
+                return "RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG";
+            case RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG:
+                return "RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG";
+            case RIL_REQUEST_GSM_BROADCAST_ACTIVATION:
+                return "RIL_REQUEST_GSM_BROADCAST_ACTIVATION";
+            case RIL_REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY:
+                return "RIL_REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY";
+            case RIL_REQUEST_CDMA_BROADCAST_ACTIVATION:
+                return "RIL_REQUEST_CDMA_BROADCAST_ACTIVATION";
+            case RIL_REQUEST_CDMA_SUBSCRIPTION:
+                return "RIL_REQUEST_CDMA_SUBSCRIPTION";
+            case RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM:
+                return "RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM";
+            case RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM:
+                return "RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM";
+            case RIL_REQUEST_DEVICE_IDENTITY:
+                return "RIL_REQUEST_DEVICE_IDENTITY";
+            case RIL_REQUEST_GET_SMSC_ADDRESS:
+                return "RIL_REQUEST_GET_SMSC_ADDRESS";
+            case RIL_REQUEST_SET_SMSC_ADDRESS:
+                return "RIL_REQUEST_SET_SMSC_ADDRESS";
+            case RIL_REQUEST_EXIT_EMERGENCY_CALLBACK_MODE:
+                return "REQUEST_EXIT_EMERGENCY_CALLBACK_MODE";
+            case RIL_REQUEST_REPORT_SMS_MEMORY_STATUS:
+                return "RIL_REQUEST_REPORT_SMS_MEMORY_STATUS";
+            case RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING:
+                return "RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING";
+            case RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE:
+                return "RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE";
+            case RIL_REQUEST_ISIM_AUTHENTICATION:
+                return "RIL_REQUEST_ISIM_AUTHENTICATION";
+            case RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU:
+                return "RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU";
+            case RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS:
+                return "RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS";
+            case RIL_REQUEST_VOICE_RADIO_TECH:
+                return "RIL_REQUEST_VOICE_RADIO_TECH";
+            case RIL_REQUEST_GET_CELL_INFO_LIST:
+                return "RIL_REQUEST_GET_CELL_INFO_LIST";
+            case RIL_REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE:
+                return "RIL_REQUEST_SET_CELL_INFO_LIST_RATE";
+            case RIL_REQUEST_SET_INITIAL_ATTACH_APN:
+                return "RIL_REQUEST_SET_INITIAL_ATTACH_APN";
+            case RIL_REQUEST_SET_DATA_PROFILE:
+                return "RIL_REQUEST_SET_DATA_PROFILE";
+            case RIL_REQUEST_IMS_REGISTRATION_STATE:
+                return "RIL_REQUEST_IMS_REGISTRATION_STATE";
+            case RIL_REQUEST_IMS_SEND_SMS:
+                return "RIL_REQUEST_IMS_SEND_SMS";
+            case RIL_REQUEST_SIM_TRANSMIT_APDU_BASIC:
+                return "RIL_REQUEST_SIM_TRANSMIT_APDU_BASIC";
+            case RIL_REQUEST_SIM_OPEN_CHANNEL:
+                return "RIL_REQUEST_SIM_OPEN_CHANNEL";
+            case RIL_REQUEST_SIM_CLOSE_CHANNEL:
+                return "RIL_REQUEST_SIM_CLOSE_CHANNEL";
+            case RIL_REQUEST_SIM_TRANSMIT_APDU_CHANNEL:
+                return "RIL_REQUEST_SIM_TRANSMIT_APDU_CHANNEL";
+            case RIL_REQUEST_NV_READ_ITEM:
+                return "RIL_REQUEST_NV_READ_ITEM";
+            case RIL_REQUEST_NV_WRITE_ITEM:
+                return "RIL_REQUEST_NV_WRITE_ITEM";
+            case RIL_REQUEST_NV_WRITE_CDMA_PRL:
+                return "RIL_REQUEST_NV_WRITE_CDMA_PRL";
+            case RIL_REQUEST_NV_RESET_CONFIG:
+                return "RIL_REQUEST_NV_RESET_CONFIG";
+            case RIL_REQUEST_SET_UICC_SUBSCRIPTION:
+                return "RIL_REQUEST_SET_UICC_SUBSCRIPTION";
+            case RIL_REQUEST_ALLOW_DATA:
+                return "RIL_REQUEST_ALLOW_DATA";
+            case RIL_REQUEST_GET_HARDWARE_CONFIG:
+                return "GET_HARDWARE_CONFIG";
+            case RIL_REQUEST_SIM_AUTHENTICATION:
+                return "RIL_REQUEST_SIM_AUTHENTICATION";
+            case RIL_REQUEST_SHUTDOWN:
+                return "RIL_REQUEST_SHUTDOWN";
+            case RIL_REQUEST_SET_RADIO_CAPABILITY:
+                return "RIL_REQUEST_SET_RADIO_CAPABILITY";
+            case RIL_REQUEST_GET_RADIO_CAPABILITY:
+                return "RIL_REQUEST_GET_RADIO_CAPABILITY";
+            case RIL_REQUEST_START_LCE:
+                return "RIL_REQUEST_START_LCE";
+            case RIL_REQUEST_STOP_LCE:
+                return "RIL_REQUEST_STOP_LCE";
+            case RIL_REQUEST_PULL_LCEDATA:
+                return "RIL_REQUEST_PULL_LCEDATA";
+            case RIL_REQUEST_GET_ACTIVITY_INFO:
+                return "RIL_REQUEST_GET_ACTIVITY_INFO";
+            case RIL_REQUEST_SET_ALLOWED_CARRIERS:
+                return "RIL_REQUEST_SET_ALLOWED_CARRIERS";
+            case RIL_REQUEST_GET_ALLOWED_CARRIERS:
+                return "RIL_REQUEST_GET_ALLOWED_CARRIERS";
+            case RIL_REQUEST_SET_SIM_CARD_POWER:
+                return "RIL_REQUEST_SET_SIM_CARD_POWER";
+            case RIL_REQUEST_SEND_DEVICE_STATE:
+                return "RIL_REQUEST_SEND_DEVICE_STATE";
+            case RIL_REQUEST_SET_UNSOLICITED_RESPONSE_FILTER:
+                return "RIL_REQUEST_SET_UNSOLICITED_RESPONSE_FILTER";
+            case RIL_RESPONSE_ACKNOWLEDGEMENT:
+                return "RIL_RESPONSE_ACKNOWLEDGEMENT";
+            case RIL_REQUEST_SET_CARRIER_INFO_IMSI_ENCRYPTION:
+                return "RIL_REQUEST_SET_CARRIER_INFO_IMSI_ENCRYPTION";
+            case RIL_REQUEST_START_NETWORK_SCAN:
+                return "RIL_REQUEST_START_NETWORK_SCAN";
+            case RIL_REQUEST_STOP_NETWORK_SCAN:
+                return "RIL_REQUEST_STOP_NETWORK_SCAN";
+            default: return "<unknown request>";
+        }
+    }
+
+    static String responseToString(int request) {
+        switch(request) {
+            case RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED:
+                return "UNSOL_RESPONSE_RADIO_STATE_CHANGED";
+            case RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED:
+                return "UNSOL_RESPONSE_CALL_STATE_CHANGED";
+            case RIL_UNSOL_RESPONSE_NETWORK_STATE_CHANGED:
+                return "UNSOL_RESPONSE_NETWORK_STATE_CHANGED";
+            case RIL_UNSOL_RESPONSE_NEW_SMS:
+                return "UNSOL_RESPONSE_NEW_SMS";
+            case RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT:
+                return "UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT";
+            case RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM:
+                return "UNSOL_RESPONSE_NEW_SMS_ON_SIM";
+            case RIL_UNSOL_ON_USSD:
+                return "UNSOL_ON_USSD";
+            case RIL_UNSOL_ON_USSD_REQUEST:
+                return "UNSOL_ON_USSD_REQUEST";
+            case RIL_UNSOL_NITZ_TIME_RECEIVED:
+                return "UNSOL_NITZ_TIME_RECEIVED";
+            case RIL_UNSOL_SIGNAL_STRENGTH:
+                return "UNSOL_SIGNAL_STRENGTH";
+            case RIL_UNSOL_DATA_CALL_LIST_CHANGED:
+                return "UNSOL_DATA_CALL_LIST_CHANGED";
+            case RIL_UNSOL_SUPP_SVC_NOTIFICATION:
+                return "UNSOL_SUPP_SVC_NOTIFICATION";
+            case RIL_UNSOL_STK_SESSION_END:
+                return "UNSOL_STK_SESSION_END";
+            case RIL_UNSOL_STK_PROACTIVE_COMMAND:
+                return "UNSOL_STK_PROACTIVE_COMMAND";
+            case RIL_UNSOL_STK_EVENT_NOTIFY:
+                return "UNSOL_STK_EVENT_NOTIFY";
+            case RIL_UNSOL_STK_CALL_SETUP:
+                return "UNSOL_STK_CALL_SETUP";
+            case RIL_UNSOL_SIM_SMS_STORAGE_FULL:
+                return "UNSOL_SIM_SMS_STORAGE_FULL";
+            case RIL_UNSOL_SIM_REFRESH:
+                return "UNSOL_SIM_REFRESH";
+            case RIL_UNSOL_CALL_RING:
+                return "UNSOL_CALL_RING";
+            case RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED:
+                return "UNSOL_RESPONSE_SIM_STATUS_CHANGED";
+            case RIL_UNSOL_RESPONSE_CDMA_NEW_SMS:
+                return "UNSOL_RESPONSE_CDMA_NEW_SMS";
+            case RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS:
+                return "UNSOL_RESPONSE_NEW_BROADCAST_SMS";
+            case RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL:
+                return "UNSOL_CDMA_RUIM_SMS_STORAGE_FULL";
+            case RIL_UNSOL_RESTRICTED_STATE_CHANGED:
+                return "UNSOL_RESTRICTED_STATE_CHANGED";
+            case RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE:
+                return "UNSOL_ENTER_EMERGENCY_CALLBACK_MODE";
+            case RIL_UNSOL_CDMA_CALL_WAITING:
+                return "UNSOL_CDMA_CALL_WAITING";
+            case RIL_UNSOL_CDMA_OTA_PROVISION_STATUS:
+                return "UNSOL_CDMA_OTA_PROVISION_STATUS";
+            case RIL_UNSOL_CDMA_INFO_REC:
+                return "UNSOL_CDMA_INFO_REC";
+            case RIL_UNSOL_OEM_HOOK_RAW:
+                return "UNSOL_OEM_HOOK_RAW";
+            case RIL_UNSOL_RINGBACK_TONE:
+                return "UNSOL_RINGBACK_TONE";
+            case RIL_UNSOL_RESEND_INCALL_MUTE:
+                return "UNSOL_RESEND_INCALL_MUTE";
+            case RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED:
+                return "CDMA_SUBSCRIPTION_SOURCE_CHANGED";
+            case RIL_UNSOl_CDMA_PRL_CHANGED:
+                return "UNSOL_CDMA_PRL_CHANGED";
+            case RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE:
+                return "UNSOL_EXIT_EMERGENCY_CALLBACK_MODE";
+            case RIL_UNSOL_RIL_CONNECTED:
+                return "UNSOL_RIL_CONNECTED";
+            case RIL_UNSOL_VOICE_RADIO_TECH_CHANGED:
+                return "UNSOL_VOICE_RADIO_TECH_CHANGED";
+            case RIL_UNSOL_CELL_INFO_LIST:
+                return "UNSOL_CELL_INFO_LIST";
+            case RIL_UNSOL_RESPONSE_IMS_NETWORK_STATE_CHANGED:
+                return "UNSOL_RESPONSE_IMS_NETWORK_STATE_CHANGED";
+            case RIL_UNSOL_UICC_SUBSCRIPTION_STATUS_CHANGED:
+                return "RIL_UNSOL_UICC_SUBSCRIPTION_STATUS_CHANGED";
+            case RIL_UNSOL_SRVCC_STATE_NOTIFY:
+                return "UNSOL_SRVCC_STATE_NOTIFY";
+            case RIL_UNSOL_HARDWARE_CONFIG_CHANGED:
+                return "RIL_UNSOL_HARDWARE_CONFIG_CHANGED";
+            case RIL_UNSOL_RADIO_CAPABILITY:
+                return "RIL_UNSOL_RADIO_CAPABILITY";
+            case RIL_UNSOL_ON_SS:
+                return "UNSOL_ON_SS";
+            case RIL_UNSOL_STK_CC_ALPHA_NOTIFY:
+                return "UNSOL_STK_CC_ALPHA_NOTIFY";
+            case RIL_UNSOL_LCEDATA_RECV:
+                return "UNSOL_LCE_INFO_RECV";
+            case RIL_UNSOL_PCO_DATA:
+                return "UNSOL_PCO_DATA";
+            case RIL_UNSOL_MODEM_RESTART:
+                return "UNSOL_MODEM_RESTART";
+            case RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION:
+                return "RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION";
+            case RIL_UNSOL_NETWORK_SCAN_RESULT:
+                return "RIL_UNSOL_NETWORK_SCAN_RESULT";
+            default:
+                return "<unknown response>";
+        }
+    }
+
+    void riljLog(String msg) {
+        Rlog.d(RILJ_LOG_TAG, msg
+                + (mPhoneId != null ? (" [SUB" + mPhoneId + "]") : ""));
+    }
+
+    void riljLoge(String msg) {
+        Rlog.e(RILJ_LOG_TAG, msg
+                + (mPhoneId != null ? (" [SUB" + mPhoneId + "]") : ""));
+    }
+
+    void riljLoge(String msg, Exception e) {
+        Rlog.e(RILJ_LOG_TAG, msg
+                + (mPhoneId != null ? (" [SUB" + mPhoneId + "]") : ""), e);
+    }
+
+    void riljLogv(String msg) {
+        Rlog.v(RILJ_LOG_TAG, msg
+                + (mPhoneId != null ? (" [SUB" + mPhoneId + "]") : ""));
+    }
+
+    void unsljLog(int response) {
+        riljLog("[UNSL]< " + responseToString(response));
+    }
+
+    void unsljLogMore(int response, String more) {
+        riljLog("[UNSL]< " + responseToString(response) + " " + more);
+    }
+
+    void unsljLogRet(int response, Object ret) {
+        riljLog("[UNSL]< " + responseToString(response) + " " + retToString(response, ret));
+    }
+
+    void unsljLogvRet(int response, Object ret) {
+        riljLogv("[UNSL]< " + responseToString(response) + " " + retToString(response, ret));
+    }
+
+    @Override
+    public void setPhoneType(int phoneType) { // Called by GsmCdmaPhone
+        if (RILJ_LOGD) riljLog("setPhoneType=" + phoneType + " old value=" + mPhoneType);
+        mPhoneType = phoneType;
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.telephony.BaseCommands#testingEmergencyCall()
+     */
+    @Override
+    public void testingEmergencyCall() {
+        if (RILJ_LOGD) riljLog("testingEmergencyCall");
+        mTestingEmergencyCall.set(true);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("RIL: " + this);
+        pw.println(" mWakeLock=" + mWakeLock);
+        pw.println(" mWakeLockTimeout=" + mWakeLockTimeout);
+        synchronized (mRequestList) {
+            synchronized (mWakeLock) {
+                pw.println(" mWakeLockCount=" + mWakeLockCount);
+            }
+            int count = mRequestList.size();
+            pw.println(" mRequestList count=" + count);
+            for (int i = 0; i < count; i++) {
+                RILRequest rr = mRequestList.valueAt(i);
+                pw.println("  [" + rr.mSerial + "] " + requestToString(rr.mRequest));
+            }
+        }
+        pw.println(" mLastNITZTimeInfo=" + Arrays.toString(mLastNITZTimeInfo));
+        pw.println(" mTestingEmergencyCall=" + mTestingEmergencyCall.get());
+        mClientWakelockTracker.dumpClientRequestTracker();
+    }
+
+    public List<ClientRequestStats> getClientRequestStats() {
+        return mClientWakelockTracker.getClientRequestStats();
+    }
+
+    public static ArrayList<Byte> primitiveArrayToArrayList(byte[] arr) {
+        ArrayList<Byte> arrayList = new ArrayList<>(arr.length);
+        for (byte b : arr) {
+            arrayList.add(b);
+        }
+        return arrayList;
+    }
+
+    public static byte[] arrayListToPrimitiveArray(ArrayList<Byte> bytes) {
+        byte[] ret = new byte[bytes.size()];
+        for (int i = 0; i < ret.length; i++) {
+            ret[i] = bytes.get(i);
+        }
+        return ret;
+    }
+
+    static ArrayList<HardwareConfig> convertHalHwConfigList(
+            ArrayList<android.hardware.radio.V1_0.HardwareConfig> hwListRil,
+            RIL ril) {
+        int num;
+        ArrayList<HardwareConfig> response;
+        HardwareConfig hw;
+
+        num = hwListRil.size();
+        response = new ArrayList<HardwareConfig>(num);
+
+        if (RILJ_LOGV) {
+            ril.riljLog("convertHalHwConfigList: num=" + num);
+        }
+        for (android.hardware.radio.V1_0.HardwareConfig hwRil : hwListRil) {
+            int type = hwRil.type;
+            switch(type) {
+                case HardwareConfig.DEV_HARDWARE_TYPE_MODEM: {
+                    hw = new HardwareConfig(type);
+                    HardwareConfigModem hwModem = hwRil.modem.get(0);
+                    hw.assignModem(hwRil.uuid, hwRil.state, hwModem.rilModel, hwModem.rat,
+                            hwModem.maxVoice, hwModem.maxData, hwModem.maxStandby);
+                    break;
+                }
+                case HardwareConfig.DEV_HARDWARE_TYPE_SIM: {
+                    hw = new HardwareConfig(type);
+                    hw.assignSim(hwRil.uuid, hwRil.state, hwRil.sim.get(0).modemUuid);
+                    break;
+                }
+                default: {
+                    throw new RuntimeException(
+                            "RIL_REQUEST_GET_HARDWARE_CONFIG invalid hardward type:" + type);
+                }
+            }
+
+            response.add(hw);
+        }
+
+        return response;
+    }
+
+    static RadioCapability convertHalRadioCapability(
+            android.hardware.radio.V1_0.RadioCapability rcRil, RIL ril) {
+        int session = rcRil.session;
+        int phase = rcRil.phase;
+        int rat = rcRil.raf;
+        String logicModemUuid = rcRil.logicalModemUuid;
+        int status = rcRil.status;
+
+        ril.riljLog("convertHalRadioCapability: session=" + session +
+                ", phase=" + phase +
+                ", rat=" + rat +
+                ", logicModemUuid=" + logicModemUuid +
+                ", status=" + status);
+        RadioCapability rc = new RadioCapability(
+                ril.mPhoneId, session, phase, rat, logicModemUuid, status);
+        return rc;
+    }
+
+    static ArrayList<Integer> convertHalLceData(LceDataInfo lce, RIL ril) {
+        final ArrayList<Integer> capacityResponse = new ArrayList<Integer>();
+        final int capacityDownKbps = lce.lastHopCapacityKbps;
+        final int confidenceLevel = Byte.toUnsignedInt(lce.confidenceLevel);
+        final int lceSuspended = lce.lceSuspended ? 1 : 0;
+
+        ril.riljLog("LCE capacity information received:" +
+                " capacity=" + capacityDownKbps +
+                " confidence=" + confidenceLevel +
+                " lceSuspended=" + lceSuspended);
+
+        capacityResponse.add(capacityDownKbps);
+        capacityResponse.add(confidenceLevel);
+        capacityResponse.add(lceSuspended);
+        return capacityResponse;
+    }
+
+    static ArrayList<CellInfo> convertHalCellInfoList(
+            ArrayList<android.hardware.radio.V1_0.CellInfo> records) {
+        ArrayList<CellInfo> response = new ArrayList<CellInfo>(records.size());
+
+        for (android.hardware.radio.V1_0.CellInfo record : records) {
+            // first convert RIL CellInfo to Parcel
+            Parcel p = Parcel.obtain();
+            p.writeInt(record.cellInfoType);
+            p.writeInt(record.registered ? 1 : 0);
+            p.writeInt(record.timeStampType);
+            p.writeLong(record.timeStamp);
+            switch (record.cellInfoType) {
+                case CellInfoType.GSM: {
+                    CellInfoGsm cellInfoGsm = record.gsm.get(0);
+                    p.writeInt(Integer.parseInt(cellInfoGsm.cellIdentityGsm.mcc));
+                    p.writeInt(Integer.parseInt(cellInfoGsm.cellIdentityGsm.mnc));
+                    p.writeInt(cellInfoGsm.cellIdentityGsm.lac);
+                    p.writeInt(cellInfoGsm.cellIdentityGsm.cid);
+                    p.writeInt(cellInfoGsm.cellIdentityGsm.arfcn);
+                    p.writeInt(Byte.toUnsignedInt(cellInfoGsm.cellIdentityGsm.bsic));
+                    p.writeInt(cellInfoGsm.signalStrengthGsm.signalStrength);
+                    p.writeInt(cellInfoGsm.signalStrengthGsm.bitErrorRate);
+                    p.writeInt(cellInfoGsm.signalStrengthGsm.timingAdvance);
+                    break;
+                }
+
+                case CellInfoType.CDMA: {
+                    CellInfoCdma cellInfoCdma = record.cdma.get(0);
+                    p.writeInt(cellInfoCdma.cellIdentityCdma.networkId);
+                    p.writeInt(cellInfoCdma.cellIdentityCdma.systemId);
+                    p.writeInt(cellInfoCdma.cellIdentityCdma.baseStationId);
+                    p.writeInt(cellInfoCdma.cellIdentityCdma.longitude);
+                    p.writeInt(cellInfoCdma.cellIdentityCdma.latitude);
+                    p.writeInt(cellInfoCdma.signalStrengthCdma.dbm);
+                    p.writeInt(cellInfoCdma.signalStrengthCdma.ecio);
+                    p.writeInt(cellInfoCdma.signalStrengthEvdo.dbm);
+                    p.writeInt(cellInfoCdma.signalStrengthEvdo.ecio);
+                    p.writeInt(cellInfoCdma.signalStrengthEvdo.signalNoiseRatio);
+                    break;
+                }
+
+                case CellInfoType.LTE: {
+                    CellInfoLte cellInfoLte = record.lte.get(0);
+                    p.writeInt(Integer.parseInt(cellInfoLte.cellIdentityLte.mcc));
+                    p.writeInt(Integer.parseInt(cellInfoLte.cellIdentityLte.mnc));
+                    p.writeInt(cellInfoLte.cellIdentityLte.ci);
+                    p.writeInt(cellInfoLte.cellIdentityLte.pci);
+                    p.writeInt(cellInfoLte.cellIdentityLte.tac);
+                    p.writeInt(cellInfoLte.cellIdentityLte.earfcn);
+                    p.writeInt(cellInfoLte.signalStrengthLte.signalStrength);
+                    p.writeInt(cellInfoLte.signalStrengthLte.rsrp);
+                    p.writeInt(cellInfoLte.signalStrengthLte.rsrq);
+                    p.writeInt(cellInfoLte.signalStrengthLte.rssnr);
+                    p.writeInt(cellInfoLte.signalStrengthLte.cqi);
+                    p.writeInt(cellInfoLte.signalStrengthLte.timingAdvance);
+                    break;
+                }
+
+                case CellInfoType.WCDMA: {
+                    CellInfoWcdma cellInfoWcdma = record.wcdma.get(0);
+                    p.writeInt(Integer.parseInt(cellInfoWcdma.cellIdentityWcdma.mcc));
+                    p.writeInt(Integer.parseInt(cellInfoWcdma.cellIdentityWcdma.mnc));
+                    p.writeInt(cellInfoWcdma.cellIdentityWcdma.lac);
+                    p.writeInt(cellInfoWcdma.cellIdentityWcdma.cid);
+                    p.writeInt(cellInfoWcdma.cellIdentityWcdma.psc);
+                    p.writeInt(cellInfoWcdma.cellIdentityWcdma.uarfcn);
+                    p.writeInt(cellInfoWcdma.signalStrengthWcdma.signalStrength);
+                    p.writeInt(cellInfoWcdma.signalStrengthWcdma.bitErrorRate);
+                    break;
+                }
+
+                default:
+                    throw new RuntimeException("unexpected cellinfotype: " + record.cellInfoType);
+            }
+
+            p.setDataPosition(0);
+            CellInfo InfoRec = CellInfo.CREATOR.createFromParcel(p);
+            p.recycle();
+            response.add(InfoRec);
+        }
+
+        return response;
+    }
+
+    static SignalStrength convertHalSignalStrength(
+            android.hardware.radio.V1_0.SignalStrength signalStrength) {
+        return new SignalStrength(signalStrength.gw.signalStrength,
+                signalStrength.gw.bitErrorRate,
+                signalStrength.cdma.dbm,
+                signalStrength.cdma.ecio,
+                signalStrength.evdo.dbm,
+                signalStrength.evdo.ecio,
+                signalStrength.evdo.signalNoiseRatio,
+                signalStrength.lte.signalStrength,
+                signalStrength.lte.rsrp,
+                signalStrength.lte.rsrq,
+                signalStrength.lte.rssnr,
+                signalStrength.lte.cqi,
+                signalStrength.tdScdma.rscp,
+                false /* gsmFlag - don't care; will be changed by SST */);
+    }
+}
diff --git a/com/android/internal/telephony/RILConstants.java b/com/android/internal/telephony/RILConstants.java
new file mode 100644
index 0000000..e2d25b8
--- /dev/null
+++ b/com/android/internal/telephony/RILConstants.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * TODO: This should probably not be an interface see
+ * http://www.javaworld.com/javaworld/javaqa/2001-06/01-qa-0608-constants.html and google with
+ * http://www.google.com/search?q=interface+constants&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a
+ *
+ * Also they should all probably be static final.
+ */
+
+import android.os.SystemProperties;
+
+/**
+ * {@hide}
+ */
+public interface RILConstants {
+    // From the top of ril.cpp
+    int RIL_ERRNO_INVALID_RESPONSE = -1;
+
+    // from RIL_Errno
+    int SUCCESS = 0;
+    int RADIO_NOT_AVAILABLE = 1;              /* If radio did not start or is resetting */
+    int GENERIC_FAILURE = 2;
+    int PASSWORD_INCORRECT = 3;               /* for PIN/PIN2 methods only! */
+    int SIM_PIN2 = 4;                         /* Operation requires SIM PIN2 to be entered */
+    int SIM_PUK2 = 5;                         /* Operation requires SIM PIN2 to be entered */
+    int REQUEST_NOT_SUPPORTED = 6;
+    int REQUEST_CANCELLED = 7;
+    int OP_NOT_ALLOWED_DURING_VOICE_CALL = 8; /* data operation is not allowed during voice call in
+                                                 class C */
+    int OP_NOT_ALLOWED_BEFORE_REG_NW = 9;     /* request is not allowed before device registers to
+                                                 network */
+    int SMS_SEND_FAIL_RETRY = 10;             /* send sms fail and need retry */
+    int SIM_ABSENT = 11;                      /* ICC card is absent */
+    int SUBSCRIPTION_NOT_AVAILABLE = 12;      /* fail to find CDMA subscription from specified
+                                                 location */
+    int MODE_NOT_SUPPORTED = 13;              /* HW does not support preferred network type */
+    int FDN_CHECK_FAILURE = 14;               /* send operation barred error when FDN is enabled */
+    int ILLEGAL_SIM_OR_ME = 15;               /* network selection failure due
+                                                 to wrong SIM/ME and no
+                                                 retries needed */
+    int MISSING_RESOURCE = 16;                /* no logical channel available */
+    int NO_SUCH_ELEMENT = 17;                 /* application not found on SIM */
+    int DIAL_MODIFIED_TO_USSD = 18;           /* DIAL request modified to USSD */
+    int DIAL_MODIFIED_TO_SS = 19;             /* DIAL request modified to SS */
+    int DIAL_MODIFIED_TO_DIAL = 20;           /* DIAL request modified to DIAL with different data*/
+    int USSD_MODIFIED_TO_DIAL = 21;           /* USSD request modified to DIAL */
+    int USSD_MODIFIED_TO_SS = 22;             /* USSD request modified to SS */
+    int USSD_MODIFIED_TO_USSD = 23;           /* USSD request modified to different USSD request */
+    int SS_MODIFIED_TO_DIAL = 24;             /* SS request modified to DIAL */
+    int SS_MODIFIED_TO_USSD = 25;             /* SS request modified to USSD */
+    int SUBSCRIPTION_NOT_SUPPORTED = 26;      /* Subscription not supported */
+    int SS_MODIFIED_TO_SS = 27;               /* SS request modified to different SS request */
+    int SIM_ALREADY_POWERED_OFF = 29;         /* SAP: 0x03, Error card aleready powered off */
+    int SIM_ALREADY_POWERED_ON = 30;          /* SAP: 0x05, Error card already powered on */
+    int SIM_DATA_NOT_AVAILABLE = 31;          /* SAP: 0x06, Error data not available */
+    int SIM_SAP_CONNECT_FAILURE = 32;
+    int SIM_SAP_MSG_SIZE_TOO_LARGE = 33;
+    int SIM_SAP_MSG_SIZE_TOO_SMALL = 34;
+    int SIM_SAP_CONNECT_OK_CALL_ONGOING = 35;
+    int LCE_NOT_SUPPORTED = 36;               /* Link Capacity Estimation (LCE) not supported */
+    int NO_MEMORY = 37;                       /* Not sufficient memory to process the request */
+    int INTERNAL_ERR = 38;                    /* Hit unexpected vendor internal error scenario */
+    int SYSTEM_ERR = 39;                      /* Hit platform or system error */
+    int MODEM_ERR = 40;                       /* Hit unexpected modem error */
+    int INVALID_STATE = 41;                   /* Unexpected request for the current state */
+    int NO_RESOURCES = 42;                    /* Not sufficient resource to process the request */
+    int SIM_ERR = 43;                         /* Received error from SIM card */
+    int INVALID_ARGUMENTS = 44;               /* Received invalid arguments in request */
+    int INVALID_SIM_STATE = 45;               /* Can not process the request in current SIM state */
+    int INVALID_MODEM_STATE = 46;             /* Can not process the request in current Modem state */
+    int INVALID_CALL_ID = 47;                 /* Received invalid call id in request */
+    int NO_SMS_TO_ACK = 48;                   /* ACK received when there is no SMS to ack */
+    int NETWORK_ERR = 49;                     /* Received error from network */
+    int REQUEST_RATE_LIMITED = 50;            /* Operation denied due to overly-frequent requests */
+    int SIM_BUSY = 51;                        /* SIM is busy */
+    int SIM_FULL = 52;                        /* The target EF is full */
+    int NETWORK_REJECT = 53;                  /* Request is rejected by network */
+    int OPERATION_NOT_ALLOWED = 54;           /* Not allowed the request now */
+    int EMPTY_RECORD = 55;                    /* The request record is empty */
+    int INVALID_SMS_FORMAT = 56;              /* Invalid sms format */
+    int ENCODING_ERR = 57;                    /* Message not encoded properly */
+    int INVALID_SMSC_ADDRESS = 58;            /* SMSC address specified is invalid */
+    int NO_SUCH_ENTRY = 59;                   /* No such entry present to perform the request */
+    int NETWORK_NOT_READY = 60;               /* Network is not ready to perform the request */
+    int NOT_PROVISIONED = 61;                 /* Device doesnot have this value provisioned */
+    int NO_SUBSCRIPTION = 62;                 /* Device doesnot have subscription */
+    int NO_NETWORK_FOUND = 63;                /* Network cannot be found */
+    int DEVICE_IN_USE = 64;                   /* Operation cannot be performed because the device
+                                                 is currently in use */
+    int ABORTED = 65;                         /* Operation aborted */
+    // Below is list of OEM specific error codes which can by used by OEMs in case they don't want to
+    // reveal particular replacement for Generic failure
+    int OEM_ERROR_1 = 501;
+    int OEM_ERROR_2 = 502;
+    int OEM_ERROR_3 = 503;
+    int OEM_ERROR_4 = 504;
+    int OEM_ERROR_5 = 505;
+    int OEM_ERROR_6 = 506;
+    int OEM_ERROR_7 = 507;
+    int OEM_ERROR_8 = 508;
+    int OEM_ERROR_9 = 509;
+    int OEM_ERROR_10 = 510;
+    int OEM_ERROR_11 = 511;
+    int OEM_ERROR_12 = 512;
+    int OEM_ERROR_13 = 513;
+    int OEM_ERROR_14 = 514;
+    int OEM_ERROR_15 = 515;
+    int OEM_ERROR_16 = 516;
+    int OEM_ERROR_17 = 517;
+    int OEM_ERROR_18 = 518;
+    int OEM_ERROR_19 = 519;
+    int OEM_ERROR_20 = 520;
+    int OEM_ERROR_21 = 521;
+    int OEM_ERROR_22 = 522;
+    int OEM_ERROR_23 = 523;
+    int OEM_ERROR_24 = 524;
+    int OEM_ERROR_25 = 525;
+
+    /* NETWORK_MODE_* See ril.h RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE */
+    int NETWORK_MODE_WCDMA_PREF     = 0; /* GSM/WCDMA (WCDMA preferred) */
+    int NETWORK_MODE_GSM_ONLY       = 1; /* GSM only */
+    int NETWORK_MODE_WCDMA_ONLY     = 2; /* WCDMA only */
+    int NETWORK_MODE_GSM_UMTS       = 3; /* GSM/WCDMA (auto mode, according to PRL)
+                                            AVAILABLE Application Settings menu*/
+    int NETWORK_MODE_CDMA           = 4; /* CDMA and EvDo (auto mode, according to PRL)
+                                            AVAILABLE Application Settings menu*/
+    int NETWORK_MODE_CDMA_NO_EVDO   = 5; /* CDMA only */
+    int NETWORK_MODE_EVDO_NO_CDMA   = 6; /* EvDo only */
+    int NETWORK_MODE_GLOBAL         = 7; /* GSM/WCDMA, CDMA, and EvDo (auto mode, according to PRL)
+                                            AVAILABLE Application Settings menu*/
+    int NETWORK_MODE_LTE_CDMA_EVDO  = 8; /* LTE, CDMA and EvDo */
+    int NETWORK_MODE_LTE_GSM_WCDMA  = 9; /* LTE, GSM/WCDMA */
+    int NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA = 10; /* LTE, CDMA, EvDo, GSM/WCDMA */
+    int NETWORK_MODE_LTE_ONLY       = 11; /* LTE Only mode. */
+    int NETWORK_MODE_LTE_WCDMA      = 12; /* LTE/WCDMA */
+    int NETWORK_MODE_TDSCDMA_ONLY            = 13; /* TD-SCDMA only */
+    int NETWORK_MODE_TDSCDMA_WCDMA           = 14; /* TD-SCDMA and WCDMA */
+    int NETWORK_MODE_LTE_TDSCDMA             = 15; /* TD-SCDMA and LTE */
+    int NETWORK_MODE_TDSCDMA_GSM             = 16; /* TD-SCDMA and GSM */
+    int NETWORK_MODE_LTE_TDSCDMA_GSM         = 17; /* TD-SCDMA,GSM and LTE */
+    int NETWORK_MODE_TDSCDMA_GSM_WCDMA       = 18; /* TD-SCDMA, GSM/WCDMA */
+    int NETWORK_MODE_LTE_TDSCDMA_WCDMA       = 19; /* TD-SCDMA, WCDMA and LTE */
+    int NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA   = 20; /* TD-SCDMA, GSM/WCDMA and LTE */
+    int NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA  = 21; /*TD-SCDMA,EvDo,CDMA,GSM/WCDMA*/
+    int NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = 22; /* TD-SCDMA/LTE/GSM/WCDMA, CDMA, and EvDo */
+    int PREFERRED_NETWORK_MODE      = SystemProperties.getInt("ro.telephony.default_network",
+            NETWORK_MODE_WCDMA_PREF);
+
+    int BAND_MODE_UNSPECIFIED = 0;      //"unspecified" (selected by baseband automatically)
+    int BAND_MODE_EURO = 1;             //"EURO band" (GSM-900 / DCS-1800 / WCDMA-IMT-2000)
+    int BAND_MODE_USA = 2;              //"US band" (GSM-850 / PCS-1900 / WCDMA-850 / WCDMA-PCS-1900)
+    int BAND_MODE_JPN = 3;              //"JPN band" (WCDMA-800 / WCDMA-IMT-2000)
+    int BAND_MODE_AUS = 4;              //"AUS band" (GSM-900 / DCS-1800 / WCDMA-850 / WCDMA-IMT-2000)
+    int BAND_MODE_AUS_2 = 5;            //"AUS band 2" (GSM-900 / DCS-1800 / WCDMA-850)
+    int BAND_MODE_CELL_800 = 6;         //"Cellular" (800-MHz Band)
+    int BAND_MODE_PCS = 7;              //"PCS" (1900-MHz Band)
+    int BAND_MODE_JTACS = 8;            //"Band Class 3" (JTACS Band)
+    int BAND_MODE_KOREA_PCS = 9;        //"Band Class 4" (Korean PCS Band)
+    int BAND_MODE_5_450M = 10;          //"Band Class 5" (450-MHz Band)
+    int BAND_MODE_IMT2000 = 11;         //"Band Class 6" (2-GMHz IMT2000 Band)
+    int BAND_MODE_7_700M_2 = 12;        //"Band Class 7" (Upper 700-MHz Band)
+    int BAND_MODE_8_1800M = 13;         //"Band Class 8" (1800-MHz Band)
+    int BAND_MODE_9_900M = 14;          //"Band Class 9" (900-MHz Band)
+    int BAND_MODE_10_800M_2 = 15;       //"Band Class 10" (Secondary 800-MHz Band)
+    int BAND_MODE_EURO_PAMR_400M = 16;  //"Band Class 11" (400-MHz European PAMR Band)
+    int BAND_MODE_AWS = 17;             //"Band Class 15" (AWS Band)
+    int BAND_MODE_USA_2500M = 18;       //"Band Class 16" (US 2.5-GHz Band)
+
+    int CDMA_CELL_BROADCAST_SMS_DISABLED = 1;
+    int CDMA_CELL_BROADCAST_SMS_ENABLED  = 0;
+
+    int NO_PHONE = 0;
+    int GSM_PHONE = 1;
+    int CDMA_PHONE = 2;
+    int SIP_PHONE  = 3;
+    int THIRD_PARTY_PHONE = 4;
+    int IMS_PHONE = 5;
+    int CDMA_LTE_PHONE = 6;
+
+    int LTE_ON_CDMA_UNKNOWN = -1;
+    int LTE_ON_CDMA_FALSE = 0;
+    int LTE_ON_CDMA_TRUE = 1;
+
+    int CDM_TTY_MODE_DISABLED = 0;
+    int CDM_TTY_MODE_ENABLED = 1;
+
+    int CDM_TTY_FULL_MODE = 1;
+    int CDM_TTY_HCO_MODE = 2;
+    int CDM_TTY_VCO_MODE = 3;
+
+    /* Setup a packet data connection. See ril.h RIL_REQUEST_SETUP_DATA_CALL */
+    int SETUP_DATA_TECH_CDMA      = 0;
+    int SETUP_DATA_TECH_GSM       = 1;
+
+    int SETUP_DATA_AUTH_NONE      = 0;
+    int SETUP_DATA_AUTH_PAP       = 1;
+    int SETUP_DATA_AUTH_CHAP      = 2;
+    int SETUP_DATA_AUTH_PAP_CHAP  = 3;
+
+    String SETUP_DATA_PROTOCOL_IP     = "IP";
+    String SETUP_DATA_PROTOCOL_IPV6   = "IPV6";
+    String SETUP_DATA_PROTOCOL_IPV4V6 = "IPV4V6";
+
+    /* Deactivate data call reasons */
+    int DEACTIVATE_REASON_NONE = 0;
+    int DEACTIVATE_REASON_RADIO_OFF = 1;
+    int DEACTIVATE_REASON_PDP_RESET = 2;
+
+    /* NV config radio reset types. */
+    int NV_CONFIG_RELOAD_RESET = 1;
+    int NV_CONFIG_ERASE_RESET = 2;
+    int NV_CONFIG_FACTORY_RESET = 3;
+
+    /* LCE service related constants. */
+    int LCE_NOT_AVAILABLE = -1;
+    int LCE_STOPPED = 0;
+    int LCE_ACTIVE = 1;
+
+/*
+cat include/telephony/ril.h | \
+   egrep '^#define' | \
+   sed -re 's/^#define +([^ ]+)* +([^ ]+)/    int \1 = \2;/' \
+   >>java/android/com.android.internal.telephony/gsm/RILConstants.java
+*/
+
+    /**
+     * No restriction at all including voice/SMS/USSD/SS/AV64
+     * and packet data.
+     */
+    int RIL_RESTRICTED_STATE_NONE = 0x00;
+    /**
+     * Block emergency call due to restriction.
+     * But allow all normal voice/SMS/USSD/SS/AV64.
+     */
+    int RIL_RESTRICTED_STATE_CS_EMERGENCY = 0x01;
+    /**
+     * Block all normal voice/SMS/USSD/SS/AV64 due to restriction.
+     * Only Emergency call allowed.
+     */
+    int RIL_RESTRICTED_STATE_CS_NORMAL = 0x02;
+    /**
+     * Block all voice/SMS/USSD/SS/AV64
+     * including emergency call due to restriction.
+     */
+    int RIL_RESTRICTED_STATE_CS_ALL = 0x04;
+    /**
+     * Block packet data access due to restriction.
+     */
+    int RIL_RESTRICTED_STATE_PS_ALL = 0x10;
+
+    /** Data profile for RIL_REQUEST_SETUP_DATA_CALL */
+    public static final int DATA_PROFILE_DEFAULT   = 0;
+    public static final int DATA_PROFILE_TETHERED  = 1;
+    public static final int DATA_PROFILE_IMS       = 2;
+    public static final int DATA_PROFILE_FOTA      = 3;
+    public static final int DATA_PROFILE_CBS       = 4;
+    public static final int DATA_PROFILE_OEM_BASE  = 1000;
+    public static final int DATA_PROFILE_INVALID   = 0xFFFFFFFF;
+
+    int RIL_REQUEST_GET_SIM_STATUS = 1;
+    int RIL_REQUEST_ENTER_SIM_PIN = 2;
+    int RIL_REQUEST_ENTER_SIM_PUK = 3;
+    int RIL_REQUEST_ENTER_SIM_PIN2 = 4;
+    int RIL_REQUEST_ENTER_SIM_PUK2 = 5;
+    int RIL_REQUEST_CHANGE_SIM_PIN = 6;
+    int RIL_REQUEST_CHANGE_SIM_PIN2 = 7;
+    int RIL_REQUEST_ENTER_NETWORK_DEPERSONALIZATION = 8;
+    int RIL_REQUEST_GET_CURRENT_CALLS = 9;
+    int RIL_REQUEST_DIAL = 10;
+    int RIL_REQUEST_GET_IMSI = 11;
+    int RIL_REQUEST_HANGUP = 12;
+    int RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND = 13;
+    int RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND = 14;
+    int RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE = 15;
+    int RIL_REQUEST_CONFERENCE = 16;
+    int RIL_REQUEST_UDUB = 17;
+    int RIL_REQUEST_LAST_CALL_FAIL_CAUSE = 18;
+    int RIL_REQUEST_SIGNAL_STRENGTH = 19;
+    int RIL_REQUEST_VOICE_REGISTRATION_STATE = 20;
+    int RIL_REQUEST_DATA_REGISTRATION_STATE = 21;
+    int RIL_REQUEST_OPERATOR = 22;
+    int RIL_REQUEST_RADIO_POWER = 23;
+    int RIL_REQUEST_DTMF = 24;
+    int RIL_REQUEST_SEND_SMS = 25;
+    int RIL_REQUEST_SEND_SMS_EXPECT_MORE = 26;
+    int RIL_REQUEST_SETUP_DATA_CALL = 27;
+    int RIL_REQUEST_SIM_IO = 28;
+    int RIL_REQUEST_SEND_USSD = 29;
+    int RIL_REQUEST_CANCEL_USSD = 30;
+    int RIL_REQUEST_GET_CLIR = 31;
+    int RIL_REQUEST_SET_CLIR = 32;
+    int RIL_REQUEST_QUERY_CALL_FORWARD_STATUS = 33;
+    int RIL_REQUEST_SET_CALL_FORWARD = 34;
+    int RIL_REQUEST_QUERY_CALL_WAITING = 35;
+    int RIL_REQUEST_SET_CALL_WAITING = 36;
+    int RIL_REQUEST_SMS_ACKNOWLEDGE = 37;
+    int RIL_REQUEST_GET_IMEI = 38;
+    int RIL_REQUEST_GET_IMEISV = 39;
+    int RIL_REQUEST_ANSWER = 40;
+    int RIL_REQUEST_DEACTIVATE_DATA_CALL = 41;
+    int RIL_REQUEST_QUERY_FACILITY_LOCK = 42;
+    int RIL_REQUEST_SET_FACILITY_LOCK = 43;
+    int RIL_REQUEST_CHANGE_BARRING_PASSWORD = 44;
+    int RIL_REQUEST_QUERY_NETWORK_SELECTION_MODE = 45;
+    int RIL_REQUEST_SET_NETWORK_SELECTION_AUTOMATIC = 46;
+    int RIL_REQUEST_SET_NETWORK_SELECTION_MANUAL = 47;
+    int RIL_REQUEST_QUERY_AVAILABLE_NETWORKS = 48;
+    int RIL_REQUEST_DTMF_START = 49;
+    int RIL_REQUEST_DTMF_STOP = 50;
+    int RIL_REQUEST_BASEBAND_VERSION = 51;
+    int RIL_REQUEST_SEPARATE_CONNECTION = 52;
+    int RIL_REQUEST_SET_MUTE = 53;
+    int RIL_REQUEST_GET_MUTE = 54;
+    int RIL_REQUEST_QUERY_CLIP = 55;
+    int RIL_REQUEST_LAST_DATA_CALL_FAIL_CAUSE = 56;
+    int RIL_REQUEST_DATA_CALL_LIST = 57;
+    int RIL_REQUEST_RESET_RADIO = 58;
+    int RIL_REQUEST_OEM_HOOK_RAW = 59;
+    int RIL_REQUEST_OEM_HOOK_STRINGS = 60;
+    int RIL_REQUEST_SCREEN_STATE = 61;
+    int RIL_REQUEST_SET_SUPP_SVC_NOTIFICATION = 62;
+    int RIL_REQUEST_WRITE_SMS_TO_SIM = 63;
+    int RIL_REQUEST_DELETE_SMS_ON_SIM = 64;
+    int RIL_REQUEST_SET_BAND_MODE = 65;
+    int RIL_REQUEST_QUERY_AVAILABLE_BAND_MODE = 66;
+    int RIL_REQUEST_STK_GET_PROFILE = 67;
+    int RIL_REQUEST_STK_SET_PROFILE = 68;
+    int RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND = 69;
+    int RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE = 70;
+    int RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM = 71;
+    int RIL_REQUEST_EXPLICIT_CALL_TRANSFER = 72;
+    int RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE = 73;
+    int RIL_REQUEST_GET_PREFERRED_NETWORK_TYPE = 74;
+    int RIL_REQUEST_GET_NEIGHBORING_CELL_IDS = 75;
+    int RIL_REQUEST_SET_LOCATION_UPDATES = 76;
+    int RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE = 77;
+    int RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE = 78;
+    int RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE = 79;
+    int RIL_REQUEST_SET_TTY_MODE = 80;
+    int RIL_REQUEST_QUERY_TTY_MODE = 81;
+    int RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE = 82;
+    int RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE = 83;
+    int RIL_REQUEST_CDMA_FLASH = 84;
+    int RIL_REQUEST_CDMA_BURST_DTMF = 85;
+    int RIL_REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY = 86;
+    int RIL_REQUEST_CDMA_SEND_SMS = 87;
+    int RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE = 88;
+    int RIL_REQUEST_GSM_GET_BROADCAST_CONFIG = 89;
+    int RIL_REQUEST_GSM_SET_BROADCAST_CONFIG = 90;
+    int RIL_REQUEST_GSM_BROADCAST_ACTIVATION = 91;
+    int RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG = 92;
+    int RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG = 93;
+    int RIL_REQUEST_CDMA_BROADCAST_ACTIVATION = 94;
+    int RIL_REQUEST_CDMA_SUBSCRIPTION = 95;
+    int RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM = 96;
+    int RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM = 97;
+    int RIL_REQUEST_DEVICE_IDENTITY = 98;
+    int RIL_REQUEST_EXIT_EMERGENCY_CALLBACK_MODE = 99;
+    int RIL_REQUEST_GET_SMSC_ADDRESS = 100;
+    int RIL_REQUEST_SET_SMSC_ADDRESS = 101;
+    int RIL_REQUEST_REPORT_SMS_MEMORY_STATUS = 102;
+    int RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING = 103;
+    int RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE = 104;
+    int RIL_REQUEST_ISIM_AUTHENTICATION = 105;
+    int RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU = 106;
+    int RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS = 107;
+    int RIL_REQUEST_VOICE_RADIO_TECH = 108;
+    int RIL_REQUEST_GET_CELL_INFO_LIST = 109;
+    int RIL_REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE = 110;
+    int RIL_REQUEST_SET_INITIAL_ATTACH_APN = 111;
+    int RIL_REQUEST_IMS_REGISTRATION_STATE = 112;
+    int RIL_REQUEST_IMS_SEND_SMS = 113;
+    int RIL_REQUEST_SIM_TRANSMIT_APDU_BASIC = 114;
+    int RIL_REQUEST_SIM_OPEN_CHANNEL = 115;
+    int RIL_REQUEST_SIM_CLOSE_CHANNEL = 116;
+    int RIL_REQUEST_SIM_TRANSMIT_APDU_CHANNEL = 117;
+    int RIL_REQUEST_NV_READ_ITEM = 118;
+    int RIL_REQUEST_NV_WRITE_ITEM = 119;
+    int RIL_REQUEST_NV_WRITE_CDMA_PRL = 120;
+    int RIL_REQUEST_NV_RESET_CONFIG = 121;
+    int RIL_REQUEST_SET_UICC_SUBSCRIPTION = 122;
+    int RIL_REQUEST_ALLOW_DATA = 123;
+    int RIL_REQUEST_GET_HARDWARE_CONFIG = 124;
+    int RIL_REQUEST_SIM_AUTHENTICATION = 125;
+    int RIL_REQUEST_GET_DC_RT_INFO = 126;
+    int RIL_REQUEST_SET_DC_RT_INFO_RATE = 127;
+    int RIL_REQUEST_SET_DATA_PROFILE = 128;
+    int RIL_REQUEST_SHUTDOWN = 129;
+    int RIL_REQUEST_GET_RADIO_CAPABILITY = 130;
+    int RIL_REQUEST_SET_RADIO_CAPABILITY = 131;
+    int RIL_REQUEST_START_LCE = 132;
+    int RIL_REQUEST_STOP_LCE = 133;
+    int RIL_REQUEST_PULL_LCEDATA = 134;
+    int RIL_REQUEST_GET_ACTIVITY_INFO = 135;
+    int RIL_REQUEST_SET_ALLOWED_CARRIERS = 136;
+    int RIL_REQUEST_GET_ALLOWED_CARRIERS = 137;
+    int RIL_REQUEST_SEND_DEVICE_STATE = 138;
+    int RIL_REQUEST_SET_UNSOLICITED_RESPONSE_FILTER = 139;
+    int RIL_REQUEST_SET_SIM_CARD_POWER = 140;
+    int RIL_REQUEST_SET_CARRIER_INFO_IMSI_ENCRYPTION = 141;
+    int RIL_REQUEST_START_NETWORK_SCAN = 142;
+    int RIL_REQUEST_STOP_NETWORK_SCAN = 143;
+
+    int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
+
+    int RIL_UNSOL_RESPONSE_BASE = 1000;
+    int RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED = 1000;
+    int RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED = 1001;
+    int RIL_UNSOL_RESPONSE_NETWORK_STATE_CHANGED = 1002;
+    int RIL_UNSOL_RESPONSE_NEW_SMS = 1003;
+    int RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT = 1004;
+    int RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM = 1005;
+    int RIL_UNSOL_ON_USSD = 1006;
+    int RIL_UNSOL_ON_USSD_REQUEST = 1007;
+    int RIL_UNSOL_NITZ_TIME_RECEIVED = 1008;
+    int RIL_UNSOL_SIGNAL_STRENGTH = 1009;
+    int RIL_UNSOL_DATA_CALL_LIST_CHANGED = 1010;
+    int RIL_UNSOL_SUPP_SVC_NOTIFICATION = 1011;
+    int RIL_UNSOL_STK_SESSION_END = 1012;
+    int RIL_UNSOL_STK_PROACTIVE_COMMAND = 1013;
+    int RIL_UNSOL_STK_EVENT_NOTIFY = 1014;
+    int RIL_UNSOL_STK_CALL_SETUP = 1015;
+    int RIL_UNSOL_SIM_SMS_STORAGE_FULL = 1016;
+    int RIL_UNSOL_SIM_REFRESH = 1017;
+    int RIL_UNSOL_CALL_RING = 1018;
+    int RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED = 1019;
+    int RIL_UNSOL_RESPONSE_CDMA_NEW_SMS = 1020;
+    int RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS = 1021;
+    int RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL = 1022;
+    int RIL_UNSOL_RESTRICTED_STATE_CHANGED = 1023;
+    int RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE = 1024;
+    int RIL_UNSOL_CDMA_CALL_WAITING = 1025;
+    int RIL_UNSOL_CDMA_OTA_PROVISION_STATUS = 1026;
+    int RIL_UNSOL_CDMA_INFO_REC = 1027;
+    int RIL_UNSOL_OEM_HOOK_RAW = 1028;
+    int RIL_UNSOL_RINGBACK_TONE = 1029;
+    int RIL_UNSOL_RESEND_INCALL_MUTE = 1030;
+    int RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 1031;
+    int RIL_UNSOl_CDMA_PRL_CHANGED = 1032;
+    int RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE = 1033;
+    int RIL_UNSOL_RIL_CONNECTED = 1034;
+    int RIL_UNSOL_VOICE_RADIO_TECH_CHANGED = 1035;
+    int RIL_UNSOL_CELL_INFO_LIST = 1036;
+    int RIL_UNSOL_RESPONSE_IMS_NETWORK_STATE_CHANGED = 1037;
+    int RIL_UNSOL_UICC_SUBSCRIPTION_STATUS_CHANGED = 1038;
+    int RIL_UNSOL_SRVCC_STATE_NOTIFY = 1039;
+    int RIL_UNSOL_HARDWARE_CONFIG_CHANGED = 1040;
+    int RIL_UNSOL_DC_RT_INFO_CHANGED = 1041;
+    int RIL_UNSOL_RADIO_CAPABILITY = 1042;
+    int RIL_UNSOL_ON_SS = 1043;
+    int RIL_UNSOL_STK_CC_ALPHA_NOTIFY = 1044;
+    int RIL_UNSOL_LCEDATA_RECV = 1045;
+    int RIL_UNSOL_PCO_DATA = 1046;
+    int RIL_UNSOL_MODEM_RESTART = 1047;
+    int RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION = 1048;
+    int RIL_UNSOL_NETWORK_SCAN_RESULT = 1049;
+}
diff --git a/com/android/internal/telephony/RILRequest.java b/com/android/internal/telephony/RILRequest.java
new file mode 100644
index 0000000..c0a0b70
--- /dev/null
+++ b/com/android/internal/telephony/RILRequest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.os.AsyncResult;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.WorkSource;
+import android.telephony.Rlog;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * {@hide}
+ */
+
+public class RILRequest {
+    static final String LOG_TAG = "RilRequest";
+
+    //***** Class Variables
+    static Random sRandom = new Random();
+    static AtomicInteger sNextSerial = new AtomicInteger(0);
+    private static Object sPoolSync = new Object();
+    private static RILRequest sPool = null;
+    private static int sPoolSize = 0;
+    private static final int MAX_POOL_SIZE = 4;
+
+    //***** Instance Variables
+    int mSerial;
+    int mRequest;
+    Message mResult;
+    RILRequest mNext;
+    int mWakeLockType;
+    WorkSource mWorkSource;
+    String mClientId;
+    // time in ms when RIL request was made
+    long mStartTimeMs;
+
+    public int getSerial() {
+        return mSerial;
+    }
+
+    public int getRequest() {
+        return mRequest;
+    }
+
+    public Message getResult() {
+        return mResult;
+    }
+
+    /**
+     * Retrieves a new RILRequest instance from the pool.
+     *
+     * @param request RIL_REQUEST_*
+     * @param result sent when operation completes
+     * @return a RILRequest instance from the pool.
+     */
+    private static RILRequest obtain(int request, Message result) {
+        RILRequest rr = null;
+
+        synchronized (sPoolSync) {
+            if (sPool != null) {
+                rr = sPool;
+                sPool = rr.mNext;
+                rr.mNext = null;
+                sPoolSize--;
+            }
+        }
+
+        if (rr == null) {
+            rr = new RILRequest();
+        }
+
+        rr.mSerial = sNextSerial.getAndIncrement();
+
+        rr.mRequest = request;
+        rr.mResult = result;
+
+        rr.mWakeLockType = RIL.INVALID_WAKELOCK;
+        rr.mWorkSource = null;
+        rr.mStartTimeMs = SystemClock.elapsedRealtime();
+        if (result != null && result.getTarget() == null) {
+            throw new NullPointerException("Message target must not be null");
+        }
+
+        return rr;
+    }
+
+
+    /**
+     * Retrieves a new RILRequest instance from the pool and sets the clientId
+     *
+     * @param request RIL_REQUEST_*
+     * @param result sent when operation completes
+     * @param workSource WorkSource to track the client
+     * @return a RILRequest instance from the pool.
+     */
+    static RILRequest obtain(int request, Message result, WorkSource workSource) {
+        RILRequest rr = null;
+
+        rr = obtain(request, result);
+        if (workSource != null) {
+            rr.mWorkSource = workSource;
+            rr.mClientId = String.valueOf(workSource.get(0)) + ":" + workSource.getName(0);
+        } else {
+            Rlog.e(LOG_TAG, "null workSource " + request);
+        }
+
+        return rr;
+    }
+
+    /**
+     * Returns a RILRequest instance to the pool.
+     *
+     * Note: This should only be called once per use.
+     */
+    void release() {
+        synchronized (sPoolSync) {
+            if (sPoolSize < MAX_POOL_SIZE) {
+                mNext = sPool;
+                sPool = this;
+                sPoolSize++;
+                mResult = null;
+                if (mWakeLockType != RIL.INVALID_WAKELOCK) {
+                    //This is OK for some wakelock types and not others
+                    if (mWakeLockType == RIL.FOR_WAKELOCK) {
+                        Rlog.e(LOG_TAG, "RILRequest releasing with held wake lock: "
+                                + serialString());
+                    }
+                }
+            }
+        }
+    }
+
+    private RILRequest() {
+    }
+
+    static void resetSerial() {
+        // use a random so that on recovery we probably don't mix old requests
+        // with new.
+        sNextSerial.set(sRandom.nextInt());
+    }
+
+    String serialString() {
+        //Cheesy way to do %04d
+        StringBuilder sb = new StringBuilder(8);
+        String sn;
+
+        long adjustedSerial = (((long) mSerial) - Integer.MIN_VALUE) % 10000;
+
+        sn = Long.toString(adjustedSerial);
+
+        //sb.append("J[");
+        sb.append('[');
+        for (int i = 0, s = sn.length(); i < 4 - s; i++) {
+            sb.append('0');
+        }
+
+        sb.append(sn);
+        sb.append(']');
+        return sb.toString();
+    }
+
+    void onError(int error, Object ret) {
+        CommandException ex;
+
+        ex = CommandException.fromRilErrno(error);
+
+        if (RIL.RILJ_LOGD) {
+            Rlog.d(LOG_TAG, serialString() + "< "
+                    + RIL.requestToString(mRequest)
+                    + " error: " + ex + " ret=" + RIL.retToString(mRequest, ret));
+        }
+
+        if (mResult != null) {
+            AsyncResult.forMessage(mResult, ret, ex);
+            mResult.sendToTarget();
+        }
+    }
+}
diff --git a/com/android/internal/telephony/RadioCapability.java b/com/android/internal/telephony/RadioCapability.java
new file mode 100644
index 0000000..a7e73fc
--- /dev/null
+++ b/com/android/internal/telephony/RadioCapability.java
@@ -0,0 +1,213 @@
+/*
+* Copyright (C) 2014 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.internal.telephony;
+
+/**
+ * Object to indicate the phone radio capability.
+ *
+ * @hide
+ */
+public class RadioCapability {
+
+    /*
+     * The RC_PHASE constants are the set of valid values for the mPhase field.
+     */
+
+    /**
+     *  LM is configured is initial value and value after FINISH completes.
+     */
+    public static final int RC_PHASE_CONFIGURED = 0;
+
+    /**
+     * START is sent before Apply and indicates that an APPLY will be
+     * forthcoming with these same parameters.
+     */
+    public static final int RC_PHASE_START = 1;
+
+    /**
+     * APPLY is sent after all LM's receive START and returned
+     * RIL_RadioCapability. status = 0, if any START's fail no APPLY will
+     * be sent.
+     */
+    public static final int RC_PHASE_APPLY = 2;
+
+    /**
+     *  UNSOL_RSP is sent with RIL_UNSOL_RADIO_CAPABILITY.
+     */
+    public static final int RC_PHASE_UNSOL_RSP = 3;
+
+    /**
+     * RC_PHASE_FINISH is sent after all previous phases have completed.
+     * If an error occurs in any previous commands the RIL_RadioAccessesFamily
+     * and LogicalModemId fields will be the prior configuration thus
+     * restoring the configuration to the previous value. An error returned
+     * by this command will generally be ignored or may cause that logical
+     * modem to be removed from service
+     */
+    public static final int RC_PHASE_FINISH = 4;
+
+    /*
+     * The RC_STATUS_xxx constants are returned in the mStatus field.
+     */
+
+     /**
+      *  this parameter is no meaning with RC_Phase_START, RC_Phase_APPLY
+      */
+    public static final int RC_STATUS_NONE = 0;
+
+    /**
+     * Tell modem  the action transaction of set radio capability is
+     * success with RC_Phase_FINISH.
+     */
+    public static final int RC_STATUS_SUCCESS = 1;
+
+    /**
+     * tell modem the action transaction of set radio capability is fail
+     * with RC_Phase_FINISH
+     */
+    public static final int RC_STATUS_FAIL = 2;
+
+    /** Version of structure, RIL_RadioCapability_Version */
+    private static final int RADIO_CAPABILITY_VERSION = 1;
+
+    /** Unique session value defined by framework returned in all "responses/unsol" */
+    private int mSession;
+
+    /** CONFIGURED, START, APPLY, FINISH */
+    private int mPhase;
+
+    /**
+     * RadioAccessFamily is a bit field of radio access technologies the
+     * for the modem is currently supporting. The initial value returned
+     * my the modem must the the set of bits that the modem currently supports.
+     * see RadioAccessFamily#RADIO_TECHNOLOGY_XXXX
+     */
+    private int mRadioAccessFamily;
+
+    /**
+     * Logical modem this radio is be connected to.
+     * This must be Globally unique on convention is
+     * to use a registered name such as com.google.android.lm0
+     */
+    private String mLogicalModemUuid;
+
+    /** Return status and an input parameter for RC_Phase_FINISH */
+    private int mStatus;
+
+    /** Phone ID of phone */
+    private int mPhoneId;
+
+    /**
+     * Constructor.
+     *
+     * @param phoneId the phone ID
+     * @param session the request transaction id
+     * @param phase the request phase id
+     * @param radioAccessFamily the phone radio access family defined in
+     *        RadioAccessFamily. It's a bit mask value to represent
+     *        the support type.
+     * @param logicalModemUuid the logicalModem UUID which phone connected to
+     * @param status tell modem the action transaction of
+     *        set radio capability is success or fail with RC_Phase_FINISH
+     */
+    public RadioCapability(int phoneId, int session, int phase,
+            int radioAccessFamily, String logicalModemUuid, int status) {
+        mPhoneId = phoneId;
+        mSession = session;
+        mPhase = phase;
+        mRadioAccessFamily = radioAccessFamily;
+        mLogicalModemUuid = logicalModemUuid;
+        mStatus = status;
+    }
+
+    /**
+     * Get phone ID.
+     *
+     * @return phone ID
+     */
+    public int getPhoneId() {
+        return mPhoneId;
+    }
+
+    /**
+     * Get radio capability version.
+     *
+     * @return radio capability version
+     */
+    public int getVersion() {
+        return RADIO_CAPABILITY_VERSION;
+    }
+
+    /**
+     * Get unique session id.
+     *
+     * @return unique session id
+     */
+    public int getSession() {
+        return mSession;
+    }
+
+
+    /**
+     * get radio capability phase.
+     *
+     * @return RadioCapabilityPhase, including CONFIGURED, START, APPLY, FINISH
+     */
+    public int getPhase() {
+        return mPhase;
+    }
+
+    /**
+     * get radio access family.
+     *
+     * @return radio access family
+     */
+    public int getRadioAccessFamily() {
+        return mRadioAccessFamily;
+    }
+
+    /**
+     * get logical modem Universally Unique ID.
+     *
+     * @return logical modem uuid
+     */
+    public String getLogicalModemUuid() {
+        return mLogicalModemUuid;
+    }
+
+    /**
+     * get request status.
+     *
+     * @return status and an input parameter for RC_PHASE_FINISH
+     */
+    public int getStatus() {
+        return mStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "{mPhoneId = " + mPhoneId
+                + " mVersion=" + getVersion()
+                + " mSession=" + getSession()
+                + " mPhase=" + getPhase()
+                + " mRadioAccessFamily=" + getRadioAccessFamily()
+                + " mLogicModemId=" + getLogicalModemUuid()
+                + " mStatus=" + getStatus()
+                + "}";
+    }
+}
+
diff --git a/com/android/internal/telephony/RadioIndication.java b/com/android/internal/telephony/RadioIndication.java
new file mode 100644
index 0000000..a7d2418
--- /dev/null
+++ b/com/android/internal/telephony/RadioIndication.java
@@ -0,0 +1,845 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CALL_RING;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CDMA_CALL_WAITING;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CDMA_INFO_REC;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CDMA_OTA_PROVISION_STATUS;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_CELL_INFO_LIST;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_DATA_CALL_LIST_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_HARDWARE_CONFIG_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_LCEDATA_RECV;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_MODEM_RESTART;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_NETWORK_SCAN_RESULT;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_NITZ_TIME_RECEIVED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_ON_SS;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_ON_USSD;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_PCO_DATA;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RADIO_CAPABILITY;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESEND_INCALL_MUTE;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_CDMA_NEW_SMS;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_IMS_NETWORK_STATE_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_NETWORK_STATE_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_NEW_SMS;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RESTRICTED_STATE_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RIL_CONNECTED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_RINGBACK_TONE;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_SIGNAL_STRENGTH;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_SIM_REFRESH;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_SIM_SMS_STORAGE_FULL;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_SRVCC_STATE_NOTIFY;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_STK_CALL_SETUP;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_STK_CC_ALPHA_NOTIFY;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_STK_EVENT_NOTIFY;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_STK_PROACTIVE_COMMAND;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_STK_SESSION_END;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_SUPP_SVC_NOTIFICATION;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_UICC_SUBSCRIPTION_STATUS_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_VOICE_RADIO_TECH_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOl_CDMA_PRL_CHANGED;
+
+import android.hardware.radio.V1_0.CdmaCallWaiting;
+import android.hardware.radio.V1_0.CdmaInformationRecord;
+import android.hardware.radio.V1_0.CdmaLineControlInfoRecord;
+import android.hardware.radio.V1_0.CdmaNumberInfoRecord;
+import android.hardware.radio.V1_0.CdmaRedirectingNumberInfoRecord;
+import android.hardware.radio.V1_0.CdmaSignalInfoRecord;
+import android.hardware.radio.V1_0.CdmaSmsMessage;
+import android.hardware.radio.V1_0.CdmaT53AudioControlInfoRecord;
+import android.hardware.radio.V1_0.CfData;
+import android.hardware.radio.V1_0.LceDataInfo;
+import android.hardware.radio.V1_0.PcoDataInfo;
+import android.hardware.radio.V1_0.SetupDataCallResult;
+import android.hardware.radio.V1_0.SimRefreshResult;
+import android.hardware.radio.V1_0.SsInfoData;
+import android.hardware.radio.V1_0.StkCcUnsolSsResult;
+import android.hardware.radio.V1_0.SuppSvcNotification;
+import android.hardware.radio.V1_1.IRadioIndication;
+import android.hardware.radio.V1_1.KeepaliveStatus;
+import android.os.AsyncResult;
+import android.os.SystemProperties;
+import android.telephony.CellInfo;
+import android.telephony.PcoData;
+import android.telephony.SignalStrength;
+import android.telephony.SmsMessage;
+
+import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
+import com.android.internal.telephony.cdma.CdmaInformationRecords;
+import com.android.internal.telephony.cdma.SmsMessageConverter;
+import com.android.internal.telephony.dataconnection.DataCallResponse;
+import com.android.internal.telephony.gsm.SsData;
+import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
+import com.android.internal.telephony.uicc.IccRefreshResponse;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.util.ArrayList;
+
+public class RadioIndication extends IRadioIndication.Stub {
+    RIL mRil;
+
+    RadioIndication(RIL ril) {
+        mRil = ril;
+    }
+
+    /**
+     * Indicates when radio state changes.
+     * @param indicationType RadioIndicationType
+     * @param radioState android.hardware.radio.V1_0.RadioState
+     */
+    public void radioStateChanged(int indicationType, int radioState) {
+        mRil.processIndication(indicationType);
+
+        CommandsInterface.RadioState newState = getRadioStateFromInt(radioState);
+        if (RIL.RILJ_LOGD) {
+            mRil.unsljLogMore(RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED, "radioStateChanged: " +
+                    newState);
+        }
+
+        mRil.setRadioState(newState);
+    }
+
+    public void callStateChanged(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED);
+
+        mRil.mCallStateRegistrants.notifyRegistrants();
+    }
+
+    /**
+     * Indicates when either voice or data network state changed
+     * @param indicationType RadioIndicationType
+     */
+    public void networkStateChanged(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_NETWORK_STATE_CHANGED);
+
+        mRil.mNetworkStateRegistrants.notifyRegistrants();
+    }
+
+    public void newSms(int indicationType, ArrayList<Byte> pdu) {
+        mRil.processIndication(indicationType);
+
+        byte[] pduArray = RIL.arrayListToPrimitiveArray(pdu);
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_NEW_SMS);
+
+        mRil.writeMetricsNewSms(SmsSession.Event.Tech.SMS_GSM,
+                SmsSession.Event.Format.SMS_FORMAT_3GPP);
+
+        SmsMessage sms = SmsMessage.newFromCMT(pduArray);
+        if (mRil.mGsmSmsRegistrant != null) {
+            mRil.mGsmSmsRegistrant.notifyRegistrant(new AsyncResult(null, sms, null));
+        }
+    }
+
+    public void newSmsStatusReport(int indicationType, ArrayList<Byte> pdu) {
+        mRil.processIndication(indicationType);
+
+        byte[] pduArray = RIL.arrayListToPrimitiveArray(pdu);
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT);
+
+        if (mRil.mSmsStatusRegistrant != null) {
+            mRil.mSmsStatusRegistrant.notifyRegistrant(new AsyncResult(null, pduArray, null));
+        }
+    }
+
+    public void newSmsOnSim(int indicationType, int recordNumber) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM);
+
+        if (mRil.mSmsOnSimRegistrant != null) {
+            mRil.mSmsOnSimRegistrant.notifyRegistrant(new AsyncResult(null, recordNumber, null));
+        }
+    }
+
+    public void onUssd(int indicationType, int ussdModeType, String msg) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogMore(RIL_UNSOL_ON_USSD, "" + ussdModeType);
+
+        // todo: Clean this up with a parcelable class for better self-documentation
+        String[] resp = new String[2];
+        resp[0] = "" + ussdModeType;
+        resp[1] = msg;
+        if (mRil.mUSSDRegistrant != null) {
+            mRil.mUSSDRegistrant.notifyRegistrant(new AsyncResult (null, resp, null));
+        }
+    }
+
+    public void nitzTimeReceived(int indicationType, String nitzTime, long receivedTime) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_NITZ_TIME_RECEIVED, nitzTime);
+
+        // todo: Clean this up with a parcelable class for better self-documentation
+        Object[] result = new Object[2];
+        result[0] = nitzTime;
+        result[1] = receivedTime;
+
+        boolean ignoreNitz = SystemProperties.getBoolean(
+                TelephonyProperties.PROPERTY_IGNORE_NITZ, false);
+
+        if (ignoreNitz) {
+            if (RIL.RILJ_LOGD) mRil.riljLog("ignoring UNSOL_NITZ_TIME_RECEIVED");
+        } else {
+            if (mRil.mNITZTimeRegistrant != null) {
+                mRil.mNITZTimeRegistrant.notifyRegistrant(new AsyncResult (null, result, null));
+            }
+            // in case NITZ time registrant isn't registered yet, or a new registrant
+            // registers later
+            mRil.mLastNITZTimeInfo = result;
+        }
+    }
+
+    public void currentSignalStrength(int indicationType,
+                                      android.hardware.radio.V1_0.SignalStrength signalStrength) {
+        mRil.processIndication(indicationType);
+
+        SignalStrength ss = RIL.convertHalSignalStrength(signalStrength);
+        // Note this is set to "verbose" because it happens frequently
+        if (RIL.RILJ_LOGV) mRil.unsljLogvRet(RIL_UNSOL_SIGNAL_STRENGTH, ss);
+
+        if (mRil.mSignalStrengthRegistrant != null) {
+            mRil.mSignalStrengthRegistrant.notifyRegistrant(new AsyncResult (null, ss, null));
+        }
+    }
+
+    public void dataCallListChanged(int indicationType, ArrayList<SetupDataCallResult> dcList) {
+        mRil.processIndication(indicationType);
+
+        ArrayList<DataCallResponse> response = new ArrayList<>();
+
+        for (SetupDataCallResult dcResult : dcList) {
+            response.add(RIL.convertDataCallResult(dcResult));
+        }
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_DATA_CALL_LIST_CHANGED, response);
+
+        mRil.mDataCallListChangedRegistrants.notifyRegistrants(
+                new AsyncResult(null, response, null));
+    }
+
+    public void suppSvcNotify(int indicationType, SuppSvcNotification suppSvcNotification) {
+        mRil.processIndication(indicationType);
+
+        SuppServiceNotification notification = new SuppServiceNotification();
+        notification.notificationType = suppSvcNotification.isMT ? 1 : 0;
+        notification.code = suppSvcNotification.code;
+        notification.index = suppSvcNotification.index;
+        notification.type = suppSvcNotification.type;
+        notification.number = suppSvcNotification.number;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_SUPP_SVC_NOTIFICATION, notification);
+
+        if (mRil.mSsnRegistrant != null) {
+            mRil.mSsnRegistrant.notifyRegistrant(new AsyncResult (null, notification, null));
+        }
+    }
+
+    public void stkSessionEnd(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_STK_SESSION_END);
+
+        if (mRil.mCatSessionEndRegistrant != null) {
+            mRil.mCatSessionEndRegistrant.notifyRegistrant(new AsyncResult (null, null, null));
+        }
+    }
+
+    public void stkProactiveCommand(int indicationType, String cmd) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_STK_PROACTIVE_COMMAND);
+
+        if (mRil.mCatProCmdRegistrant != null) {
+            mRil.mCatProCmdRegistrant.notifyRegistrant(new AsyncResult (null, cmd, null));
+        }
+    }
+
+    public void stkEventNotify(int indicationType, String cmd) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_STK_EVENT_NOTIFY);
+
+        if (mRil.mCatEventRegistrant != null) {
+            mRil.mCatEventRegistrant.notifyRegistrant(new AsyncResult (null, cmd, null));
+        }
+    }
+
+    public void stkCallSetup(int indicationType, long timeout) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_STK_CALL_SETUP, timeout);
+
+        if (mRil.mCatCallSetUpRegistrant != null) {
+            mRil.mCatCallSetUpRegistrant.notifyRegistrant(new AsyncResult (null, timeout, null));
+        }
+    }
+
+    public void simSmsStorageFull(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_SIM_SMS_STORAGE_FULL);
+
+        if (mRil.mIccSmsFullRegistrant != null) {
+            mRil.mIccSmsFullRegistrant.notifyRegistrant();
+        }
+    }
+
+    public void simRefresh(int indicationType, SimRefreshResult refreshResult) {
+        mRil.processIndication(indicationType);
+
+        IccRefreshResponse response = new IccRefreshResponse();
+        response.refreshResult = refreshResult.type;
+        response.efId = refreshResult.efId;
+        response.aid = refreshResult.aid;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_SIM_REFRESH, response);
+
+        mRil.mIccRefreshRegistrants.notifyRegistrants(new AsyncResult (null, response, null));
+    }
+
+    public void callRing(int indicationType, boolean isGsm, CdmaSignalInfoRecord record) {
+        mRil.processIndication(indicationType);
+
+        char response[] = null;
+
+        // Ignore record for gsm
+        if (!isGsm) {
+            // todo: Clean this up with a parcelable class for better self-documentation
+            response = new char[4];
+            response[0] = (char) (record.isPresent ? 1 : 0);
+            response[1] = (char) record.signalType;
+            response[2] = (char) record.alertPitch;
+            response[3] = (char) record.signal;
+            mRil.writeMetricsCallRing(response);
+        }
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CALL_RING, response);
+
+        if (mRil.mRingRegistrant != null) {
+            mRil.mRingRegistrant.notifyRegistrant(new AsyncResult (null, response, null));
+        }
+    }
+
+    public void simStatusChanged(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED);
+
+        mRil.mIccStatusChangedRegistrants.notifyRegistrants();
+    }
+
+    public void cdmaNewSms(int indicationType, CdmaSmsMessage msg) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_CDMA_NEW_SMS);
+
+        mRil.writeMetricsNewSms(SmsSession.Event.Tech.SMS_CDMA,
+                SmsSession.Event.Format.SMS_FORMAT_3GPP2);
+
+        // todo: conversion from CdmaSmsMessage to SmsMessage should be contained in this class so
+        // that usage of auto-generated HAL classes is limited to this file
+        SmsMessage sms = SmsMessageConverter.newSmsMessageFromCdmaSmsMessage(msg);
+        if (mRil.mCdmaSmsRegistrant != null) {
+            mRil.mCdmaSmsRegistrant.notifyRegistrant(new AsyncResult(null, sms, null));
+        }
+    }
+
+    public void newBroadcastSms(int indicationType, ArrayList<Byte> data) {
+        mRil.processIndication(indicationType);
+
+        byte response[] = RIL.arrayListToPrimitiveArray(data);
+        if (RIL.RILJ_LOGD) {
+            mRil.unsljLogvRet(RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS,
+                    IccUtils.bytesToHexString(response));
+        }
+
+        if (mRil.mGsmBroadcastSmsRegistrant != null) {
+            mRil.mGsmBroadcastSmsRegistrant.notifyRegistrant(new AsyncResult(null, response, null));
+        }
+    }
+
+    public void cdmaRuimSmsStorageFull(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL);
+
+        if (mRil.mIccSmsFullRegistrant != null) {
+            mRil.mIccSmsFullRegistrant.notifyRegistrant();
+        }
+    }
+
+    public void restrictedStateChanged(int indicationType, int state) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogvRet(RIL_UNSOL_RESTRICTED_STATE_CHANGED, state);
+
+        if (mRil.mRestrictedStateRegistrant != null) {
+            mRil.mRestrictedStateRegistrant.notifyRegistrant(new AsyncResult (null, state, null));
+        }
+    }
+
+    public void enterEmergencyCallbackMode(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE);
+
+        if (mRil.mEmergencyCallbackModeRegistrant != null) {
+            mRil.mEmergencyCallbackModeRegistrant.notifyRegistrant();
+        }
+    }
+
+    public void cdmaCallWaiting(int indicationType, CdmaCallWaiting callWaitingRecord) {
+        mRil.processIndication(indicationType);
+
+        // todo: create a CdmaCallWaitingNotification constructor that takes in these fields to make
+        // sure no fields are missing
+        CdmaCallWaitingNotification notification = new CdmaCallWaitingNotification();
+        notification.number = callWaitingRecord.number;
+        notification.numberPresentation = CdmaCallWaitingNotification.presentationFromCLIP(
+                callWaitingRecord.numberPresentation);
+        notification.name = callWaitingRecord.name;
+        notification.namePresentation = notification.numberPresentation;
+        notification.isPresent = callWaitingRecord.signalInfoRecord.isPresent ? 1 : 0;
+        notification.signalType = callWaitingRecord.signalInfoRecord.signalType;
+        notification.alertPitch = callWaitingRecord.signalInfoRecord.alertPitch;
+        notification.signal = callWaitingRecord.signalInfoRecord.signal;
+        notification.numberType = callWaitingRecord.numberType;
+        notification.numberPlan = callWaitingRecord.numberPlan;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CDMA_CALL_WAITING, notification);
+
+        mRil.mCallWaitingInfoRegistrants.notifyRegistrants(
+                new AsyncResult (null, notification, null));
+    }
+
+    public void cdmaOtaProvisionStatus(int indicationType, int status) {
+        mRil.processIndication(indicationType);
+
+        int response[] = new int[1];
+        response[0] = status;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CDMA_OTA_PROVISION_STATUS, response);
+
+        mRil.mOtaProvisionRegistrants.notifyRegistrants(new AsyncResult (null, response, null));
+    }
+
+    public void cdmaInfoRec(int indicationType,
+                            android.hardware.radio.V1_0.CdmaInformationRecords records) {
+        mRil.processIndication(indicationType);
+
+        int numberOfInfoRecs = records.infoRec.size();
+        for (int i = 0; i < numberOfInfoRecs; i++) {
+            CdmaInformationRecord record = records.infoRec.get(i);
+            int id = record.name;
+            CdmaInformationRecords cdmaInformationRecords;
+            switch (id) {
+                case CdmaInformationRecords.RIL_CDMA_DISPLAY_INFO_REC:
+                case CdmaInformationRecords.RIL_CDMA_EXTENDED_DISPLAY_INFO_REC:
+                    CdmaInformationRecords.CdmaDisplayInfoRec cdmaDisplayInfoRec =
+                            new CdmaInformationRecords.CdmaDisplayInfoRec(id,
+                            record.display.get(0).alphaBuf);
+                    cdmaInformationRecords = new CdmaInformationRecords(cdmaDisplayInfoRec);
+                    break;
+
+                case CdmaInformationRecords.RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC:
+                case CdmaInformationRecords.RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC:
+                case CdmaInformationRecords.RIL_CDMA_CONNECTED_NUMBER_INFO_REC:
+                    CdmaNumberInfoRecord numInfoRecord = record.number.get(0);
+                    CdmaInformationRecords.CdmaNumberInfoRec cdmaNumberInfoRec =
+                            new CdmaInformationRecords.CdmaNumberInfoRec(id,
+                            numInfoRecord.number,
+                            numInfoRecord.numberType,
+                            numInfoRecord.numberPlan,
+                            numInfoRecord.pi,
+                            numInfoRecord.si);
+                    cdmaInformationRecords = new CdmaInformationRecords(cdmaNumberInfoRec);
+                    break;
+
+                case CdmaInformationRecords.RIL_CDMA_SIGNAL_INFO_REC:
+                    CdmaSignalInfoRecord signalInfoRecord = record.signal.get(0);
+                    CdmaInformationRecords.CdmaSignalInfoRec cdmaSignalInfoRec =
+                            new CdmaInformationRecords.CdmaSignalInfoRec(
+                            signalInfoRecord.isPresent ? 1 : 0,
+                            signalInfoRecord.signalType,
+                            signalInfoRecord.alertPitch,
+                            signalInfoRecord.signal);
+                    cdmaInformationRecords = new CdmaInformationRecords(cdmaSignalInfoRec);
+                    break;
+
+                case CdmaInformationRecords.RIL_CDMA_REDIRECTING_NUMBER_INFO_REC:
+                    CdmaRedirectingNumberInfoRecord redirectingNumberInfoRecord =
+                            record.redir.get(0);
+                    CdmaInformationRecords.CdmaRedirectingNumberInfoRec
+                            cdmaRedirectingNumberInfoRec =
+                            new CdmaInformationRecords.CdmaRedirectingNumberInfoRec(
+                            redirectingNumberInfoRecord.redirectingNumber.number,
+                            redirectingNumberInfoRecord.redirectingNumber.numberType,
+                            redirectingNumberInfoRecord.redirectingNumber.numberPlan,
+                            redirectingNumberInfoRecord.redirectingNumber.pi,
+                            redirectingNumberInfoRecord.redirectingNumber.si,
+                            redirectingNumberInfoRecord.redirectingReason);
+                    cdmaInformationRecords = new CdmaInformationRecords(
+                            cdmaRedirectingNumberInfoRec);
+                    break;
+
+                case CdmaInformationRecords.RIL_CDMA_LINE_CONTROL_INFO_REC:
+                    CdmaLineControlInfoRecord lineControlInfoRecord = record.lineCtrl.get(0);
+                    CdmaInformationRecords.CdmaLineControlInfoRec cdmaLineControlInfoRec =
+                            new CdmaInformationRecords.CdmaLineControlInfoRec(
+                            lineControlInfoRecord.lineCtrlPolarityIncluded,
+                            lineControlInfoRecord.lineCtrlToggle,
+                            lineControlInfoRecord.lineCtrlReverse,
+                            lineControlInfoRecord.lineCtrlPowerDenial);
+                    cdmaInformationRecords = new CdmaInformationRecords(cdmaLineControlInfoRec);
+                    break;
+
+                case CdmaInformationRecords.RIL_CDMA_T53_CLIR_INFO_REC:
+                    CdmaInformationRecords.CdmaT53ClirInfoRec cdmaT53ClirInfoRec =
+                            new CdmaInformationRecords.CdmaT53ClirInfoRec(record.clir.get(0).cause);
+                    cdmaInformationRecords = new CdmaInformationRecords(cdmaT53ClirInfoRec);
+                    break;
+
+                case CdmaInformationRecords.RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC:
+                    CdmaT53AudioControlInfoRecord audioControlInfoRecord = record.audioCtrl.get(0);
+                    CdmaInformationRecords.CdmaT53AudioControlInfoRec cdmaT53AudioControlInfoRec =
+                            new CdmaInformationRecords.CdmaT53AudioControlInfoRec(
+                            audioControlInfoRecord.upLink,
+                            audioControlInfoRecord.downLink);
+                    cdmaInformationRecords = new CdmaInformationRecords(cdmaT53AudioControlInfoRec);
+                    break;
+
+                default:
+                    throw new RuntimeException("RIL_UNSOL_CDMA_INFO_REC: unsupported record. Got "
+                            + CdmaInformationRecords.idToString(id) + " ");
+            }
+
+            if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CDMA_INFO_REC, cdmaInformationRecords);
+            mRil.notifyRegistrantsCdmaInfoRec(cdmaInformationRecords);
+        }
+    }
+
+    public void indicateRingbackTone(int indicationType, boolean start) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogvRet(RIL_UNSOL_RINGBACK_TONE, start);
+
+        mRil.mRingbackToneRegistrants.notifyRegistrants(new AsyncResult(null, start, null));
+    }
+
+    public void resendIncallMute(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESEND_INCALL_MUTE);
+
+        mRil.mResendIncallMuteRegistrants.notifyRegistrants();
+    }
+
+    public void cdmaSubscriptionSourceChanged(int indicationType, int cdmaSource) {
+        mRil.processIndication(indicationType);
+
+        int response[] = new int[1];
+        response[0] = cdmaSource;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED, response);
+
+        mRil.mCdmaSubscriptionChangedRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void cdmaPrlChanged(int indicationType, int version) {
+        mRil.processIndication(indicationType);
+
+        int response[] = new int[1];
+        response[0] = version;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOl_CDMA_PRL_CHANGED, response);
+
+        mRil.mCdmaPrlChangedRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void exitEmergencyCallbackMode(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE);
+
+        mRil.mExitEmergencyCallbackModeRegistrants.notifyRegistrants();
+    }
+
+    public void rilConnected(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RIL_CONNECTED);
+
+        // Initial conditions
+        mRil.setRadioPower(false, null);
+        mRil.setCdmaSubscriptionSource(mRil.mCdmaSubscription, null);
+        mRil.setCellInfoListRate();
+        // todo: this should not require a version number now. Setting it to latest RIL version for
+        // now.
+        mRil.notifyRegistrantsRilConnectionChanged(15);
+    }
+
+    public void voiceRadioTechChanged(int indicationType, int rat) {
+        mRil.processIndication(indicationType);
+
+        int response[] = new int[1];
+        response[0] = rat;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_VOICE_RADIO_TECH_CHANGED, response);
+
+        mRil.mVoiceRadioTechChangedRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void cellInfoList(int indicationType,
+                             ArrayList<android.hardware.radio.V1_0.CellInfo> records) {
+        mRil.processIndication(indicationType);
+
+        ArrayList<CellInfo> response = RIL.convertHalCellInfoList(records);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CELL_INFO_LIST, response);
+
+        mRil.mRilCellInfoListRegistrants.notifyRegistrants(new AsyncResult (null, response, null));
+    }
+
+    /** Incremental network scan results */
+    public void networkScanResult(int indicationType,
+                                  android.hardware.radio.V1_1.NetworkScanResult result) {
+        responseCellInfos(indicationType, result);
+    }
+
+    public void imsNetworkStateChanged(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLog(RIL_UNSOL_RESPONSE_IMS_NETWORK_STATE_CHANGED);
+
+        mRil.mImsNetworkStateChangedRegistrants.notifyRegistrants();
+    }
+
+    public void subscriptionStatusChanged(int indicationType, boolean activate) {
+        mRil.processIndication(indicationType);
+
+        int response[] = new int[1];
+        response[0] = activate ? 1 : 0;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_UICC_SUBSCRIPTION_STATUS_CHANGED, response);
+
+        mRil.mSubscriptionStatusRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void srvccStateNotify(int indicationType, int state) {
+        mRil.processIndication(indicationType);
+
+        int response[] = new int[1];
+        response[0] = state;
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_SRVCC_STATE_NOTIFY, response);
+
+        mRil.writeMetricsSrvcc(state);
+
+        mRil.mSrvccStateRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void hardwareConfigChanged(
+            int indicationType,
+            ArrayList<android.hardware.radio.V1_0.HardwareConfig> configs) {
+        mRil.processIndication(indicationType);
+
+        ArrayList<HardwareConfig> response = RIL.convertHalHwConfigList(configs, mRil);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_HARDWARE_CONFIG_CHANGED, response);
+
+        mRil.mHardwareConfigChangeRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void radioCapabilityIndication(int indicationType,
+                                          android.hardware.radio.V1_0.RadioCapability rc) {
+        mRil.processIndication(indicationType);
+
+        RadioCapability response = RIL.convertHalRadioCapability(rc, mRil);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_RADIO_CAPABILITY, response);
+
+        mRil.mPhoneRadioCapabilityChangedRegistrants.notifyRegistrants(
+                new AsyncResult (null, response, null));
+    }
+
+    public void onSupplementaryServiceIndication(int indicationType, StkCcUnsolSsResult ss) {
+        mRil.processIndication(indicationType);
+
+        int num;
+        SsData ssData = new SsData();
+
+        ssData.serviceType = ssData.ServiceTypeFromRILInt(ss.serviceType);
+        ssData.requestType = ssData.RequestTypeFromRILInt(ss.requestType);
+        ssData.teleserviceType = ssData.TeleserviceTypeFromRILInt(ss.teleserviceType);
+        ssData.serviceClass = ss.serviceClass; // This is service class sent in the SS request.
+        ssData.result = ss.result; // This is the result of the SS request.
+
+        if (ssData.serviceType.isTypeCF() &&
+                ssData.requestType.isTypeInterrogation()) {
+            CfData cfData = ss.cfData.get(0);
+            num = cfData.cfInfo.size();
+            ssData.cfInfo = new CallForwardInfo[num];
+
+            for (int i = 0; i < num; i++) {
+                android.hardware.radio.V1_0.CallForwardInfo cfInfo = cfData.cfInfo.get(i);
+                ssData.cfInfo[i] = new CallForwardInfo();
+
+                ssData.cfInfo[i].status = cfInfo.status;
+                ssData.cfInfo[i].reason = cfInfo.reason;
+                ssData.cfInfo[i].serviceClass = cfInfo.serviceClass;
+                ssData.cfInfo[i].toa = cfInfo.toa;
+                ssData.cfInfo[i].number = cfInfo.number;
+                ssData.cfInfo[i].timeSeconds = cfInfo.timeSeconds;
+
+                mRil.riljLog("[SS Data] CF Info " + i + " : " +  ssData.cfInfo[i]);
+            }
+        } else {
+            SsInfoData ssInfo = ss.ssInfo.get(0);
+            num = ssInfo.ssInfo.size();
+            ssData.ssInfo = new int[num];
+            for (int i = 0; i < num; i++) {
+                ssData.ssInfo[i] = ssInfo.ssInfo.get(i);
+                mRil.riljLog("[SS Data] SS Info " + i + " : " +  ssData.ssInfo[i]);
+            }
+        }
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_ON_SS, ssData);
+
+        if (mRil.mSsRegistrant != null) {
+            mRil.mSsRegistrant.notifyRegistrant(new AsyncResult(null, ssData, null));
+        }
+    }
+
+    public void stkCallControlAlphaNotify(int indicationType, String alpha) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_STK_CC_ALPHA_NOTIFY, alpha);
+
+        if (mRil.mCatCcAlphaRegistrant != null) {
+            mRil.mCatCcAlphaRegistrant.notifyRegistrant(new AsyncResult (null, alpha, null));
+        }
+    }
+
+    public void lceData(int indicationType, LceDataInfo lce) {
+        mRil.processIndication(indicationType);
+
+        ArrayList<Integer> response = RIL.convertHalLceData(lce, mRil);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_LCEDATA_RECV, response);
+
+        if (mRil.mLceInfoRegistrant != null) {
+            mRil.mLceInfoRegistrant.notifyRegistrant(new AsyncResult(null, response, null));
+        }
+    }
+
+    public void pcoData(int indicationType, PcoDataInfo pco) {
+        mRil.processIndication(indicationType);
+
+        PcoData response = new PcoData(pco.cid,
+                pco.bearerProto,
+                pco.pcoId,
+                RIL.arrayListToPrimitiveArray(pco.contents));
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_PCO_DATA, response);
+
+        mRil.mPcoDataRegistrants.notifyRegistrants(new AsyncResult(null, response, null));
+    }
+
+    public void modemReset(int indicationType, String reason) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_MODEM_RESTART, reason);
+
+        mRil.writeMetricsModemRestartEvent(reason);
+        mRil.mModemResetRegistrants.notifyRegistrants(new AsyncResult(null, reason, null));
+    }
+
+    /**
+     * Indicates when the carrier info to encrypt IMSI is being requested
+     * @param indicationType RadioIndicationType
+     */
+    public void carrierInfoForImsiEncryption(int indicationType) {
+        mRil.processIndication(indicationType);
+
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION, null);
+
+        mRil.mCarrierInfoForImsiEncryptionRegistrants.notifyRegistrants(
+                new AsyncResult(null, null, null));
+    }
+
+    /**
+     * Indicates a change in the status of an ongoing Keepalive session
+     * @param indicationType RadioIndicationType
+     * @param keepaliveStatus Status of the ongoing Keepalive session
+     */
+    public void keepaliveStatus(int indicationType, KeepaliveStatus keepaliveStatus) {
+        throw new UnsupportedOperationException("keepaliveStatus Indications are not implemented");
+    }
+
+    private CommandsInterface.RadioState getRadioStateFromInt(int stateInt) {
+        CommandsInterface.RadioState state;
+
+        switch(stateInt) {
+            case android.hardware.radio.V1_0.RadioState.OFF:
+                state = CommandsInterface.RadioState.RADIO_OFF;
+                break;
+            case android.hardware.radio.V1_0.RadioState.UNAVAILABLE:
+                state = CommandsInterface.RadioState.RADIO_UNAVAILABLE;
+                break;
+            case android.hardware.radio.V1_0.RadioState.ON:
+                state = CommandsInterface.RadioState.RADIO_ON;
+                break;
+            default:
+                throw new RuntimeException("Unrecognized RadioState: " + stateInt);
+        }
+        return state;
+    }
+
+    private void responseCellInfos(int indicationType,
+                                   android.hardware.radio.V1_1.NetworkScanResult result) {
+        mRil.processIndication(indicationType);
+
+        NetworkScanResult nsr = null;
+        ArrayList<CellInfo> infos = RIL.convertHalCellInfoList(result.networkInfos);
+        nsr = new NetworkScanResult(result.status, result.error, infos);
+        if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_NETWORK_SCAN_RESULT, nsr);
+        mRil.mRilNetworkScanResultRegistrants.notifyRegistrants(new AsyncResult(null, nsr, null));
+    }
+}
diff --git a/com/android/internal/telephony/RadioNVItems.java b/com/android/internal/telephony/RadioNVItems.java
new file mode 100644
index 0000000..758ac43
--- /dev/null
+++ b/com/android/internal/telephony/RadioNVItems.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+/**
+ * List of radio NV items that can be set/get through the RIL interface.
+ * Used for device configuration by some CDMA operators.
+ *
+ * @see RIL#nvReadItem
+ * @see RIL#nvWriteItem
+ */
+public interface RadioNVItems {
+
+    // CDMA radio and account information (items 1-10)
+    int RIL_NV_CDMA_MEID = 1;                   // CDMA MEID (hex)
+    int RIL_NV_CDMA_MIN = 2;                    // CDMA MIN (MSID)
+    int RIL_NV_CDMA_MDN = 3;                    // CDMA MDN
+    int RIL_NV_CDMA_ACCOLC = 4;                 // CDMA access overload control
+
+    // Carrier device provisioning (items 11-30)
+    int RIL_NV_DEVICE_MSL = 11;                 // device MSL
+    int RIL_NV_RTN_RECONDITIONED_STATUS = 12;   // RTN reconditioned status
+    int RIL_NV_RTN_ACTIVATION_DATE = 13;        // RTN activation date
+    int RIL_NV_RTN_LIFE_TIMER = 14;             // RTN life timer
+    int RIL_NV_RTN_LIFE_CALLS = 15;             // RTN life calls
+    int RIL_NV_RTN_LIFE_DATA_TX = 16;           // RTN life data TX
+    int RIL_NV_RTN_LIFE_DATA_RX = 17;           // RTN life data RX
+    int RIL_NV_OMADM_HFA_LEVEL = 18;            // HFA in progress
+
+    // Mobile IP profile information (items 31-50)
+    int RIL_NV_MIP_PROFILE_NAI = 31;            // NAI realm
+    int RIL_NV_MIP_PROFILE_HOME_ADDRESS = 32;   // MIP home address
+    int RIL_NV_MIP_PROFILE_AAA_AUTH = 33;       // AAA auth
+    int RIL_NV_MIP_PROFILE_HA_AUTH = 34;        // HA auth
+    int RIL_NV_MIP_PROFILE_PRI_HA_ADDR = 35;    // primary HA address
+    int RIL_NV_MIP_PROFILE_SEC_HA_ADDR = 36;    // secondary HA address
+    int RIL_NV_MIP_PROFILE_REV_TUN_PREF = 37;   // reverse TUN pref
+    int RIL_NV_MIP_PROFILE_HA_SPI = 38;         // HA SPI
+    int RIL_NV_MIP_PROFILE_AAA_SPI = 39;        // AAA SPI
+    int RIL_NV_MIP_PROFILE_MN_HA_SS = 40;       // HA shared secret
+    int RIL_NV_MIP_PROFILE_MN_AAA_SS = 41;      // AAA shared secret
+
+    // CDMA network and band config (items 51-70)
+    int RIL_NV_CDMA_PRL_VERSION = 51;           // CDMA PRL version
+    int RIL_NV_CDMA_BC10 = 52;                  // CDMA band class 10
+    int RIL_NV_CDMA_BC14 = 53;                  // CDMA band class 14
+    int RIL_NV_CDMA_SO68 = 54;                  // CDMA SO68
+    int RIL_NV_CDMA_SO73_COP0 = 55;             // CDMA SO73 COP0
+    int RIL_NV_CDMA_SO73_COP1TO7 = 56;          // CDMA SO73 COP1-7
+    int RIL_NV_CDMA_1X_ADVANCED_ENABLED = 57;   // CDMA 1X Advanced enabled
+    int RIL_NV_CDMA_EHRPD_ENABLED = 58;         // CDMA eHRPD enabled
+    int RIL_NV_CDMA_EHRPD_FORCED = 59;          // CDMA eHRPD forced
+
+    // LTE network and band config (items 71-90)
+    int RIL_NV_LTE_BAND_ENABLE_25 = 71;         // LTE band 25 enable
+    int RIL_NV_LTE_BAND_ENABLE_26 = 72;         // LTE band 26 enable
+    int RIL_NV_LTE_BAND_ENABLE_41 = 73;         // LTE band 41 enable
+
+    int RIL_NV_LTE_SCAN_PRIORITY_25 = 74;       // LTE band 25 scan priority
+    int RIL_NV_LTE_SCAN_PRIORITY_26 = 75;       // LTE band 26 scan priority
+    int RIL_NV_LTE_SCAN_PRIORITY_41 = 76;       // LTE band 41 scan priority
+
+    int RIL_NV_LTE_HIDDEN_BAND_PRIORITY_25 = 77;    // LTE hidden band 25 priority
+    int RIL_NV_LTE_HIDDEN_BAND_PRIORITY_26 = 78;    // LTE hidden band 26 priority
+    int RIL_NV_LTE_HIDDEN_BAND_PRIORITY_41 = 79;    // LTE hidden band 41 priority
+
+    int RIL_NV_LTE_NEXT_SCAN = 80;              // LTE next scan
+    int RIL_NV_LTE_BSR_TIMER = 81;              // LTE BSR timer
+    int RIL_NV_LTE_BSR_MAX_TIME = 82;           // LTE BSR max time
+}
diff --git a/com/android/internal/telephony/RadioResponse.java b/com/android/internal/telephony/RadioResponse.java
new file mode 100644
index 0000000..caf4477
--- /dev/null
+++ b/com/android/internal/telephony/RadioResponse.java
@@ -0,0 +1,1814 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.content.Context;
+import android.hardware.radio.V1_0.ActivityStatsInfo;
+import android.hardware.radio.V1_0.AppStatus;
+import android.hardware.radio.V1_0.CardStatus;
+import android.hardware.radio.V1_0.CarrierRestrictions;
+import android.hardware.radio.V1_0.CdmaBroadcastSmsConfigInfo;
+import android.hardware.radio.V1_0.DataRegStateResult;
+import android.hardware.radio.V1_0.GsmBroadcastSmsConfigInfo;
+import android.hardware.radio.V1_0.LastCallFailCauseInfo;
+import android.hardware.radio.V1_0.LceDataInfo;
+import android.hardware.radio.V1_0.LceStatusInfo;
+import android.hardware.radio.V1_0.NeighboringCell;
+import android.hardware.radio.V1_0.RadioError;
+import android.hardware.radio.V1_0.RadioResponseInfo;
+import android.hardware.radio.V1_0.SendSmsResult;
+import android.hardware.radio.V1_0.SetupDataCallResult;
+import android.hardware.radio.V1_0.VoiceRegStateResult;
+import android.hardware.radio.V1_1.IRadioResponse;
+import android.hardware.radio.V1_1.KeepaliveStatus;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.os.SystemClock;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.CellInfo;
+import android.telephony.ModemActivityInfo;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.dataconnection.DataCallResponse;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus;
+import com.android.internal.telephony.uicc.IccCardStatus;
+import com.android.internal.telephony.uicc.IccIoResult;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class RadioResponse extends IRadioResponse.Stub {
+    // The number of the required config values for broadcast SMS stored in the C struct
+    // RIL_CDMA_BroadcastServiceInfo
+    private static final int CDMA_BSI_NO_OF_INTS_STRUCT = 3;
+
+    private static final int CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES = 31;
+
+    RIL mRil;
+
+    public RadioResponse(RIL ril) {
+        mRil = ril;
+    }
+
+    /**
+     * Helper function to send response msg
+     * @param msg Response message to be sent
+     * @param ret Return object to be included in the response message
+     */
+    static void sendMessageResponse(Message msg, Object ret) {
+        if (msg != null) {
+            AsyncResult.forMessage(msg, ret, null);
+            msg.sendToTarget();
+        }
+    }
+
+    /**
+     * Acknowledge the receipt of radio request sent to the vendor. This must be sent only for
+     * radio request which take long time to respond.
+     * For more details, refer https://source.android.com/devices/tech/connect/ril.html
+     *
+     * @param serial Serial no. of the request whose acknowledgement is sent.
+     */
+    public void acknowledgeRequest(int serial) {
+        mRil.processRequestAck(serial);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param cardStatus ICC card status as defined by CardStatus in types.hal
+     */
+    public void getIccCardStatusResponse(RadioResponseInfo responseInfo, CardStatus cardStatus) {
+        responseIccCardStatus(responseInfo, cardStatus);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void supplyIccPinForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
+        responseInts(responseInfo, remainingAttempts);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void supplyIccPukForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
+        responseInts(responseInfo, remainingAttempts);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void supplyIccPin2ForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
+        responseInts(responseInfo, remainingAttempts);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void supplyIccPuk2ForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
+        responseInts(responseInfo, remainingAttempts);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void changeIccPinForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
+        responseInts(responseInfo, remainingAttempts);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void changeIccPin2ForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
+        responseInts(responseInfo, remainingAttempts);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param retriesRemaining Number of retries remaining, must be equal to -1 if unknown.
+     */
+    public void supplyNetworkDepersonalizationResponse(RadioResponseInfo responseInfo,
+                                                       int retriesRemaining) {
+        responseInts(responseInfo, retriesRemaining);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param calls Current call list
+     */
+    public void getCurrentCallsResponse(RadioResponseInfo responseInfo,
+                                        ArrayList<android.hardware.radio.V1_0.Call> calls) {
+        responseCurrentCalls(responseInfo, calls);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void dialResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param imsi String containing the IMSI
+     */
+    public void getIMSIForAppResponse(RadioResponseInfo responseInfo, String imsi) {
+        responseString(responseInfo, imsi);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void hangupConnectionResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void hangupWaitingOrBackgroundResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void hangupForegroundResumeBackgroundResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void switchWaitingOrHoldingAndActiveResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void conferenceResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void rejectCallResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param fcInfo Contains LastCallFailCause and vendor cause code. GSM failure reasons
+     *        are mapped to cause codes defined in TS 24.008 Annex H where possible. CDMA
+     *        failure reasons are derived from the possible call failure scenarios
+     *        described in the "CDMA IS-2000 Release A (C.S0005-A v6.0)" standard.
+     */
+    public void getLastCallFailCauseResponse(RadioResponseInfo responseInfo,
+                                             LastCallFailCauseInfo fcInfo) {
+        responseLastCallFailCauseInfo(responseInfo, fcInfo);
+    }
+
+    public void getSignalStrengthResponse(RadioResponseInfo responseInfo,
+                                          android.hardware.radio.V1_0.SignalStrength sigStrength) {
+        responseSignalStrength(responseInfo, sigStrength);
+    }
+
+    /*
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param voiceRegResponse Current Voice registration response as defined by VoiceRegStateResult
+     *        in types.hal
+     */
+    public void getVoiceRegistrationStateResponse(RadioResponseInfo responseInfo,
+                                                  VoiceRegStateResult voiceRegResponse) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, voiceRegResponse);
+            }
+            mRil.processResponseDone(rr, responseInfo, voiceRegResponse);
+        }
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param dataRegResponse Current Data registration response as defined by DataRegStateResult in
+     *        types.hal
+     */
+    public void getDataRegistrationStateResponse(RadioResponseInfo responseInfo,
+                                                 DataRegStateResult dataRegResponse) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, dataRegResponse);
+            }
+            mRil.processResponseDone(rr, responseInfo, dataRegResponse);
+        }
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param longName is long alpha ONS or EONS or empty string if unregistered
+     * @param shortName is short alpha ONS or EONS or empty string if unregistered
+     * @param numeric is 5 or 6 digit numeric code (MCC + MNC) or empty string if unregistered
+     */
+    public void getOperatorResponse(RadioResponseInfo responseInfo,
+                                    String longName,
+                                    String shortName,
+                                    String numeric) {
+        responseStrings(responseInfo, longName, shortName, numeric);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setRadioPowerResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void sendDtmfResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param sms Response to sms sent as defined by SendSmsResult in types.hal
+     */
+    public void sendSmsResponse(RadioResponseInfo responseInfo,
+                                SendSmsResult sms) {
+        responseSms(responseInfo, sms);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param sms Response to sms sent as defined by SendSmsResult in types.hal
+     */
+    public void sendSMSExpectMoreResponse(RadioResponseInfo responseInfo,
+                                          SendSmsResult sms) {
+        responseSms(responseInfo, sms);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param setupDataCallResult Response to data call setup as defined by setupDataCallResult in
+     *                            types.hal
+     */
+    public void setupDataCallResponse(RadioResponseInfo responseInfo,
+                                      SetupDataCallResult setupDataCallResult) {
+        responseSetupDataCall(responseInfo, setupDataCallResult);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param iccIo ICC io operation response as defined by IccIoResult in types.hal
+     */
+    public void iccIOForAppResponse(RadioResponseInfo responseInfo,
+                            android.hardware.radio.V1_0.IccIoResult iccIo) {
+        responseIccIo(responseInfo, iccIo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void sendUssdResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void cancelPendingUssdResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param n is "n" parameter from TS 27.007 7.7
+     * @param m is "m" parameter from TS 27.007 7.7
+     */
+    public void getClirResponse(RadioResponseInfo responseInfo, int n, int m) {
+        responseInts(responseInfo, n, m);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setClirResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param callForwardInfos points to a vector of CallForwardInfo, one for
+     *        each distinct registered phone number.
+     */
+    public void getCallForwardStatusResponse(RadioResponseInfo responseInfo,
+                                             ArrayList<android.hardware.radio.V1_0.CallForwardInfo>
+                                                     callForwardInfos) {
+        responseCallForwardInfo(responseInfo, callForwardInfos);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCallForwardResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param enable If current call waiting state is disabled, enable = false else true
+     * @param serviceClass If enable, then callWaitingResp[1]
+     *        must follow, with the TS 27.007 service class bit vector of services
+     *        for which call waiting is enabled.
+     *        For example, if callWaitingResp[0] is 1 and
+     *        callWaitingResp[1] is 3, then call waiting is enabled for data
+     *        and voice and disabled for everything else.
+     */
+    public void getCallWaitingResponse(RadioResponseInfo responseInfo,
+                                   boolean enable,
+                                   int serviceClass) {
+        responseInts(responseInfo, enable ? 1 : 0, serviceClass);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCallWaitingResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void acknowledgeLastIncomingGsmSmsResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void acceptCallResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void deactivateDataCallResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param response 0 is the TS 27.007 service class bit vector of
+     *        services for which the specified barring facility
+     *        is active. "0" means "disabled for all"
+     */
+    public void getFacilityLockForAppResponse(RadioResponseInfo responseInfo, int response) {
+        responseInts(responseInfo, response);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param retry 0 is the number of retries remaining, or -1 if unknown
+     */
+    public void setFacilityLockForAppResponse(RadioResponseInfo responseInfo, int retry) {
+        responseInts(responseInfo, retry);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setBarringPasswordResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param selection false for automatic selection, true for manual selection
+     */
+    public void getNetworkSelectionModeResponse(RadioResponseInfo responseInfo, boolean selection) {
+        responseInts(responseInfo, selection ? 1 : 0);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setNetworkSelectionModeAutomaticResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setNetworkSelectionModeManualResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param networkInfos List of network operator information as OperatorInfos defined in
+     *                     types.hal
+     */
+    public void getAvailableNetworksResponse(RadioResponseInfo responseInfo,
+                                             ArrayList<android.hardware.radio.V1_0.OperatorInfo>
+                                                     networkInfos) {
+        responseOperatorInfos(responseInfo, networkInfos);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void startNetworkScanResponse(RadioResponseInfo responseInfo) {
+        responseScanStatus(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void stopNetworkScanResponse(RadioResponseInfo responseInfo) {
+        responseScanStatus(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void startDtmfResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void stopDtmfResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param version string containing version string for log reporting
+     */
+    public void getBasebandVersionResponse(RadioResponseInfo responseInfo, String version) {
+        responseString(responseInfo, version);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void separateConnectionResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setMuteResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param enable true for "mute enabled" and false for "mute disabled"
+     */
+    public void getMuteResponse(RadioResponseInfo responseInfo, boolean enable) {
+        responseInts(responseInfo, enable ? 1 : 0);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param status indicates CLIP status
+     */
+    public void getClipResponse(RadioResponseInfo responseInfo, int status) {
+        responseInts(responseInfo, status);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param dataCallResultList Response to get data call list as defined by setupDataCallResult in
+     *                           types.hal
+     */
+    public void getDataCallListResponse(RadioResponseInfo responseInfo,
+                                        ArrayList<SetupDataCallResult> dataCallResultList) {
+        responseDataCallList(responseInfo, dataCallResultList);
+    }
+
+    public void sendOemRilRequestRawResponse(RadioResponseInfo responseInfo,
+                                             ArrayList<Byte> var2) {}
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setSuppServiceNotificationsResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param index record index where the message is stored
+     */
+    public void writeSmsToSimResponse(RadioResponseInfo responseInfo, int index) {
+        responseInts(responseInfo, index);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void deleteSmsOnSimResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setBandModeResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param bandModes List of RadioBandMode listing supported modes
+     */
+    public void getAvailableBandModesResponse(RadioResponseInfo responseInfo,
+                                              ArrayList<Integer> bandModes) {
+        responseIntArrayList(responseInfo, bandModes);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param commandResponse SAT/USAT response in hexadecimal format
+     *        string starting with first byte of response
+     */
+    public void sendEnvelopeResponse(RadioResponseInfo responseInfo, String commandResponse) {
+        responseString(responseInfo, commandResponse);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void sendTerminalResponseToSimResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void handleStkCallSetupRequestFromSimResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void explicitCallTransferResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setPreferredNetworkTypeResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param nwType RadioPreferredNetworkType defined in types.hal
+     */
+    public void getPreferredNetworkTypeResponse(RadioResponseInfo responseInfo, int nwType) {
+        mRil.mPreferredNetworkType = nwType;
+        responseInts(responseInfo, nwType);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param cells Vector of neighboring radio cell information
+     */
+    public void getNeighboringCidsResponse(RadioResponseInfo responseInfo,
+                                           ArrayList<NeighboringCell> cells) {
+        responseCellList(responseInfo, cells);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setLocationUpdatesResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCdmaSubscriptionSourceResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCdmaRoamingPreferenceResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param type CdmaRoamingType defined in types.hal
+     */
+    public void getCdmaRoamingPreferenceResponse(RadioResponseInfo responseInfo, int type) {
+        responseInts(responseInfo, type);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setTTYModeResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param mode TTY mode
+     */
+    public void getTTYModeResponse(RadioResponseInfo responseInfo, int mode) {
+        responseInts(responseInfo, mode);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setPreferredVoicePrivacyResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param enable false for Standard Privacy Mode (Public Long Code Mask)
+     *        true for Enhanced Privacy Mode (Private Long Code Mask)
+     */
+    public void getPreferredVoicePrivacyResponse(RadioResponseInfo responseInfo,
+                                                 boolean enable) {
+        responseInts(responseInfo, enable ? 1 : 0);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void sendCDMAFeatureCodeResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void sendBurstDtmfResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param sms Sms result struct as defined by SendSmsResult in types.hal
+     */
+    public void sendCdmaSmsResponse(RadioResponseInfo responseInfo, SendSmsResult sms) {
+        responseSms(responseInfo, sms);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void acknowledgeLastIncomingCdmaSmsResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param configs Vector of GSM/WCDMA Cell broadcast configs
+     */
+    public void getGsmBroadcastConfigResponse(RadioResponseInfo responseInfo,
+                                              ArrayList<GsmBroadcastSmsConfigInfo> configs) {
+        responseGmsBroadcastConfig(responseInfo, configs);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setGsmBroadcastConfigResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setGsmBroadcastActivationResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param configs Vector of CDMA Broadcast SMS configs.
+     */
+    public void getCdmaBroadcastConfigResponse(RadioResponseInfo responseInfo,
+                                               ArrayList<CdmaBroadcastSmsConfigInfo> configs) {
+        responseCdmaBroadcastConfig(responseInfo, configs);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCdmaBroadcastConfigResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCdmaBroadcastActivationResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param mdn MDN if CDMA subscription is available
+     * @param hSid is a comma separated list of H_SID (Home SID) if
+     *        CDMA subscription is available, in decimal format
+     * @param hNid is a comma separated list of H_NID (Home NID) if
+     *        CDMA subscription is available, in decimal format
+     * @param min MIN (10 digits, MIN2+MIN1) if CDMA subscription is available
+     * @param prl PRL version if CDMA subscription is available
+     */
+    public void getCDMASubscriptionResponse(RadioResponseInfo responseInfo, String mdn,
+                                            String hSid, String hNid, String min, String prl) {
+        responseStrings(responseInfo, mdn, hSid, hNid, min, prl);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param index record index where the cmda sms message is stored
+     */
+    public void writeSmsToRuimResponse(RadioResponseInfo responseInfo, int index) {
+        responseInts(responseInfo, index);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void deleteSmsOnRuimResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param imei IMEI if GSM subscription is available
+     * @param imeisv IMEISV if GSM subscription is available
+     * @param esn ESN if CDMA subscription is available
+     * @param meid MEID if CDMA subscription is available
+     */
+    public void getDeviceIdentityResponse(RadioResponseInfo responseInfo, String imei,
+                                          String imeisv, String esn, String meid) {
+        responseStrings(responseInfo, imei, imeisv, esn, meid);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void exitEmergencyCallbackModeResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param smsc Short Message Service Center address on the device
+     */
+    public void getSmscAddressResponse(RadioResponseInfo responseInfo, String smsc) {
+        responseString(responseInfo, smsc);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setSmscAddressResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void reportSmsMemoryStatusResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void reportStkServiceIsRunningResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param source CDMA subscription source
+     */
+    public void getCdmaSubscriptionSourceResponse(RadioResponseInfo responseInfo, int source) {
+        responseInts(responseInfo, source);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param response response string of the challenge/response algo for ISIM auth in base64 format
+     */
+    public void requestIsimAuthenticationResponse(RadioResponseInfo responseInfo, String response) {
+        responseString(responseInfo, response);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void acknowledgeIncomingGsmSmsWithPduResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param iccIo IccIoResult as defined in types.hal corresponding to ICC IO response
+     */
+    public void sendEnvelopeWithStatusResponse(RadioResponseInfo responseInfo,
+                                               android.hardware.radio.V1_0.IccIoResult iccIo) {
+        responseIccIo(responseInfo, iccIo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param rat Current voice RAT
+     */
+    public void getVoiceRadioTechnologyResponse(RadioResponseInfo responseInfo, int rat) {
+        responseInts(responseInfo, rat);
+    }
+
+    public void getCellInfoListResponse(RadioResponseInfo responseInfo,
+                                        ArrayList<android.hardware.radio.V1_0.CellInfo> cellInfo) {
+        responseCellInfoList(responseInfo, cellInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCellInfoListRateResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setInitialAttachApnResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param isRegistered false = not registered, true = registered
+     * @param ratFamily RadioTechnologyFamily as defined in types.hal. This value is valid only if
+     *        isRegistered is true.
+     */
+    public void getImsRegistrationStateResponse(RadioResponseInfo responseInfo,
+                                                boolean isRegistered, int ratFamily) {
+        responseInts(responseInfo, isRegistered ? 1 : 0, ratFamily);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param sms Response to sms sent as defined by SendSmsResult in types.hal
+     */
+    public void sendImsSmsResponse(RadioResponseInfo responseInfo, SendSmsResult sms) {
+        responseSms(responseInfo, sms);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param result IccIoResult as defined in types.hal
+     */
+    public void iccTransmitApduBasicChannelResponse(RadioResponseInfo responseInfo,
+                                                    android.hardware.radio.V1_0.IccIoResult
+                                                            result) {
+        responseIccIo(responseInfo, result);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param channelId session id of the logical channel.
+     * @param selectResponse Contains the select response for the open channel command with one
+     *        byte per integer
+     */
+    public void iccOpenLogicalChannelResponse(RadioResponseInfo responseInfo, int channelId,
+                                              ArrayList<Byte> selectResponse) {
+        ArrayList<Integer> arr = new ArrayList<>();
+        arr.add(channelId);
+        for (int i = 0; i < selectResponse.size(); i++) {
+            arr.add((int) selectResponse.get(i));
+        }
+        responseIntArrayList(responseInfo, arr);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void iccCloseLogicalChannelResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param result IccIoResult as defined in types.hal
+     */
+    public void iccTransmitApduLogicalChannelResponse(
+            RadioResponseInfo responseInfo,
+            android.hardware.radio.V1_0.IccIoResult result) {
+        responseIccIo(responseInfo, result);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param result string containing the contents of the NV item
+     */
+    public void nvReadItemResponse(RadioResponseInfo responseInfo, String result) {
+        responseString(responseInfo, result);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void nvWriteItemResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void nvWriteCdmaPrlResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void nvResetConfigResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setUiccSubscriptionResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setDataAllowedResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    public void getHardwareConfigResponse(
+            RadioResponseInfo responseInfo,
+            ArrayList<android.hardware.radio.V1_0.HardwareConfig> config) {
+        responseHardwareConfig(responseInfo, config);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param result IccIoResult as defined in types.hal
+     */
+    public void requestIccSimAuthenticationResponse(RadioResponseInfo responseInfo,
+                                                    android.hardware.radio.V1_0.IccIoResult
+                                                            result) {
+        responseICC_IOBase64(responseInfo, result);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setDataProfileResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void requestShutdownResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    public void getRadioCapabilityResponse(RadioResponseInfo responseInfo,
+                                           android.hardware.radio.V1_0.RadioCapability rc) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            RadioCapability ret = RIL.convertHalRadioCapability(rc, mRil);
+            if (responseInfo.error == RadioError.REQUEST_NOT_SUPPORTED
+                    || responseInfo.error == RadioError.GENERIC_FAILURE) {
+                // we should construct the RAF bitmask the radio
+                // supports based on preferred network bitmasks
+                ret = mRil.makeStaticRadioCapability();
+                responseInfo.error = RadioError.NONE;
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    public void setRadioCapabilityResponse(RadioResponseInfo responseInfo,
+                                           android.hardware.radio.V1_0.RadioCapability rc) {
+        responseRadioCapability(responseInfo, rc);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param statusInfo LceStatusInfo indicating LCE status
+     */
+    public void startLceServiceResponse(RadioResponseInfo responseInfo, LceStatusInfo statusInfo) {
+        responseLceStatus(responseInfo, statusInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param statusInfo LceStatusInfo indicating LCE status
+     */
+    public void stopLceServiceResponse(RadioResponseInfo responseInfo, LceStatusInfo statusInfo) {
+        responseLceStatus(responseInfo, statusInfo);
+    }
+
+    public void pullLceDataResponse(RadioResponseInfo responseInfo, LceDataInfo lceInfo) {
+        responseLceData(responseInfo, lceInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param activityInfo modem activity information
+     */
+    public void getModemActivityInfoResponse(RadioResponseInfo responseInfo,
+                                             ActivityStatsInfo activityInfo) {
+        responseActivityData(responseInfo, activityInfo);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param numAllowed number of allowed carriers which have been set correctly.
+     *        On success, it must match the length of list Carriers->allowedCarriers.
+     *        if Length of allowed carriers list is 0, numAllowed = 0.
+     */
+    public void setAllowedCarriersResponse(RadioResponseInfo responseInfo, int numAllowed) {
+        responseInts(responseInfo, numAllowed);
+    }
+
+    /**
+     *
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param allAllowed true only when all carriers are allowed. Ignore "carriers" struct.
+     *                   If false, consider "carriers" struct
+     * @param carriers Carrier restriction information.
+     */
+    public void getAllowedCarriersResponse(RadioResponseInfo responseInfo, boolean allAllowed,
+                                           CarrierRestrictions carriers) {
+        responseCarrierIdentifiers(responseInfo, allAllowed, carriers);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void sendDeviceStateResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setCarrierInfoForImsiEncryptionResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setIndicationFilterResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setSimCardPowerResponse(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void setSimCardPowerResponse_1_1(RadioResponseInfo responseInfo) {
+        responseVoid(responseInfo);
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     * @param keepaliveStatus status of the keepalive with a handle for the session
+     */
+    public void startKeepaliveResponse(RadioResponseInfo responseInfo,
+            KeepaliveStatus keepaliveStatus) {
+        throw new UnsupportedOperationException("startKeepaliveResponse not implemented");
+    }
+
+    /**
+     * @param responseInfo Response info struct containing response type, serial no. and error
+     */
+    public void stopKeepaliveResponse(RadioResponseInfo responseInfo) {
+        throw new UnsupportedOperationException("stopKeepaliveResponse not implemented");
+    }
+
+    private void responseIccCardStatus(RadioResponseInfo responseInfo, CardStatus cardStatus) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            IccCardStatus iccCardStatus = new IccCardStatus();
+            iccCardStatus.setCardState(cardStatus.cardState);
+            iccCardStatus.setUniversalPinState(cardStatus.universalPinState);
+            iccCardStatus.mGsmUmtsSubscriptionAppIndex = cardStatus.gsmUmtsSubscriptionAppIndex;
+            iccCardStatus.mCdmaSubscriptionAppIndex = cardStatus.cdmaSubscriptionAppIndex;
+            iccCardStatus.mImsSubscriptionAppIndex = cardStatus.imsSubscriptionAppIndex;
+            int numApplications = cardStatus.applications.size();
+
+            // limit to maximum allowed applications
+            if (numApplications
+                    > com.android.internal.telephony.uicc.IccCardStatus.CARD_MAX_APPS) {
+                numApplications =
+                        com.android.internal.telephony.uicc.IccCardStatus.CARD_MAX_APPS;
+            }
+            iccCardStatus.mApplications = new IccCardApplicationStatus[numApplications];
+            for (int i = 0; i < numApplications; i++) {
+                AppStatus rilAppStatus = cardStatus.applications.get(i);
+                IccCardApplicationStatus appStatus = new IccCardApplicationStatus();
+                appStatus.app_type       = appStatus.AppTypeFromRILInt(rilAppStatus.appType);
+                appStatus.app_state      = appStatus.AppStateFromRILInt(rilAppStatus.appState);
+                appStatus.perso_substate = appStatus.PersoSubstateFromRILInt(
+                        rilAppStatus.persoSubstate);
+                appStatus.aid            = rilAppStatus.aidPtr;
+                appStatus.app_label      = rilAppStatus.appLabelPtr;
+                appStatus.pin1_replaced  = rilAppStatus.pin1Replaced;
+                appStatus.pin1           = appStatus.PinStateFromRILInt(rilAppStatus.pin1);
+                appStatus.pin2           = appStatus.PinStateFromRILInt(rilAppStatus.pin2);
+                iccCardStatus.mApplications[i] = appStatus;
+            }
+            mRil.riljLog("responseIccCardStatus: from HIDL: " + iccCardStatus);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, iccCardStatus);
+            }
+            mRil.processResponseDone(rr, responseInfo, iccCardStatus);
+        }
+    }
+
+    private void responseInts(RadioResponseInfo responseInfo, int ...var) {
+        final ArrayList<Integer> ints = new ArrayList<>();
+        for (int i = 0; i < var.length; i++) {
+            ints.add(var[i]);
+        }
+        responseIntArrayList(responseInfo, ints);
+    }
+
+    private void responseIntArrayList(RadioResponseInfo responseInfo, ArrayList<Integer> var) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            int[] ret = new int[var.size()];
+            for (int i = 0; i < var.size(); i++) {
+                ret[i] = var.get(i);
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseCurrentCalls(RadioResponseInfo responseInfo,
+                                      ArrayList<android.hardware.radio.V1_0.Call> calls) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            int num = calls.size();
+            ArrayList<DriverCall> dcCalls = new ArrayList<DriverCall>(num);
+            DriverCall dc;
+
+            for (int i = 0; i < num; i++) {
+                dc = new DriverCall();
+                // TODO: change name of function stateFromCLCC() in DriverCall.java to name
+                // clarifying what is CLCC
+                dc.state = DriverCall.stateFromCLCC((int) (calls.get(i).state));
+                dc.index = calls.get(i).index;
+                dc.TOA = calls.get(i).toa;
+                dc.isMpty = calls.get(i).isMpty;
+                dc.isMT = calls.get(i).isMT;
+                dc.als = calls.get(i).als;
+                dc.isVoice = calls.get(i).isVoice;
+                dc.isVoicePrivacy = calls.get(i).isVoicePrivacy;
+                dc.number = calls.get(i).number;
+                dc.numberPresentation =
+                        DriverCall.presentationFromCLIP(
+                                (int) (calls.get(i).numberPresentation));
+                dc.name = calls.get(i).name;
+                dc.namePresentation =
+                        DriverCall.presentationFromCLIP((int) (calls.get(i).namePresentation));
+                if (calls.get(i).uusInfo.size() == 1) {
+                    dc.uusInfo = new UUSInfo();
+                    dc.uusInfo.setType(calls.get(i).uusInfo.get(0).uusType);
+                    dc.uusInfo.setDcs(calls.get(i).uusInfo.get(0).uusDcs);
+                    if (!TextUtils.isEmpty(calls.get(i).uusInfo.get(0).uusData)) {
+                        byte[] userData = calls.get(i).uusInfo.get(0).uusData.getBytes();
+                        dc.uusInfo.setUserData(userData);
+                    } else {
+                        mRil.riljLog("responseCurrentCalls: uusInfo data is null or empty");
+                    }
+
+                    mRil.riljLogv(String.format("Incoming UUS : type=%d, dcs=%d, length=%d",
+                            dc.uusInfo.getType(), dc.uusInfo.getDcs(),
+                            dc.uusInfo.getUserData().length));
+                    mRil.riljLogv("Incoming UUS : data (hex): "
+                            + IccUtils.bytesToHexString(dc.uusInfo.getUserData()));
+                } else {
+                    mRil.riljLogv("Incoming UUS : NOT present!");
+                }
+
+                // Make sure there's a leading + on addresses with a TOA of 145
+                dc.number = PhoneNumberUtils.stringFromStringAndTOA(dc.number, dc.TOA);
+
+                dcCalls.add(dc);
+
+                if (dc.isVoicePrivacy) {
+                    mRil.mVoicePrivacyOnRegistrants.notifyRegistrants();
+                    mRil.riljLog("InCall VoicePrivacy is enabled");
+                } else {
+                    mRil.mVoicePrivacyOffRegistrants.notifyRegistrants();
+                    mRil.riljLog("InCall VoicePrivacy is disabled");
+                }
+            }
+
+            Collections.sort(dcCalls);
+
+            if ((num == 0) && mRil.mTestingEmergencyCall.getAndSet(false)) {
+                if (mRil.mEmergencyCallbackModeRegistrant != null) {
+                    mRil.riljLog("responseCurrentCalls: call ended, testing emergency call,"
+                            + " notify ECM Registrants");
+                    mRil.mEmergencyCallbackModeRegistrant.notifyRegistrant();
+                }
+            }
+
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, dcCalls);
+            }
+            mRil.processResponseDone(rr, responseInfo, dcCalls);
+        }
+    }
+
+    private void responseVoid(RadioResponseInfo responseInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            Object ret = null;
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseString(RadioResponseInfo responseInfo, String str) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, str);
+            }
+            mRil.processResponseDone(rr, responseInfo, str);
+        }
+    }
+
+    private void responseStrings(RadioResponseInfo responseInfo, String ...str) {
+        ArrayList<String> strings = new ArrayList<>();
+        for (int i = 0; i < str.length; i++) {
+            strings.add(str[i]);
+        }
+        responseStringArrayList(mRil, responseInfo, strings);
+    }
+
+    static void responseStringArrayList(RIL ril, RadioResponseInfo responseInfo,
+                                        ArrayList<String> strings) {
+        RILRequest rr = ril.processResponse(responseInfo);
+
+        if (rr != null) {
+            String[] ret = new String[strings.size()];
+            for (int i = 0; i < strings.size(); i++) {
+                ret[i] = strings.get(i);
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            ril.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseLastCallFailCauseInfo(RadioResponseInfo responseInfo,
+                                               LastCallFailCauseInfo fcInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            LastCallFailCause ret = new LastCallFailCause();
+            ret.causeCode = fcInfo.causeCode;
+            ret.vendorCause = fcInfo.vendorCause;
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseSignalStrength(RadioResponseInfo responseInfo,
+                                        android.hardware.radio.V1_0.SignalStrength sigStrength) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            SignalStrength ret = RIL.convertHalSignalStrength(sigStrength);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseSms(RadioResponseInfo responseInfo, SendSmsResult sms) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            SmsResponse ret = new SmsResponse(sms.messageRef, sms.ackPDU, sms.errorCode);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseSetupDataCall(RadioResponseInfo responseInfo,
+                                       SetupDataCallResult setupDataCallResult) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            DataCallResponse ret = RIL.convertDataCallResult(setupDataCallResult);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseIccIo(RadioResponseInfo responseInfo,
+                               android.hardware.radio.V1_0.IccIoResult result) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            IccIoResult ret = new IccIoResult(result.sw1, result.sw2, result.simResponse);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseCallForwardInfo(RadioResponseInfo responseInfo,
+                                         ArrayList<android.hardware.radio.V1_0.CallForwardInfo>
+                                                 callForwardInfos) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+        if (rr != null) {
+            CallForwardInfo[] ret = new CallForwardInfo[callForwardInfos.size()];
+            for (int i = 0; i < callForwardInfos.size(); i++) {
+                ret[i] = new CallForwardInfo();
+                ret[i].status = callForwardInfos.get(i).status;
+                ret[i].reason = callForwardInfos.get(i).reason;
+                ret[i].serviceClass = callForwardInfos.get(i).serviceClass;
+                ret[i].toa = callForwardInfos.get(i).toa;
+                ret[i].number = callForwardInfos.get(i).number;
+                ret[i].timeSeconds = callForwardInfos.get(i).timeSeconds;
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private static String convertOpertatorInfoToString(int status) {
+        if (status == android.hardware.radio.V1_0.OperatorStatus.UNKNOWN) {
+            return "unknown";
+        } else if (status == android.hardware.radio.V1_0.OperatorStatus.AVAILABLE) {
+            return "available";
+        } else if (status == android.hardware.radio.V1_0.OperatorStatus.CURRENT) {
+            return "current";
+        } else if (status == android.hardware.radio.V1_0.OperatorStatus.FORBIDDEN) {
+            return "forbidden";
+        } else {
+            return "";
+        }
+    }
+
+    private void responseOperatorInfos(RadioResponseInfo responseInfo,
+                                       ArrayList<android.hardware.radio.V1_0.OperatorInfo>
+                                               networkInfos) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<OperatorInfo> ret = new ArrayList<OperatorInfo>();
+            for (int i = 0; i < networkInfos.size(); i++) {
+                ret.add(new OperatorInfo(networkInfos.get(i).alphaLong,
+                        networkInfos.get(i).alphaShort, networkInfos.get(i).operatorNumeric,
+                        convertOpertatorInfoToString(networkInfos.get(i).status)));
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseScanStatus(RadioResponseInfo responseInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            NetworkScanResult nsr = null;
+            if (responseInfo.error == RadioError.NONE) {
+                nsr = new NetworkScanResult(
+                        NetworkScanResult.SCAN_STATUS_PARTIAL, RadioError.NONE, null);
+                sendMessageResponse(rr.mResult, nsr);
+            }
+            mRil.processResponseDone(rr, responseInfo, nsr);
+        }
+    }
+
+    private void responseDataCallList(RadioResponseInfo responseInfo,
+                                      ArrayList<SetupDataCallResult> dataCallResultList) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<DataCallResponse> dcResponseList = new ArrayList<>();
+            for (SetupDataCallResult dcResult : dataCallResultList) {
+                dcResponseList.add(RIL.convertDataCallResult(dcResult));
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, dcResponseList);
+            }
+            mRil.processResponseDone(rr, responseInfo, dcResponseList);
+        }
+    }
+
+    private void responseCellList(RadioResponseInfo responseInfo,
+                                  ArrayList<NeighboringCell> cells) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            int rssi;
+            String location;
+            ArrayList<NeighboringCellInfo> ret = new ArrayList<NeighboringCellInfo>();
+            NeighboringCellInfo cell;
+
+            int[] subId = SubscriptionManager.getSubId(mRil.mPhoneId);
+            int radioType =
+                    ((TelephonyManager) mRil.mContext.getSystemService(
+                            Context.TELEPHONY_SERVICE)).getDataNetworkType(subId[0]);
+
+            if (radioType != TelephonyManager.NETWORK_TYPE_UNKNOWN) {
+                for (int i = 0; i < cells.size(); i++) {
+                    rssi = cells.get(i).rssi;
+                    location = cells.get(i).cid;
+                    cell = new NeighboringCellInfo(rssi, location, radioType);
+                    ret.add(cell);
+                }
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseGmsBroadcastConfig(RadioResponseInfo responseInfo,
+                                            ArrayList<GsmBroadcastSmsConfigInfo> configs) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<SmsBroadcastConfigInfo> ret = new ArrayList<SmsBroadcastConfigInfo>();
+            for (int i = 0; i < configs.size(); i++) {
+                ret.add(new SmsBroadcastConfigInfo(configs.get(i).fromServiceId,
+                        configs.get(i).toServiceId, configs.get(i).fromCodeScheme,
+                        configs.get(i).toCodeScheme, configs.get(i).selected));
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseCdmaBroadcastConfig(RadioResponseInfo responseInfo,
+                                            ArrayList<CdmaBroadcastSmsConfigInfo> configs) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            int[] ret = null;
+
+            int numServiceCategories = configs.size();
+
+            if (numServiceCategories == 0) {
+                // TODO: The logic of providing default values should
+                // not be done by this transport layer. And needs to
+                // be done by the vendor ril or application logic.
+                int numInts;
+                numInts = CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES
+                        * CDMA_BSI_NO_OF_INTS_STRUCT + 1;
+                ret = new int[numInts];
+
+                // Faking a default record for all possible records.
+                ret[0] = CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES;
+
+                // Loop over CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES set 'english' as
+                // default language and selection status to false for all.
+                for (int i = 1; i < numInts; i += CDMA_BSI_NO_OF_INTS_STRUCT) {
+                    ret[i + 0] = i / CDMA_BSI_NO_OF_INTS_STRUCT;
+                    ret[i + 1] = 1;
+                    ret[i + 2] = 0;
+                }
+            } else {
+                int numInts;
+                numInts = (numServiceCategories * CDMA_BSI_NO_OF_INTS_STRUCT) + 1;
+                ret = new int[numInts];
+
+                ret[0] = numServiceCategories;
+                for (int i = 1, j = 0; j < configs.size();
+                        j++, i = i + CDMA_BSI_NO_OF_INTS_STRUCT) {
+                    ret[i] = configs.get(j).serviceCategory;
+                    ret[i + 1] = configs.get(j).language;
+                    ret[i + 2] = configs.get(j).selected ? 1 : 0;
+                }
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseCellInfoList(RadioResponseInfo responseInfo,
+                                      ArrayList<android.hardware.radio.V1_0.CellInfo> cellInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<CellInfo> ret = RIL.convertHalCellInfoList(cellInfo);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseActivityData(RadioResponseInfo responseInfo,
+                                      ActivityStatsInfo activityInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ModemActivityInfo ret = null;
+            if (responseInfo.error == RadioError.NONE) {
+                final int sleepModeTimeMs = activityInfo.sleepModeTimeMs;
+                final int idleModeTimeMs = activityInfo.idleModeTimeMs;
+                int [] txModeTimeMs = new int[ModemActivityInfo.TX_POWER_LEVELS];
+                for (int i = 0; i < ModemActivityInfo.TX_POWER_LEVELS; i++) {
+                    txModeTimeMs[i] = activityInfo.txmModetimeMs[i];
+                }
+                final int rxModeTimeMs = activityInfo.rxModeTimeMs;
+                ret = new ModemActivityInfo(SystemClock.elapsedRealtime(), sleepModeTimeMs,
+                        idleModeTimeMs, txModeTimeMs, rxModeTimeMs, 0);
+            } else {
+                ret = new ModemActivityInfo(0, 0, 0, new int [ModemActivityInfo.TX_POWER_LEVELS],
+                        0, 0);
+                responseInfo.error = RadioError.NONE;
+            }
+            sendMessageResponse(rr.mResult, ret);
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseHardwareConfig(
+            RadioResponseInfo responseInfo,
+            ArrayList<android.hardware.radio.V1_0.HardwareConfig> config) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<HardwareConfig> ret = RIL.convertHalHwConfigList(config, mRil);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseICC_IOBase64(RadioResponseInfo responseInfo,
+                                      android.hardware.radio.V1_0.IccIoResult result) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            IccIoResult ret = new IccIoResult(
+                    result.sw1,
+                    result.sw2,
+                    (!(result.simResponse).equals(""))
+                            ? android.util.Base64.decode(result.simResponse,
+                            android.util.Base64.DEFAULT) : (byte[]) null);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseRadioCapability(RadioResponseInfo responseInfo,
+                                         android.hardware.radio.V1_0.RadioCapability rc) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            RadioCapability ret = RIL.convertHalRadioCapability(rc, mRil);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseLceStatus(RadioResponseInfo responseInfo, LceStatusInfo statusInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<Integer> ret = new ArrayList<Integer>();
+            ret.add(statusInfo.lceStatus);
+            ret.add(Byte.toUnsignedInt(statusInfo.actualIntervalMs));
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseLceData(RadioResponseInfo responseInfo, LceDataInfo lceInfo) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            ArrayList<Integer> ret = RIL.convertHalLceData(lceInfo, mRil);
+            if (responseInfo.error == RadioError.NONE) {
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+
+    private void responseCarrierIdentifiers(RadioResponseInfo responseInfo, boolean allAllowed,
+                                            CarrierRestrictions carriers) {
+        RILRequest rr = mRil.processResponse(responseInfo);
+
+        if (rr != null) {
+            List<CarrierIdentifier> ret = new ArrayList<CarrierIdentifier>();
+            for (int i = 0; i < carriers.allowedCarriers.size(); i++) {
+                String mcc = carriers.allowedCarriers.get(i).mcc;
+                String mnc = carriers.allowedCarriers.get(i).mnc;
+                String spn = null, imsi = null, gid1 = null, gid2 = null;
+                int matchType = carriers.allowedCarriers.get(i).matchType;
+                String matchData = carriers.allowedCarriers.get(i).matchData;
+                if (matchType == CarrierIdentifier.MatchType.SPN) {
+                    spn = matchData;
+                } else if (matchType == CarrierIdentifier.MatchType.IMSI_PREFIX) {
+                    imsi = matchData;
+                } else if (matchType == CarrierIdentifier.MatchType.GID1) {
+                    gid1 = matchData;
+                } else if (matchType == CarrierIdentifier.MatchType.GID2) {
+                    gid2 = matchData;
+                }
+                ret.add(new CarrierIdentifier(mcc, mnc, spn, imsi, gid1, gid2));
+            }
+            if (responseInfo.error == RadioError.NONE) {
+                /* TODO: Handle excluded carriers */
+                sendMessageResponse(rr.mResult, ret);
+            }
+            mRil.processResponseDone(rr, responseInfo, ret);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/RatRatcheter.java b/com/android/internal/telephony/RatRatcheter.java
new file mode 100644
index 0000000..d582af0
--- /dev/null
+++ b/com/android/internal/telephony/RatRatcheter.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ServiceState;
+import android.telephony.Rlog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import java.util.ArrayList;
+
+/**
+ * This class loads configuration from CarrierConfig and uses it to determine
+ * what RATs are within a ratcheting family.  For example all the HSPA/HSDPA/HSUPA RATs.
+ * Then, until reset the class will only ratchet upwards within the family (order
+ * determined by the CarrierConfig data).  The ServiceStateTracker will reset this
+ * on cell-change.
+ */
+public class RatRatcheter {
+    private final static String LOG_TAG = "RilRatcheter";
+
+    /**
+     * This is a map of RAT types -> RAT families for rapid lookup.
+     * The RAT families are defined by RAT type -> RAT Rank SparseIntArrays, so
+     * we can compare the priorities of two RAT types by comparing the values
+     * stored in the SparseIntArrays, higher values are higher priority.
+     */
+    private final SparseArray<SparseIntArray> mRatFamilyMap = new SparseArray<>();
+
+    private final Phone mPhone;
+
+    /** Constructor */
+    public RatRatcheter(Phone phone) {
+        mPhone = phone;
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        phone.getContext().registerReceiverAsUser(mConfigChangedReceiver, UserHandle.ALL,
+                intentFilter, null, null);
+        resetRatFamilyMap();
+    }
+
+    public int ratchetRat(int oldRat, int newRat) {
+        synchronized (mRatFamilyMap) {
+            final SparseIntArray oldFamily = mRatFamilyMap.get(oldRat);
+            if (oldFamily == null) return newRat;
+
+            final SparseIntArray newFamily = mRatFamilyMap.get(newRat);
+            if (newFamily != oldFamily) return newRat;
+
+            // now go with the higher of the two
+            final int oldRatRank = newFamily.get(oldRat, -1);
+            final int newRatRank = newFamily.get(newRat, -1);
+            return (oldRatRank > newRatRank ? oldRat : newRat);
+        }
+    }
+
+    public void ratchetRat(ServiceState oldSS, ServiceState newSS) {
+        int newVoiceRat = ratchetRat(oldSS.getRilVoiceRadioTechnology(),
+                newSS.getRilVoiceRadioTechnology());
+        int newDataRat = ratchetRat(oldSS.getRilDataRadioTechnology(),
+                newSS.getRilDataRadioTechnology());
+        boolean newUsingCA = oldSS.isUsingCarrierAggregation() ||
+                newSS.isUsingCarrierAggregation();
+
+        newSS.setRilVoiceRadioTechnology(newVoiceRat);
+        newSS.setRilDataRadioTechnology(newDataRat);
+        newSS.setIsUsingCarrierAggregation(newUsingCA);
+    }
+
+    private BroadcastReceiver mConfigChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)) {
+                resetRatFamilyMap();
+            }
+        }
+    };
+
+    private void resetRatFamilyMap() {
+        synchronized(mRatFamilyMap) {
+            mRatFamilyMap.clear();
+
+            final CarrierConfigManager configManager = (CarrierConfigManager)
+                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configManager == null) return;
+            PersistableBundle b = configManager.getConfig();
+            if (b == null) return;
+
+            // Reads an array of strings, eg:
+            // ["GPRS, EDGE", "EVDO, EVDO_A, EVDO_B", "HSPA, HSDPA, HSUPA, HSPAP"]
+            // Each string defines a family and the order of rats within the string express
+            // the priority of the RAT within the family (ie, we'd move up to later-listed RATs, but
+            // not down).
+            String[] ratFamilies = b.getStringArray(CarrierConfigManager.KEY_RATCHET_RAT_FAMILIES);
+            if (ratFamilies == null) return;
+            for (String ratFamily : ratFamilies) {
+                String[] rats = ratFamily.split(",");
+                if (rats.length < 2) continue;
+                SparseIntArray currentFamily = new SparseIntArray(rats.length);
+                int pos = 0;
+                for (String ratString : rats) {
+                    int ratInt;
+                    try {
+                        ratInt = Integer.parseInt(ratString.trim());
+                    } catch (NumberFormatException e) {
+                        Rlog.e(LOG_TAG, "NumberFormatException on " + ratString);
+                        break;
+                    }
+                    if (mRatFamilyMap.get(ratInt) != null) {
+                        Rlog.e(LOG_TAG, "RAT listed twice: " + ratString);
+                        break;
+                    }
+                    currentFamily.put(ratInt, pos++);
+                    mRatFamilyMap.put(ratInt, currentFamily);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/RestrictedState.java b/com/android/internal/telephony/RestrictedState.java
new file mode 100644
index 0000000..f09af8a
--- /dev/null
+++ b/com/android/internal/telephony/RestrictedState.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+public class RestrictedState {
+
+    /**
+     * Set true to block packet data access due to restriction
+     */
+    private boolean mPsRestricted;
+    /**
+     * Set true to block all normal voice/SMS/USSD/SS/AV64 due to restriction
+     */
+    private boolean mCsNormalRestricted;
+    /**
+     * Set true to block emergency call due to restriction
+     */
+    private boolean mCsEmergencyRestricted;
+
+    public RestrictedState() {
+        setPsRestricted(false);
+        setCsNormalRestricted(false);
+        setCsEmergencyRestricted(false);
+    }
+
+    /**
+     * @param csEmergencyRestricted the csEmergencyRestricted to set
+     */
+    public void setCsEmergencyRestricted(boolean csEmergencyRestricted) {
+        mCsEmergencyRestricted = csEmergencyRestricted;
+    }
+
+    /**
+     * @return the csEmergencyRestricted
+     */
+    public boolean isCsEmergencyRestricted() {
+        return mCsEmergencyRestricted;
+    }
+
+    /**
+     * @param csNormalRestricted the csNormalRestricted to set
+     */
+    public void setCsNormalRestricted(boolean csNormalRestricted) {
+        mCsNormalRestricted = csNormalRestricted;
+    }
+
+    /**
+     * @return the csNormalRestricted
+     */
+    public boolean isCsNormalRestricted() {
+        return mCsNormalRestricted;
+    }
+
+    /**
+     * @param psRestricted the psRestricted to set
+     */
+    public void setPsRestricted(boolean psRestricted) {
+        mPsRestricted = psRestricted;
+    }
+
+    /**
+     * @return the psRestricted
+     */
+    public boolean isPsRestricted() {
+        return mPsRestricted;
+    }
+
+    public boolean isCsRestricted() {
+        return mCsNormalRestricted && mCsEmergencyRestricted;
+    }
+
+    public boolean isAnyCsRestricted() {
+        return mCsNormalRestricted || mCsEmergencyRestricted;
+    }
+
+    @Override
+    public boolean equals (Object o) {
+        RestrictedState s;
+
+        try {
+            s = (RestrictedState) o;
+        } catch (ClassCastException ex) {
+            return false;
+        }
+
+        if (o == null) {
+            return false;
+        }
+
+        return mPsRestricted == s.mPsRestricted
+        && mCsNormalRestricted == s.mCsNormalRestricted
+        && mCsEmergencyRestricted == s.mCsEmergencyRestricted;
+    }
+
+    @Override
+    public String toString() {
+        String csString = "none";
+
+        if (mCsEmergencyRestricted && mCsNormalRestricted) {
+            csString = "all";
+        } else if (mCsEmergencyRestricted && !mCsNormalRestricted) {
+            csString = "emergency";
+        } else if (!mCsEmergencyRestricted && mCsNormalRestricted) {
+            csString = "normal call";
+        }
+
+        return  "Restricted State CS: " + csString + " PS:" + mPsRestricted;
+    }
+
+}
diff --git a/com/android/internal/telephony/RetryManager.java b/com/android/internal/telephony/RetryManager.java
new file mode 100644
index 0000000..23c3498
--- /dev/null
+++ b/com/android/internal/telephony/RetryManager.java
@@ -0,0 +1,680 @@
+/**
+ * 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.internal.telephony;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.PersistableBundle;
+import android.os.SystemProperties;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.internal.telephony.dataconnection.ApnSetting;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+/**
+ * Retry manager allows a simple way to declare a series of
+ * retry timeouts. After creating a RetryManager the configure
+ * method is used to define the sequence. A simple linear series
+ * may be initialized using configure with three integer parameters
+ * The other configure method allows a series to be declared using
+ * a string.
+ *<p>
+ * The format of the configuration string is the apn type followed by a series of parameters
+ * separated by a comma. There are two name value pair parameters plus a series
+ * of delay times. The units of of these delay times is unspecified.
+ * The name value pairs which may be specified are:
+ *<ul>
+ *<li>max_retries=<value>
+ *<li>default_randomizationTime=<value>
+ *</ul>
+ *<p>
+ * apn type specifies the APN type that the retry pattern will apply for. "others" is for all other
+ * APN types not specified in the config.
+ *
+ * max_retries is the number of times that incrementRetryCount
+ * maybe called before isRetryNeeded will return false. if value
+ * is infinite then isRetryNeeded will always return true.
+ *
+ * default_randomizationTime will be used as the randomizationTime
+ * for delay times which have no supplied randomizationTime. If
+ * default_randomizationTime is not defined it defaults to 0.
+ *<p>
+ * The other parameters define The series of delay times and each
+ * may have an optional randomization value separated from the
+ * delay time by a colon.
+ *<p>
+ * Examples:
+ * <ul>
+ * <li>3 retries for mms with no randomization value which means its 0:
+ * <ul><li><code>"mms:1000, 2000, 3000"</code></ul>
+ *
+ * <li>10 retries for default APN with a 500 default randomization value for each and
+ * the 4..10 retries all using 3000 as the delay:
+ * <ul><li><code>"default:max_retries=10, default_randomization=500, 1000, 2000, 3000"</code></ul>
+ *
+ * <li>4 retries for supl APN with a 100 as the default randomization value for the first 2 values
+ * and the other two having specified values of 500:
+ * <ul><li><code>"supl:default_randomization=100, 1000, 2000, 4000:500, 5000:500"</code></ul>
+ *
+ * <li>Infinite number of retries for all other APNs with the first one at 1000, the second at 2000
+ * all others will be at 3000.
+ * <ul><li><code>"others:max_retries=infinite,1000,2000,3000</code></ul>
+ * </ul>
+ *
+ * {@hide}
+ */
+public class RetryManager {
+    public static final String LOG_TAG = "RetryManager";
+    public static final boolean DBG = true;
+    public static final boolean VDBG = false; // STOPSHIP if true
+
+    /**
+     * The default retry configuration for APNs. See above for the syntax.
+     */
+    private static final String DEFAULT_DATA_RETRY_CONFIG = "max_retries=3, 5000, 5000, 5000";
+
+    /**
+     * The APN type used for all other APNs retry configuration.
+     */
+    private static final String OTHERS_APN_TYPE = "others";
+
+    /**
+     * The default value (in milliseconds) for delay between APN trying (mInterApnDelay)
+     * within the same round
+     */
+    private static final long DEFAULT_INTER_APN_DELAY = 20000;
+
+    /**
+     * The default value (in milliseconds) for delay between APN trying (mFailFastInterApnDelay)
+     * within the same round when we are in fail fast mode
+     */
+    private static final long DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING = 3000;
+
+    /**
+     * The default value (in milliseconds) for retrying APN after disconnect
+     */
+    private static final long DEFAULT_APN_RETRY_AFTER_DISCONNECT_DELAY = 10000;
+
+    /**
+     * The value indicating no retry is needed
+     */
+    public static final long NO_RETRY = -1;
+
+    /**
+     * The value indicating modem did not suggest any retry delay
+     */
+    public static final long NO_SUGGESTED_RETRY_DELAY = -2;
+
+    /**
+     * If the modem suggests a retry delay in the data call setup response, we will retry
+     * the current APN setting again. However, if the modem keeps suggesting retrying the same
+     * APN setting, we'll fall into an infinite loop. Therefore adding a counter to retry up to
+     * MAX_SAME_APN_RETRY times can avoid it.
+     */
+    private static final int MAX_SAME_APN_RETRY = 3;
+
+    /**
+     * The delay (in milliseconds) between APN trying within the same round
+     */
+    private long mInterApnDelay;
+
+    /**
+     * The delay (in milliseconds) between APN trying within the same round when we are in
+     * fail fast mode
+     */
+    private long mFailFastInterApnDelay;
+
+    /**
+     * The delay (in milliseconds) for APN retrying after disconnect (e.g. Modem suddenly reports
+     * data call lost)
+     */
+    private long mApnRetryAfterDisconnectDelay;
+
+    /**
+     * Modem suggested delay for retrying the current APN
+     */
+    private long mModemSuggestedDelay = NO_SUGGESTED_RETRY_DELAY;
+
+    /**
+     * The counter for same APN retrying. See MAX_SAME_APN_RETRY for the details.
+     */
+    private int mSameApnRetryCount = 0;
+
+    /**
+     * Retry record with times in milli-seconds
+     */
+    private static class RetryRec {
+        RetryRec(int delayTime, int randomizationTime) {
+            mDelayTime = delayTime;
+            mRandomizationTime = randomizationTime;
+        }
+
+        int mDelayTime;
+        int mRandomizationTime;
+    }
+
+    /**
+     * The array of retry records
+     */
+    private ArrayList<RetryRec> mRetryArray = new ArrayList<RetryRec>();
+
+    private Phone mPhone;
+
+    /**
+     * Flag indicating whether retrying forever regardless the maximum retry count mMaxRetryCount
+     */
+    private boolean mRetryForever = false;
+
+    /**
+     * The maximum number of retries to attempt
+     */
+    private int mMaxRetryCount;
+
+    /**
+     * The current number of retries
+     */
+    private int mRetryCount = 0;
+
+    /**
+     * Random number generator. The random delay will be added into retry timer to avoid all devices
+     * around retrying the APN at the same time.
+     */
+    private Random mRng = new Random();
+
+    /**
+     * Retry manager configuration string. See top of the detailed explanation.
+     */
+    private String mConfig;
+
+    /**
+     * The list to store APN setting candidates for data call setup. Most of the carriers only have
+     * one APN, but few carriers have more than one.
+     */
+    private ArrayList<ApnSetting> mWaitingApns = null;
+
+    /**
+     * Index pointing to the current trying APN from mWaitingApns
+     */
+    private int mCurrentApnIndex = -1;
+
+    /**
+     * Apn context type. Could be "default, "mms", "supl", etc...
+     */
+    private String mApnType;
+
+    /**
+     * Retry manager constructor
+     * @param phone Phone object
+     * @param apnType APN type
+     */
+    public RetryManager(Phone phone, String apnType) {
+        mPhone = phone;
+        mApnType = apnType;
+    }
+
+    /**
+     * Configure for using string which allow arbitrary
+     * sequences of times. See class comments for the
+     * string format.
+     *
+     * @return true if successful
+     */
+    private boolean configure(String configStr) {
+        // Strip quotes if present.
+        if ((configStr.startsWith("\"") && configStr.endsWith("\""))) {
+            configStr = configStr.substring(1, configStr.length() - 1);
+        }
+
+        // Reset the retry manager since delay, max retry count, etc...will be reset.
+        reset();
+
+        if (DBG) log("configure: '" + configStr + "'");
+        mConfig = configStr;
+
+        if (!TextUtils.isEmpty(configStr)) {
+            int defaultRandomization = 0;
+
+            if (VDBG) log("configure: not empty");
+
+            String strArray[] = configStr.split(",");
+            for (int i = 0; i < strArray.length; i++) {
+                if (VDBG) log("configure: strArray[" + i + "]='" + strArray[i] + "'");
+                Pair<Boolean, Integer> value;
+                String splitStr[] = strArray[i].split("=", 2);
+                splitStr[0] = splitStr[0].trim();
+                if (VDBG) log("configure: splitStr[0]='" + splitStr[0] + "'");
+                if (splitStr.length > 1) {
+                    splitStr[1] = splitStr[1].trim();
+                    if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'");
+                    if (TextUtils.equals(splitStr[0], "default_randomization")) {
+                        value = parseNonNegativeInt(splitStr[0], splitStr[1]);
+                        if (!value.first) return false;
+                        defaultRandomization = value.second;
+                    } else if (TextUtils.equals(splitStr[0], "max_retries")) {
+                        if (TextUtils.equals("infinite", splitStr[1])) {
+                            mRetryForever = true;
+                        } else {
+                            value = parseNonNegativeInt(splitStr[0], splitStr[1]);
+                            if (!value.first) return false;
+                            mMaxRetryCount = value.second;
+                        }
+                    } else {
+                        Rlog.e(LOG_TAG, "Unrecognized configuration name value pair: "
+                                        + strArray[i]);
+                        return false;
+                    }
+                } else {
+                    /**
+                     * Assume a retry time with an optional randomization value
+                     * following a ":"
+                     */
+                    splitStr = strArray[i].split(":", 2);
+                    splitStr[0] = splitStr[0].trim();
+                    RetryRec rr = new RetryRec(0, 0);
+                    value = parseNonNegativeInt("delayTime", splitStr[0]);
+                    if (!value.first) return false;
+                    rr.mDelayTime = value.second;
+
+                    // Check if optional randomization value present
+                    if (splitStr.length > 1) {
+                        splitStr[1] = splitStr[1].trim();
+                        if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'");
+                        value = parseNonNegativeInt("randomizationTime", splitStr[1]);
+                        if (!value.first) return false;
+                        rr.mRandomizationTime = value.second;
+                    } else {
+                        rr.mRandomizationTime = defaultRandomization;
+                    }
+                    mRetryArray.add(rr);
+                }
+            }
+            if (mRetryArray.size() > mMaxRetryCount) {
+                mMaxRetryCount = mRetryArray.size();
+                if (VDBG) log("configure: setting mMaxRetryCount=" + mMaxRetryCount);
+            }
+        } else {
+            log("configure: cleared");
+        }
+
+        if (VDBG) log("configure: true");
+        return true;
+    }
+
+    /**
+     * Configure the retry manager
+     */
+    private void configureRetry() {
+        String configString = null;
+        String otherConfigString = null;
+
+        try {
+            if (Build.IS_DEBUGGABLE) {
+                // Using system properties is easier for testing from command line.
+                String config = SystemProperties.get("test.data_retry_config");
+                if (!TextUtils.isEmpty(config)) {
+                    configure(config);
+                    return;
+                }
+            }
+
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            PersistableBundle b = configManager.getConfigForSubId(mPhone.getSubId());
+
+            mInterApnDelay = b.getLong(
+                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG,
+                    DEFAULT_INTER_APN_DELAY);
+            mFailFastInterApnDelay = b.getLong(
+                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG,
+                    DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING);
+            mApnRetryAfterDisconnectDelay = b.getLong(
+                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_RETRY_AFTER_DISCONNECT_LONG,
+                    DEFAULT_APN_RETRY_AFTER_DISCONNECT_DELAY);
+
+            // Load all retry patterns for all different APNs.
+            String[] allConfigStrings = b.getStringArray(
+                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS);
+            if (allConfigStrings != null) {
+                for (String s : allConfigStrings) {
+                    if (!TextUtils.isEmpty(s)) {
+                        String splitStr[] = s.split(":", 2);
+                        if (splitStr.length == 2) {
+                            String apnType = splitStr[0].trim();
+                            // Check if this retry pattern is for the APN we want.
+                            if (apnType.equals(mApnType)) {
+                                // Extract the config string. Note that an empty string is valid
+                                // here, meaning no retry for the specified APN.
+                                configString = splitStr[1];
+                                break;
+                            } else if (apnType.equals(OTHERS_APN_TYPE)) {
+                                // Extract the config string. Note that an empty string is valid
+                                // here, meaning no retry for all other APNs.
+                                otherConfigString = splitStr[1];
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (configString == null) {
+                if (otherConfigString != null) {
+                    configString = otherConfigString;
+                } else {
+                    // We should never reach here. If we reach here, it must be a configuration
+                    // error bug.
+                    log("Invalid APN retry configuration!. Use the default one now.");
+                    configString = DEFAULT_DATA_RETRY_CONFIG;
+                }
+            }
+        } catch (NullPointerException ex) {
+            // We should never reach here unless there is a bug
+            log("Failed to read configuration! Use the hardcoded default value.");
+
+            mInterApnDelay = DEFAULT_INTER_APN_DELAY;
+            mFailFastInterApnDelay = DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING;
+            configString = DEFAULT_DATA_RETRY_CONFIG;
+        }
+
+        if (VDBG) {
+            log("mInterApnDelay = " + mInterApnDelay + ", mFailFastInterApnDelay = " +
+                    mFailFastInterApnDelay);
+        }
+
+        configure(configString);
+    }
+
+    /**
+     * Return the timer that should be used to trigger the data reconnection
+     */
+    private int getRetryTimer() {
+        int index;
+        if (mRetryCount < mRetryArray.size()) {
+            index = mRetryCount;
+        } else {
+            index = mRetryArray.size() - 1;
+        }
+
+        int retVal;
+        if ((index >= 0) && (index < mRetryArray.size())) {
+            retVal = mRetryArray.get(index).mDelayTime + nextRandomizationTime(index);
+        } else {
+            retVal = 0;
+        }
+
+        if (DBG) log("getRetryTimer: " + retVal);
+        return retVal;
+    }
+
+    /**
+     * Parse an integer validating the value is not negative.
+     * @param name Name
+     * @param stringValue Value
+     * @return Pair.first == true if stringValue an integer >= 0
+     */
+    private Pair<Boolean, Integer> parseNonNegativeInt(String name, String stringValue) {
+        int value;
+        Pair<Boolean, Integer> retVal;
+        try {
+            value = Integer.parseInt(stringValue);
+            retVal = new Pair<Boolean, Integer>(validateNonNegativeInt(name, value), value);
+        } catch (NumberFormatException e) {
+            Rlog.e(LOG_TAG, name + " bad value: " + stringValue, e);
+            retVal = new Pair<Boolean, Integer>(false, 0);
+        }
+        if (VDBG) {
+            log("parseNonNetativeInt: " + name + ", " + stringValue + ", "
+                    + retVal.first + ", " + retVal.second);
+        }
+        return retVal;
+    }
+
+    /**
+     * Validate an integer is >= 0 and logs an error if not
+     * @param name Name
+     * @param value Value
+     * @return Pair.first
+     */
+    private boolean validateNonNegativeInt(String name, int value) {
+        boolean retVal;
+        if (value < 0) {
+            Rlog.e(LOG_TAG, name + " bad value: is < 0");
+            retVal = false;
+        } else {
+            retVal = true;
+        }
+        if (VDBG) log("validateNonNegative: " + name + ", " + value + ", " + retVal);
+        return retVal;
+    }
+
+    /**
+     * Return next random number for the index
+     * @param index Retry index
+     */
+    private int nextRandomizationTime(int index) {
+        int randomTime = mRetryArray.get(index).mRandomizationTime;
+        if (randomTime == 0) {
+            return 0;
+        } else {
+            return mRng.nextInt(randomTime);
+        }
+    }
+
+    /**
+     * Get the next APN setting for data call setup.
+     * @return APN setting to try
+     */
+    public ApnSetting getNextApnSetting() {
+
+        if (mWaitingApns == null || mWaitingApns.size() == 0) {
+            log("Waiting APN list is null or empty.");
+            return null;
+        }
+
+        // If the modem had suggested a retry delay, we should retry the current APN again
+        // (up to MAX_SAME_APN_RETRY times) instead of getting the next APN setting from
+        // our own list.
+        if (mModemSuggestedDelay != NO_SUGGESTED_RETRY_DELAY &&
+                mSameApnRetryCount < MAX_SAME_APN_RETRY) {
+            mSameApnRetryCount++;
+            return mWaitingApns.get(mCurrentApnIndex);
+        }
+
+        mSameApnRetryCount = 0;
+
+        int index = mCurrentApnIndex;
+        // Loop through the APN list to find out the index of next non-permanent failed APN.
+        while (true) {
+            if (++index == mWaitingApns.size()) index = 0;
+
+            // Stop if we find the non-failed APN.
+            if (mWaitingApns.get(index).permanentFailed == false) break;
+
+            // If we've already cycled through all the APNs, that means there is no APN we can try
+            if (index == mCurrentApnIndex) return null;
+        }
+
+        mCurrentApnIndex = index;
+        return mWaitingApns.get(mCurrentApnIndex);
+    }
+
+    /**
+     * Get the delay for trying the next waiting APN from the list.
+     * @param failFastEnabled True if fail fast mode enabled. In this case we'll use a shorter
+     *                        delay.
+     * @return delay in milliseconds
+     */
+    public long getDelayForNextApn(boolean failFastEnabled) {
+
+        if (mWaitingApns == null || mWaitingApns.size() == 0) {
+            log("Waiting APN list is null or empty.");
+            return NO_RETRY;
+        }
+
+        if (mModemSuggestedDelay == NO_RETRY) {
+            log("Modem suggested not retrying.");
+            return NO_RETRY;
+        }
+
+        if (mModemSuggestedDelay != NO_SUGGESTED_RETRY_DELAY &&
+                mSameApnRetryCount < MAX_SAME_APN_RETRY) {
+            // If the modem explicitly suggests a retry delay, we should use it, even in fail fast
+            // mode.
+            log("Modem suggested retry in " + mModemSuggestedDelay + " ms.");
+            return mModemSuggestedDelay;
+        }
+
+        // In order to determine the delay to try next APN, we need to peek the next available APN.
+        // Case 1 - If we will start the next round of APN trying,
+        //    we use the exponential-growth delay. (e.g. 5s, 10s, 30s...etc.)
+        // Case 2 - If we are still within the same round of APN trying,
+        //    we use the fixed standard delay between APNs. (e.g. 20s)
+
+        int index = mCurrentApnIndex;
+        while (true) {
+            if (++index >= mWaitingApns.size()) index = 0;
+
+            // Stop if we find the non-failed APN.
+            if (mWaitingApns.get(index).permanentFailed == false) break;
+
+            // If we've already cycled through all the APNs, that means all APNs have
+            // permanently failed
+            if (index == mCurrentApnIndex) {
+                log("All APNs have permanently failed.");
+                return NO_RETRY;
+            }
+        }
+
+        long delay;
+        if (index <= mCurrentApnIndex) {
+            // Case 1, if the next APN is in the next round.
+            if (!mRetryForever && mRetryCount + 1 > mMaxRetryCount) {
+                log("Reached maximum retry count " + mMaxRetryCount + ".");
+                return NO_RETRY;
+            }
+            delay = getRetryTimer();
+            ++mRetryCount;
+        } else {
+            // Case 2, if the next APN is still in the same round.
+            delay = mInterApnDelay;
+        }
+
+        if (failFastEnabled && delay > mFailFastInterApnDelay) {
+            // If we enable fail fast mode, and the delay we got is longer than
+            // fail-fast delay (mFailFastInterApnDelay), use the fail-fast delay.
+            // If the delay we calculated is already shorter than fail-fast delay,
+            // then ignore fail-fast delay.
+            delay = mFailFastInterApnDelay;
+        }
+
+        return delay;
+    }
+
+    /**
+     * Mark the APN setting permanently failed.
+     * @param apn APN setting to be marked as permanently failed
+     * */
+    public void markApnPermanentFailed(ApnSetting apn) {
+        if (apn != null) {
+            apn.permanentFailed = true;
+        }
+    }
+
+    /**
+     * Reset the retry manager.
+     */
+    private void reset() {
+        mMaxRetryCount = 0;
+        mRetryCount = 0;
+        mCurrentApnIndex = -1;
+        mSameApnRetryCount = 0;
+        mModemSuggestedDelay = NO_SUGGESTED_RETRY_DELAY;
+        mRetryArray.clear();
+    }
+
+    /**
+     * Set waiting APNs for retrying in case needed.
+     * @param waitingApns Waiting APN list
+     */
+    public void setWaitingApns(ArrayList<ApnSetting> waitingApns) {
+
+        if (waitingApns == null) {
+            log("No waiting APNs provided");
+            return;
+        }
+
+        mWaitingApns = waitingApns;
+
+        // Since we replace the entire waiting APN list, we need to re-config this retry manager.
+        configureRetry();
+
+        for (ApnSetting apn : mWaitingApns) {
+            apn.permanentFailed = false;
+        }
+
+        log("Setting " + mWaitingApns.size() + " waiting APNs.");
+
+        if (VDBG) {
+            for (int i = 0; i < mWaitingApns.size(); i++) {
+                log("  [" + i + "]:" + mWaitingApns.get(i));
+            }
+        }
+    }
+
+    /**
+     * Get the list of waiting APNs.
+     * @return the list of waiting APNs
+     */
+    public ArrayList<ApnSetting> getWaitingApns() {
+        return mWaitingApns;
+    }
+
+    /**
+     * Save the modem suggested delay for retrying the current APN.
+     * This method is called when we get the suggested delay from RIL.
+     * @param delay The delay in milliseconds
+     */
+    public void setModemSuggestedDelay(long delay) {
+        mModemSuggestedDelay = delay;
+    }
+
+    /**
+     * Get the delay in milliseconds for APN retry after disconnect
+     * @return The delay in milliseconds
+     */
+    public long getRetryAfterDisconnectDelay() {
+        return mApnRetryAfterDisconnectDelay;
+    }
+
+    public String toString() {
+        if (mConfig == null) return "";
+        return "RetryManager: mApnType=" + mApnType + " mRetryCount=" + mRetryCount
+                + " mMaxRetryCount=" + mMaxRetryCount + " mCurrentApnIndex=" + mCurrentApnIndex
+                + " mSameApnRtryCount=" + mSameApnRetryCount + " mModemSuggestedDelay="
+                + mModemSuggestedDelay + " mRetryForever=" + mRetryForever + " mInterApnDelay="
+                + mInterApnDelay + " mApnRetryAfterDisconnectDelay=" + mApnRetryAfterDisconnectDelay
+                + " mConfig={" + mConfig + "}";
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[" + mApnType + "] " + s);
+    }
+}
diff --git a/com/android/internal/telephony/RilWakelockInfo.java b/com/android/internal/telephony/RilWakelockInfo.java
new file mode 100644
index 0000000..5d9e54b
--- /dev/null
+++ b/com/android/internal/telephony/RilWakelockInfo.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.telephony.Rlog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+@TargetApi(8)
+public class RilWakelockInfo {
+    private final String LOG_TAG = RilWakelockInfo.class.getSimpleName();
+    private int mRilRequestSent;
+    private int mTokenNumber;
+    private long mRequestTime;
+    private long mResponseTime;
+
+    /* If there are n requests waiting for a response for time t, the time attributed to
+    each request will be t/n. If the number of outstanding requests changes at time t1,
+    then we will compute the wakelock time till t1 and store it in mWakelockTimeAttributedSoFar
+    and update mConcurrentRequests. mLastAggregatedTime will be set to t1 and used to
+    compute the time taken for this request using the new mConcurrentRequests
+     */
+    private long mWakelockTimeAttributedSoFar;
+    private long mLastAggregatedTime;
+    private int mConcurrentRequests;
+
+    @VisibleForTesting
+    public int getConcurrentRequests() {
+        return mConcurrentRequests;
+    }
+
+    RilWakelockInfo(int rilRequest, int tokenNumber, int concurrentRequests, long requestTime) {
+        concurrentRequests = validateConcurrentRequests(concurrentRequests);
+        this.mRilRequestSent = rilRequest;
+        this.mTokenNumber = tokenNumber;
+        this.mConcurrentRequests = concurrentRequests;
+        this.mRequestTime = requestTime;
+        this.mWakelockTimeAttributedSoFar = 0;
+        this.mLastAggregatedTime = requestTime;
+    }
+
+    private int validateConcurrentRequests(int concurrentRequests) {
+        if(concurrentRequests <= 0) {
+            if(Build.IS_DEBUGGABLE) {
+                IllegalArgumentException e = new IllegalArgumentException(
+                    "concurrentRequests should always be greater than 0.");
+                Rlog.e(LOG_TAG, e.toString());
+                throw e;
+            } else {
+                concurrentRequests = 1;
+            }
+        }
+        return concurrentRequests;
+    }
+
+    int getTokenNumber() {
+        return mTokenNumber;
+    }
+
+    int getRilRequestSent() {
+        return mRilRequestSent;
+    }
+
+    void setResponseTime(long responseTime) {
+        updateTime(responseTime);
+        this.mResponseTime = responseTime;
+    }
+
+    void updateConcurrentRequests(int concurrentRequests, long time) {
+        concurrentRequests = validateConcurrentRequests(concurrentRequests);
+        updateTime(time);
+        mConcurrentRequests = concurrentRequests;
+    }
+
+    synchronized void updateTime(long time) {
+        mWakelockTimeAttributedSoFar += (time - mLastAggregatedTime) / mConcurrentRequests;
+        mLastAggregatedTime = time;
+    }
+
+    long getWakelockTimeAttributedToClient() {
+        return mWakelockTimeAttributedSoFar;
+    }
+
+    @Override
+    public String toString() {
+        return "WakelockInfo{" +
+                "rilRequestSent=" + mRilRequestSent +
+                ", tokenNumber=" + mTokenNumber +
+                ", requestTime=" + mRequestTime +
+                ", responseTime=" + mResponseTime +
+                ", mWakelockTimeAttributed=" + mWakelockTimeAttributedSoFar +
+                '}';
+    }
+}
diff --git a/com/android/internal/telephony/SMSDispatcher.java b/com/android/internal/telephony/SMSDispatcher.java
new file mode 100644
index 0000000..2ec5101
--- /dev/null
+++ b/com/android/internal/telephony/SMSDispatcher.java
@@ -0,0 +1,1822 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import static android.Manifest.permission.SEND_SMS_NO_CONFIRMATION;
+import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE;
+import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE;
+import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED;
+import static android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE;
+import static android.telephony.SmsManager.RESULT_ERROR_NULL_PDU;
+import static android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF;
+import static android.telephony.SmsManager.RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED;
+import static android.telephony.SmsManager.RESULT_ERROR_SHORT_CODE_NOT_ALLOWED;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.sqlite.SqliteWrapper;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.provider.Telephony.Sms;
+import android.service.carrier.CarrierMessagingService;
+import android.service.carrier.ICarrierMessagingCallback;
+import android.service.carrier.ICarrierMessagingService;
+import android.telephony.CarrierMessagingServiceManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public abstract class SMSDispatcher extends Handler {
+    static final String TAG = "SMSDispatcher";    // accessed from inner class
+    static final boolean DBG = false;
+    private static final String SEND_NEXT_MSG_EXTRA = "SendNextMsg";
+
+    private static final int PREMIUM_RULE_USE_SIM = 1;
+    private static final int PREMIUM_RULE_USE_NETWORK = 2;
+    private static final int PREMIUM_RULE_USE_BOTH = 3;
+    private final AtomicInteger mPremiumSmsRule = new AtomicInteger(PREMIUM_RULE_USE_SIM);
+    private final SettingsObserver mSettingsObserver;
+
+    /** SMS send complete. */
+    protected static final int EVENT_SEND_SMS_COMPLETE = 2;
+
+    /** Retry sending a previously failed SMS message */
+    private static final int EVENT_SEND_RETRY = 3;
+
+    /** Confirmation required for sending a large number of messages. */
+    private static final int EVENT_SEND_LIMIT_REACHED_CONFIRMATION = 4;
+
+    /** Send the user confirmed SMS */
+    static final int EVENT_SEND_CONFIRMED_SMS = 5;  // accessed from inner class
+
+    /** Don't send SMS (user did not confirm). */
+    static final int EVENT_STOP_SENDING = 7;        // accessed from inner class
+
+    /** Confirmation required for third-party apps sending to an SMS short code. */
+    private static final int EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE = 8;
+
+    /** Confirmation required for third-party apps sending to an SMS short code. */
+    private static final int EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE = 9;
+
+    /** Handle status report from {@code CdmaInboundSmsHandler}. */
+    protected static final int EVENT_HANDLE_STATUS_REPORT = 10;
+
+    /** Radio is ON */
+    protected static final int EVENT_RADIO_ON = 11;
+
+    /** IMS registration/SMS format changed */
+    protected static final int EVENT_IMS_STATE_CHANGED = 12;
+
+    /** Callback from RIL_REQUEST_IMS_REGISTRATION_STATE */
+    protected static final int EVENT_IMS_STATE_DONE = 13;
+
+    // other
+    protected static final int EVENT_NEW_ICC_SMS = 14;
+    protected static final int EVENT_ICC_CHANGED = 15;
+
+    protected Phone mPhone;
+    protected final Context mContext;
+    protected final ContentResolver mResolver;
+    protected final CommandsInterface mCi;
+    protected final TelephonyManager mTelephonyManager;
+
+    /** Maximum number of times to retry sending a failed SMS. */
+    private static final int MAX_SEND_RETRIES = 3;
+    /** Delay before next send attempt on a failed SMS, in milliseconds. */
+    private static final int SEND_RETRY_DELAY = 2000;
+    /** single part SMS */
+    private static final int SINGLE_PART_SMS = 1;
+    /** Message sending queue limit */
+    private static final int MO_MSG_QUEUE_LIMIT = 5;
+
+    /**
+     * Message reference for a CONCATENATED_8_BIT_REFERENCE or
+     * CONCATENATED_16_BIT_REFERENCE message set.  Should be
+     * incremented for each set of concatenated messages.
+     * Static field shared by all dispatcher objects.
+     */
+    private static int sConcatenatedRef = new Random().nextInt(256);
+
+    /** Outgoing message counter. Shared by all dispatchers. */
+    private SmsUsageMonitor mUsageMonitor;
+
+    private ImsSMSDispatcher mImsSMSDispatcher;
+
+    /** Number of outgoing SmsTrackers waiting for user confirmation. */
+    private int mPendingTrackerCount;
+
+    /* Flags indicating whether the current device allows sms service */
+    protected boolean mSmsCapable = true;
+    protected boolean mSmsSendDisabled;
+
+    protected static int getNextConcatenatedRef() {
+        sConcatenatedRef += 1;
+        return sConcatenatedRef;
+    }
+
+    /**
+     * Create a new SMS dispatcher.
+     * @param phone the Phone to use
+     * @param usageMonitor the SmsUsageMonitor to use
+     */
+    protected SMSDispatcher(Phone phone, SmsUsageMonitor usageMonitor,
+            ImsSMSDispatcher imsSMSDispatcher) {
+        mPhone = phone;
+        mImsSMSDispatcher = imsSMSDispatcher;
+        mContext = phone.getContext();
+        mResolver = mContext.getContentResolver();
+        mCi = phone.mCi;
+        mUsageMonitor = usageMonitor;
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        mSettingsObserver = new SettingsObserver(this, mPremiumSmsRule, mContext);
+        mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
+                Settings.Global.SMS_SHORT_CODE_RULE), false, mSettingsObserver);
+
+        mSmsCapable = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_sms_capable);
+        mSmsSendDisabled = !mTelephonyManager.getSmsSendCapableForPhone(
+                mPhone.getPhoneId(), mSmsCapable);
+        Rlog.d(TAG, "SMSDispatcher: ctor mSmsCapable=" + mSmsCapable + " format=" + getFormat()
+                + " mSmsSendDisabled=" + mSmsSendDisabled);
+    }
+
+    /**
+     * Observe the secure setting for updated premium sms determination rules
+     */
+    private static class SettingsObserver extends ContentObserver {
+        private final AtomicInteger mPremiumSmsRule;
+        private final Context mContext;
+        SettingsObserver(Handler handler, AtomicInteger premiumSmsRule, Context context) {
+            super(handler);
+            mPremiumSmsRule = premiumSmsRule;
+            mContext = context;
+            onChange(false); // load initial value;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mPremiumSmsRule.set(Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.SMS_SHORT_CODE_RULE, PREMIUM_RULE_USE_SIM));
+        }
+    }
+
+    protected void updatePhoneObject(Phone phone) {
+        mPhone = phone;
+        mUsageMonitor = phone.mSmsUsageMonitor;
+        Rlog.d(TAG, "Active phone changed to " + mPhone.getPhoneName() );
+    }
+
+    /** Unregister for incoming SMS events. */
+    public void dispose() {
+        mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
+    }
+
+    /**
+     * The format of the message PDU in the associated broadcast intent.
+     * This will be either "3gpp" for GSM/UMTS/LTE messages in 3GPP format
+     * or "3gpp2" for CDMA/LTE messages in 3GPP2 format.
+     *
+     * Note: All applications which handle incoming SMS messages by processing the
+     * SMS_RECEIVED_ACTION broadcast intent MUST pass the "format" extra from the intent
+     * into the new methods in {@link android.telephony.SmsMessage} which take an
+     * extra format parameter. This is required in order to correctly decode the PDU on
+     * devices which require support for both 3GPP and 3GPP2 formats at the same time,
+     * such as CDMA/LTE devices and GSM/CDMA world phones.
+     *
+     * @return the format of the message PDU
+     */
+    protected abstract String getFormat();
+
+    /**
+     * Pass the Message object to subclass to handle. Currently used to pass CDMA status reports
+     * from {@link com.android.internal.telephony.cdma.CdmaInboundSmsHandler}.
+     * @param o the SmsMessage containing the status report
+     */
+    protected void handleStatusReport(Object o) {
+        Rlog.d(TAG, "handleStatusReport() called with no subclass.");
+    }
+
+    /* TODO: Need to figure out how to keep track of status report routing in a
+     *       persistent manner. If the phone process restarts (reboot or crash),
+     *       we will lose this list and any status reports that come in after
+     *       will be dropped.
+     */
+    /** Sent messages awaiting a delivery status report. */
+    protected final ArrayList<SmsTracker> deliveryPendingList = new ArrayList<SmsTracker>();
+
+    /**
+     * Handles events coming from the phone stack. Overridden from handler.
+     *
+     * @param msg the message to handle
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+        case EVENT_SEND_SMS_COMPLETE:
+            // An outbound SMS has been successfully transferred, or failed.
+            handleSendComplete((AsyncResult) msg.obj);
+            break;
+
+        case EVENT_SEND_RETRY:
+            Rlog.d(TAG, "SMS retry..");
+            sendRetrySms((SmsTracker) msg.obj);
+            break;
+
+        case EVENT_SEND_LIMIT_REACHED_CONFIRMATION:
+            handleReachSentLimit((SmsTracker)(msg.obj));
+            break;
+
+        case EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE:
+            handleConfirmShortCode(false, (SmsTracker)(msg.obj));
+            break;
+
+        case EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE:
+            handleConfirmShortCode(true, (SmsTracker)(msg.obj));
+            break;
+
+        case EVENT_SEND_CONFIRMED_SMS:
+        {
+            SmsTracker tracker = (SmsTracker) msg.obj;
+            if (tracker.isMultipart()) {
+                sendMultipartSms(tracker);
+            } else {
+                if (mPendingTrackerCount > 1) {
+                    tracker.mExpectMore = true;
+                } else {
+                    tracker.mExpectMore = false;
+                }
+                sendSms(tracker);
+            }
+            mPendingTrackerCount--;
+            break;
+        }
+
+        case EVENT_STOP_SENDING:
+        {
+            SmsTracker tracker = (SmsTracker) msg.obj;
+            if (msg.arg1 == ConfirmDialogListener.SHORT_CODE_MSG) {
+                if (msg.arg2 == ConfirmDialogListener.NEVER_ALLOW) {
+                    tracker.onFailed(mContext,
+                            RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED, 0/*errorCode*/);
+                    Rlog.d(TAG, "SMSDispatcher: EVENT_STOP_SENDING - "
+                            + "sending SHORT_CODE_NEVER_ALLOWED error code.");
+                } else {
+                    tracker.onFailed(mContext,
+                            RESULT_ERROR_SHORT_CODE_NOT_ALLOWED, 0/*errorCode*/);
+                    Rlog.d(TAG, "SMSDispatcher: EVENT_STOP_SENDING - "
+                            + "sending SHORT_CODE_NOT_ALLOWED error code.");
+                }
+            } else if (msg.arg1 == ConfirmDialogListener.RATE_LIMIT) {
+                tracker.onFailed(mContext, RESULT_ERROR_LIMIT_EXCEEDED, 0/*errorCode*/);
+                Rlog.d(TAG, "SMSDispatcher: EVENT_STOP_SENDING - "
+                        + "sending LIMIT_EXCEEDED error code.");
+            } else {
+                Rlog.e(TAG, "SMSDispatcher: EVENT_STOP_SENDING - unexpected cases.");
+            }
+            mPendingTrackerCount--;
+            break;
+        }
+
+        case EVENT_HANDLE_STATUS_REPORT:
+            handleStatusReport(msg.obj);
+            break;
+
+        default:
+            Rlog.e(TAG, "handleMessage() ignoring message of unexpected type " + msg.what);
+        }
+    }
+
+    /**
+     * Use the carrier messaging service to send a data or text SMS.
+     */
+    protected abstract class SmsSender extends CarrierMessagingServiceManager {
+        protected final SmsTracker mTracker;
+        // Initialized in sendSmsByCarrierApp
+        protected volatile SmsSenderCallback mSenderCallback;
+
+        protected SmsSender(SmsTracker tracker) {
+            mTracker = tracker;
+        }
+
+        public void sendSmsByCarrierApp(String carrierPackageName,
+                                        SmsSenderCallback senderCallback) {
+            mSenderCallback = senderCallback;
+            if (!bindToCarrierMessagingService(mContext, carrierPackageName)) {
+                Rlog.e(TAG, "bindService() for carrier messaging service failed");
+                mSenderCallback.onSendSmsComplete(
+                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                        0 /* messageRef */);
+            } else {
+                Rlog.d(TAG, "bindService() for carrier messaging service succeeded");
+            }
+        }
+    }
+
+    private static int getSendSmsFlag(@Nullable PendingIntent deliveryIntent) {
+        if (deliveryIntent == null) {
+            return 0;
+        }
+        return CarrierMessagingService.SEND_FLAG_REQUEST_DELIVERY_STATUS;
+    }
+
+    /**
+     * Use the carrier messaging service to send a text SMS.
+     */
+    protected final class TextSmsSender extends SmsSender {
+        public TextSmsSender(SmsTracker tracker) {
+            super(tracker);
+        }
+
+        @Override
+        protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
+            HashMap<String, Object> map = mTracker.getData();
+            String text = (String) map.get("text");
+
+            if (text != null) {
+                try {
+                    carrierMessagingService.sendTextSms(text, getSubId(),
+                            mTracker.mDestAddress, getSendSmsFlag(mTracker.mDeliveryIntent),
+                            mSenderCallback);
+                } catch (RemoteException e) {
+                    Rlog.e(TAG, "Exception sending the SMS: " + e);
+                    mSenderCallback.onSendSmsComplete(
+                            CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                            0 /* messageRef */);
+                }
+            } else {
+                mSenderCallback.onSendSmsComplete(
+                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                        0 /* messageRef */);
+            }
+        }
+    }
+
+    /**
+     * Use the carrier messaging service to send a data SMS.
+     */
+    protected final class DataSmsSender extends SmsSender {
+        public DataSmsSender(SmsTracker tracker) {
+            super(tracker);
+        }
+
+        @Override
+        protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
+            HashMap<String, Object> map = mTracker.getData();
+            byte[] data = (byte[]) map.get("data");
+            int destPort = (int) map.get("destPort");
+
+            if (data != null) {
+                try {
+                    carrierMessagingService.sendDataSms(data, getSubId(),
+                            mTracker.mDestAddress, destPort,
+                            getSendSmsFlag(mTracker.mDeliveryIntent), mSenderCallback);
+                } catch (RemoteException e) {
+                    Rlog.e(TAG, "Exception sending the SMS: " + e);
+                    mSenderCallback.onSendSmsComplete(
+                            CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                            0 /* messageRef */);
+                }
+            } else {
+                mSenderCallback.onSendSmsComplete(
+                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                        0 /* messageRef */);
+            }
+        }
+    }
+
+    /**
+     * Callback for TextSmsSender and DataSmsSender from the carrier messaging service.
+     * Once the result is ready, the carrier messaging service connection is disposed.
+     */
+    protected final class SmsSenderCallback extends ICarrierMessagingCallback.Stub {
+        private final SmsSender mSmsSender;
+
+        public SmsSenderCallback(SmsSender smsSender) {
+            mSmsSender = smsSender;
+        }
+
+        /**
+         * This method should be called only once.
+         */
+        @Override
+        public void onSendSmsComplete(int result, int messageRef) {
+            checkCallerIsPhoneOrCarrierApp();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mSmsSender.disposeConnection(mContext);
+                processSendSmsResponse(mSmsSender.mTracker, result, messageRef);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onSendMultipartSmsComplete(int result, int[] messageRefs) {
+            Rlog.e(TAG, "Unexpected onSendMultipartSmsComplete call with result: " + result);
+        }
+
+        @Override
+        public void onFilterComplete(int result) {
+            Rlog.e(TAG, "Unexpected onFilterComplete call with result: " + result);
+        }
+
+        @Override
+        public void onSendMmsComplete(int result, byte[] sendConfPdu) {
+            Rlog.e(TAG, "Unexpected onSendMmsComplete call with result: " + result);
+        }
+
+        @Override
+        public void onDownloadMmsComplete(int result) {
+            Rlog.e(TAG, "Unexpected onDownloadMmsComplete call with result: " + result);
+        }
+    }
+
+    private void processSendSmsResponse(SmsTracker tracker, int result, int messageRef) {
+        if (tracker == null) {
+            Rlog.e(TAG, "processSendSmsResponse: null tracker");
+            return;
+        }
+
+        SmsResponse smsResponse = new SmsResponse(
+                messageRef, null /* ackPdu */, -1 /* unknown error code */);
+
+        switch (result) {
+        case CarrierMessagingService.SEND_STATUS_OK:
+            Rlog.d(TAG, "Sending SMS by IP succeeded.");
+            sendMessage(obtainMessage(EVENT_SEND_SMS_COMPLETE,
+                                      new AsyncResult(tracker,
+                                                      smsResponse,
+                                                      null /* exception*/ )));
+            break;
+        case CarrierMessagingService.SEND_STATUS_ERROR:
+            Rlog.d(TAG, "Sending SMS by IP failed.");
+            sendMessage(obtainMessage(EVENT_SEND_SMS_COMPLETE,
+                    new AsyncResult(tracker, smsResponse,
+                            new CommandException(CommandException.Error.GENERIC_FAILURE))));
+            break;
+        case CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK:
+            Rlog.d(TAG, "Sending SMS by IP failed. Retry on carrier network.");
+            sendSubmitPdu(tracker);
+            break;
+        default:
+            Rlog.d(TAG, "Unknown result " + result + " Retry on carrier network.");
+            sendSubmitPdu(tracker);
+        }
+    }
+
+    /**
+     * Use the carrier messaging service to send a multipart text SMS.
+     */
+    private final class MultipartSmsSender extends CarrierMessagingServiceManager {
+        private final List<String> mParts;
+        public final SmsTracker[] mTrackers;
+        // Initialized in sendSmsByCarrierApp
+        private volatile MultipartSmsSenderCallback mSenderCallback;
+
+        MultipartSmsSender(ArrayList<String> parts, SmsTracker[] trackers) {
+            mParts = parts;
+            mTrackers = trackers;
+        }
+
+        void sendSmsByCarrierApp(String carrierPackageName,
+                                 MultipartSmsSenderCallback senderCallback) {
+            mSenderCallback = senderCallback;
+            if (!bindToCarrierMessagingService(mContext, carrierPackageName)) {
+                Rlog.e(TAG, "bindService() for carrier messaging service failed");
+                mSenderCallback.onSendMultipartSmsComplete(
+                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                        null /* smsResponse */);
+            } else {
+                Rlog.d(TAG, "bindService() for carrier messaging service succeeded");
+            }
+        }
+
+        @Override
+        protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
+            try {
+                carrierMessagingService.sendMultipartTextSms(
+                        mParts, getSubId(), mTrackers[0].mDestAddress,
+                        getSendSmsFlag(mTrackers[0].mDeliveryIntent), mSenderCallback);
+            } catch (RemoteException e) {
+                Rlog.e(TAG, "Exception sending the SMS: " + e);
+                mSenderCallback.onSendMultipartSmsComplete(
+                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
+                        null /* smsResponse */);
+            }
+        }
+    }
+
+    /**
+     * Callback for MultipartSmsSender from the carrier messaging service.
+     * Once the result is ready, the carrier messaging service connection is disposed.
+     */
+    private final class MultipartSmsSenderCallback extends ICarrierMessagingCallback.Stub {
+        private final MultipartSmsSender mSmsSender;
+
+        MultipartSmsSenderCallback(MultipartSmsSender smsSender) {
+            mSmsSender = smsSender;
+        }
+
+        @Override
+        public void onSendSmsComplete(int result, int messageRef) {
+            Rlog.e(TAG, "Unexpected onSendSmsComplete call with result: " + result);
+        }
+
+        /**
+         * This method should be called only once.
+         */
+        @Override
+        public void onSendMultipartSmsComplete(int result, int[] messageRefs) {
+            mSmsSender.disposeConnection(mContext);
+
+            if (mSmsSender.mTrackers == null) {
+                Rlog.e(TAG, "Unexpected onSendMultipartSmsComplete call with null trackers.");
+                return;
+            }
+
+            checkCallerIsPhoneOrCarrierApp();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                for (int i = 0; i < mSmsSender.mTrackers.length; i++) {
+                    int messageRef = 0;
+                    if (messageRefs != null && messageRefs.length > i) {
+                        messageRef = messageRefs[i];
+                    }
+                    processSendSmsResponse(mSmsSender.mTrackers[i], result, messageRef);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void onFilterComplete(int result) {
+            Rlog.e(TAG, "Unexpected onFilterComplete call with result: " + result);
+        }
+
+        @Override
+        public void onSendMmsComplete(int result, byte[] sendConfPdu) {
+            Rlog.e(TAG, "Unexpected onSendMmsComplete call with result: " + result);
+        }
+
+        @Override
+        public void onDownloadMmsComplete(int result) {
+            Rlog.e(TAG, "Unexpected onDownloadMmsComplete call with result: " + result);
+        }
+    }
+
+    /**
+     * Send an SMS PDU. Usually just calls {@link sendRawPdu}.
+     */
+    protected abstract void sendSubmitPdu(SmsTracker tracker);
+
+    /**
+     * Called when SMS send completes. Broadcasts a sentIntent on success.
+     * On failure, either sets up retries or broadcasts a sentIntent with
+     * the failure in the result code.
+     *
+     * @param ar AsyncResult passed into the message handler.  ar.result should
+     *           an SmsResponse instance if send was successful.  ar.userObj
+     *           should be an SmsTracker instance.
+     */
+    protected void handleSendComplete(AsyncResult ar) {
+        SmsTracker tracker = (SmsTracker) ar.userObj;
+        PendingIntent sentIntent = tracker.mSentIntent;
+
+        if (ar.result != null) {
+            tracker.mMessageRef = ((SmsResponse)ar.result).mMessageRef;
+        } else {
+            Rlog.d(TAG, "SmsResponse was null");
+        }
+
+        if (ar.exception == null) {
+            if (DBG) Rlog.d(TAG, "SMS send complete. Broadcasting intent: " + sentIntent);
+
+            if (tracker.mDeliveryIntent != null) {
+                // Expecting a status report.  Add it to the list.
+                deliveryPendingList.add(tracker);
+            }
+            tracker.onSent(mContext);
+        } else {
+            if (DBG) Rlog.d(TAG, "SMS send failed");
+
+            int ss = mPhone.getServiceState().getState();
+
+            if ( tracker.mImsRetry > 0 && ss != ServiceState.STATE_IN_SERVICE) {
+                // This is retry after failure over IMS but voice is not available.
+                // Set retry to max allowed, so no retry is sent and
+                //   cause RESULT_ERROR_GENERIC_FAILURE to be returned to app.
+                tracker.mRetryCount = MAX_SEND_RETRIES;
+
+                Rlog.d(TAG, "handleSendComplete: Skipping retry: "
+                +" isIms()="+isIms()
+                +" mRetryCount="+tracker.mRetryCount
+                +" mImsRetry="+tracker.mImsRetry
+                +" mMessageRef="+tracker.mMessageRef
+                +" SS= "+mPhone.getServiceState().getState());
+            }
+
+            // if sms over IMS is not supported on data and voice is not available...
+            if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
+                tracker.onFailed(mContext, getNotInServiceError(ss), 0/*errorCode*/);
+            } else if ((((CommandException)(ar.exception)).getCommandError()
+                    == CommandException.Error.SMS_FAIL_RETRY) &&
+                   tracker.mRetryCount < MAX_SEND_RETRIES) {
+                // Retry after a delay if needed.
+                // TODO: According to TS 23.040, 9.2.3.6, we should resend
+                //       with the same TP-MR as the failed message, and
+                //       TP-RD set to 1.  However, we don't have a means of
+                //       knowing the MR for the failed message (EF_SMSstatus
+                //       may or may not have the MR corresponding to this
+                //       message, depending on the failure).  Also, in some
+                //       implementations this retry is handled by the baseband.
+                tracker.mRetryCount++;
+                Message retryMsg = obtainMessage(EVENT_SEND_RETRY, tracker);
+                sendMessageDelayed(retryMsg, SEND_RETRY_DELAY);
+            } else {
+                int errorCode = 0;
+                if (ar.result != null) {
+                    errorCode = ((SmsResponse)ar.result).mErrorCode;
+                }
+                int error = RESULT_ERROR_GENERIC_FAILURE;
+                if (((CommandException)(ar.exception)).getCommandError()
+                        == CommandException.Error.FDN_CHECK_FAILURE) {
+                    error = RESULT_ERROR_FDN_CHECK_FAILURE;
+                }
+                tracker.onFailed(mContext, error, errorCode);
+            }
+        }
+    }
+
+    /**
+     * Handles outbound message when the phone is not in service.
+     *
+     * @param ss     Current service state.  Valid values are:
+     *                  OUT_OF_SERVICE
+     *                  EMERGENCY_ONLY
+     *                  POWER_OFF
+     * @param sentIntent the PendingIntent to send the error to
+     */
+    protected static void handleNotInService(int ss, PendingIntent sentIntent) {
+        if (sentIntent != null) {
+            try {
+                if (ss == ServiceState.STATE_POWER_OFF) {
+                    sentIntent.send(RESULT_ERROR_RADIO_OFF);
+                } else {
+                    sentIntent.send(RESULT_ERROR_NO_SERVICE);
+                }
+            } catch (CanceledException ex) {}
+        }
+    }
+
+    /**
+     * @param ss service state
+     * @return The result error based on input service state for not in service error
+     */
+    protected static int getNotInServiceError(int ss) {
+        if (ss == ServiceState.STATE_POWER_OFF) {
+            return RESULT_ERROR_RADIO_OFF;
+        }
+        return RESULT_ERROR_NO_SERVICE;
+    }
+
+    /**
+     * Send a data based SMS to a specific application port.
+     *
+     * @param destAddr the address to send the message to
+     * @param scAddr is the service center address or null to use
+     *  the current default SMSC
+     * @param destPort the port to deliver the message to
+     * @param data the body of the message to send
+     * @param sentIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is successfully sent, or failed.
+     *  The result code will be <code>Activity.RESULT_OK<code> for success,
+     *  or one of these errors:<br>
+     *  <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
+     *  <code>RESULT_ERROR_RADIO_OFF</code><br>
+     *  <code>RESULT_ERROR_NULL_PDU</code><br>
+     *  <code>RESULT_ERROR_NO_SERVICE</code><br>.
+     *  For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include
+     *  the extra "errorCode" containing a radio technology specific value,
+     *  generally only useful for troubleshooting.<br>
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * @param deliveryIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is delivered to the recipient.  The
+     *  raw pdu of the status report is in the extended data ("pdu").
+     */
+    protected abstract void sendData(String destAddr, String scAddr, int destPort,
+            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent);
+
+    /**
+     * Send a text based SMS.
+     *  @param destAddr the address to send the message to
+     * @param scAddr is the service center address or null to use
+     *  the current default SMSC
+     * @param text the body of the message to send
+     * @param sentIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is successfully sent, or failed.
+     *  The result code will be <code>Activity.RESULT_OK<code> for success,
+     *  or one of these errors:<br>
+     *  <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
+     *  <code>RESULT_ERROR_RADIO_OFF</code><br>
+     *  <code>RESULT_ERROR_NULL_PDU</code><br>
+     *  <code>RESULT_ERROR_NO_SERVICE</code><br>.
+     *  For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include
+     *  the extra "errorCode" containing a radio technology specific value,
+     *  generally only useful for troubleshooting.<br>
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * @param deliveryIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is delivered to the recipient.  The
+     * @param messageUri optional URI of the message if it is already stored in the system
+     * @param callingPkg the calling package name
+     * @param persistMessage whether to save the sent message into SMS DB for a
+     *   non-default SMS app.
+     */
+    protected abstract void sendText(String destAddr, String scAddr, String text,
+            PendingIntent sentIntent, PendingIntent deliveryIntent, Uri messageUri,
+            String callingPkg, boolean persistMessage);
+
+    /**
+     * Inject an SMS PDU into the android platform.
+     *
+     * @param pdu is the byte array of pdu to be injected into android telephony layer
+     * @param format is the format of SMS pdu (3gpp or 3gpp2)
+     * @param receivedIntent if not NULL this <code>PendingIntent</code> is
+     *  broadcast when the message is successfully received by the
+     *  android telephony layer. This intent is broadcasted at
+     *  the same time an SMS received from radio is responded back.
+     */
+    protected abstract void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent);
+
+    /**
+     * Calculate the number of septets needed to encode the message. This function should only be
+     * called for individual segments of multipart message.
+     *
+     * @param messageBody the message to encode
+     * @param use7bitOnly ignore (but still count) illegal characters if true
+     * @return TextEncodingDetails
+     */
+    protected abstract TextEncodingDetails calculateLength(CharSequence messageBody,
+            boolean use7bitOnly);
+
+    /**
+     * Send a multi-part text based SMS.
+     *  @param destAddr the address to send the message to
+     * @param scAddr is the service center address or null to use
+     *   the current default SMSC
+     * @param parts an <code>ArrayList</code> of strings that, in order,
+     *   comprise the original message
+     * @param sentIntents if not null, an <code>ArrayList</code> of
+     *   <code>PendingIntent</code>s (one for each message part) that is
+     *   broadcast when the corresponding message part has been sent.
+     *   The result code will be <code>Activity.RESULT_OK<code> for success,
+     *   or one of these errors:
+     *   <code>RESULT_ERROR_GENERIC_FAILURE</code>
+     *   <code>RESULT_ERROR_RADIO_OFF</code>
+     *   <code>RESULT_ERROR_NULL_PDU</code>
+     *   <code>RESULT_ERROR_NO_SERVICE</code>.
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * @param deliveryIntents if not null, an <code>ArrayList</code> of
+     *   <code>PendingIntent</code>s (one for each message part) that is
+     *   broadcast when the corresponding message part has been delivered
+     *   to the recipient.  The raw pdu of the status report is in the
+     * @param messageUri optional URI of the message if it is already stored in the system
+     * @param callingPkg the calling package name
+     * @param persistMessage whether to save the sent message into SMS DB for a
+     *   non-default SMS app.
+     */
+    protected void sendMultipartText(String destAddr, String scAddr,
+            ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
+            ArrayList<PendingIntent> deliveryIntents, Uri messageUri, String callingPkg,
+            boolean persistMessage) {
+        final String fullMessageText = getMultipartMessageText(parts);
+        int refNumber = getNextConcatenatedRef() & 0x00FF;
+        int msgCount = parts.size();
+        int encoding = SmsConstants.ENCODING_UNKNOWN;
+
+        TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount];
+        for (int i = 0; i < msgCount; i++) {
+            TextEncodingDetails details = calculateLength(parts.get(i), false);
+            if (encoding != details.codeUnitSize
+                    && (encoding == SmsConstants.ENCODING_UNKNOWN
+                            || encoding == SmsConstants.ENCODING_7BIT)) {
+                encoding = details.codeUnitSize;
+            }
+            encodingForParts[i] = details;
+        }
+
+        SmsTracker[] trackers = new SmsTracker[msgCount];
+
+        // States to track at the message level (for all parts)
+        final AtomicInteger unsentPartCount = new AtomicInteger(msgCount);
+        final AtomicBoolean anyPartFailed = new AtomicBoolean(false);
+
+        for (int i = 0; i < msgCount; i++) {
+            SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
+            concatRef.refNumber = refNumber;
+            concatRef.seqNumber = i + 1;  // 1-based sequence
+            concatRef.msgCount = msgCount;
+            // TODO: We currently set this to true since our messaging app will never
+            // send more than 255 parts (it converts the message to MMS well before that).
+            // However, we should support 3rd party messaging apps that might need 16-bit
+            // references
+            // Note:  It's not sufficient to just flip this bit to true; it will have
+            // ripple effects (several calculations assume 8-bit ref).
+            concatRef.isEightBits = true;
+            SmsHeader smsHeader = new SmsHeader();
+            smsHeader.concatRef = concatRef;
+
+            // Set the national language tables for 3GPP 7-bit encoding, if enabled.
+            if (encoding == SmsConstants.ENCODING_7BIT) {
+                smsHeader.languageTable = encodingForParts[i].languageTable;
+                smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable;
+            }
+
+            PendingIntent sentIntent = null;
+            if (sentIntents != null && sentIntents.size() > i) {
+                sentIntent = sentIntents.get(i);
+            }
+
+            PendingIntent deliveryIntent = null;
+            if (deliveryIntents != null && deliveryIntents.size() > i) {
+                deliveryIntent = deliveryIntents.get(i);
+            }
+
+            trackers[i] =
+                getNewSubmitPduTracker(destAddr, scAddr, parts.get(i), smsHeader, encoding,
+                        sentIntent, deliveryIntent, (i == (msgCount - 1)),
+                        unsentPartCount, anyPartFailed, messageUri, fullMessageText);
+            trackers[i].mPersistMessage = persistMessage;
+        }
+
+        if (parts == null || trackers == null || trackers.length == 0
+                || trackers[0] == null) {
+            Rlog.e(TAG, "Cannot send multipart text. parts=" + parts + " trackers=" + trackers);
+            return;
+        }
+
+        String carrierPackage = getCarrierAppPackageName();
+        if (carrierPackage != null) {
+            Rlog.d(TAG, "Found carrier package.");
+            MultipartSmsSender smsSender = new MultipartSmsSender(parts, trackers);
+            smsSender.sendSmsByCarrierApp(carrierPackage, new MultipartSmsSenderCallback(smsSender));
+        } else {
+            Rlog.v(TAG, "No carrier package.");
+            for (SmsTracker tracker : trackers) {
+                if (tracker != null) {
+                    sendSubmitPdu(tracker);
+                } else {
+                    Rlog.e(TAG, "Null tracker.");
+                }
+            }
+        }
+    }
+
+    /**
+     * Create a new SubmitPdu and return the SMS tracker.
+     */
+    protected abstract SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
+            String message, SmsHeader smsHeader, int encoding,
+            PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart,
+            AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
+            String fullMessageText);
+
+    /**
+     * Send an SMS
+     * @param tracker will contain:
+     * -smsc the SMSC to send the message through, or NULL for the
+     *  default SMSC
+     * -pdu the raw PDU to send
+     * -sentIntent if not NULL this <code>Intent</code> is
+     *  broadcast when the message is successfully sent, or failed.
+     *  The result code will be <code>Activity.RESULT_OK<code> for success,
+     *  or one of these errors:
+     *  <code>RESULT_ERROR_GENERIC_FAILURE</code>
+     *  <code>RESULT_ERROR_RADIO_OFF</code>
+     *  <code>RESULT_ERROR_NULL_PDU</code>
+     *  <code>RESULT_ERROR_NO_SERVICE</code>.
+     *  The per-application based SMS control checks sentIntent. If sentIntent
+     *  is NULL the caller will be checked against all unknown applications,
+     *  which cause smaller number of SMS to be sent in checking period.
+     * -deliveryIntent if not NULL this <code>Intent</code> is
+     *  broadcast when the message is delivered to the recipient.  The
+     *  raw pdu of the status report is in the extended data ("pdu").
+     * -param destAddr the destination phone number (for short code confirmation)
+     */
+    @VisibleForTesting
+    public void sendRawPdu(SmsTracker tracker) {
+        HashMap map = tracker.getData();
+        byte pdu[] = (byte[]) map.get("pdu");
+
+        if (mSmsSendDisabled) {
+            Rlog.e(TAG, "Device does not support sending sms.");
+            tracker.onFailed(mContext, RESULT_ERROR_NO_SERVICE, 0/*errorCode*/);
+            return;
+        }
+
+        if (pdu == null) {
+            Rlog.e(TAG, "Empty PDU");
+            tracker.onFailed(mContext, RESULT_ERROR_NULL_PDU, 0/*errorCode*/);
+            return;
+        }
+
+        // Get calling app package name via UID from Binder call
+        PackageManager pm = mContext.getPackageManager();
+        String[] packageNames = pm.getPackagesForUid(Binder.getCallingUid());
+
+        if (packageNames == null || packageNames.length == 0) {
+            // Refuse to send SMS if we can't get the calling package name.
+            Rlog.e(TAG, "Can't get calling app package name: refusing to send SMS");
+            tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/*errorCode*/);
+            return;
+        }
+
+        // Get package info via packagemanager
+        PackageInfo appInfo;
+        try {
+            // XXX this is lossy- apps can share a UID
+            appInfo = pm.getPackageInfoAsUser(
+                    packageNames[0], PackageManager.GET_SIGNATURES, tracker.mUserId);
+        } catch (PackageManager.NameNotFoundException e) {
+            Rlog.e(TAG, "Can't get calling app package info: refusing to send SMS");
+            tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/*errorCode*/);
+            return;
+        }
+
+        // checkDestination() returns true if the destination is not a premium short code or the
+        // sending app is approved to send to short codes. Otherwise, a message is sent to our
+        // handler with the SmsTracker to request user confirmation before sending.
+        if (checkDestination(tracker)) {
+            // check for excessive outgoing SMS usage by this app
+            if (!mUsageMonitor.check(appInfo.packageName, SINGLE_PART_SMS)) {
+                sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker));
+                return;
+            }
+
+            sendSms(tracker);
+        }
+
+        if (PhoneNumberUtils.isLocalEmergencyNumber(mContext, tracker.mDestAddress)) {
+            new AsyncEmergencyContactNotifier(mContext).execute();
+        }
+    }
+
+    /**
+     * Check if destination is a potential premium short code and sender is not pre-approved to
+     * send to short codes.
+     *
+     * @param tracker the tracker for the SMS to send
+     * @return true if the destination is approved; false if user confirmation event was sent
+     */
+    boolean checkDestination(SmsTracker tracker) {
+        if (mContext.checkCallingOrSelfPermission(SEND_SMS_NO_CONFIRMATION)
+                == PackageManager.PERMISSION_GRANTED) {
+            return true;            // app is pre-approved to send to short codes
+        } else {
+            int rule = mPremiumSmsRule.get();
+            int smsCategory = SmsUsageMonitor.CATEGORY_NOT_SHORT_CODE;
+            if (rule == PREMIUM_RULE_USE_SIM || rule == PREMIUM_RULE_USE_BOTH) {
+                String simCountryIso = mTelephonyManager.getSimCountryIso();
+                if (simCountryIso == null || simCountryIso.length() != 2) {
+                    Rlog.e(TAG, "Can't get SIM country Iso: trying network country Iso");
+                    simCountryIso = mTelephonyManager.getNetworkCountryIso();
+                }
+
+                smsCategory = mUsageMonitor.checkDestination(tracker.mDestAddress, simCountryIso);
+            }
+            if (rule == PREMIUM_RULE_USE_NETWORK || rule == PREMIUM_RULE_USE_BOTH) {
+                String networkCountryIso = mTelephonyManager.getNetworkCountryIso();
+                if (networkCountryIso == null || networkCountryIso.length() != 2) {
+                    Rlog.e(TAG, "Can't get Network country Iso: trying SIM country Iso");
+                    networkCountryIso = mTelephonyManager.getSimCountryIso();
+                }
+
+                smsCategory = SmsUsageMonitor.mergeShortCodeCategories(smsCategory,
+                        mUsageMonitor.checkDestination(tracker.mDestAddress, networkCountryIso));
+            }
+
+            if (smsCategory == SmsUsageMonitor.CATEGORY_NOT_SHORT_CODE
+                    || smsCategory == SmsUsageMonitor.CATEGORY_FREE_SHORT_CODE
+                    || smsCategory == SmsUsageMonitor.CATEGORY_STANDARD_SHORT_CODE) {
+                return true;    // not a premium short code
+            }
+
+            // Do not allow any premium sms during SuW
+            if (Settings.Global.getInt(mResolver, Settings.Global.DEVICE_PROVISIONED, 0) == 0) {
+                Rlog.e(TAG, "Can't send premium sms during Setup Wizard");
+                return false;
+            }
+
+            // Wait for user confirmation unless the user has set permission to always allow/deny
+            int premiumSmsPermission = mUsageMonitor.getPremiumSmsPermission(
+                    tracker.getAppPackageName());
+            if (premiumSmsPermission == SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN) {
+                // First time trying to send to premium SMS.
+                premiumSmsPermission = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER;
+            }
+
+            switch (premiumSmsPermission) {
+                case SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW:
+                    Rlog.d(TAG, "User approved this app to send to premium SMS");
+                    return true;
+
+                case SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW:
+                    Rlog.w(TAG, "User denied this app from sending to premium SMS");
+                    Message msg = obtainMessage(EVENT_STOP_SENDING, tracker);
+                    msg.arg1 = ConfirmDialogListener.SHORT_CODE_MSG;
+                    msg.arg2 = ConfirmDialogListener.NEVER_ALLOW;
+                    sendMessage(msg);
+                    return false;   // reject this message
+
+                case SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER:
+                default:
+                    int event;
+                    if (smsCategory == SmsUsageMonitor.CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE) {
+                        event = EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE;
+                    } else {
+                        event = EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE;
+                    }
+                    sendMessage(obtainMessage(event, tracker));
+                    return false;   // wait for user confirmation
+            }
+        }
+    }
+
+    /**
+     * Deny sending an SMS if the outgoing queue limit is reached. Used when the message
+     * must be confirmed by the user due to excessive usage or potential premium SMS detected.
+     * @param tracker the SmsTracker for the message to send
+     * @return true if the message was denied; false to continue with send confirmation
+     */
+    private boolean denyIfQueueLimitReached(SmsTracker tracker) {
+        if (mPendingTrackerCount >= MO_MSG_QUEUE_LIMIT) {
+            // Deny sending message when the queue limit is reached.
+            Rlog.e(TAG, "Denied because queue limit reached");
+            tracker.onFailed(mContext, RESULT_ERROR_LIMIT_EXCEEDED, 0/*errorCode*/);
+            return true;
+        }
+        mPendingTrackerCount++;
+        return false;
+    }
+
+    /**
+     * Returns the label for the specified app package name.
+     * @param appPackage the package name of the app requesting to send an SMS
+     * @return the label for the specified app, or the package name if getApplicationInfo() fails
+     */
+    private CharSequence getAppLabel(String appPackage, @UserIdInt int userId) {
+        PackageManager pm = mContext.getPackageManager();
+        try {
+            ApplicationInfo appInfo = pm.getApplicationInfoAsUser(appPackage, 0, userId);
+            return appInfo.loadSafeLabel(pm);
+        } catch (PackageManager.NameNotFoundException e) {
+            Rlog.e(TAG, "PackageManager Name Not Found for package " + appPackage);
+            return appPackage;  // fall back to package name if we can't get app label
+        }
+    }
+
+    /**
+     * Post an alert when SMS needs confirmation due to excessive usage.
+     * @param tracker an SmsTracker for the current message.
+     */
+    protected void handleReachSentLimit(SmsTracker tracker) {
+        if (denyIfQueueLimitReached(tracker)) {
+            return;     // queue limit reached; error was returned to caller
+        }
+
+        CharSequence appLabel = getAppLabel(tracker.getAppPackageName(), tracker.mUserId);
+        Resources r = Resources.getSystem();
+        Spanned messageText = Html.fromHtml(r.getString(R.string.sms_control_message, appLabel));
+
+        // Construct ConfirmDialogListenter for Rate Limit handling
+        ConfirmDialogListener listener = new ConfirmDialogListener(tracker, null,
+                ConfirmDialogListener.RATE_LIMIT);
+
+        AlertDialog d = new AlertDialog.Builder(mContext)
+                .setTitle(R.string.sms_control_title)
+                .setIcon(R.drawable.stat_sys_warning)
+                .setMessage(messageText)
+                .setPositiveButton(r.getString(R.string.sms_control_yes), listener)
+                .setNegativeButton(r.getString(R.string.sms_control_no), listener)
+                .setOnCancelListener(listener)
+                .create();
+
+        d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+        d.show();
+    }
+
+    /**
+     * Post an alert for user confirmation when sending to a potential short code.
+     * @param isPremium true if the destination is known to be a premium short code
+     * @param tracker the SmsTracker for the current message.
+     */
+    protected void handleConfirmShortCode(boolean isPremium, SmsTracker tracker) {
+        if (denyIfQueueLimitReached(tracker)) {
+            return;     // queue limit reached; error was returned to caller
+        }
+
+        int detailsId;
+        if (isPremium) {
+            detailsId = R.string.sms_premium_short_code_details;
+        } else {
+            detailsId = R.string.sms_short_code_details;
+        }
+
+        CharSequence appLabel = getAppLabel(tracker.getAppPackageName(), tracker.mUserId);
+        Resources r = Resources.getSystem();
+        Spanned messageText = Html.fromHtml(r.getString(R.string.sms_short_code_confirm_message,
+                appLabel, tracker.mDestAddress));
+
+        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        View layout = inflater.inflate(R.layout.sms_short_code_confirmation_dialog, null);
+
+        // Construct ConfirmDialogListenter for short code message sending
+        ConfirmDialogListener listener = new ConfirmDialogListener(tracker,
+                (TextView) layout.findViewById(R.id.sms_short_code_remember_undo_instruction),
+                ConfirmDialogListener.SHORT_CODE_MSG);
+
+
+        TextView messageView = (TextView) layout.findViewById(R.id.sms_short_code_confirm_message);
+        messageView.setText(messageText);
+
+        ViewGroup detailsLayout = (ViewGroup) layout.findViewById(
+                R.id.sms_short_code_detail_layout);
+        TextView detailsView = (TextView) detailsLayout.findViewById(
+                R.id.sms_short_code_detail_message);
+        detailsView.setText(detailsId);
+
+        CheckBox rememberChoice = (CheckBox) layout.findViewById(
+                R.id.sms_short_code_remember_choice_checkbox);
+        rememberChoice.setOnCheckedChangeListener(listener);
+
+        AlertDialog d = new AlertDialog.Builder(mContext)
+                .setView(layout)
+                .setPositiveButton(r.getString(R.string.sms_short_code_confirm_allow), listener)
+                .setNegativeButton(r.getString(R.string.sms_short_code_confirm_deny), listener)
+                .setOnCancelListener(listener)
+                .create();
+
+        d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+        d.show();
+
+        listener.setPositiveButton(d.getButton(DialogInterface.BUTTON_POSITIVE));
+        listener.setNegativeButton(d.getButton(DialogInterface.BUTTON_NEGATIVE));
+    }
+
+    /**
+     * Returns the premium SMS permission for the specified package. If the package has never
+     * been seen before, the default {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER}
+     * will be returned.
+     * @param packageName the name of the package to query permission
+     * @return one of {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_UNKNOWN},
+     *  {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER},
+     *  {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
+     *  {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
+     */
+    public int getPremiumSmsPermission(String packageName) {
+        return mUsageMonitor.getPremiumSmsPermission(packageName);
+    }
+
+    /**
+     * Sets the premium SMS permission for the specified package and save the value asynchronously
+     * to persistent storage.
+     * @param packageName the name of the package to set permission
+     * @param permission one of {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER},
+     *  {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
+     *  {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
+     */
+    public void setPremiumSmsPermission(String packageName, int permission) {
+        mUsageMonitor.setPremiumSmsPermission(packageName, permission);
+    }
+
+    /**
+     * Send the message along to the radio.
+     *
+     * @param tracker holds the SMS message to send
+     */
+    protected abstract void sendSms(SmsTracker tracker);
+
+    /**
+     * Send the SMS via the PSTN network.
+     *
+     * @param tracker holds the Sms tracker ready to be sent
+     */
+    protected abstract void sendSmsByPstn(SmsTracker tracker);
+
+    /**
+     * Retry the message along to the radio.
+     *
+     * @param tracker holds the SMS message to send
+     */
+    public void sendRetrySms(SmsTracker tracker) {
+        // re-routing to ImsSMSDispatcher
+        if (mImsSMSDispatcher != null) {
+            mImsSMSDispatcher.sendRetrySms(tracker);
+        } else {
+            Rlog.e(TAG, mImsSMSDispatcher + " is null. Retry failed");
+        }
+    }
+
+    /**
+     * Send the multi-part SMS based on multipart Sms tracker
+     *
+     * @param tracker holds the multipart Sms tracker ready to be sent
+     */
+    private void sendMultipartSms(SmsTracker tracker) {
+        ArrayList<String> parts;
+        ArrayList<PendingIntent> sentIntents;
+        ArrayList<PendingIntent> deliveryIntents;
+
+        HashMap<String, Object> map = tracker.getData();
+
+        String destinationAddress = (String) map.get("destination");
+        String scAddress = (String) map.get("scaddress");
+
+        parts = (ArrayList<String>) map.get("parts");
+        sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents");
+        deliveryIntents = (ArrayList<PendingIntent>) map.get("deliveryIntents");
+
+        // check if in service
+        int ss = mPhone.getServiceState().getState();
+        // if sms over IMS is not supported on data and voice is not available...
+        if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
+            for (int i = 0, count = parts.size(); i < count; i++) {
+                PendingIntent sentIntent = null;
+                if (sentIntents != null && sentIntents.size() > i) {
+                    sentIntent = sentIntents.get(i);
+                }
+                handleNotInService(ss, sentIntent);
+            }
+            return;
+        }
+
+        sendMultipartText(destinationAddress, scAddress, parts, sentIntents, deliveryIntents,
+                null/*messageUri*/, null/*callingPkg*/, tracker.mPersistMessage);
+    }
+
+    /**
+     * Keeps track of an SMS that has been sent to the RIL, until it has
+     * successfully been sent, or we're done trying.
+     */
+    public static class SmsTracker {
+        // fields need to be public for derived SmsDispatchers
+        private final HashMap<String, Object> mData;
+        public int mRetryCount;
+        public int mImsRetry; // nonzero indicates initial message was sent over Ims
+        public int mMessageRef;
+        public boolean mExpectMore;
+        String mFormat;
+
+        public final PendingIntent mSentIntent;
+        public final PendingIntent mDeliveryIntent;
+
+        public final PackageInfo mAppInfo;
+        public final String mDestAddress;
+
+        public final SmsHeader mSmsHeader;
+
+        private long mTimestamp = System.currentTimeMillis();
+        public Uri mMessageUri; // Uri of persisted message if we wrote one
+
+        // Reference to states of a multipart message that this part belongs to
+        private AtomicInteger mUnsentPartCount;
+        private AtomicBoolean mAnyPartFailed;
+        // The full message content of a single part message
+        // or a multipart message that this part belongs to
+        private String mFullMessageText;
+
+        private int mSubId;
+
+        // If this is a text message (instead of data message)
+        private boolean mIsText;
+
+        private boolean mPersistMessage;
+
+        // User who sends the SMS.
+        private final @UserIdInt int mUserId;
+
+        private SmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
+                PendingIntent deliveryIntent, PackageInfo appInfo, String destAddr, String format,
+                AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
+                SmsHeader smsHeader, boolean isExpectMore, String fullMessageText, int subId,
+                boolean isText, boolean persistMessage, int userId) {
+            mData = data;
+            mSentIntent = sentIntent;
+            mDeliveryIntent = deliveryIntent;
+            mRetryCount = 0;
+            mAppInfo = appInfo;
+            mDestAddress = destAddr;
+            mFormat = format;
+            mExpectMore = isExpectMore;
+            mImsRetry = 0;
+            mMessageRef = 0;
+            mUnsentPartCount = unsentPartCount;
+            mAnyPartFailed = anyPartFailed;
+            mMessageUri = messageUri;
+            mSmsHeader = smsHeader;
+            mFullMessageText = fullMessageText;
+            mSubId = subId;
+            mIsText = isText;
+            mPersistMessage = persistMessage;
+            mUserId = userId;
+        }
+
+        /**
+         * Returns whether this tracker holds a multi-part SMS.
+         * @return true if the tracker holds a multi-part SMS; false otherwise
+         */
+        boolean isMultipart() {
+            return mData.containsKey("parts");
+        }
+
+        public HashMap<String, Object> getData() {
+            return mData;
+        }
+
+        /**
+         * Get the App package name
+         * @return App package name info
+         */
+        public String getAppPackageName() {
+            return mAppInfo != null ? mAppInfo.packageName : null;
+        }
+
+        /**
+         * Update the status of this message if we persisted it
+         */
+        public void updateSentMessageStatus(Context context, int status) {
+            if (mMessageUri != null) {
+                // If we wrote this message in writeSentMessage, update it now
+                ContentValues values = new ContentValues(1);
+                values.put(Sms.STATUS, status);
+                SqliteWrapper.update(context, context.getContentResolver(),
+                        mMessageUri, values, null, null);
+            }
+        }
+
+        /**
+         * Set the final state of a message: FAILED or SENT
+         *
+         * @param context The Context
+         * @param messageType The final message type
+         * @param errorCode The error code
+         */
+        private void updateMessageState(Context context, int messageType, int errorCode) {
+            if (mMessageUri == null) {
+                return;
+            }
+            final ContentValues values = new ContentValues(2);
+            values.put(Sms.TYPE, messageType);
+            values.put(Sms.ERROR_CODE, errorCode);
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                if (SqliteWrapper.update(context, context.getContentResolver(), mMessageUri, values,
+                        null/*where*/, null/*selectionArgs*/) != 1) {
+                    Rlog.e(TAG, "Failed to move message to " + messageType);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Persist a sent SMS if required:
+         * 1. It is a text message
+         * 2. SmsApplication tells us to persist: sent from apps that are not default-SMS app or
+         *    bluetooth
+         *
+         * @param context
+         * @param messageType The folder to store (FAILED or SENT)
+         * @param errorCode The current error code for this SMS or SMS part
+         * @return The telephony provider URI if stored
+         */
+        private Uri persistSentMessageIfRequired(Context context, int messageType, int errorCode) {
+            if (!mIsText || !mPersistMessage ||
+                    !SmsApplication.shouldWriteMessageForPackage(mAppInfo.packageName, context)) {
+                return null;
+            }
+            Rlog.d(TAG, "Persist SMS into "
+                    + (messageType == Sms.MESSAGE_TYPE_FAILED ? "FAILED" : "SENT"));
+            final ContentValues values = new ContentValues();
+            values.put(Sms.SUBSCRIPTION_ID, mSubId);
+            values.put(Sms.ADDRESS, mDestAddress);
+            values.put(Sms.BODY, mFullMessageText);
+            values.put(Sms.DATE, System.currentTimeMillis()); // milliseconds
+            values.put(Sms.SEEN, 1);
+            values.put(Sms.READ, 1);
+            final String creator = mAppInfo != null ? mAppInfo.packageName : null;
+            if (!TextUtils.isEmpty(creator)) {
+                values.put(Sms.CREATOR, creator);
+            }
+            if (mDeliveryIntent != null) {
+                values.put(Sms.STATUS, Telephony.Sms.STATUS_PENDING);
+            }
+            if (errorCode != 0) {
+                values.put(Sms.ERROR_CODE, errorCode);
+            }
+            final long identity = Binder.clearCallingIdentity();
+            final ContentResolver resolver = context.getContentResolver();
+            try {
+                final Uri uri =  resolver.insert(Telephony.Sms.Sent.CONTENT_URI, values);
+                if (uri != null && messageType == Sms.MESSAGE_TYPE_FAILED) {
+                    // Since we can't persist a message directly into FAILED box,
+                    // we have to update the column after we persist it into SENT box.
+                    // The gap between the state change is tiny so I would not expect
+                    // it to cause any serious problem
+                    // TODO: we should add a "failed" URI for this in SmsProvider?
+                    final ContentValues updateValues = new ContentValues(1);
+                    updateValues.put(Sms.TYPE, Sms.MESSAGE_TYPE_FAILED);
+                    resolver.update(uri, updateValues, null/*where*/, null/*selectionArgs*/);
+                }
+                return uri;
+            } catch (Exception e) {
+                Rlog.e(TAG, "writeOutboxMessage: Failed to persist outbox message", e);
+                return null;
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Persist or update an SMS depending on if we send a new message or a stored message
+         *
+         * @param context
+         * @param messageType The message folder for this SMS, FAILED or SENT
+         * @param errorCode The current error code for this SMS or SMS part
+         */
+        private void persistOrUpdateMessage(Context context, int messageType, int errorCode) {
+            if (mMessageUri != null) {
+                updateMessageState(context, messageType, errorCode);
+            } else {
+                mMessageUri = persistSentMessageIfRequired(context, messageType, errorCode);
+            }
+        }
+
+        /**
+         * Handle a failure of a single part message or a part of a multipart message
+         *
+         * @param context The Context
+         * @param error The error to send back with
+         * @param errorCode
+         */
+        public void onFailed(Context context, int error, int errorCode) {
+            if (mAnyPartFailed != null) {
+                mAnyPartFailed.set(true);
+            }
+            // is single part or last part of multipart message
+            boolean isSinglePartOrLastPart = true;
+            if (mUnsentPartCount != null) {
+                isSinglePartOrLastPart = mUnsentPartCount.decrementAndGet() == 0;
+            }
+            if (isSinglePartOrLastPart) {
+                persistOrUpdateMessage(context, Sms.MESSAGE_TYPE_FAILED, errorCode);
+            }
+            if (mSentIntent != null) {
+                try {
+                    // Extra information to send with the sent intent
+                    Intent fillIn = new Intent();
+                    if (mMessageUri != null) {
+                        // Pass this to SMS apps so that they know where it is stored
+                        fillIn.putExtra("uri", mMessageUri.toString());
+                    }
+                    if (errorCode != 0) {
+                        fillIn.putExtra("errorCode", errorCode);
+                    }
+                    if (mUnsentPartCount != null && isSinglePartOrLastPart) {
+                        // Is multipart and last part
+                        fillIn.putExtra(SEND_NEXT_MSG_EXTRA, true);
+                    }
+                    mSentIntent.send(context, error, fillIn);
+                } catch (CanceledException ex) {
+                    Rlog.e(TAG, "Failed to send result");
+                }
+            }
+        }
+
+        /**
+         * Handle the sent of a single part message or a part of a multipart message
+         *
+         * @param context The Context
+         */
+        public void onSent(Context context) {
+            // is single part or last part of multipart message
+            boolean isSinglePartOrLastPart = true;
+            if (mUnsentPartCount != null) {
+                isSinglePartOrLastPart = mUnsentPartCount.decrementAndGet() == 0;
+            }
+            if (isSinglePartOrLastPart) {
+                int messageType = Sms.MESSAGE_TYPE_SENT;
+                if (mAnyPartFailed != null && mAnyPartFailed.get()) {
+                    messageType = Sms.MESSAGE_TYPE_FAILED;
+                }
+                persistOrUpdateMessage(context, messageType, 0/*errorCode*/);
+            }
+            if (mSentIntent != null) {
+                try {
+                    // Extra information to send with the sent intent
+                    Intent fillIn = new Intent();
+                    if (mMessageUri != null) {
+                        // Pass this to SMS apps so that they know where it is stored
+                        fillIn.putExtra("uri", mMessageUri.toString());
+                    }
+                    if (mUnsentPartCount != null && isSinglePartOrLastPart) {
+                        // Is multipart and last part
+                        fillIn.putExtra(SEND_NEXT_MSG_EXTRA, true);
+                    }
+                    mSentIntent.send(context, Activity.RESULT_OK, fillIn);
+                } catch (CanceledException ex) {
+                    Rlog.e(TAG, "Failed to send result");
+                }
+            }
+        }
+    }
+
+    protected SmsTracker getSmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, String format, AtomicInteger unsentPartCount,
+            AtomicBoolean anyPartFailed, Uri messageUri, SmsHeader smsHeader,
+            boolean isExpectMore, String fullMessageText, boolean isText, boolean persistMessage) {
+        // Get calling app package name via UID from Binder call
+        PackageManager pm = mContext.getPackageManager();
+        String[] packageNames = pm.getPackagesForUid(Binder.getCallingUid());
+
+        // Get package info via packagemanager
+        final int userId = UserHandle.getCallingUserId();
+        PackageInfo appInfo = null;
+        if (packageNames != null && packageNames.length > 0) {
+            try {
+                // XXX this is lossy- apps can share a UID
+                appInfo = pm.getPackageInfoAsUser(
+                        packageNames[0], PackageManager.GET_SIGNATURES, userId);
+            } catch (PackageManager.NameNotFoundException e) {
+                // error will be logged in sendRawPdu
+            }
+        }
+        // Strip non-digits from destination phone number before checking for short codes
+        // and before displaying the number to the user if confirmation is required.
+        String destAddr = PhoneNumberUtils.extractNetworkPortion((String) data.get("destAddr"));
+        return new SmsTracker(data, sentIntent, deliveryIntent, appInfo, destAddr, format,
+                unsentPartCount, anyPartFailed, messageUri, smsHeader, isExpectMore,
+                fullMessageText, getSubId(), isText, persistMessage, userId);
+    }
+
+    protected SmsTracker getSmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, String format, Uri messageUri, boolean isExpectMore,
+            String fullMessageText, boolean isText, boolean persistMessage) {
+        return getSmsTracker(data, sentIntent, deliveryIntent, format, null/*unsentPartCount*/,
+                null/*anyPartFailed*/, messageUri, null/*smsHeader*/, isExpectMore,
+                fullMessageText, isText, persistMessage);
+    }
+
+    protected HashMap<String, Object> getSmsTrackerMap(String destAddr, String scAddr,
+            String text, SmsMessageBase.SubmitPduBase pdu) {
+        HashMap<String, Object> map = new HashMap<String, Object>();
+        map.put("destAddr", destAddr);
+        map.put("scAddr", scAddr);
+        map.put("text", text);
+        map.put("smsc", pdu.encodedScAddress);
+        map.put("pdu", pdu.encodedMessage);
+        return map;
+    }
+
+    protected HashMap<String, Object> getSmsTrackerMap(String destAddr, String scAddr,
+            int destPort, byte[] data, SmsMessageBase.SubmitPduBase pdu) {
+        HashMap<String, Object> map = new HashMap<String, Object>();
+        map.put("destAddr", destAddr);
+        map.put("scAddr", scAddr);
+        map.put("destPort", destPort);
+        map.put("data", data);
+        map.put("smsc", pdu.encodedScAddress);
+        map.put("pdu", pdu.encodedMessage);
+        return map;
+    }
+
+    /**
+     * Dialog listener for SMS confirmation dialog.
+     */
+    private final class ConfirmDialogListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener,
+            CompoundButton.OnCheckedChangeListener {
+
+        private final SmsTracker mTracker;
+        private Button mPositiveButton;
+        private Button mNegativeButton;
+        private boolean mRememberChoice;    // default is unchecked
+        private final TextView mRememberUndoInstruction;
+        private int mConfirmationType;  // 0 - Short Code Msg Sending; 1 - Rate Limit Exceeded
+        private static final int SHORT_CODE_MSG = 0; // Short Code Msg
+        private static final int RATE_LIMIT = 1; // Rate Limit Exceeded
+        private static final int NEVER_ALLOW = 1; // Never Allow
+
+        ConfirmDialogListener(SmsTracker tracker, TextView textView, int confirmationType) {
+            mTracker = tracker;
+            mRememberUndoInstruction = textView;
+            mConfirmationType = confirmationType;
+        }
+
+        void setPositiveButton(Button button) {
+            mPositiveButton = button;
+        }
+
+        void setNegativeButton(Button button) {
+            mNegativeButton = button;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            // Always set the SMS permission so that Settings will show a permission setting
+            // for the app (it won't be shown until after the app tries to send to a short code).
+            int newSmsPermission = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER;
+
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                Rlog.d(TAG, "CONFIRM sending SMS");
+                // XXX this is lossy- apps can have more than one signature
+                EventLog.writeEvent(EventLogTags.EXP_DET_SMS_SENT_BY_USER,
+                                    mTracker.mAppInfo.applicationInfo == null ?
+                                    -1 : mTracker.mAppInfo.applicationInfo.uid);
+                sendMessage(obtainMessage(EVENT_SEND_CONFIRMED_SMS, mTracker));
+                if (mRememberChoice) {
+                    newSmsPermission = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW;
+                }
+            } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                Rlog.d(TAG, "DENY sending SMS");
+                // XXX this is lossy- apps can have more than one signature
+                EventLog.writeEvent(EventLogTags.EXP_DET_SMS_DENIED_BY_USER,
+                                    mTracker.mAppInfo.applicationInfo == null ?
+                                    -1 :  mTracker.mAppInfo.applicationInfo.uid);
+                Message msg = obtainMessage(EVENT_STOP_SENDING, mTracker);
+                msg.arg1 = mConfirmationType;
+                if (mRememberChoice) {
+                    newSmsPermission = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW;
+                    msg.arg2 = ConfirmDialogListener.NEVER_ALLOW;
+                }
+                sendMessage(msg);
+            }
+            setPremiumSmsPermission(mTracker.getAppPackageName(), newSmsPermission);
+        }
+
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            Rlog.d(TAG, "dialog dismissed: don't send SMS");
+            Message msg = obtainMessage(EVENT_STOP_SENDING, mTracker);
+            msg.arg1 = mConfirmationType;
+            sendMessage(msg);
+        }
+
+        @Override
+        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+            Rlog.d(TAG, "remember this choice: " + isChecked);
+            mRememberChoice = isChecked;
+            if (isChecked) {
+                mPositiveButton.setText(R.string.sms_short_code_confirm_always_allow);
+                mNegativeButton.setText(R.string.sms_short_code_confirm_never_allow);
+                if (mRememberUndoInstruction != null) {
+                    mRememberUndoInstruction.
+                            setText(R.string.sms_short_code_remember_undo_instruction);
+                    mRememberUndoInstruction.setPadding(0,0,0,32);
+                }
+            } else {
+                mPositiveButton.setText(R.string.sms_short_code_confirm_allow);
+                mNegativeButton.setText(R.string.sms_short_code_confirm_deny);
+                if (mRememberUndoInstruction != null) {
+                    mRememberUndoInstruction.setText("");
+                    mRememberUndoInstruction.setPadding(0,0,0,0);
+                }
+            }
+        }
+    }
+
+    public boolean isIms() {
+        if (mImsSMSDispatcher != null) {
+            return mImsSMSDispatcher.isIms();
+        } else {
+            Rlog.e(TAG, mImsSMSDispatcher + " is null");
+            return false;
+        }
+    }
+
+    public String getImsSmsFormat() {
+        if (mImsSMSDispatcher != null) {
+            return mImsSMSDispatcher.getImsSmsFormat();
+        } else {
+            Rlog.e(TAG, mImsSMSDispatcher + " is null");
+            return null;
+        }
+    }
+
+    private String getMultipartMessageText(ArrayList<String> parts) {
+        final StringBuilder sb = new StringBuilder();
+        for (String part : parts) {
+            if (part != null) {
+                sb.append(part);
+            }
+        }
+        return sb.toString();
+    }
+
+    protected String getCarrierAppPackageName() {
+        UiccCard card = UiccController.getInstance().getUiccCard(mPhone.getPhoneId());
+        if (card == null) {
+            return null;
+        }
+
+        List<String> carrierPackages = card.getCarrierPackageNamesForIntent(
+            mContext.getPackageManager(), new Intent(CarrierMessagingService.SERVICE_INTERFACE));
+        if (carrierPackages != null && carrierPackages.size() == 1) {
+            return carrierPackages.get(0);
+        }
+        // If there is no carrier package which implements CarrierMessagingService, then lookup if
+        // for a carrierImsPackage that implements CarrierMessagingService.
+        return CarrierSmsUtils.getCarrierImsPackageForIntent(mContext, mPhone,
+                new Intent(CarrierMessagingService.SERVICE_INTERFACE));
+    }
+
+    protected int getSubId() {
+        return SubscriptionController.getInstance().getSubIdUsingPhoneId(mPhone.getPhoneId());
+    }
+
+    private void checkCallerIsPhoneOrCarrierApp() {
+        int uid = Binder.getCallingUid();
+        int appId = UserHandle.getAppId(uid);
+        if (appId == Process.PHONE_UID || uid == 0) {
+            return;
+        }
+        try {
+            PackageManager pm = mContext.getPackageManager();
+            ApplicationInfo ai = pm.getApplicationInfo(getCarrierAppPackageName(), 0);
+            if (!UserHandle.isSameApp(ai.uid, Binder.getCallingUid())) {
+                throw new SecurityException("Caller is not phone or carrier app!");
+            }
+        } catch (PackageManager.NameNotFoundException re) {
+            throw new SecurityException("Caller is not phone or carrier app!");
+        }
+    }
+}
diff --git a/com/android/internal/telephony/ServiceStateTracker.java b/com/android/internal/telephony/ServiceStateTracker.java
new file mode 100644
index 0000000..b379440
--- /dev/null
+++ b/com/android/internal/telephony/ServiceStateTracker.java
@@ -0,0 +1,5020 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import static android.provider.Telephony.ServiceStateTable.getContentValuesForServiceState;
+import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionId;
+
+import static com.android.internal.telephony.CarrierActionAgent.CARRIER_ACTION_SET_RADIO_ENABLED;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.hardware.radio.V1_0.CellInfoType;
+import android.hardware.radio.V1_0.DataRegStateResult;
+import android.hardware.radio.V1_0.RegState;
+import android.hardware.radio.V1_0.VoiceRegStateResult;
+import android.os.AsyncResult;
+import android.os.BaseBundle;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellIdentityGsm;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellIdentityWcdma;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoWcdma;
+import android.telephony.CellLocation;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaCellLocation;
+import android.telephony.gsm.GsmCellLocation;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.LocalLog;
+import android.util.Pair;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.cdma.EriInfo;
+import com.android.internal.telephony.dataconnection.DcTracker;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.RuimRecords;
+import com.android.internal.telephony.uicc.SIMRecords;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.util.NotificationChannelController;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * {@hide}
+ */
+public class ServiceStateTracker extends Handler {
+    private static final String LOG_TAG = "SST";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;  // STOPSHIP if true
+
+    private static final String PROP_FORCE_ROAMING = "telephony.test.forceRoaming";
+
+    private CommandsInterface mCi;
+    private UiccController mUiccController = null;
+    private UiccCardApplication mUiccApplcation = null;
+    private IccRecords mIccRecords = null;
+
+    private boolean mVoiceCapable;
+
+    public ServiceState mSS;
+    private ServiceState mNewSS;
+
+    private static final long LAST_CELL_INFO_LIST_MAX_AGE_MS = 2000;
+    private long mLastCellInfoListTime;
+    private List<CellInfo> mLastCellInfoList = null;
+
+    private SignalStrength mSignalStrength;
+
+    // TODO - this should not be public, right now used externally GsmConnetion.
+    public RestrictedState mRestrictedState;
+
+    /**
+     * A unique identifier to track requests associated with a poll
+     * and ignore stale responses.  The value is a count-down of
+     * expected responses in this pollingContext.
+     */
+    private int[] mPollingContext;
+    private boolean mDesiredPowerState;
+
+    /**
+     * By default, strength polling is enabled.  However, if we're
+     * getting unsolicited signal strength updates from the radio, set
+     * value to true and don't bother polling any more.
+     */
+    private boolean mDontPollSignalStrength = false;
+
+    private RegistrantList mVoiceRoamingOnRegistrants = new RegistrantList();
+    private RegistrantList mVoiceRoamingOffRegistrants = new RegistrantList();
+    private RegistrantList mDataRoamingOnRegistrants = new RegistrantList();
+    private RegistrantList mDataRoamingOffRegistrants = new RegistrantList();
+    protected RegistrantList mAttachedRegistrants = new RegistrantList();
+    protected RegistrantList mDetachedRegistrants = new RegistrantList();
+    private RegistrantList mDataRegStateOrRatChangedRegistrants = new RegistrantList();
+    private RegistrantList mNetworkAttachedRegistrants = new RegistrantList();
+    private RegistrantList mNetworkDetachedRegistrants = new RegistrantList();
+    private RegistrantList mPsRestrictEnabledRegistrants = new RegistrantList();
+    private RegistrantList mPsRestrictDisabledRegistrants = new RegistrantList();
+
+    /* Radio power off pending flag and tag counter */
+    private boolean mPendingRadioPowerOffAfterDataOff = false;
+    private int mPendingRadioPowerOffAfterDataOffTag = 0;
+
+    /** Signal strength poll rate. */
+    private static final int POLL_PERIOD_MILLIS = 20 * 1000;
+
+    /** Waiting period before recheck gprs and voice registration. */
+    public static final int DEFAULT_GPRS_CHECK_PERIOD_MILLIS = 60 * 1000;
+
+    /** GSM events */
+    protected static final int EVENT_RADIO_STATE_CHANGED               = 1;
+    protected static final int EVENT_NETWORK_STATE_CHANGED             = 2;
+    protected static final int EVENT_GET_SIGNAL_STRENGTH               = 3;
+    protected static final int EVENT_POLL_STATE_REGISTRATION           = 4;
+    protected static final int EVENT_POLL_STATE_GPRS                   = 5;
+    protected static final int EVENT_POLL_STATE_OPERATOR               = 6;
+    protected static final int EVENT_POLL_SIGNAL_STRENGTH              = 10;
+    protected static final int EVENT_NITZ_TIME                         = 11;
+    protected static final int EVENT_SIGNAL_STRENGTH_UPDATE            = 12;
+    protected static final int EVENT_POLL_STATE_NETWORK_SELECTION_MODE = 14;
+    protected static final int EVENT_GET_LOC_DONE                      = 15;
+    protected static final int EVENT_SIM_RECORDS_LOADED                = 16;
+    protected static final int EVENT_SIM_READY                         = 17;
+    protected static final int EVENT_LOCATION_UPDATES_ENABLED          = 18;
+    protected static final int EVENT_GET_PREFERRED_NETWORK_TYPE        = 19;
+    protected static final int EVENT_SET_PREFERRED_NETWORK_TYPE        = 20;
+    protected static final int EVENT_RESET_PREFERRED_NETWORK_TYPE      = 21;
+    protected static final int EVENT_CHECK_REPORT_GPRS                 = 22;
+    protected static final int EVENT_RESTRICTED_STATE_CHANGED          = 23;
+
+    /** CDMA events */
+    protected static final int EVENT_RUIM_READY                        = 26;
+    protected static final int EVENT_RUIM_RECORDS_LOADED               = 27;
+    protected static final int EVENT_POLL_STATE_CDMA_SUBSCRIPTION      = 34;
+    protected static final int EVENT_NV_READY                          = 35;
+    protected static final int EVENT_ERI_FILE_LOADED                   = 36;
+    protected static final int EVENT_OTA_PROVISION_STATUS_CHANGE       = 37;
+    protected static final int EVENT_SET_RADIO_POWER_OFF               = 38;
+    protected static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED  = 39;
+    protected static final int EVENT_CDMA_PRL_VERSION_CHANGED          = 40;
+
+    protected static final int EVENT_RADIO_ON                          = 41;
+    public    static final int EVENT_ICC_CHANGED                       = 42;
+    protected static final int EVENT_GET_CELL_INFO_LIST                = 43;
+    protected static final int EVENT_UNSOL_CELL_INFO_LIST              = 44;
+    protected static final int EVENT_CHANGE_IMS_STATE                  = 45;
+    protected static final int EVENT_IMS_STATE_CHANGED                 = 46;
+    protected static final int EVENT_IMS_STATE_DONE                    = 47;
+    protected static final int EVENT_IMS_CAPABILITY_CHANGED            = 48;
+    protected static final int EVENT_ALL_DATA_DISCONNECTED             = 49;
+    protected static final int EVENT_PHONE_TYPE_SWITCHED               = 50;
+    protected static final int EVENT_RADIO_POWER_FROM_CARRIER          = 51;
+
+    protected static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
+
+    /**
+     * List of ISO codes for countries that can have an offset of
+     * GMT+0 when not in daylight savings time.  This ignores some
+     * small places such as the Canary Islands (Spain) and
+     * Danmarkshavn (Denmark).  The list must be sorted by code.
+    */
+    protected static final String[] GMT_COUNTRY_CODES = {
+        "bf", // Burkina Faso
+        "ci", // Cote d'Ivoire
+        "eh", // Western Sahara
+        "fo", // Faroe Islands, Denmark
+        "gb", // United Kingdom of Great Britain and Northern Ireland
+        "gh", // Ghana
+        "gm", // Gambia
+        "gn", // Guinea
+        "gw", // Guinea Bissau
+        "ie", // Ireland
+        "lr", // Liberia
+        "is", // Iceland
+        "ma", // Morocco
+        "ml", // Mali
+        "mr", // Mauritania
+        "pt", // Portugal
+        "sl", // Sierra Leone
+        "sn", // Senegal
+        "st", // Sao Tome and Principe
+        "tg", // Togo
+    };
+
+    private class CellInfoResult {
+        List<CellInfo> list;
+        Object lockObj = new Object();
+    }
+
+    /** Reason for registration denial. */
+    protected static final String REGISTRATION_DENIED_GEN  = "General";
+    protected static final String REGISTRATION_DENIED_AUTH = "Authentication Failure";
+
+    private boolean mImsRegistrationOnOff = false;
+    private boolean mAlarmSwitch = false;
+    /** Radio is disabled by carrier. Radio power will not be override if this field is set */
+    private boolean mRadioDisabledByCarrier = false;
+    private PendingIntent mRadioOffIntent = null;
+    private static final String ACTION_RADIO_OFF = "android.intent.action.ACTION_RADIO_OFF";
+    private boolean mPowerOffDelayNeed = true;
+    private boolean mDeviceShuttingDown = false;
+    /** Keep track of SPN display rules, so we only broadcast intent if something changes. */
+    private boolean mSpnUpdatePending = false;
+    private String mCurSpn = null;
+    private String mCurDataSpn = null;
+    private String mCurPlmn = null;
+    private boolean mCurShowPlmn = false;
+    private boolean mCurShowSpn = false;
+    private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+    private boolean mImsRegistered = false;
+
+    private SubscriptionManager mSubscriptionManager;
+    private SubscriptionController mSubscriptionController;
+    private final SstSubscriptionsChangedListener mOnSubscriptionsChangedListener =
+        new SstSubscriptionsChangedListener();
+
+
+    private final RatRatcheter mRatRatcheter;
+
+    private final LocalLog mRoamingLog = new LocalLog(10);
+    private final LocalLog mAttachLog = new LocalLog(10);
+    private final LocalLog mPhoneTypeLog = new LocalLog(10);
+    private final LocalLog mRatLog = new LocalLog(20);
+    private final LocalLog mRadioPowerLog = new LocalLog(20);
+    private final LocalLog mTimeLog = new LocalLog(15);
+    private final LocalLog mTimeZoneLog = new LocalLog(15);
+
+    private class SstSubscriptionsChangedListener extends OnSubscriptionsChangedListener {
+        public final AtomicInteger mPreviousSubId =
+                new AtomicInteger(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+        /**
+         * Callback invoked when there is any change to any SubscriptionInfo. Typically
+         * this method would invoke {@link SubscriptionManager#getActiveSubscriptionInfoList}
+         */
+        @Override
+        public void onSubscriptionsChanged() {
+            if (DBG) log("SubscriptionListener.onSubscriptionInfoChanged");
+            // Set the network type, in case the radio does not restore it.
+            int subId = mPhone.getSubId();
+            if (mPreviousSubId.getAndSet(subId) != subId) {
+                if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                    Context context = mPhone.getContext();
+
+                    mPhone.notifyPhoneStateChanged();
+                    mPhone.notifyCallForwardingIndicator();
+
+                    boolean restoreSelection = !context.getResources().getBoolean(
+                            com.android.internal.R.bool.skip_restoring_network_selection);
+                    mPhone.sendSubscriptionSettings(restoreSelection);
+
+                    mPhone.setSystemProperty(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE,
+                            ServiceState.rilRadioTechnologyToString(
+                                    mSS.getRilDataRadioTechnology()));
+
+                    if (mSpnUpdatePending) {
+                        mSubscriptionController.setPlmnSpn(mPhone.getPhoneId(), mCurShowPlmn,
+                                mCurPlmn, mCurShowSpn, mCurSpn);
+                        mSpnUpdatePending = false;
+                    }
+
+                    // Remove old network selection sharedPreferences since SP key names are now
+                    // changed to include subId. This will be done only once when upgrading from an
+                    // older build that did not include subId in the names.
+                    SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(
+                            context);
+                    String oldNetworkSelection = sp.getString(
+                            Phone.NETWORK_SELECTION_KEY, "");
+                    String oldNetworkSelectionName = sp.getString(
+                            Phone.NETWORK_SELECTION_NAME_KEY, "");
+                    String oldNetworkSelectionShort = sp.getString(
+                            Phone.NETWORK_SELECTION_SHORT_KEY, "");
+                    if (!TextUtils.isEmpty(oldNetworkSelection) ||
+                            !TextUtils.isEmpty(oldNetworkSelectionName) ||
+                            !TextUtils.isEmpty(oldNetworkSelectionShort)) {
+                        SharedPreferences.Editor editor = sp.edit();
+                        editor.putString(Phone.NETWORK_SELECTION_KEY + subId,
+                                oldNetworkSelection);
+                        editor.putString(Phone.NETWORK_SELECTION_NAME_KEY + subId,
+                                oldNetworkSelectionName);
+                        editor.putString(Phone.NETWORK_SELECTION_SHORT_KEY + subId,
+                                oldNetworkSelectionShort);
+                        editor.remove(Phone.NETWORK_SELECTION_KEY);
+                        editor.remove(Phone.NETWORK_SELECTION_NAME_KEY);
+                        editor.remove(Phone.NETWORK_SELECTION_SHORT_KEY);
+                        editor.commit();
+                    }
+
+                    // Once sub id becomes valid, we need to update the service provider name
+                    // displayed on the UI again. The old SPN update intents sent to
+                    // MobileSignalController earlier were actually ignored due to invalid sub id.
+                    updateSpnDisplay();
+                }
+                // update voicemail count and notify message waiting changed
+                mPhone.updateVoiceMail();
+            }
+        }
+    };
+
+    //Common
+    private GsmCdmaPhone mPhone;
+    public CellLocation mCellLoc;
+    private CellLocation mNewCellLoc;
+    public static final int MS_PER_HOUR = 60 * 60 * 1000;
+    /* Time stamp after 19 January 2038 is not supported under 32 bit */
+    private static final int MAX_NITZ_YEAR = 2037;
+    /**
+     * Sometimes we get the NITZ time before we know what country we
+     * are in. Keep the time zone information from the NITZ string so
+     * we can fix the time zone once know the country.
+     */
+    private boolean mNeedFixZoneAfterNitz = false;
+    private int mZoneOffset;
+    private boolean mZoneDst;
+    private long mZoneTime;
+    private boolean mGotCountryCode = false;
+    private String mSavedTimeZone;
+    private long mSavedTime;
+    private long mSavedAtTime;
+    /** Wake lock used while setting time of day. */
+    private PowerManager.WakeLock mWakeLock;
+    public static final String WAKELOCK_TAG = "ServiceStateTracker";
+    private ContentResolver mCr;
+    private ContentObserver mAutoTimeObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            Rlog.i(LOG_TAG, "Auto time state changed");
+            revertToNitzTime();
+        }
+    };
+
+    private ContentObserver mAutoTimeZoneObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            Rlog.i(LOG_TAG, "Auto time zone state changed");
+            revertToNitzTimeZone();
+        }
+    };
+
+    //GSM
+    private int mPreferredNetworkType;
+    private int mMaxDataCalls = 1;
+    private int mNewMaxDataCalls = 1;
+    private int mReasonDataDenied = -1;
+    private int mNewReasonDataDenied = -1;
+
+    /**
+     * The code of the rejection cause that is sent by network when the CS
+     * registration is rejected. It should be shown to the user as a notification.
+     */
+    private int mRejectCode;
+    private int mNewRejectCode;
+
+    /**
+     * GSM roaming status solely based on TS 27.007 7.2 CREG. Only used by
+     * handlePollStateResult to store CREG roaming result.
+     */
+    private boolean mGsmRoaming = false;
+    /**
+     * Data roaming status solely based on TS 27.007 10.1.19 CGREG. Only used by
+     * handlePollStateResult to store CGREG roaming result.
+     */
+    private boolean mDataRoaming = false;
+    /**
+     * Mark when service state is in emergency call only mode
+     */
+    private boolean mEmergencyOnly = false;
+    /** Boolean is true is setTimeFromNITZString was called */
+    private boolean mNitzUpdatedTime = false;
+    /** Started the recheck process after finding gprs should registered but not. */
+    private boolean mStartedGprsRegCheck;
+    /** Already sent the event-log for no gprs register. */
+    private boolean mReportedGprsNoReg;
+
+    private CarrierServiceStateTracker mCSST;
+    /**
+     * The Notification object given to the NotificationManager.
+     */
+    private Notification mNotification;
+    /** Notification type. */
+    public static final int PS_ENABLED = 1001;            // Access Control blocks data service
+    public static final int PS_DISABLED = 1002;           // Access Control enables data service
+    public static final int CS_ENABLED = 1003;            // Access Control blocks all voice/sms service
+    public static final int CS_DISABLED = 1004;           // Access Control enables all voice/sms service
+    public static final int CS_NORMAL_ENABLED = 1005;     // Access Control blocks normal voice/sms service
+    public static final int CS_EMERGENCY_ENABLED = 1006;  // Access Control blocks emergency call service
+    public static final int CS_REJECT_CAUSE_ENABLED = 2001;     // Notify MM rejection cause
+    public static final int CS_REJECT_CAUSE_DISABLED = 2002;    // Cancel MM rejection cause
+    /** Notification id. */
+    public static final int PS_NOTIFICATION = 888;  // Id to update and cancel PS restricted
+    public static final int CS_NOTIFICATION = 999;  // Id to update and cancel CS restricted
+    public static final int CS_REJECT_CAUSE_NOTIFICATION = 111; // Id to update and cancel MM
+                                                                // rejection cause
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(
+                    CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                updateLteEarfcnLists();
+                return;
+            }
+
+            if (!mPhone.isPhoneTypeGsm()) {
+                loge("Ignoring intent " + intent + " received on CDMA phone");
+                return;
+            }
+
+            if (intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED)) {
+                // update emergency string whenever locale changed
+                updateSpnDisplay();
+            } else if (intent.getAction().equals(ACTION_RADIO_OFF)) {
+                mAlarmSwitch = false;
+                DcTracker dcTracker = mPhone.mDcTracker;
+                powerOffRadioSafely(dcTracker);
+            }
+        }
+    };
+
+    //CDMA
+    // Min values used to by getOtasp()
+    public static final String UNACTIVATED_MIN2_VALUE = "000000";
+    public static final String UNACTIVATED_MIN_VALUE = "1111110111";
+    // Current Otasp value
+    private int mCurrentOtaspMode = TelephonyManager.OTASP_UNINITIALIZED;
+    /** if time between NITZ updates is less than mNitzUpdateSpacing the update may be ignored. */
+    public static final int NITZ_UPDATE_SPACING_DEFAULT = 1000 * 60 * 10;
+    private int mNitzUpdateSpacing = SystemProperties.getInt("ro.nitz_update_spacing",
+            NITZ_UPDATE_SPACING_DEFAULT);
+    /** If mNitzUpdateSpacing hasn't been exceeded but update is > mNitzUpdate do the update */
+    public static final int NITZ_UPDATE_DIFF_DEFAULT = 2000;
+    private int mNitzUpdateDiff = SystemProperties.getInt("ro.nitz_update_diff",
+            NITZ_UPDATE_DIFF_DEFAULT);
+    private int mRoamingIndicator;
+    private boolean mIsInPrl;
+    private int mDefaultRoamingIndicator;
+    /**
+     * Initially assume no data connection.
+     */
+    private int mRegistrationState = -1;
+    private RegistrantList mCdmaForSubscriptionInfoReadyRegistrants = new RegistrantList();
+    private String mMdn;
+    private int mHomeSystemId[] = null;
+    private int mHomeNetworkId[] = null;
+    private String mMin;
+    private String mPrlVersion;
+    private boolean mIsMinInfoReady = false;
+    private boolean mIsEriTextLoaded = false;
+    private boolean mIsSubscriptionFromRuim = false;
+    private CdmaSubscriptionSourceManager mCdmaSSM;
+    public static final String INVALID_MCC = "000";
+    public static final String DEFAULT_MNC = "00";
+    private HbpcdUtils mHbpcdUtils = null;
+    /* Used only for debugging purposes. */
+    private String mRegistrationDeniedReason;
+    private String mCurrentCarrier = null;
+
+    /* list of LTE EARFCNs (E-UTRA Absolute Radio Frequency Channel Number,
+     * Reference: 3GPP TS 36.104 5.4.3)
+     * inclusive ranges for which the lte rsrp boost is applied */
+    private ArrayList<Pair<Integer, Integer>> mEarfcnPairListForRsrpBoost = null;
+
+    private int mLteRsrpBoost = 0; // offset which is reduced from the rsrp threshold
+                                   // while calculating signal strength level.
+    private final Object mLteRsrpBoostLock = new Object();
+    private static final int INVALID_LTE_EARFCN = -1;
+
+    public ServiceStateTracker(GsmCdmaPhone phone, CommandsInterface ci) {
+        mPhone = phone;
+        mCi = ci;
+
+        mRatRatcheter = new RatRatcheter(mPhone);
+        mVoiceCapable = mPhone.getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_voice_capable);
+        mUiccController = UiccController.getInstance();
+
+        mUiccController.registerForIccChanged(this, EVENT_ICC_CHANGED, null);
+        mCi.setOnSignalStrengthUpdate(this, EVENT_SIGNAL_STRENGTH_UPDATE, null);
+        mCi.registerForCellInfoList(this, EVENT_UNSOL_CELL_INFO_LIST, null);
+
+        mSubscriptionController = SubscriptionController.getInstance();
+        mSubscriptionManager = SubscriptionManager.from(phone.getContext());
+        mSubscriptionManager
+                .addOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
+        mRestrictedState = new RestrictedState();
+
+        mCi.registerForImsNetworkStateChanged(this, EVENT_IMS_STATE_CHANGED, null);
+
+        PowerManager powerManager =
+                (PowerManager)phone.getContext().getSystemService(Context.POWER_SERVICE);
+        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
+
+        mCi.registerForRadioStateChanged(this, EVENT_RADIO_STATE_CHANGED, null);
+        mCi.registerForNetworkStateChanged(this, EVENT_NETWORK_STATE_CHANGED, null);
+        mCi.setOnNITZTime(this, EVENT_NITZ_TIME, null);
+
+        mCr = phone.getContext().getContentResolver();
+        // system setting property AIRPLANE_MODE_ON is set in Settings.
+        int airplaneMode = Settings.Global.getInt(mCr, Settings.Global.AIRPLANE_MODE_ON, 0);
+        int enableCellularOnBoot = Settings.Global.getInt(mCr,
+                Settings.Global.ENABLE_CELLULAR_ON_BOOT, 1);
+        mDesiredPowerState = (enableCellularOnBoot > 0) && ! (airplaneMode > 0);
+        mRadioPowerLog.log("init : airplane mode = " + airplaneMode + " enableCellularOnBoot = " +
+                enableCellularOnBoot);
+
+
+        mCr.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
+                mAutoTimeObserver);
+        mCr.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
+                mAutoTimeZoneObserver);
+        setSignalStrengthDefaultValues();
+        mPhone.getCarrierActionAgent().registerForCarrierAction(CARRIER_ACTION_SET_RADIO_ENABLED,
+                this, EVENT_RADIO_POWER_FROM_CARRIER, null, false);
+
+        // Monitor locale change
+        Context context = mPhone.getContext();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+        context.registerReceiver(mIntentReceiver, filter);
+        filter = new IntentFilter();
+        filter.addAction(ACTION_RADIO_OFF);
+        context.registerReceiver(mIntentReceiver, filter);
+        filter = new IntentFilter();
+        filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        context.registerReceiver(mIntentReceiver, filter);
+
+        mPhone.notifyOtaspChanged(TelephonyManager.OTASP_UNINITIALIZED);
+
+        mCi.setOnRestrictedStateChanged(this, EVENT_RESTRICTED_STATE_CHANGED, null);
+        updatePhoneType();
+
+        mCSST = new CarrierServiceStateTracker(phone, this);
+
+        registerForNetworkAttached(mCSST,
+                CarrierServiceStateTracker.CARRIER_EVENT_VOICE_REGISTRATION, null);
+        registerForNetworkDetached(mCSST,
+                CarrierServiceStateTracker.CARRIER_EVENT_VOICE_DEREGISTRATION, null);
+        registerForDataConnectionAttached(mCSST,
+                CarrierServiceStateTracker.CARRIER_EVENT_DATA_REGISTRATION, null);
+        registerForDataConnectionDetached(mCSST,
+                CarrierServiceStateTracker.CARRIER_EVENT_DATA_DEREGISTRATION, null);
+    }
+
+    @VisibleForTesting
+    public void updatePhoneType() {
+        // If we are previously voice roaming, we need to notify that roaming status changed before
+        // we change back to non-roaming.
+        if (mSS != null && mSS.getVoiceRoaming()) {
+            mVoiceRoamingOffRegistrants.notifyRegistrants();
+        }
+
+        // If we are previously data roaming, we need to notify that roaming status changed before
+        // we change back to non-roaming.
+        if (mSS != null && mSS.getDataRoaming()) {
+            mDataRoamingOffRegistrants.notifyRegistrants();
+        }
+
+        // If we are previously in service, we need to notify that we are out of service now.
+        if (mSS != null && mSS.getDataRegState() == ServiceState.STATE_IN_SERVICE) {
+            mDetachedRegistrants.notifyRegistrants();
+        }
+
+        mSS = new ServiceState();
+        mNewSS = new ServiceState();
+        mLastCellInfoListTime = 0;
+        mLastCellInfoList = null;
+        mSignalStrength = new SignalStrength();
+        mStartedGprsRegCheck = false;
+        mReportedGprsNoReg = false;
+        mMdn = null;
+        mMin = null;
+        mPrlVersion = null;
+        mIsMinInfoReady = false;
+        mNitzUpdatedTime = false;
+
+        //cancel any pending pollstate request on voice tech switching
+        cancelPollState();
+
+        if (mPhone.isPhoneTypeGsm()) {
+            //clear CDMA registrations first
+            if (mCdmaSSM != null) {
+                mCdmaSSM.dispose(this);
+            }
+
+            mCi.unregisterForCdmaPrlChanged(this);
+            mPhone.unregisterForEriFileLoaded(this);
+            mCi.unregisterForCdmaOtaProvision(this);
+            mPhone.unregisterForSimRecordsLoaded(this);
+
+            mCellLoc = new GsmCellLocation();
+            mNewCellLoc = new GsmCellLocation();
+        } else {
+            if (mPhone.isPhoneTypeCdmaLte()) {
+                mPhone.registerForSimRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
+            }
+            mCellLoc = new CdmaCellLocation();
+            mNewCellLoc = new CdmaCellLocation();
+            mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(mPhone.getContext(), mCi, this,
+                    EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null);
+            mIsSubscriptionFromRuim = (mCdmaSSM.getCdmaSubscriptionSource() ==
+                    CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM);
+
+            mCi.registerForCdmaPrlChanged(this, EVENT_CDMA_PRL_VERSION_CHANGED, null);
+            mPhone.registerForEriFileLoaded(this, EVENT_ERI_FILE_LOADED, null);
+            mCi.registerForCdmaOtaProvision(this, EVENT_OTA_PROVISION_STATUS_CHANGE, null);
+
+            mHbpcdUtils = new HbpcdUtils(mPhone.getContext());
+            // update OTASP state in case previously set by another service
+            updateOtaspState();
+        }
+
+        // This should be done after the technology specific initializations above since it relies
+        // on fields like mIsSubscriptionFromRuim (which is updated above)
+        onUpdateIccAvailability();
+
+        mPhone.setSystemProperty(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE,
+                ServiceState.rilRadioTechnologyToString(ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN));
+        // Query signal strength from the modem after service tracker is created (i.e. boot up,
+        // switching between GSM and CDMA phone), because the unsolicited signal strength
+        // information might come late or even never come. This will get the accurate signal
+        // strength information displayed on the UI.
+        mCi.getSignalStrength(obtainMessage(EVENT_GET_SIGNAL_STRENGTH));
+        sendMessage(obtainMessage(EVENT_PHONE_TYPE_SWITCHED));
+
+        logPhoneTypeChange();
+
+        // Tell everybody that the registration state and RAT have changed.
+        notifyDataRegStateRilRadioTechnologyChanged();
+    }
+
+    @VisibleForTesting
+    public void requestShutdown() {
+        if (mDeviceShuttingDown == true) return;
+        mDeviceShuttingDown = true;
+        mDesiredPowerState = false;
+        setPowerStateToDesired();
+    }
+
+    public void dispose() {
+        mCi.unSetOnSignalStrengthUpdate(this);
+        mUiccController.unregisterForIccChanged(this);
+        mCi.unregisterForCellInfoList(this);
+        mSubscriptionManager
+            .removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
+        mCi.unregisterForImsNetworkStateChanged(this);
+        mPhone.getCarrierActionAgent().unregisterForCarrierAction(this,
+                CARRIER_ACTION_SET_RADIO_ENABLED);
+    }
+
+    public boolean getDesiredPowerState() {
+        return mDesiredPowerState;
+    }
+    public boolean getPowerStateFromCarrier() { return !mRadioDisabledByCarrier; }
+
+    private SignalStrength mLastSignalStrength = null;
+    protected boolean notifySignalStrength() {
+        boolean notified = false;
+        if (!mSignalStrength.equals(mLastSignalStrength)) {
+            try {
+                mPhone.notifySignalStrength();
+                notified = true;
+                mLastSignalStrength = mSignalStrength;
+            } catch (NullPointerException ex) {
+                loge("updateSignalStrength() Phone already destroyed: " + ex
+                        + "SignalStrength not notified");
+            }
+        }
+        return notified;
+    }
+
+    /**
+     * Notify all mDataConnectionRatChangeRegistrants using an
+     * AsyncResult in msg.obj where AsyncResult#result contains the
+     * new RAT as an Integer Object.
+     */
+    protected void notifyDataRegStateRilRadioTechnologyChanged() {
+        int rat = mSS.getRilDataRadioTechnology();
+        int drs = mSS.getDataRegState();
+        if (DBG) log("notifyDataRegStateRilRadioTechnologyChanged: drs=" + drs + " rat=" + rat);
+
+        mPhone.setSystemProperty(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE,
+                ServiceState.rilRadioTechnologyToString(rat));
+        mDataRegStateOrRatChangedRegistrants.notifyResult(new Pair<Integer, Integer>(drs, rat));
+    }
+
+    /**
+     * Some operators have been known to report registration failure
+     * data only devices, to fix that use DataRegState.
+     */
+    protected void useDataRegStateForDataOnlyDevices() {
+        if (mVoiceCapable == false) {
+            if (DBG) {
+                log("useDataRegStateForDataOnlyDevice: VoiceRegState=" + mNewSS.getVoiceRegState()
+                    + " DataRegState=" + mNewSS.getDataRegState());
+            }
+            // TODO: Consider not lying and instead have callers know the difference.
+            mNewSS.setVoiceRegState(mNewSS.getDataRegState());
+        }
+    }
+
+    protected void updatePhoneObject() {
+        if (mPhone.getContext().getResources().
+                getBoolean(com.android.internal.R.bool.config_switch_phone_on_voice_reg_state_change)) {
+            // If the phone is not registered on a network, no need to update.
+            boolean isRegistered = mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE ||
+                    mSS.getVoiceRegState() == ServiceState.STATE_EMERGENCY_ONLY;
+            if (!isRegistered) {
+                log("updatePhoneObject: Ignore update");
+                return;
+            }
+            mPhone.updatePhoneObject(mSS.getRilVoiceRadioTechnology());
+        }
+    }
+
+    /**
+     * Registration point for combined roaming on of mobile voice
+     * combined roaming is true when roaming is true and ONS differs SPN
+     *
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForVoiceRoamingOn(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mVoiceRoamingOnRegistrants.add(r);
+
+        if (mSS.getVoiceRoaming()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForVoiceRoamingOn(Handler h) {
+        mVoiceRoamingOnRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for roaming off of mobile voice
+     * combined roaming is true when roaming is true and ONS differs SPN
+     *
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForVoiceRoamingOff(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mVoiceRoamingOffRegistrants.add(r);
+
+        if (!mSS.getVoiceRoaming()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForVoiceRoamingOff(Handler h) {
+        mVoiceRoamingOffRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for combined roaming on of mobile data
+     * combined roaming is true when roaming is true and ONS differs SPN
+     *
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForDataRoamingOn(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mDataRoamingOnRegistrants.add(r);
+
+        if (mSS.getDataRoaming()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForDataRoamingOn(Handler h) {
+        mDataRoamingOnRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for roaming off of mobile data
+     * combined roaming is true when roaming is true and ONS differs SPN
+     *
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     * @param notifyNow notify upon registration if data roaming is off
+     */
+    public void registerForDataRoamingOff(Handler h, int what, Object obj, boolean notifyNow) {
+        Registrant r = new Registrant(h, what, obj);
+        mDataRoamingOffRegistrants.add(r);
+
+        if (notifyNow && !mSS.getDataRoaming()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForDataRoamingOff(Handler h) {
+        mDataRoamingOffRegistrants.remove(h);
+    }
+
+    /**
+     * Re-register network by toggling preferred network type.
+     * This is a work-around to deregister and register network since there is
+     * no ril api to set COPS=2 (deregister) only.
+     *
+     * @param onComplete is dispatched when this is complete.  it will be
+     * an AsyncResult, and onComplete.obj.exception will be non-null
+     * on failure.
+     */
+    public void reRegisterNetwork(Message onComplete) {
+        mCi.getPreferredNetworkType(
+                obtainMessage(EVENT_GET_PREFERRED_NETWORK_TYPE, onComplete));
+    }
+
+    public void
+    setRadioPower(boolean power) {
+        mDesiredPowerState = power;
+
+        setPowerStateToDesired();
+    }
+
+    /**
+     * Radio power set from carrier action. if set to false means carrier desire to turn radio off
+     * and radio wont be re-enabled unless carrier explicitly turn it back on.
+     * @param enable indicate if radio power is enabled or disabled from carrier action.
+     */
+    public void setRadioPowerFromCarrier(boolean enable) {
+        mRadioDisabledByCarrier = !enable;
+        setPowerStateToDesired();
+    }
+
+    /**
+     * These two flags manage the behavior of the cell lock -- the
+     * lock should be held if either flag is true.  The intention is
+     * to allow temporary acquisition of the lock to get a single
+     * update.  Such a lock grab and release can thus be made to not
+     * interfere with more permanent lock holds -- in other words, the
+     * lock will only be released if both flags are false, and so
+     * releases by temporary users will only affect the lock state if
+     * there is no continuous user.
+     */
+    private boolean mWantContinuousLocationUpdates;
+    private boolean mWantSingleLocationUpdate;
+
+    public void enableSingleLocationUpdate() {
+        if (mWantSingleLocationUpdate || mWantContinuousLocationUpdates) return;
+        mWantSingleLocationUpdate = true;
+        mCi.setLocationUpdates(true, obtainMessage(EVENT_LOCATION_UPDATES_ENABLED));
+    }
+
+    public void enableLocationUpdates() {
+        if (mWantSingleLocationUpdate || mWantContinuousLocationUpdates) return;
+        mWantContinuousLocationUpdates = true;
+        mCi.setLocationUpdates(true, obtainMessage(EVENT_LOCATION_UPDATES_ENABLED));
+    }
+
+    protected void disableSingleLocationUpdate() {
+        mWantSingleLocationUpdate = false;
+        if (!mWantSingleLocationUpdate && !mWantContinuousLocationUpdates) {
+            mCi.setLocationUpdates(false, null);
+        }
+    }
+
+    public void disableLocationUpdates() {
+        mWantContinuousLocationUpdates = false;
+        if (!mWantSingleLocationUpdate && !mWantContinuousLocationUpdates) {
+            mCi.setLocationUpdates(false, null);
+        }
+    }
+
+    private void processCellLocationInfo(CellLocation cellLocation,
+                                         VoiceRegStateResult voiceRegStateResult) {
+        if (mPhone.isPhoneTypeGsm()) {
+            int psc = -1;
+            int cid = -1;
+            int lac = -1;
+            switch(voiceRegStateResult.cellIdentity.cellInfoType) {
+                case CellInfoType.GSM: {
+                    if (voiceRegStateResult.cellIdentity.cellIdentityGsm.size() == 1) {
+                        android.hardware.radio.V1_0.CellIdentityGsm cellIdentityGsm =
+                                voiceRegStateResult.cellIdentity.cellIdentityGsm.get(0);
+                        cid = cellIdentityGsm.cid;
+                        lac = cellIdentityGsm.lac;
+                    }
+                    break;
+                }
+                case CellInfoType.WCDMA: {
+                    if (voiceRegStateResult.cellIdentity.cellIdentityWcdma.size() == 1) {
+                        android.hardware.radio.V1_0.CellIdentityWcdma cellIdentityWcdma =
+                                voiceRegStateResult.cellIdentity.cellIdentityWcdma.get(0);
+                        cid = cellIdentityWcdma.cid;
+                        lac = cellIdentityWcdma.lac;
+                        psc = cellIdentityWcdma.psc;
+                    }
+                    break;
+                }
+                case CellInfoType.TD_SCDMA: {
+                    if (voiceRegStateResult.cellIdentity.cellIdentityTdscdma.size() == 1) {
+                        android.hardware.radio.V1_0.CellIdentityTdscdma
+                                cellIdentityTdscdma =
+                                voiceRegStateResult.cellIdentity.cellIdentityTdscdma.get(0);
+                        cid = cellIdentityTdscdma.cid;
+                        lac = cellIdentityTdscdma.lac;
+                    }
+                    break;
+                }
+                case CellInfoType.LTE: {
+                    if (voiceRegStateResult.cellIdentity.cellIdentityLte.size() == 1) {
+                        android.hardware.radio.V1_0.CellIdentityLte cellIdentityLte =
+                                voiceRegStateResult.cellIdentity.cellIdentityLte.get(0);
+                        cid = cellIdentityLte.ci;
+
+                        /* Continuing the historical behaviour of using tac as lac. */
+                        lac = cellIdentityLte.tac;
+                    }
+                    break;
+                }
+                default: {
+                    break;
+                }
+            }
+            // LAC and CID are -1 if not avail
+            ((GsmCellLocation) cellLocation).setLacAndCid(lac, cid);
+            ((GsmCellLocation) cellLocation).setPsc(psc);
+        } else {
+            int baseStationId = -1;
+            int baseStationLatitude = CdmaCellLocation.INVALID_LAT_LONG;
+            int baseStationLongitude = CdmaCellLocation.INVALID_LAT_LONG;
+            int systemId = 0;
+            int networkId = 0;
+
+            switch(voiceRegStateResult.cellIdentity.cellInfoType) {
+                case CellInfoType.CDMA: {
+                    if (voiceRegStateResult.cellIdentity.cellIdentityCdma.size() == 1) {
+                        android.hardware.radio.V1_0.CellIdentityCdma cellIdentityCdma =
+                                voiceRegStateResult.cellIdentity.cellIdentityCdma.get(0);
+                        baseStationId = cellIdentityCdma.baseStationId;
+                        baseStationLatitude = cellIdentityCdma.latitude;
+                        baseStationLongitude = cellIdentityCdma.longitude;
+                        systemId = cellIdentityCdma.systemId;
+                        networkId = cellIdentityCdma.networkId;
+                    }
+                    break;
+                }
+                default: {
+                    break;
+                }
+            }
+
+            // Some carriers only return lat-lngs of 0,0
+            if (baseStationLatitude == 0 && baseStationLongitude == 0) {
+                baseStationLatitude  = CdmaCellLocation.INVALID_LAT_LONG;
+                baseStationLongitude = CdmaCellLocation.INVALID_LAT_LONG;
+            }
+
+            // Values are -1 if not available.
+            ((CdmaCellLocation) cellLocation).setCellLocationData(baseStationId,
+                    baseStationLatitude, baseStationLongitude, systemId, networkId);
+        }
+    }
+
+    private int getLteEarfcn(DataRegStateResult dataRegStateResult) {
+        int lteEarfcn = INVALID_LTE_EARFCN;
+        switch(dataRegStateResult.cellIdentity.cellInfoType) {
+            case CellInfoType.LTE: {
+                if (dataRegStateResult.cellIdentity.cellIdentityLte.size() == 1) {
+                    android.hardware.radio.V1_0.CellIdentityLte cellIdentityLte =
+                            dataRegStateResult.cellIdentity.cellIdentityLte.get(0);
+                    lteEarfcn = cellIdentityLte.earfcn;
+                }
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+
+        return lteEarfcn;
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+        int[] ints;
+        Message message;
+
+        if (VDBG) log("received event " + msg.what);
+        switch (msg.what) {
+            case EVENT_SET_RADIO_POWER_OFF:
+                synchronized(this) {
+                    if (mPendingRadioPowerOffAfterDataOff &&
+                            (msg.arg1 == mPendingRadioPowerOffAfterDataOffTag)) {
+                        if (DBG) log("EVENT_SET_RADIO_OFF, turn radio off now.");
+                        hangupAndPowerOff();
+                        mPendingRadioPowerOffAfterDataOffTag += 1;
+                        mPendingRadioPowerOffAfterDataOff = false;
+                    } else {
+                        log("EVENT_SET_RADIO_OFF is stale arg1=" + msg.arg1 +
+                                "!= tag=" + mPendingRadioPowerOffAfterDataOffTag);
+                    }
+                }
+                break;
+
+            case EVENT_ICC_CHANGED:
+                onUpdateIccAvailability();
+                break;
+
+            case EVENT_GET_CELL_INFO_LIST: {
+                ar = (AsyncResult) msg.obj;
+                CellInfoResult result = (CellInfoResult) ar.userObj;
+                synchronized(result.lockObj) {
+                    if (ar.exception != null) {
+                        log("EVENT_GET_CELL_INFO_LIST: error ret null, e=" + ar.exception);
+                        result.list = null;
+                    } else {
+                        result.list = (List<CellInfo>) ar.result;
+
+                        if (VDBG) {
+                            log("EVENT_GET_CELL_INFO_LIST: size=" + result.list.size()
+                                    + " list=" + result.list);
+                        }
+                    }
+                    mLastCellInfoListTime = SystemClock.elapsedRealtime();
+                    mLastCellInfoList = result.list;
+                    result.lockObj.notify();
+                }
+                break;
+            }
+
+            case EVENT_UNSOL_CELL_INFO_LIST: {
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception != null) {
+                    log("EVENT_UNSOL_CELL_INFO_LIST: error ignoring, e=" + ar.exception);
+                } else {
+                    List<CellInfo> list = (List<CellInfo>) ar.result;
+                    if (VDBG) {
+                        log("EVENT_UNSOL_CELL_INFO_LIST: size=" + list.size() + " list=" + list);
+                    }
+                    mLastCellInfoListTime = SystemClock.elapsedRealtime();
+                    mLastCellInfoList = list;
+                    mPhone.notifyCellInfo(list);
+                }
+                break;
+            }
+
+            case  EVENT_IMS_STATE_CHANGED: // received unsol
+                mCi.getImsRegistrationState(this.obtainMessage(EVENT_IMS_STATE_DONE));
+                break;
+
+            case EVENT_IMS_STATE_DONE:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null) {
+                    int[] responseArray = (int[])ar.result;
+                    mImsRegistered = (responseArray[0] == 1) ? true : false;
+                }
+                break;
+
+            // GSM
+            case EVENT_SIM_READY:
+                // Reset the mPreviousSubId so we treat a SIM power bounce
+                // as a first boot.  See b/19194287
+                mOnSubscriptionsChangedListener.mPreviousSubId.set(-1);
+                pollState();
+                // Signal strength polling stops when radio is off
+                queueNextSignalStrengthPoll();
+                break;
+
+            case EVENT_RADIO_STATE_CHANGED:
+            case EVENT_PHONE_TYPE_SWITCHED:
+                if(!mPhone.isPhoneTypeGsm() &&
+                        mCi.getRadioState() == CommandsInterface.RadioState.RADIO_ON) {
+                    handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource());
+
+                    // Signal strength polling stops when radio is off.
+                    queueNextSignalStrengthPoll();
+                }
+                // This will do nothing in the 'radio not available' case
+                setPowerStateToDesired();
+                // These events are modem triggered, so pollState() needs to be forced
+                modemTriggeredPollState();
+                break;
+
+            case EVENT_NETWORK_STATE_CHANGED:
+                modemTriggeredPollState();
+                break;
+
+            case EVENT_GET_SIGNAL_STRENGTH:
+                // This callback is called when signal strength is polled
+                // all by itself
+
+                if (!(mCi.getRadioState().isOn())) {
+                    // Polling will continue when radio turns back on
+                    return;
+                }
+                ar = (AsyncResult) msg.obj;
+                onSignalStrengthResult(ar);
+                queueNextSignalStrengthPoll();
+
+                break;
+
+            case EVENT_GET_LOC_DONE:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null) {
+                    processCellLocationInfo(mCellLoc, (VoiceRegStateResult) ar.result);
+                    mPhone.notifyLocationChanged();
+                }
+
+                // Release any temporary cell lock, which could have been
+                // acquired to allow a single-shot location update.
+                disableSingleLocationUpdate();
+                break;
+
+            case EVENT_POLL_STATE_REGISTRATION:
+            case EVENT_POLL_STATE_GPRS:
+            case EVENT_POLL_STATE_OPERATOR:
+                ar = (AsyncResult) msg.obj;
+                handlePollStateResult(msg.what, ar);
+                break;
+
+            case EVENT_POLL_STATE_NETWORK_SELECTION_MODE:
+                if (DBG) log("EVENT_POLL_STATE_NETWORK_SELECTION_MODE");
+                ar = (AsyncResult) msg.obj;
+                if (mPhone.isPhoneTypeGsm()) {
+                    handlePollStateResult(msg.what, ar);
+                } else {
+                    if (ar.exception == null && ar.result != null) {
+                        ints = (int[])ar.result;
+                        if (ints[0] == 1) {  // Manual selection.
+                            mPhone.setNetworkSelectionModeAutomatic(null);
+                        }
+                    } else {
+                        log("Unable to getNetworkSelectionMode");
+                    }
+                }
+                break;
+
+            case EVENT_POLL_SIGNAL_STRENGTH:
+                // Just poll signal strength...not part of pollState()
+
+                mCi.getSignalStrength(obtainMessage(EVENT_GET_SIGNAL_STRENGTH));
+                break;
+
+            case EVENT_NITZ_TIME:
+                ar = (AsyncResult) msg.obj;
+
+                String nitzString = (String)((Object[])ar.result)[0];
+                long nitzReceiveTime = ((Long)((Object[])ar.result)[1]).longValue();
+
+                setTimeFromNITZString(nitzString, nitzReceiveTime);
+                break;
+
+            case EVENT_SIGNAL_STRENGTH_UPDATE:
+                // This is a notification from CommandsInterface.setOnSignalStrengthUpdate
+
+                ar = (AsyncResult) msg.obj;
+
+                // The radio is telling us about signal strength changes
+                // we don't have to ask it
+                mDontPollSignalStrength = true;
+
+                onSignalStrengthResult(ar);
+                break;
+
+            case EVENT_SIM_RECORDS_LOADED:
+                log("EVENT_SIM_RECORDS_LOADED: what=" + msg.what);
+                updatePhoneObject();
+                updateOtaspState();
+                if (mPhone.isPhoneTypeGsm()) {
+                    updateSpnDisplay();
+                }
+                break;
+
+            case EVENT_LOCATION_UPDATES_ENABLED:
+                ar = (AsyncResult) msg.obj;
+
+                if (ar.exception == null) {
+                    mCi.getVoiceRegistrationState(obtainMessage(EVENT_GET_LOC_DONE, null));
+                }
+                break;
+
+            case EVENT_SET_PREFERRED_NETWORK_TYPE:
+                ar = (AsyncResult) msg.obj;
+                // Don't care the result, only use for dereg network (COPS=2)
+                message = obtainMessage(EVENT_RESET_PREFERRED_NETWORK_TYPE, ar.userObj);
+                mCi.setPreferredNetworkType(mPreferredNetworkType, message);
+                break;
+
+            case EVENT_RESET_PREFERRED_NETWORK_TYPE:
+                ar = (AsyncResult) msg.obj;
+                if (ar.userObj != null) {
+                    AsyncResult.forMessage(((Message) ar.userObj)).exception
+                            = ar.exception;
+                    ((Message) ar.userObj).sendToTarget();
+                }
+                break;
+
+            case EVENT_GET_PREFERRED_NETWORK_TYPE:
+                ar = (AsyncResult) msg.obj;
+
+                if (ar.exception == null) {
+                    mPreferredNetworkType = ((int[])ar.result)[0];
+                } else {
+                    mPreferredNetworkType = RILConstants.NETWORK_MODE_GLOBAL;
+                }
+
+                message = obtainMessage(EVENT_SET_PREFERRED_NETWORK_TYPE, ar.userObj);
+                int toggledNetworkType = RILConstants.NETWORK_MODE_GLOBAL;
+
+                mCi.setPreferredNetworkType(toggledNetworkType, message);
+                break;
+
+            case EVENT_CHECK_REPORT_GPRS:
+                if (mPhone.isPhoneTypeGsm() && mSS != null &&
+                        !isGprsConsistent(mSS.getDataRegState(), mSS.getVoiceRegState())) {
+
+                    // Can't register data service while voice service is ok
+                    // i.e. CREG is ok while CGREG is not
+                    // possible a network or baseband side error
+                    GsmCellLocation loc = ((GsmCellLocation)mPhone.getCellLocation());
+                    EventLog.writeEvent(EventLogTags.DATA_NETWORK_REGISTRATION_FAIL,
+                            mSS.getOperatorNumeric(), loc != null ? loc.getCid() : -1);
+                    mReportedGprsNoReg = true;
+                }
+                mStartedGprsRegCheck = false;
+                break;
+
+            case EVENT_RESTRICTED_STATE_CHANGED:
+                if (mPhone.isPhoneTypeGsm()) {
+                    // This is a notification from
+                    // CommandsInterface.setOnRestrictedStateChanged
+
+                    if (DBG) log("EVENT_RESTRICTED_STATE_CHANGED");
+
+                    ar = (AsyncResult) msg.obj;
+
+                    onRestrictedStateChanged(ar);
+                }
+                break;
+
+            case EVENT_ALL_DATA_DISCONNECTED:
+                int dds = SubscriptionManager.getDefaultDataSubscriptionId();
+                ProxyController.getInstance().unregisterForAllDataDisconnected(dds, this);
+                synchronized(this) {
+                    if (mPendingRadioPowerOffAfterDataOff) {
+                        if (DBG) log("EVENT_ALL_DATA_DISCONNECTED, turn radio off now.");
+                        hangupAndPowerOff();
+                        mPendingRadioPowerOffAfterDataOff = false;
+                    } else {
+                        log("EVENT_ALL_DATA_DISCONNECTED is stale");
+                    }
+                }
+                break;
+
+            case EVENT_CHANGE_IMS_STATE:
+                if (DBG) log("EVENT_CHANGE_IMS_STATE:");
+
+                setPowerStateToDesired();
+                break;
+
+            case EVENT_IMS_CAPABILITY_CHANGED:
+                if (DBG) log("EVENT_IMS_CAPABILITY_CHANGED");
+                updateSpnDisplay();
+                break;
+
+            //CDMA
+            case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED:
+                handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource());
+                break;
+
+            case EVENT_RUIM_READY:
+                if (mPhone.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE) {
+                    // Subscription will be read from SIM I/O
+                    if (DBG) log("Receive EVENT_RUIM_READY");
+                    pollState();
+                } else {
+                    if (DBG) log("Receive EVENT_RUIM_READY and Send Request getCDMASubscription.");
+                    getSubscriptionInfoAndStartPollingThreads();
+                }
+
+                // Only support automatic selection mode in CDMA.
+                mCi.getNetworkSelectionMode(obtainMessage(EVENT_POLL_STATE_NETWORK_SELECTION_MODE));
+
+                break;
+
+            case EVENT_NV_READY:
+                updatePhoneObject();
+
+                // Only support automatic selection mode in CDMA.
+                mCi.getNetworkSelectionMode(obtainMessage(EVENT_POLL_STATE_NETWORK_SELECTION_MODE));
+
+                // For Non-RUIM phones, the subscription information is stored in
+                // Non Volatile. Here when Non-Volatile is ready, we can poll the CDMA
+                // subscription info.
+                getSubscriptionInfoAndStartPollingThreads();
+                break;
+
+            case EVENT_POLL_STATE_CDMA_SUBSCRIPTION: // Handle RIL_CDMA_SUBSCRIPTION
+                if (!mPhone.isPhoneTypeGsm()) {
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception == null) {
+                        String cdmaSubscription[] = (String[]) ar.result;
+                        if (cdmaSubscription != null && cdmaSubscription.length >= 5) {
+                            mMdn = cdmaSubscription[0];
+                            parseSidNid(cdmaSubscription[1], cdmaSubscription[2]);
+
+                            mMin = cdmaSubscription[3];
+                            mPrlVersion = cdmaSubscription[4];
+                            if (DBG) log("GET_CDMA_SUBSCRIPTION: MDN=" + mMdn);
+
+                            mIsMinInfoReady = true;
+
+                            updateOtaspState();
+                            // Notify apps subscription info is ready
+                            notifyCdmaSubscriptionInfoReady();
+
+                            if (!mIsSubscriptionFromRuim && mIccRecords != null) {
+                                if (DBG) {
+                                    log("GET_CDMA_SUBSCRIPTION set imsi in mIccRecords");
+                                }
+                                mIccRecords.setImsi(getImsi());
+                            } else {
+                                if (DBG) {
+                                    log("GET_CDMA_SUBSCRIPTION either mIccRecords is null or NV " +
+                                            "type device - not setting Imsi in mIccRecords");
+                                }
+                            }
+                        } else {
+                            if (DBG) {
+                                log("GET_CDMA_SUBSCRIPTION: error parsing cdmaSubscription " +
+                                        "params num=" + cdmaSubscription.length);
+                            }
+                        }
+                    }
+                }
+                break;
+
+            case EVENT_RUIM_RECORDS_LOADED:
+                if (!mPhone.isPhoneTypeGsm()) {
+                    log("EVENT_RUIM_RECORDS_LOADED: what=" + msg.what);
+                    updatePhoneObject();
+                    if (mPhone.isPhoneTypeCdma()) {
+                        updateSpnDisplay();
+                    } else {
+                        RuimRecords ruim = (RuimRecords) mIccRecords;
+                        if (ruim != null) {
+                            if (ruim.isProvisioned()) {
+                                mMdn = ruim.getMdn();
+                                mMin = ruim.getMin();
+                                parseSidNid(ruim.getSid(), ruim.getNid());
+                                mPrlVersion = ruim.getPrlVersion();
+                                mIsMinInfoReady = true;
+                            }
+                            updateOtaspState();
+                            // Notify apps subscription info is ready
+                            notifyCdmaSubscriptionInfoReady();
+                        }
+                        // SID/NID/PRL is loaded. Poll service state
+                        // again to update to the roaming state with
+                        // the latest variables.
+                        pollState();
+                    }
+                }
+                break;
+
+            case EVENT_ERI_FILE_LOADED:
+                // Repoll the state once the ERI file has been loaded.
+                if (DBG) log("ERI file has been loaded, repolling.");
+                pollState();
+                break;
+
+            case EVENT_OTA_PROVISION_STATUS_CHANGE:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    ints = (int[]) ar.result;
+                    int otaStatus = ints[0];
+                    if (otaStatus == Phone.CDMA_OTA_PROVISION_STATUS_COMMITTED
+                            || otaStatus == Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED) {
+                        if (DBG) log("EVENT_OTA_PROVISION_STATUS_CHANGE: Complete, Reload MDN");
+                        mCi.getCDMASubscription( obtainMessage(EVENT_POLL_STATE_CDMA_SUBSCRIPTION));
+                    }
+                }
+                break;
+
+            case EVENT_CDMA_PRL_VERSION_CHANGED:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    ints = (int[]) ar.result;
+                    mPrlVersion = Integer.toString(ints[0]);
+                }
+                break;
+
+            case EVENT_RADIO_POWER_FROM_CARRIER:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null) {
+                    boolean enable = (boolean) ar.result;
+                    if (DBG) log("EVENT_RADIO_POWER_FROM_CARRIER: " + enable);
+                    setRadioPowerFromCarrier(enable);
+                }
+                break;
+
+            default:
+                log("Unhandled message with number: " + msg.what);
+                break;
+        }
+    }
+
+    protected boolean isSidsAllZeros() {
+        if (mHomeSystemId != null) {
+            for (int i=0; i < mHomeSystemId.length; i++) {
+                if (mHomeSystemId[i] != 0) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Check whether a specified system ID that matches one of the home system IDs.
+     */
+    private boolean isHomeSid(int sid) {
+        if (mHomeSystemId != null) {
+            for (int i=0; i < mHomeSystemId.length; i++) {
+                if (sid == mHomeSystemId[i]) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public String getMdnNumber() {
+        return mMdn;
+    }
+
+    public String getCdmaMin() {
+        return mMin;
+    }
+
+    /** Returns null if NV is not yet ready */
+    public String getPrlVersion() {
+        return mPrlVersion;
+    }
+
+    /**
+     * Returns IMSI as MCC + MNC + MIN
+     */
+    public String getImsi() {
+        // TODO: When RUIM is enabled, IMSI will come from RUIM not build-time props.
+        String operatorNumeric = ((TelephonyManager) mPhone.getContext().
+                getSystemService(Context.TELEPHONY_SERVICE)).
+                getSimOperatorNumericForPhone(mPhone.getPhoneId());
+
+        if (!TextUtils.isEmpty(operatorNumeric) && getCdmaMin() != null) {
+            return (operatorNumeric + getCdmaMin());
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Check if subscription data has been assigned to mMin
+     *
+     * return true if MIN info is ready; false otherwise.
+     */
+    public boolean isMinInfoReady() {
+        return mIsMinInfoReady;
+    }
+
+    /**
+     * Returns OTASP_UNKNOWN, OTASP_UNINITIALIZED, OTASP_NEEDED or OTASP_NOT_NEEDED
+     */
+    public int getOtasp() {
+        int provisioningState;
+        // if sim is not loaded, return otasp uninitialized
+        if(!mPhone.getIccRecordsLoaded()) {
+            if(DBG) log("getOtasp: otasp uninitialized due to sim not loaded");
+            return TelephonyManager.OTASP_UNINITIALIZED;
+        }
+        // if voice tech is Gsm, return otasp not needed
+        if(mPhone.isPhoneTypeGsm()) {
+            if(DBG) log("getOtasp: otasp not needed for GSM");
+            return TelephonyManager.OTASP_NOT_NEEDED;
+        }
+        // for ruim, min is null means require otasp.
+        if (mIsSubscriptionFromRuim && mMin == null) {
+            return TelephonyManager.OTASP_NEEDED;
+        }
+        if (mMin == null || (mMin.length() < 6)) {
+            if (DBG) log("getOtasp: bad mMin='" + mMin + "'");
+            provisioningState = TelephonyManager.OTASP_UNKNOWN;
+        } else {
+            if ((mMin.equals(UNACTIVATED_MIN_VALUE)
+                    || mMin.substring(0,6).equals(UNACTIVATED_MIN2_VALUE))
+                    || SystemProperties.getBoolean("test_cdma_setup", false)) {
+                provisioningState = TelephonyManager.OTASP_NEEDED;
+            } else {
+                provisioningState = TelephonyManager.OTASP_NOT_NEEDED;
+            }
+        }
+        if (DBG) log("getOtasp: state=" + provisioningState);
+        return provisioningState;
+    }
+
+    protected void parseSidNid (String sidStr, String nidStr) {
+        if (sidStr != null) {
+            String[] sid = sidStr.split(",");
+            mHomeSystemId = new int[sid.length];
+            for (int i = 0; i < sid.length; i++) {
+                try {
+                    mHomeSystemId[i] = Integer.parseInt(sid[i]);
+                } catch (NumberFormatException ex) {
+                    loge("error parsing system id: " + ex);
+                }
+            }
+        }
+        if (DBG) log("CDMA_SUBSCRIPTION: SID=" + sidStr);
+
+        if (nidStr != null) {
+            String[] nid = nidStr.split(",");
+            mHomeNetworkId = new int[nid.length];
+            for (int i = 0; i < nid.length; i++) {
+                try {
+                    mHomeNetworkId[i] = Integer.parseInt(nid[i]);
+                } catch (NumberFormatException ex) {
+                    loge("CDMA_SUBSCRIPTION: error parsing network id: " + ex);
+                }
+            }
+        }
+        if (DBG) log("CDMA_SUBSCRIPTION: NID=" + nidStr);
+    }
+
+    protected void updateOtaspState() {
+        int otaspMode = getOtasp();
+        int oldOtaspMode = mCurrentOtaspMode;
+        mCurrentOtaspMode = otaspMode;
+
+        if (oldOtaspMode != mCurrentOtaspMode) {
+            if (DBG) {
+                log("updateOtaspState: call notifyOtaspChanged old otaspMode=" +
+                        oldOtaspMode + " new otaspMode=" + mCurrentOtaspMode);
+            }
+            mPhone.notifyOtaspChanged(mCurrentOtaspMode);
+        }
+    }
+
+    protected Phone getPhone() {
+        return mPhone;
+    }
+
+    protected void handlePollStateResult(int what, AsyncResult ar) {
+        // Ignore stale requests from last poll
+        if (ar.userObj != mPollingContext) return;
+
+        if (ar.exception != null) {
+            CommandException.Error err=null;
+
+            if (ar.exception instanceof CommandException) {
+                err = ((CommandException)(ar.exception)).getCommandError();
+            }
+
+            if (err == CommandException.Error.RADIO_NOT_AVAILABLE) {
+                // Radio has crashed or turned off
+                cancelPollState();
+                return;
+            }
+
+            if (err != CommandException.Error.OP_NOT_ALLOWED_BEFORE_REG_NW) {
+                loge("RIL implementation has returned an error where it must succeed" +
+                        ar.exception);
+            }
+        } else try {
+            handlePollStateResultMessage(what, ar);
+        } catch (RuntimeException ex) {
+            loge("Exception while polling service state. Probably malformed RIL response." + ex);
+        }
+
+        mPollingContext[0]--;
+
+        if (mPollingContext[0] == 0) {
+            if (mPhone.isPhoneTypeGsm()) {
+                updateRoamingState();
+                mNewSS.setEmergencyOnly(mEmergencyOnly);
+            } else {
+                boolean namMatch = false;
+                if (!isSidsAllZeros() && isHomeSid(mNewSS.getSystemId())) {
+                    namMatch = true;
+                }
+
+                // Setting SS Roaming (general)
+                if (mIsSubscriptionFromRuim) {
+                    boolean isRoamingBetweenOperators = isRoamingBetweenOperators(
+                            mNewSS.getVoiceRoaming(), mNewSS);
+                    if (isRoamingBetweenOperators != mNewSS.getVoiceRoaming()) {
+                        log("isRoamingBetweenOperators=" + isRoamingBetweenOperators
+                                + ". Override CDMA voice roaming to " + isRoamingBetweenOperators);
+                        mNewSS.setVoiceRoaming(isRoamingBetweenOperators);
+                    }
+                }
+                /**
+                 * For CDMA, voice and data should have the same roaming status.
+                 * If voice is not in service, use TSB58 roaming indicator to set
+                 * data roaming status. If TSB58 roaming indicator is not in the
+                 * carrier-specified list of ERIs for home system then set roaming.
+                 */
+                final int dataRat = mNewSS.getRilDataRadioTechnology();
+                if (ServiceState.isCdma(dataRat)) {
+                    final boolean isVoiceInService =
+                            (mNewSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE);
+                    if (isVoiceInService) {
+                        boolean isVoiceRoaming = mNewSS.getVoiceRoaming();
+                        if (mNewSS.getDataRoaming() != isVoiceRoaming) {
+                            log("Data roaming != Voice roaming. Override data roaming to "
+                                    + isVoiceRoaming);
+                            mNewSS.setDataRoaming(isVoiceRoaming);
+                        }
+                    } else {
+                        /**
+                         * As per VoiceRegStateResult from radio types.hal the TSB58
+                         * Roaming Indicator shall be sent if device is registered
+                         * on a CDMA or EVDO system.
+                         */
+                        boolean isRoamIndForHomeSystem = isRoamIndForHomeSystem(
+                                Integer.toString(mRoamingIndicator));
+                        if (mNewSS.getDataRoaming() == isRoamIndForHomeSystem) {
+                            log("isRoamIndForHomeSystem=" + isRoamIndForHomeSystem
+                                    + ", override data roaming to " + !isRoamIndForHomeSystem);
+                            mNewSS.setDataRoaming(!isRoamIndForHomeSystem);
+                        }
+                    }
+                }
+
+                // Setting SS CdmaRoamingIndicator and CdmaDefaultRoamingIndicator
+                mNewSS.setCdmaDefaultRoamingIndicator(mDefaultRoamingIndicator);
+                mNewSS.setCdmaRoamingIndicator(mRoamingIndicator);
+                boolean isPrlLoaded = true;
+                if (TextUtils.isEmpty(mPrlVersion)) {
+                    isPrlLoaded = false;
+                }
+                if (!isPrlLoaded || (mNewSS.getRilVoiceRadioTechnology()
+                        == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN)) {
+                    log("Turn off roaming indicator if !isPrlLoaded or voice RAT is unknown");
+                    mNewSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_OFF);
+                } else if (!isSidsAllZeros()) {
+                    if (!namMatch && !mIsInPrl) {
+                        // Use default
+                        mNewSS.setCdmaRoamingIndicator(mDefaultRoamingIndicator);
+                    } else if (namMatch && !mIsInPrl) {
+                        // TODO this will be removed when we handle roaming on LTE on CDMA+LTE phones
+                        if (ServiceState.isLte(mNewSS.getRilVoiceRadioTechnology())) {
+                            log("Turn off roaming indicator as voice is LTE");
+                            mNewSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_OFF);
+                        } else {
+                            mNewSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_FLASH);
+                        }
+                    } else if (!namMatch && mIsInPrl) {
+                        // Use the one from PRL/ERI
+                        mNewSS.setCdmaRoamingIndicator(mRoamingIndicator);
+                    } else {
+                        // It means namMatch && mIsInPrl
+                        if ((mRoamingIndicator <= 2)) {
+                            mNewSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_OFF);
+                        } else {
+                            // Use the one from PRL/ERI
+                            mNewSS.setCdmaRoamingIndicator(mRoamingIndicator);
+                        }
+                    }
+                }
+
+                int roamingIndicator = mNewSS.getCdmaRoamingIndicator();
+                mNewSS.setCdmaEriIconIndex(mPhone.mEriManager.getCdmaEriIconIndex(roamingIndicator,
+                        mDefaultRoamingIndicator));
+                mNewSS.setCdmaEriIconMode(mPhone.mEriManager.getCdmaEriIconMode(roamingIndicator,
+                        mDefaultRoamingIndicator));
+
+                // NOTE: Some operator may require overriding mCdmaRoaming
+                // (set by the modem), depending on the mRoamingIndicator.
+
+                if (DBG) {
+                    log("Set CDMA Roaming Indicator to: " + mNewSS.getCdmaRoamingIndicator()
+                            + ". voiceRoaming = " + mNewSS.getVoiceRoaming()
+                            + ". dataRoaming = " + mNewSS.getDataRoaming()
+                            + ", isPrlLoaded = " + isPrlLoaded
+                            + ". namMatch = " + namMatch + " , mIsInPrl = " + mIsInPrl
+                            + ", mRoamingIndicator = " + mRoamingIndicator
+                            + ", mDefaultRoamingIndicator= " + mDefaultRoamingIndicator);
+                }
+            }
+            pollStateDone();
+        }
+
+    }
+
+    /**
+     * Set roaming state when cdmaRoaming is true and ons is different from spn
+     * @param cdmaRoaming TS 27.007 7.2 CREG registered roaming
+     * @param s ServiceState hold current ons
+     * @return true for roaming state set
+     */
+    private boolean isRoamingBetweenOperators(boolean cdmaRoaming, ServiceState s) {
+        return cdmaRoaming && !isSameOperatorNameFromSimAndSS(s);
+    }
+
+    private int getRegStateFromHalRegState(int regState) {
+        switch (regState) {
+            case RegState.NOT_REG_MT_NOT_SEARCHING_OP:
+                return ServiceState.RIL_REG_STATE_NOT_REG;
+            case RegState.REG_HOME:
+                return ServiceState.RIL_REG_STATE_HOME;
+            case RegState.NOT_REG_MT_SEARCHING_OP:
+                return ServiceState.RIL_REG_STATE_SEARCHING;
+            case RegState.REG_DENIED:
+                return ServiceState.RIL_REG_STATE_DENIED;
+            case RegState.UNKNOWN:
+                return ServiceState.RIL_REG_STATE_UNKNOWN;
+            case RegState.REG_ROAMING:
+                return ServiceState.RIL_REG_STATE_ROAMING;
+            case RegState.NOT_REG_MT_NOT_SEARCHING_OP_EM:
+                return ServiceState.RIL_REG_STATE_NOT_REG_EMERGENCY_CALL_ENABLED;
+            case RegState.NOT_REG_MT_SEARCHING_OP_EM:
+                return ServiceState.RIL_REG_STATE_SEARCHING_EMERGENCY_CALL_ENABLED;
+            case RegState.REG_DENIED_EM:
+                return ServiceState.RIL_REG_STATE_DENIED_EMERGENCY_CALL_ENABLED;
+            case RegState.UNKNOWN_EM:
+                return ServiceState.RIL_REG_STATE_UNKNOWN_EMERGENCY_CALL_ENABLED;
+            default:
+                return ServiceState.REGISTRATION_STATE_NOT_REGISTERED_AND_NOT_SEARCHING;
+        }
+    }
+
+    void handlePollStateResultMessage(int what, AsyncResult ar) {
+        int ints[];
+        switch (what) {
+            case EVENT_POLL_STATE_REGISTRATION: {
+                VoiceRegStateResult voiceRegStateResult = (VoiceRegStateResult) ar.result;
+                int registrationState = getRegStateFromHalRegState(voiceRegStateResult.regState);
+                int cssIndicator = voiceRegStateResult.cssSupported ? 1 : 0;
+
+                mNewSS.setVoiceRegState(regCodeToServiceState(registrationState));
+                mNewSS.setCssIndicator(cssIndicator);
+                mNewSS.setRilVoiceRadioTechnology(voiceRegStateResult.rat);
+
+                //Denial reason if registrationState = 3
+                int reasonForDenial = voiceRegStateResult.reasonForDenial;
+                if (mPhone.isPhoneTypeGsm()) {
+
+                    mGsmRoaming = regCodeIsRoaming(registrationState);
+                    mNewRejectCode = reasonForDenial;
+
+                    boolean isVoiceCapable = mPhone.getContext().getResources()
+                            .getBoolean(com.android.internal.R.bool.config_voice_capable);
+                    if (((registrationState
+                            == ServiceState.RIL_REG_STATE_DENIED_EMERGENCY_CALL_ENABLED)
+                            || (registrationState
+                            == ServiceState.RIL_REG_STATE_NOT_REG_EMERGENCY_CALL_ENABLED)
+                            || (registrationState
+                            == ServiceState.RIL_REG_STATE_SEARCHING_EMERGENCY_CALL_ENABLED)
+                            || (registrationState
+                            == ServiceState.RIL_REG_STATE_UNKNOWN_EMERGENCY_CALL_ENABLED))
+                            && isVoiceCapable) {
+                        mEmergencyOnly = true;
+                    } else {
+                        mEmergencyOnly = false;
+                    }
+                } else {
+                    int roamingIndicator = voiceRegStateResult.roamingIndicator;
+
+                    //Indicates if current system is in PR
+                    int systemIsInPrl = voiceRegStateResult.systemIsInPrl;
+
+                    //Is default roaming indicator from PRL
+                    int defaultRoamingIndicator = voiceRegStateResult.defaultRoamingIndicator;
+
+                    mRegistrationState = registrationState;
+                    // When registration state is roaming and TSB58
+                    // roaming indicator is not in the carrier-specified
+                    // list of ERIs for home system, mCdmaRoaming is true.
+                    boolean cdmaRoaming =
+                            regCodeIsRoaming(registrationState)
+                                    && !isRoamIndForHomeSystem(
+                                            Integer.toString(roamingIndicator));
+                    mNewSS.setVoiceRoaming(cdmaRoaming);
+                    mRoamingIndicator = roamingIndicator;
+                    mIsInPrl = (systemIsInPrl == 0) ? false : true;
+                    mDefaultRoamingIndicator = defaultRoamingIndicator;
+
+                    int systemId = 0;
+                    int networkId = 0;
+                    if (voiceRegStateResult.cellIdentity.cellInfoType == CellInfoType.CDMA
+                            && voiceRegStateResult.cellIdentity.cellIdentityCdma.size() == 1) {
+                        android.hardware.radio.V1_0.CellIdentityCdma cellIdentityCdma =
+                                voiceRegStateResult.cellIdentity.cellIdentityCdma.get(0);
+                        systemId = cellIdentityCdma.systemId;
+                        networkId = cellIdentityCdma.networkId;
+                    }
+                    mNewSS.setSystemAndNetworkId(systemId, networkId);
+
+                    if (reasonForDenial == 0) {
+                        mRegistrationDeniedReason = ServiceStateTracker.REGISTRATION_DENIED_GEN;
+                    } else if (reasonForDenial == 1) {
+                        mRegistrationDeniedReason = ServiceStateTracker.REGISTRATION_DENIED_AUTH;
+                    } else {
+                        mRegistrationDeniedReason = "";
+                    }
+
+                    if (mRegistrationState == 3) {
+                        if (DBG) log("Registration denied, " + mRegistrationDeniedReason);
+                    }
+                }
+
+                processCellLocationInfo(mNewCellLoc, voiceRegStateResult);
+
+                if (DBG) {
+                    log("handlPollVoiceRegResultMessage: regState=" + registrationState
+                            + " radioTechnology=" + voiceRegStateResult.rat);
+                }
+                break;
+            }
+
+            case EVENT_POLL_STATE_GPRS: {
+                DataRegStateResult dataRegStateResult = (DataRegStateResult) ar.result;
+                int regState = getRegStateFromHalRegState(dataRegStateResult.regState);
+                int dataRegState = regCodeToServiceState(regState);
+                int newDataRat = dataRegStateResult.rat;
+                mNewSS.setDataRegState(dataRegState);
+                mNewSS.setRilDataRadioTechnology(newDataRat);
+
+                if (mPhone.isPhoneTypeGsm()) {
+
+                    mNewReasonDataDenied = dataRegStateResult.reasonDataDenied;
+                    mNewMaxDataCalls = dataRegStateResult.maxDataCalls;
+                    mDataRoaming = regCodeIsRoaming(regState);
+                    // Save the data roaming state reported by modem registration before resource
+                    // overlay or carrier config possibly overrides it.
+                    mNewSS.setDataRoamingFromRegistration(mDataRoaming);
+
+                    if (DBG) {
+                        log("handlPollStateResultMessage: GsmSST setDataRegState=" + dataRegState
+                                + " regState=" + regState
+                                + " dataRadioTechnology=" + newDataRat);
+                    }
+                } else if (mPhone.isPhoneTypeCdma()) {
+
+                    boolean isDataRoaming = regCodeIsRoaming(regState);
+                    mNewSS.setDataRoaming(isDataRoaming);
+                    // Save the data roaming state reported by modem registration before resource
+                    // overlay or carrier config possibly overrides it.
+                    mNewSS.setDataRoamingFromRegistration(isDataRoaming);
+
+                    if (DBG) {
+                        log("handlPollStateResultMessage: cdma setDataRegState=" + dataRegState
+                                + " regState=" + regState
+                                + " dataRadioTechnology=" + newDataRat);
+                    }
+                } else {
+
+                    // If the unsolicited signal strength comes just before data RAT family changes
+                    // (i.e. from UNKNOWN to LTE, CDMA to LTE, LTE to CDMA), the signal bar might
+                    // display the wrong information until the next unsolicited signal strength
+                    // information coming from the modem, which might take a long time to come or
+                    // even not come at all.  In order to provide the best user experience, we
+                    // query the latest signal information so it will show up on the UI on time.
+                    int oldDataRAT = mSS.getRilDataRadioTechnology();
+                    if (((oldDataRAT == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN)
+                            && (newDataRat != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN))
+                            || (ServiceState.isCdma(oldDataRAT) && ServiceState.isLte(newDataRat))
+                            || (ServiceState.isLte(oldDataRAT)
+                            && ServiceState.isCdma(newDataRat))) {
+                        mCi.getSignalStrength(obtainMessage(EVENT_GET_SIGNAL_STRENGTH));
+                    }
+
+                    // voice roaming state in done while handling EVENT_POLL_STATE_REGISTRATION_CDMA
+                    boolean isDataRoaming = regCodeIsRoaming(regState);
+                    mNewSS.setDataRoaming(isDataRoaming);
+                    // Save the data roaming state reported by modem registration before resource
+                    // overlay or carrier config possibly overrides it.
+                    mNewSS.setDataRoamingFromRegistration(isDataRoaming);
+                    if (DBG) {
+                        log("handlPollStateResultMessage: CdmaLteSST setDataRegState="
+                                + dataRegState + " regState=" + regState + " dataRadioTechnology="
+                                + newDataRat);
+                    }
+                }
+
+                updateServiceStateLteEarfcnBoost(mNewSS, getLteEarfcn(dataRegStateResult));
+                break;
+            }
+
+            case EVENT_POLL_STATE_OPERATOR: {
+                if (mPhone.isPhoneTypeGsm()) {
+                    String opNames[] = (String[]) ar.result;
+
+                    if (opNames != null && opNames.length >= 3) {
+                        // FIXME: Giving brandOverride higher precedence, is this desired?
+                        String brandOverride = mUiccController.getUiccCard(getPhoneId()) != null ?
+                                mUiccController.getUiccCard(getPhoneId()).getOperatorBrandOverride() : null;
+                        if (brandOverride != null) {
+                            log("EVENT_POLL_STATE_OPERATOR: use brandOverride=" + brandOverride);
+                            mNewSS.setOperatorName(brandOverride, brandOverride, opNames[2]);
+                        } else {
+                            mNewSS.setOperatorName(opNames[0], opNames[1], opNames[2]);
+                        }
+                    }
+                } else {
+                    String opNames[] = (String[])ar.result;
+
+                    if (opNames != null && opNames.length >= 3) {
+                        // TODO: Do we care about overriding in this case.
+                        // If the NUMERIC field isn't valid use PROPERTY_CDMA_HOME_OPERATOR_NUMERIC
+                        if ((opNames[2] == null) || (opNames[2].length() < 5)
+                                || ("00000".equals(opNames[2]))) {
+                            opNames[2] = SystemProperties.get(
+                                    GsmCdmaPhone.PROPERTY_CDMA_HOME_OPERATOR_NUMERIC, "00000");
+                            if (DBG) {
+                                log("RIL_REQUEST_OPERATOR.response[2], the numeric, " +
+                                        " is bad. Using SystemProperties '" +
+                                        GsmCdmaPhone.PROPERTY_CDMA_HOME_OPERATOR_NUMERIC +
+                                        "'= " + opNames[2]);
+                            }
+                        }
+
+                        if (!mIsSubscriptionFromRuim) {
+                            // NV device (as opposed to CSIM)
+                            mNewSS.setOperatorName(opNames[0], opNames[1], opNames[2]);
+                        } else {
+                            String brandOverride = mUiccController.getUiccCard(getPhoneId()) != null ?
+                                    mUiccController.getUiccCard(getPhoneId()).getOperatorBrandOverride() : null;
+                            if (brandOverride != null) {
+                                mNewSS.setOperatorName(brandOverride, brandOverride, opNames[2]);
+                            } else {
+                                mNewSS.setOperatorName(opNames[0], opNames[1], opNames[2]);
+                            }
+                        }
+                    } else {
+                        if (DBG) log("EVENT_POLL_STATE_OPERATOR_CDMA: error parsing opNames");
+                    }
+                }
+                break;
+            }
+
+            case EVENT_POLL_STATE_NETWORK_SELECTION_MODE: {
+                ints = (int[])ar.result;
+                mNewSS.setIsManualSelection(ints[0] == 1);
+                if ((ints[0] == 1) && (mPhone.shouldForceAutoNetworkSelect())) {
+                        /*
+                         * modem is currently in manual selection but manual
+                         * selection is not allowed in the current mode so
+                         * switch to automatic registration
+                         */
+                    mPhone.setNetworkSelectionModeAutomatic (null);
+                    log(" Forcing Automatic Network Selection, " +
+                            "manual selection is not allowed");
+                }
+                break;
+            }
+
+            default:
+                loge("handlePollStateResultMessage: Unexpected RIL response received: " + what);
+        }
+    }
+
+    /**
+     * Determine whether a roaming indicator is in the carrier-specified list of ERIs for
+     * home system
+     *
+     * @param roamInd roaming indicator in String
+     * @return true if the roamInd is in the carrier-specified list of ERIs for home network
+     */
+    private boolean isRoamIndForHomeSystem(String roamInd) {
+        // retrieve the carrier-specified list of ERIs for home system
+        String[] homeRoamIndicators = Resources.getSystem()
+                .getStringArray(com.android.internal.R.array.config_cdma_home_system);
+        log("isRoamIndForHomeSystem: homeRoamIndicators=" + Arrays.toString(homeRoamIndicators));
+
+        if (homeRoamIndicators != null) {
+            // searches through the comma-separated list for a match,
+            // return true if one is found.
+            for (String homeRoamInd : homeRoamIndicators) {
+                if (homeRoamInd.equals(roamInd)) {
+                    return true;
+                }
+            }
+            // no matches found against the list!
+            log("isRoamIndForHomeSystem: No match found against list for roamInd=" + roamInd);
+            return false;
+        }
+
+        // no system property found for the roaming indicators for home system
+        log("isRoamIndForHomeSystem: No list found");
+        return false;
+    }
+
+    /**
+     * Query the carrier configuration to determine if there any network overrides
+     * for roaming or not roaming for the current service state.
+     */
+    protected void updateRoamingState() {
+        if (mPhone.isPhoneTypeGsm()) {
+            /**
+             * Since the roaming state of gsm service (from +CREG) and
+             * data service (from +CGREG) could be different, the new SS
+             * is set to roaming when either is true.
+             *
+             * There are exceptions for the above rule.
+             * The new SS is not set as roaming while gsm service reports
+             * roaming but indeed it is same operator.
+             * And the operator is considered non roaming.
+             *
+             * The test for the operators is to handle special roaming
+             * agreements and MVNO's.
+             */
+            boolean roaming = (mGsmRoaming || mDataRoaming);
+
+            if (mGsmRoaming && !isOperatorConsideredRoaming(mNewSS)
+                    && (isSameNamedOperators(mNewSS) || isOperatorConsideredNonRoaming(mNewSS))) {
+                log("updateRoamingState: resource override set non roaming.isSameNamedOperators="
+                        + isSameNamedOperators(mNewSS) + ",isOperatorConsideredNonRoaming="
+                        + isOperatorConsideredNonRoaming(mNewSS));
+                roaming = false;
+            }
+
+            CarrierConfigManager configLoader = (CarrierConfigManager)
+                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+
+            if (configLoader != null) {
+                try {
+                    PersistableBundle b = configLoader.getConfigForSubId(mPhone.getSubId());
+
+                    if (alwaysOnHomeNetwork(b)) {
+                        log("updateRoamingState: carrier config override always on home network");
+                        roaming = false;
+                    } else if (isNonRoamingInGsmNetwork(b, mNewSS.getOperatorNumeric())) {
+                        log("updateRoamingState: carrier config override set non roaming:"
+                                + mNewSS.getOperatorNumeric());
+                        roaming = false;
+                    } else if (isRoamingInGsmNetwork(b, mNewSS.getOperatorNumeric())) {
+                        log("updateRoamingState: carrier config override set roaming:"
+                                + mNewSS.getOperatorNumeric());
+                        roaming = true;
+                    }
+                } catch (Exception e) {
+                    loge("updateRoamingState: unable to access carrier config service");
+                }
+            } else {
+                log("updateRoamingState: no carrier config service available");
+            }
+
+            mNewSS.setVoiceRoaming(roaming);
+            mNewSS.setDataRoaming(roaming);
+        } else {
+            CarrierConfigManager configLoader = (CarrierConfigManager)
+                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configLoader != null) {
+                try {
+                    PersistableBundle b = configLoader.getConfigForSubId(mPhone.getSubId());
+                    String systemId = Integer.toString(mNewSS.getSystemId());
+
+                    if (alwaysOnHomeNetwork(b)) {
+                        log("updateRoamingState: carrier config override always on home network");
+                        setRoamingOff();
+                    } else if (isNonRoamingInGsmNetwork(b, mNewSS.getOperatorNumeric())
+                            || isNonRoamingInCdmaNetwork(b, systemId)) {
+                        log("updateRoamingState: carrier config override set non-roaming:"
+                                + mNewSS.getOperatorNumeric() + ", " + systemId);
+                        setRoamingOff();
+                    } else if (isRoamingInGsmNetwork(b, mNewSS.getOperatorNumeric())
+                            || isRoamingInCdmaNetwork(b, systemId)) {
+                        log("updateRoamingState: carrier config override set roaming:"
+                                + mNewSS.getOperatorNumeric() + ", " + systemId);
+                        setRoamingOn();
+                    }
+                } catch (Exception e) {
+                    loge("updateRoamingState: unable to access carrier config service");
+                }
+            } else {
+                log("updateRoamingState: no carrier config service available");
+            }
+
+            if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean(PROP_FORCE_ROAMING, false)) {
+                mNewSS.setVoiceRoaming(true);
+                mNewSS.setDataRoaming(true);
+            }
+        }
+    }
+
+    private void setRoamingOn() {
+        mNewSS.setVoiceRoaming(true);
+        mNewSS.setDataRoaming(true);
+        mNewSS.setCdmaEriIconIndex(EriInfo.ROAMING_INDICATOR_ON);
+        mNewSS.setCdmaEriIconMode(EriInfo.ROAMING_ICON_MODE_NORMAL);
+    }
+
+    private void setRoamingOff() {
+        mNewSS.setVoiceRoaming(false);
+        mNewSS.setDataRoaming(false);
+        mNewSS.setCdmaEriIconIndex(EriInfo.ROAMING_INDICATOR_OFF);
+    }
+
+    protected void updateSpnDisplay() {
+        updateOperatorNameFromEri();
+
+        String wfcVoiceSpnFormat = null;
+        String wfcDataSpnFormat = null;
+        int combinedRegState = getCombinedRegState();
+        if (mPhone.getImsPhone() != null && mPhone.getImsPhone().isWifiCallingEnabled()
+                && (combinedRegState == ServiceState.STATE_IN_SERVICE)) {
+            // In Wi-Fi Calling mode show SPN or PLMN + WiFi Calling
+            //
+            // 1) Show SPN + Wi-Fi Calling If SIM has SPN and SPN display condition
+            //    is satisfied or SPN override is enabled for this carrier
+            //
+            // 2) Show PLMN + Wi-Fi Calling if there is no valid SPN in case 1
+
+            String[] wfcSpnFormats = mPhone.getContext().getResources().getStringArray(
+                    com.android.internal.R.array.wfcSpnFormats);
+            int voiceIdx = 0;
+            int dataIdx = 0;
+            CarrierConfigManager configLoader = (CarrierConfigManager)
+                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configLoader != null) {
+                try {
+                    PersistableBundle b = configLoader.getConfigForSubId(mPhone.getSubId());
+                    if (b != null) {
+                        voiceIdx = b.getInt(CarrierConfigManager.KEY_WFC_SPN_FORMAT_IDX_INT);
+                        dataIdx = b.getInt(
+                                CarrierConfigManager.KEY_WFC_DATA_SPN_FORMAT_IDX_INT);
+                    }
+                } catch (Exception e) {
+                    loge("updateSpnDisplay: carrier config error: " + e);
+                }
+            }
+
+            wfcVoiceSpnFormat = wfcSpnFormats[voiceIdx];
+            wfcDataSpnFormat = wfcSpnFormats[dataIdx];
+        }
+
+        if (mPhone.isPhoneTypeGsm()) {
+            // The values of plmn/showPlmn change in different scenarios.
+            // 1) No service but emergency call allowed -> expected
+            //    to show "Emergency call only"
+            //    EXTRA_SHOW_PLMN = true
+            //    EXTRA_PLMN = "Emergency call only"
+
+            // 2) No service at all --> expected to show "No service"
+            //    EXTRA_SHOW_PLMN = true
+            //    EXTRA_PLMN = "No service"
+
+            // 3) Normal operation in either home or roaming service
+            //    EXTRA_SHOW_PLMN = depending on IccRecords rule
+            //    EXTRA_PLMN = plmn
+
+            // 4) No service due to power off, aka airplane mode
+            //    EXTRA_SHOW_PLMN = false
+            //    EXTRA_PLMN = null
+
+            IccRecords iccRecords = mIccRecords;
+            String plmn = null;
+            boolean showPlmn = false;
+            int rule = (iccRecords != null) ? iccRecords.getDisplayRule(mSS.getOperatorNumeric()) : 0;
+            if (combinedRegState == ServiceState.STATE_OUT_OF_SERVICE
+                    || combinedRegState == ServiceState.STATE_EMERGENCY_ONLY) {
+                showPlmn = true;
+                if (mEmergencyOnly) {
+                    // No service but emergency call allowed
+                    plmn = Resources.getSystem().
+                            getText(com.android.internal.R.string.emergency_calls_only).toString();
+                } else {
+                    // No service at all
+                    plmn = Resources.getSystem().
+                            getText(com.android.internal.R.string.lockscreen_carrier_default).toString();
+                }
+                if (DBG) log("updateSpnDisplay: radio is on but out " +
+                        "of service, set plmn='" + plmn + "'");
+            } else if (combinedRegState == ServiceState.STATE_IN_SERVICE) {
+                // In either home or roaming service
+                plmn = mSS.getOperatorAlpha();
+                showPlmn = !TextUtils.isEmpty(plmn) &&
+                        ((rule & SIMRecords.SPN_RULE_SHOW_PLMN)
+                                == SIMRecords.SPN_RULE_SHOW_PLMN);
+            } else {
+                // Power off state, such as airplane mode, show plmn as "No service"
+                showPlmn = true;
+                plmn = Resources.getSystem().
+                        getText(com.android.internal.R.string.lockscreen_carrier_default).toString();
+                if (DBG) log("updateSpnDisplay: radio is off w/ showPlmn="
+                        + showPlmn + " plmn=" + plmn);
+            }
+
+            // The value of spn/showSpn are same in different scenarios.
+            //    EXTRA_SHOW_SPN = depending on IccRecords rule and radio/IMS state
+            //    EXTRA_SPN = spn
+            //    EXTRA_DATA_SPN = dataSpn
+            String spn = (iccRecords != null) ? iccRecords.getServiceProviderName() : "";
+            String dataSpn = spn;
+            boolean showSpn = !TextUtils.isEmpty(spn)
+                    && ((rule & SIMRecords.SPN_RULE_SHOW_SPN)
+                    == SIMRecords.SPN_RULE_SHOW_SPN);
+
+            if (!TextUtils.isEmpty(spn) && !TextUtils.isEmpty(wfcVoiceSpnFormat) &&
+                    !TextUtils.isEmpty(wfcDataSpnFormat)) {
+                // Show SPN + Wi-Fi Calling If SIM has SPN and SPN display condition
+                // is satisfied or SPN override is enabled for this carrier.
+
+                String originalSpn = spn.trim();
+                spn = String.format(wfcVoiceSpnFormat, originalSpn);
+                dataSpn = String.format(wfcDataSpnFormat, originalSpn);
+                showSpn = true;
+                showPlmn = false;
+            } else if (!TextUtils.isEmpty(plmn) && !TextUtils.isEmpty(wfcVoiceSpnFormat)) {
+                // Show PLMN + Wi-Fi Calling if there is no valid SPN in the above case
+
+                String originalPlmn = plmn.trim();
+                plmn = String.format(wfcVoiceSpnFormat, originalPlmn);
+            } else if (mSS.getVoiceRegState() == ServiceState.STATE_POWER_OFF
+                    || (showPlmn && TextUtils.equals(spn, plmn))) {
+                // airplane mode or spn equals plmn, do not show spn
+                spn = null;
+                showSpn = false;
+            }
+
+            int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            int[] subIds = SubscriptionManager.getSubId(mPhone.getPhoneId());
+            if (subIds != null && subIds.length > 0) {
+                subId = subIds[0];
+            }
+
+            // Update SPN_STRINGS_UPDATED_ACTION IFF any value changes
+            if (mSubId != subId ||
+                    showPlmn != mCurShowPlmn
+                    || showSpn != mCurShowSpn
+                    || !TextUtils.equals(spn, mCurSpn)
+                    || !TextUtils.equals(dataSpn, mCurDataSpn)
+                    || !TextUtils.equals(plmn, mCurPlmn)) {
+                if (DBG) {
+                    log(String.format("updateSpnDisplay: changed sending intent rule=" + rule +
+                            " showPlmn='%b' plmn='%s' showSpn='%b' spn='%s' dataSpn='%s' " +
+                            "subId='%d'", showPlmn, plmn, showSpn, spn, dataSpn, subId));
+                }
+                Intent intent = new Intent(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION);
+                intent.putExtra(TelephonyIntents.EXTRA_SHOW_SPN, showSpn);
+                intent.putExtra(TelephonyIntents.EXTRA_SPN, spn);
+                intent.putExtra(TelephonyIntents.EXTRA_DATA_SPN, dataSpn);
+                intent.putExtra(TelephonyIntents.EXTRA_SHOW_PLMN, showPlmn);
+                intent.putExtra(TelephonyIntents.EXTRA_PLMN, plmn);
+                SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
+                mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+
+                if (!mSubscriptionController.setPlmnSpn(mPhone.getPhoneId(),
+                        showPlmn, plmn, showSpn, spn)) {
+                    mSpnUpdatePending = true;
+                }
+            }
+
+            mSubId = subId;
+            mCurShowSpn = showSpn;
+            mCurShowPlmn = showPlmn;
+            mCurSpn = spn;
+            mCurDataSpn = dataSpn;
+            mCurPlmn = plmn;
+        } else {
+            // mOperatorAlpha contains the ERI text
+            String plmn = mSS.getOperatorAlpha();
+            boolean showPlmn = false;
+
+            showPlmn = plmn != null;
+
+            int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            int[] subIds = SubscriptionManager.getSubId(mPhone.getPhoneId());
+            if (subIds != null && subIds.length > 0) {
+                subId = subIds[0];
+            }
+
+            if (!TextUtils.isEmpty(plmn) && !TextUtils.isEmpty(wfcVoiceSpnFormat)) {
+                // In Wi-Fi Calling mode show SPN+WiFi
+
+                String originalPlmn = plmn.trim();
+                plmn = String.format(wfcVoiceSpnFormat, originalPlmn);
+            } else if (mCi.getRadioState() == CommandsInterface.RadioState.RADIO_OFF) {
+                // todo: temporary hack; should have a better fix. This is to avoid using operator
+                // name from ServiceState (populated in resetServiceStateInIwlanMode()) until
+                // wifi calling is actually enabled
+                log("updateSpnDisplay: overwriting plmn from " + plmn + " to null as radio " +
+                        "state is off");
+                plmn = null;
+            }
+
+            if (combinedRegState == ServiceState.STATE_OUT_OF_SERVICE) {
+                plmn = Resources.getSystem().getText(com.android.internal.R.string
+                        .lockscreen_carrier_default).toString();
+                if (DBG) {
+                    log("updateSpnDisplay: radio is on but out of svc, set plmn='" + plmn + "'");
+                }
+            }
+
+            if (mSubId != subId || !TextUtils.equals(plmn, mCurPlmn)) {
+                // Allow A blank plmn, "" to set showPlmn to true. Previously, we
+                // would set showPlmn to true only if plmn was not empty, i.e. was not
+                // null and not blank. But this would cause us to incorrectly display
+                // "No Service". Now showPlmn is set to true for any non null string.
+                if (DBG) {
+                    log(String.format("updateSpnDisplay: changed sending intent" +
+                            " showPlmn='%b' plmn='%s' subId='%d'", showPlmn, plmn, subId));
+                }
+                Intent intent = new Intent(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION);
+                intent.putExtra(TelephonyIntents.EXTRA_SHOW_SPN, false);
+                intent.putExtra(TelephonyIntents.EXTRA_SPN, "");
+                intent.putExtra(TelephonyIntents.EXTRA_SHOW_PLMN, showPlmn);
+                intent.putExtra(TelephonyIntents.EXTRA_PLMN, plmn);
+                SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
+                mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+
+                if (!mSubscriptionController.setPlmnSpn(mPhone.getPhoneId(),
+                        showPlmn, plmn, false, "")) {
+                    mSpnUpdatePending = true;
+                }
+            }
+
+            mSubId = subId;
+            mCurShowSpn = false;
+            mCurShowPlmn = showPlmn;
+            mCurSpn = "";
+            mCurPlmn = plmn;
+        }
+    }
+
+    protected void setPowerStateToDesired() {
+        if (DBG) {
+            String tmpLog = "mDeviceShuttingDown=" + mDeviceShuttingDown +
+                    ", mDesiredPowerState=" + mDesiredPowerState +
+                    ", getRadioState=" + mCi.getRadioState() +
+                    ", mPowerOffDelayNeed=" + mPowerOffDelayNeed +
+                    ", mAlarmSwitch=" + mAlarmSwitch +
+                    ", mRadioDisabledByCarrier=" + mRadioDisabledByCarrier;
+            log(tmpLog);
+            mRadioPowerLog.log(tmpLog);
+        }
+
+        if (mPhone.isPhoneTypeGsm() && mAlarmSwitch) {
+            if(DBG) log("mAlarmSwitch == true");
+            Context context = mPhone.getContext();
+            AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+            am.cancel(mRadioOffIntent);
+            mAlarmSwitch = false;
+        }
+
+        // If we want it on and it's off, turn it on
+        if (mDesiredPowerState && !mRadioDisabledByCarrier
+                && mCi.getRadioState() == CommandsInterface.RadioState.RADIO_OFF) {
+            mCi.setRadioPower(true, null);
+        } else if ((!mDesiredPowerState || mRadioDisabledByCarrier) && mCi.getRadioState().isOn()) {
+            // If it's on and available and we want it off gracefully
+            if (mPhone.isPhoneTypeGsm() && mPowerOffDelayNeed) {
+                if (mImsRegistrationOnOff && !mAlarmSwitch) {
+                    if(DBG) log("mImsRegistrationOnOff == true");
+                    Context context = mPhone.getContext();
+                    AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+
+                    Intent intent = new Intent(ACTION_RADIO_OFF);
+                    mRadioOffIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+
+                    mAlarmSwitch = true;
+                    if (DBG) log("Alarm setting");
+                    am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                            SystemClock.elapsedRealtime() + 3000, mRadioOffIntent);
+                } else {
+                    DcTracker dcTracker = mPhone.mDcTracker;
+                    powerOffRadioSafely(dcTracker);
+                }
+            } else {
+                DcTracker dcTracker = mPhone.mDcTracker;
+                powerOffRadioSafely(dcTracker);
+            }
+        } else if (mDeviceShuttingDown && mCi.getRadioState().isAvailable()) {
+            mCi.requestShutdown(null);
+        }
+    }
+
+    protected void onUpdateIccAvailability() {
+        if (mUiccController == null ) {
+            return;
+        }
+
+        UiccCardApplication newUiccApplication = getUiccCardApplication();
+
+        if (mUiccApplcation != newUiccApplication) {
+            if (mUiccApplcation != null) {
+                log("Removing stale icc objects.");
+                mUiccApplcation.unregisterForReady(this);
+                if (mIccRecords != null) {
+                    mIccRecords.unregisterForRecordsLoaded(this);
+                }
+                mIccRecords = null;
+                mUiccApplcation = null;
+            }
+            if (newUiccApplication != null) {
+                log("New card found");
+                mUiccApplcation = newUiccApplication;
+                mIccRecords = mUiccApplcation.getIccRecords();
+                if (mPhone.isPhoneTypeGsm()) {
+                    mUiccApplcation.registerForReady(this, EVENT_SIM_READY, null);
+                    if (mIccRecords != null) {
+                        mIccRecords.registerForRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
+                    }
+                } else if (mIsSubscriptionFromRuim) {
+                    mUiccApplcation.registerForReady(this, EVENT_RUIM_READY, null);
+                    if (mIccRecords != null) {
+                        mIccRecords.registerForRecordsLoaded(this, EVENT_RUIM_RECORDS_LOADED, null);
+                    }
+                }
+            }
+        }
+    }
+
+    private void logRoamingChange() {
+        mRoamingLog.log(mSS.toString());
+    }
+
+    private void logAttachChange() {
+        mAttachLog.log(mSS.toString());
+    }
+
+    private void logPhoneTypeChange() {
+        mPhoneTypeLog.log(Integer.toString(mPhone.getPhoneType()));
+    }
+
+    private void logRatChange() {
+        mRatLog.log(mSS.toString());
+    }
+
+    protected void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    protected void loge(String s) {
+        Rlog.e(LOG_TAG, s);
+    }
+
+    /**
+     * @return The current GPRS state. IN_SERVICE is the same as "attached"
+     * and OUT_OF_SERVICE is the same as detached.
+     */
+    public int getCurrentDataConnectionState() {
+        return mSS.getDataRegState();
+    }
+
+    /**
+     * @return true if phone is camping on a technology (eg UMTS)
+     * that could support voice and data simultaneously.
+     */
+    public boolean isConcurrentVoiceAndDataAllowed() {
+        if (mSS.getCssIndicator() == 1) {
+            // Checking the Concurrent Service Supported flag first for all phone types.
+            return true;
+        } else if (mPhone.isPhoneTypeGsm()) {
+            return (mSS.getRilDataRadioTechnology() >= ServiceState.RIL_RADIO_TECHNOLOGY_UMTS);
+        } else {
+            return false;
+        }
+    }
+
+    public void setImsRegistrationState(boolean registered) {
+        log("ImsRegistrationState - registered : " + registered);
+
+        if (mImsRegistrationOnOff && !registered) {
+            if (mAlarmSwitch) {
+                mImsRegistrationOnOff = registered;
+
+                Context context = mPhone.getContext();
+                AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+                am.cancel(mRadioOffIntent);
+                mAlarmSwitch = false;
+
+                sendMessage(obtainMessage(EVENT_CHANGE_IMS_STATE));
+                return;
+            }
+        }
+        mImsRegistrationOnOff = registered;
+    }
+
+    public void onImsCapabilityChanged() {
+        sendMessage(obtainMessage(EVENT_IMS_CAPABILITY_CHANGED));
+    }
+
+    public boolean isRadioOn() {
+        return mCi.getRadioState() == CommandsInterface.RadioState.RADIO_ON;
+    }
+
+    /**
+     * A complete "service state" from our perspective is
+     * composed of a handful of separate requests to the radio.
+     *
+     * We make all of these requests at once, but then abandon them
+     * and start over again if the radio notifies us that some
+     * event has changed
+     */
+    public void pollState() {
+        pollState(false);
+    }
+    /**
+     * We insist on polling even if the radio says its off.
+     * Used when we get a network changed notification
+     * but the radio is off - part of iwlan hack
+     */
+    private void modemTriggeredPollState() {
+        pollState(true);
+    }
+
+    public void pollState(boolean modemTriggered) {
+        mPollingContext = new int[1];
+        mPollingContext[0] = 0;
+
+        log("pollState: modemTriggered=" + modemTriggered);
+
+        switch (mCi.getRadioState()) {
+            case RADIO_UNAVAILABLE:
+                mNewSS.setStateOutOfService();
+                mNewCellLoc.setStateInvalid();
+                setSignalStrengthDefaultValues();
+                mGotCountryCode = false;
+                mNitzUpdatedTime = false;
+                pollStateDone();
+                break;
+
+            case RADIO_OFF:
+                mNewSS.setStateOff();
+                mNewCellLoc.setStateInvalid();
+                setSignalStrengthDefaultValues();
+                mGotCountryCode = false;
+                mNitzUpdatedTime = false;
+                // don't poll when device is shutting down or the poll was not modemTrigged
+                // (they sent us new radio data) and current network is not IWLAN
+                if (mDeviceShuttingDown ||
+                        (!modemTriggered && ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                        != mSS.getRilDataRadioTechnology())) {
+                    pollStateDone();
+                    break;
+                }
+
+            default:
+                // Issue all poll-related commands at once then count down the responses, which
+                // are allowed to arrive out-of-order
+                mPollingContext[0]++;
+                mCi.getOperator(obtainMessage(EVENT_POLL_STATE_OPERATOR, mPollingContext));
+
+                mPollingContext[0]++;
+                mCi.getDataRegistrationState(obtainMessage(EVENT_POLL_STATE_GPRS, mPollingContext));
+
+                mPollingContext[0]++;
+                mCi.getVoiceRegistrationState(obtainMessage(EVENT_POLL_STATE_REGISTRATION,
+                        mPollingContext));
+
+                if (mPhone.isPhoneTypeGsm()) {
+                    mPollingContext[0]++;
+                    mCi.getNetworkSelectionMode(obtainMessage(
+                            EVENT_POLL_STATE_NETWORK_SELECTION_MODE, mPollingContext));
+                }
+                break;
+        }
+    }
+
+    private void pollStateDone() {
+        if (!mPhone.isPhoneTypeGsm()) {
+            updateRoamingState();
+        }
+
+        if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean(PROP_FORCE_ROAMING, false)) {
+            mNewSS.setVoiceRoaming(true);
+            mNewSS.setDataRoaming(true);
+        }
+        useDataRegStateForDataOnlyDevices();
+        resetServiceStateInIwlanMode();
+
+        if (DBG) {
+            log("Poll ServiceState done: "
+                    + " oldSS=[" + mSS + "] newSS=[" + mNewSS + "]"
+                    + " oldMaxDataCalls=" + mMaxDataCalls
+                    + " mNewMaxDataCalls=" + mNewMaxDataCalls
+                    + " oldReasonDataDenied=" + mReasonDataDenied
+                    + " mNewReasonDataDenied=" + mNewReasonDataDenied);
+        }
+
+        boolean hasRegistered =
+                mSS.getVoiceRegState() != ServiceState.STATE_IN_SERVICE
+                        && mNewSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE;
+
+        boolean hasDeregistered =
+                mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE
+                        && mNewSS.getVoiceRegState() != ServiceState.STATE_IN_SERVICE;
+
+        boolean hasDataAttached =
+                mSS.getDataRegState() != ServiceState.STATE_IN_SERVICE
+                        && mNewSS.getDataRegState() == ServiceState.STATE_IN_SERVICE;
+
+        boolean hasDataDetached =
+                mSS.getDataRegState() == ServiceState.STATE_IN_SERVICE
+                        && mNewSS.getDataRegState() != ServiceState.STATE_IN_SERVICE;
+
+        boolean hasDataRegStateChanged =
+                mSS.getDataRegState() != mNewSS.getDataRegState();
+
+        boolean hasVoiceRegStateChanged =
+                mSS.getVoiceRegState() != mNewSS.getVoiceRegState();
+
+        boolean hasLocationChanged = !mNewCellLoc.equals(mCellLoc);
+
+        // ratchet the new tech up through it's rat family but don't drop back down
+        // until cell change
+        if (!hasLocationChanged) {
+            mRatRatcheter.ratchetRat(mSS, mNewSS);
+        }
+
+        boolean hasRilVoiceRadioTechnologyChanged =
+                mSS.getRilVoiceRadioTechnology() != mNewSS.getRilVoiceRadioTechnology();
+
+        boolean hasRilDataRadioTechnologyChanged =
+                mSS.getRilDataRadioTechnology() != mNewSS.getRilDataRadioTechnology();
+
+        boolean hasChanged = !mNewSS.equals(mSS);
+
+        boolean hasVoiceRoamingOn = !mSS.getVoiceRoaming() && mNewSS.getVoiceRoaming();
+
+        boolean hasVoiceRoamingOff = mSS.getVoiceRoaming() && !mNewSS.getVoiceRoaming();
+
+        boolean hasDataRoamingOn = !mSS.getDataRoaming() && mNewSS.getDataRoaming();
+
+        boolean hasDataRoamingOff = mSS.getDataRoaming() && !mNewSS.getDataRoaming();
+
+        boolean hasRejectCauseChanged = mRejectCode != mNewRejectCode;
+
+        boolean hasCssIndicatorChanged = (mSS.getCssIndicator() != mNewSS.getCssIndicator());
+
+        boolean has4gHandoff = false;
+        boolean hasMultiApnSupport = false;
+        boolean hasLostMultiApnSupport = false;
+        if (mPhone.isPhoneTypeCdmaLte()) {
+            has4gHandoff = mNewSS.getDataRegState() == ServiceState.STATE_IN_SERVICE
+                    && ((ServiceState.isLte(mSS.getRilDataRadioTechnology())
+                    && (mNewSS.getRilDataRadioTechnology()
+                    == ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD))
+                    ||
+                    ((mSS.getRilDataRadioTechnology()
+                            == ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD)
+                            && ServiceState.isLte(mNewSS.getRilDataRadioTechnology())));
+
+            hasMultiApnSupport = ((ServiceState.isLte(mNewSS.getRilDataRadioTechnology())
+                    || (mNewSS.getRilDataRadioTechnology()
+                    == ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD))
+                    &&
+                    (!ServiceState.isLte(mSS.getRilDataRadioTechnology())
+                            && (mSS.getRilDataRadioTechnology()
+                            != ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD)));
+
+            hasLostMultiApnSupport =
+                    ((mNewSS.getRilDataRadioTechnology()
+                            >= ServiceState.RIL_RADIO_TECHNOLOGY_IS95A)
+                            && (mNewSS.getRilDataRadioTechnology()
+                            <= ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_A));
+        }
+
+        if (DBG) {
+            log("pollStateDone:"
+                    + " hasRegistered=" + hasRegistered
+                    + " hasDeregistered=" + hasDeregistered
+                    + " hasDataAttached=" + hasDataAttached
+                    + " hasDataDetached=" + hasDataDetached
+                    + " hasDataRegStateChanged=" + hasDataRegStateChanged
+                    + " hasRilVoiceRadioTechnologyChanged= " + hasRilVoiceRadioTechnologyChanged
+                    + " hasRilDataRadioTechnologyChanged=" + hasRilDataRadioTechnologyChanged
+                    + " hasChanged=" + hasChanged
+                    + " hasVoiceRoamingOn=" + hasVoiceRoamingOn
+                    + " hasVoiceRoamingOff=" + hasVoiceRoamingOff
+                    + " hasDataRoamingOn=" + hasDataRoamingOn
+                    + " hasDataRoamingOff=" + hasDataRoamingOff
+                    + " hasLocationChanged=" + hasLocationChanged
+                    + " has4gHandoff = " + has4gHandoff
+                    + " hasMultiApnSupport=" + hasMultiApnSupport
+                    + " hasLostMultiApnSupport=" + hasLostMultiApnSupport
+                    + " hasCssIndicatorChanged=" + hasCssIndicatorChanged);
+        }
+
+        // Add an event log when connection state changes
+        if (hasVoiceRegStateChanged || hasDataRegStateChanged) {
+            EventLog.writeEvent(mPhone.isPhoneTypeGsm() ? EventLogTags.GSM_SERVICE_STATE_CHANGE :
+                            EventLogTags.CDMA_SERVICE_STATE_CHANGE,
+                    mSS.getVoiceRegState(), mSS.getDataRegState(),
+                    mNewSS.getVoiceRegState(), mNewSS.getDataRegState());
+        }
+
+        if (mPhone.isPhoneTypeGsm()) {
+            // Add an event log when network type switched
+            // TODO: we may add filtering to reduce the event logged,
+            // i.e. check preferred network setting, only switch to 2G, etc
+            if (hasRilVoiceRadioTechnologyChanged) {
+                int cid = -1;
+                GsmCellLocation loc = (GsmCellLocation) mNewCellLoc;
+                if (loc != null) cid = loc.getCid();
+                // NOTE: this code was previously located after mSS and mNewSS are swapped, so
+                // existing logs were incorrectly using the new state for "network_from"
+                // and STATE_OUT_OF_SERVICE for "network_to". To avoid confusion, use a new log tag
+                // to record the correct states.
+                EventLog.writeEvent(EventLogTags.GSM_RAT_SWITCHED_NEW, cid,
+                        mSS.getRilVoiceRadioTechnology(),
+                        mNewSS.getRilVoiceRadioTechnology());
+                if (DBG) {
+                    log("RAT switched "
+                            + ServiceState.rilRadioTechnologyToString(
+                            mSS.getRilVoiceRadioTechnology())
+                            + " -> "
+                            + ServiceState.rilRadioTechnologyToString(
+                            mNewSS.getRilVoiceRadioTechnology()) + " at cell " + cid);
+                }
+            }
+
+            if (hasCssIndicatorChanged) {
+                mPhone.notifyDataConnection(Phone.REASON_CSS_INDICATOR_CHANGED);
+            }
+
+            mReasonDataDenied = mNewReasonDataDenied;
+            mMaxDataCalls = mNewMaxDataCalls;
+            mRejectCode = mNewRejectCode;
+        }
+
+        // swap mSS and mNewSS to put new state in mSS
+        ServiceState tss = mSS;
+        mSS = mNewSS;
+        mNewSS = tss;
+        // clean slate for next time
+        mNewSS.setStateOutOfService();
+
+        // swap mCellLoc and mNewCellLoc to put new state in mCellLoc
+        CellLocation tcl = mCellLoc;
+        mCellLoc = mNewCellLoc;
+        mNewCellLoc = tcl;
+
+        if (hasRilVoiceRadioTechnologyChanged) {
+            updatePhoneObject();
+        }
+
+        TelephonyManager tm =
+                (TelephonyManager) mPhone.getContext().getSystemService(Context.TELEPHONY_SERVICE);
+
+        if (hasRilDataRadioTechnologyChanged) {
+            tm.setDataNetworkTypeForPhone(mPhone.getPhoneId(), mSS.getRilDataRadioTechnology());
+
+            if (ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                    == mSS.getRilDataRadioTechnology()) {
+                log("pollStateDone: IWLAN enabled");
+            }
+        }
+
+        if (hasRegistered) {
+            mNetworkAttachedRegistrants.notifyRegistrants();
+
+            if (DBG) {
+                log("pollStateDone: registering current mNitzUpdatedTime=" + mNitzUpdatedTime
+                        + " changing to false");
+            }
+            mNitzUpdatedTime = false;
+        }
+
+        if (hasDeregistered) {
+            mNetworkDetachedRegistrants.notifyRegistrants();
+        }
+
+        if (hasRejectCauseChanged) {
+            setNotification(mRejectCode == 0 ? CS_REJECT_CAUSE_DISABLED : CS_REJECT_CAUSE_ENABLED);
+        }
+
+        if (hasChanged) {
+            updateSpnDisplay();
+
+            tm.setNetworkOperatorNameForPhone(mPhone.getPhoneId(), mSS.getOperatorAlpha());
+
+            String prevOperatorNumeric = tm.getNetworkOperatorForPhone(mPhone.getPhoneId());
+            String operatorNumeric = mSS.getOperatorNumeric();
+
+            if (!mPhone.isPhoneTypeGsm()) {
+                // try to fix the invalid Operator Numeric
+                if (isInvalidOperatorNumeric(operatorNumeric)) {
+                    int sid = mSS.getSystemId();
+                    operatorNumeric = fixUnknownMcc(operatorNumeric, sid);
+                }
+            }
+
+            tm.setNetworkOperatorNumericForPhone(mPhone.getPhoneId(), operatorNumeric);
+            updateCarrierMccMncConfiguration(operatorNumeric,
+                    prevOperatorNumeric, mPhone.getContext());
+            if (isInvalidOperatorNumeric(operatorNumeric)) {
+                if (DBG) log("operatorNumeric " + operatorNumeric + " is invalid");
+                tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), "");
+                mGotCountryCode = false;
+                mNitzUpdatedTime = false;
+            } else if (mSS.getRilDataRadioTechnology() != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN) {
+                // Update time zone, ISO, and IDD.
+                //
+                // If the device is on IWLAN, modems manufacture a ServiceState with the MCC/MNC of
+                // the SIM as if we were talking to towers. Telephony code then uses that with
+                // mccTable to suggest a timezone. We shouldn't do that if the MCC/MNC is from IWLAN
+
+                String iso = "";
+                String mcc = "";
+                try {
+                    mcc = operatorNumeric.substring(0, 3);
+                    iso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
+                } catch (NumberFormatException | StringIndexOutOfBoundsException ex) {
+                    loge("pollStateDone: countryCodeForMcc error: " + ex);
+                }
+
+                tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), iso);
+                mGotCountryCode = true;
+
+                if (!mNitzUpdatedTime && !mcc.equals("000") && !TextUtils.isEmpty(iso)
+                        && getAutoTimeZone()) {
+                    updateTimeZoneByNetworkCountryCode(iso);
+                }
+
+                if (!mPhone.isPhoneTypeGsm()) {
+                    setOperatorIdd(operatorNumeric);
+                }
+
+                if (shouldFixTimeZoneNow(mPhone, operatorNumeric, prevOperatorNumeric,
+                        mNeedFixZoneAfterNitz)) {
+                    fixTimeZone(iso);
+                }
+            }
+
+            tm.setNetworkRoamingForPhone(mPhone.getPhoneId(),
+                    mPhone.isPhoneTypeGsm() ? mSS.getVoiceRoaming() :
+                            (mSS.getVoiceRoaming() || mSS.getDataRoaming()));
+
+            setRoamingType(mSS);
+            log("Broadcasting ServiceState : " + mSS);
+            // notify using PhoneStateListener and the legacy intent ACTION_SERVICE_STATE_CHANGED
+            mPhone.notifyServiceStateChanged(mSS);
+
+            // insert into ServiceStateProvider. This will trigger apps to wake through JobScheduler
+            mPhone.getContext().getContentResolver()
+                    .insert(getUriForSubscriptionId(mPhone.getSubId()),
+                            getContentValuesForServiceState(mSS));
+
+            TelephonyMetrics.getInstance().writeServiceStateChanged(mPhone.getPhoneId(), mSS);
+        }
+
+        if (hasDataAttached || has4gHandoff || hasDataDetached || hasRegistered
+                || hasDeregistered) {
+            logAttachChange();
+        }
+
+        if (hasDataAttached || has4gHandoff) {
+            mAttachedRegistrants.notifyRegistrants();
+        }
+
+        if (hasDataDetached) {
+            mDetachedRegistrants.notifyRegistrants();
+        }
+
+        if (hasRilDataRadioTechnologyChanged || hasRilVoiceRadioTechnologyChanged) {
+            logRatChange();
+        }
+
+        if (hasDataRegStateChanged || hasRilDataRadioTechnologyChanged) {
+            notifyDataRegStateRilRadioTechnologyChanged();
+
+            if (ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                    == mSS.getRilDataRadioTechnology()) {
+                mPhone.notifyDataConnection(Phone.REASON_IWLAN_AVAILABLE);
+            } else {
+                mPhone.notifyDataConnection(null);
+            }
+        }
+
+        if (hasVoiceRoamingOn || hasVoiceRoamingOff || hasDataRoamingOn || hasDataRoamingOff) {
+            logRoamingChange();
+        }
+
+        if (hasVoiceRoamingOn) {
+            mVoiceRoamingOnRegistrants.notifyRegistrants();
+        }
+
+        if (hasVoiceRoamingOff) {
+            mVoiceRoamingOffRegistrants.notifyRegistrants();
+        }
+
+        if (hasDataRoamingOn) {
+            mDataRoamingOnRegistrants.notifyRegistrants();
+        }
+
+        if (hasDataRoamingOff) {
+            mDataRoamingOffRegistrants.notifyRegistrants();
+        }
+
+        if (hasLocationChanged) {
+            mPhone.notifyLocationChanged();
+        }
+
+        if (mPhone.isPhoneTypeGsm()) {
+            if (!isGprsConsistent(mSS.getDataRegState(), mSS.getVoiceRegState())) {
+                if (!mStartedGprsRegCheck && !mReportedGprsNoReg) {
+                    mStartedGprsRegCheck = true;
+
+                    int check_period = Settings.Global.getInt(
+                            mPhone.getContext().getContentResolver(),
+                            Settings.Global.GPRS_REGISTER_CHECK_PERIOD_MS,
+                            DEFAULT_GPRS_CHECK_PERIOD_MILLIS);
+                    sendMessageDelayed(obtainMessage(EVENT_CHECK_REPORT_GPRS),
+                            check_period);
+                }
+            } else {
+                mReportedGprsNoReg = false;
+            }
+        }
+    }
+
+    private void updateOperatorNameFromEri() {
+        if (mPhone.isPhoneTypeCdma()) {
+            if ((mCi.getRadioState().isOn()) && (!mIsSubscriptionFromRuim)) {
+                String eriText;
+                // Now the Phone sees the new ServiceState so it can get the new ERI text
+                if (mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE) {
+                    eriText = mPhone.getCdmaEriText();
+                } else {
+                    // Note that ServiceState.STATE_OUT_OF_SERVICE is valid used for
+                    // mRegistrationState 0,2,3 and 4
+                    eriText = mPhone.getContext().getText(
+                            com.android.internal.R.string.roamingTextSearching).toString();
+                }
+                mSS.setOperatorAlphaLong(eriText);
+            }
+        } else if (mPhone.isPhoneTypeCdmaLte()) {
+            boolean hasBrandOverride = mUiccController.getUiccCard(getPhoneId()) != null &&
+                    mUiccController.getUiccCard(getPhoneId()).getOperatorBrandOverride() != null;
+            if (!hasBrandOverride && (mCi.getRadioState().isOn()) && (mPhone.isEriFileLoaded()) &&
+                    (!ServiceState.isLte(mSS.getRilVoiceRadioTechnology()) ||
+                            mPhone.getContext().getResources().getBoolean(com.android.internal.R.
+                                    bool.config_LTE_eri_for_network_name))) {
+                // Only when CDMA is in service, ERI will take effect
+                String eriText = mSS.getOperatorAlpha();
+                // Now the Phone sees the new ServiceState so it can get the new ERI text
+                if (mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE) {
+                    eriText = mPhone.getCdmaEriText();
+                } else if (mSS.getVoiceRegState() == ServiceState.STATE_POWER_OFF) {
+                    eriText = (mIccRecords != null) ? mIccRecords.getServiceProviderName() : null;
+                    if (TextUtils.isEmpty(eriText)) {
+                        // Sets operator alpha property by retrieving from
+                        // build-time system property
+                        eriText = SystemProperties.get("ro.cdma.home.operator.alpha");
+                    }
+                } else if (mSS.getDataRegState() != ServiceState.STATE_IN_SERVICE) {
+                    // Note that ServiceState.STATE_OUT_OF_SERVICE is valid used
+                    // for mRegistrationState 0,2,3 and 4
+                    eriText = mPhone.getContext()
+                            .getText(com.android.internal.R.string.roamingTextSearching).toString();
+                }
+                mSS.setOperatorAlphaLong(eriText);
+            }
+
+            if (mUiccApplcation != null && mUiccApplcation.getState() == AppState.APPSTATE_READY &&
+                    mIccRecords != null && getCombinedRegState() == ServiceState.STATE_IN_SERVICE
+                    && !ServiceState.isLte(mSS.getRilVoiceRadioTechnology())) {
+                // SIM is found on the device. If ERI roaming is OFF, and SID/NID matches
+                // one configured in SIM, use operator name from CSIM record. Note that ERI, SID,
+                // and NID are CDMA only, not applicable to LTE.
+                boolean showSpn =
+                        ((RuimRecords) mIccRecords).getCsimSpnDisplayCondition();
+                int iconIndex = mSS.getCdmaEriIconIndex();
+
+                if (showSpn && (iconIndex == EriInfo.ROAMING_INDICATOR_OFF) &&
+                        isInHomeSidNid(mSS.getSystemId(), mSS.getNetworkId()) &&
+                        mIccRecords != null) {
+                    mSS.setOperatorAlphaLong(mIccRecords.getServiceProviderName());
+                }
+            }
+        }
+    }
+
+    /**
+     * Check whether the specified SID and NID pair appears in the HOME SID/NID list
+     * read from NV or SIM.
+     *
+     * @return true if provided sid/nid pair belongs to operator's home network.
+     */
+    private boolean isInHomeSidNid(int sid, int nid) {
+        // if SID/NID is not available, assume this is home network.
+        if (isSidsAllZeros()) return true;
+
+        // length of SID/NID shold be same
+        if (mHomeSystemId.length != mHomeNetworkId.length) return true;
+
+        if (sid == 0) return true;
+
+        for (int i = 0; i < mHomeSystemId.length; i++) {
+            // Use SID only if NID is a reserved value.
+            // SID 0 and NID 0 and 65535 are reserved. (C.0005 2.6.5.2)
+            if ((mHomeSystemId[i] == sid) &&
+                    ((mHomeNetworkId[i] == 0) || (mHomeNetworkId[i] == 65535) ||
+                            (nid == 0) || (nid == 65535) || (mHomeNetworkId[i] == nid))) {
+                return true;
+            }
+        }
+        // SID/NID are not in the list. So device is not in home network
+        return false;
+    }
+
+    protected void setOperatorIdd(String operatorNumeric) {
+        // Retrieve the current country information
+        // with the MCC got from opeatorNumeric.
+        String idd = mHbpcdUtils.getIddByMcc(
+                Integer.parseInt(operatorNumeric.substring(0,3)));
+        if (idd != null && !idd.isEmpty()) {
+            mPhone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_IDP_STRING,
+                    idd);
+        } else {
+            // use default "+", since we don't know the current IDP
+            mPhone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_IDP_STRING, "+");
+        }
+    }
+
+    protected boolean isInvalidOperatorNumeric(String operatorNumeric) {
+        return operatorNumeric == null || operatorNumeric.length() < 5 ||
+                operatorNumeric.startsWith(INVALID_MCC);
+    }
+
+    protected String fixUnknownMcc(String operatorNumeric, int sid) {
+        if (sid <= 0) {
+            // no cdma information is available, do nothing
+            return operatorNumeric;
+        }
+
+        // resolve the mcc from sid;
+        // if mSavedTimeZone is null, TimeZone would get the default timeZone,
+        // and the fixTimeZone couldn't help, because it depends on operator Numeric;
+        // if the sid is conflict and timezone is unavailable, the mcc may be not right.
+        boolean isNitzTimeZone = false;
+        int timeZone = 0;
+        TimeZone tzone = null;
+        if (mSavedTimeZone != null) {
+            timeZone =
+                    TimeZone.getTimeZone(mSavedTimeZone).getRawOffset()/MS_PER_HOUR;
+            isNitzTimeZone = true;
+        } else {
+            tzone = getNitzTimeZone(mZoneOffset, mZoneDst, mZoneTime);
+            if (tzone != null)
+                timeZone = tzone.getRawOffset()/MS_PER_HOUR;
+        }
+
+        int mcc = mHbpcdUtils.getMcc(sid,
+                timeZone, (mZoneDst ? 1 : 0), isNitzTimeZone);
+        if (mcc > 0) {
+            operatorNumeric = Integer.toString(mcc) + DEFAULT_MNC;
+        }
+        return operatorNumeric;
+    }
+
+    protected void fixTimeZone(String isoCountryCode) {
+        TimeZone zone = null;
+        // If the offset is (0, false) and the time zone property
+        // is set, use the time zone property rather than GMT.
+        final String zoneName = SystemProperties.get(TIMEZONE_PROPERTY);
+        if (DBG) {
+            log("fixTimeZone zoneName='" + zoneName +
+                    "' mZoneOffset=" + mZoneOffset + " mZoneDst=" + mZoneDst +
+                    " iso-cc='" + isoCountryCode +
+                    "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode));
+        }
+        if ("".equals(isoCountryCode) && mNeedFixZoneAfterNitz) {
+            // Country code not found.  This is likely a test network.
+            // Get a TimeZone based only on the NITZ parameters (best guess).
+            zone = getNitzTimeZone(mZoneOffset, mZoneDst, mZoneTime);
+            if (DBG) log("pollStateDone: using NITZ TimeZone");
+        } else if ((mZoneOffset == 0) && (mZoneDst == false) && (zoneName != null)
+                && (zoneName.length() > 0)
+                && (Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode) < 0)) {
+            // For NITZ string without time zone,
+            // need adjust time to reflect default time zone setting
+            zone = TimeZone.getDefault();
+            if (mNeedFixZoneAfterNitz) {
+                long ctm = System.currentTimeMillis();
+                long tzOffset = zone.getOffset(ctm);
+                if (DBG) {
+                    log("fixTimeZone: tzOffset=" + tzOffset +
+                            " ltod=" + TimeUtils.logTimeOfDay(ctm));
+                }
+                if (getAutoTime()) {
+                    long adj = ctm - tzOffset;
+                    if (DBG) log("fixTimeZone: adj ltod=" + TimeUtils.logTimeOfDay(adj));
+                    setAndBroadcastNetworkSetTime(adj);
+                } else {
+                    // Adjust the saved NITZ time to account for tzOffset.
+                    mSavedTime = mSavedTime - tzOffset;
+                    if (DBG) log("fixTimeZone: adj mSavedTime=" + mSavedTime);
+                }
+            }
+            if (DBG) log("fixTimeZone: using default TimeZone");
+        } else {
+            zone = TimeUtils.getTimeZone(mZoneOffset, mZoneDst, mZoneTime, isoCountryCode);
+            if (DBG) log("fixTimeZone: using getTimeZone(off, dst, time, iso)");
+        }
+
+        final String tmpLog = "fixTimeZone zoneName=" + zoneName + " mZoneOffset=" + mZoneOffset
+                + " mZoneDst=" + mZoneDst + " iso-cc=" + isoCountryCode + " mNeedFixZoneAfterNitz="
+                + mNeedFixZoneAfterNitz + " zone=" + (zone != null ? zone.getID() : "NULL");
+        mTimeZoneLog.log(tmpLog);
+
+        if (zone != null) {
+            log("fixTimeZone: zone != null zone.getID=" + zone.getID());
+            if (getAutoTimeZone()) {
+                setAndBroadcastNetworkSetTimeZone(zone.getID());
+            } else {
+                log("fixTimeZone: skip changing zone as getAutoTimeZone was false");
+            }
+            if (mNeedFixZoneAfterNitz) {
+                saveNitzTimeZone(zone.getID());
+            }
+        } else {
+            log("fixTimeZone: zone == null, do nothing for zone");
+        }
+        mNeedFixZoneAfterNitz = false;
+    }
+
+    /**
+     * Check if GPRS got registered while voice is registered.
+     *
+     * @param dataRegState i.e. CGREG in GSM
+     * @param voiceRegState i.e. CREG in GSM
+     * @return false if device only register to voice but not gprs
+     */
+    private boolean isGprsConsistent(int dataRegState, int voiceRegState) {
+        return !((voiceRegState == ServiceState.STATE_IN_SERVICE) &&
+                (dataRegState != ServiceState.STATE_IN_SERVICE));
+    }
+
+    /**
+     * Returns a TimeZone object based only on parameters from the NITZ string.
+     */
+    private TimeZone getNitzTimeZone(int offset, boolean dst, long when) {
+        TimeZone guess = findTimeZone(offset, dst, when);
+        if (guess == null) {
+            // Couldn't find a proper timezone.  Perhaps the DST data is wrong.
+            guess = findTimeZone(offset, !dst, when);
+        }
+        if (DBG) log("getNitzTimeZone returning " + (guess == null ? guess : guess.getID()));
+        return guess;
+    }
+
+    private TimeZone findTimeZone(int offset, boolean dst, long when) {
+        int rawOffset = offset;
+        if (dst) {
+            rawOffset -= MS_PER_HOUR;
+        }
+        String[] zones = TimeZone.getAvailableIDs(rawOffset);
+        TimeZone guess = null;
+        Date d = new Date(when);
+        for (String zone : zones) {
+            TimeZone tz = TimeZone.getTimeZone(zone);
+            if (tz.getOffset(when) == offset &&
+                    tz.inDaylightTime(d) == dst) {
+                guess = tz;
+                break;
+            }
+        }
+
+        return guess;
+    }
+
+    /** convert ServiceState registration code
+     * to service state */
+    private int regCodeToServiceState(int code) {
+        switch (code) {
+            case ServiceState.RIL_REG_STATE_HOME:
+            case ServiceState.RIL_REG_STATE_ROAMING:
+                return ServiceState.STATE_IN_SERVICE;
+            default:
+                return ServiceState.STATE_OUT_OF_SERVICE;
+        }
+    }
+
+    /**
+     * code is registration state 0-5 from TS 27.007 7.2
+     * returns true if registered roam, false otherwise
+     */
+    private boolean regCodeIsRoaming (int code) {
+        return ServiceState.RIL_REG_STATE_ROAMING == code;
+    }
+
+    private boolean isSameOperatorNameFromSimAndSS(ServiceState s) {
+        String spn = ((TelephonyManager) mPhone.getContext().
+                getSystemService(Context.TELEPHONY_SERVICE)).
+                getSimOperatorNameForPhone(getPhoneId());
+
+        // NOTE: in case of RUIM we should completely ignore the ERI data file and
+        // mOperatorAlphaLong is set from RIL_REQUEST_OPERATOR response 0 (alpha ONS)
+        String onsl = s.getOperatorAlphaLong();
+        String onss = s.getOperatorAlphaShort();
+
+        boolean equalsOnsl = !TextUtils.isEmpty(spn) && spn.equalsIgnoreCase(onsl);
+        boolean equalsOnss = !TextUtils.isEmpty(spn) && spn.equalsIgnoreCase(onss);
+
+        return (equalsOnsl || equalsOnss);
+    }
+
+    /**
+     * Set roaming state if operator mcc is the same as sim mcc
+     * and ons is not different from spn
+     *
+     * @param s ServiceState hold current ons
+     * @return true if same operator
+     */
+    private boolean isSameNamedOperators(ServiceState s) {
+        return currentMccEqualsSimMcc(s) && isSameOperatorNameFromSimAndSS(s);
+    }
+
+    /**
+     * Compare SIM MCC with Operator MCC
+     *
+     * @param s ServiceState hold current ons
+     * @return true if both are same
+     */
+    private boolean currentMccEqualsSimMcc(ServiceState s) {
+        String simNumeric = ((TelephonyManager) mPhone.getContext().
+                getSystemService(Context.TELEPHONY_SERVICE)).
+                getSimOperatorNumericForPhone(getPhoneId());
+        String operatorNumeric = s.getOperatorNumeric();
+        boolean equalsMcc = true;
+
+        try {
+            equalsMcc = simNumeric.substring(0, 3).
+                    equals(operatorNumeric.substring(0, 3));
+        } catch (Exception e){
+        }
+        return equalsMcc;
+    }
+
+    /**
+     * Do not set roaming state in case of oprators considered non-roaming.
+     *
+     * Can use mcc or mcc+mnc as item of
+     * {@link CarrierConfigManager#KEY_NON_ROAMING_OPERATOR_STRING_ARRAY}.
+     * For example, 302 or 21407. If mcc or mcc+mnc match with operator,
+     * don't set roaming state.
+     *
+     * @param s ServiceState hold current ons
+     * @return false for roaming state set
+     */
+    private boolean isOperatorConsideredNonRoaming(ServiceState s) {
+        String operatorNumeric = s.getOperatorNumeric();
+        final CarrierConfigManager configManager = (CarrierConfigManager) mPhone.getContext()
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        String[] numericArray = null;
+        if (configManager != null) {
+            PersistableBundle config = configManager.getConfigForSubId(mPhone.getSubId());
+            if (config != null) {
+                numericArray = config.getStringArray(
+                        CarrierConfigManager.KEY_NON_ROAMING_OPERATOR_STRING_ARRAY);
+            }
+        }
+        if (ArrayUtils.isEmpty(numericArray) || operatorNumeric == null) {
+            return false;
+        }
+
+        for (String numeric : numericArray) {
+            if (!TextUtils.isEmpty(numeric) && operatorNumeric.startsWith(numeric)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isOperatorConsideredRoaming(ServiceState s) {
+        String operatorNumeric = s.getOperatorNumeric();
+        final CarrierConfigManager configManager = (CarrierConfigManager) mPhone.getContext()
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        String[] numericArray = null;
+        if (configManager != null) {
+            PersistableBundle config = configManager.getConfigForSubId(mPhone.getSubId());
+            if (config != null) {
+                numericArray = config.getStringArray(
+                        CarrierConfigManager.KEY_ROAMING_OPERATOR_STRING_ARRAY);
+            }
+        }
+        if (ArrayUtils.isEmpty(numericArray) || operatorNumeric == null) {
+            return false;
+        }
+
+        for (String numeric : numericArray) {
+            if (!TextUtils.isEmpty(numeric) && operatorNumeric.startsWith(numeric)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Set restricted state based on the OnRestrictedStateChanged notification
+     * If any voice or packet restricted state changes, trigger a UI
+     * notification and notify registrants when sim is ready.
+     *
+     * @param ar an int value of RIL_RESTRICTED_STATE_*
+     */
+    private void onRestrictedStateChanged(AsyncResult ar) {
+        RestrictedState newRs = new RestrictedState();
+
+        if (DBG) log("onRestrictedStateChanged: E rs "+ mRestrictedState);
+
+        if (ar.exception == null && ar.result != null) {
+            int state = (int)ar.result;
+
+            newRs.setCsEmergencyRestricted(
+                    ((state & RILConstants.RIL_RESTRICTED_STATE_CS_EMERGENCY) != 0) ||
+                            ((state & RILConstants.RIL_RESTRICTED_STATE_CS_ALL) != 0) );
+            //ignore the normal call and data restricted state before SIM READY
+            if (mUiccApplcation != null && mUiccApplcation.getState() == AppState.APPSTATE_READY) {
+                newRs.setCsNormalRestricted(
+                        ((state & RILConstants.RIL_RESTRICTED_STATE_CS_NORMAL) != 0) ||
+                                ((state & RILConstants.RIL_RESTRICTED_STATE_CS_ALL) != 0) );
+                newRs.setPsRestricted(
+                        (state & RILConstants.RIL_RESTRICTED_STATE_PS_ALL)!= 0);
+            }
+
+            if (DBG) log("onRestrictedStateChanged: new rs "+ newRs);
+
+            if (!mRestrictedState.isPsRestricted() && newRs.isPsRestricted()) {
+                mPsRestrictEnabledRegistrants.notifyRegistrants();
+                setNotification(PS_ENABLED);
+            } else if (mRestrictedState.isPsRestricted() && !newRs.isPsRestricted()) {
+                mPsRestrictDisabledRegistrants.notifyRegistrants();
+                setNotification(PS_DISABLED);
+            }
+
+            /**
+             * There are two kind of cs restriction, normal and emergency. So
+             * there are 4 x 4 combinations in current and new restricted states
+             * and we only need to notify when state is changed.
+             */
+            if (mRestrictedState.isCsRestricted()) {
+                if (!newRs.isAnyCsRestricted()) {
+                    // remove all restriction
+                    setNotification(CS_DISABLED);
+                } else if (!newRs.isCsNormalRestricted()) {
+                    // remove normal restriction
+                    setNotification(CS_EMERGENCY_ENABLED);
+                } else if (!newRs.isCsEmergencyRestricted()) {
+                    // remove emergency restriction
+                    setNotification(CS_NORMAL_ENABLED);
+                }
+            } else if (mRestrictedState.isCsEmergencyRestricted() &&
+                    !mRestrictedState.isCsNormalRestricted()) {
+                if (!newRs.isAnyCsRestricted()) {
+                    // remove all restriction
+                    setNotification(CS_DISABLED);
+                } else if (newRs.isCsRestricted()) {
+                    // enable all restriction
+                    setNotification(CS_ENABLED);
+                } else if (newRs.isCsNormalRestricted()) {
+                    // remove emergency restriction and enable normal restriction
+                    setNotification(CS_NORMAL_ENABLED);
+                }
+            } else if (!mRestrictedState.isCsEmergencyRestricted() &&
+                    mRestrictedState.isCsNormalRestricted()) {
+                if (!newRs.isAnyCsRestricted()) {
+                    // remove all restriction
+                    setNotification(CS_DISABLED);
+                } else if (newRs.isCsRestricted()) {
+                    // enable all restriction
+                    setNotification(CS_ENABLED);
+                } else if (newRs.isCsEmergencyRestricted()) {
+                    // remove normal restriction and enable emergency restriction
+                    setNotification(CS_EMERGENCY_ENABLED);
+                }
+            } else {
+                if (newRs.isCsRestricted()) {
+                    // enable all restriction
+                    setNotification(CS_ENABLED);
+                } else if (newRs.isCsEmergencyRestricted()) {
+                    // enable emergency restriction
+                    setNotification(CS_EMERGENCY_ENABLED);
+                } else if (newRs.isCsNormalRestricted()) {
+                    // enable normal restriction
+                    setNotification(CS_NORMAL_ENABLED);
+                }
+            }
+
+            mRestrictedState = newRs;
+        }
+        log("onRestrictedStateChanged: X rs "+ mRestrictedState);
+    }
+
+    /**
+     * @param workSource calling WorkSource
+     * @return the current cell location information. Prefer Gsm location
+     * information if available otherwise return LTE location information
+     */
+    public CellLocation getCellLocation(WorkSource workSource) {
+        if (((GsmCellLocation)mCellLoc).getLac() >= 0 &&
+                ((GsmCellLocation)mCellLoc).getCid() >= 0) {
+            if (VDBG) log("getCellLocation(): X good mCellLoc=" + mCellLoc);
+            return mCellLoc;
+        } else {
+            List<CellInfo> result = getAllCellInfo(workSource);
+            if (result != null) {
+                // A hack to allow tunneling of LTE information via GsmCellLocation
+                // so that older Network Location Providers can return some information
+                // on LTE only networks, see bug 9228974.
+                //
+                // We'll search the return CellInfo array preferring GSM/WCDMA
+                // data, but if there is none we'll tunnel the first LTE information
+                // in the list.
+                //
+                // The tunnel'd LTE information is returned as follows:
+                //   LAC = TAC field
+                //   CID = CI field
+                //   PSC = 0.
+                GsmCellLocation cellLocOther = new GsmCellLocation();
+                for (CellInfo ci : result) {
+                    if (ci instanceof CellInfoGsm) {
+                        CellInfoGsm cellInfoGsm = (CellInfoGsm)ci;
+                        CellIdentityGsm cellIdentityGsm = cellInfoGsm.getCellIdentity();
+                        cellLocOther.setLacAndCid(cellIdentityGsm.getLac(),
+                                cellIdentityGsm.getCid());
+                        cellLocOther.setPsc(cellIdentityGsm.getPsc());
+                        if (VDBG) log("getCellLocation(): X ret GSM info=" + cellLocOther);
+                        return cellLocOther;
+                    } else if (ci instanceof CellInfoWcdma) {
+                        CellInfoWcdma cellInfoWcdma = (CellInfoWcdma)ci;
+                        CellIdentityWcdma cellIdentityWcdma = cellInfoWcdma.getCellIdentity();
+                        cellLocOther.setLacAndCid(cellIdentityWcdma.getLac(),
+                                cellIdentityWcdma.getCid());
+                        cellLocOther.setPsc(cellIdentityWcdma.getPsc());
+                        if (VDBG) log("getCellLocation(): X ret WCDMA info=" + cellLocOther);
+                        return cellLocOther;
+                    } else if ((ci instanceof CellInfoLte) &&
+                            ((cellLocOther.getLac() < 0) || (cellLocOther.getCid() < 0))) {
+                        // We'll return the first good LTE info we get if there is no better answer
+                        CellInfoLte cellInfoLte = (CellInfoLte)ci;
+                        CellIdentityLte cellIdentityLte = cellInfoLte.getCellIdentity();
+                        if ((cellIdentityLte.getTac() != Integer.MAX_VALUE)
+                                && (cellIdentityLte.getCi() != Integer.MAX_VALUE)) {
+                            cellLocOther.setLacAndCid(cellIdentityLte.getTac(),
+                                    cellIdentityLte.getCi());
+                            cellLocOther.setPsc(0);
+                            if (VDBG) {
+                                log("getCellLocation(): possible LTE cellLocOther=" + cellLocOther);
+                            }
+                        }
+                    }
+                }
+                if (VDBG) {
+                    log("getCellLocation(): X ret best answer cellLocOther=" + cellLocOther);
+                }
+                return cellLocOther;
+            } else {
+                if (VDBG) {
+                    log("getCellLocation(): X empty mCellLoc and CellInfo mCellLoc=" + mCellLoc);
+                }
+                return mCellLoc;
+            }
+        }
+    }
+
+    /**
+     * nitzReceiveTime is time_t that the NITZ time was posted
+     */
+    private void setTimeFromNITZString (String nitz, long nitzReceiveTime) {
+        // "yy/mm/dd,hh:mm:ss(+/-)tz"
+        // tz is in number of quarter-hours
+
+        long start = SystemClock.elapsedRealtime();
+        if (DBG) {
+            log("NITZ: " + nitz + "," + nitzReceiveTime
+                    + " start=" + start + " delay=" + (start - nitzReceiveTime));
+        }
+
+        try {
+            /* NITZ time (hour:min:sec) will be in UTC but it supplies the timezone
+             * offset as well (which we won't worry about until later) */
+            Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+
+            c.clear();
+            c.set(Calendar.DST_OFFSET, 0);
+
+            String[] nitzSubs = nitz.split("[/:,+-]");
+
+            int year = 2000 + Integer.parseInt(nitzSubs[0]);
+            if (year > MAX_NITZ_YEAR) {
+                if (DBG) loge("NITZ year: " + year + " exceeds limit, skip NITZ time update");
+                return;
+            }
+            c.set(Calendar.YEAR, year);
+
+            // month is 0 based!
+            int month = Integer.parseInt(nitzSubs[1]) - 1;
+            c.set(Calendar.MONTH, month);
+
+            int date = Integer.parseInt(nitzSubs[2]);
+            c.set(Calendar.DATE, date);
+
+            int hour = Integer.parseInt(nitzSubs[3]);
+            c.set(Calendar.HOUR, hour);
+
+            int minute = Integer.parseInt(nitzSubs[4]);
+            c.set(Calendar.MINUTE, minute);
+
+            int second = Integer.parseInt(nitzSubs[5]);
+            c.set(Calendar.SECOND, second);
+
+            boolean sign = (nitz.indexOf('-') == -1);
+
+            int tzOffset = Integer.parseInt(nitzSubs[6]);
+
+            int dst = (nitzSubs.length >= 8 ) ? Integer.parseInt(nitzSubs[7]) : 0;
+
+            // The zone offset received from NITZ is for current local time,
+            // so DST correction is already applied.  Don't add it again.
+            //
+            // tzOffset += dst * 4;
+            //
+            // We could unapply it if we wanted the raw offset.
+
+            tzOffset = (sign ? 1 : -1) * tzOffset * 15 * 60 * 1000;
+
+            TimeZone    zone = null;
+
+            // As a special extension, the Android emulator appends the name of
+            // the host computer's timezone to the nitz string. this is zoneinfo
+            // timezone name of the form Area!Location or Area!Location!SubLocation
+            // so we need to convert the ! into /
+            if (nitzSubs.length >= 9) {
+                String  tzname = nitzSubs[8].replace('!','/');
+                zone = TimeZone.getTimeZone( tzname );
+            }
+
+            String iso = ((TelephonyManager) mPhone.getContext().
+                    getSystemService(Context.TELEPHONY_SERVICE)).
+                    getNetworkCountryIsoForPhone(mPhone.getPhoneId());
+
+            if (zone == null) {
+
+                if (mGotCountryCode) {
+                    if (iso != null && iso.length() > 0) {
+                        zone = TimeUtils.getTimeZone(tzOffset, dst != 0,
+                                c.getTimeInMillis(),
+                                iso);
+                    } else {
+                        // We don't have a valid iso country code.  This is
+                        // most likely because we're on a test network that's
+                        // using a bogus MCC (eg, "001"), so get a TimeZone
+                        // based only on the NITZ parameters.
+                        zone = getNitzTimeZone(tzOffset, (dst != 0), c.getTimeInMillis());
+                    }
+                }
+            }
+
+            if ((zone == null) || (mZoneOffset != tzOffset) || (mZoneDst != (dst != 0))){
+                // We got the time before the country or the zone has changed
+                // so we don't know how to identify the DST rules yet.  Save
+                // the information and hope to fix it up later.
+
+                mNeedFixZoneAfterNitz = true;
+                mZoneOffset  = tzOffset;
+                mZoneDst     = dst != 0;
+                mZoneTime    = c.getTimeInMillis();
+            }
+
+            String tmpLog = "NITZ: nitz=" + nitz + " nitzReceiveTime=" + nitzReceiveTime
+                    + " tzOffset=" + tzOffset + " dst=" + dst + " zone="
+                    + (zone != null ? zone.getID() : "NULL")
+                    + " iso=" + iso + " mGotCountryCode=" + mGotCountryCode
+                    + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
+                    + " getAutoTimeZone()=" + getAutoTimeZone();
+            if (DBG) {
+                log(tmpLog);
+            }
+            mTimeZoneLog.log(tmpLog);
+
+            if (zone != null) {
+                if (getAutoTimeZone()) {
+                    setAndBroadcastNetworkSetTimeZone(zone.getID());
+                }
+                saveNitzTimeZone(zone.getID());
+            }
+
+            String ignore = SystemProperties.get("gsm.ignore-nitz");
+            if (ignore != null && ignore.equals("yes")) {
+                log("NITZ: Not setting clock because gsm.ignore-nitz is set");
+                return;
+            }
+
+            try {
+                mWakeLock.acquire();
+
+                if (!mPhone.isPhoneTypeGsm() || getAutoTime()) {
+                    long millisSinceNitzReceived
+                            = SystemClock.elapsedRealtime() - nitzReceiveTime;
+
+                    if (millisSinceNitzReceived < 0) {
+                        // Sanity check: something is wrong
+                        if (DBG) {
+                            log("NITZ: not setting time, clock has rolled "
+                                    + "backwards since NITZ time was received, "
+                                    + nitz);
+                        }
+                        return;
+                    }
+
+                    if (millisSinceNitzReceived > Integer.MAX_VALUE) {
+                        // If the time is this far off, something is wrong > 24 days!
+                        if (DBG) {
+                            log("NITZ: not setting time, processing has taken "
+                                    + (millisSinceNitzReceived / (1000 * 60 * 60 * 24))
+                                    + " days");
+                        }
+                        return;
+                    }
+
+                    // Note: with range checks above, cast to int is safe
+                    c.add(Calendar.MILLISECOND, (int)millisSinceNitzReceived);
+
+                    tmpLog = "NITZ: nitz=" + nitz + " nitzReceiveTime=" + nitzReceiveTime
+                            + " Setting time of day to " + c.getTime()
+                            + " NITZ receive delay(ms): " + millisSinceNitzReceived
+                            + " gained(ms): "
+                            + (c.getTimeInMillis() - System.currentTimeMillis())
+                            + " from " + nitz;
+                    if (DBG) {
+                        log(tmpLog);
+                    }
+                    mTimeLog.log(tmpLog);
+                    if (mPhone.isPhoneTypeGsm()) {
+                        setAndBroadcastNetworkSetTime(c.getTimeInMillis());
+                        Rlog.i(LOG_TAG, "NITZ: after Setting time of day");
+                    } else {
+                        if (getAutoTime()) {
+                            /**
+                             * Update system time automatically
+                             */
+                            long gained = c.getTimeInMillis() - System.currentTimeMillis();
+                            long timeSinceLastUpdate = SystemClock.elapsedRealtime() - mSavedAtTime;
+                            int nitzUpdateSpacing = Settings.Global.getInt(mCr,
+                                    Settings.Global.NITZ_UPDATE_SPACING, mNitzUpdateSpacing);
+                            int nitzUpdateDiff = Settings.Global.getInt(mCr,
+                                    Settings.Global.NITZ_UPDATE_DIFF, mNitzUpdateDiff);
+
+                            if ((mSavedAtTime == 0) || (timeSinceLastUpdate > nitzUpdateSpacing)
+                                    || (Math.abs(gained) > nitzUpdateDiff)) {
+                                if (DBG) {
+                                    log("NITZ: Auto updating time of day to " + c.getTime()
+                                            + " NITZ receive delay=" + millisSinceNitzReceived
+                                            + "ms gained=" + gained + "ms from " + nitz);
+                                }
+
+                                setAndBroadcastNetworkSetTime(c.getTimeInMillis());
+                            } else {
+                                if (DBG) {
+                                    log("NITZ: ignore, a previous update was "
+                                            + timeSinceLastUpdate + "ms ago and gained=" + gained + "ms");
+                                }
+                                return;
+                            }
+                        }
+                    }
+                }
+                SystemProperties.set("gsm.nitz.time", String.valueOf(c.getTimeInMillis()));
+                saveNitzTime(c.getTimeInMillis());
+                mNitzUpdatedTime = true;
+            } finally {
+                if (DBG) {
+                    long end = SystemClock.elapsedRealtime();
+                    log("NITZ: end=" + end + " dur=" + (end - start));
+                }
+                mWakeLock.release();
+            }
+        } catch (RuntimeException ex) {
+            loge("NITZ: Parsing NITZ time " + nitz + " ex=" + ex);
+        }
+    }
+
+    private boolean getAutoTime() {
+        try {
+            return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME) > 0;
+        } catch (Settings.SettingNotFoundException snfe) {
+            return true;
+        }
+    }
+
+    private boolean getAutoTimeZone() {
+        try {
+            return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE) > 0;
+        } catch (Settings.SettingNotFoundException snfe) {
+            return true;
+        }
+    }
+
+    private void saveNitzTimeZone(String zoneId) {
+        mSavedTimeZone = zoneId;
+    }
+
+    private void saveNitzTime(long time) {
+        mSavedTime = time;
+        mSavedAtTime = SystemClock.elapsedRealtime();
+    }
+
+    /**
+     * Set the timezone and send out a sticky broadcast so the system can
+     * determine if the timezone was set by the carrier.
+     *
+     * @param zoneId timezone set by carrier
+     */
+    private void setAndBroadcastNetworkSetTimeZone(String zoneId) {
+        if (DBG) log("setAndBroadcastNetworkSetTimeZone: setTimeZone=" + zoneId);
+        AlarmManager alarm =
+                (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE);
+        alarm.setTimeZone(zoneId);
+        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        intent.putExtra("time-zone", zoneId);
+        mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        if (DBG) {
+            log("setAndBroadcastNetworkSetTimeZone: call alarm.setTimeZone and broadcast zoneId=" +
+                    zoneId);
+        }
+    }
+
+    /**
+     * Set the time and Send out a sticky broadcast so the system can determine
+     * if the time was set by the carrier.
+     *
+     * @param time time set by network
+     */
+    private void setAndBroadcastNetworkSetTime(long time) {
+        if (DBG) log("setAndBroadcastNetworkSetTime: time=" + time + "ms");
+        SystemClock.setCurrentTimeMillis(time);
+        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        intent.putExtra("time", time);
+        mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+
+        TelephonyMetrics.getInstance().writeNITZEvent(mPhone.getPhoneId(), time);
+    }
+
+    private void revertToNitzTime() {
+        if (Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME, 0) == 0) {
+            return;
+        }
+        if (DBG) {
+            log("Reverting to NITZ Time: mSavedTime=" + mSavedTime + " mSavedAtTime=" +
+                    mSavedAtTime);
+        }
+        if (mSavedTime != 0 && mSavedAtTime != 0) {
+            long currTime = SystemClock.elapsedRealtime();
+            mTimeLog.log("Reverting to NITZ time, currTime=" + currTime
+                    + " mSavedAtTime=" + mSavedAtTime + " mSavedTime=" + mSavedTime);
+            setAndBroadcastNetworkSetTime(mSavedTime + (currTime - mSavedAtTime));
+        }
+    }
+
+    private void revertToNitzTimeZone() {
+        if (Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 0) == 0) {
+            return;
+        }
+        String tmpLog = "Reverting to NITZ TimeZone: tz=" + mSavedTimeZone;
+        if (DBG) log(tmpLog);
+        mTimeZoneLog.log(tmpLog);
+        if (mSavedTimeZone != null) {
+            setAndBroadcastNetworkSetTimeZone(mSavedTimeZone);
+        } else {
+            String iso = ((TelephonyManager) mPhone.getContext().getSystemService(
+                    Context.TELEPHONY_SERVICE)).getNetworkCountryIsoForPhone(mPhone.getPhoneId());
+            if (!TextUtils.isEmpty(iso)) {
+                updateTimeZoneByNetworkCountryCode(iso);
+            }
+        }
+    }
+
+    /**
+     * Post a notification to NotificationManager for restricted state and
+     * rejection cause for cs registration
+     *
+     * @param notifyType is one state of PS/CS_*_ENABLE/DISABLE
+     */
+    @VisibleForTesting
+    public void setNotification(int notifyType) {
+        if (DBG) log("setNotification: create notification " + notifyType);
+
+        // Needed because sprout RIL sends these when they shouldn't?
+        boolean isSetNotification = mPhone.getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_user_notification_of_restrictied_mobile_access);
+        if (!isSetNotification) {
+            if (DBG) log("Ignore all the notifications");
+            return;
+        }
+
+        Context context = mPhone.getContext();
+
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager != null) {
+            PersistableBundle bundle = configManager.getConfig();
+            if (bundle != null) {
+                boolean disableVoiceBarringNotification = bundle.getBoolean(
+                        CarrierConfigManager.KEY_DISABLE_VOICE_BARRING_NOTIFICATION_BOOL, false);
+                if(disableVoiceBarringNotification && (notifyType == CS_ENABLED
+                        || notifyType == CS_NORMAL_ENABLED
+                        || notifyType == CS_EMERGENCY_ENABLED)) {
+                    if (DBG) log("Voice/emergency call barred notification disabled");
+                    return;
+                }
+            }
+        }
+
+        CharSequence details = "";
+        CharSequence title = "";
+        int notificationId = CS_NOTIFICATION;
+        int icon = com.android.internal.R.drawable.stat_sys_warning;
+
+        switch (notifyType) {
+            case PS_ENABLED:
+                long dataSubId = SubscriptionManager.getDefaultDataSubscriptionId();
+                if (dataSubId != mPhone.getSubId()) {
+                    return;
+                }
+                notificationId = PS_NOTIFICATION;
+                title = context.getText(com.android.internal.R.string.RestrictedOnDataTitle);
+                details = context.getText(com.android.internal.R.string.RestrictedStateContent);
+                break;
+            case PS_DISABLED:
+                notificationId = PS_NOTIFICATION;
+                break;
+            case CS_ENABLED:
+                title = context.getText(com.android.internal.R.string.RestrictedOnAllVoiceTitle);
+                details = context.getText(
+                        com.android.internal.R.string.RestrictedStateContent);
+                break;
+            case CS_NORMAL_ENABLED:
+                title = context.getText(com.android.internal.R.string.RestrictedOnNormalTitle);
+                details = context.getText(com.android.internal.R.string.RestrictedStateContent);
+                break;
+            case CS_EMERGENCY_ENABLED:
+                title = context.getText(com.android.internal.R.string.RestrictedOnEmergencyTitle);
+                details = context.getText(
+                        com.android.internal.R.string.RestrictedStateContent);
+                break;
+            case CS_DISABLED:
+                // do nothing and cancel the notification later
+                break;
+            case CS_REJECT_CAUSE_ENABLED:
+                notificationId = CS_REJECT_CAUSE_NOTIFICATION;
+                int resId = selectResourceForRejectCode(mRejectCode);
+                if (0 == resId) {
+                    // cancel notification because current reject code is not handled.
+                    notifyType = CS_REJECT_CAUSE_DISABLED;
+                } else {
+                    icon = com.android.internal.R.drawable.stat_notify_mmcc_indication_icn;
+                    title = Resources.getSystem().getString(resId);
+                    details = null;
+                }
+                break;
+            case CS_REJECT_CAUSE_DISABLED:
+                notificationId = CS_REJECT_CAUSE_NOTIFICATION;
+                break;
+        }
+
+        if (DBG) {
+            log("setNotification, create notification, notifyType: " + notifyType
+                    + ", title: " + title + ", details: " + details);
+        }
+
+        mNotification = new Notification.Builder(context)
+                .setWhen(System.currentTimeMillis())
+                .setAutoCancel(true)
+                .setSmallIcon(icon)
+                .setTicker(title)
+                .setColor(context.getResources().getColor(
+                        com.android.internal.R.color.system_notification_accent_color))
+                .setContentTitle(title)
+                .setStyle(new Notification.BigTextStyle().bigText(details))
+                .setContentText(details)
+                .setChannel(NotificationChannelController.CHANNEL_ID_ALERT)
+                .build();
+
+        NotificationManager notificationManager = (NotificationManager)
+                context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (notifyType == PS_DISABLED || notifyType == CS_DISABLED
+                || notifyType == CS_REJECT_CAUSE_DISABLED) {
+            // cancel previous post notification
+            notificationManager.cancel(notificationId);
+        } else {
+            // update restricted state notification
+            notificationManager.notify(notificationId, mNotification);
+        }
+    }
+
+    /**
+     * Selects the resource ID, which depends on rejection cause that is sent by the network when CS
+     * registration is rejected.
+     *
+     * @param rejCode should be compatible with TS 24.008.
+     */
+    private int selectResourceForRejectCode(int rejCode) {
+        int rejResourceId = 0;
+        switch (rejCode) {
+            case 1:// Authentication reject
+                rejResourceId = com.android.internal.R.string.mmcc_authentication_reject;
+                break;
+            case 2:// IMSI unknown in HLR
+                rejResourceId = com.android.internal.R.string.mmcc_imsi_unknown_in_hlr;
+                break;
+            case 3:// Illegal MS
+                rejResourceId = com.android.internal.R.string.mmcc_illegal_ms;
+                break;
+            case 6:// Illegal ME
+                rejResourceId = com.android.internal.R.string.mmcc_illegal_me;
+                break;
+            default:
+                // The other codes are not defined or not required by operators till now.
+                break;
+        }
+        return rejResourceId;
+    }
+
+    private UiccCardApplication getUiccCardApplication() {
+        if (mPhone.isPhoneTypeGsm()) {
+            return mUiccController.getUiccCardApplication(mPhone.getPhoneId(),
+                    UiccController.APP_FAM_3GPP);
+        } else {
+            return mUiccController.getUiccCardApplication(mPhone.getPhoneId(),
+                    UiccController.APP_FAM_3GPP2);
+        }
+    }
+
+    private void queueNextSignalStrengthPoll() {
+        if (mDontPollSignalStrength) {
+            // The radio is telling us about signal strength changes
+            // we don't have to ask it
+            return;
+        }
+
+        Message msg;
+
+        msg = obtainMessage();
+        msg.what = EVENT_POLL_SIGNAL_STRENGTH;
+
+        long nextTime;
+
+        // TODO Don't poll signal strength if screen is off
+        sendMessageDelayed(msg, POLL_PERIOD_MILLIS);
+    }
+
+    private void notifyCdmaSubscriptionInfoReady() {
+        if (mCdmaForSubscriptionInfoReadyRegistrants != null) {
+            if (DBG) log("CDMA_SUBSCRIPTION: call notifyRegistrants()");
+            mCdmaForSubscriptionInfoReadyRegistrants.notifyRegistrants();
+        }
+    }
+
+    /**
+     * Registration point for transition into DataConnection attached.
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForDataConnectionAttached(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mAttachedRegistrants.add(r);
+
+        if (getCurrentDataConnectionState() == ServiceState.STATE_IN_SERVICE) {
+            r.notifyRegistrant();
+        }
+    }
+    public void unregisterForDataConnectionAttached(Handler h) {
+        mAttachedRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for transition into DataConnection detached.
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForDataConnectionDetached(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mDetachedRegistrants.add(r);
+
+        if (getCurrentDataConnectionState() != ServiceState.STATE_IN_SERVICE) {
+            r.notifyRegistrant();
+        }
+    }
+    public void unregisterForDataConnectionDetached(Handler h) {
+        mDetachedRegistrants.remove(h);
+    }
+
+    /**
+     * Registration for DataConnection RIL Data Radio Technology changing. The
+     * new radio technology will be returned AsyncResult#result as an Integer Object.
+     * The AsyncResult will be in the notification Message#obj.
+     *
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForDataRegStateOrRatChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mDataRegStateOrRatChangedRegistrants.add(r);
+        notifyDataRegStateRilRadioTechnologyChanged();
+    }
+    public void unregisterForDataRegStateOrRatChanged(Handler h) {
+        mDataRegStateOrRatChangedRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for transition into network attached.
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj in Message.obj
+     */
+    public void registerForNetworkAttached(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+
+        mNetworkAttachedRegistrants.add(r);
+        if (mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForNetworkAttached(Handler h) {
+        mNetworkAttachedRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for transition into network detached.
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj in Message.obj
+     */
+    public void registerForNetworkDetached(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+
+        mNetworkDetachedRegistrants.add(r);
+        if (mSS.getVoiceRegState() != ServiceState.STATE_IN_SERVICE) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForNetworkDetached(Handler h) {
+        mNetworkDetachedRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for transition into packet service restricted zone.
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForPsRestrictedEnabled(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mPsRestrictEnabledRegistrants.add(r);
+
+        if (mRestrictedState.isPsRestricted()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForPsRestrictedEnabled(Handler h) {
+        mPsRestrictEnabledRegistrants.remove(h);
+    }
+
+    /**
+     * Registration point for transition out of packet service restricted zone.
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForPsRestrictedDisabled(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mPsRestrictDisabledRegistrants.add(r);
+
+        if (mRestrictedState.isPsRestricted()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForPsRestrictedDisabled(Handler h) {
+        mPsRestrictDisabledRegistrants.remove(h);
+    }
+
+    /**
+     * Clean up existing voice and data connection then turn off radio power.
+     *
+     * Hang up the existing voice calls to decrease call drop rate.
+     */
+    public void powerOffRadioSafely(DcTracker dcTracker) {
+        synchronized (this) {
+            if (!mPendingRadioPowerOffAfterDataOff) {
+                if (mPhone.isPhoneTypeGsm() || mPhone.isPhoneTypeCdmaLte()) {
+                    int dds = SubscriptionManager.getDefaultDataSubscriptionId();
+                    // To minimize race conditions we call cleanUpAllConnections on
+                    // both if else paths instead of before this isDisconnected test.
+                    if (dcTracker.isDisconnected()
+                            && (dds == mPhone.getSubId()
+                            || (dds != mPhone.getSubId()
+                            && ProxyController.getInstance().isDataDisconnected(dds)))) {
+                        // To minimize race conditions we do this after isDisconnected
+                        dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
+                        if (DBG) log("Data disconnected, turn off radio right away.");
+                        hangupAndPowerOff();
+                    } else {
+                        // hang up all active voice calls first
+                        if (mPhone.isPhoneTypeGsm() && mPhone.isInCall()) {
+                            mPhone.mCT.mRingingCall.hangupIfAlive();
+                            mPhone.mCT.mBackgroundCall.hangupIfAlive();
+                            mPhone.mCT.mForegroundCall.hangupIfAlive();
+                        }
+                        dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
+                        if (dds != mPhone.getSubId()
+                                && !ProxyController.getInstance().isDataDisconnected(dds)) {
+                            if (DBG) log("Data is active on DDS.  Wait for all data disconnect");
+                            // Data is not disconnected on DDS. Wait for the data disconnect complete
+                            // before sending the RADIO_POWER off.
+                            ProxyController.getInstance().registerForAllDataDisconnected(dds, this,
+                                    EVENT_ALL_DATA_DISCONNECTED, null);
+                            mPendingRadioPowerOffAfterDataOff = true;
+                        }
+                        Message msg = Message.obtain(this);
+                        msg.what = EVENT_SET_RADIO_POWER_OFF;
+                        msg.arg1 = ++mPendingRadioPowerOffAfterDataOffTag;
+                        if (sendMessageDelayed(msg, 30000)) {
+                            if (DBG) log("Wait upto 30s for data to disconnect, then turn off radio.");
+                            mPendingRadioPowerOffAfterDataOff = true;
+                        } else {
+                            log("Cannot send delayed Msg, turn off radio right away.");
+                            hangupAndPowerOff();
+                            mPendingRadioPowerOffAfterDataOff = false;
+                        }
+                    }
+                } else {
+                    // In some network, deactivate PDP connection cause releasing of RRC connection,
+                    // which MM/IMSI detaching request needs. Without this detaching, network can
+                    // not release the network resources previously attached.
+                    // So we are avoiding data detaching on these networks.
+                    String[] networkNotClearData = mPhone.getContext().getResources()
+                            .getStringArray(com.android.internal.R.array.networks_not_clear_data);
+                    String currentNetwork = mSS.getOperatorNumeric();
+                    if ((networkNotClearData != null) && (currentNetwork != null)) {
+                        for (int i = 0; i < networkNotClearData.length; i++) {
+                            if (currentNetwork.equals(networkNotClearData[i])) {
+                                // Don't clear data connection for this carrier
+                                if (DBG)
+                                    log("Not disconnecting data for " + currentNetwork);
+                                hangupAndPowerOff();
+                                return;
+                            }
+                        }
+                    }
+                    // To minimize race conditions we call cleanUpAllConnections on
+                    // both if else paths instead of before this isDisconnected test.
+                    if (dcTracker.isDisconnected()) {
+                        // To minimize race conditions we do this after isDisconnected
+                        dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
+                        if (DBG) log("Data disconnected, turn off radio right away.");
+                        hangupAndPowerOff();
+                    } else {
+                        dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
+                        Message msg = Message.obtain(this);
+                        msg.what = EVENT_SET_RADIO_POWER_OFF;
+                        msg.arg1 = ++mPendingRadioPowerOffAfterDataOffTag;
+                        if (sendMessageDelayed(msg, 30000)) {
+                            if (DBG)
+                                log("Wait upto 30s for data to disconnect, then turn off radio.");
+                            mPendingRadioPowerOffAfterDataOff = true;
+                        } else {
+                            log("Cannot send delayed Msg, turn off radio right away.");
+                            hangupAndPowerOff();
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * process the pending request to turn radio off after data is disconnected
+     *
+     * return true if there is pending request to process; false otherwise.
+     */
+    public boolean processPendingRadioPowerOffAfterDataOff() {
+        synchronized(this) {
+            if (mPendingRadioPowerOffAfterDataOff) {
+                if (DBG) log("Process pending request to turn radio off.");
+                mPendingRadioPowerOffAfterDataOffTag += 1;
+                hangupAndPowerOff();
+                mPendingRadioPowerOffAfterDataOff = false;
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Checks if the provided earfcn falls withing the range of earfcns.
+     *
+     * return true if earfcn falls within the provided range; false otherwise.
+     */
+    private boolean containsEarfcnInEarfcnRange(ArrayList<Pair<Integer, Integer>> earfcnPairList,
+            int earfcn) {
+        if (earfcnPairList != null) {
+            for (Pair<Integer, Integer> earfcnPair : earfcnPairList) {
+                if ((earfcn >= earfcnPair.first) && (earfcn <= earfcnPair.second)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Convert the earfcnStringArray to list of pairs.
+     *
+     * Format of the earfcnsList is expected to be {"erafcn1_start-earfcn1_end",
+     * "earfcn2_start-earfcn2_end" ... }
+     */
+    ArrayList<Pair<Integer, Integer>> convertEarfcnStringArrayToPairList(String[] earfcnsList) {
+        ArrayList<Pair<Integer, Integer>> earfcnPairList = new ArrayList<Pair<Integer, Integer>>();
+
+        if (earfcnsList != null) {
+            int earfcnStart;
+            int earfcnEnd;
+            for (int i = 0; i < earfcnsList.length; i++) {
+                try {
+                    String[] earfcns = earfcnsList[i].split("-");
+                    if (earfcns.length != 2) {
+                        if (VDBG) {
+                            log("Invalid earfcn range format");
+                        }
+                        return null;
+                    }
+
+                    earfcnStart = Integer.parseInt(earfcns[0]);
+                    earfcnEnd = Integer.parseInt(earfcns[1]);
+
+                    if (earfcnStart > earfcnEnd) {
+                        if (VDBG) {
+                            log("Invalid earfcn range format");
+                        }
+                        return null;
+                    }
+
+                    earfcnPairList.add(new Pair<Integer, Integer>(earfcnStart, earfcnEnd));
+                } catch (PatternSyntaxException pse) {
+                    if (VDBG) {
+                        log("Invalid earfcn range format");
+                    }
+                    return null;
+                } catch (NumberFormatException nfe) {
+                    if (VDBG) {
+                        log("Invalid earfcn number format");
+                    }
+                    return null;
+                }
+            }
+        }
+
+        return earfcnPairList;
+    }
+    private void updateLteEarfcnLists() {
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle b = configManager.getConfigForSubId(mPhone.getSubId());
+        synchronized (mLteRsrpBoostLock) {
+            mLteRsrpBoost = b.getInt(CarrierConfigManager.KEY_LTE_EARFCNS_RSRP_BOOST_INT, 0);
+            String[] earfcnsStringArrayForRsrpBoost = b.getStringArray(
+                    CarrierConfigManager.KEY_BOOSTED_LTE_EARFCNS_STRING_ARRAY);
+            mEarfcnPairListForRsrpBoost = convertEarfcnStringArrayToPairList(
+                    earfcnsStringArrayForRsrpBoost);
+        }
+    }
+
+    private void updateServiceStateLteEarfcnBoost(ServiceState serviceState, int lteEarfcn) {
+        synchronized (mLteRsrpBoostLock) {
+            if ((lteEarfcn != INVALID_LTE_EARFCN)
+                    && containsEarfcnInEarfcnRange(mEarfcnPairListForRsrpBoost, lteEarfcn)) {
+                serviceState.setLteEarfcnRsrpBoost(mLteRsrpBoost);
+            } else {
+                serviceState.setLteEarfcnRsrpBoost(0);
+            }
+        }
+    }
+
+    /**
+     * send signal-strength-changed notification if changed Called both for
+     * solicited and unsolicited signal strength updates
+     *
+     * @return true if the signal strength changed and a notification was sent.
+     */
+    protected boolean onSignalStrengthResult(AsyncResult ar) {
+        boolean isGsm = false;
+        int dataRat = mSS.getRilDataRadioTechnology();
+        int voiceRat = mSS.getRilVoiceRadioTechnology();
+
+        // Override isGsm based on currently camped data and voice RATs
+        // Set isGsm to true if the RAT belongs to GSM family and not IWLAN
+        if ((dataRat != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                && ServiceState.isGsm(dataRat))
+                || (voiceRat != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                && ServiceState.isGsm(voiceRat))) {
+            isGsm = true;
+        }
+
+        // This signal is used for both voice and data radio signal so parse
+        // all fields
+
+        if ((ar.exception == null) && (ar.result != null)) {
+            mSignalStrength = (SignalStrength) ar.result;
+            mSignalStrength.validateInput();
+            mSignalStrength.setGsm(isGsm);
+            mSignalStrength.setLteRsrpBoost(mSS.getLteEarfcnRsrpBoost());
+        } else {
+            log("onSignalStrengthResult() Exception from RIL : " + ar.exception);
+            mSignalStrength = new SignalStrength(isGsm);
+        }
+
+        boolean ssChanged = notifySignalStrength();
+
+        return ssChanged;
+    }
+
+    /**
+     * Hang up all voice call and turn off radio. Implemented by derived class.
+     */
+    protected void hangupAndPowerOff() {
+        // hang up all active voice calls
+        if (!mPhone.isPhoneTypeGsm() || mPhone.isInCall()) {
+            mPhone.mCT.mRingingCall.hangupIfAlive();
+            mPhone.mCT.mBackgroundCall.hangupIfAlive();
+            mPhone.mCT.mForegroundCall.hangupIfAlive();
+        }
+
+        mCi.setRadioPower(false, null);
+
+    }
+
+    /** Cancel a pending (if any) pollState() operation */
+    protected void cancelPollState() {
+        // This will effectively cancel the rest of the poll requests.
+        mPollingContext = new int[1];
+    }
+
+    /**
+     * Return true if time zone needs fixing.
+     *
+     * @param phone
+     * @param operatorNumeric
+     * @param prevOperatorNumeric
+     * @param needToFixTimeZone
+     * @return true if time zone needs to be fixed
+     */
+    protected boolean shouldFixTimeZoneNow(Phone phone, String operatorNumeric,
+            String prevOperatorNumeric, boolean needToFixTimeZone) {
+        // Return false if the mcc isn't valid as we don't know where we are.
+        // Return true if we have an IccCard and the mcc changed or we
+        // need to fix it because when the NITZ time came in we didn't
+        // know the country code.
+
+        // If mcc is invalid then we'll return false
+        int mcc;
+        try {
+            mcc = Integer.parseInt(operatorNumeric.substring(0, 3));
+        } catch (Exception e) {
+            if (DBG) {
+                log("shouldFixTimeZoneNow: no mcc, operatorNumeric=" + operatorNumeric +
+                        " retVal=false");
+            }
+            return false;
+        }
+
+        // If prevMcc is invalid will make it different from mcc
+        // so we'll return true if the card exists.
+        int prevMcc;
+        try {
+            prevMcc = Integer.parseInt(prevOperatorNumeric.substring(0, 3));
+        } catch (Exception e) {
+            prevMcc = mcc + 1;
+        }
+
+        // Determine if the Icc card exists
+        boolean iccCardExist = false;
+        if (mUiccApplcation != null) {
+            iccCardExist = mUiccApplcation.getState() != AppState.APPSTATE_UNKNOWN;
+        }
+
+        // Determine retVal
+        boolean retVal = ((iccCardExist && (mcc != prevMcc)) || needToFixTimeZone);
+        if (DBG) {
+            long ctm = System.currentTimeMillis();
+            log("shouldFixTimeZoneNow: retVal=" + retVal +
+                    " iccCardExist=" + iccCardExist +
+                    " operatorNumeric=" + operatorNumeric + " mcc=" + mcc +
+                    " prevOperatorNumeric=" + prevOperatorNumeric + " prevMcc=" + prevMcc +
+                    " needToFixTimeZone=" + needToFixTimeZone +
+                    " ltod=" + TimeUtils.logTimeOfDay(ctm));
+        }
+        return retVal;
+    }
+
+    public String getSystemProperty(String property, String defValue) {
+        return TelephonyManager.getTelephonyProperty(mPhone.getPhoneId(), property, defValue);
+    }
+
+    /**
+     * @return all available cell information or null if none.
+     */
+    public List<CellInfo> getAllCellInfo(WorkSource workSource) {
+        CellInfoResult result = new CellInfoResult();
+        if (VDBG) log("SST.getAllCellInfo(): E");
+        int ver = mCi.getRilVersion();
+        if (ver >= 8) {
+            if (isCallerOnDifferentThread()) {
+                if ((SystemClock.elapsedRealtime() - mLastCellInfoListTime)
+                        > LAST_CELL_INFO_LIST_MAX_AGE_MS) {
+                    Message msg = obtainMessage(EVENT_GET_CELL_INFO_LIST, result);
+                    synchronized(result.lockObj) {
+                        result.list = null;
+                        mCi.getCellInfoList(msg, workSource);
+                        try {
+                            result.lockObj.wait(5000);
+                        } catch (InterruptedException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                } else {
+                    if (DBG) log("SST.getAllCellInfo(): return last, back to back calls");
+                    result.list = mLastCellInfoList;
+                }
+            } else {
+                if (DBG) log("SST.getAllCellInfo(): return last, same thread can't block");
+                result.list = mLastCellInfoList;
+            }
+        } else {
+            if (DBG) log("SST.getAllCellInfo(): not implemented");
+            result.list = null;
+        }
+        synchronized(result.lockObj) {
+            if (result.list != null) {
+                if (VDBG) log("SST.getAllCellInfo(): X size=" + result.list.size()
+                        + " list=" + result.list);
+                return result.list;
+            } else {
+                if (DBG) log("SST.getAllCellInfo(): X size=0 list=null");
+                return null;
+            }
+        }
+    }
+
+    /**
+     * @return signal strength
+     */
+    public SignalStrength getSignalStrength() {
+        return mSignalStrength;
+    }
+
+    /**
+     * Registration point for subscription info ready
+     * @param h handler to notify
+     * @param what what code of message when delivered
+     * @param obj placed in Message.obj
+     */
+    public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mCdmaForSubscriptionInfoReadyRegistrants.add(r);
+
+        if (isMinInfoReady()) {
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForSubscriptionInfoReady(Handler h) {
+        mCdmaForSubscriptionInfoReadyRegistrants.remove(h);
+    }
+
+    /**
+     * Save current source of cdma subscription
+     * @param source - 1 for NV, 0 for RUIM
+     */
+    private void saveCdmaSubscriptionSource(int source) {
+        log("Storing cdma subscription source: " + source);
+        Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+                Settings.Global.CDMA_SUBSCRIPTION_MODE,
+                source);
+        log("Read from settings: " + Settings.Global.getInt(mPhone.getContext().getContentResolver(),
+                Settings.Global.CDMA_SUBSCRIPTION_MODE, -1));
+    }
+
+    private void getSubscriptionInfoAndStartPollingThreads() {
+        mCi.getCDMASubscription(obtainMessage(EVENT_POLL_STATE_CDMA_SUBSCRIPTION));
+
+        // Get Registration Information
+        pollState();
+    }
+
+    private void handleCdmaSubscriptionSource(int newSubscriptionSource) {
+        log("Subscription Source : " + newSubscriptionSource);
+        mIsSubscriptionFromRuim =
+                (newSubscriptionSource == CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM);
+        log("isFromRuim: " + mIsSubscriptionFromRuim);
+        saveCdmaSubscriptionSource(newSubscriptionSource);
+        if (!mIsSubscriptionFromRuim) {
+            // NV is ready when subscription source is NV
+            sendMessage(obtainMessage(EVENT_NV_READY));
+        }
+    }
+
+    private void dumpEarfcnPairList(PrintWriter pw) {
+        pw.print(" mEarfcnPairListForRsrpBoost={");
+        if (mEarfcnPairListForRsrpBoost != null) {
+            int i = mEarfcnPairListForRsrpBoost.size();
+            for (Pair<Integer, Integer> earfcnPair : mEarfcnPairListForRsrpBoost) {
+                pw.print("(");
+                pw.print(earfcnPair.first);
+                pw.print(",");
+                pw.print(earfcnPair.second);
+                pw.print(")");
+                if ((--i) != 0) {
+                    pw.print(",");
+                }
+            }
+        }
+        pw.println("}");
+    }
+
+    private void dumpCellInfoList(PrintWriter pw) {
+        pw.print(" mLastCellInfoList={");
+        if(mLastCellInfoList != null) {
+            boolean first = true;
+            for(CellInfo info : mLastCellInfoList) {
+               if(first == false) {
+                   pw.print(",");
+               }
+               first = false;
+               pw.print(info.toString());
+            }
+        }
+        pw.println("}");
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("ServiceStateTracker:");
+        pw.println(" mSubId=" + mSubId);
+        pw.println(" mSS=" + mSS);
+        pw.println(" mNewSS=" + mNewSS);
+        pw.println(" mVoiceCapable=" + mVoiceCapable);
+        pw.println(" mRestrictedState=" + mRestrictedState);
+        pw.println(" mPollingContext=" + mPollingContext + " - " +
+                (mPollingContext != null ? mPollingContext[0] : ""));
+        pw.println(" mDesiredPowerState=" + mDesiredPowerState);
+        pw.println(" mDontPollSignalStrength=" + mDontPollSignalStrength);
+        pw.println(" mSignalStrength=" + mSignalStrength);
+        pw.println(" mLastSignalStrength=" + mLastSignalStrength);
+        pw.println(" mRestrictedState=" + mRestrictedState);
+        pw.println(" mPendingRadioPowerOffAfterDataOff=" + mPendingRadioPowerOffAfterDataOff);
+        pw.println(" mPendingRadioPowerOffAfterDataOffTag=" + mPendingRadioPowerOffAfterDataOffTag);
+        pw.println(" mCellLoc=" + Rlog.pii(VDBG, mCellLoc));
+        pw.println(" mNewCellLoc=" + Rlog.pii(VDBG, mNewCellLoc));
+        pw.println(" mLastCellInfoListTime=" + mLastCellInfoListTime);
+        dumpCellInfoList(pw);
+        pw.flush();
+        pw.println(" mPreferredNetworkType=" + mPreferredNetworkType);
+        pw.println(" mMaxDataCalls=" + mMaxDataCalls);
+        pw.println(" mNewMaxDataCalls=" + mNewMaxDataCalls);
+        pw.println(" mReasonDataDenied=" + mReasonDataDenied);
+        pw.println(" mNewReasonDataDenied=" + mNewReasonDataDenied);
+        pw.println(" mGsmRoaming=" + mGsmRoaming);
+        pw.println(" mDataRoaming=" + mDataRoaming);
+        pw.println(" mEmergencyOnly=" + mEmergencyOnly);
+        pw.println(" mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz);
+        pw.flush();
+        pw.println(" mZoneOffset=" + mZoneOffset);
+        pw.println(" mZoneDst=" + mZoneDst);
+        pw.println(" mZoneTime=" + mZoneTime);
+        pw.println(" mGotCountryCode=" + mGotCountryCode);
+        pw.println(" mNitzUpdatedTime=" + mNitzUpdatedTime);
+        pw.println(" mSavedTimeZone=" + mSavedTimeZone);
+        pw.println(" mSavedTime=" + mSavedTime);
+        pw.println(" mSavedAtTime=" + mSavedAtTime);
+        pw.println(" mStartedGprsRegCheck=" + mStartedGprsRegCheck);
+        pw.println(" mReportedGprsNoReg=" + mReportedGprsNoReg);
+        pw.println(" mNotification=" + mNotification);
+        pw.println(" mWakeLock=" + mWakeLock);
+        pw.println(" mCurSpn=" + mCurSpn);
+        pw.println(" mCurDataSpn=" + mCurDataSpn);
+        pw.println(" mCurShowSpn=" + mCurShowSpn);
+        pw.println(" mCurPlmn=" + mCurPlmn);
+        pw.println(" mCurShowPlmn=" + mCurShowPlmn);
+        pw.flush();
+        pw.println(" mCurrentOtaspMode=" + mCurrentOtaspMode);
+        pw.println(" mRoamingIndicator=" + mRoamingIndicator);
+        pw.println(" mIsInPrl=" + mIsInPrl);
+        pw.println(" mDefaultRoamingIndicator=" + mDefaultRoamingIndicator);
+        pw.println(" mRegistrationState=" + mRegistrationState);
+        pw.println(" mMdn=" + mMdn);
+        pw.println(" mHomeSystemId=" + mHomeSystemId);
+        pw.println(" mHomeNetworkId=" + mHomeNetworkId);
+        pw.println(" mMin=" + mMin);
+        pw.println(" mPrlVersion=" + mPrlVersion);
+        pw.println(" mIsMinInfoReady=" + mIsMinInfoReady);
+        pw.println(" mIsEriTextLoaded=" + mIsEriTextLoaded);
+        pw.println(" mIsSubscriptionFromRuim=" + mIsSubscriptionFromRuim);
+        pw.println(" mCdmaSSM=" + mCdmaSSM);
+        pw.println(" mRegistrationDeniedReason=" + mRegistrationDeniedReason);
+        pw.println(" mCurrentCarrier=" + mCurrentCarrier);
+        pw.flush();
+        pw.println(" mImsRegistered=" + mImsRegistered);
+        pw.println(" mImsRegistrationOnOff=" + mImsRegistrationOnOff);
+        pw.println(" mAlarmSwitch=" + mAlarmSwitch);
+        pw.println(" mRadioDisabledByCarrier" + mRadioDisabledByCarrier);
+        pw.println(" mPowerOffDelayNeed=" + mPowerOffDelayNeed);
+        pw.println(" mDeviceShuttingDown=" + mDeviceShuttingDown);
+        pw.println(" mSpnUpdatePending=" + mSpnUpdatePending);
+        pw.println(" mLteRsrpBoost=" + mLteRsrpBoost);
+        dumpEarfcnPairList(pw);
+
+        pw.println(" Roaming Log:");
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        ipw.increaseIndent();
+        mRoamingLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        ipw.println(" Attach Log:");
+        ipw.increaseIndent();
+        mAttachLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        ipw.println(" Phone Change Log:");
+        ipw.increaseIndent();
+        mPhoneTypeLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        ipw.println(" Rat Change Log:");
+        ipw.increaseIndent();
+        mRatLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        ipw.println(" Radio power Log:");
+        ipw.increaseIndent();
+        mRadioPowerLog.dump(fd, ipw, args);
+
+        ipw.println(" Time Logs:");
+        ipw.increaseIndent();
+        mTimeLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        ipw.println(" Time zone Logs:");
+        ipw.increaseIndent();
+        mTimeZoneLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+    }
+
+    public boolean isImsRegistered() {
+        return mImsRegistered;
+    }
+    /**
+     * Verifies the current thread is the same as the thread originally
+     * used in the initialization of this instance. Throws RuntimeException
+     * if not.
+     *
+     * @exception RuntimeException if the current thread is not
+     * the thread that originally obtained this Phone instance.
+     */
+    protected void checkCorrectThread() {
+        if (Thread.currentThread() != getLooper().getThread()) {
+            throw new RuntimeException(
+                    "ServiceStateTracker must be used from within one thread");
+        }
+    }
+
+    protected boolean isCallerOnDifferentThread() {
+        boolean value = Thread.currentThread() != getLooper().getThread();
+        if (VDBG) log("isCallerOnDifferentThread: " + value);
+        return value;
+    }
+
+    protected void updateCarrierMccMncConfiguration(String newOp, String oldOp, Context context) {
+        // if we have a change in operator, notify wifi (even to/from none)
+        if (((newOp == null) && (TextUtils.isEmpty(oldOp) == false)) ||
+                ((newOp != null) && (newOp.equals(oldOp) == false))) {
+            log("update mccmnc=" + newOp + " fromServiceState=true");
+            MccTable.updateMccMncConfiguration(context, newOp, true);
+        }
+    }
+
+    /**
+     * Check ISO country by MCC to see if phone is roaming in same registered country
+     */
+    protected boolean inSameCountry(String operatorNumeric) {
+        if (TextUtils.isEmpty(operatorNumeric) || (operatorNumeric.length() < 5)) {
+            // Not a valid network
+            return false;
+        }
+        final String homeNumeric = getHomeOperatorNumeric();
+        if (TextUtils.isEmpty(homeNumeric) || (homeNumeric.length() < 5)) {
+            // Not a valid SIM MCC
+            return false;
+        }
+        boolean inSameCountry = true;
+        final String networkMCC = operatorNumeric.substring(0, 3);
+        final String homeMCC = homeNumeric.substring(0, 3);
+        final String networkCountry = MccTable.countryCodeForMcc(Integer.parseInt(networkMCC));
+        final String homeCountry = MccTable.countryCodeForMcc(Integer.parseInt(homeMCC));
+        if (networkCountry.isEmpty() || homeCountry.isEmpty()) {
+            // Not a valid country
+            return false;
+        }
+        inSameCountry = homeCountry.equals(networkCountry);
+        if (inSameCountry) {
+            return inSameCountry;
+        }
+        // special same country cases
+        if ("us".equals(homeCountry) && "vi".equals(networkCountry)) {
+            inSameCountry = true;
+        } else if ("vi".equals(homeCountry) && "us".equals(networkCountry)) {
+            inSameCountry = true;
+        }
+        return inSameCountry;
+    }
+
+    /**
+     * Set both voice and data roaming type,
+     * judging from the ISO country of SIM VS network.
+     */
+    protected void setRoamingType(ServiceState currentServiceState) {
+        final boolean isVoiceInService =
+                (currentServiceState.getVoiceRegState() == ServiceState.STATE_IN_SERVICE);
+        if (isVoiceInService) {
+            if (currentServiceState.getVoiceRoaming()) {
+                if (mPhone.isPhoneTypeGsm()) {
+                    // check roaming type by MCC
+                    if (inSameCountry(currentServiceState.getVoiceOperatorNumeric())) {
+                        currentServiceState.setVoiceRoamingType(
+                                ServiceState.ROAMING_TYPE_DOMESTIC);
+                    } else {
+                        currentServiceState.setVoiceRoamingType(
+                                ServiceState.ROAMING_TYPE_INTERNATIONAL);
+                    }
+                } else {
+                    // some carrier defines international roaming by indicator
+                    int[] intRoamingIndicators = mPhone.getContext().getResources().getIntArray(
+                            com.android.internal.R.array.config_cdma_international_roaming_indicators);
+                    if ((intRoamingIndicators != null) && (intRoamingIndicators.length > 0)) {
+                        // It's domestic roaming at least now
+                        currentServiceState.setVoiceRoamingType(ServiceState.ROAMING_TYPE_DOMESTIC);
+                        int curRoamingIndicator = currentServiceState.getCdmaRoamingIndicator();
+                        for (int i = 0; i < intRoamingIndicators.length; i++) {
+                            if (curRoamingIndicator == intRoamingIndicators[i]) {
+                                currentServiceState.setVoiceRoamingType(
+                                        ServiceState.ROAMING_TYPE_INTERNATIONAL);
+                                break;
+                            }
+                        }
+                    } else {
+                        // check roaming type by MCC
+                        if (inSameCountry(currentServiceState.getVoiceOperatorNumeric())) {
+                            currentServiceState.setVoiceRoamingType(
+                                    ServiceState.ROAMING_TYPE_DOMESTIC);
+                        } else {
+                            currentServiceState.setVoiceRoamingType(
+                                    ServiceState.ROAMING_TYPE_INTERNATIONAL);
+                        }
+                    }
+                }
+            } else {
+                currentServiceState.setVoiceRoamingType(ServiceState.ROAMING_TYPE_NOT_ROAMING);
+            }
+        }
+        final boolean isDataInService =
+                (currentServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE);
+        final int dataRegType = currentServiceState.getRilDataRadioTechnology();
+        if (isDataInService) {
+            if (!currentServiceState.getDataRoaming()) {
+                currentServiceState.setDataRoamingType(ServiceState.ROAMING_TYPE_NOT_ROAMING);
+            } else {
+                if (mPhone.isPhoneTypeGsm()) {
+                    if (ServiceState.isGsm(dataRegType)) {
+                        if (isVoiceInService) {
+                            // GSM data should have the same state as voice
+                            currentServiceState.setDataRoamingType(currentServiceState
+                                    .getVoiceRoamingType());
+                        } else {
+                            // we can not decide GSM data roaming type without voice
+                            currentServiceState.setDataRoamingType(ServiceState.ROAMING_TYPE_UNKNOWN);
+                        }
+                    } else {
+                        // we can not decide 3gpp2 roaming state here
+                        currentServiceState.setDataRoamingType(ServiceState.ROAMING_TYPE_UNKNOWN);
+                    }
+                } else {
+                    if (ServiceState.isCdma(dataRegType)) {
+                        if (isVoiceInService) {
+                            // CDMA data should have the same state as voice
+                            currentServiceState.setDataRoamingType(currentServiceState
+                                    .getVoiceRoamingType());
+                        } else {
+                            // we can not decide CDMA data roaming type without voice
+                            // set it as same as last time
+                            currentServiceState.setDataRoamingType(ServiceState.ROAMING_TYPE_UNKNOWN);
+                        }
+                    } else {
+                        // take it as 3GPP roaming
+                        if (inSameCountry(currentServiceState.getDataOperatorNumeric())) {
+                            currentServiceState.setDataRoamingType(ServiceState.ROAMING_TYPE_DOMESTIC);
+                        } else {
+                            currentServiceState.setDataRoamingType(
+                                    ServiceState.ROAMING_TYPE_INTERNATIONAL);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void setSignalStrengthDefaultValues() {
+        mSignalStrength = new SignalStrength(true);
+    }
+
+    protected String getHomeOperatorNumeric() {
+        String numeric = ((TelephonyManager) mPhone.getContext().
+                getSystemService(Context.TELEPHONY_SERVICE)).
+                getSimOperatorNumericForPhone(mPhone.getPhoneId());
+        if (!mPhone.isPhoneTypeGsm() && TextUtils.isEmpty(numeric)) {
+            numeric = SystemProperties.get(GsmCdmaPhone.PROPERTY_CDMA_HOME_OPERATOR_NUMERIC, "");
+        }
+        return numeric;
+    }
+
+    protected int getPhoneId() {
+        return mPhone.getPhoneId();
+    }
+
+    /* Reset Service state when IWLAN is enabled as polling in airplane mode
+     * causes state to go to OUT_OF_SERVICE state instead of STATE_OFF
+     */
+    protected void resetServiceStateInIwlanMode() {
+        if (mCi.getRadioState() == CommandsInterface.RadioState.RADIO_OFF) {
+            boolean resetIwlanRatVal = false;
+            log("set service state as POWER_OFF");
+            if (ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                        == mNewSS.getRilDataRadioTechnology()) {
+                log("pollStateDone: mNewSS = " + mNewSS);
+                log("pollStateDone: reset iwlan RAT value");
+                resetIwlanRatVal = true;
+            }
+            // operator info should be kept in SS
+            String operator = mNewSS.getOperatorAlphaLong();
+            mNewSS.setStateOff();
+            if (resetIwlanRatVal) {
+                mNewSS.setRilDataRadioTechnology(ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN);
+                mNewSS.setDataRegState(ServiceState.STATE_IN_SERVICE);
+                mNewSS.setOperatorAlphaLong(operator);
+                log("pollStateDone: mNewSS = " + mNewSS);
+            }
+        }
+    }
+
+    /**
+     * Check if device is non-roaming and always on home network.
+     *
+     * @param b carrier config bundle obtained from CarrierConfigManager
+     * @return true if network is always on home network, false otherwise
+     * @see CarrierConfigManager
+     */
+    protected final boolean alwaysOnHomeNetwork(BaseBundle b) {
+        return b.getBoolean(CarrierConfigManager.KEY_FORCE_HOME_NETWORK_BOOL);
+    }
+
+    /**
+     * Check if the network identifier has membership in the set of
+     * network identifiers stored in the carrier config bundle.
+     *
+     * @param b carrier config bundle obtained from CarrierConfigManager
+     * @param network The network identifier to check network existence in bundle
+     * @param key The key to index into the bundle presenting a string array of
+     *            networks to check membership
+     * @return true if network has membership in bundle networks, false otherwise
+     * @see CarrierConfigManager
+     */
+    private boolean isInNetwork(BaseBundle b, String network, String key) {
+        String[] networks = b.getStringArray(key);
+
+        if (networks != null && Arrays.asList(networks).contains(network)) {
+            return true;
+        }
+        return false;
+    }
+
+    protected final boolean isRoamingInGsmNetwork(BaseBundle b, String network) {
+        return isInNetwork(b, network, CarrierConfigManager.KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY);
+    }
+
+    protected final boolean isNonRoamingInGsmNetwork(BaseBundle b, String network) {
+        return isInNetwork(b, network, CarrierConfigManager.KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY);
+    }
+
+    protected final boolean isRoamingInCdmaNetwork(BaseBundle b, String network) {
+        return isInNetwork(b, network, CarrierConfigManager.KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY);
+    }
+
+    protected final boolean isNonRoamingInCdmaNetwork(BaseBundle b, String network) {
+        return isInNetwork(b, network, CarrierConfigManager.KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY);
+    }
+
+    /** Check if the device is shutting down. */
+    public boolean isDeviceShuttingDown() {
+        return mDeviceShuttingDown;
+    }
+
+    /**
+     * Consider dataRegState if voiceRegState is OOS to determine SPN to be displayed
+     */
+    protected int getCombinedRegState() {
+        int regState = mSS.getVoiceRegState();
+        int dataRegState = mSS.getDataRegState();
+        if ((regState == ServiceState.STATE_OUT_OF_SERVICE
+                || regState == ServiceState.STATE_POWER_OFF)
+                && (dataRegState == ServiceState.STATE_IN_SERVICE)) {
+            log("getCombinedRegState: return STATE_IN_SERVICE as Data is in service");
+            regState = dataRegState;
+        }
+        return regState;
+    }
+
+    /**
+     * Update time zone by network country code, works on countries which only have one time zone.
+     * @param iso Country code from network MCC
+     */
+    private void updateTimeZoneByNetworkCountryCode(String iso) {
+        // Test both paths if ignore nitz is true
+        boolean testOneUniqueOffsetPath = SystemProperties.getBoolean(
+                TelephonyProperties.PROPERTY_IGNORE_NITZ, false)
+                && ((SystemClock.uptimeMillis() & 1) == 0);
+
+        List<String> uniqueZoneIds = TimeUtils.getTimeZoneIdsWithUniqueOffsets(iso);
+        if ((uniqueZoneIds.size() == 1) || testOneUniqueOffsetPath) {
+            String zoneId = uniqueZoneIds.get(0);
+            if (DBG) {
+                log("updateTimeZoneByNetworkCountryCode: no nitz but one TZ for iso-cc=" + iso
+                        + " with zone.getID=" + zoneId
+                        + " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath);
+            }
+            mTimeZoneLog.log("updateTimeZoneByNetworkCountryCode: set time zone=" + zoneId
+                    + " iso=" + iso);
+            setAndBroadcastNetworkSetTimeZone(zoneId);
+        } else {
+            if (DBG) {
+                log("updateTimeZoneByNetworkCountryCode: there are " + uniqueZoneIds.size()
+                        + " unique offsets for iso-cc='" + iso
+                        + " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath
+                        + "', do nothing");
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/SettingsObserver.java b/com/android/internal/telephony/SettingsObserver.java
new file mode 100644
index 0000000..2253c36
--- /dev/null
+++ b/com/android/internal/telephony/SettingsObserver.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.telephony.Rlog;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The class to describe settings observer
+ */
+public class SettingsObserver extends ContentObserver {
+    private final Map<Uri, Integer> mUriEventMap;
+    private final Context mContext;
+    private final Handler mHandler;
+    private static final String TAG = "SettingsObserver";
+
+    public SettingsObserver(Context context, Handler handler) {
+        super(null);
+        mUriEventMap = new HashMap<>();
+        mContext = context;
+        mHandler = handler;
+    }
+
+    /**
+     * Start observing a content.
+     * @param uri Content URI
+     * @param what The event to fire if the content changes
+     */
+    public void observe(Uri uri, int what) {
+        mUriEventMap.put(uri, what);
+        final ContentResolver resolver = mContext.getContentResolver();
+        resolver.registerContentObserver(uri, false, this);
+    }
+
+    /**
+     * Stop observing a content.
+     */
+    public void unobserve() {
+        final ContentResolver resolver = mContext.getContentResolver();
+        resolver.unregisterContentObserver(this);
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        Rlog.e(TAG, "Should never be reached.");
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        final Integer what = mUriEventMap.get(uri);
+        if (what != null) {
+            mHandler.obtainMessage(what.intValue()).sendToTarget();
+        } else {
+            Rlog.e(TAG, "No matching event to send for URI=" + uri);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/SimActivationTracker.java b/com/android/internal/telephony/SimActivationTracker.java
new file mode 100644
index 0000000..8fd6eed
--- /dev/null
+++ b/com/android/internal/telephony/SimActivationTracker.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.telephony.Rlog;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.security.InvalidParameterException;
+
+import static android.telephony.TelephonyManager.SIM_ACTIVATION_STATE_ACTIVATED;
+import static android.telephony.TelephonyManager.SIM_ACTIVATION_STATE_DEACTIVATED;
+import static android.telephony.TelephonyManager.SIM_ACTIVATION_STATE_UNKNOWN;
+import static android.telephony.TelephonyManager.SIM_ACTIVATION_STATE_RESTRICTED;
+import static android.telephony.TelephonyManager.SIM_ACTIVATION_STATE_ACTIVATING;
+
+public class SimActivationTracker {
+    /**
+     * SimActivationTracker(SAT) serves as a central place to keep track of all knowledge of
+     * voice & data activation state which is set by custom/default carrier apps.
+     * Each phone object maintains a single activation tracker.
+     */
+    private static final boolean DBG = true;
+    private static final String LOG_TAG = "SAT";
+    private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
+
+    private Phone mPhone;
+
+    /**
+     * Voice Activation State
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_UNKNOWN
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_ACTIVATING
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_ACTIVATED
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_DEACTIVATED
+     */
+    private int mVoiceActivationState;
+
+    /**
+     * Data Activation State
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_UNKNOWN
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_ACTIVATING
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_ACTIVATED
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_DEACTIVATED
+     * @see android.telephony.TelephonyManager#SIM_ACTIVATION_STATE_RESTRICTED
+     */
+    private int mDataActivationState;
+    private final LocalLog mVoiceActivationStateLog = new LocalLog(10);
+    private final LocalLog mDataActivationStateLog = new LocalLog(10);
+    private final BroadcastReceiver mReceiver;
+
+    public SimActivationTracker(Phone phone) {
+        mPhone = phone;
+        mVoiceActivationState = SIM_ACTIVATION_STATE_UNKNOWN;
+        mDataActivationState = SIM_ACTIVATION_STATE_UNKNOWN;
+
+        mReceiver = new  BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                String action = intent.getAction();
+                if (VDBG) log("action: " + action);
+                if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)){
+                    if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(
+                            intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE))) {
+                        if (DBG) log("onSimAbsent, reset activation state to UNKNOWN");
+                        setVoiceActivationState(SIM_ACTIVATION_STATE_UNKNOWN);
+                        setDataActivationState(SIM_ACTIVATION_STATE_UNKNOWN);
+                    }
+                }
+            }
+        };
+
+        IntentFilter intentFilter = new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+        mPhone.getContext().registerReceiver(mReceiver, intentFilter);
+    }
+
+    public void setVoiceActivationState(int state) {
+        if (!isValidActivationState(state) || (SIM_ACTIVATION_STATE_RESTRICTED == state)) {
+            throw new IllegalArgumentException("invalid voice activation state: " + state);
+        }
+        if (DBG) log("setVoiceActivationState=" + state);
+        mVoiceActivationState = state;
+        mVoiceActivationStateLog.log(toString(state));
+        mPhone.notifyVoiceActivationStateChanged(state);
+    }
+
+    public void setDataActivationState(int state) {
+        if (!isValidActivationState(state)) {
+            throw new IllegalArgumentException("invalid data activation state: " + state);
+        }
+        if (DBG) log("setDataActivationState=" + state);
+        mDataActivationState = state;
+        mDataActivationStateLog.log(toString(state));
+        mPhone.notifyDataActivationStateChanged(state);
+    }
+
+    public int getVoiceActivationState() {
+        return mVoiceActivationState;
+    }
+
+    public int getDataActivationState() {
+        return mDataActivationState;
+    }
+
+    private static boolean isValidActivationState(int state) {
+        switch (state) {
+            case SIM_ACTIVATION_STATE_UNKNOWN:
+            case SIM_ACTIVATION_STATE_ACTIVATING:
+            case SIM_ACTIVATION_STATE_ACTIVATED:
+            case SIM_ACTIVATION_STATE_DEACTIVATED:
+            case SIM_ACTIVATION_STATE_RESTRICTED:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private static String toString(int state) {
+        switch (state) {
+            case SIM_ACTIVATION_STATE_UNKNOWN:
+                return "unknown";
+            case SIM_ACTIVATION_STATE_ACTIVATING:
+                return "activating";
+            case SIM_ACTIVATION_STATE_ACTIVATED:
+                return "activated";
+            case SIM_ACTIVATION_STATE_DEACTIVATED:
+                return "deactivated";
+            case SIM_ACTIVATION_STATE_RESTRICTED:
+                return "restricted";
+            default:
+                return "invalid";
+        }
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        pw.println(" mVoiceActivationState Log:");
+        ipw.increaseIndent();
+        mVoiceActivationStateLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+
+        pw.println(" mDataActivationState Log:");
+        ipw.increaseIndent();
+        mDataActivationStateLog.dump(fd, ipw, args);
+        ipw.decreaseIndent();
+    }
+
+    public void dispose() {
+        mPhone.getContext().unregisterReceiver(mReceiver);
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/Sms7BitEncodingTranslator.java b/com/android/internal/telephony/Sms7BitEncodingTranslator.java
new file mode 100644
index 0000000..439eaea
--- /dev/null
+++ b/com/android/internal/telephony/Sms7BitEncodingTranslator.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+import android.telephony.Rlog;
+import android.os.Build;
+import android.util.SparseIntArray;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.util.XmlUtils;
+import com.android.internal.telephony.cdma.sms.UserData;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class Sms7BitEncodingTranslator {
+    private static final String TAG = "Sms7BitEncodingTranslator";
+    private static final boolean DBG = Build.IS_DEBUGGABLE ;
+    private static boolean mIs7BitTranslationTableLoaded = false;
+    private static SparseIntArray mTranslationTable = null;
+    private static SparseIntArray mTranslationTableCommon = null;
+    private static SparseIntArray mTranslationTableGSM = null;
+    private static SparseIntArray mTranslationTableCDMA = null;
+
+    // Parser variables
+    private static final String XML_START_TAG = "SmsEnforce7BitTranslationTable";
+    private static final String XML_TRANSLATION_TYPE_TAG = "TranslationType";
+    private static final String XML_CHARACTOR_TAG = "Character";
+    private static final String XML_FROM_TAG = "from";
+    private static final String XML_TO_TAG = "to";
+
+    /**
+     * Translates each message character that is not supported by GSM 7bit
+     * alphabet into a supported one
+     *
+     * @param message
+     *            message to be translated
+     * @param throwsException
+     *            if true and some error occurs during translation, an exception
+     *            is thrown; otherwise a null String is returned
+     * @return translated message or null if some error occur
+     */
+    public static String translate(CharSequence message) {
+        if (message == null) {
+            Rlog.w(TAG, "Null message can not be translated");
+            return null;
+        }
+
+        int size = message.length();
+        if (size <= 0) {
+            return "";
+        }
+
+        if (!mIs7BitTranslationTableLoaded) {
+            mTranslationTableCommon = new SparseIntArray();
+            mTranslationTableGSM = new SparseIntArray();
+            mTranslationTableCDMA = new SparseIntArray();
+            load7BitTranslationTableFromXml();
+            mIs7BitTranslationTableLoaded = true;
+        }
+
+        if ((mTranslationTableCommon != null && mTranslationTableCommon.size() > 0) ||
+                (mTranslationTableGSM != null && mTranslationTableGSM.size() > 0) ||
+                (mTranslationTableCDMA != null && mTranslationTableCDMA.size() > 0)) {
+            char[] output = new char[size];
+            boolean isCdmaFormat = useCdmaFormatForMoSms();
+            for (int i = 0; i < size; i++) {
+                output[i] = translateIfNeeded(message.charAt(i), isCdmaFormat);
+            }
+
+            return String.valueOf(output);
+        }
+
+        return null;
+    }
+
+    /**
+     * Translates a single character into its corresponding acceptable one, if
+     * needed, based on GSM 7-bit alphabet
+     *
+     * @param c
+     *            character to be translated
+     * @return original character, if it's present on GSM 7-bit alphabet; a
+     *         corresponding character, based on the translation table or white
+     *         space, if no mapping is found in the translation table for such
+     *         character
+     */
+    private static char translateIfNeeded(char c, boolean isCdmaFormat) {
+        if (noTranslationNeeded(c, isCdmaFormat)) {
+            if (DBG) {
+                Rlog.v(TAG, "No translation needed for " + Integer.toHexString(c));
+            }
+            return c;
+        }
+
+        /*
+         * Trying to translate unicode to Gsm 7-bit alphabet; If c is not
+         * present on translation table, c does not belong to Unicode Latin-1
+         * (Basic + Supplement), so we don't know how to translate it to a Gsm
+         * 7-bit character! We replace c for an empty space and advises the user
+         * about it.
+         */
+        int translation = -1;
+
+        if (mTranslationTableCommon != null) {
+            translation = mTranslationTableCommon.get(c, -1);
+        }
+
+        if (translation == -1) {
+            if (isCdmaFormat) {
+                if (mTranslationTableCDMA != null) {
+                    translation = mTranslationTableCDMA.get(c, -1);
+                }
+            } else {
+                if (mTranslationTableGSM != null) {
+                    translation = mTranslationTableGSM.get(c, -1);
+                }
+            }
+        }
+
+        if (translation != -1) {
+            if (DBG) {
+                Rlog.v(TAG, Integer.toHexString(c) + " (" + c + ")" + " translated to "
+                        + Integer.toHexString(translation) + " (" + (char) translation + ")");
+            }
+            return (char) translation;
+        } else {
+            if (DBG) {
+                Rlog.w(TAG, "No translation found for " + Integer.toHexString(c)
+                        + "! Replacing for empty space");
+            }
+            return ' ';
+        }
+    }
+
+    private static boolean noTranslationNeeded(char c, boolean isCdmaFormat) {
+        if (isCdmaFormat) {
+            return GsmAlphabet.isGsmSeptets(c) && UserData.charToAscii.get(c, -1) != -1;
+        }
+        else {
+            return GsmAlphabet.isGsmSeptets(c);
+        }
+    }
+
+    private static boolean useCdmaFormatForMoSms() {
+        if (!SmsManager.getDefault().isImsSmsSupported()) {
+            // use Voice technology to determine SMS format.
+            return TelephonyManager.getDefault().getCurrentPhoneType()
+                    == PhoneConstants.PHONE_TYPE_CDMA;
+        }
+        // IMS is registered with SMS support, check the SMS format supported
+        return (SmsConstants.FORMAT_3GPP2.equals(SmsManager.getDefault().getImsSmsFormat()));
+    }
+
+    /**
+     * Load the whole translation table file from the framework resource
+     * encoded in XML.
+     */
+    private static void load7BitTranslationTableFromXml() {
+        XmlResourceParser parser = null;
+        Resources r = Resources.getSystem();
+
+        if (parser == null) {
+            if (DBG) Rlog.d(TAG, "load7BitTranslationTableFromXml: open normal file");
+            parser = r.getXml(com.android.internal.R.xml.sms_7bit_translation_table);
+        }
+
+        try {
+            XmlUtils.beginDocument(parser, XML_START_TAG);
+            while (true)  {
+                XmlUtils.nextElement(parser);
+                String tag = parser.getName();
+                if (DBG) {
+                    Rlog.d(TAG, "tag: " + tag);
+                }
+                if (XML_TRANSLATION_TYPE_TAG.equals(tag)) {
+                    String type = parser.getAttributeValue(null, "Type");
+                    if (DBG) {
+                        Rlog.d(TAG, "type: " + type);
+                    }
+                    if (type.equals("common")) {
+                        mTranslationTable = mTranslationTableCommon;
+                    } else if (type.equals("gsm")) {
+                        mTranslationTable = mTranslationTableGSM;
+                    } else if (type.equals("cdma")) {
+                        mTranslationTable = mTranslationTableCDMA;
+                    } else {
+                        Rlog.e(TAG, "Error Parsing 7BitTranslationTable: found incorrect type" + type);
+                    }
+                } else if (XML_CHARACTOR_TAG.equals(tag) && mTranslationTable != null) {
+                    int from = parser.getAttributeUnsignedIntValue(null,
+                            XML_FROM_TAG, -1);
+                    int to = parser.getAttributeUnsignedIntValue(null,
+                            XML_TO_TAG, -1);
+                    if ((from != -1) && (to != -1)) {
+                        if (DBG) {
+                            Rlog.d(TAG, "Loading mapping " + Integer.toHexString(from)
+                                    .toUpperCase() + " -> " + Integer.toHexString(to)
+                                    .toUpperCase());
+                        }
+                        mTranslationTable.put (from, to);
+                    } else {
+                        Rlog.d(TAG, "Invalid translation table file format");
+                    }
+                } else {
+                    break;
+                }
+            }
+            if (DBG) Rlog.d(TAG, "load7BitTranslationTableFromXml: parsing successful, file loaded");
+        } catch (Exception e) {
+            Rlog.e(TAG, "Got exception while loading 7BitTranslationTable file.", e);
+        } finally {
+            if (parser instanceof XmlResourceParser) {
+                ((XmlResourceParser)parser).close();
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/SmsAddress.java b/com/android/internal/telephony/SmsAddress.java
new file mode 100644
index 0000000..b3892cb
--- /dev/null
+++ b/com/android/internal/telephony/SmsAddress.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony;
+
+public abstract class SmsAddress {
+    // From TS 23.040 9.1.2.5 and TS 24.008 table 10.5.118
+    // and C.S0005-D table 2.7.1.3.2.4-2
+    public static final int TON_UNKNOWN = 0;
+    public static final int TON_INTERNATIONAL = 1;
+    public static final int TON_NATIONAL = 2;
+    public static final int TON_NETWORK = 3;
+    public static final int TON_SUBSCRIBER = 4;
+    public static final int TON_ALPHANUMERIC = 5;
+    public static final int TON_ABBREVIATED = 6;
+
+    public int ton;
+    public String address;
+    public byte[] origBytes;
+
+    /**
+     * Returns the address of the SMS message in String form or null if unavailable
+     */
+    public String getAddressString() {
+        return address;
+    }
+
+    /**
+     * Returns true if this is an alphanumeric address
+     */
+    public boolean isAlphanumeric() {
+        return ton == TON_ALPHANUMERIC;
+    }
+
+    /**
+     * Returns true if this is a network address
+     */
+    public boolean isNetworkSpecific() {
+        return ton == TON_NETWORK;
+    }
+
+    public boolean couldBeEmailGateway() {
+        // Some carriers seems to send email gateway messages in this form:
+        // from: an UNKNOWN TON, 3 or 4 digits long, beginning with a 5
+        // PID: 0x00, Data coding scheme 0x03
+        // So we just attempt to treat any message from an address length <= 4
+        // as an email gateway
+
+        return address.length() <= 4;
+    }
+
+}
diff --git a/com/android/internal/telephony/SmsApplication.java b/com/android/internal/telephony/SmsApplication.java
new file mode 100644
index 0000000..d8ef429
--- /dev/null
+++ b/com/android/internal/telephony/SmsApplication.java
@@ -0,0 +1,980 @@
+/*
+ * 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.internal.telephony;
+
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Debug;
+import android.os.Process;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Class for managing the primary application that we will deliver SMS/MMS messages to
+ *
+ * {@hide}
+ */
+public final class SmsApplication {
+    static final String LOG_TAG = "SmsApplication";
+    private static final String PHONE_PACKAGE_NAME = "com.android.phone";
+    private static final String BLUETOOTH_PACKAGE_NAME = "com.android.bluetooth";
+    private static final String MMS_SERVICE_PACKAGE_NAME = "com.android.mms.service";
+    private static final String TELEPHONY_PROVIDER_PACKAGE_NAME = "com.android.providers.telephony";
+
+    private static final String SCHEME_SMS = "sms";
+    private static final String SCHEME_SMSTO = "smsto";
+    private static final String SCHEME_MMS = "mms";
+    private static final String SCHEME_MMSTO = "mmsto";
+    private static final boolean DEBUG_MULTIUSER = false;
+
+    private static SmsPackageMonitor sSmsPackageMonitor = null;
+
+    public static class SmsApplicationData {
+        /**
+         * Name of this SMS app for display.
+         */
+        private String mApplicationName;
+
+        /**
+         * Package name for this SMS app.
+         */
+        public String mPackageName;
+
+        /**
+         * The class name of the SMS_DELIVER_ACTION receiver in this app.
+         */
+        private String mSmsReceiverClass;
+
+        /**
+         * The class name of the WAP_PUSH_DELIVER_ACTION receiver in this app.
+         */
+        private String mMmsReceiverClass;
+
+        /**
+         * The class name of the ACTION_RESPOND_VIA_MESSAGE intent in this app.
+         */
+        private String mRespondViaMessageClass;
+
+        /**
+         * The class name of the ACTION_SENDTO intent in this app.
+         */
+        private String mSendToClass;
+
+        /**
+         * The class name of the ACTION_DEFAULT_SMS_PACKAGE_CHANGED receiver in this app.
+         */
+        private String mSmsAppChangedReceiverClass;
+
+        /**
+         * The class name of the ACTION_EXTERNAL_PROVIDER_CHANGE receiver in this app.
+         */
+        private String mProviderChangedReceiverClass;
+
+        /**
+         * The class name of the SIM_FULL_ACTION receiver in this app.
+         */
+        private String mSimFullReceiverClass;
+
+        /**
+         * The user-id for this application
+         */
+        private int mUid;
+
+        /**
+         * Returns true if this SmsApplicationData is complete (all intents handled).
+         * @return
+         */
+        public boolean isComplete() {
+            return (mSmsReceiverClass != null && mMmsReceiverClass != null
+                    && mRespondViaMessageClass != null && mSendToClass != null);
+        }
+
+        public SmsApplicationData(String packageName, int uid) {
+            mPackageName = packageName;
+            mUid = uid;
+        }
+
+        public String getApplicationName(Context context) {
+            if (mApplicationName == null) {
+                PackageManager pm = context.getPackageManager();
+                ApplicationInfo appInfo;
+                try {
+                    appInfo = pm.getApplicationInfoAsUser(mPackageName, 0,
+                            UserHandle.getUserId(mUid));
+                } catch (NameNotFoundException e) {
+                    return null;
+                }
+                if (appInfo != null) {
+                    CharSequence label  = pm.getApplicationLabel(appInfo);
+                    mApplicationName = (label == null) ? null : label.toString();
+                }
+            }
+            return mApplicationName;
+        }
+
+        @Override
+        public String toString() {
+            return " mPackageName: " + mPackageName
+                    + " mSmsReceiverClass: " + mSmsReceiverClass
+                    + " mMmsReceiverClass: " + mMmsReceiverClass
+                    + " mRespondViaMessageClass: " + mRespondViaMessageClass
+                    + " mSendToClass: " + mSendToClass
+                    + " mSmsAppChangedClass: " + mSmsAppChangedReceiverClass
+                    + " mProviderChangedReceiverClass: " + mProviderChangedReceiverClass
+                    + " mSimFullReceiverClass: " + mSimFullReceiverClass
+                    + " mUid: " + mUid;
+        }
+    }
+
+    /**
+     * Returns the userId of the Context object, if called from a system app,
+     * otherwise it returns the caller's userId
+     * @param context The context object passed in by the caller.
+     * @return
+     */
+    private static int getIncomingUserId(Context context) {
+        int contextUserId = context.getUserId();
+        final int callingUid = Binder.getCallingUid();
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getIncomingUserHandle caller=" + callingUid + ", myuid="
+                    + android.os.Process.myUid() + "\n\t" + Debug.getCallers(4));
+        }
+        if (UserHandle.getAppId(callingUid)
+                < android.os.Process.FIRST_APPLICATION_UID) {
+            return contextUserId;
+        } else {
+            return UserHandle.getUserId(callingUid);
+        }
+    }
+
+    /**
+     * Returns the list of available SMS apps defined as apps that are registered for both the
+     * SMS_RECEIVED_ACTION (SMS) and WAP_PUSH_RECEIVED_ACTION (MMS) broadcasts (and their broadcast
+     * receivers are enabled)
+     *
+     * Requirements to be an SMS application:
+     * Implement SMS_DELIVER_ACTION broadcast receiver.
+     * Require BROADCAST_SMS permission.
+     *
+     * Implement WAP_PUSH_DELIVER_ACTION broadcast receiver.
+     * Require BROADCAST_WAP_PUSH permission.
+     *
+     * Implement RESPOND_VIA_MESSAGE intent.
+     * Support smsto Uri scheme.
+     * Require SEND_RESPOND_VIA_MESSAGE permission.
+     *
+     * Implement ACTION_SENDTO intent.
+     * Support smsto Uri scheme.
+     */
+    public static Collection<SmsApplicationData> getApplicationCollection(Context context) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return getApplicationCollectionInternal(context, userId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private static Collection<SmsApplicationData> getApplicationCollectionInternal(
+            Context context, int userId) {
+        PackageManager packageManager = context.getPackageManager();
+
+        // Get the list of apps registered for SMS
+        Intent intent = new Intent(Intents.SMS_DELIVER_ACTION);
+        List<ResolveInfo> smsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, 0,
+                userId);
+
+        HashMap<String, SmsApplicationData> receivers = new HashMap<String, SmsApplicationData>();
+
+        // Add one entry to the map for every sms receiver (ignoring duplicate sms receivers)
+        for (ResolveInfo resolveInfo : smsReceivers) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            if (!permission.BROADCAST_SMS.equals(activityInfo.permission)) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            if (!receivers.containsKey(packageName)) {
+                final SmsApplicationData smsApplicationData = new SmsApplicationData(packageName,
+                        activityInfo.applicationInfo.uid);
+                smsApplicationData.mSmsReceiverClass = activityInfo.name;
+                receivers.put(packageName, smsApplicationData);
+            }
+        }
+
+        // Update any existing entries with mms receiver class
+        intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION);
+        intent.setDataAndType(null, "application/vnd.wap.mms-message");
+        List<ResolveInfo> mmsReceivers = packageManager.queryBroadcastReceiversAsUser(intent, 0,
+                userId);
+        for (ResolveInfo resolveInfo : mmsReceivers) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            if (!permission.BROADCAST_WAP_PUSH.equals(activityInfo.permission)) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (smsApplicationData != null) {
+                smsApplicationData.mMmsReceiverClass = activityInfo.name;
+            }
+        }
+
+        // Update any existing entries with respond via message intent class.
+        intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE,
+                Uri.fromParts(SCHEME_SMSTO, "", null));
+        List<ResolveInfo> respondServices = packageManager.queryIntentServicesAsUser(intent, 0,
+                userId);
+        for (ResolveInfo resolveInfo : respondServices) {
+            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            if (serviceInfo == null) {
+                continue;
+            }
+            if (!permission.SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+                continue;
+            }
+            final String packageName = serviceInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (smsApplicationData != null) {
+                smsApplicationData.mRespondViaMessageClass = serviceInfo.name;
+            }
+        }
+
+        // Update any existing entries with supports send to.
+        intent = new Intent(Intent.ACTION_SENDTO,
+                Uri.fromParts(SCHEME_SMSTO, "", null));
+        List<ResolveInfo> sendToActivities = packageManager.queryIntentActivitiesAsUser(intent, 0,
+                userId);
+        for (ResolveInfo resolveInfo : sendToActivities) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (smsApplicationData != null) {
+                smsApplicationData.mSendToClass = activityInfo.name;
+            }
+        }
+
+        // Update any existing entries with the default sms changed handler.
+        intent = new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED);
+        List<ResolveInfo> smsAppChangedReceivers =
+                packageManager.queryBroadcastReceiversAsUser(intent, 0, userId);
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplicationCollectionInternal smsAppChangedActivities=" +
+                    smsAppChangedReceivers);
+        }
+        for (ResolveInfo resolveInfo : smsAppChangedReceivers) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (DEBUG_MULTIUSER) {
+                Log.i(LOG_TAG, "getApplicationCollectionInternal packageName=" +
+                        packageName + " smsApplicationData: " + smsApplicationData +
+                        " activityInfo.name: " + activityInfo.name);
+            }
+            if (smsApplicationData != null) {
+                smsApplicationData.mSmsAppChangedReceiverClass = activityInfo.name;
+            }
+        }
+
+        // Update any existing entries with the external provider changed handler.
+        intent = new Intent(Telephony.Sms.Intents.ACTION_EXTERNAL_PROVIDER_CHANGE);
+        List<ResolveInfo> providerChangedReceivers =
+                packageManager.queryBroadcastReceiversAsUser(intent, 0, userId);
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplicationCollectionInternal providerChangedActivities=" +
+                    providerChangedReceivers);
+        }
+        for (ResolveInfo resolveInfo : providerChangedReceivers) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (DEBUG_MULTIUSER) {
+                Log.i(LOG_TAG, "getApplicationCollectionInternal packageName=" +
+                        packageName + " smsApplicationData: " + smsApplicationData +
+                        " activityInfo.name: " + activityInfo.name);
+            }
+            if (smsApplicationData != null) {
+                smsApplicationData.mProviderChangedReceiverClass = activityInfo.name;
+            }
+        }
+
+        // Update any existing entries with the sim full handler.
+        intent = new Intent(Intents.SIM_FULL_ACTION);
+        List<ResolveInfo> simFullReceivers =
+                packageManager.queryBroadcastReceiversAsUser(intent, 0, userId);
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplicationCollectionInternal simFullReceivers="
+                    + simFullReceivers);
+        }
+        for (ResolveInfo resolveInfo : simFullReceivers) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (DEBUG_MULTIUSER) {
+                Log.i(LOG_TAG, "getApplicationCollectionInternal packageName="
+                        + packageName + " smsApplicationData: " + smsApplicationData
+                        + " activityInfo.name: " + activityInfo.name);
+            }
+            if (smsApplicationData != null) {
+                smsApplicationData.mSimFullReceiverClass = activityInfo.name;
+            }
+        }
+
+        // Remove any entries for which we did not find all required intents.
+        for (ResolveInfo resolveInfo : smsReceivers) {
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            if (activityInfo == null) {
+                continue;
+            }
+            final String packageName = activityInfo.packageName;
+            final SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (smsApplicationData != null) {
+                if (!smsApplicationData.isComplete()) {
+                    receivers.remove(packageName);
+                }
+            }
+        }
+        return receivers.values();
+    }
+
+    /**
+     * Checks to see if we have a valid installed SMS application for the specified package name
+     * @return Data for the specified package name or null if there isn't one
+     */
+    private static SmsApplicationData getApplicationForPackage(
+            Collection<SmsApplicationData> applications, String packageName) {
+        if (packageName == null) {
+            return null;
+        }
+        // Is there an entry in the application list for the specified package?
+        for (SmsApplicationData application : applications) {
+            if (application.mPackageName.contentEquals(packageName)) {
+                return application;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the application we will use for delivering SMS/MMS messages.
+     *
+     * We return the preferred sms application with the following order of preference:
+     * (1) User selected SMS app (if selected, and if still valid)
+     * (2) Android Messaging (if installed)
+     * (3) The currently configured highest priority broadcast receiver
+     * (4) Null
+     */
+    private static SmsApplicationData getApplication(Context context, boolean updateIfNeeded,
+            int userId) {
+        TelephonyManager tm = (TelephonyManager)
+                context.getSystemService(Context.TELEPHONY_SERVICE);
+        if (!tm.isSmsCapable()) {
+            // No phone, no SMS
+            return null;
+        }
+
+        Collection<SmsApplicationData> applications = getApplicationCollectionInternal(context,
+                userId);
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplication userId=" + userId);
+        }
+        // Determine which application receives the broadcast
+        String defaultApplication = Settings.Secure.getStringForUser(context.getContentResolver(),
+                Settings.Secure.SMS_DEFAULT_APPLICATION, userId);
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplication defaultApp=" + defaultApplication);
+        }
+
+        SmsApplicationData applicationData = null;
+        if (defaultApplication != null) {
+            applicationData = getApplicationForPackage(applications, defaultApplication);
+        }
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplication appData=" + applicationData);
+        }
+        // Picking a new SMS app requires AppOps and Settings.Secure permissions, so we only do
+        // this if the caller asked us to.
+        if (updateIfNeeded && applicationData == null) {
+            // Try to find the default SMS package for this device
+            Resources r = context.getResources();
+            String defaultPackage =
+                    r.getString(com.android.internal.R.string.default_sms_application);
+            applicationData = getApplicationForPackage(applications, defaultPackage);
+
+            if (applicationData == null) {
+                // Are there any applications?
+                if (applications.size() != 0) {
+                    applicationData = (SmsApplicationData)applications.toArray()[0];
+                }
+            }
+
+            // If we found a new default app, update the setting
+            if (applicationData != null) {
+                setDefaultApplicationInternal(applicationData.mPackageName, context, userId);
+            }
+        }
+
+        // If we found a package, make sure AppOps permissions are set up correctly
+        if (applicationData != null) {
+            AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
+
+            // We can only call checkOp if we are privileged (updateIfNeeded) or if the app we
+            // are checking is for our current uid. Doing this check from the unprivileged current
+            // SMS app allows us to tell the current SMS app that it is not in a good state and
+            // needs to ask to be the current SMS app again to work properly.
+            if (updateIfNeeded || applicationData.mUid == android.os.Process.myUid()) {
+                // Verify that the SMS app has permissions
+                int mode = appOps.checkOp(AppOpsManager.OP_WRITE_SMS, applicationData.mUid,
+                        applicationData.mPackageName);
+                if (mode != AppOpsManager.MODE_ALLOWED) {
+                    Rlog.e(LOG_TAG, applicationData.mPackageName + " lost OP_WRITE_SMS: " +
+                            (updateIfNeeded ? " (fixing)" : " (no permission to fix)"));
+                    if (updateIfNeeded) {
+                        appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid,
+                                applicationData.mPackageName, AppOpsManager.MODE_ALLOWED);
+                    } else {
+                        // We can not return a package if permissions are not set up correctly
+                        applicationData = null;
+                    }
+                }
+            }
+
+            // We can only verify the phone and BT app's permissions from a privileged caller
+            if (updateIfNeeded) {
+                // Ensure this component is still configured as the preferred activity. Usually the
+                // current SMS app will already be the preferred activity - but checking whether or
+                // not this is true is just as expensive as reconfiguring the preferred activity so
+                // we just reconfigure every time.
+                PackageManager packageManager = context.getPackageManager();
+                configurePreferredActivity(packageManager, new ComponentName(
+                        applicationData.mPackageName, applicationData.mSendToClass),
+                        userId);
+                // Assign permission to special system apps
+                assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                        PHONE_PACKAGE_NAME);
+                assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                        BLUETOOTH_PACKAGE_NAME);
+                assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                        MMS_SERVICE_PACKAGE_NAME);
+                assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                        TELEPHONY_PROVIDER_PACKAGE_NAME);
+                // Give WRITE_SMS AppOps permission to UID 1001 which contains multiple
+                // apps, all of them should be able to write to telephony provider.
+                // This is to allow the proxy package permission check in telephony provider
+                // to pass.
+                assignWriteSmsPermissionToSystemUid(appOps, Process.PHONE_UID);
+            }
+        }
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "getApplication returning appData=" + applicationData);
+        }
+        return applicationData;
+    }
+
+    /**
+     * Sets the specified package as the default SMS/MMS application. The caller of this method
+     * needs to have permission to set AppOps and write to secure settings.
+     */
+    public static void setDefaultApplication(String packageName, Context context) {
+        TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
+        if (!tm.isSmsCapable()) {
+            // No phone, no SMS
+            return;
+        }
+
+        final int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            setDefaultApplicationInternal(packageName, context, userId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private static void setDefaultApplicationInternal(String packageName, Context context,
+            int userId) {
+        // Get old package name
+        String oldPackageName = Settings.Secure.getStringForUser(context.getContentResolver(),
+                Settings.Secure.SMS_DEFAULT_APPLICATION, userId);
+
+        if (DEBUG_MULTIUSER) {
+            Log.i(LOG_TAG, "setDefaultApplicationInternal old=" + oldPackageName +
+                    " new=" + packageName);
+        }
+
+        if (packageName != null && oldPackageName != null && packageName.equals(oldPackageName)) {
+            // No change
+            return;
+        }
+
+        // We only make the change if the new package is valid
+        PackageManager packageManager = context.getPackageManager();
+        Collection<SmsApplicationData> applications = getApplicationCollection(context);
+        SmsApplicationData oldAppData = oldPackageName != null ?
+                getApplicationForPackage(applications, oldPackageName) : null;
+        SmsApplicationData applicationData = getApplicationForPackage(applications, packageName);
+        if (applicationData != null) {
+            // Ignore OP_WRITE_SMS for the previously configured default SMS app.
+            AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
+            if (oldPackageName != null) {
+                try {
+                    PackageInfo info = packageManager.getPackageInfoAsUser(oldPackageName,
+                            0, userId);
+                    appOps.setMode(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid,
+                            oldPackageName, AppOpsManager.MODE_IGNORED);
+                } catch (NameNotFoundException e) {
+                    Rlog.w(LOG_TAG, "Old SMS package not found: " + oldPackageName);
+                }
+            }
+
+            // Update the secure setting.
+            Settings.Secure.putStringForUser(context.getContentResolver(),
+                    Settings.Secure.SMS_DEFAULT_APPLICATION, applicationData.mPackageName,
+                    userId);
+
+            // Configure this as the preferred activity for SENDTO sms/mms intents
+            configurePreferredActivity(packageManager, new ComponentName(
+                    applicationData.mPackageName, applicationData.mSendToClass), userId);
+
+            // Allow OP_WRITE_SMS for the newly configured default SMS app.
+            appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid,
+                    applicationData.mPackageName, AppOpsManager.MODE_ALLOWED);
+
+            // Assign permission to special system apps
+            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                    PHONE_PACKAGE_NAME);
+            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                    BLUETOOTH_PACKAGE_NAME);
+            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                    MMS_SERVICE_PACKAGE_NAME);
+            assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
+                    TELEPHONY_PROVIDER_PACKAGE_NAME);
+            // Give WRITE_SMS AppOps permission to UID 1001 which contains multiple
+            // apps, all of them should be able to write to telephony provider.
+            // This is to allow the proxy package permission check in telephony provider
+            // to pass.
+            assignWriteSmsPermissionToSystemUid(appOps, Process.PHONE_UID);
+
+            if (DEBUG_MULTIUSER) {
+                Log.i(LOG_TAG, "setDefaultApplicationInternal oldAppData=" + oldAppData);
+            }
+            if (oldAppData != null && oldAppData.mSmsAppChangedReceiverClass != null) {
+                // Notify the old sms app that it's no longer the default
+                final Intent oldAppIntent =
+                        new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED);
+                final ComponentName component = new ComponentName(oldAppData.mPackageName,
+                        oldAppData.mSmsAppChangedReceiverClass);
+                oldAppIntent.setComponent(component);
+                oldAppIntent.putExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, false);
+                if (DEBUG_MULTIUSER) {
+                    Log.i(LOG_TAG, "setDefaultApplicationInternal old=" + oldAppData.mPackageName);
+                }
+                context.sendBroadcast(oldAppIntent);
+            }
+            // Notify the new sms app that it's now the default (if the new sms app has a receiver
+            // to handle the changed default sms intent).
+            if (DEBUG_MULTIUSER) {
+                Log.i(LOG_TAG, "setDefaultApplicationInternal new applicationData=" +
+                        applicationData);
+            }
+            if (applicationData.mSmsAppChangedReceiverClass != null) {
+                final Intent intent =
+                        new Intent(Telephony.Sms.Intents.ACTION_DEFAULT_SMS_PACKAGE_CHANGED);
+                final ComponentName component = new ComponentName(applicationData.mPackageName,
+                        applicationData.mSmsAppChangedReceiverClass);
+                intent.setComponent(component);
+                intent.putExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, true);
+                if (DEBUG_MULTIUSER) {
+                    Log.i(LOG_TAG, "setDefaultApplicationInternal new=" + packageName);
+                }
+                context.sendBroadcast(intent);
+            }
+            MetricsLogger.action(context, MetricsEvent.ACTION_DEFAULT_SMS_APP_CHANGED,
+                    applicationData.mPackageName);
+        }
+    }
+
+    /**
+     * Assign WRITE_SMS AppOps permission to some special system apps.
+     *
+     * @param context The context
+     * @param packageManager The package manager instance
+     * @param appOps The AppOps manager instance
+     * @param packageName The package name of the system app
+     */
+    private static void assignWriteSmsPermissionToSystemApp(Context context,
+            PackageManager packageManager, AppOpsManager appOps, String packageName) {
+        // First check package signature matches the caller's package signature.
+        // Since this class is only used internally by the system, this check makes sure
+        // the package signature matches system signature.
+        final int result = packageManager.checkSignatures(context.getPackageName(), packageName);
+        if (result != PackageManager.SIGNATURE_MATCH) {
+            Rlog.e(LOG_TAG, packageName + " does not have system signature");
+            return;
+        }
+        try {
+            PackageInfo info = packageManager.getPackageInfo(packageName, 0);
+            int mode = appOps.checkOp(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid,
+                    packageName);
+            if (mode != AppOpsManager.MODE_ALLOWED) {
+                Rlog.w(LOG_TAG, packageName + " does not have OP_WRITE_SMS:  (fixing)");
+                appOps.setMode(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid,
+                        packageName, AppOpsManager.MODE_ALLOWED);
+            }
+        } catch (NameNotFoundException e) {
+            // No whitelisted system app on this device
+            Rlog.e(LOG_TAG, "Package not found: " + packageName);
+        }
+
+    }
+
+    private static void assignWriteSmsPermissionToSystemUid(AppOpsManager appOps, int uid) {
+        appOps.setUidMode(AppOpsManager.OP_WRITE_SMS, uid, AppOpsManager.MODE_ALLOWED);
+    }
+
+    /**
+     * Tracks package changes and ensures that the default SMS app is always configured to be the
+     * preferred activity for SENDTO sms/mms intents.
+     */
+    private static final class SmsPackageMonitor extends PackageMonitor {
+        final Context mContext;
+
+        public SmsPackageMonitor(Context context) {
+            super();
+            mContext = context;
+        }
+
+        @Override
+        public void onPackageDisappeared(String packageName, int reason) {
+            onPackageChanged();
+        }
+
+        @Override
+        public void onPackageAppeared(String packageName, int reason) {
+            onPackageChanged();
+        }
+
+        @Override
+        public void onPackageModified(String packageName) {
+            onPackageChanged();
+        }
+
+        private void onPackageChanged() {
+            PackageManager packageManager = mContext.getPackageManager();
+            Context userContext = mContext;
+            final int userId = getSendingUserId();
+            if (userId != UserHandle.USER_SYSTEM) {
+                try {
+                    userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0,
+                            new UserHandle(userId));
+                } catch (NameNotFoundException nnfe) {
+                    if (DEBUG_MULTIUSER) {
+                        Log.w(LOG_TAG, "Unable to create package context for user " + userId);
+                    }
+                }
+            }
+            // Ensure this component is still configured as the preferred activity
+            ComponentName componentName = getDefaultSendToApplication(userContext, true);
+            if (componentName != null) {
+                configurePreferredActivity(packageManager, componentName, userId);
+            }
+        }
+    }
+
+    public static void initSmsPackageMonitor(Context context) {
+        sSmsPackageMonitor = new SmsPackageMonitor(context);
+        sSmsPackageMonitor.register(context, context.getMainLooper(), UserHandle.ALL, false);
+    }
+
+    private static void configurePreferredActivity(PackageManager packageManager,
+            ComponentName componentName, int userId) {
+        // Add the four activity preferences we want to direct to this app.
+        replacePreferredActivity(packageManager, componentName, userId, SCHEME_SMS);
+        replacePreferredActivity(packageManager, componentName, userId, SCHEME_SMSTO);
+        replacePreferredActivity(packageManager, componentName, userId, SCHEME_MMS);
+        replacePreferredActivity(packageManager, componentName, userId, SCHEME_MMSTO);
+    }
+
+    /**
+     * Updates the ACTION_SENDTO activity to the specified component for the specified scheme.
+     */
+    private static void replacePreferredActivity(PackageManager packageManager,
+            ComponentName componentName, int userId, String scheme) {
+        // Build the set of existing activities that handle this scheme
+        Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(scheme, "", null));
+        List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivitiesAsUser(
+                intent, PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_RESOLVED_FILTER,
+                userId);
+
+        // Build the set of ComponentNames for these activities
+        final int n = resolveInfoList.size();
+        ComponentName[] set = new ComponentName[n];
+        for (int i = 0; i < n; i++) {
+            ResolveInfo info = resolveInfoList.get(i);
+            set[i] = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
+        }
+
+        // Update the preferred SENDTO activity for the specified scheme
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_SENDTO);
+        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+        intentFilter.addDataScheme(scheme);
+        packageManager.replacePreferredActivityAsUser(intentFilter,
+                IntentFilter.MATCH_CATEGORY_SCHEME + IntentFilter.MATCH_ADJUSTMENT_NORMAL,
+                set, componentName, userId);
+    }
+
+    /**
+     * Returns SmsApplicationData for this package if this package is capable of being set as the
+     * default SMS application.
+     */
+    public static SmsApplicationData getSmsApplicationData(String packageName, Context context) {
+        Collection<SmsApplicationData> applications = getApplicationCollection(context);
+        return getApplicationForPackage(applications, packageName);
+    }
+
+    /**
+     * Gets the default SMS application
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to deliver SMS messages to
+     */
+    public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ComponentName component = null;
+            SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
+                    userId);
+            if (smsApplicationData != null) {
+                component = new ComponentName(smsApplicationData.mPackageName,
+                        smsApplicationData.mSmsReceiverClass);
+            }
+            return component;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Gets the default MMS application
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to deliver MMS messages to
+     */
+    public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ComponentName component = null;
+            SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
+                    userId);
+            if (smsApplicationData != null) {
+                component = new ComponentName(smsApplicationData.mPackageName,
+                        smsApplicationData.mMmsReceiverClass);
+            }
+            return component;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Gets the default Respond Via Message application
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to direct Respond Via Message intent to
+     */
+    public static ComponentName getDefaultRespondViaMessageApplication(Context context,
+            boolean updateIfNeeded) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ComponentName component = null;
+            SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
+                    userId);
+            if (smsApplicationData != null) {
+                component = new ComponentName(smsApplicationData.mPackageName,
+                        smsApplicationData.mRespondViaMessageClass);
+            }
+            return component;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Gets the default Send To (smsto) application.
+     * <p>
+     * Caller must pass in the correct user context if calling from a singleton service.
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to direct SEND_TO (smsto) intent to
+     */
+    public static ComponentName getDefaultSendToApplication(Context context,
+            boolean updateIfNeeded) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ComponentName component = null;
+            SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
+                    userId);
+            if (smsApplicationData != null) {
+                component = new ComponentName(smsApplicationData.mPackageName,
+                        smsApplicationData.mSendToClass);
+            }
+            return component;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Gets the default application that handles external changes to the SmsProvider and
+     * MmsProvider.
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to deliver change intents to
+     */
+    public static ComponentName getDefaultExternalTelephonyProviderChangedApplication(
+            Context context, boolean updateIfNeeded) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ComponentName component = null;
+            SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
+                    userId);
+            if (smsApplicationData != null
+                    && smsApplicationData.mProviderChangedReceiverClass != null) {
+                component = new ComponentName(smsApplicationData.mPackageName,
+                        smsApplicationData.mProviderChangedReceiverClass);
+            }
+            return component;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Gets the default application that handles sim full event.
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to deliver change intents to
+     */
+    public static ComponentName getDefaultSimFullApplication(
+            Context context, boolean updateIfNeeded) {
+        int userId = getIncomingUserId(context);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ComponentName component = null;
+            SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
+                    userId);
+            if (smsApplicationData != null
+                    && smsApplicationData.mSimFullReceiverClass != null) {
+                component = new ComponentName(smsApplicationData.mPackageName,
+                        smsApplicationData.mSimFullReceiverClass);
+            }
+            return component;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Returns whether need to write the SMS message to SMS database for this package.
+     * <p>
+     * Caller must pass in the correct user context if calling from a singleton service.
+     */
+    public static boolean shouldWriteMessageForPackage(String packageName, Context context) {
+        if (SmsManager.getDefault().getAutoPersisting()) {
+            return true;
+        }
+        return !isDefaultSmsApplication(context, packageName);
+    }
+
+    /**
+     * Check if a package is default sms app (or equivalent, like bluetooth)
+     *
+     * @param context context from the calling app
+     * @param packageName the name of the package to be checked
+     * @return true if the package is default sms app or bluetooth
+     */
+    public static boolean isDefaultSmsApplication(Context context, String packageName) {
+        if (packageName == null) {
+            return false;
+        }
+        final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context);
+        if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName))
+                || BLUETOOTH_PACKAGE_NAME.equals(packageName)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static String getDefaultSmsApplicationPackageName(Context context) {
+        final ComponentName component = getDefaultSmsApplication(context, false);
+        if (component != null) {
+            return component.getPackageName();
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/SmsBroadcastUndelivered.java b/com/android/internal/telephony/SmsBroadcastUndelivered.java
new file mode 100644
index 0000000..f2f2aa3
--- /dev/null
+++ b/com/android/internal/telephony/SmsBroadcastUndelivered.java
@@ -0,0 +1,281 @@
+/*
+ * 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
+import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Called when the credential-encrypted storage is unlocked, collecting all acknowledged messages
+ * and deleting any partial message segments older than 30 days. Called from a worker thread to
+ * avoid delaying phone app startup. The last step is to broadcast the first pending message from
+ * the main thread, then the remaining pending messages will be broadcast after the previous
+ * ordered broadcast completes.
+ */
+public class SmsBroadcastUndelivered {
+    private static final String TAG = "SmsBroadcastUndelivered";
+    private static final boolean DBG = InboundSmsHandler.DBG;
+
+    /** Delete any partial message segments older than 30 days. */
+    static final long PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 30;
+
+    /**
+     * Query projection for dispatching pending messages at boot time.
+     * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}.
+     */
+    private static final String[] PDU_PENDING_MESSAGE_PROJECTION = {
+            "pdu",
+            "sequence",
+            "destination_port",
+            "date",
+            "reference_number",
+            "count",
+            "address",
+            "_id",
+            "message_body",
+            "display_originating_addr"
+    };
+
+    private static SmsBroadcastUndelivered instance;
+
+    /** Content resolver to use to access raw table from SmsProvider. */
+    private final ContentResolver mResolver;
+
+    /** Handler for 3GPP-format messages (may be null). */
+    private final GsmInboundSmsHandler mGsmInboundSmsHandler;
+
+    /** Handler for 3GPP2-format messages (may be null). */
+    private final CdmaInboundSmsHandler mCdmaInboundSmsHandler;
+
+    /** Broadcast receiver that processes the raw table when the user unlocks the phone for the
+     *  first time after reboot and the credential-encrypted storage is available.
+     */
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(final Context context, Intent intent) {
+            Rlog.d(TAG, "Received broadcast " + intent.getAction());
+            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+                new ScanRawTableThread(context).start();
+            }
+        }
+    };
+
+    private class ScanRawTableThread extends Thread {
+        private final Context context;
+
+        private ScanRawTableThread(Context context) {
+            this.context = context;
+        }
+
+        @Override
+        public void run() {
+            scanRawTable();
+            InboundSmsHandler.cancelNewMessageNotification(context);
+        }
+    }
+
+    public static void initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
+        CdmaInboundSmsHandler cdmaInboundSmsHandler) {
+        if (instance == null) {
+            instance = new SmsBroadcastUndelivered(
+                context, gsmInboundSmsHandler, cdmaInboundSmsHandler);
+        }
+
+        // Tell handlers to start processing new messages and transit from the startup state to the
+        // idle state. This method may be called multiple times for multi-sim devices. We must make
+        // sure the state transition happen to all inbound sms handlers.
+        if (gsmInboundSmsHandler != null) {
+            gsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
+        }
+        if (cdmaInboundSmsHandler != null) {
+            cdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
+        }
+    }
+
+    private SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
+            CdmaInboundSmsHandler cdmaInboundSmsHandler) {
+        mResolver = context.getContentResolver();
+        mGsmInboundSmsHandler = gsmInboundSmsHandler;
+        mCdmaInboundSmsHandler = cdmaInboundSmsHandler;
+
+        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+        if (userManager.isUserUnlocked()) {
+            new ScanRawTableThread(context).start();
+        } else {
+            IntentFilter userFilter = new IntentFilter();
+            userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
+            context.registerReceiver(mBroadcastReceiver, userFilter);
+        }
+    }
+
+    /**
+     * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete.
+     */
+    private void scanRawTable() {
+        if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages");
+        long startTime = System.nanoTime();
+        HashMap<SmsReferenceKey, Integer> multiPartReceivedCount =
+                new HashMap<SmsReferenceKey, Integer>(4);
+        HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4);
+        Cursor cursor = null;
+        try {
+            // query only non-deleted ones
+            cursor = mResolver.query(InboundSmsHandler.sRawUri, PDU_PENDING_MESSAGE_PROJECTION,
+                    "deleted = 0", null,
+                    null);
+            if (cursor == null) {
+                Rlog.e(TAG, "error getting pending message cursor");
+                return;
+            }
+
+            boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2();
+            while (cursor.moveToNext()) {
+                InboundSmsTracker tracker;
+                try {
+                    tracker = TelephonyComponentFactory.getInstance().makeInboundSmsTracker(cursor,
+                            isCurrentFormat3gpp2);
+                } catch (IllegalArgumentException e) {
+                    Rlog.e(TAG, "error loading SmsTracker: " + e);
+                    continue;
+                }
+
+                if (tracker.getMessageCount() == 1) {
+                    // deliver single-part message
+                    broadcastSms(tracker);
+                } else {
+                    SmsReferenceKey reference = new SmsReferenceKey(tracker);
+                    Integer receivedCount = multiPartReceivedCount.get(reference);
+                    if (receivedCount == null) {
+                        multiPartReceivedCount.put(reference, 1);    // first segment seen
+                        if (tracker.getTimestamp() <
+                                (System.currentTimeMillis() - PARTIAL_SEGMENT_EXPIRE_AGE)) {
+                            // older than 30 days; delete if we don't find all the segments
+                            oldMultiPartMessages.add(reference);
+                        }
+                    } else {
+                        int newCount = receivedCount + 1;
+                        if (newCount == tracker.getMessageCount()) {
+                            // looks like we've got all the pieces; send a single tracker
+                            // to state machine which will find the other pieces to broadcast
+                            if (DBG) Rlog.d(TAG, "found complete multi-part message");
+                            broadcastSms(tracker);
+                            // don't delete this old message until after we broadcast it
+                            oldMultiPartMessages.remove(reference);
+                        } else {
+                            multiPartReceivedCount.put(reference, newCount);
+                        }
+                    }
+                }
+            }
+            // Delete old incomplete message segments
+            for (SmsReferenceKey message : oldMultiPartMessages) {
+                // delete permanently
+                int rows = mResolver.delete(InboundSmsHandler.sRawUriPermanentDelete,
+                        message.getDeleteWhere(), message.getDeleteWhereArgs());
+                if (rows == 0) {
+                    Rlog.e(TAG, "No rows were deleted from raw table!");
+                } else if (DBG) {
+                    Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete "
+                            + message.mMessageCount + " part message");
+                }
+            }
+        } catch (SQLException e) {
+            Rlog.e(TAG, "error reading pending SMS messages", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+            if (DBG) Rlog.d(TAG, "finished scanning raw table in "
+                    + ((System.nanoTime() - startTime) / 1000000) + " ms");
+        }
+    }
+
+    /**
+     * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast.
+     */
+    private void broadcastSms(InboundSmsTracker tracker) {
+        InboundSmsHandler handler;
+        if (tracker.is3gpp2()) {
+            handler = mCdmaInboundSmsHandler;
+        } else {
+            handler = mGsmInboundSmsHandler;
+        }
+        if (handler != null) {
+            handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker);
+        } else {
+            Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver.");
+        }
+    }
+
+    /**
+     * Used as the HashMap key for matching concatenated message segments.
+     */
+    private static class SmsReferenceKey {
+        final String mAddress;
+        final int mReferenceNumber;
+        final int mMessageCount;
+        final String mQuery;
+
+        SmsReferenceKey(InboundSmsTracker tracker) {
+            mAddress = tracker.getAddress();
+            mReferenceNumber = tracker.getReferenceNumber();
+            mMessageCount = tracker.getMessageCount();
+            mQuery = tracker.getQueryForSegments();
+
+        }
+
+        String[] getDeleteWhereArgs() {
+            return new String[]{mAddress, Integer.toString(mReferenceNumber),
+                    Integer.toString(mMessageCount)};
+        }
+
+        String getDeleteWhere() {
+            return mQuery;
+        }
+
+        @Override
+        public int hashCode() {
+            return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof SmsReferenceKey) {
+                SmsReferenceKey other = (SmsReferenceKey) o;
+                return other.mAddress.equals(mAddress)
+                        && (other.mReferenceNumber == mReferenceNumber)
+                        && (other.mMessageCount == mMessageCount);
+            }
+            return false;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/SmsConstants.java b/com/android/internal/telephony/SmsConstants.java
new file mode 100644
index 0000000..2449108
--- /dev/null
+++ b/com/android/internal/telephony/SmsConstants.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony;
+
+/**
+ * SMS Constants and must be the same as the corresponding
+ * deprecated version in SmsMessage.
+ *
+ * @hide
+ */
+public class SmsConstants {
+    /** User data text encoding code unit size */
+    public static final int ENCODING_UNKNOWN = 0;
+    public static final int ENCODING_7BIT = 1;
+    public static final int ENCODING_8BIT = 2;
+    public static final int ENCODING_16BIT = 3;
+
+    /** The maximum number of payload septets per message */
+    public static final int MAX_USER_DATA_SEPTETS = 160;
+
+    /**
+     * The maximum number of payload septets per message if a user data header
+     * is present.  This assumes the header only contains the
+     * CONCATENATED_8_BIT_REFERENCE element.
+     */
+    public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153;
+
+    /**
+     * This value is not defined in global standard. Only in Korea, this is used.
+     */
+    public static final int ENCODING_KSC5601 = 4;
+
+    /** The maximum number of payload bytes per message */
+    public static final int MAX_USER_DATA_BYTES = 140;
+
+    /**
+     * The maximum number of payload bytes per message if a user data header
+     * is present.  This assumes the header only contains the
+     * CONCATENATED_8_BIT_REFERENCE element.
+     */
+    public static final int MAX_USER_DATA_BYTES_WITH_HEADER = 134;
+
+    /**
+     * SMS Class enumeration.
+     * See TS 23.038.
+     */
+    public enum MessageClass{
+        UNKNOWN, CLASS_0, CLASS_1, CLASS_2, CLASS_3;
+    }
+
+    /**
+     * Indicates unknown format SMS message.
+     * @hide pending API council approval
+     */
+    public static final String FORMAT_UNKNOWN = "unknown";
+
+    /**
+     * Indicates a 3GPP format SMS message.
+     * @hide pending API council approval
+     */
+    public static final String FORMAT_3GPP = "3gpp";
+
+    /**
+     * Indicates a 3GPP2 format SMS message.
+     * @hide pending API council approval
+     */
+    public static final String FORMAT_3GPP2 = "3gpp2";
+}
diff --git a/com/android/internal/telephony/SmsHeader.java b/com/android/internal/telephony/SmsHeader.java
new file mode 100644
index 0000000..b519b70
--- /dev/null
+++ b/com/android/internal/telephony/SmsHeader.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.util.HexDump;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+
+import java.util.ArrayList;
+
+/**
+ * SMS user data header, as specified in TS 23.040 9.2.3.24.
+ */
+public class SmsHeader {
+
+    // TODO(cleanup): this data structure is generally referred to as
+    // the 'user data header' or UDH, and so the class name should
+    // change to reflect this...
+
+    /** SMS user data header information element identifiers.
+     * (see TS 23.040 9.2.3.24)
+     */
+    public static final int ELT_ID_CONCATENATED_8_BIT_REFERENCE       = 0x00;
+    public static final int ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION     = 0x01;
+    public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT  = 0x04;
+    public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT = 0x05;
+    public static final int ELT_ID_SMSC_CONTROL_PARAMS                = 0x06;
+    public static final int ELT_ID_UDH_SOURCE_INDICATION              = 0x07;
+    public static final int ELT_ID_CONCATENATED_16_BIT_REFERENCE      = 0x08;
+    public static final int ELT_ID_WIRELESS_CTRL_MSG_PROTOCOL         = 0x09;
+    public static final int ELT_ID_TEXT_FORMATTING                    = 0x0A;
+    public static final int ELT_ID_PREDEFINED_SOUND                   = 0x0B;
+    public static final int ELT_ID_USER_DEFINED_SOUND                 = 0x0C;
+    public static final int ELT_ID_PREDEFINED_ANIMATION               = 0x0D;
+    public static final int ELT_ID_LARGE_ANIMATION                    = 0x0E;
+    public static final int ELT_ID_SMALL_ANIMATION                    = 0x0F;
+    public static final int ELT_ID_LARGE_PICTURE                      = 0x10;
+    public static final int ELT_ID_SMALL_PICTURE                      = 0x11;
+    public static final int ELT_ID_VARIABLE_PICTURE                   = 0x12;
+    public static final int ELT_ID_USER_PROMPT_INDICATOR              = 0x13;
+    public static final int ELT_ID_EXTENDED_OBJECT                    = 0x14;
+    public static final int ELT_ID_REUSED_EXTENDED_OBJECT             = 0x15;
+    public static final int ELT_ID_COMPRESSION_CONTROL                = 0x16;
+    public static final int ELT_ID_OBJECT_DISTR_INDICATOR             = 0x17;
+    public static final int ELT_ID_STANDARD_WVG_OBJECT                = 0x18;
+    public static final int ELT_ID_CHARACTER_SIZE_WVG_OBJECT          = 0x19;
+    public static final int ELT_ID_EXTENDED_OBJECT_DATA_REQUEST_CMD   = 0x1A;
+    public static final int ELT_ID_RFC_822_EMAIL_HEADER               = 0x20;
+    public static final int ELT_ID_HYPERLINK_FORMAT_ELEMENT           = 0x21;
+    public static final int ELT_ID_REPLY_ADDRESS_ELEMENT              = 0x22;
+    public static final int ELT_ID_ENHANCED_VOICE_MAIL_INFORMATION    = 0x23;
+    public static final int ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT     = 0x24;
+    public static final int ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT    = 0x25;
+
+    public static final int PORT_WAP_PUSH = 2948;
+    public static final int PORT_WAP_WSP  = 9200;
+
+    public static class PortAddrs {
+        public int destPort;
+        public int origPort;
+        public boolean areEightBits;
+    }
+
+    public static class ConcatRef {
+        public int refNumber;
+        public int seqNumber;
+        public int msgCount;
+        public boolean isEightBits;
+    }
+
+    public static class SpecialSmsMsg {
+        public int msgIndType;
+        public int msgCount;
+    }
+
+    /**
+     * A header element that is not explicitly parsed, meaning not
+     * PortAddrs or ConcatRef or SpecialSmsMsg.
+     */
+    public static class MiscElt {
+        public int id;
+        public byte[] data;
+    }
+
+    public PortAddrs portAddrs;
+    public ConcatRef concatRef;
+    public ArrayList<SpecialSmsMsg> specialSmsMsgList = new ArrayList<SpecialSmsMsg>();
+    public ArrayList<MiscElt> miscEltList = new ArrayList<MiscElt>();
+
+    /** 7 bit national language locking shift table, or 0 for GSM default 7 bit alphabet. */
+    public int languageTable;
+
+    /** 7 bit national language single shift table, or 0 for GSM default 7 bit extension table. */
+    public int languageShiftTable;
+
+    public SmsHeader() {}
+
+    /**
+     * Create structured SmsHeader object from serialized byte array representation.
+     * (see TS 23.040 9.2.3.24)
+     * @param data is user data header bytes
+     * @return SmsHeader object
+     */
+    public static SmsHeader fromByteArray(byte[] data) {
+        ByteArrayInputStream inStream = new ByteArrayInputStream(data);
+        SmsHeader smsHeader = new SmsHeader();
+        while (inStream.available() > 0) {
+            /**
+             * NOTE: as defined in the spec, ConcatRef and PortAddr
+             * fields should not reoccur, but if they do the last
+             * occurrence is to be used.  Also, for ConcatRef
+             * elements, if the count is zero, sequence is zero, or
+             * sequence is larger than count, the entire element is to
+             * be ignored.
+             */
+            int id = inStream.read();
+            int length = inStream.read();
+            ConcatRef concatRef;
+            PortAddrs portAddrs;
+            switch (id) {
+            case ELT_ID_CONCATENATED_8_BIT_REFERENCE:
+                concatRef = new ConcatRef();
+                concatRef.refNumber = inStream.read();
+                concatRef.msgCount = inStream.read();
+                concatRef.seqNumber = inStream.read();
+                concatRef.isEightBits = true;
+                if (concatRef.msgCount != 0 && concatRef.seqNumber != 0 &&
+                        concatRef.seqNumber <= concatRef.msgCount) {
+                    smsHeader.concatRef = concatRef;
+                }
+                break;
+            case ELT_ID_CONCATENATED_16_BIT_REFERENCE:
+                concatRef = new ConcatRef();
+                concatRef.refNumber = (inStream.read() << 8) | inStream.read();
+                concatRef.msgCount = inStream.read();
+                concatRef.seqNumber = inStream.read();
+                concatRef.isEightBits = false;
+                if (concatRef.msgCount != 0 && concatRef.seqNumber != 0 &&
+                        concatRef.seqNumber <= concatRef.msgCount) {
+                    smsHeader.concatRef = concatRef;
+                }
+                break;
+            case ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT:
+                portAddrs = new PortAddrs();
+                portAddrs.destPort = inStream.read();
+                portAddrs.origPort = inStream.read();
+                portAddrs.areEightBits = true;
+                smsHeader.portAddrs = portAddrs;
+                break;
+            case ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT:
+                portAddrs = new PortAddrs();
+                portAddrs.destPort = (inStream.read() << 8) | inStream.read();
+                portAddrs.origPort = (inStream.read() << 8) | inStream.read();
+                portAddrs.areEightBits = false;
+                smsHeader.portAddrs = portAddrs;
+                break;
+            case ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT:
+                smsHeader.languageShiftTable = inStream.read();
+                break;
+            case ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT:
+                smsHeader.languageTable = inStream.read();
+                break;
+            case ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION:
+                SpecialSmsMsg specialSmsMsg = new SpecialSmsMsg();
+                specialSmsMsg.msgIndType = inStream.read();
+                specialSmsMsg.msgCount = inStream.read();
+                smsHeader.specialSmsMsgList.add(specialSmsMsg);
+                break;
+            default:
+                MiscElt miscElt = new MiscElt();
+                miscElt.id = id;
+                miscElt.data = new byte[length];
+                inStream.read(miscElt.data, 0, length);
+                smsHeader.miscEltList.add(miscElt);
+            }
+        }
+        return smsHeader;
+    }
+
+    /**
+     * Create serialized byte array representation from structured SmsHeader object.
+     * (see TS 23.040 9.2.3.24)
+     * @return Byte array representing the SmsHeader
+     */
+    public static byte[] toByteArray(SmsHeader smsHeader) {
+        if ((smsHeader.portAddrs == null) &&
+            (smsHeader.concatRef == null) &&
+            (smsHeader.specialSmsMsgList.isEmpty()) &&
+            (smsHeader.miscEltList.isEmpty()) &&
+            (smsHeader.languageShiftTable == 0) &&
+            (smsHeader.languageTable == 0)) {
+            return null;
+        }
+
+        ByteArrayOutputStream outStream =
+                new ByteArrayOutputStream(SmsConstants.MAX_USER_DATA_BYTES);
+        ConcatRef concatRef = smsHeader.concatRef;
+        if (concatRef != null) {
+            if (concatRef.isEightBits) {
+                outStream.write(ELT_ID_CONCATENATED_8_BIT_REFERENCE);
+                outStream.write(3);
+                outStream.write(concatRef.refNumber);
+            } else {
+                outStream.write(ELT_ID_CONCATENATED_16_BIT_REFERENCE);
+                outStream.write(4);
+                outStream.write(concatRef.refNumber >>> 8);
+                outStream.write(concatRef.refNumber & 0x00FF);
+            }
+            outStream.write(concatRef.msgCount);
+            outStream.write(concatRef.seqNumber);
+        }
+        PortAddrs portAddrs = smsHeader.portAddrs;
+        if (portAddrs != null) {
+            if (portAddrs.areEightBits) {
+                outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT);
+                outStream.write(2);
+                outStream.write(portAddrs.destPort);
+                outStream.write(portAddrs.origPort);
+            } else {
+                outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT);
+                outStream.write(4);
+                outStream.write(portAddrs.destPort >>> 8);
+                outStream.write(portAddrs.destPort & 0x00FF);
+                outStream.write(portAddrs.origPort >>> 8);
+                outStream.write(portAddrs.origPort & 0x00FF);
+            }
+        }
+        if (smsHeader.languageShiftTable != 0) {
+            outStream.write(ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT);
+            outStream.write(1);
+            outStream.write(smsHeader.languageShiftTable);
+        }
+        if (smsHeader.languageTable != 0) {
+            outStream.write(ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT);
+            outStream.write(1);
+            outStream.write(smsHeader.languageTable);
+        }
+        for (SpecialSmsMsg specialSmsMsg : smsHeader.specialSmsMsgList) {
+            outStream.write(ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION);
+            outStream.write(2);
+            outStream.write(specialSmsMsg.msgIndType & 0xFF);
+            outStream.write(specialSmsMsg.msgCount & 0xFF);
+        }
+        for (MiscElt miscElt : smsHeader.miscEltList) {
+            outStream.write(miscElt.id);
+            outStream.write(miscElt.data.length);
+            outStream.write(miscElt.data, 0, miscElt.data.length);
+        }
+        return outStream.toByteArray();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("UserDataHeader ");
+        builder.append("{ ConcatRef ");
+        if (concatRef == null) {
+            builder.append("unset");
+        } else {
+            builder.append("{ refNumber=" + concatRef.refNumber);
+            builder.append(", msgCount=" + concatRef.msgCount);
+            builder.append(", seqNumber=" + concatRef.seqNumber);
+            builder.append(", isEightBits=" + concatRef.isEightBits);
+            builder.append(" }");
+        }
+        builder.append(", PortAddrs ");
+        if (portAddrs == null) {
+            builder.append("unset");
+        } else {
+            builder.append("{ destPort=" + portAddrs.destPort);
+            builder.append(", origPort=" + portAddrs.origPort);
+            builder.append(", areEightBits=" + portAddrs.areEightBits);
+            builder.append(" }");
+        }
+        if (languageShiftTable != 0) {
+            builder.append(", languageShiftTable=" + languageShiftTable);
+        }
+        if (languageTable != 0) {
+            builder.append(", languageTable=" + languageTable);
+        }
+        for (SpecialSmsMsg specialSmsMsg : specialSmsMsgList) {
+            builder.append(", SpecialSmsMsg ");
+            builder.append("{ msgIndType=" + specialSmsMsg.msgIndType);
+            builder.append(", msgCount=" + specialSmsMsg.msgCount);
+            builder.append(" }");
+        }
+        for (MiscElt miscElt : miscEltList) {
+            builder.append(", MiscElt ");
+            builder.append("{ id=" + miscElt.id);
+            builder.append(", length=" + miscElt.data.length);
+            builder.append(", data=" + HexDump.toHexString(miscElt.data));
+            builder.append(" }");
+        }
+        builder.append(" }");
+        return builder.toString();
+    }
+
+}
diff --git a/com/android/internal/telephony/SmsMessageBase.java b/com/android/internal/telephony/SmsMessageBase.java
new file mode 100644
index 0000000..e5821dc
--- /dev/null
+++ b/com/android/internal/telephony/SmsMessageBase.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony;
+
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import java.text.BreakIterator;
+import java.util.Arrays;
+
+import android.provider.Telephony;
+import android.telephony.SmsMessage;
+import android.text.Emoji;
+
+/**
+ * Base class declaring the specific methods and members for SmsMessage.
+ * {@hide}
+ */
+public abstract class SmsMessageBase {
+    /** {@hide} The address of the SMSC. May be null */
+    protected String mScAddress;
+
+    /** {@hide} The address of the sender */
+    protected SmsAddress mOriginatingAddress;
+
+    /** {@hide} The message body as a string. May be null if the message isn't text */
+    protected String mMessageBody;
+
+    /** {@hide} */
+    protected String mPseudoSubject;
+
+    /** {@hide} Non-null if this is an email gateway message */
+    protected String mEmailFrom;
+
+    /** {@hide} Non-null if this is an email gateway message */
+    protected String mEmailBody;
+
+    /** {@hide} */
+    protected boolean mIsEmail;
+
+    /** {@hide} Time when SC (service centre) received the message */
+    protected long mScTimeMillis;
+
+    /** {@hide} The raw PDU of the message */
+    protected byte[] mPdu;
+
+    /** {@hide} The raw bytes for the user data section of the message */
+    protected byte[] mUserData;
+
+    /** {@hide} */
+    protected SmsHeader mUserDataHeader;
+
+    // "Message Waiting Indication Group"
+    // 23.038 Section 4
+    /** {@hide} */
+    protected boolean mIsMwi;
+
+    /** {@hide} */
+    protected boolean mMwiSense;
+
+    /** {@hide} */
+    protected boolean mMwiDontStore;
+
+    /**
+     * Indicates status for messages stored on the ICC.
+     */
+    protected int mStatusOnIcc = -1;
+
+    /**
+     * Record index of message in the EF.
+     */
+    protected int mIndexOnIcc = -1;
+
+    /** TP-Message-Reference - Message Reference of sent message. @hide */
+    public int mMessageRef;
+
+    // TODO(): This class is duplicated in SmsMessage.java. Refactor accordingly.
+    public static abstract class SubmitPduBase  {
+        public byte[] encodedScAddress; // Null if not applicable.
+        public byte[] encodedMessage;
+
+        @Override
+        public String toString() {
+            return "SubmitPdu: encodedScAddress = "
+                    + Arrays.toString(encodedScAddress)
+                    + ", encodedMessage = "
+                    + Arrays.toString(encodedMessage);
+        }
+    }
+
+    /**
+     * Returns the address of the SMS service center that relayed this message
+     * or null if there is none.
+     */
+    public String getServiceCenterAddress() {
+        return mScAddress;
+    }
+
+    /**
+     * Returns the originating address (sender) of this SMS message in String
+     * form or null if unavailable
+     */
+    public String getOriginatingAddress() {
+        if (mOriginatingAddress == null) {
+            return null;
+        }
+
+        return mOriginatingAddress.getAddressString();
+    }
+
+    /**
+     * Returns the originating address, or email from address if this message
+     * was from an email gateway. Returns null if originating address
+     * unavailable.
+     */
+    public String getDisplayOriginatingAddress() {
+        if (mIsEmail) {
+            return mEmailFrom;
+        } else {
+            return getOriginatingAddress();
+        }
+    }
+
+    /**
+     * Returns the message body as a String, if it exists and is text based.
+     * @return message body is there is one, otherwise null
+     */
+    public String getMessageBody() {
+        return mMessageBody;
+    }
+
+    /**
+     * Returns the class of this message.
+     */
+    public abstract SmsConstants.MessageClass getMessageClass();
+
+    /**
+     * Returns the message body, or email message body if this message was from
+     * an email gateway. Returns null if message body unavailable.
+     */
+    public String getDisplayMessageBody() {
+        if (mIsEmail) {
+            return mEmailBody;
+        } else {
+            return getMessageBody();
+        }
+    }
+
+    /**
+     * Unofficial convention of a subject line enclosed in parens empty string
+     * if not present
+     */
+    public String getPseudoSubject() {
+        return mPseudoSubject == null ? "" : mPseudoSubject;
+    }
+
+    /**
+     * Returns the service centre timestamp in currentTimeMillis() format
+     */
+    public long getTimestampMillis() {
+        return mScTimeMillis;
+    }
+
+    /**
+     * Returns true if message is an email.
+     *
+     * @return true if this message came through an email gateway and email
+     *         sender / subject / parsed body are available
+     */
+    public boolean isEmail() {
+        return mIsEmail;
+    }
+
+    /**
+     * @return if isEmail() is true, body of the email sent through the gateway.
+     *         null otherwise
+     */
+    public String getEmailBody() {
+        return mEmailBody;
+    }
+
+    /**
+     * @return if isEmail() is true, email from address of email sent through
+     *         the gateway. null otherwise
+     */
+    public String getEmailFrom() {
+        return mEmailFrom;
+    }
+
+    /**
+     * Get protocol identifier.
+     */
+    public abstract int getProtocolIdentifier();
+
+    /**
+     * See TS 23.040 9.2.3.9 returns true if this is a "replace short message"
+     * SMS
+     */
+    public abstract boolean isReplace();
+
+    /**
+     * Returns true for CPHS MWI toggle message.
+     *
+     * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section
+     *         B.4.2
+     */
+    public abstract boolean isCphsMwiMessage();
+
+    /**
+     * returns true if this message is a CPHS voicemail / message waiting
+     * indicator (MWI) clear message
+     */
+    public abstract boolean isMWIClearMessage();
+
+    /**
+     * returns true if this message is a CPHS voicemail / message waiting
+     * indicator (MWI) set message
+     */
+    public abstract boolean isMWISetMessage();
+
+    /**
+     * returns true if this message is a "Message Waiting Indication Group:
+     * Discard Message" notification and should not be stored.
+     */
+    public abstract boolean isMwiDontStore();
+
+    /**
+     * returns the user data section minus the user data header if one was
+     * present.
+     */
+    public byte[] getUserData() {
+        return mUserData;
+    }
+
+    /**
+     * Returns an object representing the user data header
+     *
+     * {@hide}
+     */
+    public SmsHeader getUserDataHeader() {
+        return mUserDataHeader;
+    }
+
+    /**
+     * TODO(cleanup): The term PDU is used in a seemingly non-unique
+     * manner -- for example, what is the difference between this byte
+     * array and the contents of SubmitPdu objects.  Maybe a more
+     * illustrative term would be appropriate.
+     */
+
+    /**
+     * Returns the raw PDU for the message.
+     */
+    public byte[] getPdu() {
+        return mPdu;
+    }
+
+    /**
+     * For an SMS-STATUS-REPORT message, this returns the status field from
+     * the status report.  This field indicates the status of a previously
+     * submitted SMS, if requested.  See TS 23.040, 9.2.3.15 TP-Status for a
+     * description of values.
+     *
+     * @return 0 indicates the previously sent message was received.
+     *         See TS 23.040, 9.9.2.3.15 for a description of other possible
+     *         values.
+     */
+    public abstract int getStatus();
+
+    /**
+     * Return true iff the message is a SMS-STATUS-REPORT message.
+     */
+    public abstract boolean isStatusReportMessage();
+
+    /**
+     * Returns true iff the <code>TP-Reply-Path</code> bit is set in
+     * this message.
+     */
+    public abstract boolean isReplyPathPresent();
+
+    /**
+     * Returns the status of the message on the ICC (read, unread, sent, unsent).
+     *
+     * @return the status of the message on the ICC.  These are:
+     *         SmsManager.STATUS_ON_ICC_FREE
+     *         SmsManager.STATUS_ON_ICC_READ
+     *         SmsManager.STATUS_ON_ICC_UNREAD
+     *         SmsManager.STATUS_ON_ICC_SEND
+     *         SmsManager.STATUS_ON_ICC_UNSENT
+     */
+    public int getStatusOnIcc() {
+        return mStatusOnIcc;
+    }
+
+    /**
+     * Returns the record index of the message on the ICC (1-based index).
+     * @return the record index of the message on the ICC, or -1 if this
+     *         SmsMessage was not created from a ICC SMS EF record.
+     */
+    public int getIndexOnIcc() {
+        return mIndexOnIcc;
+    }
+
+    protected void parseMessageBody() {
+        // originatingAddress could be null if this message is from a status
+        // report.
+        if (mOriginatingAddress != null && mOriginatingAddress.couldBeEmailGateway()) {
+            extractEmailAddressFromMessageBody();
+        }
+    }
+
+    /**
+     * Try to parse this message as an email gateway message
+     * There are two ways specified in TS 23.040 Section 3.8 :
+     *  - SMS message "may have its TP-PID set for Internet electronic mail - MT
+     * SMS format: [<from-address><space>]<message> - "Depending on the
+     * nature of the gateway, the destination/origination address is either
+     * derived from the content of the SMS TP-OA or TP-DA field, or the
+     * TP-OA/TP-DA field contains a generic gateway address and the to/from
+     * address is added at the beginning as shown above." (which is supported here)
+     * - Multiple addresses separated by commas, no spaces, Subject field delimited
+     * by '()' or '##' and '#' Section 9.2.3.24.11 (which are NOT supported here)
+     */
+    protected void extractEmailAddressFromMessageBody() {
+
+        /* Some carriers may use " /" delimiter as below
+         *
+         * 1. [x@y][ ]/[subject][ ]/[body]
+         * -or-
+         * 2. [x@y][ ]/[body]
+         */
+         String[] parts = mMessageBody.split("( /)|( )", 2);
+         if (parts.length < 2) return;
+         mEmailFrom = parts[0];
+         mEmailBody = parts[1];
+         mIsEmail = Telephony.Mms.isEmailAddress(mEmailFrom);
+    }
+
+    /**
+     * Find the next position to start a new fragment of a multipart SMS.
+     *
+     * @param currentPosition current start position of the fragment
+     * @param byteLimit maximum number of bytes in the fragment
+     * @param msgBody text of the SMS in UTF-16 encoding
+     * @return the position to start the next fragment
+     */
+    public static int findNextUnicodePosition(
+            int currentPosition, int byteLimit, CharSequence msgBody) {
+        int nextPos = Math.min(currentPosition + byteLimit / 2, msgBody.length());
+        // Check whether the fragment ends in a character boundary. Some characters take 4-bytes
+        // in UTF-16 encoding. Many carriers cannot handle
+        // a fragment correctly if it does not end at a character boundary.
+        if (nextPos < msgBody.length()) {
+            BreakIterator breakIterator = BreakIterator.getCharacterInstance();
+            breakIterator.setText(msgBody.toString());
+            if (!breakIterator.isBoundary(nextPos)) {
+                int breakPos = breakIterator.preceding(nextPos);
+                while (breakPos + 4 <= nextPos
+                        && Emoji.isRegionalIndicatorSymbol(
+                            Character.codePointAt(msgBody, breakPos))
+                        && Emoji.isRegionalIndicatorSymbol(
+                            Character.codePointAt(msgBody, breakPos + 2))) {
+                    // skip forward over flags (pairs of Regional Indicator Symbol)
+                    breakPos += 4;
+                }
+                if (breakPos > currentPosition) {
+                    nextPos = breakPos;
+                } else if (Character.isHighSurrogate(msgBody.charAt(nextPos - 1))) {
+                    // no character boundary in this fragment, try to at least land on a code point
+                    nextPos -= 1;
+                }
+            }
+        }
+        return nextPos;
+    }
+
+    /**
+     * Calculate the TextEncodingDetails of a message encoded in Unicode.
+     */
+    public static TextEncodingDetails calcUnicodeEncodingDetails(CharSequence msgBody) {
+        TextEncodingDetails ted = new TextEncodingDetails();
+        int octets = msgBody.length() * 2;
+        ted.codeUnitSize = SmsConstants.ENCODING_16BIT;
+        ted.codeUnitCount = msgBody.length();
+        if (octets > SmsConstants.MAX_USER_DATA_BYTES) {
+            // If EMS is not supported, break down EMS into single segment SMS
+            // and add page info " x/y".
+            // In the case of UCS2 encoding type, we need 8 bytes for this
+            // but we only have 6 bytes from UDH, so truncate the limit for
+            // each segment by 2 bytes (1 char).
+            int maxUserDataBytesWithHeader = SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER;
+            if (!SmsMessage.hasEmsSupport()) {
+                // make sure total number of segments is less than 10
+                if (octets <= 9 * (maxUserDataBytesWithHeader - 2)) {
+                    maxUserDataBytesWithHeader -= 2;
+                }
+            }
+
+            int pos = 0;  // Index in code units.
+            int msgCount = 0;
+            while (pos < msgBody.length()) {
+                int nextPos = findNextUnicodePosition(pos, maxUserDataBytesWithHeader,
+                        msgBody);
+                if (nextPos == msgBody.length()) {
+                    ted.codeUnitsRemaining = pos + maxUserDataBytesWithHeader / 2 -
+                            msgBody.length();
+                }
+                pos = nextPos;
+                msgCount++;
+            }
+            ted.msgCount = msgCount;
+        } else {
+            ted.msgCount = 1;
+            ted.codeUnitsRemaining = (SmsConstants.MAX_USER_DATA_BYTES - octets) / 2;
+        }
+
+        return ted;
+    }
+}
diff --git a/com/android/internal/telephony/SmsNumberUtils.java b/com/android/internal/telephony/SmsNumberUtils.java
new file mode 100644
index 0000000..bf548d6
--- /dev/null
+++ b/com/android/internal/telephony/SmsNumberUtils.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.os.Binder;
+import android.os.Build;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.HbpcdLookup.MccIdd;
+import com.android.internal.telephony.HbpcdLookup.MccLookup;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+
+ /**
+ * This class implements handle the MO SMS target address before sending.
+ * This is special for VZW requirement. Follow the specifications of assisted dialing
+ * of MO SMS while traveling on VZW CDMA, international CDMA or GSM markets.
+ * {@hide}
+ */
+public class SmsNumberUtils {
+    private static final String TAG = "SmsNumberUtils";
+    private static final boolean DBG = Build.IS_DEBUGGABLE;
+
+    private static final String PLUS_SIGN = "+";
+
+    private static final int NANP_SHORT_LENGTH = 7;
+    private static final int NANP_MEDIUM_LENGTH = 10;
+    private static final int NANP_LONG_LENGTH = 11;
+
+    private static final int NANP_CC = 1;
+    private static final String NANP_NDD = "1";
+    private static final String NANP_IDD = "011";
+
+    private static final int MIN_COUNTRY_AREA_LOCAL_LENGTH = 10;
+
+    private static final int GSM_UMTS_NETWORK = 0;
+    private static final int CDMA_HOME_NETWORK = 1;
+    private static final int CDMA_ROAMING_NETWORK = 2;
+
+    private static final int NP_NONE = 0;
+    private static final int NP_NANP_BEGIN = 1;
+
+    /* <Phone Number>, <NXX>-<XXXX> N[2-9] */
+    private static final int NP_NANP_LOCAL = NP_NANP_BEGIN;
+
+    /* <Area_code>-<Phone Number>, <NXX>-<NXX>-<XXXX> N[2-9] */
+    private static final int NP_NANP_AREA_LOCAL = NP_NANP_BEGIN + 1;
+
+    /* <1>-<Area_code>-<Phone Number>, 1-<NXX>-<NXX>-<XXXX> N[2-9] */
+    private static final int NP_NANP_NDD_AREA_LOCAL = NP_NANP_BEGIN + 2;
+
+    /* <+><U.S.Country_code><Area_code><Phone Number>, +1-<NXX>-<NXX>-<XXXX> N[2-9] */
+    private static final int NP_NANP_NBPCD_CC_AREA_LOCAL = NP_NANP_BEGIN + 3;
+
+    /* <Local_IDD><Country_code><Area_code><Phone Number>, 001-1-<NXX>-<NXX>-<XXXX> N[2-9] */
+    private static final int NP_NANP_LOCALIDD_CC_AREA_LOCAL = NP_NANP_BEGIN + 4;
+
+    /* <+><Home_IDD><Country_code><Area_code><Phone Number>, +011-1-<NXX>-<NXX>-<XXXX> N[2-9] */
+    private static final int NP_NANP_NBPCD_HOMEIDD_CC_AREA_LOCAL = NP_NANP_BEGIN + 5;
+
+    private static final int NP_INTERNATIONAL_BEGIN = 100;
+    /* <+>-<Home_IDD>-<Country_code>-<Area_code>-<Phone Number>, +011-86-25-86281234 */
+    private static final int NP_NBPCD_HOMEIDD_CC_AREA_LOCAL = NP_INTERNATIONAL_BEGIN;
+
+    /* <Home_IDD>-<Country_code>-<Area_code>-<Phone Number>, 011-86-25-86281234 */
+    private static final int NP_HOMEIDD_CC_AREA_LOCAL = NP_INTERNATIONAL_BEGIN + 1;
+
+    /* <NBPCD>-<Country_code>-<Area_code>-<Phone Number>, +1-86-25-86281234 */
+    private static final int NP_NBPCD_CC_AREA_LOCAL = NP_INTERNATIONAL_BEGIN + 2;
+
+    /* <Local_IDD>-<Country_code>-<Area_code>-<Phone Number>, 00-86-25-86281234 */
+    private static final int NP_LOCALIDD_CC_AREA_LOCAL = NP_INTERNATIONAL_BEGIN + 3;
+
+    /* <Country_code>-<Area_code>-<Phone Number>, 86-25-86281234*/
+    private static final int NP_CC_AREA_LOCAL = NP_INTERNATIONAL_BEGIN + 4;
+
+    private static int[] ALL_COUNTRY_CODES = null;
+    private static int MAX_COUNTRY_CODES_LENGTH;
+    private static HashMap<String, ArrayList<String>> IDDS_MAPS =
+            new HashMap<String, ArrayList<String>>();
+
+    private static class NumberEntry {
+        public String number;
+        public String IDD;
+        public int countryCode;
+        public NumberEntry(String number) {
+            this.number = number;
+        }
+    }
+
+    /* Breaks the given number down and formats it according to the rules
+     * for different number plans and different network.
+     *
+     * @param number destination number which need to be format
+     * @param activeMcc current network's mcc
+     * @param networkType current network type
+     *
+     * @return the number after formatting.
+     */
+    private static String formatNumber(Context context, String number,
+                               String activeMcc,
+                               int networkType) {
+        if (number == null ) {
+            throw new IllegalArgumentException("number is null");
+        }
+
+        if (activeMcc == null || activeMcc.trim().length() == 0) {
+            throw new IllegalArgumentException("activeMcc is null or empty!");
+        }
+
+        String networkPortionNumber = PhoneNumberUtils.extractNetworkPortion(number);
+        if (networkPortionNumber == null || networkPortionNumber.length() == 0) {
+            throw new IllegalArgumentException("Number is invalid!");
+        }
+
+        NumberEntry numberEntry = new NumberEntry(networkPortionNumber);
+        ArrayList<String> allIDDs = getAllIDDs(context, activeMcc);
+
+        // First check whether the number is a NANP number.
+        int nanpState = checkNANP(numberEntry, allIDDs);
+        if (DBG) Rlog.d(TAG, "NANP type: " + getNumberPlanType(nanpState));
+
+        if ((nanpState == NP_NANP_LOCAL)
+            || (nanpState == NP_NANP_AREA_LOCAL)
+            || (nanpState == NP_NANP_NDD_AREA_LOCAL)) {
+            return networkPortionNumber;
+        } else if (nanpState == NP_NANP_NBPCD_CC_AREA_LOCAL) {
+            if (networkType == CDMA_HOME_NETWORK
+                    || networkType == CDMA_ROAMING_NETWORK) {
+                // Remove "+"
+                return networkPortionNumber.substring(1);
+            } else {
+                return networkPortionNumber;
+            }
+        } else if (nanpState == NP_NANP_LOCALIDD_CC_AREA_LOCAL) {
+            if (networkType == CDMA_HOME_NETWORK) {
+                return networkPortionNumber;
+            } else if (networkType == GSM_UMTS_NETWORK) {
+                // Remove the local IDD and replace with "+"
+                int iddLength  =  numberEntry.IDD != null ? numberEntry.IDD.length() : 0;
+                return PLUS_SIGN + networkPortionNumber.substring(iddLength);
+            } else if (networkType == CDMA_ROAMING_NETWORK) {
+                // Remove the local IDD
+                int iddLength  =  numberEntry.IDD != null ? numberEntry.IDD.length() : 0;
+                return  networkPortionNumber.substring(iddLength);
+            }
+        }
+
+        int internationalState = checkInternationalNumberPlan(context, numberEntry, allIDDs,
+                NANP_IDD);
+        if (DBG) Rlog.d(TAG, "International type: " + getNumberPlanType(internationalState));
+        String returnNumber = null;
+
+        switch (internationalState) {
+            case NP_NBPCD_HOMEIDD_CC_AREA_LOCAL:
+                if (networkType == GSM_UMTS_NETWORK) {
+                    // Remove "+"
+                    returnNumber = networkPortionNumber.substring(1);
+                }
+                break;
+
+            case NP_NBPCD_CC_AREA_LOCAL:
+                // Replace "+" with "011"
+                returnNumber = NANP_IDD + networkPortionNumber.substring(1);
+                break;
+
+            case NP_LOCALIDD_CC_AREA_LOCAL:
+                if (networkType == GSM_UMTS_NETWORK || networkType == CDMA_ROAMING_NETWORK) {
+                    int iddLength  =  numberEntry.IDD != null ? numberEntry.IDD.length() : 0;
+                    // Replace <Local IDD> to <Home IDD>("011")
+                    returnNumber = NANP_IDD + networkPortionNumber.substring(iddLength);
+                }
+                break;
+
+            case NP_CC_AREA_LOCAL:
+                int countryCode = numberEntry.countryCode;
+
+                if (!inExceptionListForNpCcAreaLocal(numberEntry)
+                    && networkPortionNumber.length() >= 11 && countryCode != NANP_CC) {
+                    // Add "011"
+                    returnNumber = NANP_IDD + networkPortionNumber;
+                }
+                break;
+
+            case NP_HOMEIDD_CC_AREA_LOCAL:
+                returnNumber = networkPortionNumber;
+                break;
+
+            default:
+                // Replace "+" with 011 in CDMA network if the number's country
+                // code is not in the HbpcdLookup database.
+                if (networkPortionNumber.startsWith(PLUS_SIGN)
+                    && (networkType == CDMA_HOME_NETWORK || networkType == CDMA_ROAMING_NETWORK)) {
+                    if (networkPortionNumber.startsWith(PLUS_SIGN + NANP_IDD)) {
+                        // Only remove "+"
+                        returnNumber = networkPortionNumber.substring(1);
+                    } else {
+                        // Replace "+" with "011"
+                        returnNumber = NANP_IDD + networkPortionNumber.substring(1);
+                    }
+                }
+        }
+
+        if (returnNumber == null) {
+            returnNumber = networkPortionNumber;
+        }
+        return returnNumber;
+    }
+
+    /* Query International direct dialing from HbpcdLookup.db
+     * for specified country code
+     *
+     * @param mcc current network's country code
+     *
+     * @return the IDD array list.
+     */
+    private static ArrayList<String> getAllIDDs(Context context, String mcc) {
+        ArrayList<String> allIDDs = IDDS_MAPS.get(mcc);
+        if (allIDDs != null) {
+            return allIDDs;
+        } else {
+            allIDDs = new ArrayList<String>();
+        }
+
+        String projection[] = {MccIdd.IDD, MccIdd.MCC};
+        String where = null;
+
+        // if mcc is null         : return all rows
+        // if mcc is empty-string : return those rows whose mcc is emptry-string
+        String[] selectionArgs = null;
+        if (mcc != null) {
+            where = MccIdd.MCC + "=?";
+            selectionArgs = new String[] {mcc};
+        }
+
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(MccIdd.CONTENT_URI, projection,
+                    where, selectionArgs, null);
+            if (cursor.getCount() > 0) {
+                while (cursor.moveToNext()) {
+                    String idd = cursor.getString(0);
+                    if (!allIDDs.contains(idd)) {
+                        allIDDs.add(idd);
+                    }
+                }
+            }
+        } catch (SQLException e) {
+            Rlog.e(TAG, "Can't access HbpcdLookup database", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        IDDS_MAPS.put(mcc, allIDDs);
+
+        if (DBG) Rlog.d(TAG, "MCC = " + mcc + ", all IDDs = " + allIDDs);
+        return allIDDs;
+    }
+
+
+    /* Verify if the the destination number is a NANP number
+     *
+     * @param numberEntry including number and IDD array
+     * @param allIDDs the IDD array list of the current network's country code
+     *
+     * @return the number plan type related NANP
+     */
+    private static int checkNANP(NumberEntry numberEntry, ArrayList<String> allIDDs) {
+        boolean isNANP = false;
+        String number = numberEntry.number;
+
+        if (number.length() == NANP_SHORT_LENGTH) {
+            // 7 digits - Seven digit phone numbers
+            char firstChar = number.charAt(0);
+            if (firstChar >= '2' && firstChar <= '9') {
+                isNANP = true;
+                for (int i=1; i< NANP_SHORT_LENGTH; i++ ) {
+                    char c= number.charAt(i);
+                    if (!PhoneNumberUtils.isISODigit(c)) {
+                        isNANP = false;
+                        break;
+                    }
+                }
+            }
+            if (isNANP) {
+                return NP_NANP_LOCAL;
+            }
+        } else if (number.length() == NANP_MEDIUM_LENGTH) {
+            // 10 digits - Three digit area code followed by seven digit phone numbers/
+            if (isNANP(number)) {
+                return NP_NANP_AREA_LOCAL;
+            }
+        } else if (number.length() == NANP_LONG_LENGTH) {
+            // 11 digits - One digit U.S. NDD(National Direct Dial) prefix '1',
+            // followed by three digit area code and seven digit phone numbers
+            if (isNANP(number)) {
+                return NP_NANP_NDD_AREA_LOCAL;
+            }
+        } else if (number.startsWith(PLUS_SIGN)) {
+            number = number.substring(1);
+            if (number.length() == NANP_LONG_LENGTH) {
+                // '+' and 11 digits -'+', followed by NANP CC prefix '1' followed by
+                // three digit area code and seven digit phone numbers
+                if (isNANP(number)) {
+                    return NP_NANP_NBPCD_CC_AREA_LOCAL;
+                }
+            } else if (number.startsWith(NANP_IDD) && number.length() == NANP_LONG_LENGTH + 3) {
+                // '+' and 14 digits -'+', followed by NANP IDD "011" followed by NANP CC
+                // prefix '1' followed by three digit area code and seven digit phone numbers
+                number = number.substring(3);
+                if (isNANP(number)) {
+                    return NP_NANP_NBPCD_HOMEIDD_CC_AREA_LOCAL;
+                }
+            }
+        } else {
+            // Check whether it's NP_NANP_LOCALIDD_CC_AREA_LOCAL
+            for (String idd : allIDDs) {
+                if (number.startsWith(idd)) {
+                    String number2 = number.substring(idd.length());
+                    if(number2 !=null && number2.startsWith(String.valueOf(NANP_CC))){
+                        if (isNANP(number2)) {
+                            numberEntry.IDD = idd;
+                            return NP_NANP_LOCALIDD_CC_AREA_LOCAL;
+                        }
+                    }
+                }
+            }
+        }
+
+        return NP_NONE;
+    }
+
+    private static boolean isNANP(String number) {
+        if (number.length() == NANP_MEDIUM_LENGTH
+            || (number.length() == NANP_LONG_LENGTH  && number.startsWith(NANP_NDD))) {
+            if (number.length() == NANP_LONG_LENGTH) {
+                number = number.substring(1);
+            }
+            return (PhoneNumberUtils.isNanp(number));
+        }
+        return false;
+    }
+
+    /* Verify if the the destination number is an internal number
+     *
+     * @param numberEntry including number and IDD array
+     * @param allIDDs the IDD array list of the current network's country code
+     *
+     * @return the number plan type related international number
+     */
+    private static int checkInternationalNumberPlan(Context context, NumberEntry numberEntry,
+            ArrayList<String> allIDDs,String homeIDD) {
+        String number = numberEntry.number;
+        int countryCode = -1;
+
+        if (number.startsWith(PLUS_SIGN)) {
+            // +xxxxxxxxxx
+            String numberNoNBPCD = number.substring(1);
+            if (numberNoNBPCD.startsWith(homeIDD)) {
+                // +011xxxxxxxx
+                String numberCountryAreaLocal = numberNoNBPCD.substring(homeIDD.length());
+                if ((countryCode = getCountryCode(context, numberCountryAreaLocal)) > 0) {
+                    numberEntry.countryCode = countryCode;
+                    return NP_NBPCD_HOMEIDD_CC_AREA_LOCAL;
+                }
+            } else if ((countryCode = getCountryCode(context, numberNoNBPCD)) > 0) {
+                numberEntry.countryCode = countryCode;
+                return NP_NBPCD_CC_AREA_LOCAL;
+            }
+
+        } else if (number.startsWith(homeIDD)) {
+            // 011xxxxxxxxx
+            String numberCountryAreaLocal = number.substring(homeIDD.length());
+            if ((countryCode = getCountryCode(context, numberCountryAreaLocal)) > 0) {
+                numberEntry.countryCode = countryCode;
+                return NP_HOMEIDD_CC_AREA_LOCAL;
+            }
+        } else {
+            for (String exitCode : allIDDs) {
+                if (number.startsWith(exitCode)) {
+                    String numberNoIDD = number.substring(exitCode.length());
+                    if ((countryCode = getCountryCode(context, numberNoIDD)) > 0) {
+                        numberEntry.countryCode = countryCode;
+                        numberEntry.IDD = exitCode;
+                        return NP_LOCALIDD_CC_AREA_LOCAL;
+                    }
+                }
+            }
+
+            if (!number.startsWith("0") && (countryCode = getCountryCode(context, number)) > 0) {
+                numberEntry.countryCode = countryCode;
+                return NP_CC_AREA_LOCAL;
+            }
+        }
+        return NP_NONE;
+    }
+
+    /**
+     *  Returns the country code from the given number.
+     */
+    private static int getCountryCode(Context context, String number) {
+        int countryCode = -1;
+        if (number.length() >= MIN_COUNTRY_AREA_LOCAL_LENGTH) {
+            // Check Country code
+            int[] allCCs = getAllCountryCodes(context);
+            if (allCCs == null) {
+                return countryCode;
+            }
+
+            int[] ccArray = new int[MAX_COUNTRY_CODES_LENGTH];
+            for (int i = 0; i < MAX_COUNTRY_CODES_LENGTH; i ++) {
+                ccArray[i] = Integer.parseInt(number.substring(0, i+1));
+            }
+
+            for (int i = 0; i < allCCs.length; i ++) {
+                int tempCC = allCCs[i];
+                for (int j = 0; j < MAX_COUNTRY_CODES_LENGTH; j ++) {
+                    if (tempCC == ccArray[j]) {
+                        if (DBG) Rlog.d(TAG, "Country code = " + tempCC);
+                        return tempCC;
+                    }
+                }
+            }
+        }
+
+        return countryCode;
+    }
+
+    /**
+     *  Gets all country Codes information with given MCC.
+     */
+    private static int[] getAllCountryCodes(Context context) {
+        if (ALL_COUNTRY_CODES != null) {
+            return ALL_COUNTRY_CODES;
+        }
+
+        Cursor cursor = null;
+        try {
+            String projection[] = {MccLookup.COUNTRY_CODE};
+            cursor = context.getContentResolver().query(MccLookup.CONTENT_URI,
+                    projection, null, null, null);
+
+            if (cursor.getCount() > 0) {
+                ALL_COUNTRY_CODES = new int[cursor.getCount()];
+                int i = 0;
+                while (cursor.moveToNext()) {
+                    int countryCode = cursor.getInt(0);
+                    ALL_COUNTRY_CODES[i++] = countryCode;
+                    int length = String.valueOf(countryCode).trim().length();
+                    if (length > MAX_COUNTRY_CODES_LENGTH) {
+                        MAX_COUNTRY_CODES_LENGTH = length;
+                    }
+                }
+            }
+        } catch (SQLException e) {
+            Rlog.e(TAG, "Can't access HbpcdLookup database", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return ALL_COUNTRY_CODES;
+    }
+
+    private static boolean inExceptionListForNpCcAreaLocal(NumberEntry numberEntry) {
+        int countryCode = numberEntry.countryCode;
+        boolean result = (numberEntry.number.length() == 12
+                          && (countryCode == 7 || countryCode == 20
+                              || countryCode == 65 || countryCode == 90));
+        return result;
+    }
+
+    private static String getNumberPlanType(int state) {
+        String numberPlanType = "Number Plan type (" + state + "): ";
+
+        if (state == NP_NANP_LOCAL) {
+            numberPlanType = "NP_NANP_LOCAL";
+        } else if (state == NP_NANP_AREA_LOCAL) {
+            numberPlanType = "NP_NANP_AREA_LOCAL";
+        } else if (state  == NP_NANP_NDD_AREA_LOCAL) {
+            numberPlanType = "NP_NANP_NDD_AREA_LOCAL";
+        } else if (state == NP_NANP_NBPCD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_NANP_NBPCD_CC_AREA_LOCAL";
+        } else if (state == NP_NANP_LOCALIDD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_NANP_LOCALIDD_CC_AREA_LOCAL";
+        } else if (state == NP_NANP_NBPCD_HOMEIDD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_NANP_NBPCD_HOMEIDD_CC_AREA_LOCAL";
+        } else if (state == NP_NBPCD_HOMEIDD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_NBPCD_HOMEIDD_CC_AREA_LOCAL";
+        } else if (state == NP_HOMEIDD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_HOMEIDD_CC_AREA_LOCAL";
+        } else if (state == NP_NBPCD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_NBPCD_CC_AREA_LOCAL";
+        } else if (state == NP_LOCALIDD_CC_AREA_LOCAL) {
+            numberPlanType = "NP_LOCALIDD_CC_AREA_LOCAL";
+        } else if (state == NP_CC_AREA_LOCAL) {
+            numberPlanType = "NP_CC_AREA_LOCAL";
+        } else {
+            numberPlanType = "Unknown type";
+        }
+        return numberPlanType;
+    }
+
+    /**
+     *  Filter the destination number if using VZW sim card.
+     */
+    public static String filterDestAddr(Phone phone, String destAddr) {
+        if (DBG) Rlog.d(TAG, "enter filterDestAddr. destAddr=\"" + Rlog.pii(TAG, destAddr) + "\"" );
+
+        if (destAddr == null || !PhoneNumberUtils.isGlobalPhoneNumber(destAddr)) {
+            Rlog.w(TAG, "destAddr" + Rlog.pii(TAG, destAddr) +
+                    " is not a global phone number! Nothing changed.");
+            return destAddr;
+        }
+
+        final String networkOperator = TelephonyManager.from(phone.getContext()).
+                getNetworkOperator(phone.getSubId());
+        String result = null;
+
+        if (needToConvert(phone)) {
+            final int networkType = getNetworkType(phone);
+            if (networkType != -1 && !TextUtils.isEmpty(networkOperator)) {
+                String networkMcc = networkOperator.substring(0, 3);
+                if (networkMcc != null && networkMcc.trim().length() > 0) {
+                    result = formatNumber(phone.getContext(), destAddr, networkMcc, networkType);
+                }
+            }
+        }
+
+        if (DBG) {
+            Rlog.d(TAG, "destAddr is " + ((result != null)?"formatted.":"not formatted."));
+            Rlog.d(TAG, "leave filterDestAddr, new destAddr=\"" + (result != null ? Rlog.pii(TAG,
+                    result) : Rlog.pii(TAG, destAddr)) + "\"");
+        }
+        return result != null ? result : destAddr;
+    }
+
+    /**
+     * Returns the current network type
+     */
+    private static int getNetworkType(Phone phone) {
+        int networkType = -1;
+        int phoneType = phone.getPhoneType();
+
+        if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+            networkType = GSM_UMTS_NETWORK;
+        } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+            if (isInternationalRoaming(phone)) {
+                networkType = CDMA_ROAMING_NETWORK;
+            } else {
+                networkType = CDMA_HOME_NETWORK;
+            }
+        } else {
+            if (DBG) Rlog.w(TAG, "warning! unknown mPhoneType value=" + phoneType);
+        }
+
+        return networkType;
+    }
+
+    private static boolean isInternationalRoaming(Phone phone) {
+        String operatorIsoCountry = TelephonyManager.from(phone.getContext()).
+                getNetworkCountryIsoForPhone(phone.getPhoneId());
+        String simIsoCountry = TelephonyManager.from(phone.getContext()).getSimCountryIsoForPhone(
+                phone.getPhoneId());
+        boolean internationalRoaming = !TextUtils.isEmpty(operatorIsoCountry)
+                && !TextUtils.isEmpty(simIsoCountry)
+                && !simIsoCountry.equals(operatorIsoCountry);
+        if (internationalRoaming) {
+            if ("us".equals(simIsoCountry)) {
+                internationalRoaming = !"vi".equals(operatorIsoCountry);
+            } else if ("vi".equals(simIsoCountry)) {
+                internationalRoaming = !"us".equals(operatorIsoCountry);
+            }
+        }
+        return internationalRoaming;
+    }
+
+    private static boolean needToConvert(Phone phone) {
+        // Calling package may not have READ_PHONE_STATE which is required for getConfig().
+        // Clear the calling identity so that it is called as self.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configManager != null) {
+                PersistableBundle bundle = configManager.getConfig();
+                if (bundle != null) {
+                    return bundle.getBoolean(CarrierConfigManager
+                            .KEY_SMS_REQUIRES_DESTINATION_NUMBER_CONVERSION_BOOL);
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        // by default this value is false
+        return false;
+    }
+
+    private static boolean compareGid1(Phone phone, String serviceGid1) {
+        String gid1 = phone.getGroupIdLevel1();
+        boolean ret = true;
+
+        if (TextUtils.isEmpty(serviceGid1)) {
+            if (DBG) Rlog.d(TAG, "compareGid1 serviceGid is empty, return " + ret);
+            return ret;
+        }
+
+        int gid_length = serviceGid1.length();
+        // Check if gid1 match service GID1
+        if (!((gid1 != null) && (gid1.length() >= gid_length) &&
+                gid1.substring(0, gid_length).equalsIgnoreCase(serviceGid1))) {
+            if (DBG) Rlog.d(TAG, " gid1 " + gid1 + " serviceGid1 " + serviceGid1);
+            ret = false;
+        }
+        if (DBG) Rlog.d(TAG, "compareGid1 is " + (ret?"Same":"Different"));
+        return ret;
+    }
+}
diff --git a/com/android/internal/telephony/SmsRawData.java b/com/android/internal/telephony/SmsRawData.java
new file mode 100644
index 0000000..891d942
--- /dev/null
+++ b/com/android/internal/telephony/SmsRawData.java
@@ -0,0 +1,62 @@
+/*
+** Copyright 2007, 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.internal.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ *  A parcelable holder class of byte[] for ISms aidl implementation
+ */
+public class SmsRawData implements Parcelable {
+    byte[] data;
+
+    //Static Methods
+    public static final Parcelable.Creator<SmsRawData> CREATOR
+            = new Parcelable.Creator<SmsRawData> (){
+        public SmsRawData createFromParcel(Parcel source) {
+            int size;
+            size = source.readInt();
+            byte[] data = new byte[size];
+            source.readByteArray(data);
+            return new SmsRawData(data);
+        }
+
+        public SmsRawData[] newArray(int size) {
+            return new SmsRawData[size];
+        }
+    };
+
+    // Constructor
+    public SmsRawData(byte[] data) {
+        this.data = data;
+    }
+
+    public byte[] getBytes() {
+        return data;
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(data.length);
+        dest.writeByteArray(data);
+    }
+}
diff --git a/com/android/internal/telephony/SmsResponse.java b/com/android/internal/telephony/SmsResponse.java
new file mode 100644
index 0000000..1aac242
--- /dev/null
+++ b/com/android/internal/telephony/SmsResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony;
+
+/**
+ * Object returned by the RIL upon successful completion of sendSMS.
+ * Contains message reference and ackPdu.
+ *
+ */
+public class SmsResponse {
+    /** Message reference of the just-sent SMS. */
+    int mMessageRef;
+    /** ackPdu for the just-sent SMS. */
+    String mAckPdu;
+    /**
+     * errorCode: See 3GPP 27.005, 3.2.5 for GSM/UMTS,
+     * 3GPP2 N.S0005 (IS-41C) Table 171 for CDMA, -1 if unknown or not applicable.
+     */
+    public int mErrorCode;
+
+    public SmsResponse(int messageRef, String ackPdu, int errorCode) {
+        mMessageRef = messageRef;
+        mAckPdu = ackPdu;
+        mErrorCode = errorCode;
+    }
+
+    @Override
+    public String toString() {
+        String ret = "{ mMessageRef = " + mMessageRef
+                        + ", mErrorCode = " + mErrorCode
+                        + ", mAckPdu = " + mAckPdu
+                        + "}";
+        return ret;
+    }
+}
diff --git a/com/android/internal/telephony/SmsStorageMonitor.java b/com/android/internal/telephony/SmsStorageMonitor.java
new file mode 100644
index 0000000..77ac097
--- /dev/null
+++ b/com/android/internal/telephony/SmsStorageMonitor.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+
+/**
+ * Monitors the device and ICC storage, and sends the appropriate events.
+ *
+ * This code was formerly part of {@link SMSDispatcher}, and has been moved
+ * into a separate class to support instantiation of multiple SMSDispatchers on
+ * dual-mode devices that require support for both 3GPP and 3GPP2 format messages.
+ */
+public class SmsStorageMonitor extends Handler {
+    private static final String TAG = "SmsStorageMonitor";
+
+    /** SIM/RUIM storage is full */
+    private static final int EVENT_ICC_FULL = 1;
+
+    /** Memory status reporting is acknowledged by RIL */
+    private static final int EVENT_REPORT_MEMORY_STATUS_DONE = 2;
+
+    /** Radio is ON */
+    private static final int EVENT_RADIO_ON = 3;
+
+    /** Context from phone object passed to constructor. */
+    private final Context mContext;
+
+    /** Wake lock to ensure device stays awake while dispatching the SMS intent. */
+    private PowerManager.WakeLock mWakeLock;
+
+    private boolean mReportMemoryStatusPending;
+
+    /** it is use to put in to extra value for SIM_FULL_ACTION and SMS_REJECTED_ACTION */
+    Phone mPhone;
+
+    final CommandsInterface mCi;                            // accessed from inner class
+    boolean mStorageAvailable = true;                       // accessed from inner class
+
+    /**
+     * Hold the wake lock for 5 seconds, which should be enough time for
+     * any receiver(s) to grab its own wake lock.
+     */
+    private static final int WAKE_LOCK_TIMEOUT = 5000;
+
+    /**
+     * Creates an SmsStorageMonitor and registers for events.
+     * @param phone the Phone to use
+     */
+    public SmsStorageMonitor(Phone phone) {
+        mPhone = phone;
+        mContext = phone.getContext();
+        mCi = phone.mCi;
+
+        createWakelock();
+
+        mCi.setOnIccSmsFull(this, EVENT_ICC_FULL, null);
+        mCi.registerForOn(this, EVENT_RADIO_ON, null);
+
+        // Register for device storage intents.  Use these to notify the RIL
+        // that storage for SMS is or is not available.
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_DEVICE_STORAGE_FULL);
+        filter.addAction(Intent.ACTION_DEVICE_STORAGE_NOT_FULL);
+        mContext.registerReceiver(mResultReceiver, filter);
+    }
+
+    public void dispose() {
+        mCi.unSetOnIccSmsFull(this);
+        mCi.unregisterForOn(this);
+        mContext.unregisterReceiver(mResultReceiver);
+    }
+
+    /**
+     * Handles events coming from the phone stack. Overridden from handler.
+     * @param msg the message to handle
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+            case EVENT_ICC_FULL:
+                handleIccFull();
+                break;
+
+            case EVENT_REPORT_MEMORY_STATUS_DONE:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception != null) {
+                    mReportMemoryStatusPending = true;
+                    Rlog.v(TAG, "Memory status report to modem pending : mStorageAvailable = "
+                            + mStorageAvailable);
+                } else {
+                    mReportMemoryStatusPending = false;
+                }
+                break;
+
+            case EVENT_RADIO_ON:
+                if (mReportMemoryStatusPending) {
+                    Rlog.v(TAG, "Sending pending memory status report : mStorageAvailable = "
+                            + mStorageAvailable);
+                    mCi.reportSmsMemoryStatus(mStorageAvailable,
+                            obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE));
+                }
+                break;
+        }
+    }
+
+    private void createWakelock() {
+        PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SmsStorageMonitor");
+        mWakeLock.setReferenceCounted(true);
+    }
+
+    /**
+     * Called when SIM_FULL message is received from the RIL. Notifies the default SMS application
+     * that SIM storage for SMS messages is full.
+     */
+    private void handleIccFull() {
+        // broadcast SIM_FULL intent
+        Intent intent = new Intent(Intents.SIM_FULL_ACTION);
+        intent.setComponent(SmsApplication.getDefaultSimFullApplication(mContext, false));
+        mWakeLock.acquire(WAKE_LOCK_TIMEOUT);
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
+        mContext.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS);
+    }
+
+    /** Returns whether or not there is storage available for an incoming SMS. */
+    public boolean isStorageAvailable() {
+        return mStorageAvailable;
+    }
+
+    private final BroadcastReceiver mResultReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_FULL)) {
+                mStorageAvailable = false;
+                mCi.reportSmsMemoryStatus(false, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE));
+            } else if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_NOT_FULL)) {
+                mStorageAvailable = true;
+                mCi.reportSmsMemoryStatus(true, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE));
+            }
+        }
+    };
+}
diff --git a/com/android/internal/telephony/SmsUsageMonitor.java b/com/android/internal/telephony/SmsUsageMonitor.java
new file mode 100644
index 0000000..402a5ef
--- /dev/null
+++ b/com/android/internal/telephony/SmsUsageMonitor.java
@@ -0,0 +1,650 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony;
+
+import android.app.AppGlobals;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.XmlResourceParser;
+import android.database.ContentObserver;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.util.AtomicFile;
+import android.util.Xml;
+
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+
+/**
+ * Implement the per-application based SMS control, which limits the number of
+ * SMS/MMS messages an app can send in the checking period.
+ *
+ * This code was formerly part of {@link SMSDispatcher}, and has been moved
+ * into a separate class to support instantiation of multiple SMSDispatchers on
+ * dual-mode devices that require support for both 3GPP and 3GPP2 format messages.
+ */
+public class SmsUsageMonitor {
+    private static final String TAG = "SmsUsageMonitor";
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    private static final String SHORT_CODE_PATH = "/data/misc/sms/codes";
+
+    /** Default checking period for SMS sent without user permission. */
+    private static final int DEFAULT_SMS_CHECK_PERIOD = 60000;      // 1 minute
+
+    /** Default number of SMS sent in checking period without user permission. */
+    private static final int DEFAULT_SMS_MAX_COUNT = 30;
+
+    /** Return value from {@link #checkDestination} for regular phone numbers. */
+    static final int CATEGORY_NOT_SHORT_CODE = 0;
+
+    /** Return value from {@link #checkDestination} for free (no cost) short codes. */
+    static final int CATEGORY_FREE_SHORT_CODE = 1;
+
+    /** Return value from {@link #checkDestination} for standard rate (non-premium) short codes. */
+    static final int CATEGORY_STANDARD_SHORT_CODE = 2;
+
+    /** Return value from {@link #checkDestination} for possible premium short codes. */
+    public static final int CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE = 3;
+
+    /** Return value from {@link #checkDestination} for premium short codes. */
+    static final int CATEGORY_PREMIUM_SHORT_CODE = 4;
+
+    /** @hide */
+    public static int mergeShortCodeCategories(int type1, int type2) {
+        if (type1 > type2) return type1;
+        return type2;
+    }
+
+    /** Premium SMS permission for a new package (ask user when first premium SMS sent). */
+    public static final int PREMIUM_SMS_PERMISSION_UNKNOWN = 0;
+
+    /** Default premium SMS permission (ask user for each premium SMS sent). */
+    public static final int PREMIUM_SMS_PERMISSION_ASK_USER = 1;
+
+    /** Premium SMS permission when the owner has denied the app from sending premium SMS. */
+    public static final int PREMIUM_SMS_PERMISSION_NEVER_ALLOW = 2;
+
+    /** Premium SMS permission when the owner has allowed the app to send premium SMS. */
+    public static final int PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW = 3;
+
+    private final int mCheckPeriod;
+    private final int mMaxAllowed;
+
+    private final HashMap<String, ArrayList<Long>> mSmsStamp =
+            new HashMap<String, ArrayList<Long>>();
+
+    /** Context for retrieving regexes from XML resource. */
+    private final Context mContext;
+
+    /** Country code for the cached short code pattern matcher. */
+    private String mCurrentCountry;
+
+    /** Cached short code pattern matcher for {@link #mCurrentCountry}. */
+    private ShortCodePatternMatcher mCurrentPatternMatcher;
+
+    /** Notice when the enabled setting changes - can be changed through gservices */
+    private final AtomicBoolean mCheckEnabled = new AtomicBoolean(true);
+
+    /** Handler for responding to content observer updates. */
+    private final SettingsObserverHandler mSettingsObserverHandler;
+
+    /** File holding the patterns */
+    private final File mPatternFile = new File(SHORT_CODE_PATH);
+
+    /** Last modified time for pattern file */
+    private long mPatternFileLastModified = 0;
+
+    /** Directory for per-app SMS permission XML file. */
+    private static final String SMS_POLICY_FILE_DIRECTORY = "/data/misc/sms";
+
+    /** Per-app SMS permission XML filename. */
+    private static final String SMS_POLICY_FILE_NAME = "premium_sms_policy.xml";
+
+    /** XML tag for root element. */
+    private static final String TAG_SHORTCODES = "shortcodes";
+
+    /** XML tag for short code patterns for a specific country. */
+    private static final String TAG_SHORTCODE = "shortcode";
+
+    /** XML attribute for the country code. */
+    private static final String ATTR_COUNTRY = "country";
+
+    /** XML attribute for the short code regex pattern. */
+    private static final String ATTR_PATTERN = "pattern";
+
+    /** XML attribute for the premium short code regex pattern. */
+    private static final String ATTR_PREMIUM = "premium";
+
+    /** XML attribute for the free short code regex pattern. */
+    private static final String ATTR_FREE = "free";
+
+    /** XML attribute for the standard rate short code regex pattern. */
+    private static final String ATTR_STANDARD = "standard";
+
+    /** Stored copy of premium SMS package permissions. */
+    private AtomicFile mPolicyFile;
+
+    /** Loaded copy of premium SMS package permissions. */
+    private final HashMap<String, Integer> mPremiumSmsPolicy = new HashMap<String, Integer>();
+
+    /** XML tag for root element of premium SMS permissions. */
+    private static final String TAG_SMS_POLICY_BODY = "premium-sms-policy";
+
+    /** XML tag for a package. */
+    private static final String TAG_PACKAGE = "package";
+
+    /** XML attribute for the package name. */
+    private static final String ATTR_PACKAGE_NAME = "name";
+
+    /** XML attribute for the package's premium SMS permission (integer type). */
+    private static final String ATTR_PACKAGE_SMS_POLICY = "sms-policy";
+
+    /**
+     * SMS short code regex pattern matcher for a specific country.
+     */
+    private static final class ShortCodePatternMatcher {
+        private final Pattern mShortCodePattern;
+        private final Pattern mPremiumShortCodePattern;
+        private final Pattern mFreeShortCodePattern;
+        private final Pattern mStandardShortCodePattern;
+
+        ShortCodePatternMatcher(String shortCodeRegex, String premiumShortCodeRegex,
+                String freeShortCodeRegex, String standardShortCodeRegex) {
+            mShortCodePattern = (shortCodeRegex != null ? Pattern.compile(shortCodeRegex) : null);
+            mPremiumShortCodePattern = (premiumShortCodeRegex != null ?
+                    Pattern.compile(premiumShortCodeRegex) : null);
+            mFreeShortCodePattern = (freeShortCodeRegex != null ?
+                    Pattern.compile(freeShortCodeRegex) : null);
+            mStandardShortCodePattern = (standardShortCodeRegex != null ?
+                    Pattern.compile(standardShortCodeRegex) : null);
+        }
+
+        int getNumberCategory(String phoneNumber) {
+            if (mFreeShortCodePattern != null && mFreeShortCodePattern.matcher(phoneNumber)
+                    .matches()) {
+                return CATEGORY_FREE_SHORT_CODE;
+            }
+            if (mStandardShortCodePattern != null && mStandardShortCodePattern.matcher(phoneNumber)
+                    .matches()) {
+                return CATEGORY_STANDARD_SHORT_CODE;
+            }
+            if (mPremiumShortCodePattern != null && mPremiumShortCodePattern.matcher(phoneNumber)
+                    .matches()) {
+                return CATEGORY_PREMIUM_SHORT_CODE;
+            }
+            if (mShortCodePattern != null && mShortCodePattern.matcher(phoneNumber).matches()) {
+                return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE;
+            }
+            return CATEGORY_NOT_SHORT_CODE;
+        }
+    }
+
+    /**
+     * Observe the secure setting for enable flag
+     */
+    private static class SettingsObserver extends ContentObserver {
+        private final Context mContext;
+        private final AtomicBoolean mEnabled;
+
+        SettingsObserver(Handler handler, Context context, AtomicBoolean enabled) {
+            super(handler);
+            mContext = context;
+            mEnabled = enabled;
+            onChange(false);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mEnabled.set(Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.SMS_SHORT_CODE_CONFIRMATION, 1) != 0);
+        }
+    }
+
+    private static class SettingsObserverHandler extends Handler {
+        SettingsObserverHandler(Context context, AtomicBoolean enabled) {
+            ContentResolver resolver = context.getContentResolver();
+            ContentObserver globalObserver = new SettingsObserver(this, context, enabled);
+            resolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.SMS_SHORT_CODE_CONFIRMATION), false, globalObserver);
+        }
+    }
+
+    /**
+     * Create SMS usage monitor.
+     * @param context the context to use to load resources and get TelephonyManager service
+     */
+    public SmsUsageMonitor(Context context) {
+        mContext = context;
+        ContentResolver resolver = context.getContentResolver();
+
+        mMaxAllowed = Settings.Global.getInt(resolver,
+                Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT,
+                DEFAULT_SMS_MAX_COUNT);
+
+        mCheckPeriod = Settings.Global.getInt(resolver,
+                Settings.Global.SMS_OUTGOING_CHECK_INTERVAL_MS,
+                DEFAULT_SMS_CHECK_PERIOD);
+
+        mSettingsObserverHandler = new SettingsObserverHandler(mContext, mCheckEnabled);
+
+        loadPremiumSmsPolicyDb();
+    }
+
+    /**
+     * Return a pattern matcher object for the specified country.
+     * @param country the country to search for
+     * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found
+     */
+    private ShortCodePatternMatcher getPatternMatcherFromFile(String country) {
+        FileReader patternReader = null;
+        XmlPullParser parser = null;
+        try {
+            patternReader = new FileReader(mPatternFile);
+            parser = Xml.newPullParser();
+            parser.setInput(patternReader);
+            return getPatternMatcherFromXmlParser(parser, country);
+        } catch (FileNotFoundException e) {
+            Rlog.e(TAG, "Short Code Pattern File not found");
+        } catch (XmlPullParserException e) {
+            Rlog.e(TAG, "XML parser exception reading short code pattern file", e);
+        } finally {
+            mPatternFileLastModified = mPatternFile.lastModified();
+            if (patternReader != null) {
+                try {
+                    patternReader.close();
+                } catch (IOException e) {}
+            }
+        }
+        return null;
+    }
+
+    private ShortCodePatternMatcher getPatternMatcherFromResource(String country) {
+        int id = com.android.internal.R.xml.sms_short_codes;
+        XmlResourceParser parser = null;
+        try {
+            parser = mContext.getResources().getXml(id);
+            return getPatternMatcherFromXmlParser(parser, country);
+        } finally {
+            if (parser != null) parser.close();
+        }
+    }
+
+    private ShortCodePatternMatcher getPatternMatcherFromXmlParser(XmlPullParser parser,
+            String country) {
+        try {
+            XmlUtils.beginDocument(parser, TAG_SHORTCODES);
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+                String element = parser.getName();
+                if (element == null) {
+                    Rlog.e(TAG, "Parsing pattern data found null");
+                    break;
+                }
+
+                if (element.equals(TAG_SHORTCODE)) {
+                    String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY);
+                    if (VDBG) Rlog.d(TAG, "Found country " + currentCountry);
+                    if (country.equals(currentCountry)) {
+                        String pattern = parser.getAttributeValue(null, ATTR_PATTERN);
+                        String premium = parser.getAttributeValue(null, ATTR_PREMIUM);
+                        String free = parser.getAttributeValue(null, ATTR_FREE);
+                        String standard = parser.getAttributeValue(null, ATTR_STANDARD);
+                        return new ShortCodePatternMatcher(pattern, premium, free, standard);
+                    }
+                } else {
+                    Rlog.e(TAG, "Error: skipping unknown XML tag " + element);
+                }
+            }
+        } catch (XmlPullParserException e) {
+            Rlog.e(TAG, "XML parser exception reading short code patterns", e);
+        } catch (IOException e) {
+            Rlog.e(TAG, "I/O exception reading short code patterns", e);
+        }
+        if (DBG) Rlog.d(TAG, "Country (" + country + ") not found");
+        return null;    // country not found
+    }
+
+    /** Clear the SMS application list for disposal. */
+    void dispose() {
+        mSmsStamp.clear();
+    }
+
+    /**
+     * Check to see if an application is allowed to send new SMS messages, and confirm with
+     * user if the send limit was reached or if a non-system app is potentially sending to a
+     * premium SMS short code or number.
+     *
+     * @param appName the package name of the app requesting to send an SMS
+     * @param smsWaiting the number of new messages desired to send
+     * @return true if application is allowed to send the requested number
+     *  of new sms messages
+     */
+    public boolean check(String appName, int smsWaiting) {
+        synchronized (mSmsStamp) {
+            removeExpiredTimestamps();
+
+            ArrayList<Long> sentList = mSmsStamp.get(appName);
+            if (sentList == null) {
+                sentList = new ArrayList<Long>();
+                mSmsStamp.put(appName, sentList);
+            }
+
+            return isUnderLimit(sentList, smsWaiting);
+        }
+    }
+
+    /**
+     * Check if the destination is a possible premium short code.
+     * NOTE: the caller is expected to strip non-digits from the destination number with
+     * {@link PhoneNumberUtils#extractNetworkPortion} before calling this method.
+     * This happens in {@link SMSDispatcher#sendRawPdu} so that we use the same phone number
+     * for testing and in the user confirmation dialog if the user needs to confirm the number.
+     * This makes it difficult for malware to fool the user or the short code pattern matcher
+     * by using non-ASCII characters to make the number appear to be different from the real
+     * destination phone number.
+     *
+     * @param destAddress the destination address to test for possible short code
+     * @return {@link #CATEGORY_NOT_SHORT_CODE}, {@link #CATEGORY_FREE_SHORT_CODE},
+     *  {@link #CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE}, or {@link #CATEGORY_PREMIUM_SHORT_CODE}.
+     */
+    public int checkDestination(String destAddress, String countryIso) {
+        synchronized (mSettingsObserverHandler) {
+            // always allow emergency numbers
+            if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) {
+                if (DBG) Rlog.d(TAG, "isEmergencyNumber");
+                return CATEGORY_NOT_SHORT_CODE;
+            }
+            // always allow if the feature is disabled
+            if (!mCheckEnabled.get()) {
+                if (DBG) Rlog.e(TAG, "check disabled");
+                return CATEGORY_NOT_SHORT_CODE;
+            }
+
+            if (countryIso != null) {
+                if (mCurrentCountry == null || !countryIso.equals(mCurrentCountry) ||
+                        mPatternFile.lastModified() != mPatternFileLastModified) {
+                    if (mPatternFile.exists()) {
+                        if (DBG) Rlog.d(TAG, "Loading SMS Short Code patterns from file");
+                        mCurrentPatternMatcher = getPatternMatcherFromFile(countryIso);
+                    } else {
+                        if (DBG) Rlog.d(TAG, "Loading SMS Short Code patterns from resource");
+                        mCurrentPatternMatcher = getPatternMatcherFromResource(countryIso);
+                    }
+                    mCurrentCountry = countryIso;
+                }
+            }
+
+            if (mCurrentPatternMatcher != null) {
+                return mCurrentPatternMatcher.getNumberCategory(destAddress);
+            } else {
+                // Generic rule: numbers of 5 digits or less are considered potential short codes
+                Rlog.e(TAG, "No patterns for \"" + countryIso + "\": using generic short code rule");
+                if (destAddress.length() <= 5) {
+                    return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE;
+                } else {
+                    return CATEGORY_NOT_SHORT_CODE;
+                }
+            }
+        }
+    }
+
+    /**
+     * Load the premium SMS policy from an XML file.
+     * Based on code from NotificationManagerService.
+     */
+    private void loadPremiumSmsPolicyDb() {
+        synchronized (mPremiumSmsPolicy) {
+            if (mPolicyFile == null) {
+                File dir = new File(SMS_POLICY_FILE_DIRECTORY);
+                mPolicyFile = new AtomicFile(new File(dir, SMS_POLICY_FILE_NAME));
+
+                mPremiumSmsPolicy.clear();
+
+                FileInputStream infile = null;
+                try {
+                    infile = mPolicyFile.openRead();
+                    final XmlPullParser parser = Xml.newPullParser();
+                    parser.setInput(infile, StandardCharsets.UTF_8.name());
+
+                    XmlUtils.beginDocument(parser, TAG_SMS_POLICY_BODY);
+
+                    while (true) {
+                        XmlUtils.nextElement(parser);
+
+                        String element = parser.getName();
+                        if (element == null) break;
+
+                        if (element.equals(TAG_PACKAGE)) {
+                            String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
+                            String policy = parser.getAttributeValue(null, ATTR_PACKAGE_SMS_POLICY);
+                            if (packageName == null) {
+                                Rlog.e(TAG, "Error: missing package name attribute");
+                            } else if (policy == null) {
+                                Rlog.e(TAG, "Error: missing package policy attribute");
+                            } else try {
+                                mPremiumSmsPolicy.put(packageName, Integer.parseInt(policy));
+                            } catch (NumberFormatException e) {
+                                Rlog.e(TAG, "Error: non-numeric policy type " + policy);
+                            }
+                        } else {
+                            Rlog.e(TAG, "Error: skipping unknown XML tag " + element);
+                        }
+                    }
+                } catch (FileNotFoundException e) {
+                    // No data yet
+                } catch (IOException e) {
+                    Rlog.e(TAG, "Unable to read premium SMS policy database", e);
+                } catch (NumberFormatException e) {
+                    Rlog.e(TAG, "Unable to parse premium SMS policy database", e);
+                } catch (XmlPullParserException e) {
+                    Rlog.e(TAG, "Unable to parse premium SMS policy database", e);
+                } finally {
+                    if (infile != null) {
+                        try {
+                            infile.close();
+                        } catch (IOException ignored) {
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Persist the premium SMS policy to an XML file.
+     * Based on code from NotificationManagerService.
+     */
+    private void writePremiumSmsPolicyDb() {
+        synchronized (mPremiumSmsPolicy) {
+            FileOutputStream outfile = null;
+            try {
+                outfile = mPolicyFile.startWrite();
+
+                XmlSerializer out = new FastXmlSerializer();
+                out.setOutput(outfile, StandardCharsets.UTF_8.name());
+
+                out.startDocument(null, true);
+
+                out.startTag(null, TAG_SMS_POLICY_BODY);
+
+                for (Map.Entry<String, Integer> policy : mPremiumSmsPolicy.entrySet()) {
+                    out.startTag(null, TAG_PACKAGE);
+                    out.attribute(null, ATTR_PACKAGE_NAME, policy.getKey());
+                    out.attribute(null, ATTR_PACKAGE_SMS_POLICY, policy.getValue().toString());
+                    out.endTag(null, TAG_PACKAGE);
+                }
+
+                out.endTag(null, TAG_SMS_POLICY_BODY);
+                out.endDocument();
+
+                mPolicyFile.finishWrite(outfile);
+            } catch (IOException e) {
+                Rlog.e(TAG, "Unable to write premium SMS policy database", e);
+                if (outfile != null) {
+                    mPolicyFile.failWrite(outfile);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the premium SMS permission for the specified package. If the package has never
+     * been seen before, the default {@link #PREMIUM_SMS_PERMISSION_ASK_USER}
+     * will be returned.
+     * @param packageName the name of the package to query permission
+     * @return one of {@link #PREMIUM_SMS_PERMISSION_UNKNOWN},
+     *  {@link #PREMIUM_SMS_PERMISSION_ASK_USER},
+     *  {@link #PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
+     *  {@link #PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
+     * @throws SecurityException if the caller is not a system process
+     */
+    public int getPremiumSmsPermission(String packageName) {
+        checkCallerIsSystemOrPhoneOrSameApp(packageName);
+        synchronized (mPremiumSmsPolicy) {
+            Integer policy = mPremiumSmsPolicy.get(packageName);
+            if (policy == null) {
+                return PREMIUM_SMS_PERMISSION_UNKNOWN;
+            } else {
+                return policy;
+            }
+        }
+    }
+
+    /**
+     * Sets the premium SMS permission for the specified package and save the value asynchronously
+     * to persistent storage.
+     * @param packageName the name of the package to set permission
+     * @param permission one of {@link #PREMIUM_SMS_PERMISSION_ASK_USER},
+     *  {@link #PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
+     *  {@link #PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
+     * @throws SecurityException if the caller is not a system process
+     */
+    public void setPremiumSmsPermission(String packageName, int permission) {
+        checkCallerIsSystemOrPhoneApp();
+        if (permission < PREMIUM_SMS_PERMISSION_ASK_USER
+                || permission > PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW) {
+            throw new IllegalArgumentException("invalid SMS permission type " + permission);
+        }
+        synchronized (mPremiumSmsPolicy) {
+            mPremiumSmsPolicy.put(packageName, permission);
+        }
+        // write policy file in the background
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                writePremiumSmsPolicyDb();
+            }
+        }).start();
+    }
+
+    private static void checkCallerIsSystemOrPhoneOrSameApp(String pkg) {
+        int uid = Binder.getCallingUid();
+        int appId = UserHandle.getAppId(uid);
+        if (appId == Process.SYSTEM_UID || appId == Process.PHONE_UID || uid == 0) {
+            return;
+        }
+        try {
+            ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo(
+                    pkg, 0, UserHandle.getCallingUserId());
+            if (!UserHandle.isSameApp(ai.uid, uid)) {
+                throw new SecurityException("Calling uid " + uid + " gave package"
+                        + pkg + " which is owned by uid " + ai.uid);
+            }
+        } catch (RemoteException re) {
+            throw new SecurityException("Unknown package " + pkg + "\n" + re);
+        }
+    }
+
+    private static void checkCallerIsSystemOrPhoneApp() {
+        int uid = Binder.getCallingUid();
+        int appId = UserHandle.getAppId(uid);
+        if (appId == Process.SYSTEM_UID || appId == Process.PHONE_UID || uid == 0) {
+            return;
+        }
+        throw new SecurityException("Disallowed call for uid " + uid);
+    }
+
+    /**
+     * Remove keys containing only old timestamps. This can happen if an SMS app is used
+     * to send messages and then uninstalled.
+     */
+    private void removeExpiredTimestamps() {
+        long beginCheckPeriod = System.currentTimeMillis() - mCheckPeriod;
+
+        synchronized (mSmsStamp) {
+            Iterator<Map.Entry<String, ArrayList<Long>>> iter = mSmsStamp.entrySet().iterator();
+            while (iter.hasNext()) {
+                Map.Entry<String, ArrayList<Long>> entry = iter.next();
+                ArrayList<Long> oldList = entry.getValue();
+                if (oldList.isEmpty() || oldList.get(oldList.size() - 1) < beginCheckPeriod) {
+                    iter.remove();
+                }
+            }
+        }
+    }
+
+    private boolean isUnderLimit(ArrayList<Long> sent, int smsWaiting) {
+        Long ct = System.currentTimeMillis();
+        long beginCheckPeriod = ct - mCheckPeriod;
+
+        if (VDBG) log("SMS send size=" + sent.size() + " time=" + ct);
+
+        while (!sent.isEmpty() && sent.get(0) < beginCheckPeriod) {
+            sent.remove(0);
+        }
+
+        if ((sent.size() + smsWaiting) <= mMaxAllowed) {
+            for (int i = 0; i < smsWaiting; i++ ) {
+                sent.add(ct);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private static void log(String msg) {
+        Rlog.d(TAG, msg);
+    }
+}
diff --git a/com/android/internal/telephony/SubscriptionController.java b/com/android/internal/telephony/SubscriptionController.java
new file mode 100644
index 0000000..f122cc0
--- /dev/null
+++ b/com/android/internal/telephony/SubscriptionController.java
@@ -0,0 +1,2142 @@
+/*
+* Copyright (C) 2014 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.internal.telephony;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.RadioAccessFamily;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccAccessRule;
+import android.telephony.euicc.EuiccManager;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IccCardConstants.State;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+/**
+ * SubscriptionController to provide an inter-process communication to
+ * access Sms in Icc.
+ *
+ * Any setters which take subId, slotIndex or phoneId as a parameter will throw an exception if the
+ * parameter equals the corresponding INVALID_XXX_ID or DEFAULT_XXX_ID.
+ *
+ * All getters will lookup the corresponding default if the parameter is DEFAULT_XXX_ID. Ie calling
+ * getPhoneId(DEFAULT_SUB_ID) will return the same as getPhoneId(getDefaultSubId()).
+ *
+ * Finally, any getters which perform the mapping between subscriptions, slots and phones will
+ * return the corresponding INVALID_XXX_ID if the parameter is INVALID_XXX_ID. All other getters
+ * will fail and return the appropriate error value. Ie calling
+ * getSlotIndex(INVALID_SUBSCRIPTION_ID) will return INVALID_SIM_SLOT_INDEX and calling
+ * getSubInfoForSubscriber(INVALID_SUBSCRIPTION_ID) will return null.
+ *
+ */
+public class SubscriptionController extends ISub.Stub {
+    static final String LOG_TAG = "SubscriptionController";
+    static final boolean DBG = true;
+    static final boolean VDBG = false;
+    static final boolean DBG_CACHE = false;
+    static final int MAX_LOCAL_LOG_LINES = 500; // TODO: Reduce to 100 when 17678050 is fixed
+    private ScLocalLog mLocalLog = new ScLocalLog(MAX_LOCAL_LOG_LINES);
+
+    /* The Cache of Active SubInfoRecord(s) list of currently in use SubInfoRecord(s) */
+    private AtomicReference<List<SubscriptionInfo>> mCacheActiveSubInfoList = new AtomicReference();
+
+    /**
+     * Copied from android.util.LocalLog with flush() adding flush and line number
+     * TODO: Update LocalLog
+     */
+    static class ScLocalLog {
+
+        private LinkedList<String> mLog;
+        private int mMaxLines;
+        private Time mNow;
+
+        public ScLocalLog(int maxLines) {
+            mLog = new LinkedList<String>();
+            mMaxLines = maxLines;
+            mNow = new Time();
+        }
+
+        public synchronized void log(String msg) {
+            if (mMaxLines > 0) {
+                int pid = android.os.Process.myPid();
+                int tid = android.os.Process.myTid();
+                mNow.setToNow();
+                mLog.add(mNow.format("%m-%d %H:%M:%S") + " pid=" + pid + " tid=" + tid + " " + msg);
+                while (mLog.size() > mMaxLines) mLog.remove();
+            }
+        }
+
+        public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            final int LOOPS_PER_FLUSH = 10; // Flush every N loops.
+            Iterator<String> itr = mLog.listIterator(0);
+            int i = 0;
+            while (itr.hasNext()) {
+                pw.println(Integer.toString(i++) + ": " + itr.next());
+                // Flush periodically so we don't drop lines
+                if ((i % LOOPS_PER_FLUSH) == 0) pw.flush();
+            }
+        }
+    }
+
+    private static final Comparator<SubscriptionInfo> SUBSCRIPTION_INFO_COMPARATOR =
+            (arg0, arg1) -> {
+                // Primary sort key on SimSlotIndex
+                int flag = arg0.getSimSlotIndex() - arg1.getSimSlotIndex();
+                if (flag == 0) {
+                    // Secondary sort on SubscriptionId
+                    return arg0.getSubscriptionId() - arg1.getSubscriptionId();
+                }
+                return flag;
+            };
+
+    protected final Object mLock = new Object();
+
+    /** The singleton instance. */
+    private static SubscriptionController sInstance = null;
+    protected static Phone[] sPhones;
+    protected Context mContext;
+    protected TelephonyManager mTelephonyManager;
+    protected CallManager mCM;
+
+    private AppOpsManager mAppOps;
+
+    // FIXME: Does not allow for multiple subs in a slot and change to SparseArray
+    private static Map<Integer, Integer> sSlotIndexToSubId =
+            new ConcurrentHashMap<Integer, Integer>();
+    private static int mDefaultFallbackSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    private static int mDefaultPhoneId = SubscriptionManager.DEFAULT_PHONE_INDEX;
+
+    private int[] colorArr;
+
+    public static SubscriptionController init(Phone phone) {
+        synchronized (SubscriptionController.class) {
+            if (sInstance == null) {
+                sInstance = new SubscriptionController(phone);
+            } else {
+                Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
+            }
+            return sInstance;
+        }
+    }
+
+    public static SubscriptionController init(Context c, CommandsInterface[] ci) {
+        synchronized (SubscriptionController.class) {
+            if (sInstance == null) {
+                sInstance = new SubscriptionController(c);
+            } else {
+                Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
+            }
+            return sInstance;
+        }
+    }
+
+    public static SubscriptionController getInstance() {
+        if (sInstance == null)
+        {
+           Log.wtf(LOG_TAG, "getInstance null");
+        }
+
+        return sInstance;
+    }
+
+    protected SubscriptionController(Context c) {
+        init(c);
+    }
+
+    protected void init(Context c) {
+        mContext = c;
+        mCM = CallManager.getInstance();
+        mTelephonyManager = TelephonyManager.from(mContext);
+
+        mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE);
+
+        if(ServiceManager.getService("isub") == null) {
+                ServiceManager.addService("isub", this);
+        }
+
+        if (DBG) logdl("[SubscriptionController] init by Context");
+    }
+
+    private boolean isSubInfoReady() {
+        return sSlotIndexToSubId.size() > 0;
+    }
+
+    private SubscriptionController(Phone phone) {
+        mContext = phone.getContext();
+        mCM = CallManager.getInstance();
+        mAppOps = mContext.getSystemService(AppOpsManager.class);
+
+        if(ServiceManager.getService("isub") == null) {
+                ServiceManager.addService("isub", this);
+        }
+
+        if (DBG) logdl("[SubscriptionController] init by Phone");
+    }
+
+    /**
+     * Make sure the caller can read phone state which requires holding the
+     * READ_PHONE_STATE permission and the OP_READ_PHONE_STATE app op being
+     * set to MODE_ALLOWED.
+     *
+     * @param callingPackage The package claiming to make the IPC.
+     * @param message The name of the access protected method.
+     *
+     * @throws SecurityException if the caller does not have READ_PHONE_STATE permission.
+     */
+    private boolean canReadPhoneState(String callingPackage, String message) {
+        try {
+            mContext.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, message);
+
+            // SKIP checking run-time permission since self or using PRIVILEDGED permission
+            return true;
+        } catch (SecurityException e) {
+            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE,
+                    message);
+        }
+
+        return mAppOps.noteOp(AppOpsManager.OP_READ_PHONE_STATE, Binder.getCallingUid(),
+                callingPackage) == AppOpsManager.MODE_ALLOWED;
+    }
+
+    private void enforceModifyPhoneState(String message) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE, message);
+    }
+
+    /**
+     * Broadcast when SubscriptionInfo has changed
+     * FIXME: Hopefully removed if the API council accepts SubscriptionInfoListener
+     */
+     private void broadcastSimInfoContentChanged() {
+        Intent intent = new Intent(TelephonyIntents.ACTION_SUBINFO_CONTENT_CHANGE);
+        mContext.sendBroadcast(intent);
+        intent = new Intent(TelephonyIntents.ACTION_SUBINFO_RECORD_UPDATED);
+        mContext.sendBroadcast(intent);
+     }
+
+     public void notifySubscriptionInfoChanged() {
+         ITelephonyRegistry tr = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(
+                 "telephony.registry"));
+         try {
+             if (DBG) logd("notifySubscriptionInfoChanged:");
+             tr.notifySubscriptionInfoChanged();
+         } catch (RemoteException ex) {
+             // Should never happen because its always available.
+         }
+
+         // FIXME: Remove if listener technique accepted.
+         broadcastSimInfoContentChanged();
+     }
+
+    /**
+     * New SubInfoRecord instance and fill in detail info
+     * @param cursor
+     * @return the query result of desired SubInfoRecord
+     */
+    private SubscriptionInfo getSubInfoRecord(Cursor cursor) {
+        int id = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID));
+        String iccId = cursor.getString(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.ICC_ID));
+        int simSlotIndex = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.SIM_SLOT_INDEX));
+        String displayName = cursor.getString(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.DISPLAY_NAME));
+        String carrierName = cursor.getString(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.CARRIER_NAME));
+        int nameSource = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.NAME_SOURCE));
+        int iconTint = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.COLOR));
+        String number = cursor.getString(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.NUMBER));
+        int dataRoaming = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.DATA_ROAMING));
+        // Get the blank bitmap for this SubInfoRecord
+        Bitmap iconBitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                com.android.internal.R.drawable.ic_sim_card_multi_24px_clr);
+        int mcc = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.MCC));
+        int mnc = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.MNC));
+        // FIXME: consider stick this into database too
+        String countryIso = getSubscriptionCountryIso(id);
+        boolean isEmbedded = cursor.getInt(cursor.getColumnIndexOrThrow(
+                SubscriptionManager.IS_EMBEDDED)) == 1;
+        UiccAccessRule[] accessRules;
+        if (isEmbedded) {
+            accessRules = UiccAccessRule.decodeRules(cursor.getBlob(
+                    cursor.getColumnIndexOrThrow(SubscriptionManager.ACCESS_RULES)));
+        } else {
+            accessRules = null;
+        }
+
+        if (VDBG) {
+            String iccIdToPrint = SubscriptionInfo.givePrintableIccid(iccId);
+            logd("[getSubInfoRecord] id:" + id + " iccid:" + iccIdToPrint + " simSlotIndex:"
+                    + simSlotIndex + " displayName:" + displayName + " nameSource:" + nameSource
+                    + " iconTint:" + iconTint + " dataRoaming:" + dataRoaming
+                    + " mcc:" + mcc + " mnc:" + mnc + " countIso:" + countryIso + " isEmbedded:"
+                    + isEmbedded + " accessRules:" + Arrays.toString(accessRules));
+        }
+
+        // If line1number has been set to a different number, use it instead.
+        String line1Number = mTelephonyManager.getLine1Number(id);
+        if (!TextUtils.isEmpty(line1Number) && !line1Number.equals(number)) {
+            number = line1Number;
+        }
+        return new SubscriptionInfo(id, iccId, simSlotIndex, displayName, carrierName,
+                nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso,
+                isEmbedded, accessRules);
+    }
+
+    /**
+     * Get ISO country code for the subscription's provider
+     *
+     * @param subId The subscription ID
+     * @return The ISO country code for the subscription's provider
+     */
+    private String getSubscriptionCountryIso(int subId) {
+        final int phoneId = getPhoneId(subId);
+        if (phoneId < 0) {
+            return "";
+        }
+        return mTelephonyManager.getSimCountryIsoForPhone(phoneId);
+    }
+
+    /**
+     * Query SubInfoRecord(s) from subinfo database
+     * @param selection A filter declaring which rows to return
+     * @param queryKey query key content
+     * @return Array list of queried result from database
+     */
+     private List<SubscriptionInfo> getSubInfo(String selection, Object queryKey) {
+        if (VDBG) logd("selection:" + selection + " " + queryKey);
+        String[] selectionArgs = null;
+        if (queryKey != null) {
+            selectionArgs = new String[] {queryKey.toString()};
+        }
+        ArrayList<SubscriptionInfo> subList = null;
+        Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
+                null, selection, selectionArgs, null);
+        try {
+            if (cursor != null) {
+                while (cursor.moveToNext()) {
+                    SubscriptionInfo subInfo = getSubInfoRecord(cursor);
+                    if (subInfo != null)
+                    {
+                        if (subList == null)
+                        {
+                            subList = new ArrayList<SubscriptionInfo>();
+                        }
+                        subList.add(subInfo);
+                }
+                }
+            } else {
+                if (DBG) logd("Query fail");
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return subList;
+    }
+
+    /**
+     * Find unused color to be set for new SubInfoRecord
+     * @param callingPackage The package making the IPC.
+     * @return RGB integer value of color
+     */
+    private int getUnusedColor(String callingPackage) {
+        List<SubscriptionInfo> availableSubInfos = getActiveSubscriptionInfoList(callingPackage);
+        colorArr = mContext.getResources().getIntArray(com.android.internal.R.array.sim_colors);
+        int colorIdx = 0;
+
+        if (availableSubInfos != null) {
+            for (int i = 0; i < colorArr.length; i++) {
+                int j;
+                for (j = 0; j < availableSubInfos.size(); j++) {
+                    if (colorArr[i] == availableSubInfos.get(j).getIconTint()) {
+                        break;
+                    }
+                }
+                if (j == availableSubInfos.size()) {
+                    return colorArr[i];
+                }
+            }
+            colorIdx = availableSubInfos.size() % colorArr.length;
+        }
+        return colorArr[colorIdx];
+    }
+
+    /**
+     * Get the active SubscriptionInfo with the subId key
+     * @param subId The unique SubscriptionInfo key in database
+     * @param callingPackage The package making the IPC.
+     * @return SubscriptionInfo, maybe null if its not active
+     */
+    @Override
+    public SubscriptionInfo getActiveSubscriptionInfo(int subId, String callingPackage) {
+        if (!canReadPhoneState(callingPackage, "getActiveSubscriptionInfo")) {
+            return null;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            List<SubscriptionInfo> subList = getActiveSubscriptionInfoList(
+                    mContext.getOpPackageName());
+            if (subList != null) {
+                for (SubscriptionInfo si : subList) {
+                    if (si.getSubscriptionId() == subId) {
+                        if (DBG) {
+                            logd("[getActiveSubscriptionInfo]+ subId=" + subId + " subInfo=" + si);
+                        }
+
+                        return si;
+                    }
+                }
+            }
+            if (DBG) {
+                logd("[getActiveSubInfoForSubscriber]- subId=" + subId
+                        + " subList=" + subList + " subInfo=null");
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the active SubscriptionInfo associated with the iccId
+     * @param iccId the IccId of SIM card
+     * @param callingPackage The package making the IPC.
+     * @return SubscriptionInfo, maybe null if its not active
+     */
+    @Override
+    public SubscriptionInfo getActiveSubscriptionInfoForIccId(String iccId, String callingPackage) {
+        if (!canReadPhoneState(callingPackage, "getActiveSubscriptionInfoForIccId") ||
+                iccId == null) {
+            return null;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            List<SubscriptionInfo> subList = getActiveSubscriptionInfoList(
+                    mContext.getOpPackageName());
+            if (subList != null) {
+                for (SubscriptionInfo si : subList) {
+                    if (iccId.equals(si.getIccId())) {
+                        if (DBG)
+                            logd("[getActiveSubInfoUsingIccId]+ iccId=" + iccId + " subInfo=" + si);
+                        return si;
+                    }
+                }
+            }
+            if (DBG) {
+                logd("[getActiveSubInfoUsingIccId]+ iccId=" + iccId
+                        + " subList=" + subList + " subInfo=null");
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the active SubscriptionInfo associated with the slotIndex
+     * @param slotIndex the slot which the subscription is inserted
+     * @param callingPackage The package making the IPC.
+     * @return SubscriptionInfo, maybe null if its not active
+     */
+    @Override
+    public SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int slotIndex,
+            String callingPackage) {
+        if (!canReadPhoneState(callingPackage, "getActiveSubscriptionInfoForSimSlotIndex")) {
+            return null;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            List<SubscriptionInfo> subList = getActiveSubscriptionInfoList(
+                    mContext.getOpPackageName());
+            if (subList != null) {
+                for (SubscriptionInfo si : subList) {
+                    if (si.getSimSlotIndex() == slotIndex) {
+                        if (DBG) {
+                            logd("[getActiveSubscriptionInfoForSimSlotIndex]+ slotIndex="
+                                    + slotIndex + " subId=" + si);
+                        }
+                        return si;
+                    }
+                }
+                if (DBG) {
+                    logd("[getActiveSubscriptionInfoForSimSlotIndex]+ slotIndex=" + slotIndex
+                            + " subId=null");
+                }
+            } else {
+                if (DBG) {
+                    logd("[getActiveSubscriptionInfoForSimSlotIndex]+ subList=null");
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param callingPackage The package making the IPC.
+     * @return List of all SubscriptionInfo records in database,
+     * include those that were inserted before, maybe empty but not null.
+     * @hide
+     */
+    @Override
+    public List<SubscriptionInfo> getAllSubInfoList(String callingPackage) {
+        if (DBG) logd("[getAllSubInfoList]+");
+
+        if (!canReadPhoneState(callingPackage, "getAllSubInfoList")) {
+            return null;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            List<SubscriptionInfo> subList = null;
+            subList = getSubInfo(null, null);
+            if (subList != null) {
+                if (DBG) logd("[getAllSubInfoList]- " + subList.size() + " infos return");
+            } else {
+                if (DBG) logd("[getAllSubInfoList]- no info return");
+            }
+            return subList;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Get the SubInfoRecord(s) of the currently inserted SIM(s)
+     * @param callingPackage The package making the IPC.
+     * @return Array list of currently inserted SubInfoRecord(s)
+     */
+    @Override
+    public List<SubscriptionInfo> getActiveSubscriptionInfoList(String callingPackage) {
+
+        if (!canReadPhoneState(callingPackage, "getActiveSubscriptionInfoList")) {
+            return null;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (!isSubInfoReady()) {
+                if (DBG) logdl("[getActiveSubInfoList] Sub Controller not ready");
+                return null;
+            }
+
+            // Get the active subscription info list from the cache if the cache is not null
+            List<SubscriptionInfo> tmpCachedSubList = mCacheActiveSubInfoList.get();
+            if (tmpCachedSubList != null) {
+                if (DBG_CACHE) {
+                    for (SubscriptionInfo si : tmpCachedSubList) {
+                        logd("[getActiveSubscriptionInfoList] Getting Cached subInfo=" + si);
+                    }
+                }
+                return new ArrayList<SubscriptionInfo>(tmpCachedSubList);
+            } else {
+                if (DBG_CACHE) {
+                    logd("[getActiveSubscriptionInfoList] Cached subInfo is null");
+                }
+                return null;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Refresh the cache of SubInfoRecord(s) of the currently inserted SIM(s)
+     */
+    @VisibleForTesting
+    protected void refreshCachedActiveSubscriptionInfoList() {
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (!isSubInfoReady()) {
+                if (DBG_CACHE) {
+                    logdl("[refreshCachedActiveSubscriptionInfoList] "
+                            + "Sub Controller not ready ");
+                }
+                return;
+            }
+
+            List<SubscriptionInfo> subList = getSubInfo(
+                    SubscriptionManager.SIM_SLOT_INDEX + ">=0", null);
+
+            if (subList != null) {
+                // FIXME: Unnecessary when an insertion sort is used!
+                subList.sort(SUBSCRIPTION_INFO_COMPARATOR);
+
+                if (DBG_CACHE) {
+                    logdl("[refreshCachedActiveSubscriptionInfoList]- " + subList.size()
+                            + " infos return");
+                }
+            } else {
+                if (DBG_CACHE) logdl("[refreshCachedActiveSubscriptionInfoList]- no info return");
+            }
+
+            if (DBG_CACHE) {
+                for (SubscriptionInfo si : subList) {
+                    logd("[refreshCachedActiveSubscriptionInfoList] Setting Cached subInfo=" + si);
+                }
+            }
+            mCacheActiveSubInfoList.set(subList);
+
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Get the SUB count of active SUB(s)
+     * @param callingPackage The package making the IPC.
+     * @return active SIM count
+     */
+    @Override
+    public int getActiveSubInfoCount(String callingPackage) {
+        if (!canReadPhoneState(callingPackage, "getActiveSubInfoCount")) {
+            return 0;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            List<SubscriptionInfo> records = getActiveSubscriptionInfoList(
+                    mContext.getOpPackageName());
+            if (records == null) {
+                if (VDBG) logd("[getActiveSubInfoCount] records null");
+                return 0;
+            }
+            if (VDBG) logd("[getActiveSubInfoCount]- count: " + records.size());
+            return records.size();
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Get the SUB count of all SUB(s) in SubscriptoinInfo database
+     * @param callingPackage The package making the IPC.
+     * @return all SIM count in database, include what was inserted before
+     */
+    @Override
+    public int getAllSubInfoCount(String callingPackage) {
+        if (DBG) logd("[getAllSubInfoCount]+");
+
+        if (!canReadPhoneState(callingPackage, "getAllSubInfoCount")) {
+            return 0;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
+                    null, null, null, null);
+            try {
+                if (cursor != null) {
+                    int count = cursor.getCount();
+                    if (DBG) logd("[getAllSubInfoCount]- " + count + " SUB(s) in DB");
+                    return count;
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+            if (DBG) logd("[getAllSubInfoCount]- no SUB in DB");
+
+            return 0;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * @return the maximum number of subscriptions this device will support at any one time.
+     */
+    @Override
+    public int getActiveSubInfoCountMax() {
+        // FIXME: This valid now but change to use TelephonyDevController in the future
+        return mTelephonyManager.getSimCount();
+    }
+
+    @Override
+    public List<SubscriptionInfo> getAvailableSubscriptionInfoList(String callingPackage) {
+        if (!canReadPhoneState(callingPackage, "getAvailableSubscriptionInfoList")) {
+            throw new SecurityException("Need READ_PHONE_STATE to call "
+                    + " getAvailableSubscriptionInfoList");
+        }
+
+        // Now that all security checks pass, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            EuiccManager euiccManager =
+                    (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
+            if (!euiccManager.isEnabled()) {
+                if (DBG) logdl("[getAvailableSubInfoList] Embedded subscriptions are disabled");
+                return null;
+            }
+
+            List<SubscriptionInfo> subList = getSubInfo(
+                    SubscriptionManager.SIM_SLOT_INDEX + ">=0 OR "
+                            + SubscriptionManager.IS_EMBEDDED + "=1", null);
+
+            if (subList != null) {
+                subList.sort(SUBSCRIPTION_INFO_COMPARATOR);
+
+                if (VDBG) logdl("[getAvailableSubInfoList]- " + subList.size() + " infos return");
+            } else {
+                if (DBG) logdl("[getAvailableSubInfoList]- no info return");
+            }
+
+            return subList;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public List<SubscriptionInfo> getAccessibleSubscriptionInfoList(String callingPackage) {
+        EuiccManager euiccManager = (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
+        if (!euiccManager.isEnabled()) {
+            if (DBG) {
+                logdl("[getAccessibleSubInfoList] Embedded subscriptions are disabled");
+            }
+            return null;
+        }
+
+        // Verify that the given package belongs to the calling UID.
+        mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+
+        // Perform the operation as ourselves. If the caller cannot read phone state, they may still
+        // have carrier privileges per the subscription metadata, so we always need to make the
+        // query and then filter the results.
+        final long identity = Binder.clearCallingIdentity();
+        List<SubscriptionInfo> subList;
+        try {
+            subList = getSubInfo(SubscriptionManager.IS_EMBEDDED + "=1", null);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+
+        if (subList == null) {
+            if (DBG) logdl("[getAccessibleSubInfoList] No info returned");
+            return null;
+        }
+
+        // Filter the list to only include subscriptions which the (restored) caller can manage.
+        List<SubscriptionInfo> filteredList = subList.stream()
+                .filter(subscriptionInfo ->
+                        subscriptionInfo.canManageSubscription(mContext, callingPackage))
+                .sorted(SUBSCRIPTION_INFO_COMPARATOR)
+                .collect(Collectors.toList());
+        if (VDBG) {
+            logdl("[getAccessibleSubInfoList] " + filteredList.size() + " infos returned");
+        }
+        return filteredList;
+    }
+
+    /**
+     * Return the list of subscriptions in the database which are either:
+     * <ul>
+     * <li>Embedded (but see note about {@code includeNonRemovableSubscriptions}, or
+     * <li>In the given list of current embedded ICCIDs (which may not yet be in the database, or
+     *     which may not currently be marked as embedded).
+     * </ul>
+     *
+     * <p>NOTE: This is not accessible to external processes, so it does not need a permission
+     * check. It is only intended for use by {@link SubscriptionInfoUpdater}.
+     *
+     * @param embeddedIccids all ICCIDs of available embedded subscriptions. This is used to surface
+     *     entries for profiles which had been previously deleted.
+     * @param isEuiccRemovable whether the current ICCID is removable. Non-removable subscriptions
+     *     will only be returned if the current ICCID is not removable; otherwise, they are left
+     *     alone (not returned here unless in the embeddedIccids list) under the assumption that
+     *     they will still be accessible when the eUICC containing them is activated.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public List<SubscriptionInfo> getSubscriptionInfoListForEmbeddedSubscriptionUpdate(
+            String[] embeddedIccids, boolean isEuiccRemovable) {
+        StringBuilder whereClause = new StringBuilder();
+        whereClause.append("(").append(SubscriptionManager.IS_EMBEDDED).append("=1");
+        if (isEuiccRemovable) {
+            // Current eUICC is removable, so don't return non-removable subscriptions (which would
+            // be deleted), as these are expected to still be present on a different, non-removable
+            // eUICC.
+            whereClause.append(" AND ").append(SubscriptionManager.IS_REMOVABLE).append("=1");
+        }
+        // Else, return both removable and non-removable subscriptions. This is expected to delete
+        // all removable subscriptions, which is desired as they may not be accessible.
+
+        whereClause.append(") OR ").append(SubscriptionManager.ICC_ID).append(" IN (");
+        // ICCIDs are validated to contain only numbers when passed in, and come from a trusted
+        // app, so no need to escape.
+        for (int i = 0; i < embeddedIccids.length; i++) {
+            if (i > 0) {
+                whereClause.append(",");
+            }
+            whereClause.append("\"").append(embeddedIccids[i]).append("\"");
+        }
+        whereClause.append(")");
+
+        List<SubscriptionInfo> list = getSubInfo(whereClause.toString(), null);
+        if (list == null) {
+            return Collections.emptyList();
+        }
+        return list;
+    }
+
+    @Override
+    public void requestEmbeddedSubscriptionInfoListRefresh() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS,
+                "requestEmbeddedSubscriptionInfoListRefresh");
+        long token = Binder.clearCallingIdentity();
+        try {
+            PhoneFactory.requestEmbeddedSubscriptionInfoListRefresh(null /* callback */);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Asynchronously refresh the embedded subscription info list.
+     *
+     * @param callback Optional callback to execute after the refresh completes. Must terminate
+     *     quickly as it will be called from SubscriptionInfoUpdater's handler thread.
+     */
+    // No permission check needed as this is not exposed via AIDL.
+    public void requestEmbeddedSubscriptionInfoListRefresh(@Nullable Runnable callback) {
+        PhoneFactory.requestEmbeddedSubscriptionInfoListRefresh(callback);
+    }
+
+    /**
+     * Add a new SubInfoRecord to subinfo database if needed
+     * @param iccId the IccId of the SIM card
+     * @param slotIndex the slot which the SIM is inserted
+     * @return 0 if success, < 0 on error.
+     */
+    @Override
+    public int addSubInfoRecord(String iccId, int slotIndex) {
+        if (DBG) logdl("[addSubInfoRecord]+ iccId:" + SubscriptionInfo.givePrintableIccid(iccId) +
+                " slotIndex:" + slotIndex);
+
+        enforceModifyPhoneState("addSubInfoRecord");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (iccId == null) {
+                if (DBG) logdl("[addSubInfoRecord]- null iccId");
+                return -1;
+            }
+
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(SubscriptionManager.CONTENT_URI,
+                    new String[]{SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID,
+                            SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.NAME_SOURCE},
+                    SubscriptionManager.ICC_ID + "=?", new String[]{iccId}, null);
+
+            boolean setDisplayName = false;
+            try {
+                if (cursor == null || !cursor.moveToFirst()) {
+                    setDisplayName = true;
+                    Uri uri = insertEmptySubInfoRecord(iccId, slotIndex);
+                    if (DBG) logdl("[addSubInfoRecord] New record created: " + uri);
+                } else {
+                    int subId = cursor.getInt(0);
+                    int oldSimInfoId = cursor.getInt(1);
+                    int nameSource = cursor.getInt(2);
+                    ContentValues value = new ContentValues();
+
+                    if (slotIndex != oldSimInfoId) {
+                        value.put(SubscriptionManager.SIM_SLOT_INDEX, slotIndex);
+                    }
+
+                    if (nameSource != SubscriptionManager.NAME_SOURCE_USER_INPUT) {
+                        setDisplayName = true;
+                    }
+
+                    if (value.size() > 0) {
+                        resolver.update(SubscriptionManager.CONTENT_URI, value,
+                                SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID +
+                                        "=" + Long.toString(subId), null);
+
+                        // Refresh the Cache of Active Subscription Info List
+                        refreshCachedActiveSubscriptionInfoList();
+                    }
+
+                    if (DBG) logdl("[addSubInfoRecord] Record already exists");
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            cursor = resolver.query(SubscriptionManager.CONTENT_URI, null,
+                    SubscriptionManager.SIM_SLOT_INDEX + "=?",
+                    new String[] {String.valueOf(slotIndex)}, null);
+            try {
+                if (cursor != null && cursor.moveToFirst()) {
+                    do {
+                        int subId = cursor.getInt(cursor.getColumnIndexOrThrow(
+                                SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID));
+                        // If sSlotIndexToSubId already has the same subId for a slotIndex/phoneId,
+                        // do not add it.
+                        Integer currentSubId = sSlotIndexToSubId.get(slotIndex);
+                        if (currentSubId == null
+                                || currentSubId != subId
+                                || !SubscriptionManager.isValidSubscriptionId(currentSubId)) {
+                            // TODO While two subs active, if user deactivats first
+                            // one, need to update the default subId with second one.
+
+                            // FIXME: Currently we assume phoneId == slotIndex which in the future
+                            // may not be true, for instance with multiple subs per slot.
+                            // But is true at the moment.
+                            sSlotIndexToSubId.put(slotIndex, subId);
+                            int subIdCountMax = getActiveSubInfoCountMax();
+                            int defaultSubId = getDefaultSubId();
+                            if (DBG) {
+                                logdl("[addSubInfoRecord]"
+                                        + " sSlotIndexToSubId.size=" + sSlotIndexToSubId.size()
+                                        + " slotIndex=" + slotIndex + " subId=" + subId
+                                        + " defaultSubId=" + defaultSubId + " simCount=" + subIdCountMax);
+                            }
+
+                            // Set the default sub if not set or if single sim device
+                            if (!SubscriptionManager.isValidSubscriptionId(defaultSubId)
+                                    || subIdCountMax == 1) {
+                                setDefaultFallbackSubId(subId);
+                            }
+                            // If single sim device, set this subscription as the default for everything
+                            if (subIdCountMax == 1) {
+                                if (DBG) {
+                                    logdl("[addSubInfoRecord] one sim set defaults to subId=" + subId);
+                                }
+                                setDefaultDataSubId(subId);
+                                setDefaultSmsSubId(subId);
+                                setDefaultVoiceSubId(subId);
+                            }
+                        } else {
+                            if (DBG) {
+                                logdl("[addSubInfoRecord] currentSubId != null"
+                                        + " && currentSubId is valid, IGNORE");
+                            }
+                        }
+                        if (DBG) logdl("[addSubInfoRecord] hashmap(" + slotIndex + "," + subId + ")");
+                    } while (cursor.moveToNext());
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            // Set Display name after sub id is set above so as to get valid simCarrierName
+            int subId = getSubIdUsingPhoneId(slotIndex);
+            if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+                if (DBG) {
+                    logdl("[addSubInfoRecord]- getSubId failed invalid subId = " + subId);
+                }
+                return -1;
+            }
+            if (setDisplayName) {
+                String simCarrierName = mTelephonyManager.getSimOperatorName(subId);
+                String nameToSet;
+
+                if (!TextUtils.isEmpty(simCarrierName)) {
+                    nameToSet = simCarrierName;
+                } else {
+                    nameToSet = "CARD " + Integer.toString(slotIndex + 1);
+                }
+
+                ContentValues value = new ContentValues();
+                value.put(SubscriptionManager.DISPLAY_NAME, nameToSet);
+                resolver.update(SubscriptionManager.CONTENT_URI, value,
+                        SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID +
+                                "=" + Long.toString(subId), null);
+
+                // Refresh the Cache of Active Subscription Info List
+                refreshCachedActiveSubscriptionInfoList();
+
+                if (DBG) logdl("[addSubInfoRecord] sim name = " + nameToSet);
+            }
+
+            // Once the records are loaded, notify DcTracker
+            sPhones[slotIndex].updateDataConnectionTracker();
+
+            if (DBG) logdl("[addSubInfoRecord]- info size=" + sSlotIndexToSubId.size());
+
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        return 0;
+    }
+
+    /**
+     * Insert an empty SubInfo record into the database.
+     *
+     * <p>NOTE: This is not accessible to external processes, so it does not need a permission
+     * check. It is only intended for use by {@link SubscriptionInfoUpdater}.
+     *
+     * <p>Precondition: No record exists with this iccId.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public Uri insertEmptySubInfoRecord(String iccId, int slotIndex) {
+        ContentResolver resolver = mContext.getContentResolver();
+        ContentValues value = new ContentValues();
+        value.put(SubscriptionManager.ICC_ID, iccId);
+        int color = getUnusedColor(mContext.getOpPackageName());
+        // default SIM color differs between slots
+        value.put(SubscriptionManager.COLOR, color);
+        value.put(SubscriptionManager.SIM_SLOT_INDEX, slotIndex);
+        value.put(SubscriptionManager.CARRIER_NAME, "");
+
+        Uri uri = resolver.insert(SubscriptionManager.CONTENT_URI, value);
+
+        // Refresh the Cache of Active Subscription Info List
+        refreshCachedActiveSubscriptionInfoList();
+
+        return uri;
+    }
+
+    /**
+     * Generate and set carrier text based on input parameters
+     * @param showPlmn flag to indicate if plmn should be included in carrier text
+     * @param plmn plmn to be included in carrier text
+     * @param showSpn flag to indicate if spn should be included in carrier text
+     * @param spn spn to be included in carrier text
+     * @return true if carrier text is set, false otherwise
+     */
+    public boolean setPlmnSpn(int slotIndex, boolean showPlmn, String plmn, boolean showSpn,
+                              String spn) {
+        synchronized (mLock) {
+            int subId = getSubIdUsingPhoneId(slotIndex);
+            if (mContext.getPackageManager().resolveContentProvider(
+                    SubscriptionManager.CONTENT_URI.getAuthority(), 0) == null ||
+                    !SubscriptionManager.isValidSubscriptionId(subId)) {
+                // No place to store this info. Notify registrants of the change anyway as they
+                // might retrieve the SPN/PLMN text from the SST sticky broadcast.
+                // TODO: This can be removed once SubscriptionController is not running on devices
+                // that don't need it, such as TVs.
+                if (DBG) logd("[setPlmnSpn] No valid subscription to store info");
+                notifySubscriptionInfoChanged();
+                return false;
+            }
+            String carrierText = "";
+            if (showPlmn) {
+                carrierText = plmn;
+                if (showSpn) {
+                    // Need to show both plmn and spn if both are not same.
+                    if(!Objects.equals(spn, plmn)) {
+                        String separator = mContext.getString(
+                                com.android.internal.R.string.kg_text_message_separator).toString();
+                        carrierText = new StringBuilder().append(carrierText).append(separator)
+                                .append(spn).toString();
+                    }
+                }
+            } else if (showSpn) {
+                carrierText = spn;
+            }
+            setCarrierText(carrierText, subId);
+            return true;
+        }
+    }
+
+    /**
+     * Set carrier text by simInfo index
+     * @param text new carrier text
+     * @param subId the unique SubInfoRecord index in database
+     * @return the number of records updated
+     */
+    private int setCarrierText(String text, int subId) {
+        if (DBG) logd("[setCarrierText]+ text:" + text + " subId:" + subId);
+
+        enforceModifyPhoneState("setCarrierText");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            ContentValues value = new ContentValues(1);
+            value.put(SubscriptionManager.CARRIER_NAME, text);
+
+            int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
+                    value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" +
+                    Long.toString(subId), null);
+
+            // Refresh the Cache of Active Subscription Info List
+            refreshCachedActiveSubscriptionInfoList();
+
+            notifySubscriptionInfoChanged();
+
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Set SIM color tint by simInfo index
+     * @param tint the tint color of the SIM
+     * @param subId the unique SubInfoRecord index in database
+     * @return the number of records updated
+     */
+    @Override
+    public int setIconTint(int tint, int subId) {
+        if (DBG) logd("[setIconTint]+ tint:" + tint + " subId:" + subId);
+
+        enforceModifyPhoneState("setIconTint");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            validateSubId(subId);
+            ContentValues value = new ContentValues(1);
+            value.put(SubscriptionManager.COLOR, tint);
+            if (DBG) logd("[setIconTint]- tint:" + tint + " set");
+
+            int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
+                    value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" +
+                            Long.toString(subId), null);
+
+            // Refresh the Cache of Active Subscription Info List
+            refreshCachedActiveSubscriptionInfoList();
+
+            notifySubscriptionInfoChanged();
+
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Set display name by simInfo index
+     * @param displayName the display name of SIM card
+     * @param subId the unique SubInfoRecord index in database
+     * @return the number of records updated
+     */
+    @Override
+    public int setDisplayName(String displayName, int subId) {
+        return setDisplayNameUsingSrc(displayName, subId, -1);
+    }
+
+    /**
+     * Set display name by simInfo index with name source
+     * @param displayName the display name of SIM card
+     * @param subId the unique SubInfoRecord index in database
+     * @param nameSource 0: NAME_SOURCE_DEFAULT_SOURCE, 1: NAME_SOURCE_SIM_SOURCE,
+     *                   2: NAME_SOURCE_USER_INPUT, -1 NAME_SOURCE_UNDEFINED
+     * @return the number of records updated
+     */
+    @Override
+    public int setDisplayNameUsingSrc(String displayName, int subId, long nameSource) {
+        if (DBG) {
+            logd("[setDisplayName]+  displayName:" + displayName + " subId:" + subId
+                + " nameSource:" + nameSource);
+        }
+
+        enforceModifyPhoneState("setDisplayNameUsingSrc");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            validateSubId(subId);
+            String nameToSet;
+            if (displayName == null) {
+                nameToSet = mContext.getString(SubscriptionManager.DEFAULT_NAME_RES);
+            } else {
+                nameToSet = displayName;
+            }
+            ContentValues value = new ContentValues(1);
+            value.put(SubscriptionManager.DISPLAY_NAME, nameToSet);
+            if (nameSource >= SubscriptionManager.NAME_SOURCE_DEFAULT_SOURCE) {
+                if (DBG) logd("Set nameSource=" + nameSource);
+                value.put(SubscriptionManager.NAME_SOURCE, nameSource);
+            }
+            if (DBG) logd("[setDisplayName]- mDisplayName:" + nameToSet + " set");
+            // TODO(b/33075886): If this is an embedded subscription, we must also save the new name
+            // to the eSIM itself. Currently it will be blown away the next time the subscription
+            // list is updated.
+
+            int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
+                    value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" +
+                    Long.toString(subId), null);
+
+            // Refresh the Cache of Active Subscription Info List
+            refreshCachedActiveSubscriptionInfoList();
+
+            notifySubscriptionInfoChanged();
+
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Set phone number by subId
+     * @param number the phone number of the SIM
+     * @param subId the unique SubInfoRecord index in database
+     * @return the number of records updated
+     */
+    @Override
+    public int setDisplayNumber(String number, int subId) {
+        if (DBG) logd("[setDisplayNumber]+ subId:" + subId);
+
+        enforceModifyPhoneState("setDisplayNumber");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            validateSubId(subId);
+            int result;
+            int phoneId = getPhoneId(subId);
+
+            if (number == null || phoneId < 0 ||
+                    phoneId >= mTelephonyManager.getPhoneCount()) {
+                if (DBG) logd("[setDispalyNumber]- fail");
+                return -1;
+            }
+            ContentValues value = new ContentValues(1);
+            value.put(SubscriptionManager.NUMBER, number);
+
+            // This function had a call to update number on the SIM (Phone.setLine1Number()) but
+            // that was removed as there doesn't seem to be a reason for that. If it is added
+            // back, watch out for deadlocks.
+
+            result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI, value,
+                    SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID
+                            + "=" + Long.toString(subId), null);
+
+            // Refresh the Cache of Active Subscription Info List
+            refreshCachedActiveSubscriptionInfoList();
+
+            if (DBG) logd("[setDisplayNumber]- update result :" + result);
+            notifySubscriptionInfoChanged();
+
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Set data roaming by simInfo index
+     * @param roaming 0:Don't allow data when roaming, 1:Allow data when roaming
+     * @param subId the unique SubInfoRecord index in database
+     * @return the number of records updated
+     */
+    @Override
+    public int setDataRoaming(int roaming, int subId) {
+        if (DBG) logd("[setDataRoaming]+ roaming:" + roaming + " subId:" + subId);
+
+        enforceModifyPhoneState("setDataRoaming");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            validateSubId(subId);
+            if (roaming < 0) {
+                if (DBG) logd("[setDataRoaming]- fail");
+                return -1;
+            }
+            ContentValues value = new ContentValues(1);
+            value.put(SubscriptionManager.DATA_ROAMING, roaming);
+            if (DBG) logd("[setDataRoaming]- roaming:" + roaming + " set");
+
+            int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
+                    value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" +
+                    Long.toString(subId), null);
+
+            // Refresh the Cache of Active Subscription Info List
+            refreshCachedActiveSubscriptionInfoList();
+
+            notifySubscriptionInfoChanged();
+
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Set MCC/MNC by subscription ID
+     * @param mccMnc MCC/MNC associated with the subscription
+     * @param subId the unique SubInfoRecord index in database
+     * @return the number of records updated
+     */
+    public int setMccMnc(String mccMnc, int subId) {
+        int mcc = 0;
+        int mnc = 0;
+        try {
+            mcc = Integer.parseInt(mccMnc.substring(0,3));
+            mnc = Integer.parseInt(mccMnc.substring(3));
+        } catch (NumberFormatException e) {
+            loge("[setMccMnc] - couldn't parse mcc/mnc: " + mccMnc);
+        }
+        if (DBG) logd("[setMccMnc]+ mcc/mnc:" + mcc + "/" + mnc + " subId:" + subId);
+        ContentValues value = new ContentValues(2);
+        value.put(SubscriptionManager.MCC, mcc);
+        value.put(SubscriptionManager.MNC, mnc);
+
+        int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI, value,
+                SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + Long.toString(subId), null);
+
+        // Refresh the Cache of Active Subscription Info List
+        refreshCachedActiveSubscriptionInfoList();
+
+        notifySubscriptionInfoChanged();
+
+        return result;
+    }
+
+    @Override
+    public int getSlotIndex(int subId) {
+        if (VDBG) printStackTrace("[getSlotIndex] subId=" + subId);
+
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            subId = getDefaultSubId();
+        }
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            if (DBG) logd("[getSlotIndex]- subId invalid");
+            return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+        }
+
+        int size = sSlotIndexToSubId.size();
+
+        if (size == 0)
+        {
+            if (DBG) logd("[getSlotIndex]- size == 0, return SIM_NOT_INSERTED instead");
+            return SubscriptionManager.SIM_NOT_INSERTED;
+        }
+
+        for (Entry<Integer, Integer> entry: sSlotIndexToSubId.entrySet()) {
+            int sim = entry.getKey();
+            int sub = entry.getValue();
+
+            if (subId == sub)
+            {
+                if (VDBG) logv("[getSlotIndex]- return = " + sim);
+                return sim;
+            }
+        }
+
+        if (DBG) logd("[getSlotIndex]- return fail");
+        return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+    }
+
+    /**
+     * Return the subId for specified slot Id.
+     * @deprecated
+     */
+    @Override
+    @Deprecated
+    public int[] getSubId(int slotIndex) {
+        if (VDBG) printStackTrace("[getSubId]+ slotIndex=" + slotIndex);
+
+        // Map default slotIndex to the current default subId.
+        // TODO: Not used anywhere sp consider deleting as it's somewhat nebulous
+        // as a slot maybe used for multiple different type of "connections"
+        // such as: voice, data and sms. But we're doing the best we can and using
+        // getDefaultSubId which makes a best guess.
+        if (slotIndex == SubscriptionManager.DEFAULT_SIM_SLOT_INDEX) {
+            slotIndex = getSlotIndex(getDefaultSubId());
+            if (VDBG) logd("[getSubId] map default slotIndex=" + slotIndex);
+        }
+
+        // Check that we have a valid slotIndex
+        if (!SubscriptionManager.isValidSlotIndex(slotIndex)) {
+            if (DBG) logd("[getSubId]- invalid slotIndex=" + slotIndex);
+            return null;
+        }
+
+        // Check if we've got any SubscriptionInfo records using slotIndexToSubId as a surrogate.
+        int size = sSlotIndexToSubId.size();
+        if (size == 0) {
+            if (VDBG) {
+                logd("[getSubId]- sSlotIndexToSubId.size == 0, return DummySubIds slotIndex="
+                        + slotIndex);
+            }
+            return getDummySubIds(slotIndex);
+        }
+
+        // Create an array of subIds that are in this slot?
+        ArrayList<Integer> subIds = new ArrayList<Integer>();
+        for (Entry<Integer, Integer> entry: sSlotIndexToSubId.entrySet()) {
+            int slot = entry.getKey();
+            int sub = entry.getValue();
+            if (slotIndex == slot) {
+                subIds.add(sub);
+            }
+        }
+
+        // Convert ArrayList to array
+        int numSubIds = subIds.size();
+        if (numSubIds > 0) {
+            int[] subIdArr = new int[numSubIds];
+            for (int i = 0; i < numSubIds; i++) {
+                subIdArr[i] = subIds.get(i);
+            }
+            if (VDBG) logd("[getSubId]- subIdArr=" + subIdArr);
+            return subIdArr;
+        } else {
+            if (DBG) logd("[getSubId]- numSubIds == 0, return DummySubIds slotIndex=" + slotIndex);
+            return getDummySubIds(slotIndex);
+        }
+    }
+
+    @Override
+    public int getPhoneId(int subId) {
+        if (VDBG) printStackTrace("[getPhoneId] subId=" + subId);
+        int phoneId;
+
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            subId = getDefaultSubId();
+            if (DBG) logdl("[getPhoneId] asked for default subId=" + subId);
+        }
+
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            if (VDBG) {
+                logdl("[getPhoneId]- invalid subId return="
+                        + SubscriptionManager.INVALID_PHONE_INDEX);
+            }
+            return SubscriptionManager.INVALID_PHONE_INDEX;
+        }
+
+        int size = sSlotIndexToSubId.size();
+        if (size == 0) {
+            phoneId = mDefaultPhoneId;
+            if (DBG) logdl("[getPhoneId]- no sims, returning default phoneId=" + phoneId);
+            return phoneId;
+        }
+
+        // FIXME: Assumes phoneId == slotIndex
+        for (Entry<Integer, Integer> entry: sSlotIndexToSubId.entrySet()) {
+            int sim = entry.getKey();
+            int sub = entry.getValue();
+
+            if (subId == sub) {
+                if (VDBG) logdl("[getPhoneId]- found subId=" + subId + " phoneId=" + sim);
+                return sim;
+            }
+        }
+
+        phoneId = mDefaultPhoneId;
+        if (DBG) {
+            logdl("[getPhoneId]- subId=" + subId + " not found return default phoneId=" + phoneId);
+        }
+        return phoneId;
+
+    }
+
+    private int[] getDummySubIds(int slotIndex) {
+        // FIXME: Remove notion of Dummy SUBSCRIPTION_ID.
+        // I tested this returning null as no one appears to care,
+        // but no connection came up on sprout with two sims.
+        // We need to figure out why and hopefully remove DummySubsIds!!!
+        int numSubs = getActiveSubInfoCountMax();
+        if (numSubs > 0) {
+            int[] dummyValues = new int[numSubs];
+            for (int i = 0; i < numSubs; i++) {
+                dummyValues[i] = SubscriptionManager.DUMMY_SUBSCRIPTION_ID_BASE - slotIndex;
+            }
+            if (VDBG) {
+                logd("getDummySubIds: slotIndex=" + slotIndex
+                    + " return " + numSubs + " DummySubIds with each subId=" + dummyValues[0]);
+            }
+            return dummyValues;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @return the number of records cleared
+     */
+    @Override
+    public int clearSubInfo() {
+        enforceModifyPhoneState("clearSubInfo");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            int size = sSlotIndexToSubId.size();
+
+            if (size == 0) {
+                if (DBG) logdl("[clearSubInfo]- no simInfo size=" + size);
+                return 0;
+            }
+
+            sSlotIndexToSubId.clear();
+            if (DBG) logdl("[clearSubInfo]- clear size=" + size);
+            return size;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private void logvl(String msg) {
+        logv(msg);
+        mLocalLog.log(msg);
+    }
+
+    private void logv(String msg) {
+        Rlog.v(LOG_TAG, msg);
+    }
+
+    private void logdl(String msg) {
+        logd(msg);
+        mLocalLog.log(msg);
+    }
+
+    private static void slogd(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    private void logd(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    private void logel(String msg) {
+        loge(msg);
+        mLocalLog.log(msg);
+    }
+
+    private void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+
+    @Override
+    public int getDefaultSubId() {
+        int subId;
+        boolean isVoiceCapable = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_voice_capable);
+        if (isVoiceCapable) {
+            subId = getDefaultVoiceSubId();
+            if (VDBG) logdl("[getDefaultSubId] isVoiceCapable subId=" + subId);
+        } else {
+            subId = getDefaultDataSubId();
+            if (VDBG) logdl("[getDefaultSubId] NOT VoiceCapable subId=" + subId);
+        }
+        if (!isActiveSubId(subId)) {
+            subId = mDefaultFallbackSubId;
+            if (VDBG) logdl("[getDefaultSubId] NOT active use fall back subId=" + subId);
+        }
+        if (VDBG) logv("[getDefaultSubId]- value = " + subId);
+        return subId;
+    }
+
+    @Override
+    public void setDefaultSmsSubId(int subId) {
+        enforceModifyPhoneState("setDefaultSmsSubId");
+
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            throw new RuntimeException("setDefaultSmsSubId called with DEFAULT_SUB_ID");
+        }
+        if (DBG) logdl("[setDefaultSmsSubId] subId=" + subId);
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION, subId);
+        broadcastDefaultSmsSubIdChanged(subId);
+    }
+
+    private void broadcastDefaultSmsSubIdChanged(int subId) {
+        // Broadcast an Intent for default sms sub change
+        if (DBG) logdl("[broadcastDefaultSmsSubIdChanged] subId=" + subId);
+        Intent intent = new Intent(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, subId);
+        intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    @Override
+    public int getDefaultSmsSubId() {
+        int subId = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (VDBG) logd("[getDefaultSmsSubId] subId=" + subId);
+        return subId;
+    }
+
+    @Override
+    public void setDefaultVoiceSubId(int subId) {
+        enforceModifyPhoneState("setDefaultVoiceSubId");
+
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            throw new RuntimeException("setDefaultVoiceSubId called with DEFAULT_SUB_ID");
+        }
+        if (DBG) logdl("[setDefaultVoiceSubId] subId=" + subId);
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION, subId);
+        broadcastDefaultVoiceSubIdChanged(subId);
+    }
+
+    private void broadcastDefaultVoiceSubIdChanged(int subId) {
+        // Broadcast an Intent for default voice sub change
+        if (DBG) logdl("[broadcastDefaultVoiceSubIdChanged] subId=" + subId);
+        Intent intent = new Intent(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, subId);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    @Override
+    public int getDefaultVoiceSubId() {
+        int subId = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (VDBG) logd("[getDefaultVoiceSubId] subId=" + subId);
+        return subId;
+    }
+
+    @Override
+    public int getDefaultDataSubId() {
+        int subId = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (VDBG) logd("[getDefaultDataSubId] subId= " + subId);
+        return subId;
+    }
+
+    @Override
+    public void setDefaultDataSubId(int subId) {
+        enforceModifyPhoneState("setDefaultDataSubId");
+
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            throw new RuntimeException("setDefaultDataSubId called with DEFAULT_SUB_ID");
+        }
+
+        ProxyController proxyController = ProxyController.getInstance();
+        int len = sPhones.length;
+        logdl("[setDefaultDataSubId] num phones=" + len + ", subId=" + subId);
+
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            // Only re-map modems if the new default data sub is valid
+            RadioAccessFamily[] rafs = new RadioAccessFamily[len];
+            boolean atLeastOneMatch = false;
+            for (int phoneId = 0; phoneId < len; phoneId++) {
+                Phone phone = sPhones[phoneId];
+                int raf;
+                int id = phone.getSubId();
+                if (id == subId) {
+                    // TODO Handle the general case of N modems and M subscriptions.
+                    raf = proxyController.getMaxRafSupported();
+                    atLeastOneMatch = true;
+                } else {
+                    // TODO Handle the general case of N modems and M subscriptions.
+                    raf = proxyController.getMinRafSupported();
+                }
+                logdl("[setDefaultDataSubId] phoneId=" + phoneId + " subId=" + id + " RAF=" + raf);
+                rafs[phoneId] = new RadioAccessFamily(phoneId, raf);
+            }
+            if (atLeastOneMatch) {
+                proxyController.setRadioCapability(rafs);
+            } else {
+                if (DBG) logdl("[setDefaultDataSubId] no valid subId's found - not updating.");
+            }
+        }
+
+        // FIXME is this still needed?
+        updateAllDataConnectionTrackers();
+
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION, subId);
+        broadcastDefaultDataSubIdChanged(subId);
+    }
+
+    private void updateAllDataConnectionTrackers() {
+        // Tell Phone Proxies to update data connection tracker
+        int len = sPhones.length;
+        if (DBG) logdl("[updateAllDataConnectionTrackers] sPhones.length=" + len);
+        for (int phoneId = 0; phoneId < len; phoneId++) {
+            if (DBG) logdl("[updateAllDataConnectionTrackers] phoneId=" + phoneId);
+            sPhones[phoneId].updateDataConnectionTracker();
+        }
+    }
+
+    private void broadcastDefaultDataSubIdChanged(int subId) {
+        // Broadcast an Intent for default data sub change
+        if (DBG) logdl("[broadcastDefaultDataSubIdChanged] subId=" + subId);
+        Intent intent = new Intent(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, subId);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    /* Sets the default subscription. If only one sub is active that
+     * sub is set as default subId. If two or more  sub's are active
+     * the first sub is set as default subscription
+     */
+    private void setDefaultFallbackSubId(int subId) {
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            throw new RuntimeException("setDefaultSubId called with DEFAULT_SUB_ID");
+        }
+        if (DBG) logdl("[setDefaultFallbackSubId] subId=" + subId);
+        if (SubscriptionManager.isValidSubscriptionId(subId)) {
+            int phoneId = getPhoneId(subId);
+            if (phoneId >= 0 && (phoneId < mTelephonyManager.getPhoneCount()
+                    || mTelephonyManager.getSimCount() == 1)) {
+                if (DBG) logdl("[setDefaultFallbackSubId] set mDefaultFallbackSubId=" + subId);
+                mDefaultFallbackSubId = subId;
+                // Update MCC MNC device configuration information
+                String defaultMccMnc = mTelephonyManager.getSimOperatorNumericForPhone(phoneId);
+                MccTable.updateMccMncConfiguration(mContext, defaultMccMnc, false);
+
+                // Broadcast an Intent for default sub change
+                Intent intent = new Intent(TelephonyIntents.ACTION_DEFAULT_SUBSCRIPTION_CHANGED);
+                intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+                SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId, subId);
+                if (DBG) {
+                    logdl("[setDefaultFallbackSubId] broadcast default subId changed phoneId=" +
+                            phoneId + " subId=" + subId);
+                }
+                mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+            } else {
+                if (DBG) {
+                    logdl("[setDefaultFallbackSubId] not set invalid phoneId=" + phoneId
+                            + " subId=" + subId);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void clearDefaultsForInactiveSubIds() {
+        enforceModifyPhoneState("clearDefaultsForInactiveSubIds");
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final List<SubscriptionInfo> records = getActiveSubscriptionInfoList(
+                    mContext.getOpPackageName());
+            if (DBG) logdl("[clearDefaultsForInactiveSubIds] records: " + records);
+            if (shouldDefaultBeCleared(records, getDefaultDataSubId())) {
+                if (DBG) logd("[clearDefaultsForInactiveSubIds] clearing default data sub id");
+                setDefaultDataSubId(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+            }
+            if (shouldDefaultBeCleared(records, getDefaultSmsSubId())) {
+                if (DBG) logdl("[clearDefaultsForInactiveSubIds] clearing default sms sub id");
+                setDefaultSmsSubId(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+            }
+            if (shouldDefaultBeCleared(records, getDefaultVoiceSubId())) {
+                if (DBG) logdl("[clearDefaultsForInactiveSubIds] clearing default voice sub id");
+                setDefaultVoiceSubId(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private boolean shouldDefaultBeCleared(List<SubscriptionInfo> records, int subId) {
+        if (DBG) logdl("[shouldDefaultBeCleared: subId] " + subId);
+        if (records == null) {
+            if (DBG) logdl("[shouldDefaultBeCleared] return true no records subId=" + subId);
+            return true;
+        }
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            // If the subId parameter is not valid its already cleared so return false.
+            if (DBG) logdl("[shouldDefaultBeCleared] return false only one subId, subId=" + subId);
+            return false;
+        }
+        for (SubscriptionInfo record : records) {
+            int id = record.getSubscriptionId();
+            if (DBG) logdl("[shouldDefaultBeCleared] Record.id: " + id);
+            if (id == subId) {
+                logdl("[shouldDefaultBeCleared] return false subId is active, subId=" + subId);
+                return false;
+            }
+        }
+        if (DBG) logdl("[shouldDefaultBeCleared] return true not active subId=" + subId);
+        return true;
+    }
+
+    // FIXME: We need we should not be assuming phoneId == slotIndex as it will not be true
+    // when there are multiple subscriptions per sim and probably for other reasons.
+    public int getSubIdUsingPhoneId(int phoneId) {
+        int[] subIds = getSubId(phoneId);
+        if (subIds == null || subIds.length == 0) {
+            return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        }
+        return subIds[0];
+    }
+
+    public List<SubscriptionInfo> getSubInfoUsingSlotIndexWithCheck(int slotIndex,
+                                                                    boolean needCheck,
+                                                                    String callingPackage) {
+        if (DBG) logd("[getSubInfoUsingSlotIndexWithCheck]+ slotIndex:" + slotIndex);
+        if (!canReadPhoneState(callingPackage, "getSubInfoUsingSlotIndexWithCheck")) {
+            return null;
+        }
+
+        // Now that all security checks passes, perform the operation as ourselves.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (slotIndex == SubscriptionManager.DEFAULT_SIM_SLOT_INDEX) {
+                slotIndex = getSlotIndex(getDefaultSubId());
+            }
+            if (!SubscriptionManager.isValidSlotIndex(slotIndex)) {
+                if (DBG) logd("[getSubInfoUsingSlotIndexWithCheck]- invalid slotIndex");
+                return null;
+            }
+
+            if (needCheck && !isSubInfoReady()) {
+                if (DBG) logd("[getSubInfoUsingSlotIndexWithCheck]- not ready");
+                return null;
+            }
+
+            Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
+                    null, SubscriptionManager.SIM_SLOT_INDEX + "=?",
+                    new String[]{String.valueOf(slotIndex)}, null);
+            ArrayList<SubscriptionInfo> subList = null;
+            try {
+                if (cursor != null) {
+                    while (cursor.moveToNext()) {
+                        SubscriptionInfo subInfo = getSubInfoRecord(cursor);
+                        if (subInfo != null) {
+                            if (subList == null) {
+                                subList = new ArrayList<SubscriptionInfo>();
+                            }
+                            subList.add(subInfo);
+                        }
+                    }
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+            if (DBG) logd("[getSubInfoUsingSlotIndex]- null info return");
+
+            return subList;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private void validateSubId(int subId) {
+        if (DBG) logd("validateSubId subId: " + subId);
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new RuntimeException("Invalid sub id passed as parameter");
+        } else if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            throw new RuntimeException("Default sub id passed as parameter");
+        }
+    }
+
+    public void updatePhonesAvailability(Phone[] phones) {
+        sPhones = phones;
+    }
+
+    /**
+     * @return the list of subId's that are active, is never null but the length maybe 0.
+     */
+    @Override
+    public int[] getActiveSubIdList() {
+        Set<Entry<Integer, Integer>> simInfoSet = new HashSet<>(sSlotIndexToSubId.entrySet());
+
+        int[] subIdArr = new int[simInfoSet.size()];
+        int i = 0;
+        for (Entry<Integer, Integer> entry: simInfoSet) {
+            int sub = entry.getValue();
+            subIdArr[i] = sub;
+            i++;
+        }
+
+        if (VDBG) {
+            logdl("[getActiveSubIdList] simInfoSet=" + simInfoSet + " subIdArr.length="
+                    + subIdArr.length);
+        }
+        return subIdArr;
+    }
+
+    @Override
+    public boolean isActiveSubId(int subId) {
+        boolean retVal = SubscriptionManager.isValidSubscriptionId(subId)
+                && sSlotIndexToSubId.containsValue(subId);
+
+        if (VDBG) logdl("[isActiveSubId]- " + retVal);
+        return retVal;
+    }
+
+    /**
+     * Get the SIM state for the slot index
+     * @return SIM state as the ordinal of {@See IccCardConstants.State}
+     */
+    @Override
+    public int getSimStateForSlotIndex(int slotIndex) {
+        State simState;
+        String err;
+        if (slotIndex < 0) {
+            simState = IccCardConstants.State.UNKNOWN;
+            err = "invalid slotIndex";
+        } else {
+            Phone phone = PhoneFactory.getPhone(slotIndex);
+            if (phone == null) {
+                simState = IccCardConstants.State.UNKNOWN;
+                err = "phone == null";
+            } else {
+                IccCard icc = phone.getIccCard();
+                if (icc == null) {
+                    simState = IccCardConstants.State.UNKNOWN;
+                    err = "icc == null";
+                } else {
+                    simState = icc.getState();
+                    err = "";
+                }
+            }
+        }
+        if (VDBG) {
+            logd("getSimStateForSlotIndex: " + err + " simState=" + simState
+                    + " ordinal=" + simState.ordinal() + " slotIndex=" + slotIndex);
+        }
+        return simState.ordinal();
+    }
+
+    /**
+     * Store properties associated with SubscriptionInfo in database
+     * @param subId Subscription Id of Subscription
+     * @param propKey Column name in database associated with SubscriptionInfo
+     * @param propValue Value to store in DB for particular subId & column name
+     * @hide
+     */
+    @Override
+    public void setSubscriptionProperty(int subId, String propKey, String propValue) {
+        enforceModifyPhoneState("setSubscriptionProperty");
+        final long token = Binder.clearCallingIdentity();
+        ContentResolver resolver = mContext.getContentResolver();
+        ContentValues value = new ContentValues();
+        switch (propKey) {
+            case SubscriptionManager.CB_EXTREME_THREAT_ALERT:
+            case SubscriptionManager.CB_SEVERE_THREAT_ALERT:
+            case SubscriptionManager.CB_AMBER_ALERT:
+            case SubscriptionManager.CB_EMERGENCY_ALERT:
+            case SubscriptionManager.CB_ALERT_SOUND_DURATION:
+            case SubscriptionManager.CB_ALERT_REMINDER_INTERVAL:
+            case SubscriptionManager.CB_ALERT_VIBRATE:
+            case SubscriptionManager.CB_ALERT_SPEECH:
+            case SubscriptionManager.CB_ETWS_TEST_ALERT:
+            case SubscriptionManager.CB_CHANNEL_50_ALERT:
+            case SubscriptionManager.CB_CMAS_TEST_ALERT:
+            case SubscriptionManager.CB_OPT_OUT_DIALOG:
+                value.put(propKey, Integer.parseInt(propValue));
+                break;
+            default:
+                if(DBG) logd("Invalid column name");
+                break;
+        }
+
+        resolver.update(SubscriptionManager.CONTENT_URI, value,
+                SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID +
+                        "=" + Integer.toString(subId), null);
+
+        // Refresh the Cache of Active Subscription Info List
+        refreshCachedActiveSubscriptionInfoList();
+
+        Binder.restoreCallingIdentity(token);
+    }
+
+    /**
+     * Store properties associated with SubscriptionInfo in database
+     * @param subId Subscription Id of Subscription
+     * @param propKey Column name in SubscriptionInfo database
+     * @return Value associated with subId and propKey column in database
+     * @hide
+     */
+    @Override
+    public String getSubscriptionProperty(int subId, String propKey, String callingPackage) {
+        if (!canReadPhoneState(callingPackage, "getSubInfoUsingSlotIndexWithCheck")) {
+            return null;
+        }
+        String resultValue = null;
+        ContentResolver resolver = mContext.getContentResolver();
+        Cursor cursor = resolver.query(SubscriptionManager.CONTENT_URI,
+                new String[]{propKey},
+                SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=?",
+                new String[]{subId + ""}, null);
+
+        try {
+            if (cursor != null) {
+                if (cursor.moveToFirst()) {
+                    switch (propKey) {
+                        case SubscriptionManager.CB_EXTREME_THREAT_ALERT:
+                        case SubscriptionManager.CB_SEVERE_THREAT_ALERT:
+                        case SubscriptionManager.CB_AMBER_ALERT:
+                        case SubscriptionManager.CB_EMERGENCY_ALERT:
+                        case SubscriptionManager.CB_ALERT_SOUND_DURATION:
+                        case SubscriptionManager.CB_ALERT_REMINDER_INTERVAL:
+                        case SubscriptionManager.CB_ALERT_VIBRATE:
+                        case SubscriptionManager.CB_ALERT_SPEECH:
+                        case SubscriptionManager.CB_ETWS_TEST_ALERT:
+                        case SubscriptionManager.CB_CHANNEL_50_ALERT:
+                        case SubscriptionManager.CB_CMAS_TEST_ALERT:
+                        case SubscriptionManager.CB_OPT_OUT_DIALOG:
+                            resultValue = cursor.getInt(0) + "";
+                            break;
+                        default:
+                            if(DBG) logd("Invalid column name");
+                            break;
+                    }
+                } else {
+                    if(DBG) logd("Valid row not present in db");
+                }
+            } else {
+                if(DBG) logd("Query failed");
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        if (DBG) logd("getSubscriptionProperty Query value = " + resultValue);
+        return resultValue;
+    }
+
+    private static void printStackTrace(String msg) {
+        RuntimeException re = new RuntimeException();
+        slogd("StackTrace - " + msg);
+        StackTraceElement[] st = re.getStackTrace();
+        boolean first = true;
+        for (StackTraceElement ste : st) {
+            if (first) {
+                first = false;
+            } else {
+                slogd(ste.toString());
+            }
+        }
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
+                "Requires DUMP");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            pw.println("SubscriptionController:");
+            pw.println(" defaultSubId=" + getDefaultSubId());
+            pw.println(" defaultDataSubId=" + getDefaultDataSubId());
+            pw.println(" defaultVoiceSubId=" + getDefaultVoiceSubId());
+            pw.println(" defaultSmsSubId=" + getDefaultSmsSubId());
+
+            pw.println(" defaultDataPhoneId=" + SubscriptionManager
+                    .from(mContext).getDefaultDataPhoneId());
+            pw.println(" defaultVoicePhoneId=" + SubscriptionManager.getDefaultVoicePhoneId());
+            pw.println(" defaultSmsPhoneId=" + SubscriptionManager
+                    .from(mContext).getDefaultSmsPhoneId());
+            pw.flush();
+
+            for (Entry<Integer, Integer> entry : sSlotIndexToSubId.entrySet()) {
+                pw.println(" sSlotIndexToSubId[" + entry.getKey() + "]: subId=" + entry.getValue());
+            }
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+
+            List<SubscriptionInfo> sirl = getActiveSubscriptionInfoList(
+                    mContext.getOpPackageName());
+            if (sirl != null) {
+                pw.println(" ActiveSubInfoList:");
+                for (SubscriptionInfo entry : sirl) {
+                    pw.println("  " + entry.toString());
+                }
+            } else {
+                pw.println(" ActiveSubInfoList: is null");
+            }
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+
+            sirl = getAllSubInfoList(mContext.getOpPackageName());
+            if (sirl != null) {
+                pw.println(" AllSubInfoList:");
+                for (SubscriptionInfo entry : sirl) {
+                    pw.println("  " + entry.toString());
+                }
+            } else {
+                pw.println(" AllSubInfoList: is null");
+            }
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+
+            mLocalLog.dump(fd, pw, args);
+            pw.flush();
+            pw.println("++++++++++++++++++++++++++++++++");
+            pw.flush();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/SubscriptionInfoUpdater.java b/com/android/internal/telephony/SubscriptionInfoUpdater.java
new file mode 100644
index 0000000..7e8f51e
--- /dev/null
+++ b/com/android/internal/telephony/SubscriptionInfoUpdater.java
@@ -0,0 +1,843 @@
+/*
+* Copyright (C) 2014 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.UserSwitchObserver;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.IPackageManager;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.service.euicc.EuiccProfileInfo;
+import android.service.euicc.EuiccService;
+import android.service.euicc.GetEuiccProfileInfoListResult;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccAccessRule;
+import android.telephony.euicc.EuiccManager;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.euicc.EuiccController;
+import com.android.internal.telephony.uicc.IccCardProxy;
+import com.android.internal.telephony.uicc.IccConstants;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *@hide
+ */
+public class SubscriptionInfoUpdater extends Handler {
+    private static final String LOG_TAG = "SubscriptionInfoUpdater";
+    private static final int PROJECT_SIM_NUM = TelephonyManager.getDefault().getPhoneCount();
+
+    private static final int EVENT_SIM_LOCKED_QUERY_ICCID_DONE = 1;
+    private static final int EVENT_GET_NETWORK_SELECTION_MODE_DONE = 2;
+    private static final int EVENT_SIM_LOADED = 3;
+    private static final int EVENT_SIM_ABSENT = 4;
+    private static final int EVENT_SIM_LOCKED = 5;
+    private static final int EVENT_SIM_IO_ERROR = 6;
+    private static final int EVENT_SIM_UNKNOWN = 7;
+    private static final int EVENT_SIM_RESTRICTED = 8;
+    private static final int EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS = 9;
+
+    private static final String ICCID_STRING_FOR_NO_SIM = "";
+    /**
+     *  int[] sInsertSimState maintains all slots' SIM inserted status currently,
+     *  it may contain 4 kinds of values:
+     *    SIM_NOT_INSERT : no SIM inserted in slot i now
+     *    SIM_CHANGED    : a valid SIM insert in slot i and is different SIM from last time
+     *                     it will later become SIM_NEW or SIM_REPOSITION during update procedure
+     *    SIM_NOT_CHANGE : a valid SIM insert in slot i and is the same SIM as last time
+     *    SIM_NEW        : a valid SIM insert in slot i and is a new SIM
+     *    SIM_REPOSITION : a valid SIM insert in slot i and is inserted in different slot last time
+     *    positive integer #: index to distinguish SIM cards with the same IccId
+     */
+    public static final int SIM_NOT_CHANGE = 0;
+    public static final int SIM_CHANGED    = -1;
+    public static final int SIM_NEW        = -2;
+    public static final int SIM_REPOSITION = -3;
+    public static final int SIM_NOT_INSERT = -99;
+
+    public static final int STATUS_NO_SIM_INSERTED = 0x00;
+    public static final int STATUS_SIM1_INSERTED = 0x01;
+    public static final int STATUS_SIM2_INSERTED = 0x02;
+    public static final int STATUS_SIM3_INSERTED = 0x04;
+    public static final int STATUS_SIM4_INSERTED = 0x08;
+
+    // Key used to read/write the current IMSI. Updated on SIM_STATE_CHANGED - LOADED.
+    public static final String CURR_SUBID = "curr_subid";
+
+    private static Phone[] mPhone;
+    private static Context mContext = null;
+    private static String mIccId[] = new String[PROJECT_SIM_NUM];
+    private static int[] mInsertSimState = new int[PROJECT_SIM_NUM];
+    private SubscriptionManager mSubscriptionManager = null;
+    private EuiccManager mEuiccManager;
+    private IPackageManager mPackageManager;
+
+    // The current foreground user ID.
+    private int mCurrentlyActiveUserId;
+    private CarrierServiceBindHelper mCarrierServiceBindHelper;
+
+    public SubscriptionInfoUpdater(
+            Looper looper, Context context, Phone[] phone, CommandsInterface[] ci) {
+        super(looper);
+        logd("Constructor invoked");
+
+        mContext = context;
+        mPhone = phone;
+        mSubscriptionManager = SubscriptionManager.from(mContext);
+        mEuiccManager = (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
+        mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+
+        IntentFilter intentFilter = new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+        intentFilter.addAction(IccCardProxy.ACTION_INTERNAL_SIM_STATE_CHANGED);
+        mContext.registerReceiver(sReceiver, intentFilter);
+
+        mCarrierServiceBindHelper = new CarrierServiceBindHelper(mContext);
+        initializeCarrierApps();
+    }
+
+    private void initializeCarrierApps() {
+        // Initialize carrier apps:
+        // -Now (on system startup)
+        // -Whenever new carrier privilege rules might change (new SIM is loaded)
+        // -Whenever we switch to a new user
+        mCurrentlyActiveUserId = 0;
+        try {
+            ActivityManager.getService().registerUserSwitchObserver(new UserSwitchObserver() {
+                @Override
+                public void onUserSwitching(int newUserId, IRemoteCallback reply)
+                        throws RemoteException {
+                    mCurrentlyActiveUserId = newUserId;
+                    CarrierAppUtils.disableCarrierAppsUntilPrivileged(mContext.getOpPackageName(),
+                            mPackageManager, TelephonyManager.getDefault(),
+                            mContext.getContentResolver(), mCurrentlyActiveUserId);
+
+                    if (reply != null) {
+                        try {
+                            reply.sendResult(null);
+                        } catch (RemoteException e) {
+                        }
+                    }
+                }
+            }, LOG_TAG);
+            mCurrentlyActiveUserId = ActivityManager.getService().getCurrentUser().id;
+        } catch (RemoteException e) {
+            logd("Couldn't get current user ID; guessing it's 0: " + e.getMessage());
+        }
+        CarrierAppUtils.disableCarrierAppsUntilPrivileged(mContext.getOpPackageName(),
+                mPackageManager, TelephonyManager.getDefault(), mContext.getContentResolver(),
+                mCurrentlyActiveUserId);
+    }
+
+    private final BroadcastReceiver sReceiver = new  BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            logd("[Receiver]+");
+            String action = intent.getAction();
+            logd("Action: " + action);
+
+            if (!action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED) &&
+                    !action.equals(IccCardProxy.ACTION_INTERNAL_SIM_STATE_CHANGED)) {
+                return;
+            }
+
+            int slotIndex = intent.getIntExtra(PhoneConstants.PHONE_KEY,
+                    SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+            logd("slotIndex: " + slotIndex);
+            if (!SubscriptionManager.isValidSlotIndex(slotIndex)) {
+                logd("ACTION_SIM_STATE_CHANGED contains invalid slotIndex: " + slotIndex);
+                return;
+            }
+
+            String simStatus = intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE);
+            logd("simStatus: " + simStatus);
+
+            // TODO: All of the below should be converted to ACTION_INTERNAL_SIM_STATE_CHANGED to
+            // ensure that the SubscriptionInfo is updated before the broadcasts are sent out.
+            if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
+                if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(simStatus)) {
+                    sendMessage(obtainMessage(EVENT_SIM_ABSENT, slotIndex, -1));
+                } else if (IccCardConstants.INTENT_VALUE_ICC_UNKNOWN.equals(simStatus)) {
+                    sendMessage(obtainMessage(EVENT_SIM_UNKNOWN, slotIndex, -1));
+                } else if (IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR.equals(simStatus)) {
+                    sendMessage(obtainMessage(EVENT_SIM_IO_ERROR, slotIndex, -1));
+                } else if (IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED.equals(simStatus)) {
+                    sendMessage(obtainMessage(EVENT_SIM_RESTRICTED, slotIndex, -1));
+                } else if (IccCardConstants.INTENT_VALUE_ICC_NOT_READY.equals(simStatus)) {
+                    // ICC_NOT_READY is a terminal state for an eSIM on the boot profile. At this
+                    // phase, the subscription list is accessible.
+                    // TODO(b/64216093): Clean up this special case, likely by treating NOT_READY
+                    // as equivalent to ABSENT, once the rest of the system can handle it. Currently
+                    // this breaks SystemUI which shows a "No SIM" icon.
+                    sendEmptyMessage(EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS);
+                } else {
+                    logd("Ignoring simStatus: " + simStatus);
+                }
+            } else if (action.equals(IccCardProxy.ACTION_INTERNAL_SIM_STATE_CHANGED)) {
+                if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(simStatus)) {
+                    String reason = intent.getStringExtra(
+                        IccCardConstants.INTENT_KEY_LOCKED_REASON);
+                    sendMessage(obtainMessage(EVENT_SIM_LOCKED, slotIndex, -1, reason));
+                } else if (IccCardConstants.INTENT_VALUE_ICC_LOADED.equals(simStatus)) {
+                    sendMessage(obtainMessage(EVENT_SIM_LOADED, slotIndex, -1));
+                } else {
+                    logd("Ignoring simStatus: " + simStatus);
+                }
+            }
+            logd("[Receiver]-");
+        }
+    };
+
+    private boolean isAllIccIdQueryDone() {
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            if (mIccId[i] == null) {
+                logd("Wait for SIM" + (i + 1) + " IccId");
+                return false;
+            }
+        }
+        logd("All IccIds query complete");
+
+        return true;
+    }
+
+    public void setDisplayNameForNewSub(String newSubName, int subId, int newNameSource) {
+        SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo(subId);
+        if (subInfo != null) {
+            // overwrite SIM display name if it is not assigned by user
+            int oldNameSource = subInfo.getNameSource();
+            CharSequence oldSubName = subInfo.getDisplayName();
+            logd("[setDisplayNameForNewSub] subId = " + subInfo.getSubscriptionId()
+                    + ", oldSimName = " + oldSubName + ", oldNameSource = " + oldNameSource
+                    + ", newSubName = " + newSubName + ", newNameSource = " + newNameSource);
+            if (oldSubName == null ||
+                (oldNameSource ==
+                    SubscriptionManager.NAME_SOURCE_DEFAULT_SOURCE && newSubName != null) ||
+                (oldNameSource == SubscriptionManager.NAME_SOURCE_SIM_SOURCE && newSubName != null
+                        && !newSubName.equals(oldSubName))) {
+                mSubscriptionManager.setDisplayName(newSubName, subInfo.getSubscriptionId(),
+                        newNameSource);
+            }
+        } else {
+            logd("SUB" + (subId + 1) + " SubInfo not created yet");
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case EVENT_SIM_LOCKED_QUERY_ICCID_DONE: {
+                AsyncResult ar = (AsyncResult)msg.obj;
+                QueryIccIdUserObj uObj = (QueryIccIdUserObj) ar.userObj;
+                int slotId = uObj.slotId;
+                logd("handleMessage : <EVENT_SIM_LOCKED_QUERY_ICCID_DONE> SIM" + (slotId + 1));
+                if (ar.exception == null) {
+                    if (ar.result != null) {
+                        byte[] data = (byte[])ar.result;
+                        mIccId[slotId] = IccUtils.bcdToString(data, 0, data.length);
+                    } else {
+                        logd("Null ar");
+                        mIccId[slotId] = ICCID_STRING_FOR_NO_SIM;
+                    }
+                } else {
+                    mIccId[slotId] = ICCID_STRING_FOR_NO_SIM;
+                    logd("Query IccId fail: " + ar.exception);
+                }
+                logd("sIccId[" + slotId + "] = " + mIccId[slotId]);
+                if (isAllIccIdQueryDone()) {
+                    updateSubscriptionInfoByIccId();
+                }
+                broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED,
+                                         uObj.reason);
+                if (!ICCID_STRING_FOR_NO_SIM.equals(mIccId[slotId])) {
+                    updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED);
+                }
+                break;
+            }
+
+            case EVENT_GET_NETWORK_SELECTION_MODE_DONE: {
+                AsyncResult ar = (AsyncResult)msg.obj;
+                Integer slotId = (Integer)ar.userObj;
+                if (ar.exception == null && ar.result != null) {
+                    int[] modes = (int[])ar.result;
+                    if (modes[0] == 1) {  // Manual mode.
+                        mPhone[slotId].setNetworkSelectionModeAutomatic(null);
+                    }
+                } else {
+                    logd("EVENT_GET_NETWORK_SELECTION_MODE_DONE: error getting network mode.");
+                }
+                break;
+            }
+
+           case EVENT_SIM_LOADED:
+                handleSimLoaded(msg.arg1);
+                break;
+
+            case EVENT_SIM_ABSENT:
+                handleSimAbsent(msg.arg1);
+                break;
+
+            case EVENT_SIM_LOCKED:
+                handleSimLocked(msg.arg1, (String) msg.obj);
+                break;
+
+            case EVENT_SIM_UNKNOWN:
+                updateCarrierServices(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_UNKNOWN);
+                break;
+
+            case EVENT_SIM_IO_ERROR:
+                handleSimError(msg.arg1);
+                break;
+
+            case EVENT_SIM_RESTRICTED:
+                updateCarrierServices(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED);
+                break;
+
+            case EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS:
+                if (updateEmbeddedSubscriptions()) {
+                    SubscriptionController.getInstance().notifySubscriptionInfoChanged();
+                }
+                if (msg.obj != null) {
+                    ((Runnable) msg.obj).run();
+                }
+                break;
+
+            default:
+                logd("Unknown msg:" + msg.what);
+        }
+    }
+
+    void requestEmbeddedSubscriptionInfoListRefresh(@Nullable Runnable callback) {
+        sendMessage(obtainMessage(EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS, callback));
+    }
+
+    private static class QueryIccIdUserObj {
+        public String reason;
+        public int slotId;
+
+        QueryIccIdUserObj(String reason, int slotId) {
+            this.reason = reason;
+            this.slotId = slotId;
+        }
+    };
+
+    private void handleSimLocked(int slotId, String reason) {
+        if (mIccId[slotId] != null && mIccId[slotId].equals(ICCID_STRING_FOR_NO_SIM)) {
+            logd("SIM" + (slotId + 1) + " hot plug in");
+            mIccId[slotId] = null;
+        }
+
+
+        IccFileHandler fileHandler = mPhone[slotId].getIccCard() == null ? null :
+                mPhone[slotId].getIccCard().getIccFileHandler();
+
+        if (fileHandler != null) {
+            String iccId = mIccId[slotId];
+            if (iccId == null) {
+                logd("Querying IccId");
+                fileHandler.loadEFTransparent(IccConstants.EF_ICCID,
+                        obtainMessage(EVENT_SIM_LOCKED_QUERY_ICCID_DONE,
+                                new QueryIccIdUserObj(reason, slotId)));
+            } else {
+                logd("NOT Querying IccId its already set sIccid[" + slotId + "]=" + iccId);
+                updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED);
+                broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED, reason);
+            }
+        } else {
+            logd("sFh[" + slotId + "] is null, ignore");
+        }
+    }
+
+    private void handleSimLoaded(int slotId) {
+        logd("handleSimLoaded: slotId: " + slotId);
+
+        // The SIM should be loaded at this state, but it is possible in cases such as SIM being
+        // removed or a refresh RESET that the IccRecords could be null. The right behavior is to
+        // not broadcast the SIM loaded.
+        IccRecords records = mPhone[slotId].getIccCard().getIccRecords();
+        if (records == null) {  // Possibly a race condition.
+            logd("handleSimLoaded: IccRecords null");
+            return;
+        }
+        if (records.getIccId() == null) {
+            logd("handleSimLoaded: IccID null");
+            return;
+        }
+        mIccId[slotId] = records.getIccId();
+
+        if (isAllIccIdQueryDone()) {
+            updateSubscriptionInfoByIccId();
+            int[] subIds = mSubscriptionManager.getActiveSubscriptionIdList();
+            for (int subId : subIds) {
+                TelephonyManager tm = TelephonyManager.getDefault();
+
+                String operator = tm.getSimOperatorNumeric(subId);
+                slotId = SubscriptionController.getInstance().getPhoneId(subId);
+
+                if (!TextUtils.isEmpty(operator)) {
+                    if (subId == SubscriptionController.getInstance().getDefaultSubId()) {
+                        MccTable.updateMccMncConfiguration(mContext, operator, false);
+                    }
+                    SubscriptionController.getInstance().setMccMnc(operator, subId);
+                } else {
+                    logd("EVENT_RECORDS_LOADED Operator name is null");
+                }
+
+                String msisdn = tm.getLine1Number(subId);
+                ContentResolver contentResolver = mContext.getContentResolver();
+
+                if (msisdn != null) {
+                    ContentValues number = new ContentValues(1);
+                    number.put(SubscriptionManager.NUMBER, msisdn);
+                    contentResolver.update(SubscriptionManager.CONTENT_URI, number,
+                            SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "="
+                                    + Long.toString(subId), null);
+
+                    // refresh Cached Active Subscription Info List
+                    SubscriptionController.getInstance().refreshCachedActiveSubscriptionInfoList();
+                }
+
+                SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo(subId);
+                String nameToSet;
+                String simCarrierName = tm.getSimOperatorName(subId);
+                ContentValues name = new ContentValues(1);
+
+                if (subInfo != null && subInfo.getNameSource() !=
+                        SubscriptionManager.NAME_SOURCE_USER_INPUT) {
+                    if (!TextUtils.isEmpty(simCarrierName)) {
+                        nameToSet = simCarrierName;
+                    } else {
+                        nameToSet = "CARD " + Integer.toString(slotId + 1);
+                    }
+                    name.put(SubscriptionManager.DISPLAY_NAME, nameToSet);
+                    logd("sim name = " + nameToSet);
+                    contentResolver.update(SubscriptionManager.CONTENT_URI, name,
+                            SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID
+                                    + "=" + Long.toString(subId), null);
+
+                    // refresh Cached Active Subscription Info List
+                    SubscriptionController.getInstance().refreshCachedActiveSubscriptionInfoList();
+                }
+
+                /* Update preferred network type and network selection mode on SIM change.
+                 * Storing last subId in SharedPreference for now to detect SIM change. */
+                SharedPreferences sp =
+                        PreferenceManager.getDefaultSharedPreferences(mContext);
+                int storedSubId = sp.getInt(CURR_SUBID + slotId, -1);
+
+                if (storedSubId != subId) {
+                    int networkType = RILConstants.PREFERRED_NETWORK_MODE;
+
+                    // Set the modem network mode
+                    mPhone[slotId].setPreferredNetworkType(networkType, null);
+                    Settings.Global.putInt(mPhone[slotId].getContext().getContentResolver(),
+                            Settings.Global.PREFERRED_NETWORK_MODE + subId,
+                            networkType);
+
+                    // Only support automatic selection mode on SIM change.
+                    mPhone[slotId].getNetworkSelectionMode(
+                            obtainMessage(EVENT_GET_NETWORK_SELECTION_MODE_DONE,
+                                    new Integer(slotId)));
+
+                    // Update stored subId
+                    SharedPreferences.Editor editor = sp.edit();
+                    editor.putInt(CURR_SUBID + slotId, subId);
+                    editor.apply();
+                }
+
+                // Update set of enabled carrier apps now that the privilege rules may have changed.
+                CarrierAppUtils.disableCarrierAppsUntilPrivileged(mContext.getOpPackageName(),
+                        mPackageManager, TelephonyManager.getDefault(),
+                        mContext.getContentResolver(), mCurrentlyActiveUserId);
+
+                broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOADED, null);
+                updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOADED);
+            }
+        }
+    }
+
+    private void updateCarrierServices(int slotId, String simState) {
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        configManager.updateConfigForPhoneId(slotId, simState);
+        mCarrierServiceBindHelper.updateForPhoneId(slotId, simState);
+    }
+
+    private void handleSimAbsent(int slotId) {
+        if (mIccId[slotId] != null && !mIccId[slotId].equals(ICCID_STRING_FOR_NO_SIM)) {
+            logd("SIM" + (slotId + 1) + " hot plug out");
+        }
+        mIccId[slotId] = ICCID_STRING_FOR_NO_SIM;
+        if (isAllIccIdQueryDone()) {
+            updateSubscriptionInfoByIccId();
+        }
+        updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_ABSENT);
+    }
+
+    private void handleSimError(int slotId) {
+        if (mIccId[slotId] != null && !mIccId[slotId].equals(ICCID_STRING_FOR_NO_SIM)) {
+            logd("SIM" + (slotId + 1) + " Error ");
+        }
+        mIccId[slotId] = ICCID_STRING_FOR_NO_SIM;
+        if (isAllIccIdQueryDone()) {
+            updateSubscriptionInfoByIccId();
+        }
+        updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR);
+    }
+
+    /**
+     * TODO: Simplify more, as no one is interested in what happened
+     * only what the current list contains.
+     */
+    synchronized private void updateSubscriptionInfoByIccId() {
+        logd("updateSubscriptionInfoByIccId:+ Start");
+
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            mInsertSimState[i] = SIM_NOT_CHANGE;
+        }
+
+        int insertedSimCount = PROJECT_SIM_NUM;
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            if (ICCID_STRING_FOR_NO_SIM.equals(mIccId[i])) {
+                insertedSimCount--;
+                mInsertSimState[i] = SIM_NOT_INSERT;
+            }
+        }
+        logd("insertedSimCount = " + insertedSimCount);
+
+        // We only clear the slot-to-sub map when one/some SIM was removed. Note this is a
+        // workaround for some race conditions that the empty map was accessed while we are
+        // rebuilding the map.
+        if (SubscriptionController.getInstance().getActiveSubIdList().length > insertedSimCount) {
+            SubscriptionController.getInstance().clearSubInfo();
+        }
+
+        int index = 0;
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            if (mInsertSimState[i] == SIM_NOT_INSERT) {
+                continue;
+            }
+            index = 2;
+            for (int j = i + 1; j < PROJECT_SIM_NUM; j++) {
+                if (mInsertSimState[j] == SIM_NOT_CHANGE && mIccId[i].equals(mIccId[j])) {
+                    mInsertSimState[i] = 1;
+                    mInsertSimState[j] = index;
+                    index++;
+                }
+            }
+        }
+
+        ContentResolver contentResolver = mContext.getContentResolver();
+        String[] oldIccId = new String[PROJECT_SIM_NUM];
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            oldIccId[i] = null;
+            List<SubscriptionInfo> oldSubInfo =
+                    SubscriptionController.getInstance().getSubInfoUsingSlotIndexWithCheck(i, false,
+                    mContext.getOpPackageName());
+            if (oldSubInfo != null && oldSubInfo.size() > 0) {
+                oldIccId[i] = oldSubInfo.get(0).getIccId();
+                logd("updateSubscriptionInfoByIccId: oldSubId = "
+                        + oldSubInfo.get(0).getSubscriptionId());
+                if (mInsertSimState[i] == SIM_NOT_CHANGE && !mIccId[i].equals(oldIccId[i])) {
+                    mInsertSimState[i] = SIM_CHANGED;
+                }
+                if (mInsertSimState[i] != SIM_NOT_CHANGE) {
+                    ContentValues value = new ContentValues(1);
+                    value.put(SubscriptionManager.SIM_SLOT_INDEX,
+                            SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+                    contentResolver.update(SubscriptionManager.CONTENT_URI, value,
+                            SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "="
+                            + Integer.toString(oldSubInfo.get(0).getSubscriptionId()), null);
+
+                    // refresh Cached Active Subscription Info List
+                    SubscriptionController.getInstance().refreshCachedActiveSubscriptionInfoList();
+                }
+            } else {
+                if (mInsertSimState[i] == SIM_NOT_CHANGE) {
+                    // no SIM inserted last time, but there is one SIM inserted now
+                    mInsertSimState[i] = SIM_CHANGED;
+                }
+                oldIccId[i] = ICCID_STRING_FOR_NO_SIM;
+                logd("updateSubscriptionInfoByIccId: No SIM in slot " + i + " last time");
+            }
+        }
+
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            logd("updateSubscriptionInfoByIccId: oldIccId[" + i + "] = " + oldIccId[i] +
+                    ", sIccId[" + i + "] = " + mIccId[i]);
+        }
+
+        //check if the inserted SIM is new SIM
+        int nNewCardCount = 0;
+        int nNewSimStatus = 0;
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            if (mInsertSimState[i] == SIM_NOT_INSERT) {
+                logd("updateSubscriptionInfoByIccId: No SIM inserted in slot " + i + " this time");
+            } else {
+                if (mInsertSimState[i] > 0) {
+                    //some special SIMs may have the same IccIds, add suffix to distinguish them
+                    //FIXME: addSubInfoRecord can return an error.
+                    mSubscriptionManager.addSubscriptionInfoRecord(mIccId[i]
+                            + Integer.toString(mInsertSimState[i]), i);
+                    logd("SUB" + (i + 1) + " has invalid IccId");
+                } else /*if (sInsertSimState[i] != SIM_NOT_INSERT)*/ {
+                    mSubscriptionManager.addSubscriptionInfoRecord(mIccId[i], i);
+                }
+                if (isNewSim(mIccId[i], oldIccId)) {
+                    nNewCardCount++;
+                    switch (i) {
+                        case PhoneConstants.SUB1:
+                            nNewSimStatus |= STATUS_SIM1_INSERTED;
+                            break;
+                        case PhoneConstants.SUB2:
+                            nNewSimStatus |= STATUS_SIM2_INSERTED;
+                            break;
+                        case PhoneConstants.SUB3:
+                            nNewSimStatus |= STATUS_SIM3_INSERTED;
+                            break;
+                        //case PhoneConstants.SUB3:
+                        //    nNewSimStatus |= STATUS_SIM4_INSERTED;
+                        //    break;
+                    }
+
+                    mInsertSimState[i] = SIM_NEW;
+                }
+            }
+        }
+
+        for (int i = 0; i < PROJECT_SIM_NUM; i++) {
+            if (mInsertSimState[i] == SIM_CHANGED) {
+                mInsertSimState[i] = SIM_REPOSITION;
+            }
+            logd("updateSubscriptionInfoByIccId: sInsertSimState[" + i + "] = "
+                    + mInsertSimState[i]);
+        }
+
+        List<SubscriptionInfo> subInfos = mSubscriptionManager.getActiveSubscriptionInfoList();
+        int nSubCount = (subInfos == null) ? 0 : subInfos.size();
+        logd("updateSubscriptionInfoByIccId: nSubCount = " + nSubCount);
+        for (int i=0; i < nSubCount; i++) {
+            SubscriptionInfo temp = subInfos.get(i);
+
+            String msisdn = TelephonyManager.getDefault().getLine1Number(
+                    temp.getSubscriptionId());
+
+            if (msisdn != null) {
+                ContentValues value = new ContentValues(1);
+                value.put(SubscriptionManager.NUMBER, msisdn);
+                contentResolver.update(SubscriptionManager.CONTENT_URI, value,
+                        SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "="
+                        + Integer.toString(temp.getSubscriptionId()), null);
+
+                // refresh Cached Active Subscription Info List
+                SubscriptionController.getInstance().refreshCachedActiveSubscriptionInfoList();
+            }
+        }
+
+        // Ensure the modems are mapped correctly
+        mSubscriptionManager.setDefaultDataSubId(
+                mSubscriptionManager.getDefaultDataSubscriptionId());
+
+        // No need to check return value here as we notify for the above changes anyway.
+        updateEmbeddedSubscriptions();
+
+        SubscriptionController.getInstance().notifySubscriptionInfoChanged();
+        logd("updateSubscriptionInfoByIccId:- SubscriptionInfo update complete");
+    }
+
+    /**
+     * Update the cached list of embedded subscriptions.
+     *
+     * @return true if changes may have been made. This is not a guarantee that changes were made,
+     * but notifications about subscription changes may be skipped if this returns false as an
+     * optimization to avoid spurious notifications.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public boolean updateEmbeddedSubscriptions() {
+        // Do nothing if eUICCs are disabled. (Previous entries may remain in the cache, but they
+        // are filtered out of list calls as long as EuiccManager.isEnabled returns false).
+        if (!mEuiccManager.isEnabled()) {
+            return false;
+        }
+
+        GetEuiccProfileInfoListResult result =
+                EuiccController.get().blockingGetEuiccProfileInfoList();
+        if (result == null) {
+            // IPC to the eUICC controller failed.
+            return false;
+        }
+
+        final EuiccProfileInfo[] embeddedProfiles;
+        if (result.result == EuiccService.RESULT_OK) {
+            embeddedProfiles = result.profiles;
+        } else {
+            logd("updatedEmbeddedSubscriptions: error " + result.result + " listing profiles");
+            // If there's an error listing profiles, treat it equivalently to a successful
+            // listing which returned no profiles under the assumption that none are currently
+            // accessible.
+            embeddedProfiles = new EuiccProfileInfo[0];
+        }
+        final boolean isRemovable = result.isRemovable;
+
+        final String[] embeddedIccids = new String[embeddedProfiles.length];
+        for (int i = 0; i < embeddedProfiles.length; i++) {
+            embeddedIccids[i] = embeddedProfiles[i].iccid;
+        }
+
+        // Note that this only tracks whether we make any writes to the DB. It's possible this will
+        // be set to true for an update even when the row contents remain exactly unchanged from
+        // before, since we don't compare against the previous value. Since this is only intended to
+        // avoid some spurious broadcasts (particularly for users who don't use eSIM at all), this
+        // is fine.
+        boolean hasChanges = false;
+
+        // Update or insert records for all embedded subscriptions (except non-removable ones if the
+        // current eUICC is non-removable, since we assume these are still accessible though not
+        // returned by the eUICC controller).
+        List<SubscriptionInfo> existingSubscriptions = SubscriptionController.getInstance()
+                .getSubscriptionInfoListForEmbeddedSubscriptionUpdate(embeddedIccids, isRemovable);
+        ContentResolver contentResolver = mContext.getContentResolver();
+        for (EuiccProfileInfo embeddedProfile : embeddedProfiles) {
+            int index =
+                    findSubscriptionInfoForIccid(existingSubscriptions, embeddedProfile.iccid);
+            if (index < 0) {
+                // No existing entry for this ICCID; create an empty one.
+                SubscriptionController.getInstance().insertEmptySubInfoRecord(
+                        embeddedProfile.iccid, SubscriptionManager.SIM_NOT_INSERTED);
+            } else {
+                existingSubscriptions.remove(index);
+            }
+            ContentValues values = new ContentValues();
+            values.put(SubscriptionManager.IS_EMBEDDED, 1);
+            values.put(SubscriptionManager.ACCESS_RULES,
+                    embeddedProfile.accessRules == null ? null :
+                            UiccAccessRule.encodeRules(embeddedProfile.accessRules));
+            values.put(SubscriptionManager.IS_REMOVABLE, isRemovable);
+            values.put(SubscriptionManager.DISPLAY_NAME, embeddedProfile.nickname);
+            values.put(SubscriptionManager.NAME_SOURCE, SubscriptionManager.NAME_SOURCE_USER_INPUT);
+            hasChanges = true;
+            contentResolver.update(SubscriptionManager.CONTENT_URI, values,
+                    SubscriptionManager.ICC_ID + "=\"" + embeddedProfile.iccid + "\"", null);
+
+            // refresh Cached Active Subscription Info List
+            SubscriptionController.getInstance().refreshCachedActiveSubscriptionInfoList();
+        }
+
+        // Remove all remaining subscriptions which have embedded = true. We set embedded to false
+        // to ensure they are not returned in the list of embedded subscriptions (but keep them
+        // around in case the subscription is added back later, which is equivalent to a removable
+        // SIM being removed and reinserted).
+        if (!existingSubscriptions.isEmpty()) {
+            List<String> iccidsToRemove = new ArrayList<>();
+            for (int i = 0; i < existingSubscriptions.size(); i++) {
+                SubscriptionInfo info = existingSubscriptions.get(i);
+                if (info.isEmbedded()) {
+                    iccidsToRemove.add("\"" + info.getIccId() + "\"");
+                }
+            }
+            String whereClause = SubscriptionManager.ICC_ID + " IN ("
+                    + TextUtils.join(",", iccidsToRemove) + ")";
+            ContentValues values = new ContentValues();
+            values.put(SubscriptionManager.IS_EMBEDDED, 0);
+            hasChanges = true;
+            contentResolver.update(SubscriptionManager.CONTENT_URI, values, whereClause, null);
+
+            // refresh Cached Active Subscription Info List
+            SubscriptionController.getInstance().refreshCachedActiveSubscriptionInfoList();
+        }
+
+        return hasChanges;
+    }
+
+    private static int findSubscriptionInfoForIccid(List<SubscriptionInfo> list, String iccid) {
+        for (int i = 0; i < list.size(); i++) {
+            if (TextUtils.equals(iccid, list.get(i).getIccId())) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private boolean isNewSim(String iccId, String[] oldIccId) {
+        boolean newSim = true;
+        for(int i = 0; i < PROJECT_SIM_NUM; i++) {
+            if(iccId.equals(oldIccId[i])) {
+                newSim = false;
+                break;
+            }
+        }
+        logd("newSim = " + newSim);
+
+        return newSim;
+    }
+
+    private void broadcastSimStateChanged(int slotId, String state, String reason) {
+        Intent i = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+        // TODO - we'd like this intent to have a single snapshot of all sim state,
+        // but until then this should not use REPLACE_PENDING or we may lose
+        // information
+        // i.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+        //         | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        i.putExtra(PhoneConstants.PHONE_NAME_KEY, "Phone");
+        i.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, state);
+        i.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason);
+        SubscriptionManager.putPhoneIdAndSubIdExtra(i, slotId);
+        logd("Broadcasting intent ACTION_SIM_STATE_CHANGED " + state + " reason " + reason +
+             " for mCardIndex: " + slotId);
+        IntentBroadcaster.getInstance().broadcastStickyIntent(i, slotId);
+    }
+
+    public void dispose() {
+        logd("[dispose]");
+        mContext.unregisterReceiver(sReceiver);
+    }
+
+    private void logd(String message) {
+        Rlog.d(LOG_TAG, message);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("SubscriptionInfoUpdater:");
+        mCarrierServiceBindHelper.dump(fd, pw, args);
+    }
+}
diff --git a/com/android/internal/telephony/SubscriptionMonitor.java b/com/android/internal/telephony/SubscriptionMonitor.java
new file mode 100644
index 0000000..4307875
--- /dev/null
+++ b/com/android/internal/telephony/SubscriptionMonitor.java
@@ -0,0 +1,239 @@
+/*
+* Copyright (C) 2015 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.internal.telephony;
+
+import static android.telephony.SubscriptionManager.INVALID_PHONE_INDEX;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.ISub;
+import com.android.internal.telephony.IOnSubscriptionsChangedListener;
+import com.android.internal.telephony.ITelephonyRegistry;
+import com.android.internal.telephony.PhoneConstants;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.IllegalArgumentException;
+
+/**
+ * Utility singleton to monitor subscription changes and help people act on them.
+ * Uses Registrant model to post messages to handlers.
+ *
+ */
+public class SubscriptionMonitor {
+
+    private final RegistrantList mSubscriptionsChangedRegistrants[];
+    private final RegistrantList mDefaultDataSubChangedRegistrants[];
+
+    private final SubscriptionController mSubscriptionController;
+    private final Context mContext;
+
+    private final int mPhoneSubId[];
+    private int mDefaultDataSubId;
+    private int mDefaultDataPhoneId;
+
+    private final Object mLock = new Object();
+
+    private final static boolean VDBG = true;
+    private final static String LOG_TAG = "SubscriptionMonitor";
+
+    private final static int MAX_LOGLINES = 100;
+    private final LocalLog mLocalLog = new LocalLog(MAX_LOGLINES);
+
+    public SubscriptionMonitor(ITelephonyRegistry tr, Context context,
+            SubscriptionController subscriptionController, int numPhones) {
+        try {
+            tr.addOnSubscriptionsChangedListener("SubscriptionMonitor",
+                    mSubscriptionsChangedListener);
+        } catch (RemoteException e) {
+        }
+
+        mSubscriptionController = subscriptionController;
+        mContext = context;
+
+        mSubscriptionsChangedRegistrants = new RegistrantList[numPhones];
+        mDefaultDataSubChangedRegistrants = new RegistrantList[numPhones];
+        mPhoneSubId = new int[numPhones];
+
+        mDefaultDataSubId = mSubscriptionController.getDefaultDataSubId();
+        mDefaultDataPhoneId = mSubscriptionController.getPhoneId(mDefaultDataSubId);
+
+        for (int phoneId = 0; phoneId < numPhones; phoneId++) {
+            mSubscriptionsChangedRegistrants[phoneId] = new RegistrantList();
+            mDefaultDataSubChangedRegistrants[phoneId] = new RegistrantList();
+            mPhoneSubId[phoneId] = mSubscriptionController.getSubIdUsingPhoneId(phoneId);
+        }
+
+        mContext.registerReceiver(mDefaultDataSubscriptionChangedReceiver,
+                new IntentFilter(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED));
+    }
+
+    @VisibleForTesting
+    public SubscriptionMonitor() {
+        mSubscriptionsChangedRegistrants = null;
+        mDefaultDataSubChangedRegistrants = null;
+        mSubscriptionController = null;
+        mContext = null;
+        mPhoneSubId = null;
+    }
+
+    private final IOnSubscriptionsChangedListener mSubscriptionsChangedListener =
+            new IOnSubscriptionsChangedListener.Stub() {
+        @Override
+        public void onSubscriptionsChanged() {
+            synchronized (mLock) {
+                int newDefaultDataPhoneId = INVALID_PHONE_INDEX;
+                for (int phoneId = 0; phoneId < mPhoneSubId.length; phoneId++) {
+                    final int newSubId = mSubscriptionController.getSubIdUsingPhoneId(phoneId);
+                    final int oldSubId = mPhoneSubId[phoneId];
+                    if (oldSubId != newSubId) {
+                        log("Phone[" + phoneId + "] subId changed " + oldSubId + "->" +
+                                newSubId + ", " +
+                                mSubscriptionsChangedRegistrants[phoneId].size() + " registrants");
+                        mPhoneSubId[phoneId] = newSubId;
+                        mSubscriptionsChangedRegistrants[phoneId].notifyRegistrants();
+
+                        // if the default isn't set, just move along..
+                        if (mDefaultDataSubId == INVALID_SUBSCRIPTION_ID) continue;
+
+                        // check if this affects default data
+                        if (newSubId == mDefaultDataSubId || oldSubId == mDefaultDataSubId) {
+                            log("mDefaultDataSubId = " + mDefaultDataSubId + ", " +
+                                    mDefaultDataSubChangedRegistrants[phoneId].size() +
+                                    " registrants");
+                            mDefaultDataSubChangedRegistrants[phoneId].notifyRegistrants();
+                        }
+                    }
+                    if (newSubId == mDefaultDataSubId) {
+                        newDefaultDataPhoneId = phoneId;
+                    }
+                }
+                mDefaultDataPhoneId = newDefaultDataPhoneId;
+            }
+        }
+    };
+
+    private final BroadcastReceiver mDefaultDataSubscriptionChangedReceiver =
+            new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final int newDefaultDataSubId = mSubscriptionController.getDefaultDataSubId();
+            synchronized (mLock) {
+                if (mDefaultDataSubId != newDefaultDataSubId) {
+                    log("Default changed " + mDefaultDataSubId + "->" + newDefaultDataSubId);
+                    final int oldDefaultDataSubId = mDefaultDataSubId;
+                    final int oldDefaultDataPhoneId = mDefaultDataPhoneId;
+                    mDefaultDataSubId = newDefaultDataSubId;
+
+                    int newDefaultDataPhoneId =
+                            mSubscriptionController.getPhoneId(INVALID_SUBSCRIPTION_ID);
+                    if (newDefaultDataSubId != INVALID_SUBSCRIPTION_ID) {
+                        for (int phoneId = 0; phoneId < mPhoneSubId.length; phoneId++) {
+                            if (mPhoneSubId[phoneId] == newDefaultDataSubId) {
+                                newDefaultDataPhoneId = phoneId;
+                                if (VDBG) log("newDefaultDataPhoneId=" + newDefaultDataPhoneId);
+                                break;
+                            }
+                        }
+                    }
+                    if (newDefaultDataPhoneId != oldDefaultDataPhoneId) {
+                        log("Default phoneId changed " + oldDefaultDataPhoneId + "->" +
+                                newDefaultDataPhoneId + ", " +
+                                (invalidPhoneId(oldDefaultDataPhoneId) ?
+                                 0 :
+                                 mDefaultDataSubChangedRegistrants[oldDefaultDataPhoneId].size()) +
+                                "," + (invalidPhoneId(newDefaultDataPhoneId) ?
+                                  0 :
+                                  mDefaultDataSubChangedRegistrants[newDefaultDataPhoneId].size()) +
+                                " registrants");
+                        mDefaultDataPhoneId = newDefaultDataPhoneId;
+                        if (!invalidPhoneId(oldDefaultDataPhoneId)) {
+                            mDefaultDataSubChangedRegistrants[oldDefaultDataPhoneId].
+                                    notifyRegistrants();
+                        }
+                        if (!invalidPhoneId(newDefaultDataPhoneId)) {
+                            mDefaultDataSubChangedRegistrants[newDefaultDataPhoneId].
+                                    notifyRegistrants();
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    public void registerForSubscriptionChanged(int phoneId, Handler h, int what, Object o) {
+        if (invalidPhoneId(phoneId)) {
+            throw new IllegalArgumentException("Invalid PhoneId");
+        }
+        Registrant r = new Registrant(h, what, o);
+        mSubscriptionsChangedRegistrants[phoneId].add(r);
+        r.notifyRegistrant();
+    }
+
+    public void unregisterForSubscriptionChanged(int phoneId, Handler h) {
+        if (invalidPhoneId(phoneId)) {
+            throw new IllegalArgumentException("Invalid PhoneId");
+        }
+        mSubscriptionsChangedRegistrants[phoneId].remove(h);
+    }
+
+    public void registerForDefaultDataSubscriptionChanged(int phoneId, Handler h, int what,
+            Object o) {
+        if (invalidPhoneId(phoneId)) {
+            throw new IllegalArgumentException("Invalid PhoneId");
+        }
+        Registrant r = new Registrant(h, what, o);
+        mDefaultDataSubChangedRegistrants[phoneId].add(r);
+        r.notifyRegistrant();
+    }
+
+    public void unregisterForDefaultDataSubscriptionChanged(int phoneId, Handler h) {
+        if (invalidPhoneId(phoneId)) {
+            throw new IllegalArgumentException("Invalid PhoneId");
+        }
+        mDefaultDataSubChangedRegistrants[phoneId].remove(h);
+    }
+
+    private boolean invalidPhoneId(int phoneId) {
+        if (phoneId >= 0 && phoneId < mPhoneSubId.length) return false;
+        return true;
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+        mLocalLog.log(s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+        synchronized (mLock) {
+            mLocalLog.dump(fd, printWriter, args);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/TelephonyCapabilities.java b/com/android/internal/telephony/TelephonyCapabilities.java
new file mode 100644
index 0000000..b7c68a3
--- /dev/null
+++ b/com/android/internal/telephony/TelephonyCapabilities.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.Phone;
+
+/**
+ * Utilities that check if the phone supports specified capabilities.
+ */
+public class TelephonyCapabilities {
+    private static final String LOG_TAG = "TelephonyCapabilities";
+
+    /** This class is never instantiated. */
+    private TelephonyCapabilities() {
+    }
+
+    /**
+     * Return true if the current phone supports ECM ("Emergency Callback
+     * Mode"), which is a feature where the device goes into a special
+     * state for a short period of time after making an outgoing emergency
+     * call.
+     *
+     * (On current devices, that state lasts 5 minutes.  It prevents data
+     * usage by other apps, to avoid conflicts with any possible incoming
+     * calls.  It also puts up a notification in the status bar, showing a
+     * countdown while ECM is active, and allowing the user to exit ECM.)
+     *
+     * Currently this is assumed to be true for CDMA phones, and false
+     * otherwise.
+     */
+    public static boolean supportsEcm(Phone phone) {
+        Rlog.d(LOG_TAG, "supportsEcm: Phone type = " + phone.getPhoneType() +
+                  " Ims Phone = " + phone.getImsPhone());
+        return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA ||
+                phone.getImsPhone() != null);
+    }
+
+    /**
+     * Return true if the current phone supports Over The Air Service
+     * Provisioning (OTASP)
+     *
+     * Currently this is assumed to be true for CDMA phones, and false
+     * otherwise.
+     *
+     * TODO: Watch out: this is also highly carrier-specific, since the
+     * OTASP procedure is different from one carrier to the next, *and* the
+     * different carriers may want very different onscreen UI as well.
+     * The procedure may even be different for different devices with the
+     * same carrier.
+     *
+     * So we eventually will need a much more flexible, pluggable design.
+     * This method here is just a placeholder to reduce hardcoded
+     * "if (CDMA)" checks sprinkled throughout the phone app.
+     */
+    public static boolean supportsOtasp(Phone phone) {
+        return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA);
+    }
+
+    /**
+     * Return true if the current phone supports voice message count.
+     * and the count is available
+     * Both CDMA and GSM phones support voice message count
+     */
+    public static boolean supportsVoiceMessageCount(Phone phone) {
+        return (phone.getVoiceMessageCount() != -1);
+    }
+
+    /**
+     * Return true if this phone allows the user to select which
+     * network to use.
+     *
+     * Currently this is assumed to be true only on GSM phones.
+     *
+     * TODO: Should CDMA phones allow this as well?
+     */
+    public static boolean supportsNetworkSelection(Phone phone) {
+        return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM);
+    }
+
+    /**
+     * Returns a resource ID for a label to use when displaying the
+     * "device id" of the current device.  (This is currently used as the
+     * title of the "device id" dialog.)
+     *
+     * This is specific to the device's telephony technology: the device
+     * id is called "IMEI" on GSM phones and "MEID" on CDMA phones.
+     */
+    public static int getDeviceIdLabel(Phone phone) {
+        if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
+            return com.android.internal.R.string.imei;
+        } else if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+            return com.android.internal.R.string.meid;
+        } else {
+            Rlog.w(LOG_TAG, "getDeviceIdLabel: no known label for phone "
+                  + phone.getPhoneName());
+            return 0;
+        }
+    }
+
+    /**
+     * Return true if the current phone supports the ability to explicitly
+     * manage the state of a conference call (i.e. view the participants,
+     * and hangup or separate individual callers.)
+     *
+     * The in-call screen's "Manage conference" UI is available only on
+     * devices that support this feature.
+     *
+     * Currently this is assumed to be true on GSM phones and false otherwise.
+     */
+    public static boolean supportsConferenceCallManagement(Phone phone) {
+        return ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)
+                || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP));
+    }
+
+    /**
+     * Return true if the current phone supports explicit "Hold" and
+     * "Unhold" actions for an active call.  (If so, the in-call UI will
+     * provide onscreen "Hold" / "Unhold" buttons.)
+     *
+     * Currently this is assumed to be true on GSM phones and false
+     * otherwise.  (In particular, CDMA has no concept of "putting a call
+     * on hold.")
+     */
+    public static boolean supportsHoldAndUnhold(Phone phone) {
+        return ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)
+                || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP)
+                || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS));
+    }
+
+    /**
+     * Return true if the current phone supports distinct "Answer & Hold"
+     * and "Answer & End" behaviors in the call-waiting scenario.  If so,
+     * the in-call UI may provide separate buttons or menu items for these
+     * two actions.
+     *
+     * Currently this is assumed to be true on GSM phones and false
+     * otherwise.  (In particular, CDMA has no concept of explicitly
+     * managing the background call, or "putting a call on hold.")
+     *
+     * TODO: It might be better to expose this capability in a more
+     * generic form, like maybe "supportsExplicitMultipleLineManagement()"
+     * rather than focusing specifically on call-waiting behavior.
+     */
+    public static boolean supportsAnswerAndHold(Phone phone) {
+        return ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)
+                || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP));
+    }
+
+    /**
+     * Return true if phones with the given phone type support ADN
+     * (Abbreviated Dialing Numbers).
+     *
+     * Currently this returns true when the phone type is GSM
+     * ({@link PhoneConstants#PHONE_TYPE_GSM}).
+     *
+     * This is using int for an argument for letting apps outside
+     * Phone process access to it, while other methods in this class is
+     * using Phone object.
+     *
+     * TODO: Theoretically phones other than GSM may have the ADN capability.
+     * Consider having better check here, or have better capability as part
+     * of public API, with which the argument should be replaced with
+     * something more appropriate.
+     */
+    public static boolean supportsAdn(int phoneType) {
+        return phoneType == PhoneConstants.PHONE_TYPE_GSM;
+    }
+
+    /**
+     * Returns true if the device can distinguish the phone's dialing state
+     * (Call.State.DIALING/ALERTING) and connected state (Call.State.ACTIVE).
+     *
+     * Currently this returns true for GSM phones as we cannot know when a CDMA
+     * phone has transitioned from dialing/active to connected.
+     */
+    public static boolean canDistinguishDialingAndConnected(int phoneType) {
+        return phoneType == PhoneConstants.PHONE_TYPE_GSM;
+    }
+}
diff --git a/com/android/internal/telephony/TelephonyComponentFactory.java b/com/android/internal/telephony/TelephonyComponentFactory.java
new file mode 100644
index 0000000..193d29e
--- /dev/null
+++ b/com/android/internal/telephony/TelephonyComponentFactory.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.IDeviceIdleController;
+import android.os.ServiceManager;
+
+import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.cdma.EriManager;
+import com.android.internal.telephony.dataconnection.DcTracker;
+import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
+import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
+import com.android.internal.telephony.uicc.IccCardProxy;
+
+/**
+ * This class has one-line methods to instantiate objects only. The purpose is to make code
+ * unit-test friendly and use this class as a way to do dependency injection. Instantiating objects
+ * this way makes it easier to mock them in tests.
+ */
+public class TelephonyComponentFactory {
+    private static TelephonyComponentFactory sInstance;
+
+    public static TelephonyComponentFactory getInstance() {
+        if (sInstance == null) {
+            sInstance = new TelephonyComponentFactory();
+        }
+        return sInstance;
+    }
+
+    public GsmCdmaCallTracker makeGsmCdmaCallTracker(GsmCdmaPhone phone) {
+        return new GsmCdmaCallTracker(phone);
+    }
+
+    public SmsStorageMonitor makeSmsStorageMonitor(Phone phone) {
+        return new SmsStorageMonitor(phone);
+    }
+
+    public SmsUsageMonitor makeSmsUsageMonitor(Context context) {
+        return new SmsUsageMonitor(context);
+    }
+
+    public ServiceStateTracker makeServiceStateTracker(GsmCdmaPhone phone, CommandsInterface ci) {
+        return new ServiceStateTracker(phone, ci);
+    }
+
+    public SimActivationTracker makeSimActivationTracker(Phone phone) {
+        return new SimActivationTracker(phone);
+    }
+
+    public DcTracker makeDcTracker(Phone phone) {
+        return new DcTracker(phone);
+    }
+
+    public CarrierSignalAgent makeCarrierSignalAgent(Phone phone) {
+        return new CarrierSignalAgent(phone);
+    }
+
+    public CarrierActionAgent makeCarrierActionAgent(Phone phone) {
+        return new CarrierActionAgent(phone);
+    }
+
+    public IccPhoneBookInterfaceManager makeIccPhoneBookInterfaceManager(Phone phone) {
+        return new IccPhoneBookInterfaceManager(phone);
+    }
+
+    public IccSmsInterfaceManager makeIccSmsInterfaceManager(Phone phone) {
+        return new IccSmsInterfaceManager(phone);
+    }
+
+    public IccCardProxy makeIccCardProxy(Context context, CommandsInterface ci, int phoneId) {
+        return new IccCardProxy(context, ci, phoneId);
+    }
+
+    public EriManager makeEriManager(Phone phone, Context context, int eriFileSource) {
+        return new EriManager(phone, context, eriFileSource);
+    }
+
+    public WspTypeDecoder makeWspTypeDecoder(byte[] pdu) {
+        return new WspTypeDecoder(pdu);
+    }
+
+    /**
+     * Create a tracker for a single-part SMS.
+     */
+    public InboundSmsTracker makeInboundSmsTracker(byte[] pdu, long timestamp, int destPort,
+            boolean is3gpp2, boolean is3gpp2WapPdu, String address, String displayAddr,
+            String messageBody) {
+        return new InboundSmsTracker(pdu, timestamp, destPort, is3gpp2, is3gpp2WapPdu, address,
+                displayAddr, messageBody);
+    }
+
+    /**
+     * Create a tracker for a multi-part SMS.
+     */
+    public InboundSmsTracker makeInboundSmsTracker(byte[] pdu, long timestamp, int destPort,
+            boolean is3gpp2, String address, String displayAddr, int referenceNumber, int sequenceNumber,
+            int messageCount, boolean is3gpp2WapPdu, String messageBody) {
+        return new InboundSmsTracker(pdu, timestamp, destPort, is3gpp2, address, displayAddr,
+                referenceNumber, sequenceNumber, messageCount, is3gpp2WapPdu, messageBody);
+    }
+
+    /**
+     * Create a tracker from a row of raw table
+     */
+    public InboundSmsTracker makeInboundSmsTracker(Cursor cursor, boolean isCurrentFormat3gpp2) {
+        return new InboundSmsTracker(cursor, isCurrentFormat3gpp2);
+    }
+
+    public ImsPhoneCallTracker makeImsPhoneCallTracker(ImsPhone imsPhone) {
+        return new ImsPhoneCallTracker(imsPhone);
+    }
+
+    public ImsExternalCallTracker makeImsExternalCallTracker(ImsPhone imsPhone) {
+
+        return new ImsExternalCallTracker(imsPhone);
+    }
+
+    /**
+     * Create an AppSmsManager for per-app SMS message.
+     */
+    public AppSmsManager makeAppSmsManager(Context context) {
+        return new AppSmsManager(context);
+    }
+
+    public DeviceStateMonitor makeDeviceStateMonitor(Phone phone) {
+        return new DeviceStateMonitor(phone);
+    }
+
+    public CdmaSubscriptionSourceManager
+    getCdmaSubscriptionSourceManagerInstance(Context context, CommandsInterface ci, Handler h,
+                                             int what, Object obj) {
+        return CdmaSubscriptionSourceManager.getInstance(context, ci, h, what, obj);
+    }
+
+    public IDeviceIdleController getIDeviceIdleController() {
+        return IDeviceIdleController.Stub.asInterface(
+                ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+    }
+}
diff --git a/com/android/internal/telephony/TelephonyDevController.java b/com/android/internal/telephony/TelephonyDevController.java
new file mode 100644
index 0000000..448e5bd
--- /dev/null
+++ b/com/android/internal/telephony/TelephonyDevController.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony;
+
+import android.content.res.Resources;
+import com.android.internal.telephony.*;
+import android.telephony.TelephonyManager;
+
+import android.os.AsyncResult;
+import android.telephony.Rlog;
+import java.util.BitSet;
+import java.util.List;
+import java.util.ArrayList;
+import android.text.TextUtils;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.telephony.ServiceState;
+
+/**
+ * TelephonyDevController - provides a unified view of the
+ * telephony hardware resources on a device.
+ *
+ * manages the set of HardwareConfig for the framework.
+ */
+public class TelephonyDevController extends Handler {
+    private static final String LOG_TAG = "TDC";
+    private static final boolean DBG = true;
+    private static final Object mLock = new Object();
+
+    private static final int EVENT_HARDWARE_CONFIG_CHANGED = 1;
+
+    private static TelephonyDevController sTelephonyDevController;
+    private static ArrayList<HardwareConfig> mModems = new ArrayList<HardwareConfig>();
+    private static ArrayList<HardwareConfig> mSims = new ArrayList<HardwareConfig>();
+
+    private static Message sRilHardwareConfig;
+
+    private static void logd(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private static void loge(String s) {
+        Rlog.e(LOG_TAG, s);
+    }
+
+    public static TelephonyDevController create() {
+        synchronized (mLock) {
+            if (sTelephonyDevController != null) {
+                throw new RuntimeException("TelephonyDevController already created!?!");
+            }
+            sTelephonyDevController = new TelephonyDevController();
+            return sTelephonyDevController;
+        }
+    }
+
+    public static TelephonyDevController getInstance() {
+        synchronized (mLock) {
+            if (sTelephonyDevController == null) {
+                throw new RuntimeException("TelephonyDevController not yet created!?!");
+            }
+            return sTelephonyDevController;
+        }
+    }
+
+    private void initFromResource() {
+        Resources resource = Resources.getSystem();
+        String[] hwStrings = resource.getStringArray(
+            com.android.internal.R.array.config_telephonyHardware);
+        if (hwStrings != null) {
+            for (String hwString : hwStrings) {
+                HardwareConfig hw = new HardwareConfig(hwString);
+                if (hw != null) {
+                    if (hw.type == HardwareConfig.DEV_HARDWARE_TYPE_MODEM) {
+                        updateOrInsert(hw, mModems);
+                    } else if (hw.type == HardwareConfig.DEV_HARDWARE_TYPE_SIM) {
+                        updateOrInsert(hw, mSims);
+                    }
+                }
+            }
+        }
+    }
+
+    private TelephonyDevController() {
+        initFromResource();
+
+        mModems.trimToSize();
+        mSims.trimToSize();
+    }
+
+    /**
+     * each RIL call this interface to register/unregister the unsolicited hardware
+     * configuration callback data it can provide.
+     */
+    public static void registerRIL(CommandsInterface cmdsIf) {
+        /* get the current configuration from this ril... */
+        cmdsIf.getHardwareConfig(sRilHardwareConfig);
+        /* ... process it ... */
+        if (sRilHardwareConfig != null) {
+            AsyncResult ar = (AsyncResult) sRilHardwareConfig.obj;
+            if (ar.exception == null) {
+                handleGetHardwareConfigChanged(ar);
+            }
+        }
+        /* and register for async device configuration change. */
+        cmdsIf.registerForHardwareConfigChanged(sTelephonyDevController, EVENT_HARDWARE_CONFIG_CHANGED, null);
+    }
+
+    public static void unregisterRIL(CommandsInterface cmdsIf) {
+        cmdsIf.unregisterForHardwareConfigChanged(sTelephonyDevController);
+    }
+
+    /**
+     * handle callbacks from RIL.
+     */
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+        switch (msg.what) {
+            case EVENT_HARDWARE_CONFIG_CHANGED:
+                if (DBG) logd("handleMessage: received EVENT_HARDWARE_CONFIG_CHANGED");
+                ar = (AsyncResult) msg.obj;
+                handleGetHardwareConfigChanged(ar);
+            break;
+            default:
+                loge("handleMessage: Unknown Event " + msg.what);
+        }
+    }
+
+    /**
+     * hardware configuration update or insert.
+     */
+    private static void updateOrInsert(HardwareConfig hw, ArrayList<HardwareConfig> list) {
+        int size;
+        HardwareConfig item;
+        synchronized (mLock) {
+            size = list.size();
+            for (int i = 0 ; i < size ; i++) {
+                item = list.get(i);
+                if (item.uuid.compareTo(hw.uuid) == 0) {
+                    if (DBG) logd("updateOrInsert: removing: " + item);
+                    list.remove(i);
+                }
+            }
+            if (DBG) logd("updateOrInsert: inserting: " + hw);
+            list.add(hw);
+        }
+    }
+
+    /**
+     * hardware configuration changed.
+     */
+    private static void handleGetHardwareConfigChanged(AsyncResult ar) {
+        if ((ar.exception == null) && (ar.result != null)) {
+            List hwcfg = (List)ar.result;
+            for (int i = 0 ; i < hwcfg.size() ; i++) {
+                HardwareConfig hw = null;
+
+                hw = (HardwareConfig) hwcfg.get(i);
+                if (hw != null) {
+                    if (hw.type == HardwareConfig.DEV_HARDWARE_TYPE_MODEM) {
+                        updateOrInsert(hw, mModems);
+                    } else if (hw.type == HardwareConfig.DEV_HARDWARE_TYPE_SIM) {
+                        updateOrInsert(hw, mSims);
+                    }
+                }
+            }
+        } else {
+            /* error detected, ignore.  are we missing some real time configutation
+             * at this point?  what to do...
+             */
+            loge("handleGetHardwareConfigChanged - returned an error.");
+        }
+    }
+
+    /**
+     * get total number of registered modem.
+     */
+    public static int getModemCount() {
+        synchronized (mLock) {
+            int count = mModems.size();
+            if (DBG) logd("getModemCount: " + count);
+            return count;
+        }
+    }
+
+    /**
+     * get modem at index 'index'.
+     */
+    public HardwareConfig getModem(int index) {
+        synchronized (mLock) {
+            if (mModems.isEmpty()) {
+                loge("getModem: no registered modem device?!?");
+                return null;
+            }
+
+            if (index > getModemCount()) {
+                loge("getModem: out-of-bounds access for modem device " + index + " max: " + getModemCount());
+                return null;
+            }
+
+            if (DBG) logd("getModem: " + index);
+            return mModems.get(index);
+        }
+    }
+
+    /**
+     * get total number of registered sims.
+     */
+    public int getSimCount() {
+        synchronized (mLock) {
+            int count = mSims.size();
+            if (DBG) logd("getSimCount: " + count);
+            return count;
+        }
+    }
+
+    /**
+     * get sim at index 'index'.
+     */
+    public HardwareConfig getSim(int index) {
+        synchronized (mLock) {
+            if (mSims.isEmpty()) {
+                loge("getSim: no registered sim device?!?");
+                return null;
+            }
+
+            if (index > getSimCount()) {
+                loge("getSim: out-of-bounds access for sim device " + index + " max: " + getSimCount());
+                return null;
+            }
+
+            if (DBG) logd("getSim: " + index);
+            return mSims.get(index);
+        }
+    }
+
+    /**
+     * get modem associated with sim index 'simIndex'.
+     */
+    public HardwareConfig getModemForSim(int simIndex) {
+        synchronized (mLock) {
+            if (mModems.isEmpty() || mSims.isEmpty()) {
+                loge("getModemForSim: no registered modem/sim device?!?");
+                return null;
+            }
+
+            if (simIndex > getSimCount()) {
+                loge("getModemForSim: out-of-bounds access for sim device " + simIndex + " max: " + getSimCount());
+                return null;
+            }
+
+            if (DBG) logd("getModemForSim " + simIndex);
+
+            HardwareConfig sim = getSim(simIndex);
+            for (HardwareConfig modem: mModems) {
+                if (modem.uuid.equals(sim.modemUuid)) {
+                    return modem;
+                }
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * get all sim's associated with modem at index 'modemIndex'.
+     */
+    public ArrayList<HardwareConfig> getAllSimsForModem(int modemIndex) {
+        synchronized (mLock) {
+            if (mSims.isEmpty()) {
+                loge("getAllSimsForModem: no registered sim device?!?");
+                return null;
+            }
+
+            if (modemIndex > getModemCount()) {
+                loge("getAllSimsForModem: out-of-bounds access for modem device " + modemIndex + " max: " + getModemCount());
+                return null;
+            }
+
+            if (DBG) logd("getAllSimsForModem " + modemIndex);
+
+            ArrayList<HardwareConfig> result = new ArrayList<HardwareConfig>();
+            HardwareConfig modem = getModem(modemIndex);
+            for (HardwareConfig sim: mSims) {
+                if (sim.modemUuid.equals(modem.uuid)) {
+                    result.add(sim);
+                }
+            }
+            return result;
+        }
+    }
+
+    /**
+     * get all modem's registered.
+     */
+    public ArrayList<HardwareConfig> getAllModems() {
+        synchronized (mLock) {
+            ArrayList<HardwareConfig> modems = new ArrayList<HardwareConfig>();
+            if (mModems.isEmpty()) {
+                if (DBG) logd("getAllModems: empty list.");
+            } else {
+                for (HardwareConfig modem: mModems) {
+                    modems.add(modem);
+                }
+            }
+
+            return modems;
+        }
+    }
+
+    /**
+     * get all sim's registered.
+     */
+    public ArrayList<HardwareConfig> getAllSims() {
+        synchronized (mLock) {
+            ArrayList<HardwareConfig> sims = new ArrayList<HardwareConfig>();
+            if (mSims.isEmpty()) {
+                if (DBG) logd("getAllSims: empty list.");
+            } else {
+                for (HardwareConfig sim: mSims) {
+                    sims.add(sim);
+                }
+            }
+
+            return sims;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/TelephonyIntents.java b/com/android/internal/telephony/TelephonyIntents.java
new file mode 100644
index 0000000..f29d993
--- /dev/null
+++ b/com/android/internal/telephony/TelephonyIntents.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony;
+import android.content.Intent;
+
+import android.content.Intent;
+import android.telephony.SubscriptionManager;
+
+/**
+ * The intents that the telephony services broadcast.
+ *
+ * <p class="warning">
+ * THESE ARE NOT THE API!  Use the {@link android.telephony.TelephonyManager} class.
+ * DON'T LISTEN TO THESE DIRECTLY.
+ */
+public class TelephonyIntents {
+
+    /**
+     * Broadcast Action: The phone service state has changed. The intent will have the following
+     * extra values:</p>
+     * <ul>
+     *   <li><em>state</em> - An int with one of the following values:
+     *          {@link android.telephony.ServiceState#STATE_IN_SERVICE},
+     *          {@link android.telephony.ServiceState#STATE_OUT_OF_SERVICE},
+     *          {@link android.telephony.ServiceState#STATE_EMERGENCY_ONLY}
+     *          or {@link android.telephony.ServiceState#STATE_POWER_OFF}
+     *   <li><em>roaming</em> - A boolean value indicating whether the phone is roaming.</li>
+     *   <li><em>operator-alpha-long</em> - The carrier name as a string.</li>
+     *   <li><em>operator-alpha-short</em> - A potentially shortened version of the carrier name,
+     *          as a string.</li>
+     *   <li><em>operator-numeric</em> - A number representing the carrier, as a string. This is
+     *          a five or six digit number consisting of the MCC (Mobile Country Code, 3 digits)
+     *          and MNC (Mobile Network code, 2-3 digits).</li>
+     *   <li><em>manual</em> - A boolean, where true indicates that the user has chosen to select
+     *          the network manually, and false indicates that network selection is handled by the
+     *          phone.</li>
+     * </ul>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     * @deprecated use {@link Intent#ACTION_SERVICE_STATE}
+     */
+    public static final String ACTION_SERVICE_STATE_CHANGED = Intent.ACTION_SERVICE_STATE;
+
+    /**
+     * <p>Broadcast Action: The radio technology has changed. The intent will have the following
+     * extra values:</p>
+     * <ul>
+     *   <li><em>phoneName</em> - A string version of the new phone name.</li>
+     * </ul>
+     *
+     * <p class="note">
+     * You can <em>not</em> receive this through components declared
+     * in manifests, only by explicitly registering for it with
+     * {@link android.content.Context#registerReceiver(android.content.BroadcastReceiver,
+     * android.content.IntentFilter) Context.registerReceiver()}.
+     *
+     * <p class="note">
+     * Requires no permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_RADIO_TECHNOLOGY_CHANGED
+            = "android.intent.action.RADIO_TECHNOLOGY";
+
+    /**
+     * <p>Broadcast Action: The emergency callback mode is changed.
+     * <ul>
+     *   <li><em>phoneinECMState</em> - A boolean value,true=phone in ECM, false=ECM off</li>
+     * </ul>
+     * <p class="note">
+     * You can <em>not</em> receive this through components declared
+     * in manifests, only by explicitly registering for it with
+     * {@link android.content.Context#registerReceiver(android.content.BroadcastReceiver,
+     * android.content.IntentFilter) Context.registerReceiver()}.
+     *
+     * <p class="note">
+     * Requires no permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_EMERGENCY_CALLBACK_MODE_CHANGED
+            = "android.intent.action.EMERGENCY_CALLBACK_MODE_CHANGED";
+
+    /**
+     * <p>Broadcast Action: The emergency call state is changed.
+     * <ul>
+     *   <li><em>phoneInEmergencyCall</em> - A boolean value, true if phone in emergency call,
+     *   false otherwise</li>
+     * </ul>
+     * <p class="note">
+     * You can <em>not</em> receive this through components declared
+     * in manifests, only by explicitly registering for it with
+     * {@link android.content.Context#registerReceiver(android.content.BroadcastReceiver,
+     * android.content.IntentFilter) Context.registerReceiver()}.
+     *
+     * <p class="note">
+     * Requires no permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_EMERGENCY_CALL_STATE_CHANGED
+            = "android.intent.action.EMERGENCY_CALL_STATE_CHANGED";
+
+    /**
+     * Broadcast Action: The phone's signal strength has changed. The intent will have the
+     * following extra values:</p>
+     * <ul>
+     *   <li><em>phoneName</em> - A string version of the phone name.</li>
+     *   <li><em>asu</em> - A numeric value for the signal strength.
+     *          An ASU is 0-31 or -1 if unknown (for GSM, dBm = -113 - 2 * asu).
+     *          The following special values are defined:
+     *          <ul><li>0 means "-113 dBm or less".</li><li>31 means "-51 dBm or greater".</li></ul>
+     *   </li>
+     * </ul>
+     *
+     * <p class="note">
+     * You can <em>not</em> receive this through components declared
+     * in manifests, only by exlicitly registering for it with
+     * {@link android.content.Context#registerReceiver(android.content.BroadcastReceiver,
+     * android.content.IntentFilter) Context.registerReceiver()}.
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_SIGNAL_STRENGTH_CHANGED = "android.intent.action.SIG_STR";
+
+
+    /**
+     * Broadcast Action: The data connection state has changed for any one of the
+     * phone's mobile data connections (eg, default, MMS or GPS specific connection).
+     * The intent will have the following extra values:</p>
+     * <dl>
+     *   <dt>phoneName</dt><dd>A string version of the phone name.</dd>
+     *   <dt>state</dt><dd>One of {@code CONNECTED}, {@code CONNECTING},
+     *      or {@code DISCONNECTED}.</dd>
+     *   <dt>apn</dt><dd>A string that is the APN associated with this connection.</dd>
+     *   <dt>apnType</dt><dd>A string array of APN types associated with this connection.
+     *      The APN type {@code *} is a special type that means this APN services all types.</dd>
+     * </dl>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_ANY_DATA_CONNECTION_STATE_CHANGED
+            = "android.intent.action.ANY_DATA_STATE";
+
+    /**
+     * Broadcast Action: An attempt to establish a data connection has failed.
+     * The intent will have the following extra values:</p>
+     * <dl>
+     *   <dt>phoneName</dt><dd>A string version of the phone name.</dd>
+     *   <dt>state</dt><dd>One of {@code CONNECTED}, {@code CONNECTING}, or {code DISCONNECTED}.</dd>
+     *   <dt>reason</dt><dd>A string indicating the reason for the failure, if available.</dd>
+     * </dl>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_DATA_CONNECTION_FAILED
+            = "android.intent.action.DATA_CONNECTION_FAILED";
+
+    /**
+     * Broadcast Action: The sim card state has changed.
+     * The intent will have the following extra values:</p>
+     * <dl>
+     *   <dt>phoneName</dt><dd>A string version of the phone name.</dd>
+     *   <dt>ss</dt><dd>The sim state. One of:
+     *     <dl>
+     *       <dt>{@code ABSENT}</dt><dd>SIM card not found</dd>
+     *       <dt>{@code LOCKED}</dt><dd>SIM card locked (see {@code reason})</dd>
+     *       <dt>{@code READY}</dt><dd>SIM card ready</dd>
+     *       <dt>{@code IMSI}</dt><dd>FIXME: what is this state?</dd>
+     *       <dt>{@code LOADED}</dt><dd>SIM card data loaded</dd>
+     *     </dl></dd>
+     *   <dt>reason</dt><dd>The reason why ss is {@code LOCKED}; null otherwise.</dd>
+     *   <dl>
+     *       <dt>{@code PIN}</dt><dd>locked on PIN1</dd>
+     *       <dt>{@code PUK}</dt><dd>locked on PUK1</dd>
+     *       <dt>{@code NETWORK}</dt><dd>locked on network personalization</dd>
+     *   </dl>
+     *   <dt>rebroadcastOnUnlock</dt>
+     *   <dd>A boolean indicates a rebroadcast on unlock. optional extra, defaults to {@code false}
+     *   if not specified </dd>
+     * </dl>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_SIM_STATE_CHANGED
+            = Intent.ACTION_SIM_STATE_CHANGED;
+
+    public static final String EXTRA_REBROADCAST_ON_UNLOCK= "rebroadcastOnUnlock";
+
+    /**
+     * Broadcast Action: The time was set by the carrier (typically by the NITZ string).
+     * This is a sticky broadcast.
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li><em>time</em> - The time as a long in UTC milliseconds.</li>
+     * </ul>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_NETWORK_SET_TIME = "android.intent.action.NETWORK_SET_TIME";
+
+
+    /**
+     * Broadcast Action: The timezone was set by the carrier (typically by the NITZ string).
+     * This is a sticky broadcast.
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li><em>time-zone</em> - The java.util.TimeZone.getID() value identifying the new time
+     *          zone.</li>
+     * </ul>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_NETWORK_SET_TIMEZONE
+            = "android.intent.action.NETWORK_SET_TIMEZONE";
+
+    /**
+     * <p>Broadcast Action: It indicates the Emergency callback mode blocks datacall/sms
+     * <p class="note">.
+     * This is to pop up a notice to show user that the phone is in emergency callback mode
+     * and atacalls and outgoing sms are blocked.
+     *
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS
+            = "com.android.internal.intent.action.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS";
+
+    /**
+     * <p>Broadcast Action: Indicates that the action is forbidden by network.
+     * <p class="note">
+     * This is for the OEM applications to understand about possible provisioning issues.
+     * Used in OMA-DM applications.
+     */
+    public static final String ACTION_FORBIDDEN_NO_SERVICE_AUTHORIZATION
+            = "com.android.internal.intent.action.ACTION_FORBIDDEN_NO_SERVICE_AUTHORIZATION";
+
+    /**
+     * Broadcast Action: A "secret code" has been entered in the dialer. Secret codes are
+     * of the form {@code *#*#<code>#*#*}. The intent will have the data URI:
+     *
+     * {@code android_secret_code://<code>}
+     */
+    public static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE";
+
+    /**
+     * Broadcast Action: The Service Provider string(s) have been updated.  Activities or
+     * services that use these strings should update their display.
+     * The intent will have the following extra values:</p>
+     *
+     * <dl>
+     *   <dt>showPlmn</dt><dd>Boolean that indicates whether the PLMN should be shown.</dd>
+     *   <dt>plmn</dt><dd>The operator name of the registered network, as a string.</dd>
+     *   <dt>showSpn</dt><dd>Boolean that indicates whether the SPN should be shown.</dd>
+     *   <dt>spn</dt><dd>The service provider name, as a string.</dd>
+     * </dl>
+     *
+     * Note that <em>showPlmn</em> may indicate that <em>plmn</em> should be displayed, even
+     * though the value for <em>plmn</em> is null.  This can happen, for example, if the phone
+     * has not registered to a network yet.  In this case the receiver may substitute an
+     * appropriate placeholder string (eg, "No service").
+     *
+     * It is recommended to display <em>plmn</em> before / above <em>spn</em> if
+     * both are displayed.
+     *
+     * <p>Note: this is a protected intent that can only be sent by the system.
+     */
+    public static final String SPN_STRINGS_UPDATED_ACTION =
+            "android.provider.Telephony.SPN_STRINGS_UPDATED";
+
+    public static final String EXTRA_SHOW_PLMN  = "showPlmn";
+    public static final String EXTRA_PLMN       = "plmn";
+    public static final String EXTRA_SHOW_SPN   = "showSpn";
+    public static final String EXTRA_SPN        = "spn";
+    public static final String EXTRA_DATA_SPN   = "spnData";
+
+    /**
+     * <p>Broadcast Action: It indicates one column of a subinfo record has been changed
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_SUBINFO_CONTENT_CHANGE
+            = "android.intent.action.ACTION_SUBINFO_CONTENT_CHANGE";
+
+    /**
+     * <p>Broadcast Action: It indicates subinfo record update is completed
+     * when SIM inserted state change
+     * <p class="note">This is a protected intent that can only be sent
+     * by the system.
+     */
+    public static final String ACTION_SUBINFO_RECORD_UPDATED
+            = "android.intent.action.ACTION_SUBINFO_RECORD_UPDATED";
+
+    /**
+     * Broadcast Action: The default subscription has changed.  This has the following
+     * extra values:</p>
+     * <ul>
+     *   <li><em>subscription</em> - A int, the current default subscription.</li>
+     * </ul>
+     * @deprecated Use {@link SubscriptionManager#ACTION_DEFAULT_SUBSCRIPTION_CHANGED}
+     */
+    @Deprecated
+    public static final String ACTION_DEFAULT_SUBSCRIPTION_CHANGED
+            = SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED;
+
+    /**
+     * Broadcast Action: The default data subscription has changed.  This has the following
+     * extra values:</p>
+     * <ul>
+     *   <li><em>subscription</em> - A int, the current data default subscription.</li>
+     * </ul>
+     */
+    public static final String ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED
+            = "android.intent.action.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED";
+
+    /**
+     * Broadcast Action: The default voice subscription has changed.  This has the following
+     * extra values:</p>
+     * <ul>
+     *   <li><em>subscription</em> - A int, the current voice default subscription.</li>
+     * </ul>
+     */
+    public static final String ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED
+            = "android.intent.action.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED";
+
+    /**
+     * Broadcast Action: The default sms subscription has changed.  This has the following
+     * extra values:</p>
+     * <ul>
+     *   <li><em>subscription</em> - A int, the current sms default subscription.</li>
+     * </ul>
+     * @deprecated Use {@link SubscriptionManager#ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED}
+     */
+    @Deprecated
+    public static final String ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED
+            = SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED;
+
+    /*
+     * Broadcast Action: An attempt to set phone radio type and access technology has changed.
+     * This has the following extra values:
+     * <ul>
+     *   <li><em>phones radio access family </em> - A RadioAccessFamily
+     *   array, contain phone ID and new radio access family for each phone.</li>
+     * </ul>
+     *
+     * <p class="note">
+     * Requires the READ_PHONE_STATE permission.
+     */
+    public static final String ACTION_SET_RADIO_CAPABILITY_DONE =
+            "android.intent.action.ACTION_SET_RADIO_CAPABILITY_DONE";
+
+    public static final String EXTRA_RADIO_ACCESS_FAMILY = "rafs";
+
+    /*
+     * Broadcast Action: An attempt to set phone radio access family has failed.
+     */
+    public static final String ACTION_SET_RADIO_CAPABILITY_FAILED =
+            "android.intent.action.ACTION_SET_RADIO_CAPABILITY_FAILED";
+
+    /**
+     * <p>Broadcast Action: when data connections get redirected with validation failure.
+     * intended for sim/account status checks and only sent to the specified carrier app
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li>apnType</li><dd>A string with the apn type.</dd>
+     *   <li>redirectionUrl</li><dd>redirection url string</dd>
+     *   <li>subId</li><dd>Sub Id which associated the data connection failure.</dd>
+     * </ul>
+     * <p class="note">This is a protected intent that can only be sent by the system.</p>
+     */
+    public static final String ACTION_CARRIER_SIGNAL_REDIRECTED =
+            "com.android.internal.telephony.CARRIER_SIGNAL_REDIRECTED";
+    /**
+     * <p>Broadcast Action: when data connections setup fails.
+     * intended for sim/account status checks and only sent to the specified carrier app
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li>apnType</li><dd>A string with the apn type.</dd>
+     *   <li>errorCode</li><dd>A integer with dataFailCause.</dd>
+     *   <li>subId</li><dd>Sub Id which associated the data connection failure.</dd>
+     * </ul>
+     * <p class="note">This is a protected intent that can only be sent by the system. </p>
+     */
+    public static final String ACTION_CARRIER_SIGNAL_REQUEST_NETWORK_FAILED =
+            "com.android.internal.telephony.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED";
+
+    /**
+     * <p>Broadcast Action: when pco value is available.
+     * intended for sim/account status checks and only sent to the specified carrier app
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li>apnType</li><dd>A string with the apn type.</dd>
+     *   <li>apnProto</li><dd>A string with the protocol of the apn connection (IP,IPV6,
+     *                        IPV4V6)</dd>
+     *   <li>pcoId</li><dd>An integer indicating the pco id for the data.</dd>
+     *   <li>pcoValue</li><dd>A byte array of pco data read from modem.</dd>
+     *   <li>subId</li><dd>Sub Id which associated the data connection.</dd>
+     * </ul>
+     * <p class="note">This is a protected intent that can only be sent by the system. </p>
+     */
+    public static final String ACTION_CARRIER_SIGNAL_PCO_VALUE =
+            "com.android.internal.telephony.CARRIER_SIGNAL_PCO_VALUE";
+
+    /**
+     * <p>Broadcast Action: when system default network available/unavailable with
+     * carrier-disabled mobile data. Intended for carrier apps to set/reset carrier actions when
+     * other network becomes system default network, Wi-Fi for example.
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li>defaultNetworkAvailable</li><dd>A boolean indicates default network available.</dd>
+     *   <li>subId</li><dd>Sub Id which associated the default data.</dd>
+     * </ul>
+     * <p class="note">This is a protected intent that can only be sent by the system. </p>
+     */
+    public static final String ACTION_CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE =
+            "com.android.internal.telephony.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE";
+
+    /**
+     * <p>Broadcast Action: when framework reset all carrier actions on sim load or absent.
+     * intended for carrier apps clean up (clear UI e.g.) and only sent to the specified carrier app
+     * The intent will have the following extra values:</p>
+     * <ul>
+     *   <li>subId</li><dd>Sub Id which associated the data connection failure.</dd>
+     * </ul>
+     * <p class="note">This is a protected intent that can only be sent by the system.</p>
+     */
+    public static final String ACTION_CARRIER_SIGNAL_RESET =
+            "com.android.internal.telephony.CARRIER_SIGNAL_RESET";
+
+    // CARRIER_SIGNAL_ACTION extra keys
+    public static final String EXTRA_REDIRECTION_URL_KEY = "redirectionUrl";
+    public static final String EXTRA_ERROR_CODE_KEY = "errorCode";
+    public static final String EXTRA_APN_TYPE_KEY = "apnType";
+    public static final String EXTRA_APN_PROTO_KEY = "apnProto";
+    public static final String EXTRA_PCO_ID_KEY = "pcoId";
+    public static final String EXTRA_PCO_VALUE_KEY = "pcoValue";
+    public static final String EXTRA_DEFAULT_NETWORK_AVAILABLE_KEY = "defaultNetworkAvailable";
+
+   /**
+     * Broadcast action to trigger CI OMA-DM Session.
+    */
+    public static final String ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE =
+            "com.android.omadm.service.CONFIGURATION_UPDATE";
+}
diff --git a/com/android/internal/telephony/TelephonyProperties.java b/com/android/internal/telephony/TelephonyProperties.java
new file mode 100644
index 0000000..6567ea7
--- /dev/null
+++ b/com/android/internal/telephony/TelephonyProperties.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony;
+
+/**
+ * Contains a list of string constants used to get or set telephone properties
+ * in the system. You can use {@link android.os.SystemProperties os.SystemProperties}
+ * to get and set these values.
+ * @hide
+ */
+public interface TelephonyProperties
+{
+    //****** Baseband and Radio Interface version
+
+    //TODO T: property strings do not have to be gsm specific
+    //        change gsm.*operator.*" properties to "operator.*" properties
+
+    /**
+     * Baseband version
+     * Availability: property is available any time radio is on
+     */
+    static final String PROPERTY_BASEBAND_VERSION = "gsm.version.baseband";
+
+    /** Radio Interface Layer (RIL) library implementation. */
+    static final String PROPERTY_RIL_IMPL = "gsm.version.ril-impl";
+
+    //****** Current Network
+
+    /** Alpha name of current registered operator.<p>
+     *  Availability: when registered to a network. Result may be unreliable on
+     *  CDMA networks.
+     */
+    static final String PROPERTY_OPERATOR_ALPHA = "gsm.operator.alpha";
+    //TODO: most of these properties are generic, substitute gsm. with phone. bug 1856959
+
+    /** Numeric name (MCC+MNC) of current registered operator.<p>
+     *  Availability: when registered to a network. Result may be unreliable on
+     *  CDMA networks.
+     */
+    static final String PROPERTY_OPERATOR_NUMERIC = "gsm.operator.numeric";
+
+    /** 'true' if the device is on a manually selected network
+     *
+     *  Availability: when registered to a network
+     */
+    static final String PROPERTY_OPERATOR_ISMANUAL = "operator.ismanual";
+
+    /** 'true' if the device is considered roaming on this network for GSM
+     *  purposes.
+     *  Availability: when registered to a network
+     */
+    static final String PROPERTY_OPERATOR_ISROAMING = "gsm.operator.isroaming";
+
+    /** The ISO country code equivalent of the current registered operator's
+     *  MCC (Mobile Country Code)<p>
+     *  Availability: when registered to a network. Result may be unreliable on
+     *  CDMA networks.
+     */
+    static final String PROPERTY_OPERATOR_ISO_COUNTRY = "gsm.operator.iso-country";
+
+    /**
+     * The contents of this property is the value of the kernel command line
+     * product_type variable that corresponds to a product that supports LTE on CDMA.
+     * {@see BaseCommands#getLteOnCdmaMode()}
+     */
+    static final String PROPERTY_LTE_ON_CDMA_PRODUCT_TYPE = "telephony.lteOnCdmaProductType";
+
+    /**
+     * The contents of this property is the one of {@link Phone#LTE_ON_CDMA_TRUE} or
+     * {@link Phone#LTE_ON_CDMA_FALSE}. If absent the value will assumed to be false
+     * and the {@see #PROPERTY_LTE_ON_CDMA_PRODUCT_TYPE} will be used to determine its
+     * final value which could also be {@link Phone#LTE_ON_CDMA_FALSE}.
+     * {@see BaseCommands#getLteOnCdmaMode()}
+     */
+    static final String PROPERTY_LTE_ON_CDMA_DEVICE = "telephony.lteOnCdmaDevice";
+
+    static final String CURRENT_ACTIVE_PHONE = "gsm.current.phone-type";
+
+    //****** SIM Card
+    /**
+     * One of <code>"UNKNOWN"</code> <code>"ABSENT"</code> <code>"PIN_REQUIRED"</code>
+     * <code>"PUK_REQUIRED"</code> <code>"NETWORK_LOCKED"</code> or <code>"READY"</code>
+     */
+    static String PROPERTY_SIM_STATE = "gsm.sim.state";
+
+    /** The MCC+MNC (mobile country code+mobile network code) of the
+     *  provider of the SIM. 5 or 6 decimal digits.
+     *  Availability: SIM state must be "READY"
+     */
+    static String PROPERTY_ICC_OPERATOR_NUMERIC = "gsm.sim.operator.numeric";
+
+    /** PROPERTY_ICC_OPERATOR_ALPHA is also known as the SPN, or Service Provider Name.
+     *  Availability: SIM state must be "READY"
+     */
+    static String PROPERTY_ICC_OPERATOR_ALPHA = "gsm.sim.operator.alpha";
+
+    /** ISO country code equivalent for the SIM provider's country code*/
+    static String PROPERTY_ICC_OPERATOR_ISO_COUNTRY = "gsm.sim.operator.iso-country";
+
+    /**
+     * Indicates the available radio technology.  Values include: <code>"unknown"</code>,
+     * <code>"GPRS"</code>, <code>"EDGE"</code> and <code>"UMTS"</code>.
+     */
+    static String PROPERTY_DATA_NETWORK_TYPE = "gsm.network.type";
+
+    /** Indicate if phone is in emergency callback mode */
+    static final String PROPERTY_INECM_MODE = "ril.cdma.inecmmode";
+
+    /** Indicate the timer value for exiting emergency callback mode */
+    static final String PROPERTY_ECM_EXIT_TIMER = "ro.cdma.ecmexittimer";
+
+    /** the international dialing prefix of current operator network */
+    static final String PROPERTY_OPERATOR_IDP_STRING = "gsm.operator.idpstring";
+
+    /**
+     * Defines the schema for the carrier specified OTASP number
+     */
+    static final String PROPERTY_OTASP_NUM_SCHEMA = "ro.cdma.otaspnumschema";
+
+    /**
+     * Disable all calls including Emergency call when it set to true.
+     */
+    static final String PROPERTY_DISABLE_CALL = "ro.telephony.disable-call";
+
+    /**
+     * Set to true for vendor RIL's that send multiple UNSOL_CALL_RING notifications.
+     */
+    static final String PROPERTY_RIL_SENDS_MULTIPLE_CALL_RING =
+        "ro.telephony.call_ring.multiple";
+
+    /**
+     * The number of milliseconds between CALL_RING notifications.
+     */
+    static final String PROPERTY_CALL_RING_DELAY = "ro.telephony.call_ring.delay";
+
+    /**
+     * Track CDMA SMS message id numbers to ensure they increment
+     * monotonically, regardless of reboots.
+     */
+    static final String PROPERTY_CDMA_MSG_ID = "persist.radio.cdma.msgid";
+
+    /**
+     * Property to override DEFAULT_WAKE_LOCK_TIMEOUT
+     */
+    static final String PROPERTY_WAKE_LOCK_TIMEOUT = "ro.ril.wake_lock_timeout";
+
+    /**
+     * Set to true to indicate that the modem needs to be reset
+     * when there is a radio technology change.
+     */
+    static final String PROPERTY_RESET_ON_RADIO_TECH_CHANGE = "persist.radio.reset_on_switch";
+
+    /**
+     * Set to false to disable SMS receiving, default is
+     * the value of config_sms_capable
+     */
+    static final String PROPERTY_SMS_RECEIVE = "telephony.sms.receive";
+
+    /**
+     * Set to false to disable SMS sending, default is
+     * the value of config_sms_capable
+     */
+    static final String PROPERTY_SMS_SEND = "telephony.sms.send";
+
+    /**
+     * Set to true to indicate a test CSIM card is used in the device.
+     * This property is for testing purpose only. This should not be defined
+     * in commercial configuration.
+     */
+    static final String PROPERTY_TEST_CSIM = "persist.radio.test-csim";
+
+    /**
+     * Ignore RIL_UNSOL_NITZ_TIME_RECEIVED completely, used for debugging/testing.
+     */
+    static final String PROPERTY_IGNORE_NITZ = "telephony.test.ignore.nitz";
+
+     /**
+     * Property to set multi sim feature.
+     * Type:  String(dsds, dsda)
+     */
+    static final String PROPERTY_MULTI_SIM_CONFIG = "persist.radio.multisim.config";
+
+    /**
+     * Property to store default subscription.
+     */
+    static final String PROPERTY_DEFAULT_SUBSCRIPTION = "persist.radio.default.sub";
+
+    /**
+     * Property to enable MMS Mode.
+     * Type: string ( default = silent, enable to = prompt )
+     */
+    static final String PROPERTY_MMS_TRANSACTION = "mms.transaction";
+
+    /**
+     * Set to the sim count.
+     */
+    static final String PROPERTY_SIM_COUNT = "ro.telephony.sim.count";
+
+    /**
+     * Controls audio route for video calls.
+     * 0 - Use the default audio routing strategy.
+     * 1 - Disable the speaker. Route the audio to Headset or Bluetooth
+     *     or Earpiece, based on the default audio routing strategy.
+     */
+    static final String PROPERTY_VIDEOCALL_AUDIO_OUTPUT = "persist.radio.call.audio.output";
+
+}
diff --git a/com/android/internal/telephony/TelephonyTester.java b/com/android/internal/telephony/TelephonyTester.java
new file mode 100644
index 0000000..0e80937
--- /dev/null
+++ b/com/android/internal/telephony/TelephonyTester.java
@@ -0,0 +1,301 @@
+/*
+ * 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.BadParcelableException;
+import android.os.Build;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+
+import com.android.ims.ImsCall;
+import com.android.ims.ImsCallProfile;
+import com.android.ims.ImsConferenceState;
+import com.android.ims.ImsExternalCallState;
+import com.android.ims.ImsReasonInfo;
+import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
+import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.imsphone.ImsPhoneCall;
+import com.android.internal.telephony.test.TestConferenceEventPackageParser;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Telephony tester receives the following intents where {name} is the phone name
+ *
+ * adb shell am broadcast -a com.android.internal.telephony.{name}.action_detached
+ * adb shell am broadcast -a com.android.internal.telephony.{name}.action_attached
+ * adb shell am broadcast -a com.android.internal.telephony.TestConferenceEventPackage -e filename
+ *      test_filename.xml
+ */
+public class TelephonyTester {
+    private static final String LOG_TAG = "TelephonyTester";
+    private static final boolean DBG = true;
+
+    /**
+     * Test-only intent used to send a test conference event package to the IMS framework.
+     */
+    private static final String ACTION_TEST_CONFERENCE_EVENT_PACKAGE =
+            "com.android.internal.telephony.TestConferenceEventPackage";
+
+    /**
+     * Test-only intent used to send a test dialog event package to the IMS framework.
+     */
+    private static final String ACTION_TEST_DIALOG_EVENT_PACKAGE =
+            "com.android.internal.telephony.TestDialogEventPackage";
+
+    private static final String EXTRA_FILENAME = "filename";
+    private static final String EXTRA_STARTPACKAGE = "startPackage";
+    private static final String EXTRA_SENDPACKAGE = "sendPackage";
+    private static final String EXTRA_DIALOGID = "dialogId";
+    private static final String EXTRA_NUMBER = "number";
+    private static final String EXTRA_STATE = "state";
+    private static final String EXTRA_CANPULL = "canPull";
+
+    /**
+     * Test-only intent used to trigger supp service notification failure.
+     */
+    private static final String ACTION_TEST_SUPP_SRVC_FAIL =
+            "com.android.internal.telephony.TestSuppSrvcFail";
+    private static final String EXTRA_FAILURE_CODE = "failureCode";
+
+    /**
+     * Test-only intent used to trigger the signalling which occurs when a handover to WIFI fails.
+     */
+    private static final String ACTION_TEST_HANDOVER_FAIL =
+            "com.android.internal.telephony.TestHandoverFail";
+
+    /**
+     * Test-only intent used to trigger signalling of a
+     * {@link com.android.internal.telephony.gsm.SuppServiceNotification} to the {@link ImsPhone}.
+     * Use {@link #EXTRA_CODE} to specify the
+     * {@link com.android.internal.telephony.gsm.SuppServiceNotification#code}.
+     */
+    private static final String ACTION_TEST_SUPP_SRVC_NOTIFICATION =
+            "com.android.internal.telephony.TestSuppSrvcNotification";
+
+    private static final String EXTRA_CODE = "code";
+
+    private static List<ImsExternalCallState> mImsExternalCallStates = null;
+
+    private Phone mPhone;
+
+    // The static intent receiver one for all instances and we assume this
+    // is running on the same thread as Dcc.
+    protected BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+            @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            try {
+                if (DBG) log("sIntentReceiver.onReceive: action=" + action);
+                if (action.equals(mPhone.getActionDetached())) {
+                    log("simulate detaching");
+                    mPhone.getServiceStateTracker().mDetachedRegistrants.notifyRegistrants();
+                } else if (action.equals(mPhone.getActionAttached())) {
+                    log("simulate attaching");
+                    mPhone.getServiceStateTracker().mAttachedRegistrants.notifyRegistrants();
+                } else if (action.equals(ACTION_TEST_CONFERENCE_EVENT_PACKAGE)) {
+                    log("inject simulated conference event package");
+                    handleTestConferenceEventPackage(context,
+                            intent.getStringExtra(EXTRA_FILENAME));
+                } else if (action.equals(ACTION_TEST_DIALOG_EVENT_PACKAGE)) {
+                    log("handle test dialog event package intent");
+                    handleTestDialogEventPackageIntent(intent);
+                } else if (action.equals(ACTION_TEST_SUPP_SRVC_FAIL)) {
+                    log("handle test supp svc failed intent");
+                    handleSuppServiceFailedIntent(intent);
+                } else if (action.equals(ACTION_TEST_HANDOVER_FAIL)) {
+                    log("handle handover fail test intent");
+                    handleHandoverFailedIntent();
+                } else if (action.equals(ACTION_TEST_SUPP_SRVC_NOTIFICATION)) {
+                    log("handle supp service notification test intent");
+                    sendTestSuppServiceNotification(intent);
+                } else {
+                    if (DBG) log("onReceive: unknown action=" + action);
+                }
+            } catch (BadParcelableException e) {
+                Rlog.w(LOG_TAG, e);
+            }
+        }
+    };
+
+    TelephonyTester(Phone phone) {
+        mPhone = phone;
+
+        if (Build.IS_DEBUGGABLE) {
+            IntentFilter filter = new IntentFilter();
+
+            filter.addAction(mPhone.getActionDetached());
+            log("register for intent action=" + mPhone.getActionDetached());
+
+            filter.addAction(mPhone.getActionAttached());
+            log("register for intent action=" + mPhone.getActionAttached());
+
+            if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
+                log("register for intent action=" + ACTION_TEST_CONFERENCE_EVENT_PACKAGE);
+                filter.addAction(ACTION_TEST_CONFERENCE_EVENT_PACKAGE);
+                filter.addAction(ACTION_TEST_DIALOG_EVENT_PACKAGE);
+                filter.addAction(ACTION_TEST_SUPP_SRVC_FAIL);
+                filter.addAction(ACTION_TEST_HANDOVER_FAIL);
+                filter.addAction(ACTION_TEST_SUPP_SRVC_NOTIFICATION);
+                mImsExternalCallStates = new ArrayList<ImsExternalCallState>();
+            }
+
+            phone.getContext().registerReceiver(mIntentReceiver, filter, null, mPhone.getHandler());
+        }
+    }
+
+    void dispose() {
+        if (Build.IS_DEBUGGABLE) {
+            mPhone.getContext().unregisterReceiver(mIntentReceiver);
+        }
+    }
+
+    private static void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private void handleSuppServiceFailedIntent(Intent intent) {
+        ImsPhone imsPhone = (ImsPhone) mPhone;
+        if (imsPhone == null) {
+            return;
+        }
+        int code = intent.getIntExtra(EXTRA_FAILURE_CODE, 0);
+        imsPhone.notifySuppServiceFailed(PhoneInternalInterface.SuppService.values()[code]);
+    }
+
+    private void handleHandoverFailedIntent() {
+        // Attempt to get the active IMS call
+        ImsPhone imsPhone = (ImsPhone) mPhone;
+        if (imsPhone == null) {
+            return;
+        }
+
+        ImsPhoneCall imsPhoneCall = imsPhone.getForegroundCall();
+        if (imsPhoneCall == null) {
+            return;
+        }
+
+        ImsCall imsCall = imsPhoneCall.getImsCall();
+        if (imsCall == null) {
+            return;
+        }
+
+        imsCall.getImsCallSessionListenerProxy().callSessionHandoverFailed(imsCall.getCallSession(),
+                ServiceState.RIL_RADIO_TECHNOLOGY_LTE, ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN,
+                new ImsReasonInfo());
+    }
+
+    /**
+     * Handles request to send a test conference event package to the active Ims call.
+     *
+     * @see com.android.internal.telephony.test.TestConferenceEventPackageParser
+     * @param context The context.
+     * @param fileName The name of the test conference event package file to read.
+     */
+    private void handleTestConferenceEventPackage(Context context, String fileName) {
+        // Attempt to get the active IMS call before parsing the test XML file.
+        ImsPhone imsPhone = (ImsPhone) mPhone;
+        if (imsPhone == null) {
+            return;
+        }
+
+        ImsPhoneCall imsPhoneCall = imsPhone.getForegroundCall();
+        if (imsPhoneCall == null) {
+            return;
+        }
+
+        ImsCall imsCall = imsPhoneCall.getImsCall();
+        if (imsCall == null) {
+            return;
+        }
+
+        File packageFile = new File(context.getFilesDir(), fileName);
+        final FileInputStream is;
+        try {
+            is = new FileInputStream(packageFile);
+        } catch (FileNotFoundException ex) {
+            log("Test conference event package file not found: " + packageFile.getAbsolutePath());
+            return;
+        }
+
+        TestConferenceEventPackageParser parser = new TestConferenceEventPackageParser(is);
+        ImsConferenceState imsConferenceState = parser.parse();
+        if (imsConferenceState == null) {
+            return;
+        }
+
+        imsCall.conferenceStateUpdated(imsConferenceState);
+    }
+
+    /**
+     * Handles intents containing test dialog event package data.
+     *
+     * @param intent
+     */
+    private void handleTestDialogEventPackageIntent(Intent intent) {
+        ImsPhone imsPhone = (ImsPhone) mPhone;
+        if (imsPhone == null) {
+            return;
+        }
+        ImsExternalCallTracker externalCallTracker = imsPhone.getExternalCallTracker();
+        if (externalCallTracker == null) {
+            return;
+        }
+
+        if (intent.hasExtra(EXTRA_STARTPACKAGE)) {
+            mImsExternalCallStates.clear();
+        } else if (intent.hasExtra(EXTRA_SENDPACKAGE)) {
+            externalCallTracker.refreshExternalCallState(mImsExternalCallStates);
+            mImsExternalCallStates.clear();
+        } else if (intent.hasExtra(EXTRA_DIALOGID)) {
+            ImsExternalCallState state = new ImsExternalCallState(
+                    intent.getIntExtra(EXTRA_DIALOGID, 0),
+                    Uri.parse(intent.getStringExtra(EXTRA_NUMBER)),
+                    intent.getBooleanExtra(EXTRA_CANPULL, true),
+                    intent.getIntExtra(EXTRA_STATE,
+                            ImsExternalCallState.CALL_STATE_CONFIRMED),
+                    ImsCallProfile.CALL_TYPE_VOICE,
+                    false /* isHeld */
+                    );
+            mImsExternalCallStates.add(state);
+        }
+    }
+
+    private void sendTestSuppServiceNotification(Intent intent) {
+        if (intent.hasExtra(EXTRA_CODE)) {
+            int code = intent.getIntExtra(EXTRA_CODE, -1);
+            ImsPhone imsPhone = (ImsPhone) mPhone;
+            if (imsPhone == null) {
+                return;
+            }
+            log("Test supp service notification:" + code);
+            SuppServiceNotification suppServiceNotification = new SuppServiceNotification();
+            suppServiceNotification.code = code;
+            imsPhone.notifySuppSvcNotification(suppServiceNotification);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/UUSInfo.java b/com/android/internal/telephony/UUSInfo.java
new file mode 100644
index 0000000..300887c
--- /dev/null
+++ b/com/android/internal/telephony/UUSInfo.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony;
+
+public class UUSInfo {
+
+    /*
+     * User-to-User signaling Info activation types derived from 3GPP 23.087
+     * v8.0
+     */
+
+    public static final int UUS_TYPE1_IMPLICIT = 0;
+
+    public static final int UUS_TYPE1_REQUIRED = 1;
+
+    public static final int UUS_TYPE1_NOT_REQUIRED = 2;
+
+    public static final int UUS_TYPE2_REQUIRED = 3;
+
+    public static final int UUS_TYPE2_NOT_REQUIRED = 4;
+
+    public static final int UUS_TYPE3_REQUIRED = 5;
+
+    public static final int UUS_TYPE3_NOT_REQUIRED = 6;
+
+    /*
+     * User-to-User Signaling Information data coding schemes. Possible values
+     * for Octet 3 (Protocol Discriminator field) in the UUIE. The values have
+     * been specified in section 10.5.4.25 of 3GPP TS 24.008
+     */
+
+    public static final int UUS_DCS_USP = 0; /* User specified protocol */
+
+    public static final int UUS_DCS_OSIHLP = 1; /* OSI higher layer protocol */
+
+    public static final int UUS_DCS_X244 = 2; /* X.244 */
+
+    public static final int UUS_DCS_RMCF = 3; /*
+                                               * Reserved for system management
+                                               * convergence function
+                                               */
+
+    public static final int UUS_DCS_IA5c = 4; /* IA5 characters */
+
+    private int mUusType;
+
+    private int mUusDcs;
+
+    private byte[] mUusData;
+
+    public UUSInfo() {
+        mUusType = UUS_TYPE1_IMPLICIT;
+        mUusDcs = UUS_DCS_IA5c;
+        mUusData = null;
+    }
+
+    public UUSInfo(int uusType, int uusDcs, byte[] uusData) {
+        mUusType = uusType;
+        mUusDcs = uusDcs;
+        mUusData = uusData;
+    }
+
+    public int getDcs() {
+        return mUusDcs;
+    }
+
+    public void setDcs(int uusDcs) {
+        mUusDcs = uusDcs;
+    }
+
+    public int getType() {
+        return mUusType;
+    }
+
+    public void setType(int uusType) {
+        mUusType = uusType;
+    }
+
+    public byte[] getUserData() {
+        return mUusData;
+    }
+
+    public void setUserData(byte[] uusData) {
+        mUusData = uusData;
+    }
+}
diff --git a/com/android/internal/telephony/UiccPhoneBookController.java b/com/android/internal/telephony/UiccPhoneBookController.java
new file mode 100644
index 0000000..f3019f0
--- /dev/null
+++ b/com/android/internal/telephony/UiccPhoneBookController.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (c) 2011-2013, The Linux Foundation. All rights reserved.
+ * Not a Contribution.
+ *
+ * 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.internal.telephony;
+
+import android.os.ServiceManager;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.IIccPhoneBook;
+import com.android.internal.telephony.uicc.AdnRecord;
+
+import java.lang.ArrayIndexOutOfBoundsException;
+import java.lang.NullPointerException;
+import java.util.List;
+
+public class UiccPhoneBookController extends IIccPhoneBook.Stub {
+    private static final String TAG = "UiccPhoneBookController";
+    private Phone[] mPhone;
+
+    /* only one UiccPhoneBookController exists */
+    public UiccPhoneBookController(Phone[] phone) {
+        if (ServiceManager.getService("simphonebook") == null) {
+               ServiceManager.addService("simphonebook", this);
+        }
+        mPhone = phone;
+    }
+
+    @Override
+    public boolean
+    updateAdnRecordsInEfBySearch (int efid, String oldTag, String oldPhoneNumber,
+            String newTag, String newPhoneNumber, String pin2) throws android.os.RemoteException {
+        return updateAdnRecordsInEfBySearchForSubscriber(getDefaultSubscription(), efid, oldTag,
+                oldPhoneNumber, newTag, newPhoneNumber, pin2);
+    }
+
+    @Override
+    public boolean
+    updateAdnRecordsInEfBySearchForSubscriber(int subId, int efid, String oldTag,
+            String oldPhoneNumber, String newTag, String newPhoneNumber,
+            String pin2) throws android.os.RemoteException {
+        IccPhoneBookInterfaceManager iccPbkIntMgr =
+                             getIccPhoneBookInterfaceManager(subId);
+        if (iccPbkIntMgr != null) {
+            return iccPbkIntMgr.updateAdnRecordsInEfBySearch(efid, oldTag,
+                    oldPhoneNumber, newTag, newPhoneNumber, pin2);
+        } else {
+            Rlog.e(TAG,"updateAdnRecordsInEfBySearch iccPbkIntMgr is" +
+                      " null for Subscription:"+subId);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean
+    updateAdnRecordsInEfByIndex(int efid, String newTag,
+            String newPhoneNumber, int index, String pin2) throws android.os.RemoteException {
+        return updateAdnRecordsInEfByIndexForSubscriber(getDefaultSubscription(), efid, newTag,
+                newPhoneNumber, index, pin2);
+    }
+
+    @Override
+    public boolean
+    updateAdnRecordsInEfByIndexForSubscriber(int subId, int efid, String newTag,
+            String newPhoneNumber, int index, String pin2) throws android.os.RemoteException {
+        IccPhoneBookInterfaceManager iccPbkIntMgr =
+                             getIccPhoneBookInterfaceManager(subId);
+        if (iccPbkIntMgr != null) {
+            return iccPbkIntMgr.updateAdnRecordsInEfByIndex(efid, newTag,
+                    newPhoneNumber, index, pin2);
+        } else {
+            Rlog.e(TAG,"updateAdnRecordsInEfByIndex iccPbkIntMgr is" +
+                      " null for Subscription:"+subId);
+            return false;
+        }
+    }
+
+    @Override
+    public int[] getAdnRecordsSize(int efid) throws android.os.RemoteException {
+        return getAdnRecordsSizeForSubscriber(getDefaultSubscription(), efid);
+    }
+
+    @Override
+    public int[]
+    getAdnRecordsSizeForSubscriber(int subId, int efid) throws android.os.RemoteException {
+        IccPhoneBookInterfaceManager iccPbkIntMgr =
+                             getIccPhoneBookInterfaceManager(subId);
+        if (iccPbkIntMgr != null) {
+            return iccPbkIntMgr.getAdnRecordsSize(efid);
+        } else {
+            Rlog.e(TAG,"getAdnRecordsSize iccPbkIntMgr is" +
+                      " null for Subscription:"+subId);
+            return null;
+        }
+    }
+
+    @Override
+    public List<AdnRecord> getAdnRecordsInEf(int efid) throws android.os.RemoteException {
+        return getAdnRecordsInEfForSubscriber(getDefaultSubscription(), efid);
+    }
+
+    @Override
+    public List<AdnRecord> getAdnRecordsInEfForSubscriber(int subId, int efid)
+           throws android.os.RemoteException {
+        IccPhoneBookInterfaceManager iccPbkIntMgr =
+                             getIccPhoneBookInterfaceManager(subId);
+        if (iccPbkIntMgr != null) {
+            return iccPbkIntMgr.getAdnRecordsInEf(efid);
+        } else {
+            Rlog.e(TAG,"getAdnRecordsInEf iccPbkIntMgr is" +
+                      "null for Subscription:"+subId);
+            return null;
+        }
+    }
+
+    /**
+     * get phone book interface manager object based on subscription.
+     **/
+    private IccPhoneBookInterfaceManager
+            getIccPhoneBookInterfaceManager(int subId) {
+
+        int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
+        try {
+            return mPhone[phoneId].getIccPhoneBookInterfaceManager();
+        } catch (NullPointerException e) {
+            Rlog.e(TAG, "Exception is :"+e.toString()+" For subscription :"+subId );
+            e.printStackTrace(); //To print stack trace
+            return null;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            Rlog.e(TAG, "Exception is :"+e.toString()+" For subscription :"+subId );
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private int getDefaultSubscription() {
+        return PhoneFactory.getDefaultSubscription();
+    }
+}
diff --git a/com/android/internal/telephony/UiccSmsController.java b/com/android/internal/telephony/UiccSmsController.java
new file mode 100644
index 0000000..e6cb972
--- /dev/null
+++ b/com/android/internal/telephony/UiccSmsController.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (c) 2011-2013, The Linux Foundation. All rights reserved.
+ * Not a Contribution.
+ *
+ * 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import java.util.List;
+
+/**
+ * UiccSmsController to provide an inter-process communication to
+ * access Sms in Icc.
+ */
+public class UiccSmsController extends ISms.Stub {
+    static final String LOG_TAG = "RIL_UiccSmsController";
+
+    protected UiccSmsController() {
+        if (ServiceManager.getService("isms") == null) {
+            ServiceManager.addService("isms", this);
+        }
+    }
+
+    private Phone getPhone(int subId) {
+        Phone phone = PhoneFactory.getPhone(SubscriptionManager.getPhoneId(subId));
+        if (phone == null) {
+            phone = PhoneFactory.getDefaultPhone();
+        }
+        return phone;
+    }
+
+    @Override
+    public boolean
+    updateMessageOnIccEfForSubscriber(int subId, String callingPackage, int index, int status,
+                byte[] pdu) throws android.os.RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            return iccSmsIntMgr.updateMessageOnIccEf(callingPackage, index, status, pdu);
+        } else {
+            Rlog.e(LOG_TAG,"updateMessageOnIccEfForSubscriber iccSmsIntMgr is null" +
+                          " for Subscription: " + subId);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean copyMessageToIccEfForSubscriber(int subId, String callingPackage, int status,
+            byte[] pdu, byte[] smsc) throws android.os.RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            return iccSmsIntMgr.copyMessageToIccEf(callingPackage, status, pdu, smsc);
+        } else {
+            Rlog.e(LOG_TAG,"copyMessageToIccEfForSubscriber iccSmsIntMgr is null" +
+                          " for Subscription: " + subId);
+            return false;
+        }
+    }
+
+    @Override
+    public List<SmsRawData> getAllMessagesFromIccEfForSubscriber(int subId, String callingPackage)
+                throws android.os.RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            return iccSmsIntMgr.getAllMessagesFromIccEf(callingPackage);
+        } else {
+            Rlog.e(LOG_TAG,"getAllMessagesFromIccEfForSubscriber iccSmsIntMgr is" +
+                          " null for Subscription: " + subId);
+            return null;
+        }
+    }
+
+    @Override
+    public void sendDataForSubscriber(int subId, String callingPackage, String destAddr,
+            String scAddr, int destPort, byte[] data, PendingIntent sentIntent,
+            PendingIntent deliveryIntent) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            iccSmsIntMgr.sendData(callingPackage, destAddr, scAddr, destPort, data,
+                    sentIntent, deliveryIntent);
+        } else {
+            Rlog.e(LOG_TAG,"sendDataForSubscriber iccSmsIntMgr is null for" +
+                          " Subscription: " + subId);
+            // TODO: Use a more specific error code to replace RESULT_ERROR_GENERIC_FAILURE.
+            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+        }
+    }
+
+    public void sendDataForSubscriberWithSelfPermissions(int subId, String callingPackage,
+            String destAddr, String scAddr, int destPort, byte[] data, PendingIntent sentIntent,
+            PendingIntent deliveryIntent) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            iccSmsIntMgr.sendDataWithSelfPermissions(callingPackage, destAddr, scAddr, destPort, data,
+                    sentIntent, deliveryIntent);
+        } else {
+            Rlog.e(LOG_TAG,"sendText iccSmsIntMgr is null for" +
+                          " Subscription: " + subId);
+        }
+    }
+
+    public void sendText(String callingPackage, String destAddr, String scAddr,
+            String text, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        sendTextForSubscriber(getPreferredSmsSubscription(), callingPackage, destAddr, scAddr,
+            text, sentIntent, deliveryIntent, true /* persistMessageForNonDefaultSmsApp*/);
+    }
+
+    @Override
+    public void sendTextForSubscriber(int subId, String callingPackage, String destAddr,
+            String scAddr, String text, PendingIntent sentIntent, PendingIntent deliveryIntent,
+            boolean persistMessageForNonDefaultSmsApp) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            iccSmsIntMgr.sendText(callingPackage, destAddr, scAddr, text, sentIntent,
+                    deliveryIntent, persistMessageForNonDefaultSmsApp);
+        } else {
+            Rlog.e(LOG_TAG,"sendTextForSubscriber iccSmsIntMgr is null for" +
+                          " Subscription: " + subId);
+            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+        }
+    }
+
+    public void sendTextForSubscriberWithSelfPermissions(int subId, String callingPackage,
+            String destAddr, String scAddr, String text, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, boolean persistMessage) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            iccSmsIntMgr.sendTextWithSelfPermissions(callingPackage, destAddr, scAddr, text,
+                    sentIntent, deliveryIntent, persistMessage);
+        } else {
+            Rlog.e(LOG_TAG,"sendText iccSmsIntMgr is null for" +
+                          " Subscription: " + subId);
+        }
+    }
+
+    public void sendMultipartText(String callingPackage, String destAddr, String scAddr,
+            List<String> parts, List<PendingIntent> sentIntents,
+            List<PendingIntent> deliveryIntents) throws android.os.RemoteException {
+         sendMultipartTextForSubscriber(getPreferredSmsSubscription(), callingPackage, destAddr,
+                 scAddr, parts, sentIntents, deliveryIntents,
+                 true /* persistMessageForNonDefaultSmsApp */);
+    }
+
+    @Override
+    public void sendMultipartTextForSubscriber(int subId, String callingPackage, String destAddr,
+            String scAddr, List<String> parts, List<PendingIntent> sentIntents,
+            List<PendingIntent> deliveryIntents, boolean persistMessageForNonDefaultSmsApp)
+            throws android.os.RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            iccSmsIntMgr.sendMultipartText(callingPackage, destAddr, scAddr, parts, sentIntents,
+                    deliveryIntents, persistMessageForNonDefaultSmsApp);
+        } else {
+            Rlog.e(LOG_TAG,"sendMultipartTextForSubscriber iccSmsIntMgr is null for" +
+                          " Subscription: " + subId);
+            sendErrorInPendingIntents(sentIntents, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+        }
+    }
+
+    @Override
+    public boolean enableCellBroadcastForSubscriber(int subId, int messageIdentifier, int ranType)
+                throws android.os.RemoteException {
+        return enableCellBroadcastRangeForSubscriber(subId, messageIdentifier, messageIdentifier,
+                ranType);
+    }
+
+    @Override
+    public boolean enableCellBroadcastRangeForSubscriber(int subId, int startMessageId,
+            int endMessageId, int ranType) throws android.os.RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            return iccSmsIntMgr.enableCellBroadcastRange(startMessageId, endMessageId, ranType);
+        } else {
+            Rlog.e(LOG_TAG,"enableCellBroadcastRangeForSubscriber iccSmsIntMgr is null for" +
+                          " Subscription: " + subId);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean disableCellBroadcastForSubscriber(int subId, int messageIdentifier, int ranType)
+                throws android.os.RemoteException {
+        return disableCellBroadcastRangeForSubscriber(subId, messageIdentifier, messageIdentifier,
+                ranType);
+    }
+
+    @Override
+    public boolean disableCellBroadcastRangeForSubscriber(int subId, int startMessageId,
+            int endMessageId, int ranType) throws android.os.RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            return iccSmsIntMgr.disableCellBroadcastRange(startMessageId, endMessageId, ranType);
+        } else {
+            Rlog.e(LOG_TAG,"disableCellBroadcastRangeForSubscriber iccSmsIntMgr is null for" +
+                          " Subscription:"+subId);
+        }
+       return false;
+    }
+
+    @Override
+    public int getPremiumSmsPermission(String packageName) {
+        return getPremiumSmsPermissionForSubscriber(getPreferredSmsSubscription(), packageName);
+    }
+
+    @Override
+    public int getPremiumSmsPermissionForSubscriber(int subId, String packageName) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            return iccSmsIntMgr.getPremiumSmsPermission(packageName);
+        } else {
+            Rlog.e(LOG_TAG, "getPremiumSmsPermissionForSubscriber iccSmsIntMgr is null");
+        }
+        //TODO Rakesh
+        return 0;
+    }
+
+    @Override
+    public void setPremiumSmsPermission(String packageName, int permission) {
+         setPremiumSmsPermissionForSubscriber(getPreferredSmsSubscription(), packageName, permission);
+    }
+
+    @Override
+    public void setPremiumSmsPermissionForSubscriber(int subId, String packageName, int permission) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            iccSmsIntMgr.setPremiumSmsPermission(packageName, permission);
+        } else {
+            Rlog.e(LOG_TAG, "setPremiumSmsPermissionForSubscriber iccSmsIntMgr is null");
+        }
+    }
+
+    @Override
+    public boolean isImsSmsSupportedForSubscriber(int subId) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            return iccSmsIntMgr.isImsSmsSupported();
+        } else {
+            Rlog.e(LOG_TAG, "isImsSmsSupportedForSubscriber iccSmsIntMgr is null");
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isSmsSimPickActivityNeeded(int subId) {
+        final Context context = ActivityThread.currentApplication().getApplicationContext();
+        TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        List<SubscriptionInfo> subInfoList;
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            subInfoList = SubscriptionManager.from(context).getActiveSubscriptionInfoList();
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+
+        if (subInfoList != null) {
+            final int subInfoLength = subInfoList.size();
+
+            for (int i = 0; i < subInfoLength; ++i) {
+                final SubscriptionInfo sir = subInfoList.get(i);
+                if (sir != null && sir.getSubscriptionId() == subId) {
+                    // The subscription id is valid, sms sim pick activity not needed
+                    return false;
+                }
+            }
+
+            // If reached here and multiple SIMs and subs present, sms sim pick activity is needed
+            if (subInfoLength > 0 && telephonyManager.getSimCount() > 1) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public String getImsSmsFormatForSubscriber(int subId) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            return iccSmsIntMgr.getImsSmsFormat();
+        } else {
+            Rlog.e(LOG_TAG, "getImsSmsFormatForSubscriber iccSmsIntMgr is null");
+        }
+        return null;
+    }
+
+    @Override
+    public void injectSmsPduForSubscriber(
+            int subId, byte[] pdu, String format, PendingIntent receivedIntent) {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            iccSmsIntMgr.injectSmsPdu(pdu, format, receivedIntent);
+        } else {
+            Rlog.e(LOG_TAG, "injectSmsPduForSubscriber iccSmsIntMgr is null");
+            // RESULT_SMS_GENERIC_ERROR is documented for injectSmsPdu
+            sendErrorInPendingIntent(receivedIntent, Intents.RESULT_SMS_GENERIC_ERROR);
+        }
+    }
+
+    /**
+     * Get sms interface manager object based on subscription.
+     * @return ICC SMS manager
+     */
+    private @Nullable IccSmsInterfaceManager getIccSmsInterfaceManager(int subId) {
+        return getPhone(subId).getIccSmsInterfaceManager();
+    }
+
+    /**
+     * Get User preferred SMS subscription
+     * @return User preferred SMS subscription
+     */
+    @Override
+    public int getPreferredSmsSubscription() {
+        return SubscriptionController.getInstance().getDefaultSmsSubId();
+    }
+
+    /**
+     * Get SMS prompt property enabled or not
+     * @return True if SMS prompt is enabled.
+     */
+    @Override
+    public boolean isSMSPromptEnabled() {
+        return PhoneFactory.isSMSPromptEnabled();
+    }
+
+    @Override
+    public void sendStoredText(int subId, String callingPkg, Uri messageUri, String scAddress,
+            PendingIntent sentIntent, PendingIntent deliveryIntent) throws RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null) {
+            iccSmsIntMgr.sendStoredText(callingPkg, messageUri, scAddress, sentIntent,
+                    deliveryIntent);
+        } else {
+            Rlog.e(LOG_TAG,"sendStoredText iccSmsIntMgr is null for subscription: " + subId);
+            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+        }
+    }
+
+    @Override
+    public void sendStoredMultipartText(int subId, String callingPkg, Uri messageUri,
+            String scAddress, List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents)
+            throws RemoteException {
+        IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
+        if (iccSmsIntMgr != null ) {
+            iccSmsIntMgr.sendStoredMultipartText(callingPkg, messageUri, scAddress, sentIntents,
+                    deliveryIntents);
+        } else {
+            Rlog.e(LOG_TAG,"sendStoredMultipartText iccSmsIntMgr is null for subscription: "
+                    + subId);
+            sendErrorInPendingIntents(sentIntents, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+        }
+    }
+
+    @Override
+    public String createAppSpecificSmsToken(int subId, String callingPkg, PendingIntent intent) {
+        return getPhone(subId).getAppSmsManager().createAppSpecificSmsToken(callingPkg, intent);
+    }
+
+    private void sendErrorInPendingIntent(@Nullable PendingIntent intent, int errorCode) {
+        if (intent != null) {
+            try {
+                intent.send(errorCode);
+            } catch (PendingIntent.CanceledException ex) {
+            }
+        }
+    }
+
+    private void sendErrorInPendingIntents(List<PendingIntent> intents, int errorCode) {
+        for (PendingIntent intent : intents) {
+            sendErrorInPendingIntent(intent, errorCode);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/VisualVoicemailSmsFilter.java b/com/android/internal/telephony/VisualVoicemailSmsFilter.java
new file mode 100644
index 0000000..1294762
--- /dev/null
+++ b/com/android/internal/telephony/VisualVoicemailSmsFilter.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SmsMessage;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VisualVoicemailSms;
+import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link
+ * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual
+ * dispatching.
+ */
+public class VisualVoicemailSmsFilter {
+
+    /**
+     * Interface to convert subIds so the logic can be replaced in tests.
+     */
+    @VisibleForTesting
+    public interface PhoneAccountHandleConverter {
+
+        /**
+         * Convert the subId to a {@link PhoneAccountHandle}
+         */
+        PhoneAccountHandle fromSubId(int subId);
+    }
+
+    private static final String TAG = "VvmSmsFilter";
+
+    private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone";
+
+    private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT =
+            new ComponentName("com.android.phone",
+                    "com.android.services.telephony.TelephonyConnectionService");
+
+    private static Map<String, List<Pattern>> sPatterns;
+
+    private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER =
+            new PhoneAccountHandleConverter() {
+
+                @Override
+                public PhoneAccountHandle fromSubId(int subId) {
+                    if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+                        return null;
+                    }
+                    int phoneId = SubscriptionManager.getPhoneId(subId);
+                    if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
+                        return null;
+                    }
+                    return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT,
+                            PhoneFactory.getPhone(phoneId).getFullIccSerialNumber());
+                }
+            };
+
+    private static PhoneAccountHandleConverter sPhoneAccountHandleConverter =
+            DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
+
+    /**
+     * Wrapper to combine multiple PDU into an SMS message
+     */
+    private static class FullMessage {
+        public SmsMessage firstMessage;
+        public String fullMessageBody;
+    }
+
+    /**
+     * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A
+     * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony
+     * service, and the SMS will be dropped.
+     *
+     * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format:
+     *
+     * <p>[clientPrefix]:[prefix]:([key]=[value];)*
+     *
+     * Additionally, if the SMS does not match the format, but matches the regex specified by the
+     * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will
+     * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent.
+     *
+     * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped
+     */
+    public static boolean filter(Context context, byte[][] pdus, String format, int destPort,
+            int subId) {
+        TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+
+        VisualVoicemailSmsFilterSettings settings;
+        settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId);
+
+        if (settings == null) {
+            return false;
+        }
+
+        PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId);
+
+        if (phoneAccountHandle == null) {
+            Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle");
+            return false;
+        }
+
+        FullMessage fullMessage = getFullMessage(pdus, format);
+
+        if (fullMessage == null) {
+            // Carrier WAP push SMS is not recognized by android, which has a ascii PDU.
+            // Attempt to parse it.
+            Log.i(TAG, "Unparsable SMS received");
+            String asciiMessage = parseAsciiPduMessage(pdus);
+            WrappedMessageData messageData = VisualVoicemailSmsParser
+                    .parseAlternativeFormat(asciiMessage);
+            if (messageData != null) {
+                sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null);
+            }
+            // Confidence for what the message actually is is low. Don't remove the message and let
+            // system decide. Usually because it is not parsable it will be dropped.
+            return false;
+        }
+
+        String messageBody = fullMessage.fullMessageBody;
+        String clientPrefix = settings.clientPrefix;
+        WrappedMessageData messageData = VisualVoicemailSmsParser
+                .parse(clientPrefix, messageBody);
+        if (messageData != null) {
+            if (settings.destinationPort
+                    == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) {
+                if (destPort == -1) {
+                    // Non-data SMS is directed to the port "-1".
+                    Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS");
+                    return false;
+                }
+            } else if (settings.destinationPort
+                    != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) {
+                if (settings.destinationPort != destPort) {
+                    Log.i(TAG, "SMS matching VVM format received but is not directed to port "
+                            + settings.destinationPort);
+                    return false;
+                }
+            }
+
+            if (!settings.originatingNumbers.isEmpty()
+                    && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) {
+                Log.i(TAG, "SMS matching VVM format received but is not from originating numbers");
+                return false;
+            }
+
+            sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null);
+            return true;
+        }
+
+        buildPatternsMap(context);
+        String mccMnc = telephonyManager.getSimOperator(subId);
+
+        List<Pattern> patterns = sPatterns.get(mccMnc);
+        if (patterns == null || patterns.isEmpty()) {
+            return false;
+        }
+
+        for (Pattern pattern : patterns) {
+            if (pattern.matcher(messageBody).matches()) {
+                Log.w(TAG, "Incoming SMS matches pattern " + pattern + " but has illegal format, "
+                        + "still dropping as VVM SMS");
+                sendVvmSmsBroadcast(context, phoneAccountHandle, null, messageBody);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * override how subId is converted to PhoneAccountHandle for tests
+     */
+    @VisibleForTesting
+    public static void setPhoneAccountHandleConverterForTest(
+            PhoneAccountHandleConverter converter) {
+        if (converter == null) {
+            sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
+        } else {
+            sPhoneAccountHandleConverter = converter;
+        }
+    }
+
+    private static void buildPatternsMap(Context context) {
+        if (sPatterns != null) {
+            return;
+        }
+        sPatterns = new ArrayMap<>();
+        // TODO(twyen): build from CarrierConfig once public API can be updated.
+        for (String entry : context.getResources()
+                .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) {
+            String[] mccMncList = entry.split(";")[0].split(",");
+            Pattern pattern = Pattern.compile(entry.split(";")[1]);
+
+            for (String mccMnc : mccMncList) {
+                if (!sPatterns.containsKey(mccMnc)) {
+                    sPatterns.put(mccMnc, new ArrayList<>());
+                }
+                sPatterns.get(mccMnc).add(pattern);
+            }
+        }
+    }
+
+    private static void sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle,
+            @Nullable WrappedMessageData messageData, @Nullable String messageBody) {
+        Log.i(TAG, "VVM SMS received");
+        Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
+        VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder();
+        if (messageData != null) {
+            builder.setPrefix(messageData.prefix);
+            builder.setFields(messageData.fields);
+        }
+        if (messageBody != null) {
+            builder.setMessageBody(messageBody);
+        }
+        builder.setPhoneAccountHandle(phoneAccountHandle);
+        intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build());
+        intent.setPackage(TELEPHONY_SERVICE_PACKAGE);
+        context.sendBroadcast(intent);
+    }
+
+    /**
+     * @return the message body of the SMS, or {@code null} if it can not be parsed.
+     */
+    @Nullable
+    private static FullMessage getFullMessage(byte[][] pdus, String format) {
+        FullMessage result = new FullMessage();
+        StringBuilder builder = new StringBuilder();
+        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
+        for (byte pdu[] : pdus) {
+            SmsMessage message = SmsMessage.createFromPdu(pdu, format);
+            if (message == null) {
+                // The PDU is not recognized by android
+                return null;
+            }
+            if (result.firstMessage == null) {
+                result.firstMessage = message;
+            }
+            String body = message.getMessageBody();
+            if (body == null && message.getUserData() != null) {
+                // Attempt to interpret the user data as UTF-8. UTF-8 string over data SMS using
+                // 8BIT data coding scheme is our recommended way to send VVM SMS and is used in CTS
+                // Tests. The OMTP visual voicemail specification does not specify the SMS type and
+                // encoding.
+                ByteBuffer byteBuffer = ByteBuffer.wrap(message.getUserData());
+                try {
+                    body = decoder.decode(byteBuffer).toString();
+                } catch (CharacterCodingException e) {
+                    // User data is not decode-able as UTF-8. Ignoring.
+                    return null;
+                }
+            }
+            if (body != null) {
+                builder.append(body);
+            }
+        }
+        result.fullMessageBody = builder.toString();
+        return result;
+    }
+
+    private static String parseAsciiPduMessage(byte[][] pdus) {
+        StringBuilder builder = new StringBuilder();
+        for (byte pdu[] : pdus) {
+            builder.append(new String(pdu, StandardCharsets.US_ASCII));
+        }
+        return builder.toString();
+    }
+
+    private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) {
+        if (message == null) {
+            Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number");
+            return false;
+        }
+
+        for (String number : numbers) {
+            if (PhoneNumberUtils.compare(number, message.getOriginatingAddress())) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/com/android/internal/telephony/VisualVoicemailSmsParser.java b/com/android/internal/telephony/VisualVoicemailSmsParser.java
new file mode 100644
index 0000000..b6b3202
--- /dev/null
+++ b/com/android/internal/telephony/VisualVoicemailSmsParser.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+public class VisualVoicemailSmsParser {
+
+    private static final String[] ALLOWED_ALTERNATIVE_FORMAT_EVENT = new String[] {
+            "MBOXUPDATE", "UNRECOGNIZED"
+    };
+
+    /**
+     * Class wrapping the raw OMTP message data, internally represented as as map of all key-value
+     * pairs found in the SMS body. <p> All the methods return null if either the field was not
+     * present or it could not be parsed.
+     */
+    public static class WrappedMessageData {
+
+        public final String prefix;
+        public final Bundle fields;
+
+        @Override
+        public String toString() {
+            return "WrappedMessageData [type=" + prefix + " fields=" + fields + "]";
+        }
+
+        WrappedMessageData(String prefix, Bundle keyValues) {
+            this.prefix = prefix;
+            fields = keyValues;
+        }
+    }
+
+    /**
+     * Parses the supplied SMS body and returns back a structured OMTP message. Returns null if
+     * unable to parse the SMS body.
+     */
+    @Nullable
+    public static WrappedMessageData parse(String clientPrefix, String smsBody) {
+        try {
+            if (!smsBody.startsWith(clientPrefix)) {
+                return null;
+            }
+            int prefixEnd = clientPrefix.length();
+            if (!(smsBody.charAt(prefixEnd) == ':')) {
+                return null;
+            }
+            int eventTypeEnd = smsBody.indexOf(":", prefixEnd + 1);
+            if (eventTypeEnd == -1) {
+                return null;
+            }
+            String eventType = smsBody.substring(prefixEnd + 1, eventTypeEnd);
+            Bundle fields = parseSmsBody(smsBody.substring(eventTypeEnd + 1));
+            if (fields == null) {
+                return null;
+            }
+            return new WrappedMessageData(eventType, fields);
+        } catch (IndexOutOfBoundsException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Converts a String of key/value pairs into a Map object. The WrappedMessageData object
+     * contains helper functions to retrieve the values.
+     *
+     * e.g. "//VVM:STATUS:st=R;rc=0;srv=1;dn=1;ipt=1;spt=0;[email protected];pw=1" =>
+     * "WrappedMessageData [fields={st=R, ipt=1, srv=1, dn=1, [email protected], pw=1, rc=0}]"
+     *
+     * @param message The sms string with the prefix removed.
+     * @return A WrappedMessageData object containing the map.
+     */
+    @Nullable
+    private static Bundle parseSmsBody(String message) {
+        // TODO: ensure fail if format does not match
+        Bundle keyValues = new Bundle();
+        String[] entries = message.split(";");
+        for (String entry : entries) {
+            if (entry.length() == 0) {
+                continue;
+            }
+            // The format for a field is <key>=<value>.
+            // As the OMTP spec both key and value are required, but in some cases carriers will
+            // send an SMS with missing value, so only the presence of the key is enforced.
+            // For example, an SMS for a voicemail from restricted number might have "s=" for the
+            // sender field, instead of omitting the field.
+            int separatorIndex = entry.indexOf("=");
+            if (separatorIndex == -1 || separatorIndex == 0) {
+                // No separator or no key.
+                // For example "foo" or "=value".
+                // A VVM SMS should have all of its' field valid.
+                return null;
+            }
+            String key = entry.substring(0, separatorIndex);
+            String value = entry.substring(separatorIndex + 1);
+            keyValues.putString(key, value);
+        }
+
+        return keyValues;
+    }
+
+    /**
+     * The alternative format is [Event]?([key]=[value])*, for example
+     *
+     * <p>"MBOXUPDATE?m=1;server=example.com;port=143;[email protected];pw=foo".
+     *
+     * <p>This format is not protected with a client prefix and should be handled with care. For
+     * safety, the event type must be one of {@link #ALLOWED_ALTERNATIVE_FORMAT_EVENT}
+     */
+    @Nullable
+    public static WrappedMessageData parseAlternativeFormat(String smsBody) {
+        try {
+            int eventTypeEnd = smsBody.indexOf("?");
+            if (eventTypeEnd == -1) {
+                return null;
+            }
+            String eventType = smsBody.substring(0, eventTypeEnd);
+            if (!isAllowedAlternativeFormatEvent(eventType)) {
+                return null;
+            }
+            Bundle fields = parseSmsBody(smsBody.substring(eventTypeEnd + 1));
+            if (fields == null) {
+                return null;
+            }
+            return new WrappedMessageData(eventType, fields);
+        } catch (IndexOutOfBoundsException e) {
+            return null;
+        }
+    }
+
+    private static boolean isAllowedAlternativeFormatEvent(String eventType) {
+        for (String event : ALLOWED_ALTERNATIVE_FORMAT_EVENT) {
+            if (event.equals(eventType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/com/android/internal/telephony/WakeLockStateMachine.java b/com/android/internal/telephony/WakeLockStateMachine.java
new file mode 100644
index 0000000..e2061b9
--- /dev/null
+++ b/com/android/internal/telephony/WakeLockStateMachine.java
@@ -0,0 +1,254 @@
+/*
+ * 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.internal.telephony;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Message;
+import android.os.PowerManager;
+import android.telephony.Rlog;
+
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+/**
+ * Generic state machine for handling messages and waiting for ordered broadcasts to complete.
+ * Subclasses implement {@link #handleSmsMessage}, which returns true to transition into waiting
+ * state, or false to remain in idle state. The wakelock is acquired on exit from idle state,
+ * and is released a few seconds after returning to idle state, or immediately upon calling
+ * {@link #quit}.
+ */
+public abstract class WakeLockStateMachine extends StateMachine {
+    protected static final boolean DBG = true;    // TODO: change to false
+
+    private final PowerManager.WakeLock mWakeLock;
+
+    /** New message to process. */
+    public static final int EVENT_NEW_SMS_MESSAGE = 1;
+
+    /** Result receiver called for current cell broadcast. */
+    protected static final int EVENT_BROADCAST_COMPLETE = 2;
+
+    /** Release wakelock after a short timeout when returning to idle state. */
+    static final int EVENT_RELEASE_WAKE_LOCK = 3;
+
+    static final int EVENT_UPDATE_PHONE_OBJECT = 4;
+
+    protected Phone mPhone;
+
+    protected Context mContext;
+
+    /** Wakelock release delay when returning to idle state. */
+    private static final int WAKE_LOCK_TIMEOUT = 3000;
+
+    private final DefaultState mDefaultState = new DefaultState();
+    private final IdleState mIdleState = new IdleState();
+    private final WaitingState mWaitingState = new WaitingState();
+
+    protected WakeLockStateMachine(String debugTag, Context context, Phone phone) {
+        super(debugTag);
+
+        mContext = context;
+        mPhone = phone;
+
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, debugTag);
+        mWakeLock.acquire();    // wake lock released after we enter idle state
+
+        addState(mDefaultState);
+        addState(mIdleState, mDefaultState);
+        addState(mWaitingState, mDefaultState);
+        setInitialState(mIdleState);
+    }
+
+    public void updatePhoneObject(Phone phone) {
+        sendMessage(EVENT_UPDATE_PHONE_OBJECT, phone);
+    }
+
+    /**
+     * Tell the state machine to quit after processing all messages.
+     */
+    public final void dispose() {
+        quit();
+    }
+
+    @Override
+    protected void onQuitting() {
+        // fully release the wakelock
+        while (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+    }
+
+    /**
+     * Send a message with the specified object for {@link #handleSmsMessage}.
+     * @param obj the object to pass in the msg.obj field
+     */
+    public final void dispatchSmsMessage(Object obj) {
+        sendMessage(EVENT_NEW_SMS_MESSAGE, obj);
+    }
+
+    /**
+     * This parent state throws an exception (for debug builds) or prints an error for unhandled
+     * message types.
+     */
+    class DefaultState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_UPDATE_PHONE_OBJECT: {
+                    mPhone = (Phone) msg.obj;
+                    log("updatePhoneObject: phone=" + mPhone.getClass().getSimpleName());
+                    break;
+                }
+                default: {
+                    String errorText = "processMessage: unhandled message type " + msg.what;
+                    if (Build.IS_DEBUGGABLE) {
+                        throw new RuntimeException(errorText);
+                    } else {
+                        loge(errorText);
+                    }
+                    break;
+                }
+            }
+            return HANDLED;
+        }
+    }
+
+    /**
+     * Idle state delivers Cell Broadcasts to receivers. It acquires the wakelock, which is
+     * released when the broadcast completes.
+     */
+    class IdleState extends State {
+        @Override
+        public void enter() {
+            sendMessageDelayed(EVENT_RELEASE_WAKE_LOCK, WAKE_LOCK_TIMEOUT);
+        }
+
+        @Override
+        public void exit() {
+            mWakeLock.acquire();
+            if (DBG) log("acquired wakelock, leaving Idle state");
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_NEW_SMS_MESSAGE:
+                    // transition to waiting state if we sent a broadcast
+                    if (handleSmsMessage(msg)) {
+                        transitionTo(mWaitingState);
+                    }
+                    return HANDLED;
+
+                case EVENT_RELEASE_WAKE_LOCK:
+                    mWakeLock.release();
+                    if (DBG) {
+                        if (mWakeLock.isHeld()) {
+                            // this is okay as long as we call release() for every acquire()
+                            log("mWakeLock is still held after release");
+                        } else {
+                            log("mWakeLock released");
+                        }
+                    }
+                    return HANDLED;
+
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * Waiting state waits for the result receiver to be called for the current cell broadcast.
+     * In this state, any new cell broadcasts are deferred until we return to Idle state.
+     */
+    class WaitingState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_NEW_SMS_MESSAGE:
+                    log("deferring message until return to idle");
+                    deferMessage(msg);
+                    return HANDLED;
+
+                case EVENT_BROADCAST_COMPLETE:
+                    log("broadcast complete, returning to idle");
+                    transitionTo(mIdleState);
+                    return HANDLED;
+
+                case EVENT_RELEASE_WAKE_LOCK:
+                    mWakeLock.release();    // decrement wakelock from previous entry to Idle
+                    if (!mWakeLock.isHeld()) {
+                        // wakelock should still be held until 3 seconds after we enter Idle
+                        loge("mWakeLock released while still in WaitingState!");
+                    }
+                    return HANDLED;
+
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * Implemented by subclass to handle messages in {@link IdleState}.
+     * @param message the message to process
+     * @return true to transition to {@link WaitingState}; false to stay in {@link IdleState}
+     */
+    protected abstract boolean handleSmsMessage(Message message);
+
+    /**
+     * BroadcastReceiver to send message to return to idle state.
+     */
+    protected final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            sendMessage(EVENT_BROADCAST_COMPLETE);
+        }
+    };
+
+    /**
+     * Log with debug level.
+     * @param s the string to log
+     */
+    @Override
+    protected void log(String s) {
+        Rlog.d(getName(), s);
+    }
+
+    /**
+     * Log with error level.
+     * @param s the string to log
+     */
+    @Override
+    protected void loge(String s) {
+        Rlog.e(getName(), s);
+    }
+
+    /**
+     * Log with error level.
+     * @param s the string to log
+     * @param e is a Throwable which logs additional information.
+     */
+    @Override
+    protected void loge(String s, Throwable e) {
+        Rlog.e(getName(), s, e);
+    }
+}
diff --git a/com/android/internal/telephony/WapPushManagerParams.java b/com/android/internal/telephony/WapPushManagerParams.java
new file mode 100644
index 0000000..11e5ff9
--- /dev/null
+++ b/com/android/internal/telephony/WapPushManagerParams.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony;
+
+/**
+ * WapPushManager constant value definitions
+ */
+public class WapPushManagerParams {
+    /**
+     * Application type activity
+     */
+    public static final int APP_TYPE_ACTIVITY = 0;
+
+    /**
+     * Application type service
+     */
+    public static final int APP_TYPE_SERVICE = 1;
+
+    /**
+     * Process Message return value
+     * Message is handled
+     */
+    public static final int MESSAGE_HANDLED = 0x1;
+
+    /**
+     * Process Message return value
+     * Application ID or content type was not found in the application ID table
+     */
+    public static final int APP_QUERY_FAILED = 0x2;
+
+    /**
+     * Process Message return value
+     * Receiver application signature check failed
+     */
+    public static final int SIGNATURE_NO_MATCH = 0x4;
+
+    /**
+     * Process Message return value
+     * Receiver application was not found
+     */
+    public static final int INVALID_RECEIVER_NAME = 0x8;
+
+    /**
+     * Process Message return value
+     * Unknown exception
+     */
+    public static final int EXCEPTION_CAUGHT = 0x10;
+
+    /**
+     * Process Message return value
+     * Need further processing after WapPushManager message processing
+     */
+    public static final int FURTHER_PROCESSING = 0x8000;
+
+}
+
diff --git a/com/android/internal/telephony/WapPushOverSms.java b/com/android/internal/telephony/WapPushOverSms.java
new file mode 100644
index 0000000..3c4c0d6
--- /dev/null
+++ b/com/android/internal/telephony/WapPushOverSms.java
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony;
+
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SqliteWrapper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IDeviceIdleController;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Telephony;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionManager;
+import android.util.Log;
+
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.util.HashMap;
+
+import com.google.android.mms.MmsException;
+import com.google.android.mms.pdu.DeliveryInd;
+import com.google.android.mms.pdu.GenericPdu;
+import com.google.android.mms.pdu.NotificationInd;
+import com.google.android.mms.pdu.PduHeaders;
+import com.google.android.mms.pdu.PduParser;
+import com.google.android.mms.pdu.PduPersister;
+import com.google.android.mms.pdu.ReadOrigInd;
+
+/**
+ * WAP push handler class.
+ *
+ * @hide
+ */
+public class WapPushOverSms implements ServiceConnection {
+    private static final String TAG = "WAP PUSH";
+    private static final boolean DBG = false;
+
+    private final Context mContext;
+    private IDeviceIdleController mDeviceIdleController;
+
+    private String mWapPushManagerPackage;
+
+    /** Assigned from ServiceConnection callback on main threaad. */
+    private volatile IWapPushManager mWapPushManager;
+
+    /** Broadcast receiver that binds to WapPushManager when the user unlocks the phone for the
+     *  first time after reboot and the credential-encrypted storage is available.
+     */
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(final Context context, Intent intent) {
+            Rlog.d(TAG, "Received broadcast " + intent.getAction());
+            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+                new BindServiceThread(mContext).start();
+            }
+        }
+    };
+
+    private class BindServiceThread extends Thread {
+        private final Context context;
+
+        private BindServiceThread(Context context) {
+            this.context = context;
+        }
+
+        @Override
+        public void run() {
+            bindWapPushManagerService(context);
+        }
+    }
+
+    private void bindWapPushManagerService(Context context) {
+        Intent intent = new Intent(IWapPushManager.class.getName());
+        ComponentName comp = intent.resolveSystemService(context.getPackageManager(), 0);
+        intent.setComponent(comp);
+        if (comp == null || !context.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
+            Rlog.e(TAG, "bindService() for wappush manager failed");
+        } else {
+            synchronized (this) {
+                mWapPushManagerPackage = comp.getPackageName();
+            }
+            if (DBG) Rlog.v(TAG, "bindService() for wappush manager succeeded");
+        }
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        mWapPushManager = IWapPushManager.Stub.asInterface(service);
+        if (DBG) Rlog.v(TAG, "wappush manager connected to " + hashCode());
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        mWapPushManager = null;
+        if (DBG) Rlog.v(TAG, "wappush manager disconnected.");
+    }
+
+    public WapPushOverSms(Context context) {
+        mContext = context;
+        mDeviceIdleController = TelephonyComponentFactory.getInstance().getIDeviceIdleController();
+
+        UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+
+        if (userManager.isUserUnlocked()) {
+            bindWapPushManagerService(mContext);
+        } else {
+            IntentFilter userFilter = new IntentFilter();
+            userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
+            context.registerReceiver(mBroadcastReceiver, userFilter);
+        }
+    }
+
+    public void dispose() {
+        if (mWapPushManager != null) {
+            if (DBG) Rlog.v(TAG, "dispose: unbind wappush manager");
+            mContext.unbindService(this);
+        } else {
+            Rlog.e(TAG, "dispose: not bound to a wappush manager");
+        }
+    }
+
+    /**
+     * Decodes the wap push pdu. The decoded result is wrapped inside the {@link DecodedResult}
+     * object. The caller of this method should check {@link DecodedResult#statusCode} for the
+     * decoding status. It  can have the following values.
+     *
+     * Activity.RESULT_OK - the wap push pdu is successfully decoded and should be further processed
+     * Intents.RESULT_SMS_HANDLED - the wap push pdu should be ignored.
+     * Intents.RESULT_SMS_GENERIC_ERROR - the pdu is invalid.
+     */
+    private DecodedResult decodeWapPdu(byte[] pdu, InboundSmsHandler handler) {
+        DecodedResult result = new DecodedResult();
+        if (DBG) Rlog.d(TAG, "Rx: " + IccUtils.bytesToHexString(pdu));
+
+        try {
+            int index = 0;
+            int transactionId = pdu[index++] & 0xFF;
+            int pduType = pdu[index++] & 0xFF;
+
+            // Should we "abort" if no subId for now just no supplying extra param below
+            int phoneId = handler.getPhone().getPhoneId();
+
+            if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) &&
+                    (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
+                index = mContext.getResources().getInteger(
+                        com.android.internal.R.integer.config_valid_wappush_index);
+                if (index != -1) {
+                    transactionId = pdu[index++] & 0xff;
+                    pduType = pdu[index++] & 0xff;
+                    if (DBG)
+                        Rlog.d(TAG, "index = " + index + " PDU Type = " + pduType +
+                                " transactionID = " + transactionId);
+
+                    // recheck wap push pduType
+                    if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH)
+                            && (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
+                        if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
+                        result.statusCode = Intents.RESULT_SMS_HANDLED;
+                        return result;
+                    }
+                } else {
+                    if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
+                    result.statusCode = Intents.RESULT_SMS_HANDLED;
+                    return result;
+                }
+            }
+
+            WspTypeDecoder pduDecoder =
+                    TelephonyComponentFactory.getInstance().makeWspTypeDecoder(pdu);
+
+            /**
+             * Parse HeaderLen(unsigned integer).
+             * From wap-230-wsp-20010705-a section 8.1.2
+             * The maximum size of a uintvar is 32 bits.
+             * So it will be encoded in no more than 5 octets.
+             */
+            if (pduDecoder.decodeUintvarInteger(index) == false) {
+                if (DBG) Rlog.w(TAG, "Received PDU. Header Length error.");
+                result.statusCode = Intents.RESULT_SMS_GENERIC_ERROR;
+                return result;
+            }
+            int headerLength = (int) pduDecoder.getValue32();
+            index += pduDecoder.getDecodedDataLength();
+
+            int headerStartIndex = index;
+
+            /**
+             * Parse Content-Type.
+             * From wap-230-wsp-20010705-a section 8.4.2.24
+             *
+             * Content-type-value = Constrained-media | Content-general-form
+             * Content-general-form = Value-length Media-type
+             * Media-type = (Well-known-media | Extension-Media) *(Parameter)
+             * Value-length = Short-length | (Length-quote Length)
+             * Short-length = <Any octet 0-30>   (octet <= WAP_PDU_SHORT_LENGTH_MAX)
+             * Length-quote = <Octet 31>         (WAP_PDU_LENGTH_QUOTE)
+             * Length = Uintvar-integer
+             */
+            if (pduDecoder.decodeContentType(index) == false) {
+                if (DBG) Rlog.w(TAG, "Received PDU. Header Content-Type error.");
+                result.statusCode = Intents.RESULT_SMS_GENERIC_ERROR;
+                return result;
+            }
+
+            String mimeType = pduDecoder.getValueString();
+            long binaryContentType = pduDecoder.getValue32();
+            index += pduDecoder.getDecodedDataLength();
+
+            byte[] header = new byte[headerLength];
+            System.arraycopy(pdu, headerStartIndex, header, 0, header.length);
+
+            byte[] intentData;
+
+            if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) {
+                intentData = pdu;
+            } else {
+                int dataIndex = headerStartIndex + headerLength;
+                intentData = new byte[pdu.length - dataIndex];
+                System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length);
+            }
+
+            int[] subIds = SubscriptionManager.getSubId(phoneId);
+            int subId = (subIds != null) && (subIds.length > 0) ? subIds[0]
+                    : SmsManager.getDefaultSmsSubscriptionId();
+
+            // Continue if PDU parsing fails: the default messaging app may successfully parse the
+            // same PDU.
+            GenericPdu parsedPdu = null;
+            try {
+                parsedPdu = new PduParser(intentData, shouldParseContentDisposition(subId)).parse();
+            } catch (Exception e) {
+                Rlog.e(TAG, "Unable to parse PDU: " + e.toString());
+            }
+
+            if (parsedPdu != null && parsedPdu.getMessageType() == MESSAGE_TYPE_NOTIFICATION_IND) {
+                final NotificationInd nInd = (NotificationInd) parsedPdu;
+                if (nInd.getFrom() != null
+                        && BlockChecker.isBlocked(mContext, nInd.getFrom().getString())) {
+                    result.statusCode = Intents.RESULT_SMS_HANDLED;
+                    return result;
+                }
+            }
+
+            /**
+             * Seek for application ID field in WSP header.
+             * If application ID is found, WapPushManager substitute the message
+             * processing. Since WapPushManager is optional module, if WapPushManager
+             * is not found, legacy message processing will be continued.
+             */
+            if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) {
+                index = (int) pduDecoder.getValue32();
+                pduDecoder.decodeXWapApplicationId(index);
+                String wapAppId = pduDecoder.getValueString();
+                if (wapAppId == null) {
+                    wapAppId = Integer.toString((int) pduDecoder.getValue32());
+                }
+                result.wapAppId = wapAppId;
+                String contentType = ((mimeType == null) ?
+                        Long.toString(binaryContentType) : mimeType);
+                result.contentType = contentType;
+                if (DBG) Rlog.v(TAG, "appid found: " + wapAppId + ":" + contentType);
+            }
+
+            result.subId = subId;
+            result.phoneId = phoneId;
+            result.parsedPdu = parsedPdu;
+            result.mimeType = mimeType;
+            result.transactionId = transactionId;
+            result.pduType = pduType;
+            result.header = header;
+            result.intentData = intentData;
+            result.contentTypeParameters = pduDecoder.getContentParameters();
+            result.statusCode = Activity.RESULT_OK;
+        } catch (ArrayIndexOutOfBoundsException aie) {
+            // 0-byte WAP PDU or other unexpected WAP PDU contents can easily throw this;
+            // log exception string without stack trace and return false.
+            Rlog.e(TAG, "ignoring dispatchWapPdu() array index exception: " + aie);
+            result.statusCode = Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+        return result;
+    }
+
+    /**
+     * Dispatches inbound messages that are in the WAP PDU format. See
+     * wap-230-wsp-20010705-a section 8 for details on the WAP PDU format.
+     *
+     * @param pdu The WAP PDU, made up of one or more SMS PDUs
+     * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or
+     *         {@link Activity#RESULT_OK} if the message has been broadcast
+     *         to applications
+     */
+    public int dispatchWapPdu(byte[] pdu, BroadcastReceiver receiver, InboundSmsHandler handler) {
+        DecodedResult result = decodeWapPdu(pdu, handler);
+        if (result.statusCode != Activity.RESULT_OK) {
+            return result.statusCode;
+        }
+
+        if (SmsManager.getDefault().getAutoPersisting()) {
+            // Store the wap push data in telephony
+            writeInboxMessage(result.subId, result.parsedPdu);
+        }
+
+        /**
+         * If the pdu has application ID, WapPushManager substitute the message
+         * processing. Since WapPushManager is optional module, if WapPushManager
+         * is not found, legacy message processing will be continued.
+         */
+        if (result.wapAppId != null) {
+            try {
+                boolean processFurther = true;
+                IWapPushManager wapPushMan = mWapPushManager;
+
+                if (wapPushMan == null) {
+                    if (DBG) Rlog.w(TAG, "wap push manager not found!");
+                } else {
+                    synchronized (this) {
+                        mDeviceIdleController.addPowerSaveTempWhitelistAppForMms(
+                                mWapPushManagerPackage, 0, "mms-mgr");
+                    }
+
+                    Intent intent = new Intent();
+                    intent.putExtra("transactionId", result.transactionId);
+                    intent.putExtra("pduType", result.pduType);
+                    intent.putExtra("header", result.header);
+                    intent.putExtra("data", result.intentData);
+                    intent.putExtra("contentTypeParameters", result.contentTypeParameters);
+                    SubscriptionManager.putPhoneIdAndSubIdExtra(intent, result.phoneId);
+
+                    int procRet = wapPushMan.processMessage(
+                        result.wapAppId, result.contentType, intent);
+                    if (DBG) Rlog.v(TAG, "procRet:" + procRet);
+                    if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0
+                            && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) {
+                        processFurther = false;
+                    }
+                }
+                if (!processFurther) {
+                    return Intents.RESULT_SMS_HANDLED;
+                }
+            } catch (RemoteException e) {
+                if (DBG) Rlog.w(TAG, "remote func failed...");
+            }
+        }
+        if (DBG) Rlog.v(TAG, "fall back to existing handler");
+
+        if (result.mimeType == null) {
+            if (DBG) Rlog.w(TAG, "Header Content-Type error.");
+            return Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+
+        Intent intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION);
+        intent.setType(result.mimeType);
+        intent.putExtra("transactionId", result.transactionId);
+        intent.putExtra("pduType", result.pduType);
+        intent.putExtra("header", result.header);
+        intent.putExtra("data", result.intentData);
+        intent.putExtra("contentTypeParameters", result.contentTypeParameters);
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, result.phoneId);
+
+        // Direct the intent to only the default MMS app. If we can't find a default MMS app
+        // then sent it to all broadcast receivers.
+        ComponentName componentName = SmsApplication.getDefaultMmsApplication(mContext, true);
+        Bundle options = null;
+        if (componentName != null) {
+            // Deliver MMS message only to this receiver
+            intent.setComponent(componentName);
+            if (DBG) Rlog.v(TAG, "Delivering MMS to: " + componentName.getPackageName() +
+                    " " + componentName.getClassName());
+            try {
+                long duration = mDeviceIdleController.addPowerSaveTempWhitelistAppForMms(
+                        componentName.getPackageName(), 0, "mms-app");
+                BroadcastOptions bopts = BroadcastOptions.makeBasic();
+                bopts.setTemporaryAppWhitelistDuration(duration);
+                options = bopts.toBundle();
+            } catch (RemoteException e) {
+            }
+        }
+
+        handler.dispatchIntent(intent, getPermissionForType(result.mimeType),
+                getAppOpsPermissionForIntent(result.mimeType), options, receiver,
+                UserHandle.SYSTEM);
+        return Activity.RESULT_OK;
+    }
+
+    /**
+     * Check whether the pdu is a MMS WAP push pdu that should be dispatched to the SMS app.
+     */
+    public boolean isWapPushForMms(byte[] pdu, InboundSmsHandler handler) {
+        DecodedResult result = decodeWapPdu(pdu, handler);
+        return result.statusCode == Activity.RESULT_OK
+            && WspTypeDecoder.CONTENT_TYPE_B_MMS.equals(result.mimeType);
+    }
+
+    private static boolean shouldParseContentDisposition(int subId) {
+        return SmsManager
+                .getSmsManagerForSubscriptionId(subId)
+                .getCarrierConfigValues()
+                .getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION, true);
+    }
+
+    private void writeInboxMessage(int subId, GenericPdu pdu) {
+        if (pdu == null) {
+            Rlog.e(TAG, "Invalid PUSH PDU");
+        }
+        final PduPersister persister = PduPersister.getPduPersister(mContext);
+        final int type = pdu.getMessageType();
+        try {
+            switch (type) {
+                case MESSAGE_TYPE_DELIVERY_IND:
+                case MESSAGE_TYPE_READ_ORIG_IND: {
+                    final long threadId = getDeliveryOrReadReportThreadId(mContext, pdu);
+                    if (threadId == -1) {
+                        // The associated SendReq isn't found, therefore skip
+                        // processing this PDU.
+                        Rlog.e(TAG, "Failed to find delivery or read report's thread id");
+                        break;
+                    }
+                    final Uri uri = persister.persist(
+                            pdu,
+                            Telephony.Mms.Inbox.CONTENT_URI,
+                            true/*createThreadId*/,
+                            true/*groupMmsEnabled*/,
+                            null/*preOpenedFiles*/);
+                    if (uri == null) {
+                        Rlog.e(TAG, "Failed to persist delivery or read report");
+                        break;
+                    }
+                    // Update thread ID for ReadOrigInd & DeliveryInd.
+                    final ContentValues values = new ContentValues(1);
+                    values.put(Telephony.Mms.THREAD_ID, threadId);
+                    if (SqliteWrapper.update(
+                            mContext,
+                            mContext.getContentResolver(),
+                            uri,
+                            values,
+                            null/*where*/,
+                            null/*selectionArgs*/) != 1) {
+                        Rlog.e(TAG, "Failed to update delivery or read report thread id");
+                    }
+                    break;
+                }
+                case MESSAGE_TYPE_NOTIFICATION_IND: {
+                    final NotificationInd nInd = (NotificationInd) pdu;
+
+                    Bundle configs = SmsManager.getSmsManagerForSubscriptionId(subId)
+                            .getCarrierConfigValues();
+                    if (configs != null && configs.getBoolean(
+                        SmsManager.MMS_CONFIG_APPEND_TRANSACTION_ID, false)) {
+                        final byte [] contentLocation = nInd.getContentLocation();
+                        if ('=' == contentLocation[contentLocation.length - 1]) {
+                            byte [] transactionId = nInd.getTransactionId();
+                            byte [] contentLocationWithId = new byte [contentLocation.length
+                                    + transactionId.length];
+                            System.arraycopy(contentLocation, 0, contentLocationWithId,
+                                    0, contentLocation.length);
+                            System.arraycopy(transactionId, 0, contentLocationWithId,
+                                    contentLocation.length, transactionId.length);
+                            nInd.setContentLocation(contentLocationWithId);
+                        }
+                    }
+                    if (!isDuplicateNotification(mContext, nInd)) {
+                        final Uri uri = persister.persist(
+                                pdu,
+                                Telephony.Mms.Inbox.CONTENT_URI,
+                                true/*createThreadId*/,
+                                true/*groupMmsEnabled*/,
+                                null/*preOpenedFiles*/);
+                        if (uri == null) {
+                            Rlog.e(TAG, "Failed to save MMS WAP push notification ind");
+                        }
+                    } else {
+                        Rlog.d(TAG, "Skip storing duplicate MMS WAP push notification ind: "
+                                + new String(nInd.getContentLocation()));
+                    }
+                    break;
+                }
+                default:
+                    Log.e(TAG, "Received unrecognized WAP Push PDU.");
+            }
+        } catch (MmsException e) {
+            Log.e(TAG, "Failed to save MMS WAP push data: type=" + type, e);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Unexpected RuntimeException in persisting MMS WAP push data", e);
+        }
+
+    }
+
+    private static final String THREAD_ID_SELECTION =
+            Telephony.Mms.MESSAGE_ID + "=? AND " + Telephony.Mms.MESSAGE_TYPE + "=?";
+
+    private static long getDeliveryOrReadReportThreadId(Context context, GenericPdu pdu) {
+        String messageId;
+        if (pdu instanceof DeliveryInd) {
+            messageId = new String(((DeliveryInd) pdu).getMessageId());
+        } else if (pdu instanceof ReadOrigInd) {
+            messageId = new String(((ReadOrigInd) pdu).getMessageId());
+        } else {
+            Rlog.e(TAG, "WAP Push data is neither delivery or read report type: "
+                    + pdu.getClass().getCanonicalName());
+            return -1L;
+        }
+        Cursor cursor = null;
+        try {
+            cursor = SqliteWrapper.query(
+                    context,
+                    context.getContentResolver(),
+                    Telephony.Mms.CONTENT_URI,
+                    new String[]{ Telephony.Mms.THREAD_ID },
+                    THREAD_ID_SELECTION,
+                    new String[]{
+                            DatabaseUtils.sqlEscapeString(messageId),
+                            Integer.toString(PduHeaders.MESSAGE_TYPE_SEND_REQ)
+                    },
+                    null/*sortOrder*/);
+            if (cursor != null && cursor.moveToFirst()) {
+                return cursor.getLong(0);
+            }
+        } catch (SQLiteException e) {
+            Rlog.e(TAG, "Failed to query delivery or read report thread id", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return -1L;
+    }
+
+    private static final String LOCATION_SELECTION =
+            Telephony.Mms.MESSAGE_TYPE + "=? AND " + Telephony.Mms.CONTENT_LOCATION + " =?";
+
+    private static boolean isDuplicateNotification(Context context, NotificationInd nInd) {
+        final byte[] rawLocation = nInd.getContentLocation();
+        if (rawLocation != null) {
+            String location = new String(rawLocation);
+            String[] selectionArgs = new String[] { location };
+            Cursor cursor = null;
+            try {
+                cursor = SqliteWrapper.query(
+                        context,
+                        context.getContentResolver(),
+                        Telephony.Mms.CONTENT_URI,
+                        new String[]{Telephony.Mms._ID},
+                        LOCATION_SELECTION,
+                        new String[]{
+                                Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
+                                new String(rawLocation)
+                        },
+                        null/*sortOrder*/);
+                if (cursor != null && cursor.getCount() > 0) {
+                    // We already received the same notification before.
+                    return true;
+                }
+            } catch (SQLiteException e) {
+                Rlog.e(TAG, "failed to query existing notification ind", e);
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+        return false;
+    }
+
+    public static String getPermissionForType(String mimeType) {
+        String permission;
+        if (WspTypeDecoder.CONTENT_TYPE_B_MMS.equals(mimeType)) {
+            permission = android.Manifest.permission.RECEIVE_MMS;
+        } else {
+            permission = android.Manifest.permission.RECEIVE_WAP_PUSH;
+        }
+        return permission;
+    }
+
+    public static int getAppOpsPermissionForIntent(String mimeType) {
+        int appOp;
+        if (WspTypeDecoder.CONTENT_TYPE_B_MMS.equals(mimeType)) {
+            appOp = AppOpsManager.OP_RECEIVE_MMS;
+        } else {
+            appOp = AppOpsManager.OP_RECEIVE_WAP_PUSH;
+        }
+        return appOp;
+    }
+
+    /**
+     * Place holder for decoded Wap pdu data.
+     */
+    private final class DecodedResult {
+        String mimeType;
+        String contentType;
+        int transactionId;
+        int pduType;
+        int phoneId;
+        int subId;
+        byte[] header;
+        String wapAppId;
+        byte[] intentData;
+        HashMap<String, String> contentTypeParameters;
+        GenericPdu parsedPdu;
+        int statusCode;
+    }
+}
diff --git a/com/android/internal/telephony/WspTypeDecoder.java b/com/android/internal/telephony/WspTypeDecoder.java
new file mode 100644
index 0000000..281488a
--- /dev/null
+++ b/com/android/internal/telephony/WspTypeDecoder.java
@@ -0,0 +1,727 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony;
+
+import java.util.HashMap;
+
+/**
+ * Implement the WSP data type decoder.
+ *
+ * @hide
+ */
+public class WspTypeDecoder {
+
+    private static final int WAP_PDU_SHORT_LENGTH_MAX = 30;
+    private static final int WAP_PDU_LENGTH_QUOTE = 31;
+
+    public static final int PDU_TYPE_PUSH = 0x06;
+    public static final int PDU_TYPE_CONFIRMED_PUSH = 0x07;
+
+    private final static HashMap<Integer, String> WELL_KNOWN_MIME_TYPES =
+            new HashMap<Integer, String>();
+
+    private final static HashMap<Integer, String> WELL_KNOWN_PARAMETERS =
+            new HashMap<Integer, String>();
+
+    public static final int PARAMETER_ID_X_WAP_APPLICATION_ID = 0x2f;
+    private static final int Q_VALUE = 0x00;
+
+    static {
+        WELL_KNOWN_MIME_TYPES.put(0x00, "*/*");
+        WELL_KNOWN_MIME_TYPES.put(0x01, "text/*");
+        WELL_KNOWN_MIME_TYPES.put(0x02, "text/html");
+        WELL_KNOWN_MIME_TYPES.put(0x03, "text/plain");
+        WELL_KNOWN_MIME_TYPES.put(0x04, "text/x-hdml");
+        WELL_KNOWN_MIME_TYPES.put(0x05, "text/x-ttml");
+        WELL_KNOWN_MIME_TYPES.put(0x06, "text/x-vCalendar");
+        WELL_KNOWN_MIME_TYPES.put(0x07, "text/x-vCard");
+        WELL_KNOWN_MIME_TYPES.put(0x08, "text/vnd.wap.wml");
+        WELL_KNOWN_MIME_TYPES.put(0x09, "text/vnd.wap.wmlscript");
+        WELL_KNOWN_MIME_TYPES.put(0x0A, "text/vnd.wap.wta-event");
+        WELL_KNOWN_MIME_TYPES.put(0x0B, "multipart/*");
+        WELL_KNOWN_MIME_TYPES.put(0x0C, "multipart/mixed");
+        WELL_KNOWN_MIME_TYPES.put(0x0D, "multipart/form-data");
+        WELL_KNOWN_MIME_TYPES.put(0x0E, "multipart/byterantes");
+        WELL_KNOWN_MIME_TYPES.put(0x0F, "multipart/alternative");
+        WELL_KNOWN_MIME_TYPES.put(0x10, "application/*");
+        WELL_KNOWN_MIME_TYPES.put(0x11, "application/java-vm");
+        WELL_KNOWN_MIME_TYPES.put(0x12, "application/x-www-form-urlencoded");
+        WELL_KNOWN_MIME_TYPES.put(0x13, "application/x-hdmlc");
+        WELL_KNOWN_MIME_TYPES.put(0x14, "application/vnd.wap.wmlc");
+        WELL_KNOWN_MIME_TYPES.put(0x15, "application/vnd.wap.wmlscriptc");
+        WELL_KNOWN_MIME_TYPES.put(0x16, "application/vnd.wap.wta-eventc");
+        WELL_KNOWN_MIME_TYPES.put(0x17, "application/vnd.wap.uaprof");
+        WELL_KNOWN_MIME_TYPES.put(0x18, "application/vnd.wap.wtls-ca-certificate");
+        WELL_KNOWN_MIME_TYPES.put(0x19, "application/vnd.wap.wtls-user-certificate");
+        WELL_KNOWN_MIME_TYPES.put(0x1A, "application/x-x509-ca-cert");
+        WELL_KNOWN_MIME_TYPES.put(0x1B, "application/x-x509-user-cert");
+        WELL_KNOWN_MIME_TYPES.put(0x1C, "image/*");
+        WELL_KNOWN_MIME_TYPES.put(0x1D, "image/gif");
+        WELL_KNOWN_MIME_TYPES.put(0x1E, "image/jpeg");
+        WELL_KNOWN_MIME_TYPES.put(0x1F, "image/tiff");
+        WELL_KNOWN_MIME_TYPES.put(0x20, "image/png");
+        WELL_KNOWN_MIME_TYPES.put(0x21, "image/vnd.wap.wbmp");
+        WELL_KNOWN_MIME_TYPES.put(0x22, "application/vnd.wap.multipart.*");
+        WELL_KNOWN_MIME_TYPES.put(0x23, "application/vnd.wap.multipart.mixed");
+        WELL_KNOWN_MIME_TYPES.put(0x24, "application/vnd.wap.multipart.form-data");
+        WELL_KNOWN_MIME_TYPES.put(0x25, "application/vnd.wap.multipart.byteranges");
+        WELL_KNOWN_MIME_TYPES.put(0x26, "application/vnd.wap.multipart.alternative");
+        WELL_KNOWN_MIME_TYPES.put(0x27, "application/xml");
+        WELL_KNOWN_MIME_TYPES.put(0x28, "text/xml");
+        WELL_KNOWN_MIME_TYPES.put(0x29, "application/vnd.wap.wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x2A, "application/x-x968-cross-cert");
+        WELL_KNOWN_MIME_TYPES.put(0x2B, "application/x-x968-ca-cert");
+        WELL_KNOWN_MIME_TYPES.put(0x2C, "application/x-x968-user-cert");
+        WELL_KNOWN_MIME_TYPES.put(0x2D, "text/vnd.wap.si");
+        WELL_KNOWN_MIME_TYPES.put(0x2E, "application/vnd.wap.sic");
+        WELL_KNOWN_MIME_TYPES.put(0x2F, "text/vnd.wap.sl");
+        WELL_KNOWN_MIME_TYPES.put(0x30, "application/vnd.wap.slc");
+        WELL_KNOWN_MIME_TYPES.put(0x31, "text/vnd.wap.co");
+        WELL_KNOWN_MIME_TYPES.put(0x32, "application/vnd.wap.coc");
+        WELL_KNOWN_MIME_TYPES.put(0x33, "application/vnd.wap.multipart.related");
+        WELL_KNOWN_MIME_TYPES.put(0x34, "application/vnd.wap.sia");
+        WELL_KNOWN_MIME_TYPES.put(0x35, "text/vnd.wap.connectivity-xml");
+        WELL_KNOWN_MIME_TYPES.put(0x36, "application/vnd.wap.connectivity-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x37, "application/pkcs7-mime");
+        WELL_KNOWN_MIME_TYPES.put(0x38, "application/vnd.wap.hashed-certificate");
+        WELL_KNOWN_MIME_TYPES.put(0x39, "application/vnd.wap.signed-certificate");
+        WELL_KNOWN_MIME_TYPES.put(0x3A, "application/vnd.wap.cert-response");
+        WELL_KNOWN_MIME_TYPES.put(0x3B, "application/xhtml+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x3C, "application/wml+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x3D, "text/css");
+        WELL_KNOWN_MIME_TYPES.put(0x3E, "application/vnd.wap.mms-message");
+        WELL_KNOWN_MIME_TYPES.put(0x3F, "application/vnd.wap.rollover-certificate");
+        WELL_KNOWN_MIME_TYPES.put(0x40, "application/vnd.wap.locc+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x41, "application/vnd.wap.loc+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x42, "application/vnd.syncml.dm+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x43, "application/vnd.syncml.dm+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x44, "application/vnd.syncml.notification");
+        WELL_KNOWN_MIME_TYPES.put(0x45, "application/vnd.wap.xhtml+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x46, "application/vnd.wv.csp.cir");
+        WELL_KNOWN_MIME_TYPES.put(0x47, "application/vnd.oma.dd+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x48, "application/vnd.oma.drm.message");
+        WELL_KNOWN_MIME_TYPES.put(0x49, "application/vnd.oma.drm.content");
+        WELL_KNOWN_MIME_TYPES.put(0x4A, "application/vnd.oma.drm.rights+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x4B, "application/vnd.oma.drm.rights+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x4C, "application/vnd.wv.csp+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x4D, "application/vnd.wv.csp+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x4E, "application/vnd.syncml.ds.notification");
+        WELL_KNOWN_MIME_TYPES.put(0x4F, "audio/*");
+        WELL_KNOWN_MIME_TYPES.put(0x50, "video/*");
+        WELL_KNOWN_MIME_TYPES.put(0x51, "application/vnd.oma.dd2+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x52, "application/mikey");
+        WELL_KNOWN_MIME_TYPES.put(0x53, "application/vnd.oma.dcd");
+        WELL_KNOWN_MIME_TYPES.put(0x54, "application/vnd.oma.dcdc");
+
+        WELL_KNOWN_MIME_TYPES.put(0x0201, "application/vnd.uplanet.cacheop-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0202, "application/vnd.uplanet.signal");
+        WELL_KNOWN_MIME_TYPES.put(0x0203, "application/vnd.uplanet.alert-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0204, "application/vnd.uplanet.list-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0205, "application/vnd.uplanet.listcmd-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0206, "application/vnd.uplanet.channel-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0207, "application/vnd.uplanet.provisioning-status-uri");
+        WELL_KNOWN_MIME_TYPES.put(0x0208, "x-wap.multipart/vnd.uplanet.header-set");
+        WELL_KNOWN_MIME_TYPES.put(0x0209, "application/vnd.uplanet.bearer-choice-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x020A, "application/vnd.phonecom.mmc-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x020B, "application/vnd.nokia.syncset+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x020C, "image/x-up-wpng");
+        WELL_KNOWN_MIME_TYPES.put(0x0300, "application/iota.mmc-wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0301, "application/iota.mmc-xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0302, "application/vnd.syncml+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0303, "application/vnd.syncml+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0304, "text/vnd.wap.emn+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0305, "text/calendar");
+        WELL_KNOWN_MIME_TYPES.put(0x0306, "application/vnd.omads-email+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0307, "application/vnd.omads-file+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0308, "application/vnd.omads-folder+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0309, "text/directory;profile=vCard");
+        WELL_KNOWN_MIME_TYPES.put(0x030A, "application/vnd.wap.emn+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x030B, "application/vnd.nokia.ipdc-purchase-response");
+        WELL_KNOWN_MIME_TYPES.put(0x030C, "application/vnd.motorola.screen3+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x030D, "application/vnd.motorola.screen3+gzip");
+        WELL_KNOWN_MIME_TYPES.put(0x030E, "application/vnd.cmcc.setting+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x030F, "application/vnd.cmcc.bombing+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0310, "application/vnd.docomo.pf");
+        WELL_KNOWN_MIME_TYPES.put(0x0311, "application/vnd.docomo.ub");
+        WELL_KNOWN_MIME_TYPES.put(0x0312, "application/vnd.omaloc-supl-init");
+        WELL_KNOWN_MIME_TYPES.put(0x0313, "application/vnd.oma.group-usage-list+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0314, "application/oma-directory+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x0315, "application/vnd.docomo.pf2");
+        WELL_KNOWN_MIME_TYPES.put(0x0316, "application/vnd.oma.drm.roap-trigger+wbxml");
+        WELL_KNOWN_MIME_TYPES.put(0x0317, "application/vnd.sbm.mid2");
+        WELL_KNOWN_MIME_TYPES.put(0x0318, "application/vnd.wmf.bootstrap");
+        WELL_KNOWN_MIME_TYPES.put(0x0319, "application/vnc.cmcc.dcd+xml");
+        WELL_KNOWN_MIME_TYPES.put(0x031A, "application/vnd.sbm.cid");
+        WELL_KNOWN_MIME_TYPES.put(0x031B, "application/vnd.oma.bcast.provisioningtrigger");
+
+        WELL_KNOWN_PARAMETERS.put(0x00, "Q");
+        WELL_KNOWN_PARAMETERS.put(0x01, "Charset");
+        WELL_KNOWN_PARAMETERS.put(0x02, "Level");
+        WELL_KNOWN_PARAMETERS.put(0x03, "Type");
+        WELL_KNOWN_PARAMETERS.put(0x07, "Differences");
+        WELL_KNOWN_PARAMETERS.put(0x08, "Padding");
+        WELL_KNOWN_PARAMETERS.put(0x09, "Type");
+        WELL_KNOWN_PARAMETERS.put(0x0E, "Max-Age");
+        WELL_KNOWN_PARAMETERS.put(0x10, "Secure");
+        WELL_KNOWN_PARAMETERS.put(0x11, "SEC");
+        WELL_KNOWN_PARAMETERS.put(0x12, "MAC");
+        WELL_KNOWN_PARAMETERS.put(0x13, "Creation-date");
+        WELL_KNOWN_PARAMETERS.put(0x14, "Modification-date");
+        WELL_KNOWN_PARAMETERS.put(0x15, "Read-date");
+        WELL_KNOWN_PARAMETERS.put(0x16, "Size");
+        WELL_KNOWN_PARAMETERS.put(0x17, "Name");
+        WELL_KNOWN_PARAMETERS.put(0x18, "Filename");
+        WELL_KNOWN_PARAMETERS.put(0x19, "Start");
+        WELL_KNOWN_PARAMETERS.put(0x1A, "Start-info");
+        WELL_KNOWN_PARAMETERS.put(0x1B, "Comment");
+        WELL_KNOWN_PARAMETERS.put(0x1C, "Domain");
+        WELL_KNOWN_PARAMETERS.put(0x1D, "Path");
+    }
+
+    public static final String CONTENT_TYPE_B_PUSH_CO = "application/vnd.wap.coc";
+    public static final String CONTENT_TYPE_B_MMS = "application/vnd.wap.mms-message";
+    public static final String CONTENT_TYPE_B_PUSH_SYNCML_NOTI = "application/vnd.syncml.notification";
+
+    byte[] mWspData;
+    int    mDataLength;
+    long   mUnsigned32bit;
+    String mStringValue;
+
+    HashMap<String, String> mContentParameters;
+
+    public WspTypeDecoder(byte[] pdu) {
+        mWspData = pdu;
+    }
+
+    /**
+     * Decode the "Text-string" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Text-string" in this pdu
+     *
+     * @return false when error(not a Text-string) occur
+     *         return value can be retrieved by getValueString() method length of data in pdu can be
+     *         retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeTextString(int startIndex) {
+        int index = startIndex;
+        while (mWspData[index] != 0) {
+            index++;
+        }
+        mDataLength = index - startIndex + 1;
+        if (mWspData[startIndex] == 127) {
+            mStringValue = new String(mWspData, startIndex + 1, mDataLength - 2);
+        } else {
+            mStringValue = new String(mWspData, startIndex, mDataLength - 1);
+        }
+        return true;
+    }
+
+    /**
+     * Decode the "Token-text" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Token-text" in this pdu
+     *
+     * @return always true
+     *         return value can be retrieved by getValueString() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeTokenText(int startIndex) {
+        int index = startIndex;
+        while (mWspData[index] != 0) {
+            index++;
+        }
+        mDataLength = index - startIndex + 1;
+        mStringValue = new String(mWspData, startIndex, mDataLength - 1);
+
+        return true;
+    }
+
+    /**
+     * Decode the "Short-integer" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Short-integer" in this pdu
+     *
+     * @return false when error(not a Short-integer) occur
+     *         return value can be retrieved by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeShortInteger(int startIndex) {
+        if ((mWspData[startIndex] & 0x80) == 0) {
+            return false;
+        }
+        mUnsigned32bit = mWspData[startIndex] & 0x7f;
+        mDataLength = 1;
+        return true;
+    }
+
+    /**
+     * Decode the "Long-integer" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Long-integer" in this pdu
+     *
+     * @return false when error(not a Long-integer) occur
+     *         return value can be retrieved by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeLongInteger(int startIndex) {
+        int lengthMultiOctet = mWspData[startIndex] & 0xff;
+
+        if (lengthMultiOctet > WAP_PDU_SHORT_LENGTH_MAX) {
+            return false;
+        }
+        mUnsigned32bit = 0;
+        for (int i = 1; i <= lengthMultiOctet; i++) {
+            mUnsigned32bit = (mUnsigned32bit << 8) | (mWspData[startIndex + i] & 0xff);
+        }
+        mDataLength = 1 + lengthMultiOctet;
+        return true;
+    }
+
+    /**
+     * Decode the "Integer-Value" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Integer-Value" in this pdu
+     *
+     * @return false when error(not a Integer-Value) occur
+     *         return value can be retrieved by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeIntegerValue(int startIndex) {
+        if (decodeShortInteger(startIndex) == true) {
+            return true;
+        }
+        return decodeLongInteger(startIndex);
+    }
+
+    /**
+     * Decode the "Uintvar-integer" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Uintvar-integer" in this pdu
+     *
+     * @return false when error(not a Uintvar-integer) occur
+     *         return value can be retrieved by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeUintvarInteger(int startIndex) {
+        int index = startIndex;
+
+        mUnsigned32bit = 0;
+        while ((mWspData[index] & 0x80) != 0) {
+            if ((index - startIndex) >= 4) {
+                return false;
+            }
+            mUnsigned32bit = (mUnsigned32bit << 7) | (mWspData[index] & 0x7f);
+            index++;
+        }
+        mUnsigned32bit = (mUnsigned32bit << 7) | (mWspData[index] & 0x7f);
+        mDataLength = index - startIndex + 1;
+        return true;
+    }
+
+    /**
+     * Decode the "Value-length" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Value-length" in this pdu
+     *
+     * @return false when error(not a Value-length) occur
+     *         return value can be retrieved by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeValueLength(int startIndex) {
+        if ((mWspData[startIndex] & 0xff) > WAP_PDU_LENGTH_QUOTE) {
+            return false;
+        }
+        if (mWspData[startIndex] < WAP_PDU_LENGTH_QUOTE) {
+            mUnsigned32bit = mWspData[startIndex];
+            mDataLength = 1;
+        } else {
+            decodeUintvarInteger(startIndex + 1);
+            mDataLength++;
+        }
+        return true;
+    }
+
+    /**
+     * Decode the "Extension-media" type for WSP PDU.
+     *
+     * @param startIndex The starting position of the "Extension-media" in this PDU.
+     *
+     * @return false on error, such as if there is no Extension-media at startIndex.
+     *         Side-effects: updates stringValue (available with
+     *         getValueString()), which will be null on error. The length of the
+     *         data in the PDU is available with getValue32(), 0 on error.
+     */
+    public boolean decodeExtensionMedia(int startIndex) {
+        int index = startIndex;
+        mDataLength = 0;
+        mStringValue = null;
+        int length = mWspData.length;
+        boolean rtrn = index < length;
+
+        while (index < length && mWspData[index] != 0) {
+            index++;
+        }
+
+        mDataLength = index - startIndex + 1;
+        mStringValue = new String(mWspData, startIndex, mDataLength - 1);
+
+        return rtrn;
+    }
+
+    /**
+     * Decode the "Constrained-encoding" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Constrained-encoding" in this pdu
+     *
+     * @return false when error(not a Constrained-encoding) occur
+     *         return value can be retrieved first by getValueString() and second by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeConstrainedEncoding(int startIndex) {
+        if (decodeShortInteger(startIndex) == true) {
+            mStringValue = null;
+            return true;
+        }
+        return decodeExtensionMedia(startIndex);
+    }
+
+    /**
+     * Decode the "Content-type" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Content-type" in this pdu
+     *
+     * @return false when error(not a Content-type) occurs
+     *         If a content type exists in the headers (either as inline string, or as well-known
+     *         value), getValueString() will return it. If a 'well known value' is encountered that
+     *         cannot be mapped to a string mime type, getValueString() will return null, and
+     *         getValue32() will return the unknown content type value.
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     *         Any content type parameters will be accessible via getContentParameters()
+     */
+    public boolean decodeContentType(int startIndex) {
+        int mediaPrefixLength;
+        mContentParameters = new HashMap<String, String>();
+
+        try {
+            if (decodeValueLength(startIndex) == false) {
+                boolean found = decodeConstrainedEncoding(startIndex);
+                if (found) {
+                    expandWellKnownMimeType();
+                }
+                return found;
+            }
+            int headersLength = (int) mUnsigned32bit;
+            mediaPrefixLength = getDecodedDataLength();
+            if (decodeIntegerValue(startIndex + mediaPrefixLength) == true) {
+                mDataLength += mediaPrefixLength;
+                int readLength = mDataLength;
+                mStringValue = null;
+                expandWellKnownMimeType();
+                long wellKnownValue = mUnsigned32bit;
+                String mimeType = mStringValue;
+                if (readContentParameters(startIndex + mDataLength,
+                        (headersLength - (mDataLength - mediaPrefixLength)), 0)) {
+                    mDataLength += readLength;
+                    mUnsigned32bit = wellKnownValue;
+                    mStringValue = mimeType;
+                    return true;
+                }
+                return false;
+            }
+            if (decodeExtensionMedia(startIndex + mediaPrefixLength) == true) {
+                mDataLength += mediaPrefixLength;
+                int readLength = mDataLength;
+                expandWellKnownMimeType();
+                long wellKnownValue = mUnsigned32bit;
+                String mimeType = mStringValue;
+                if (readContentParameters(startIndex + mDataLength,
+                        (headersLength - (mDataLength - mediaPrefixLength)), 0)) {
+                    mDataLength += readLength;
+                    mUnsigned32bit = wellKnownValue;
+                    mStringValue = mimeType;
+                    return true;
+                }
+            }
+        } catch (ArrayIndexOutOfBoundsException e) {
+            //something doesn't add up
+            return false;
+        }
+        return false;
+    }
+
+    private boolean readContentParameters(int startIndex, int leftToRead, int accumulator) {
+
+        int totalRead = 0;
+
+        if (leftToRead > 0) {
+            byte nextByte = mWspData[startIndex];
+            String value = null;
+            String param = null;
+            if ((nextByte & 0x80) == 0x00 && nextByte > 31) { // untyped
+                decodeTokenText(startIndex);
+                param = mStringValue;
+                totalRead += mDataLength;
+            } else { // typed
+                if (decodeIntegerValue(startIndex)) {
+                    totalRead += mDataLength;
+                    int wellKnownParameterValue = (int) mUnsigned32bit;
+                    param = WELL_KNOWN_PARAMETERS.get(wellKnownParameterValue);
+                    if (param == null) {
+                        param = "unassigned/0x" + Long.toHexString(wellKnownParameterValue);
+                    }
+                    // special case for the "Q" parameter, value is a uintvar
+                    if (wellKnownParameterValue == Q_VALUE) {
+                        if (decodeUintvarInteger(startIndex + totalRead)) {
+                            totalRead += mDataLength;
+                            value = String.valueOf(mUnsigned32bit);
+                            mContentParameters.put(param, value);
+                            return readContentParameters(startIndex + totalRead, leftToRead
+                                                            - totalRead, accumulator + totalRead);
+                        } else {
+                            return false;
+                        }
+                    }
+                } else {
+                    return false;
+                }
+            }
+
+            if (decodeNoValue(startIndex + totalRead)) {
+                totalRead += mDataLength;
+                value = null;
+            } else if (decodeIntegerValue(startIndex + totalRead)) {
+                totalRead += mDataLength;
+                int intValue = (int) mUnsigned32bit;
+                value = String.valueOf(intValue);
+            } else {
+                decodeTokenText(startIndex + totalRead);
+                totalRead += mDataLength;
+                value = mStringValue;
+                if (value.startsWith("\"")) {
+                    // quoted string, so remove the quote
+                    value = value.substring(1);
+                }
+            }
+            mContentParameters.put(param, value);
+            return readContentParameters(startIndex + totalRead, leftToRead - totalRead,
+                                            accumulator + totalRead);
+
+        } else {
+            mDataLength = accumulator;
+            return true;
+        }
+    }
+
+    /**
+     * Check if the next byte is No-Value
+     *
+     * @param startIndex The starting position of the "Content length" in this pdu
+     *
+     * @return true if and only if the next byte is 0x00
+     */
+    private boolean decodeNoValue(int startIndex) {
+        if (mWspData[startIndex] == 0) {
+            mDataLength = 1;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Populate stringValue with the mime type corresponding to the value in unsigned32bit
+     *
+     * Sets unsigned32bit to -1 if stringValue is already populated
+     */
+    private void expandWellKnownMimeType() {
+        if (mStringValue == null) {
+            int binaryContentType = (int) mUnsigned32bit;
+            mStringValue = WELL_KNOWN_MIME_TYPES.get(binaryContentType);
+        } else {
+            mUnsigned32bit = -1;
+        }
+    }
+
+    /**
+     * Decode the "Content length" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Content length" in this pdu
+     *
+     * @return false when error(not a Content length) occur
+     *         return value can be retrieved by getValue32() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeContentLength(int startIndex) {
+        return decodeIntegerValue(startIndex);
+    }
+
+    /**
+     * Decode the "Content location" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "Content location" in this pdu
+     *
+     * @return false when error(not a Content location) occur
+     *         return value can be retrieved by getValueString() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeContentLocation(int startIndex) {
+        return decodeTextString(startIndex);
+    }
+
+    /**
+     * Decode the "X-Wap-Application-Id" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "X-Wap-Application-Id" in this pdu
+     *
+     * @return false when error(not a X-Wap-Application-Id) occur
+     *         return value can be retrieved first by getValueString() and second by getValue32()
+     *         method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeXWapApplicationId(int startIndex) {
+        if (decodeIntegerValue(startIndex) == true) {
+            mStringValue = null;
+            return true;
+        }
+        return decodeTextString(startIndex);
+    }
+
+    /**
+     * Seek for the "X-Wap-Application-Id" field for WSP pdu
+     *
+     * @param startIndex The starting position of seek pointer
+     * @param endIndex Valid seek area end point
+     *
+     * @return false when error(not a X-Wap-Application-Id) occur
+     *         return value can be retrieved by getValue32()
+     */
+    public boolean seekXWapApplicationId(int startIndex, int endIndex) {
+        int index = startIndex;
+
+        try {
+            for (index = startIndex; index <= endIndex; ) {
+                /**
+                 * 8.4.1.1  Field name
+                 * Field name is integer or text.
+                 */
+                if (decodeIntegerValue(index)) {
+                    int fieldValue = (int) getValue32();
+
+                    if (fieldValue == PARAMETER_ID_X_WAP_APPLICATION_ID) {
+                        mUnsigned32bit = index + 1;
+                        return true;
+                    }
+                } else {
+                    if (!decodeTextString(index)) return false;
+                }
+                index += getDecodedDataLength();
+                if (index > endIndex) return false;
+
+                /**
+                 * 8.4.1.2 Field values
+                 * Value Interpretation of First Octet
+                 * 0 - 30 This octet is followed by the indicated number (0 - 30)
+                        of data octets
+                 * 31 This octet is followed by a uintvar, which indicates the number
+                 *      of data octets after it
+                 * 32 - 127 The value is a text string, terminated by a zero octet
+                        (NUL character)
+                 * 128 - 255 It is an encoded 7-bit value; this header has no more data
+                 */
+                byte val = mWspData[index];
+                if (0 <= val && val <= WAP_PDU_SHORT_LENGTH_MAX) {
+                    index += mWspData[index] + 1;
+                } else if (val == WAP_PDU_LENGTH_QUOTE) {
+                    if (index + 1 >= endIndex) return false;
+                    index++;
+                    if (!decodeUintvarInteger(index)) return false;
+                    index += getDecodedDataLength();
+                } else if (WAP_PDU_LENGTH_QUOTE < val && val <= 127) {
+                    if (!decodeTextString(index)) return false;
+                    index += getDecodedDataLength();
+                } else {
+                    index++;
+                }
+            }
+        } catch (ArrayIndexOutOfBoundsException e) {
+            //seek application ID failed. WSP header might be corrupted
+            return false;
+        }
+        return false;
+    }
+
+    /**
+     * Decode the "X-Wap-Content-URI" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "X-Wap-Content-URI" in this pdu
+     *
+     * @return false when error(not a X-Wap-Content-URI) occur
+     *         return value can be retrieved by getValueString() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeXWapContentURI(int startIndex) {
+        return decodeTextString(startIndex);
+    }
+
+    /**
+     * Decode the "X-Wap-Initiator-URI" type for WSP pdu
+     *
+     * @param startIndex The starting position of the "X-Wap-Initiator-URI" in this pdu
+     *
+     * @return false when error(not a X-Wap-Initiator-URI) occur
+     *         return value can be retrieved by getValueString() method
+     *         length of data in pdu can be retrieved by getDecodedDataLength() method
+     */
+    public boolean decodeXWapInitiatorURI(int startIndex) {
+        return decodeTextString(startIndex);
+    }
+
+    /**
+     * The data length of latest operation.
+     */
+    public int getDecodedDataLength() {
+        return mDataLength;
+    }
+
+    /**
+     * The 32-bits result of latest operation.
+     */
+    public long getValue32() {
+        return mUnsigned32bit;
+    }
+
+    /**
+     * The String result of latest operation.
+     */
+    public String getValueString() {
+        return mStringValue;
+    }
+
+    /**
+     * Any parameters encountered as part of a decodeContentType() invocation.
+     *
+     * @return a map of content parameters keyed by their names, or null if
+     *         decodeContentType() has not been called If any unassigned
+     *         well-known parameters are encountered, the key of the map will be
+     *         'unassigned/0x...', where '...' is the hex value of the
+     *         unassigned parameter.  If a parameter has No-Value the value will be null.
+     *
+     */
+    public HashMap<String, String> getContentParameters() {
+        return mContentParameters;
+    }
+}
diff --git a/com/android/internal/telephony/cat/AppInterface.java b/com/android/internal/telephony/cat/AppInterface.java
new file mode 100644
index 0000000..c78b7f8
--- /dev/null
+++ b/com/android/internal/telephony/cat/AppInterface.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import android.content.ComponentName;
+
+/**
+ * Interface for communication between STK App and CAT Telephony
+ *
+ * {@hide}
+ */
+public interface AppInterface {
+
+    /*
+     * Intent's actions which are broadcasted by the Telephony once a new CAT
+     * proactive command, session end, ALPHA during STK CC arrive.
+     */
+    public static final String CAT_CMD_ACTION =
+                                    "com.android.internal.stk.command";
+    public static final String CAT_SESSION_END_ACTION =
+                                    "com.android.internal.stk.session_end";
+    public static final String CAT_ALPHA_NOTIFY_ACTION =
+                                    "com.android.internal.stk.alpha_notify";
+
+    //This is used to send ALPHA string from card to STK App.
+    public static final String ALPHA_STRING = "alpha_string";
+
+    // This is used to send refresh-result when MSG_ID_ICC_REFRESH is received.
+    public static final String REFRESH_RESULT = "refresh_result";
+    //This is used to send card status from card to STK App.
+    public static final String CARD_STATUS = "card_status";
+    //Intent's actions are broadcasted by Telephony once IccRefresh occurs.
+    public static final String CAT_ICC_STATUS_CHANGE =
+                                    "com.android.internal.stk.icc_status_change";
+
+    // Permission required by STK command receiver
+    public static final String STK_PERMISSION = "android.permission.RECEIVE_STK_COMMANDS";
+
+    // Only forwards cat broadcast to the system default stk app
+    public static ComponentName getDefaultSTKApplication() {
+        return ComponentName.unflattenFromString("com.android.stk/.StkCmdReceiver");
+    }
+
+    /*
+     * Callback function from app to telephony to pass a result code and user's
+     * input back to the ICC.
+     */
+    void onCmdResponse(CatResponseMessage resMsg);
+
+    /*
+     * Enumeration for representing "Type of Command" of proactive commands.
+     * Those are the only commands which are supported by the Telephony. Any app
+     * implementation should support those.
+     * Refer to ETSI TS 102.223 section 9.4
+     */
+    public static enum CommandType {
+        DISPLAY_TEXT(0x21),
+        GET_INKEY(0x22),
+        GET_INPUT(0x23),
+        LAUNCH_BROWSER(0x15),
+        PLAY_TONE(0x20),
+        REFRESH(0x01),
+        SELECT_ITEM(0x24),
+        SEND_SS(0x11),
+        SEND_USSD(0x12),
+        SEND_SMS(0x13),
+        SEND_DTMF(0x14),
+        SET_UP_EVENT_LIST(0x05),
+        SET_UP_IDLE_MODE_TEXT(0x28),
+        SET_UP_MENU(0x25),
+        SET_UP_CALL(0x10),
+        PROVIDE_LOCAL_INFORMATION(0x26),
+        OPEN_CHANNEL(0x40),
+        CLOSE_CHANNEL(0x41),
+        RECEIVE_DATA(0x42),
+        SEND_DATA(0x43),
+        GET_CHANNEL_STATUS(0x44);
+
+        private int mValue;
+
+        CommandType(int value) {
+            mValue = value;
+        }
+
+        public int value() {
+            return mValue;
+        }
+
+        /**
+         * Create a CommandType object.
+         *
+         * @param value Integer value to be converted to a CommandType object.
+         * @return CommandType object whose "Type of Command" value is {@code
+         *         value}. If no CommandType object has that value, null is
+         *         returned.
+         */
+        public static CommandType fromInt(int value) {
+            for (CommandType e : CommandType.values()) {
+                if (e.mValue == value) {
+                    return e;
+                }
+            }
+            return null;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cat/BerTlv.java b/com/android/internal/telephony/cat/BerTlv.java
new file mode 100644
index 0000000..c264c11
--- /dev/null
+++ b/com/android/internal/telephony/cat/BerTlv.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import java.util.List;
+
+/**
+ * Class for representing BER-TLV objects.
+ *
+ * @see "ETSI TS 102 223 Annex C" for more information.
+ *
+ * {@hide}
+ */
+class BerTlv {
+    private int mTag = BER_UNKNOWN_TAG;
+    private List<ComprehensionTlv> mCompTlvs = null;
+    private boolean mLengthValid = true;
+
+    public static final int BER_UNKNOWN_TAG             = 0x00;
+    public static final int BER_PROACTIVE_COMMAND_TAG   = 0xd0;
+    public static final int BER_MENU_SELECTION_TAG      = 0xd3;
+    public static final int BER_EVENT_DOWNLOAD_TAG      = 0xd6;
+
+    private BerTlv(int tag, List<ComprehensionTlv> ctlvs, boolean lengthValid) {
+        mTag = tag;
+        mCompTlvs = ctlvs;
+        mLengthValid = lengthValid;
+    }
+
+    /**
+     * Gets a list of ComprehensionTlv objects contained in this BER-TLV object.
+     *
+     * @return A list of COMPREHENSION-TLV object
+     */
+    public List<ComprehensionTlv> getComprehensionTlvs() {
+        return mCompTlvs;
+    }
+
+    /**
+     * Gets a tag id of the BER-TLV object.
+     *
+     * @return A tag integer.
+     */
+    public int getTag() {
+        return mTag;
+    }
+
+    /**
+     * Gets if the length of the BER-TLV object is valid
+     *
+     * @return if length valid
+     */
+     public boolean isLengthValid() {
+         return mLengthValid;
+     }
+
+    /**
+     * Decodes a BER-TLV object from a byte array.
+     *
+     * @param data A byte array to decode from
+     * @return A BER-TLV object decoded
+     * @throws ResultException
+     */
+    public static BerTlv decode(byte[] data) throws ResultException {
+        int curIndex = 0;
+        int endIndex = data.length;
+        int tag, length = 0;
+        boolean isLengthValid = true;
+
+        try {
+            /* tag */
+            tag = data[curIndex++] & 0xff;
+            if (tag == BER_PROACTIVE_COMMAND_TAG) {
+                /* length */
+                int temp = data[curIndex++] & 0xff;
+                if (temp < 0x80) {
+                    length = temp;
+                } else if (temp == 0x81) {
+                    temp = data[curIndex++] & 0xff;
+                    if (temp < 0x80) {
+                        throw new ResultException(
+                                ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                                "length < 0x80 length=" + Integer.toHexString(length) +
+                                " curIndex=" + curIndex + " endIndex=" + endIndex);
+
+                    }
+                    length = temp;
+                } else {
+                    throw new ResultException(
+                            ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                            "Expected first byte to be length or a length tag and < 0x81" +
+                            " byte= " + Integer.toHexString(temp) + " curIndex=" + curIndex +
+                            " endIndex=" + endIndex);
+                }
+            } else {
+                if (ComprehensionTlvTag.COMMAND_DETAILS.value() == (tag & ~0x80)) {
+                    tag = BER_UNKNOWN_TAG;
+                    curIndex = 0;
+                }
+            }
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING,
+                    "IndexOutOfBoundsException " +
+                    " curIndex=" + curIndex + " endIndex=" + endIndex);
+        } catch (ResultException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD, e.explanation());
+        }
+
+        /* COMPREHENSION-TLVs */
+        if (endIndex - curIndex < length) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                    "Command had extra data endIndex=" + endIndex + " curIndex=" + curIndex +
+                    " length=" + length);
+        }
+
+        List<ComprehensionTlv> ctlvs = ComprehensionTlv.decodeMany(data,
+                curIndex);
+
+        if (tag == BER_PROACTIVE_COMMAND_TAG) {
+            int totalLength = 0;
+            for (ComprehensionTlv item : ctlvs) {
+                int itemLength = item.getLength();
+                if (itemLength >= 0x80 && itemLength <= 0xFF) {
+                    totalLength += itemLength + 3; //3: 'tag'(1 byte) and 'length'(2 bytes).
+                } else if (itemLength >= 0 && itemLength < 0x80) {
+                    totalLength += itemLength + 2; //2: 'tag'(1 byte) and 'length'(1 byte).
+                } else {
+                    isLengthValid = false;
+                    break;
+                }
+            }
+
+            // According to 3gpp11.14, chapter 6.10.6 "Length errors",
+
+            // If the total lengths of the SIMPLE-TLV data objects are not
+            // consistent with the length given in the BER-TLV data object,
+            // then the whole BER-TLV data object shall be rejected. The
+            // result field in the TERMINAL RESPONSE shall have the error
+            // condition "Command data not understood by ME".
+            if (length != totalLength) {
+                isLengthValid = false;
+            }
+        }
+
+        return new BerTlv(tag, ctlvs, isLengthValid);
+    }
+}
diff --git a/com/android/internal/telephony/cat/CatCmdMessage.java b/com/android/internal/telephony/cat/CatCmdMessage.java
new file mode 100644
index 0000000..62d8869
--- /dev/null
+++ b/com/android/internal/telephony/cat/CatCmdMessage.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Class used to pass CAT messages from telephony to application. Application
+ * should call getXXX() to get commands's specific values.
+ *
+ */
+public class CatCmdMessage implements Parcelable {
+    // members
+    CommandDetails mCmdDet;
+    private TextMessage mTextMsg;
+    private Menu mMenu;
+    private Input mInput;
+    private BrowserSettings mBrowserSettings = null;
+    private ToneSettings mToneSettings = null;
+    private CallSettings mCallSettings = null;
+    private SetupEventListSettings mSetupEventListSettings = null;
+    private boolean mLoadIconFailed = false;
+
+    /*
+     * Container for Launch Browser command settings.
+     */
+    public class BrowserSettings {
+        public String url;
+        public LaunchBrowserMode mode;
+    }
+
+    /*
+     * Container for Call Setup command settings.
+     */
+    public class CallSettings {
+        public TextMessage confirmMsg;
+        public TextMessage callMsg;
+    }
+
+    public class SetupEventListSettings {
+        public int[] eventList;
+    }
+
+    public final class SetupEventListConstants {
+        // Event values in SETUP_EVENT_LIST Proactive Command as per ETSI 102.223
+        public static final int USER_ACTIVITY_EVENT          = 0x04;
+        public static final int IDLE_SCREEN_AVAILABLE_EVENT  = 0x05;
+        public static final int LANGUAGE_SELECTION_EVENT     = 0x07;
+        public static final int BROWSER_TERMINATION_EVENT    = 0x08;
+        public static final int BROWSING_STATUS_EVENT        = 0x0F;
+    }
+
+    public final class BrowserTerminationCauses {
+        public static final int USER_TERMINATION             = 0x00;
+        public static final int ERROR_TERMINATION            = 0x01;
+    }
+
+    CatCmdMessage(CommandParams cmdParams) {
+        mCmdDet = cmdParams.mCmdDet;
+        mLoadIconFailed =  cmdParams.mLoadIconFailed;
+        switch(getCmdType()) {
+        case SET_UP_MENU:
+        case SELECT_ITEM:
+            mMenu = ((SelectItemParams) cmdParams).mMenu;
+            break;
+        case DISPLAY_TEXT:
+        case SET_UP_IDLE_MODE_TEXT:
+        case SEND_DTMF:
+        case SEND_SMS:
+        case SEND_SS:
+        case SEND_USSD:
+            mTextMsg = ((DisplayTextParams) cmdParams).mTextMsg;
+            break;
+        case GET_INPUT:
+        case GET_INKEY:
+            mInput = ((GetInputParams) cmdParams).mInput;
+            break;
+        case LAUNCH_BROWSER:
+            mTextMsg = ((LaunchBrowserParams) cmdParams).mConfirmMsg;
+            mBrowserSettings = new BrowserSettings();
+            mBrowserSettings.url = ((LaunchBrowserParams) cmdParams).mUrl;
+            mBrowserSettings.mode = ((LaunchBrowserParams) cmdParams).mMode;
+            break;
+        case PLAY_TONE:
+            PlayToneParams params = (PlayToneParams) cmdParams;
+            mToneSettings = params.mSettings;
+            mTextMsg = params.mTextMsg;
+            break;
+        case GET_CHANNEL_STATUS:
+            mTextMsg = ((CallSetupParams) cmdParams).mConfirmMsg;
+            break;
+        case SET_UP_CALL:
+            mCallSettings = new CallSettings();
+            mCallSettings.confirmMsg = ((CallSetupParams) cmdParams).mConfirmMsg;
+            mCallSettings.callMsg = ((CallSetupParams) cmdParams).mCallMsg;
+            break;
+        case OPEN_CHANNEL:
+        case CLOSE_CHANNEL:
+        case RECEIVE_DATA:
+        case SEND_DATA:
+            BIPClientParams param = (BIPClientParams) cmdParams;
+            mTextMsg = param.mTextMsg;
+            break;
+        case SET_UP_EVENT_LIST:
+            mSetupEventListSettings = new SetupEventListSettings();
+            mSetupEventListSettings.eventList = ((SetEventListParams) cmdParams).mEventInfo;
+            break;
+        case PROVIDE_LOCAL_INFORMATION:
+        case REFRESH:
+        default:
+            break;
+        }
+    }
+
+    public CatCmdMessage(Parcel in) {
+        mCmdDet = in.readParcelable(null);
+        mTextMsg = in.readParcelable(null);
+        mMenu = in.readParcelable(null);
+        mInput = in.readParcelable(null);
+        mLoadIconFailed = (in.readByte() == 1);
+        switch (getCmdType()) {
+        case LAUNCH_BROWSER:
+            mBrowserSettings = new BrowserSettings();
+            mBrowserSettings.url = in.readString();
+            mBrowserSettings.mode = LaunchBrowserMode.values()[in.readInt()];
+            break;
+        case PLAY_TONE:
+            mToneSettings = in.readParcelable(null);
+            break;
+        case SET_UP_CALL:
+            mCallSettings = new CallSettings();
+            mCallSettings.confirmMsg = in.readParcelable(null);
+            mCallSettings.callMsg = in.readParcelable(null);
+            break;
+        case SET_UP_EVENT_LIST:
+            mSetupEventListSettings = new SetupEventListSettings();
+            int length = in.readInt();
+            mSetupEventListSettings.eventList = new int[length];
+            for (int i = 0; i < length; i++) {
+                mSetupEventListSettings.eventList[i] = in.readInt();
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mCmdDet, 0);
+        dest.writeParcelable(mTextMsg, 0);
+        dest.writeParcelable(mMenu, 0);
+        dest.writeParcelable(mInput, 0);
+        dest.writeByte((byte) (mLoadIconFailed ? 1 : 0));
+        switch(getCmdType()) {
+        case LAUNCH_BROWSER:
+            dest.writeString(mBrowserSettings.url);
+            dest.writeInt(mBrowserSettings.mode.ordinal());
+            break;
+        case PLAY_TONE:
+            dest.writeParcelable(mToneSettings, 0);
+            break;
+        case SET_UP_CALL:
+            dest.writeParcelable(mCallSettings.confirmMsg, 0);
+            dest.writeParcelable(mCallSettings.callMsg, 0);
+            break;
+        case SET_UP_EVENT_LIST:
+            dest.writeIntArray(mSetupEventListSettings.eventList);
+            break;
+        default:
+            break;
+        }
+    }
+
+    public static final Parcelable.Creator<CatCmdMessage> CREATOR = new Parcelable.Creator<CatCmdMessage>() {
+        @Override
+        public CatCmdMessage createFromParcel(Parcel in) {
+            return new CatCmdMessage(in);
+        }
+
+        @Override
+        public CatCmdMessage[] newArray(int size) {
+            return new CatCmdMessage[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /* external API to be used by application */
+    public AppInterface.CommandType getCmdType() {
+        return AppInterface.CommandType.fromInt(mCmdDet.typeOfCommand);
+    }
+
+    public Menu getMenu() {
+        return mMenu;
+    }
+
+    public Input geInput() {
+        return mInput;
+    }
+
+    public TextMessage geTextMessage() {
+        return mTextMsg;
+    }
+
+    public BrowserSettings getBrowserSettings() {
+        return mBrowserSettings;
+    }
+
+    public ToneSettings getToneSettings() {
+        return mToneSettings;
+    }
+
+    public CallSettings getCallSettings() {
+        return mCallSettings;
+    }
+
+    public SetupEventListSettings getSetEventList() {
+        return mSetupEventListSettings;
+    }
+
+    /**
+     * API to be used by application to check if loading optional icon
+     * has failed
+     */
+    public boolean hasIconLoadFailed() {
+        return mLoadIconFailed;
+    }
+}
diff --git a/com/android/internal/telephony/cat/CatException.java b/com/android/internal/telephony/cat/CatException.java
new file mode 100644
index 0000000..1bf1369
--- /dev/null
+++ b/com/android/internal/telephony/cat/CatException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import android.util.AndroidException;
+
+
+/**
+ * Base class for all the exceptions in CAT service.
+ *
+ * {@hide}
+ */
+class CatException extends AndroidException {
+    public CatException() {
+        super();
+    }
+}
diff --git a/com/android/internal/telephony/cat/CatLog.java b/com/android/internal/telephony/cat/CatLog.java
new file mode 100644
index 0000000..9a50fdb
--- /dev/null
+++ b/com/android/internal/telephony/cat/CatLog.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.telephony.Rlog;
+
+public abstract class CatLog {
+    static final boolean DEBUG = true;
+
+    public static void d(Object caller, String msg) {
+        if (!DEBUG) {
+            return;
+        }
+
+        String className = caller.getClass().getName();
+        Rlog.d("CAT", className.substring(className.lastIndexOf('.') + 1) + ": "
+                + msg);
+    }
+
+    public static void d(String caller, String msg) {
+        if (!DEBUG) {
+            return;
+        }
+
+        Rlog.d("CAT", caller + ": " + msg);
+    }
+    public static void e(Object caller, String msg) {
+        String className = caller.getClass().getName();
+        Rlog.e("CAT", className.substring(className.lastIndexOf('.') + 1) + ": "
+                + msg);
+    }
+
+    public static void e(String caller, String msg) {
+        Rlog.e("CAT", caller + ": " + msg);
+    }
+}
diff --git a/com/android/internal/telephony/cat/CatResponseMessage.java b/com/android/internal/telephony/cat/CatResponseMessage.java
new file mode 100644
index 0000000..80025f3
--- /dev/null
+++ b/com/android/internal/telephony/cat/CatResponseMessage.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+public class CatResponseMessage {
+        CommandDetails mCmdDet = null;
+        ResultCode mResCode  = ResultCode.OK;
+        int mUsersMenuSelection = 0;
+        String mUsersInput  = null;
+        boolean mUsersYesNoSelection = false;
+        boolean mUsersConfirm = false;
+        boolean mIncludeAdditionalInfo = false;
+        int mAdditionalInfo = 0;
+        int mEventValue = -1;
+        byte[] mAddedInfo = null;
+
+        public CatResponseMessage(CatCmdMessage cmdMsg) {
+            mCmdDet = cmdMsg.mCmdDet;
+        }
+
+        public void setResultCode(ResultCode resCode) {
+            mResCode = resCode;
+        }
+
+        public void setMenuSelection(int selection) {
+            mUsersMenuSelection = selection;
+        }
+
+        public void setInput(String input) {
+            mUsersInput = input;
+        }
+
+        public void setEventDownload(int event, byte[] addedInfo) {
+            this.mEventValue = event;
+            this.mAddedInfo = addedInfo;
+        }
+
+        public void setYesNo(boolean yesNo) {
+            mUsersYesNoSelection = yesNo;
+        }
+
+        public void setConfirmation(boolean confirm) {
+            mUsersConfirm = confirm;
+        }
+
+        public void setAdditionalInfo(int info) {
+            mIncludeAdditionalInfo = true;
+            mAdditionalInfo = info;
+        }
+
+        CommandDetails getCmdDetails() {
+            return mCmdDet;
+        }
+    }
diff --git a/com/android/internal/telephony/cat/CatService.java b/com/android/internal/telephony/cat/CatService.java
new file mode 100644
index 0000000..a242de4
--- /dev/null
+++ b/com/android/internal/telephony/cat/CatService.java
@@ -0,0 +1,1124 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources.NotFoundException;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
+import com.android.internal.telephony.uicc.IccRefreshResponse;
+import com.android.internal.telephony.uicc.UiccController;
+
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Locale;
+
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.IDLE_SCREEN_AVAILABLE_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.LANGUAGE_SELECTION_EVENT;
+
+class RilMessage {
+    int mId;
+    Object mData;
+    ResultCode mResCode;
+
+    RilMessage(int msgId, String rawData) {
+        mId = msgId;
+        mData = rawData;
+    }
+
+    RilMessage(RilMessage other) {
+        mId = other.mId;
+        mData = other.mData;
+        mResCode = other.mResCode;
+    }
+}
+
+/**
+ * Class that implements SIM Toolkit Telephony Service. Interacts with the RIL
+ * and application.
+ *
+ * {@hide}
+ */
+public class CatService extends Handler implements AppInterface {
+    private static final boolean DBG = false;
+
+    // Class members
+    private static IccRecords mIccRecords;
+    private static UiccCardApplication mUiccApplication;
+
+    // Service members.
+    // Protects singleton instance lazy initialization.
+    private static final Object sInstanceLock = new Object();
+    private static CatService[] sInstance = null;
+    private CommandsInterface mCmdIf;
+    private Context mContext;
+    private CatCmdMessage mCurrntCmd = null;
+    private CatCmdMessage mMenuCmd = null;
+
+    private RilMessageDecoder mMsgDecoder = null;
+    private boolean mStkAppInstalled = false;
+
+    private UiccController mUiccController;
+    private CardState mCardState = CardState.CARDSTATE_ABSENT;
+
+    // Service constants.
+    protected static final int MSG_ID_SESSION_END              = 1;
+    protected static final int MSG_ID_PROACTIVE_COMMAND        = 2;
+    protected static final int MSG_ID_EVENT_NOTIFY             = 3;
+    protected static final int MSG_ID_CALL_SETUP               = 4;
+    static final int MSG_ID_REFRESH                  = 5;
+    static final int MSG_ID_RESPONSE                 = 6;
+    static final int MSG_ID_SIM_READY                = 7;
+
+    protected static final int MSG_ID_ICC_CHANGED    = 8;
+    protected static final int MSG_ID_ALPHA_NOTIFY   = 9;
+
+    static final int MSG_ID_RIL_MSG_DECODED          = 10;
+
+    // Events to signal SIM presence or absent in the device.
+    private static final int MSG_ID_ICC_RECORDS_LOADED       = 20;
+
+    //Events to signal SIM REFRESH notificatations
+    private static final int MSG_ID_ICC_REFRESH  = 30;
+
+    private static final int DEV_ID_KEYPAD      = 0x01;
+    private static final int DEV_ID_DISPLAY     = 0x02;
+    private static final int DEV_ID_UICC        = 0x81;
+    private static final int DEV_ID_TERMINAL    = 0x82;
+    private static final int DEV_ID_NETWORK     = 0x83;
+
+    static final String STK_DEFAULT = "Default Message";
+
+    private HandlerThread mHandlerThread;
+    private int mSlotId;
+
+    /* For multisim catservice should not be singleton */
+    private CatService(CommandsInterface ci, UiccCardApplication ca, IccRecords ir,
+            Context context, IccFileHandler fh, UiccCard ic, int slotId) {
+        if (ci == null || ca == null || ir == null || context == null || fh == null
+                || ic == null) {
+            throw new NullPointerException(
+                    "Service: Input parameters must not be null");
+        }
+        mCmdIf = ci;
+        mContext = context;
+        mSlotId = slotId;
+        mHandlerThread = new HandlerThread("Cat Telephony service" + slotId);
+        mHandlerThread.start();
+
+        // Get the RilMessagesDecoder for decoding the messages.
+        mMsgDecoder = RilMessageDecoder.getInstance(this, fh, slotId);
+        if (null == mMsgDecoder) {
+            CatLog.d(this, "Null RilMessageDecoder instance");
+            return;
+        }
+        mMsgDecoder.start();
+
+        // Register ril events handling.
+        mCmdIf.setOnCatSessionEnd(this, MSG_ID_SESSION_END, null);
+        mCmdIf.setOnCatProactiveCmd(this, MSG_ID_PROACTIVE_COMMAND, null);
+        mCmdIf.setOnCatEvent(this, MSG_ID_EVENT_NOTIFY, null);
+        mCmdIf.setOnCatCallSetUp(this, MSG_ID_CALL_SETUP, null);
+        //mCmdIf.setOnSimRefresh(this, MSG_ID_REFRESH, null);
+
+        mCmdIf.registerForIccRefresh(this, MSG_ID_ICC_REFRESH, null);
+        mCmdIf.setOnCatCcAlphaNotify(this, MSG_ID_ALPHA_NOTIFY, null);
+
+        mIccRecords = ir;
+        mUiccApplication = ca;
+
+        // Register for SIM ready event.
+        mIccRecords.registerForRecordsLoaded(this, MSG_ID_ICC_RECORDS_LOADED, null);
+        CatLog.d(this, "registerForRecordsLoaded slotid=" + mSlotId + " instance:" + this);
+
+
+        mUiccController = UiccController.getInstance();
+        mUiccController.registerForIccChanged(this, MSG_ID_ICC_CHANGED, null);
+
+        // Check if STK application is available
+        mStkAppInstalled = isStkAppInstalled();
+
+        CatLog.d(this, "Running CAT service on Slotid: " + mSlotId +
+                ". STK app installed:" + mStkAppInstalled);
+    }
+
+    /**
+     * Used for instantiating the Service from the Card.
+     *
+     * @param ci CommandsInterface object
+     * @param context phone app context
+     * @param ic Icc card
+     * @param slotId to know the index of card
+     * @return The only Service object in the system
+     */
+    public static CatService getInstance(CommandsInterface ci,
+            Context context, UiccCard ic, int slotId) {
+        UiccCardApplication ca = null;
+        IccFileHandler fh = null;
+        IccRecords ir = null;
+        if (ic != null) {
+            /* Since Cat is not tied to any application, but rather is Uicc application
+             * in itself - just get first FileHandler and IccRecords object
+             */
+            ca = ic.getApplicationIndex(0);
+            if (ca != null) {
+                fh = ca.getIccFileHandler();
+                ir = ca.getIccRecords();
+            }
+        }
+
+        synchronized (sInstanceLock) {
+            if (sInstance == null) {
+                int simCount = TelephonyManager.getDefault().getSimCount();
+                sInstance = new CatService[simCount];
+                for (int i = 0; i < simCount; i++) {
+                    sInstance[i] = null;
+                }
+            }
+            if (sInstance[slotId] == null) {
+                if (ci == null || ca == null || ir == null || context == null || fh == null
+                        || ic == null) {
+                    return null;
+                }
+
+                sInstance[slotId] = new CatService(ci, ca, ir, context, fh, ic, slotId);
+            } else if ((ir != null) && (mIccRecords != ir)) {
+                if (mIccRecords != null) {
+                    mIccRecords.unregisterForRecordsLoaded(sInstance[slotId]);
+                }
+
+                mIccRecords = ir;
+                mUiccApplication = ca;
+
+                mIccRecords.registerForRecordsLoaded(sInstance[slotId],
+                        MSG_ID_ICC_RECORDS_LOADED, null);
+                CatLog.d(sInstance[slotId], "registerForRecordsLoaded slotid=" + slotId
+                        + " instance:" + sInstance[slotId]);
+            }
+            return sInstance[slotId];
+        }
+    }
+
+    public void dispose() {
+        synchronized (sInstanceLock) {
+            CatLog.d(this, "Disposing CatService object");
+            mIccRecords.unregisterForRecordsLoaded(this);
+
+            // Clean up stk icon if dispose is called
+            broadcastCardStateAndIccRefreshResp(CardState.CARDSTATE_ABSENT, null);
+
+            mCmdIf.unSetOnCatSessionEnd(this);
+            mCmdIf.unSetOnCatProactiveCmd(this);
+            mCmdIf.unSetOnCatEvent(this);
+            mCmdIf.unSetOnCatCallSetUp(this);
+            mCmdIf.unSetOnCatCcAlphaNotify(this);
+
+            mCmdIf.unregisterForIccRefresh(this);
+            if (mUiccController != null) {
+                mUiccController.unregisterForIccChanged(this);
+                mUiccController = null;
+            }
+            mMsgDecoder.dispose();
+            mMsgDecoder = null;
+            mHandlerThread.quit();
+            mHandlerThread = null;
+            removeCallbacksAndMessages(null);
+            if (sInstance != null) {
+                if (SubscriptionManager.isValidSlotIndex(mSlotId)) {
+                    sInstance[mSlotId] = null;
+                } else {
+                    CatLog.d(this, "error: invaild slot id: " + mSlotId);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void finalize() {
+        CatLog.d(this, "Service finalized");
+    }
+
+    private void handleRilMsg(RilMessage rilMsg) {
+        if (rilMsg == null) {
+            return;
+        }
+
+        // dispatch messages
+        CommandParams cmdParams = null;
+        switch (rilMsg.mId) {
+        case MSG_ID_EVENT_NOTIFY:
+            if (rilMsg.mResCode == ResultCode.OK) {
+                cmdParams = (CommandParams) rilMsg.mData;
+                if (cmdParams != null) {
+                    handleCommand(cmdParams, false);
+                }
+            }
+            break;
+        case MSG_ID_PROACTIVE_COMMAND:
+            try {
+                cmdParams = (CommandParams) rilMsg.mData;
+            } catch (ClassCastException e) {
+                // for error handling : cast exception
+                CatLog.d(this, "Fail to parse proactive command");
+                // Don't send Terminal Resp if command detail is not available
+                if (mCurrntCmd != null) {
+                    sendTerminalResponse(mCurrntCmd.mCmdDet, ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                                     false, 0x00, null);
+                }
+                break;
+            }
+            if (cmdParams != null) {
+                if (rilMsg.mResCode == ResultCode.OK) {
+                    handleCommand(cmdParams, true);
+                } else {
+                    // for proactive commands that couldn't be decoded
+                    // successfully respond with the code generated by the
+                    // message decoder.
+                    sendTerminalResponse(cmdParams.mCmdDet, rilMsg.mResCode,
+                            false, 0, null);
+                }
+            }
+            break;
+        case MSG_ID_REFRESH:
+            cmdParams = (CommandParams) rilMsg.mData;
+            if (cmdParams != null) {
+                handleCommand(cmdParams, false);
+            }
+            break;
+        case MSG_ID_SESSION_END:
+            handleSessionEnd();
+            break;
+        case MSG_ID_CALL_SETUP:
+            // prior event notify command supplied all the information
+            // needed for set up call processing.
+            break;
+        }
+    }
+
+    /**
+     * This function validates the events in SETUP_EVENT_LIST which are currently
+     * supported by the Android framework. In case of SETUP_EVENT_LIST has NULL events
+     * or no events, all the events need to be reset.
+     */
+    private boolean isSupportedSetupEventCommand(CatCmdMessage cmdMsg) {
+        boolean flag = true;
+
+        for (int eventVal: cmdMsg.getSetEventList().eventList) {
+            CatLog.d(this,"Event: " + eventVal);
+            switch (eventVal) {
+                /* Currently android is supporting only the below events in SetupEventList
+                 * Language Selection.  */
+                case IDLE_SCREEN_AVAILABLE_EVENT:
+                case LANGUAGE_SELECTION_EVENT:
+                    break;
+                default:
+                    flag = false;
+            }
+        }
+        return flag;
+    }
+
+    /**
+     * Handles RIL_UNSOL_STK_EVENT_NOTIFY or RIL_UNSOL_STK_PROACTIVE_COMMAND command
+     * from RIL.
+     * Sends valid proactive command data to the application using intents.
+     * RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE will be send back if the command is
+     * from RIL_UNSOL_STK_PROACTIVE_COMMAND.
+     */
+    private void handleCommand(CommandParams cmdParams, boolean isProactiveCmd) {
+        CatLog.d(this, cmdParams.getCommandType().name());
+
+        // Log all proactive commands.
+        if (isProactiveCmd) {
+            if (mUiccController != null) {
+                mUiccController.addCardLog("ProactiveCommand mSlotId=" + mSlotId +
+                        " cmdParams=" + cmdParams);
+            }
+        }
+
+        CharSequence message;
+        ResultCode resultCode;
+        CatCmdMessage cmdMsg = new CatCmdMessage(cmdParams);
+        switch (cmdParams.getCommandType()) {
+            case SET_UP_MENU:
+                if (removeMenu(cmdMsg.getMenu())) {
+                    mMenuCmd = null;
+                } else {
+                    mMenuCmd = cmdMsg;
+                }
+                resultCode = cmdParams.mLoadIconFailed ? ResultCode.PRFRMD_ICON_NOT_DISPLAYED
+                                                                            : ResultCode.OK;
+                sendTerminalResponse(cmdParams.mCmdDet, resultCode, false, 0, null);
+                break;
+            case DISPLAY_TEXT:
+                break;
+            case REFRESH:
+                // ME side only handles refresh commands which meant to remove IDLE
+                // MODE TEXT.
+                cmdParams.mCmdDet.typeOfCommand = CommandType.SET_UP_IDLE_MODE_TEXT.value();
+                break;
+            case SET_UP_IDLE_MODE_TEXT:
+                resultCode = cmdParams.mLoadIconFailed ? ResultCode.PRFRMD_ICON_NOT_DISPLAYED
+                                                                            : ResultCode.OK;
+                sendTerminalResponse(cmdParams.mCmdDet,resultCode, false, 0, null);
+                break;
+            case SET_UP_EVENT_LIST:
+                if (isSupportedSetupEventCommand(cmdMsg)) {
+                    sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
+                } else {
+                    sendTerminalResponse(cmdParams.mCmdDet, ResultCode.BEYOND_TERMINAL_CAPABILITY,
+                            false, 0, null);
+                }
+                break;
+            case PROVIDE_LOCAL_INFORMATION:
+                ResponseData resp;
+                switch (cmdParams.mCmdDet.commandQualifier) {
+                    case CommandParamsFactory.DTTZ_SETTING:
+                        resp = new DTTZResponseData(null);
+                        sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, resp);
+                        break;
+                    case CommandParamsFactory.LANGUAGE_SETTING:
+                        resp = new LanguageResponseData(Locale.getDefault().getLanguage());
+                        sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, resp);
+                        break;
+                    default:
+                        sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
+                }
+                // No need to start STK app here.
+                return;
+            case LAUNCH_BROWSER:
+                if ((((LaunchBrowserParams) cmdParams).mConfirmMsg.text != null)
+                        && (((LaunchBrowserParams) cmdParams).mConfirmMsg.text.equals(STK_DEFAULT))) {
+                    message = mContext.getText(com.android.internal.R.string.launchBrowserDefault);
+                    ((LaunchBrowserParams) cmdParams).mConfirmMsg.text = message.toString();
+                }
+                break;
+            case SELECT_ITEM:
+            case GET_INPUT:
+            case GET_INKEY:
+                break;
+            case SEND_DTMF:
+            case SEND_SMS:
+            case SEND_SS:
+            case SEND_USSD:
+                if ((((DisplayTextParams)cmdParams).mTextMsg.text != null)
+                        && (((DisplayTextParams)cmdParams).mTextMsg.text.equals(STK_DEFAULT))) {
+                    message = mContext.getText(com.android.internal.R.string.sending);
+                    ((DisplayTextParams)cmdParams).mTextMsg.text = message.toString();
+                }
+                break;
+            case PLAY_TONE:
+                break;
+            case SET_UP_CALL:
+                if ((((CallSetupParams) cmdParams).mConfirmMsg.text != null)
+                        && (((CallSetupParams) cmdParams).mConfirmMsg.text.equals(STK_DEFAULT))) {
+                    message = mContext.getText(com.android.internal.R.string.SetupCallDefault);
+                    ((CallSetupParams) cmdParams).mConfirmMsg.text = message.toString();
+                }
+                break;
+            case OPEN_CHANNEL:
+            case CLOSE_CHANNEL:
+            case RECEIVE_DATA:
+            case SEND_DATA:
+                BIPClientParams cmd = (BIPClientParams) cmdParams;
+                /* Per 3GPP specification 102.223,
+                 * if the alpha identifier is not provided by the UICC,
+                 * the terminal MAY give information to the user
+                 * noAlphaUsrCnf defines if you need to show user confirmation or not
+                 */
+                boolean noAlphaUsrCnf = false;
+                try {
+                    noAlphaUsrCnf = mContext.getResources().getBoolean(
+                            com.android.internal.R.bool.config_stkNoAlphaUsrCnf);
+                } catch (NotFoundException e) {
+                    noAlphaUsrCnf = false;
+                }
+                if ((cmd.mTextMsg.text == null) && (cmd.mHasAlphaId || noAlphaUsrCnf)) {
+                    CatLog.d(this, "cmd " + cmdParams.getCommandType() + " with null alpha id");
+                    // If alpha length is zero, we just respond with OK.
+                    if (isProactiveCmd) {
+                        sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
+                    } else if (cmdParams.getCommandType() == CommandType.OPEN_CHANNEL) {
+                        mCmdIf.handleCallSetupRequestFromSim(true, null);
+                    }
+                    return;
+                }
+                // Respond with permanent failure to avoid retry if STK app is not present.
+                if (!mStkAppInstalled) {
+                    CatLog.d(this, "No STK application found.");
+                    if (isProactiveCmd) {
+                        sendTerminalResponse(cmdParams.mCmdDet,
+                                             ResultCode.BEYOND_TERMINAL_CAPABILITY,
+                                             false, 0, null);
+                        return;
+                    }
+                }
+                /*
+                 * CLOSE_CHANNEL, RECEIVE_DATA and SEND_DATA can be delivered by
+                 * either PROACTIVE_COMMAND or EVENT_NOTIFY.
+                 * If PROACTIVE_COMMAND is used for those commands, send terminal
+                 * response here.
+                 */
+                if (isProactiveCmd &&
+                    ((cmdParams.getCommandType() == CommandType.CLOSE_CHANNEL) ||
+                     (cmdParams.getCommandType() == CommandType.RECEIVE_DATA) ||
+                     (cmdParams.getCommandType() == CommandType.SEND_DATA))) {
+                    sendTerminalResponse(cmdParams.mCmdDet, ResultCode.OK, false, 0, null);
+                }
+                break;
+            default:
+                CatLog.d(this, "Unsupported command");
+                return;
+        }
+        mCurrntCmd = cmdMsg;
+        broadcastCatCmdIntent(cmdMsg);
+    }
+
+
+    private void broadcastCatCmdIntent(CatCmdMessage cmdMsg) {
+        Intent intent = new Intent(AppInterface.CAT_CMD_ACTION);
+        intent.putExtra("STK CMD", cmdMsg);
+        intent.putExtra("SLOT_ID", mSlotId);
+        intent.setComponent(AppInterface.getDefaultSTKApplication());
+        CatLog.d(this, "Sending CmdMsg: " + cmdMsg+ " on slotid:" + mSlotId);
+        mContext.sendBroadcast(intent, AppInterface.STK_PERMISSION);
+    }
+
+    /**
+     * Handles RIL_UNSOL_STK_SESSION_END unsolicited command from RIL.
+     *
+     */
+    private void handleSessionEnd() {
+        CatLog.d(this, "SESSION END on "+ mSlotId);
+
+        mCurrntCmd = mMenuCmd;
+        Intent intent = new Intent(AppInterface.CAT_SESSION_END_ACTION);
+        intent.putExtra("SLOT_ID", mSlotId);
+        intent.setComponent(AppInterface.getDefaultSTKApplication());
+        mContext.sendBroadcast(intent, AppInterface.STK_PERMISSION);
+    }
+
+
+    private void sendTerminalResponse(CommandDetails cmdDet,
+            ResultCode resultCode, boolean includeAdditionalInfo,
+            int additionalInfo, ResponseData resp) {
+
+        if (cmdDet == null) {
+            return;
+        }
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+        Input cmdInput = null;
+        if (mCurrntCmd != null) {
+            cmdInput = mCurrntCmd.geInput();
+        }
+
+        // command details
+        int tag = ComprehensionTlvTag.COMMAND_DETAILS.value();
+        if (cmdDet.compRequired) {
+            tag |= 0x80;
+        }
+        buf.write(tag);
+        buf.write(0x03); // length
+        buf.write(cmdDet.commandNumber);
+        buf.write(cmdDet.typeOfCommand);
+        buf.write(cmdDet.commandQualifier);
+
+        // device identities
+        // According to TS102.223/TS31.111 section 6.8 Structure of
+        // TERMINAL RESPONSE, "For all SIMPLE-TLV objects with Min=N,
+        // the ME should set the CR(comprehension required) flag to
+        // comprehension not required.(CR=0)"
+        // Since DEVICE_IDENTITIES and DURATION TLVs have Min=N,
+        // the CR flag is not set.
+        tag = ComprehensionTlvTag.DEVICE_IDENTITIES.value();
+        buf.write(tag);
+        buf.write(0x02); // length
+        buf.write(DEV_ID_TERMINAL); // source device id
+        buf.write(DEV_ID_UICC); // destination device id
+
+        // result
+        tag = ComprehensionTlvTag.RESULT.value();
+        if (cmdDet.compRequired) {
+            tag |= 0x80;
+        }
+        buf.write(tag);
+        int length = includeAdditionalInfo ? 2 : 1;
+        buf.write(length);
+        buf.write(resultCode.value());
+
+        // additional info
+        if (includeAdditionalInfo) {
+            buf.write(additionalInfo);
+        }
+
+        // Fill optional data for each corresponding command
+        if (resp != null) {
+            resp.format(buf);
+        } else {
+            encodeOptionalTags(cmdDet, resultCode, cmdInput, buf);
+        }
+
+        byte[] rawData = buf.toByteArray();
+        String hexString = IccUtils.bytesToHexString(rawData);
+        if (DBG) {
+            CatLog.d(this, "TERMINAL RESPONSE: " + hexString);
+        }
+
+        mCmdIf.sendTerminalResponse(hexString, null);
+    }
+
+    private void encodeOptionalTags(CommandDetails cmdDet,
+            ResultCode resultCode, Input cmdInput, ByteArrayOutputStream buf) {
+        CommandType cmdType = AppInterface.CommandType.fromInt(cmdDet.typeOfCommand);
+        if (cmdType != null) {
+            switch (cmdType) {
+                case GET_INKEY:
+                    // ETSI TS 102 384,27.22.4.2.8.4.2.
+                    // If it is a response for GET_INKEY command and the response timeout
+                    // occured, then add DURATION TLV for variable timeout case.
+                    if ((resultCode.value() == ResultCode.NO_RESPONSE_FROM_USER.value()) &&
+                        (cmdInput != null) && (cmdInput.duration != null)) {
+                        getInKeyResponse(buf, cmdInput);
+                    }
+                    break;
+                case PROVIDE_LOCAL_INFORMATION:
+                    if ((cmdDet.commandQualifier == CommandParamsFactory.LANGUAGE_SETTING) &&
+                        (resultCode.value() == ResultCode.OK.value())) {
+                        getPliResponse(buf);
+                    }
+                    break;
+                default:
+                    CatLog.d(this, "encodeOptionalTags() Unsupported Cmd details=" + cmdDet);
+                    break;
+            }
+        } else {
+            CatLog.d(this, "encodeOptionalTags() bad Cmd details=" + cmdDet);
+        }
+    }
+
+    private void getInKeyResponse(ByteArrayOutputStream buf, Input cmdInput) {
+        int tag = ComprehensionTlvTag.DURATION.value();
+
+        buf.write(tag);
+        buf.write(0x02); // length
+        buf.write(cmdInput.duration.timeUnit.SECOND.value()); // Time (Unit,Seconds)
+        buf.write(cmdInput.duration.timeInterval); // Time Duration
+    }
+
+    private void getPliResponse(ByteArrayOutputStream buf) {
+        // Locale Language Setting
+        final String lang = Locale.getDefault().getLanguage();
+
+        if (lang != null) {
+            // tag
+            int tag = ComprehensionTlvTag.LANGUAGE.value();
+            buf.write(tag);
+            ResponseData.writeLength(buf, lang.length());
+            buf.write(lang.getBytes(), 0, lang.length());
+        }
+    }
+
+    private void sendMenuSelection(int menuId, boolean helpRequired) {
+
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+        // tag
+        int tag = BerTlv.BER_MENU_SELECTION_TAG;
+        buf.write(tag);
+
+        // length
+        buf.write(0x00); // place holder
+
+        // device identities
+        tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value();
+        buf.write(tag);
+        buf.write(0x02); // length
+        buf.write(DEV_ID_KEYPAD); // source device id
+        buf.write(DEV_ID_UICC); // destination device id
+
+        // item identifier
+        tag = 0x80 | ComprehensionTlvTag.ITEM_ID.value();
+        buf.write(tag);
+        buf.write(0x01); // length
+        buf.write(menuId); // menu identifier chosen
+
+        // help request
+        if (helpRequired) {
+            tag = ComprehensionTlvTag.HELP_REQUEST.value();
+            buf.write(tag);
+            buf.write(0x00); // length
+        }
+
+        byte[] rawData = buf.toByteArray();
+
+        // write real length
+        int len = rawData.length - 2; // minus (tag + length)
+        rawData[1] = (byte) len;
+
+        String hexString = IccUtils.bytesToHexString(rawData);
+
+        mCmdIf.sendEnvelope(hexString, null);
+    }
+
+    private void eventDownload(int event, int sourceId, int destinationId,
+            byte[] additionalInfo, boolean oneShot) {
+
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+        // tag
+        int tag = BerTlv.BER_EVENT_DOWNLOAD_TAG;
+        buf.write(tag);
+
+        // length
+        buf.write(0x00); // place holder, assume length < 128.
+
+        // event list
+        tag = 0x80 | ComprehensionTlvTag.EVENT_LIST.value();
+        buf.write(tag);
+        buf.write(0x01); // length
+        buf.write(event); // event value
+
+        // device identities
+        tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value();
+        buf.write(tag);
+        buf.write(0x02); // length
+        buf.write(sourceId); // source device id
+        buf.write(destinationId); // destination device id
+
+        /*
+         * Check for type of event download to be sent to UICC - Browser
+         * termination,Idle screen available, User activity, Language selection
+         * etc as mentioned under ETSI TS 102 223 section 7.5
+         */
+
+        /*
+         * Currently the below events are supported:
+         * Language Selection Event.
+         * Other event download commands should be encoded similar way
+         */
+        /* TODO: eventDownload should be extended for other Envelope Commands */
+        switch (event) {
+            case IDLE_SCREEN_AVAILABLE_EVENT:
+                CatLog.d(sInstance, " Sending Idle Screen Available event download to ICC");
+                break;
+            case LANGUAGE_SELECTION_EVENT:
+                CatLog.d(sInstance, " Sending Language Selection event download to ICC");
+                tag = 0x80 | ComprehensionTlvTag.LANGUAGE.value();
+                buf.write(tag);
+                // Language length should be 2 byte
+                buf.write(0x02);
+                break;
+            default:
+                break;
+        }
+
+        // additional information
+        if (additionalInfo != null) {
+            for (byte b : additionalInfo) {
+                buf.write(b);
+            }
+        }
+
+        byte[] rawData = buf.toByteArray();
+
+        // write real length
+        int len = rawData.length - 2; // minus (tag + length)
+        rawData[1] = (byte) len;
+
+        String hexString = IccUtils.bytesToHexString(rawData);
+
+        CatLog.d(this, "ENVELOPE COMMAND: " + hexString);
+
+        mCmdIf.sendEnvelope(hexString, null);
+    }
+
+    /**
+     * Used by application to get an AppInterface object.
+     *
+     * @return The only Service object in the system
+     */
+    //TODO Need to take care for MSIM
+    public static AppInterface getInstance() {
+        int slotId = PhoneConstants.DEFAULT_CARD_INDEX;
+        SubscriptionController sControl = SubscriptionController.getInstance();
+        if (sControl != null) {
+            slotId = sControl.getSlotIndex(sControl.getDefaultSubId());
+        }
+        return getInstance(null, null, null, slotId);
+    }
+
+    /**
+     * Used by application to get an AppInterface object.
+     *
+     * @return The only Service object in the system
+     */
+    public static AppInterface getInstance(int slotId) {
+        return getInstance(null, null, null, slotId);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        CatLog.d(this, "handleMessage[" + msg.what + "]");
+
+        switch (msg.what) {
+        case MSG_ID_SESSION_END:
+        case MSG_ID_PROACTIVE_COMMAND:
+        case MSG_ID_EVENT_NOTIFY:
+        case MSG_ID_REFRESH:
+            CatLog.d(this, "ril message arrived,slotid:" + mSlotId);
+            String data = null;
+            if (msg.obj != null) {
+                AsyncResult ar = (AsyncResult) msg.obj;
+                if (ar != null && ar.result != null) {
+                    try {
+                        data = (String) ar.result;
+                    } catch (ClassCastException e) {
+                        break;
+                    }
+                }
+            }
+            mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data));
+            break;
+        case MSG_ID_CALL_SETUP:
+            mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, null));
+            break;
+        case MSG_ID_ICC_RECORDS_LOADED:
+            break;
+        case MSG_ID_RIL_MSG_DECODED:
+            handleRilMsg((RilMessage) msg.obj);
+            break;
+        case MSG_ID_RESPONSE:
+            handleCmdResponse((CatResponseMessage) msg.obj);
+            break;
+        case MSG_ID_ICC_CHANGED:
+            CatLog.d(this, "MSG_ID_ICC_CHANGED");
+            updateIccAvailability();
+            break;
+        case MSG_ID_ICC_REFRESH:
+            if (msg.obj != null) {
+                AsyncResult ar = (AsyncResult) msg.obj;
+                if (ar != null && ar.result != null) {
+                    broadcastCardStateAndIccRefreshResp(CardState.CARDSTATE_PRESENT,
+                                  (IccRefreshResponse) ar.result);
+                } else {
+                    CatLog.d(this,"Icc REFRESH with exception: " + ar.exception);
+                }
+            } else {
+                CatLog.d(this, "IccRefresh Message is null");
+            }
+            break;
+        case MSG_ID_ALPHA_NOTIFY:
+            CatLog.d(this, "Received CAT CC Alpha message from card");
+            if (msg.obj != null) {
+                AsyncResult ar = (AsyncResult) msg.obj;
+                if (ar != null && ar.result != null) {
+                    broadcastAlphaMessage((String)ar.result);
+                } else {
+                    CatLog.d(this, "CAT Alpha message: ar.result is null");
+                }
+            } else {
+                CatLog.d(this, "CAT Alpha message: msg.obj is null");
+            }
+            break;
+        default:
+            throw new AssertionError("Unrecognized CAT command: " + msg.what);
+        }
+    }
+
+    /**
+     ** This function sends a CARD status (ABSENT, PRESENT, REFRESH) to STK_APP.
+     ** This is triggered during ICC_REFRESH or CARD STATE changes. In case
+     ** REFRESH, additional information is sent in 'refresh_result'
+     **
+     **/
+    private void  broadcastCardStateAndIccRefreshResp(CardState cardState,
+            IccRefreshResponse iccRefreshState) {
+        Intent intent = new Intent(AppInterface.CAT_ICC_STATUS_CHANGE);
+        boolean cardPresent = (cardState == CardState.CARDSTATE_PRESENT);
+
+        if (iccRefreshState != null) {
+            //This case is when MSG_ID_ICC_REFRESH is received.
+            intent.putExtra(AppInterface.REFRESH_RESULT, iccRefreshState.refreshResult);
+            CatLog.d(this, "Sending IccResult with Result: "
+                    + iccRefreshState.refreshResult);
+        }
+
+        // This sends an intent with CARD_ABSENT (0 - false) /CARD_PRESENT (1 - true).
+        intent.putExtra(AppInterface.CARD_STATUS, cardPresent);
+        intent.setComponent(AppInterface.getDefaultSTKApplication());
+        CatLog.d(this, "Sending Card Status: "
+                + cardState + " " + "cardPresent: " + cardPresent);
+        mContext.sendBroadcast(intent, AppInterface.STK_PERMISSION);
+    }
+
+    private void broadcastAlphaMessage(String alphaString) {
+        CatLog.d(this, "Broadcasting CAT Alpha message from card: " + alphaString);
+        Intent intent = new Intent(AppInterface.CAT_ALPHA_NOTIFY_ACTION);
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        intent.putExtra(AppInterface.ALPHA_STRING, alphaString);
+        intent.putExtra("SLOT_ID", mSlotId);
+        intent.setComponent(AppInterface.getDefaultSTKApplication());
+        mContext.sendBroadcast(intent, AppInterface.STK_PERMISSION);
+    }
+
+    @Override
+    public synchronized void onCmdResponse(CatResponseMessage resMsg) {
+        if (resMsg == null) {
+            return;
+        }
+        // queue a response message.
+        Message msg = obtainMessage(MSG_ID_RESPONSE, resMsg);
+        msg.sendToTarget();
+    }
+
+    private boolean validateResponse(CatResponseMessage resMsg) {
+        boolean validResponse = false;
+        if ((resMsg.mCmdDet.typeOfCommand == CommandType.SET_UP_EVENT_LIST.value())
+                || (resMsg.mCmdDet.typeOfCommand == CommandType.SET_UP_MENU.value())) {
+            CatLog.d(this, "CmdType: " + resMsg.mCmdDet.typeOfCommand);
+            validResponse = true;
+        } else if (mCurrntCmd != null) {
+            validResponse = resMsg.mCmdDet.compareTo(mCurrntCmd.mCmdDet);
+            CatLog.d(this, "isResponse for last valid cmd: " + validResponse);
+        }
+        return validResponse;
+    }
+
+    private boolean removeMenu(Menu menu) {
+        try {
+            if (menu.items.size() == 1 && menu.items.get(0) == null) {
+                return true;
+            }
+        } catch (NullPointerException e) {
+            CatLog.d(this, "Unable to get Menu's items size");
+            return true;
+        }
+        return false;
+    }
+
+    private void handleCmdResponse(CatResponseMessage resMsg) {
+        // Make sure the response details match the last valid command. An invalid
+        // response is a one that doesn't have a corresponding proactive command
+        // and sending it can "confuse" the baseband/ril.
+        // One reason for out of order responses can be UI glitches. For example,
+        // if the application launch an activity, and that activity is stored
+        // by the framework inside the history stack. That activity will be
+        // available for relaunch using the latest application dialog
+        // (long press on the home button). Relaunching that activity can send
+        // the same command's result again to the CatService and can cause it to
+        // get out of sync with the SIM. This can happen in case of
+        // non-interactive type Setup Event List and SETUP_MENU proactive commands.
+        // Stk framework would have already sent Terminal Response to Setup Event
+        // List and SETUP_MENU proactive commands. After sometime Stk app will send
+        // Envelope Command/Event Download. In which case, the response details doesn't
+        // match with last valid command (which are not related).
+        // However, we should allow Stk framework to send the message to ICC.
+        if (!validateResponse(resMsg)) {
+            return;
+        }
+        ResponseData resp = null;
+        boolean helpRequired = false;
+        CommandDetails cmdDet = resMsg.getCmdDetails();
+        AppInterface.CommandType type = AppInterface.CommandType.fromInt(cmdDet.typeOfCommand);
+
+        switch (resMsg.mResCode) {
+        case HELP_INFO_REQUIRED:
+            helpRequired = true;
+            // fall through
+        case OK:
+        case PRFRMD_WITH_PARTIAL_COMPREHENSION:
+        case PRFRMD_WITH_MISSING_INFO:
+        case PRFRMD_WITH_ADDITIONAL_EFS_READ:
+        case PRFRMD_ICON_NOT_DISPLAYED:
+        case PRFRMD_MODIFIED_BY_NAA:
+        case PRFRMD_LIMITED_SERVICE:
+        case PRFRMD_WITH_MODIFICATION:
+        case PRFRMD_NAA_NOT_ACTIVE:
+        case PRFRMD_TONE_NOT_PLAYED:
+        case LAUNCH_BROWSER_ERROR:
+        case TERMINAL_CRNTLY_UNABLE_TO_PROCESS:
+            switch (type) {
+            case SET_UP_MENU:
+                helpRequired = resMsg.mResCode == ResultCode.HELP_INFO_REQUIRED;
+                sendMenuSelection(resMsg.mUsersMenuSelection, helpRequired);
+                return;
+            case SELECT_ITEM:
+                resp = new SelectItemResponseData(resMsg.mUsersMenuSelection);
+                break;
+            case GET_INPUT:
+            case GET_INKEY:
+                Input input = mCurrntCmd.geInput();
+                if (!input.yesNo) {
+                    // when help is requested there is no need to send the text
+                    // string object.
+                    if (!helpRequired) {
+                        resp = new GetInkeyInputResponseData(resMsg.mUsersInput,
+                                input.ucs2, input.packed);
+                    }
+                } else {
+                    resp = new GetInkeyInputResponseData(
+                            resMsg.mUsersYesNoSelection);
+                }
+                break;
+            case DISPLAY_TEXT:
+                if (resMsg.mResCode == ResultCode.TERMINAL_CRNTLY_UNABLE_TO_PROCESS) {
+                    // For screenbusy case there will be addtional information in the terminal
+                    // response. And the value of the additional information byte is 0x01.
+                    resMsg.setAdditionalInfo(0x01);
+                } else {
+                    resMsg.mIncludeAdditionalInfo = false;
+                    resMsg.mAdditionalInfo = 0;
+                }
+                break;
+            case LAUNCH_BROWSER:
+                break;
+            // 3GPP TS.102.223: Open Channel alpha confirmation should not send TR
+            case OPEN_CHANNEL:
+            case SET_UP_CALL:
+                mCmdIf.handleCallSetupRequestFromSim(resMsg.mUsersConfirm, null);
+                // No need to send terminal response for SET UP CALL. The user's
+                // confirmation result is send back using a dedicated ril message
+                // invoked by the CommandInterface call above.
+                mCurrntCmd = null;
+                return;
+            case SET_UP_EVENT_LIST:
+                if (IDLE_SCREEN_AVAILABLE_EVENT == resMsg.mEventValue) {
+                    eventDownload(resMsg.mEventValue, DEV_ID_DISPLAY, DEV_ID_UICC,
+                            resMsg.mAddedInfo, false);
+                 } else {
+                     eventDownload(resMsg.mEventValue, DEV_ID_TERMINAL, DEV_ID_UICC,
+                            resMsg.mAddedInfo, false);
+                 }
+                // No need to send the terminal response after event download.
+                return;
+            default:
+                break;
+            }
+            break;
+        case BACKWARD_MOVE_BY_USER:
+        case USER_NOT_ACCEPT:
+            // if the user dismissed the alert dialog for a
+            // setup call/open channel, consider that as the user
+            // rejecting the call. Use dedicated API for this, rather than
+            // sending a terminal response.
+            if (type == CommandType.SET_UP_CALL || type == CommandType.OPEN_CHANNEL) {
+                mCmdIf.handleCallSetupRequestFromSim(false, null);
+                mCurrntCmd = null;
+                return;
+            } else {
+                resp = null;
+            }
+            break;
+        case NO_RESPONSE_FROM_USER:
+        case UICC_SESSION_TERM_BY_USER:
+            resp = null;
+            break;
+        default:
+            return;
+        }
+        sendTerminalResponse(cmdDet, resMsg.mResCode, resMsg.mIncludeAdditionalInfo,
+                resMsg.mAdditionalInfo, resp);
+        mCurrntCmd = null;
+    }
+
+    private boolean isStkAppInstalled() {
+        Intent intent = new Intent(AppInterface.CAT_CMD_ACTION);
+        PackageManager pm = mContext.getPackageManager();
+        List<ResolveInfo> broadcastReceivers =
+                            pm.queryBroadcastReceivers(intent, PackageManager.GET_META_DATA);
+        int numReceiver = broadcastReceivers == null ? 0 : broadcastReceivers.size();
+
+        return (numReceiver > 0);
+    }
+
+    public void update(CommandsInterface ci,
+            Context context, UiccCard ic) {
+        UiccCardApplication ca = null;
+        IccRecords ir = null;
+
+        if (ic != null) {
+            /* Since Cat is not tied to any application, but rather is Uicc application
+             * in itself - just get first FileHandler and IccRecords object
+             */
+            ca = ic.getApplicationIndex(0);
+            if (ca != null) {
+                ir = ca.getIccRecords();
+            }
+        }
+
+        synchronized (sInstanceLock) {
+            if ((ir != null) && (mIccRecords != ir)) {
+                if (mIccRecords != null) {
+                    mIccRecords.unregisterForRecordsLoaded(this);
+                }
+
+                CatLog.d(this,
+                        "Reinitialize the Service with SIMRecords and UiccCardApplication");
+                mIccRecords = ir;
+                mUiccApplication = ca;
+
+                // re-Register for SIM ready event.
+                mIccRecords.registerForRecordsLoaded(this, MSG_ID_ICC_RECORDS_LOADED, null);
+                CatLog.d(this, "registerForRecordsLoaded slotid=" + mSlotId + " instance:" + this);
+            }
+        }
+    }
+
+    void updateIccAvailability() {
+        if (null == mUiccController) {
+            return;
+        }
+
+        CardState newState = CardState.CARDSTATE_ABSENT;
+        UiccCard newCard = mUiccController.getUiccCard(mSlotId);
+        if (newCard != null) {
+            newState = newCard.getCardState();
+        }
+        CardState oldState = mCardState;
+        mCardState = newState;
+        CatLog.d(this,"New Card State = " + newState + " " + "Old Card State = " + oldState);
+        if (oldState == CardState.CARDSTATE_PRESENT &&
+                newState != CardState.CARDSTATE_PRESENT) {
+            broadcastCardStateAndIccRefreshResp(newState, null);
+        } else if (oldState != CardState.CARDSTATE_PRESENT &&
+                newState == CardState.CARDSTATE_PRESENT) {
+            // Card moved to PRESENT STATE.
+            mCmdIf.reportStkServiceIsRunning(null);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cat/CommandDetails.java b/com/android/internal/telephony/cat/CommandDetails.java
new file mode 100644
index 0000000..aaa9d68
--- /dev/null
+++ b/com/android/internal/telephony/cat/CommandDetails.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+abstract class ValueObject {
+    abstract ComprehensionTlvTag getTag();
+}
+
+/**
+ * Class for Command Details object of proactive commands from SIM.
+ * {@hide}
+ */
+class CommandDetails extends ValueObject implements Parcelable {
+    public boolean compRequired;
+    public int commandNumber;
+    public int typeOfCommand;
+    public int commandQualifier;
+
+    @Override
+    public ComprehensionTlvTag getTag() {
+        return ComprehensionTlvTag.COMMAND_DETAILS;
+    }
+
+    CommandDetails() {
+    }
+
+    public boolean compareTo(CommandDetails other) {
+        return (this.compRequired == other.compRequired &&
+                this.commandNumber == other.commandNumber &&
+                this.commandQualifier == other.commandQualifier &&
+                this.typeOfCommand == other.typeOfCommand);
+    }
+
+    public CommandDetails(Parcel in) {
+        compRequired = in.readInt() != 0;
+        commandNumber = in.readInt();
+        typeOfCommand = in.readInt();
+        commandQualifier = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(compRequired ? 1 : 0);
+        dest.writeInt(commandNumber);
+        dest.writeInt(typeOfCommand);
+        dest.writeInt(commandQualifier);
+    }
+
+    public static final Parcelable.Creator<CommandDetails> CREATOR =
+                                new Parcelable.Creator<CommandDetails>() {
+        @Override
+        public CommandDetails createFromParcel(Parcel in) {
+            return new CommandDetails(in);
+        }
+
+        @Override
+        public CommandDetails[] newArray(int size) {
+            return new CommandDetails[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return "CmdDetails: compRequired=" + compRequired +
+                " commandNumber=" + commandNumber +
+                " typeOfCommand=" + typeOfCommand +
+                " commandQualifier=" + commandQualifier;
+    }
+}
+
+class DeviceIdentities extends ValueObject {
+    public int sourceId;
+    public int destinationId;
+
+    @Override
+    ComprehensionTlvTag getTag() {
+        return ComprehensionTlvTag.DEVICE_IDENTITIES;
+    }
+}
+
+// Container class to hold icon identifier value.
+class IconId extends ValueObject {
+    int recordNumber;
+    boolean selfExplanatory;
+
+    @Override
+    ComprehensionTlvTag getTag() {
+        return ComprehensionTlvTag.ICON_ID;
+    }
+}
+
+// Container class to hold item icon identifier list value.
+class ItemsIconId extends ValueObject {
+    int [] recordNumbers;
+    boolean selfExplanatory;
+
+    @Override
+    ComprehensionTlvTag getTag() {
+        return ComprehensionTlvTag.ITEM_ICON_ID_LIST;
+    }
+}
diff --git a/com/android/internal/telephony/cat/CommandParams.java b/com/android/internal/telephony/cat/CommandParams.java
new file mode 100644
index 0000000..7dfedab
--- /dev/null
+++ b/com/android/internal/telephony/cat/CommandParams.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.graphics.Bitmap;
+
+/**
+ * Container class for proactive command parameters.
+ *
+ */
+class CommandParams {
+    CommandDetails mCmdDet;
+    // Variable to track if an optional icon load has failed.
+    boolean mLoadIconFailed = false;
+
+    CommandParams(CommandDetails cmdDet) {
+        mCmdDet = cmdDet;
+    }
+
+    AppInterface.CommandType getCommandType() {
+        return AppInterface.CommandType.fromInt(mCmdDet.typeOfCommand);
+    }
+
+    boolean setIcon(Bitmap icon) { return true; }
+
+    @Override
+    public String toString() {
+        return mCmdDet.toString();
+    }
+}
+
+class DisplayTextParams extends CommandParams {
+    TextMessage mTextMsg;
+
+    DisplayTextParams(CommandDetails cmdDet, TextMessage textMsg) {
+        super(cmdDet);
+        mTextMsg = textMsg;
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon != null && mTextMsg != null) {
+            mTextMsg.icon = icon;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "TextMessage=" + mTextMsg + " " + super.toString();
+    }
+}
+
+class LaunchBrowserParams extends CommandParams {
+    TextMessage mConfirmMsg;
+    LaunchBrowserMode mMode;
+    String mUrl;
+
+    LaunchBrowserParams(CommandDetails cmdDet, TextMessage confirmMsg,
+            String url, LaunchBrowserMode mode) {
+        super(cmdDet);
+        mConfirmMsg = confirmMsg;
+        mMode = mode;
+        mUrl = url;
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon != null && mConfirmMsg != null) {
+            mConfirmMsg.icon = icon;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "TextMessage=" + mConfirmMsg + " " + super.toString();
+    }
+}
+
+class SetEventListParams extends CommandParams {
+    int[] mEventInfo;
+    SetEventListParams(CommandDetails cmdDet, int[] eventInfo) {
+        super(cmdDet);
+        this.mEventInfo = eventInfo;
+    }
+}
+
+class PlayToneParams extends CommandParams {
+    TextMessage mTextMsg;
+    ToneSettings mSettings;
+
+    PlayToneParams(CommandDetails cmdDet, TextMessage textMsg,
+            Tone tone, Duration duration, boolean vibrate) {
+        super(cmdDet);
+        mTextMsg = textMsg;
+        mSettings = new ToneSettings(duration, tone, vibrate);
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon != null && mTextMsg != null) {
+            mTextMsg.icon = icon;
+            return true;
+        }
+        return false;
+    }
+}
+
+class CallSetupParams extends CommandParams {
+    TextMessage mConfirmMsg;
+    TextMessage mCallMsg;
+
+    CallSetupParams(CommandDetails cmdDet, TextMessage confirmMsg,
+            TextMessage callMsg) {
+        super(cmdDet);
+        mConfirmMsg = confirmMsg;
+        mCallMsg = callMsg;
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon == null) {
+            return false;
+        }
+        if (mConfirmMsg != null && mConfirmMsg.icon == null) {
+            mConfirmMsg.icon = icon;
+            return true;
+        } else if (mCallMsg != null && mCallMsg.icon == null) {
+            mCallMsg.icon = icon;
+            return true;
+        }
+        return false;
+    }
+}
+
+class SelectItemParams extends CommandParams {
+    Menu mMenu = null;
+    boolean mLoadTitleIcon = false;
+
+    SelectItemParams(CommandDetails cmdDet, Menu menu, boolean loadTitleIcon) {
+        super(cmdDet);
+        mMenu = menu;
+        mLoadTitleIcon = loadTitleIcon;
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon != null && mMenu != null) {
+            if (mLoadTitleIcon && mMenu.titleIcon == null) {
+                mMenu.titleIcon = icon;
+            } else {
+                for (Item item : mMenu.items) {
+                    if (item.icon != null) {
+                        continue;
+                    }
+                    item.icon = icon;
+                    break;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+}
+
+class GetInputParams extends CommandParams {
+    Input mInput = null;
+
+    GetInputParams(CommandDetails cmdDet, Input input) {
+        super(cmdDet);
+        mInput = input;
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon != null && mInput != null) {
+            mInput.icon = icon;
+        }
+        return true;
+    }
+}
+
+/*
+ * BIP (Bearer Independent Protocol) is the mechanism for SIM card applications
+ * to access data connection through the mobile device.
+ *
+ * SIM utilizes proactive commands (OPEN CHANNEL, CLOSE CHANNEL, SEND DATA and
+ * RECEIVE DATA to control/read/write data for BIP. Refer to ETSI TS 102 223 for
+ * the details of proactive commands procedures and their structures.
+ */
+class BIPClientParams extends CommandParams {
+    TextMessage mTextMsg;
+    boolean mHasAlphaId;
+
+    BIPClientParams(CommandDetails cmdDet, TextMessage textMsg, boolean has_alpha_id) {
+        super(cmdDet);
+        mTextMsg = textMsg;
+        mHasAlphaId = has_alpha_id;
+    }
+
+    @Override
+    boolean setIcon(Bitmap icon) {
+        if (icon != null && mTextMsg != null) {
+            mTextMsg.icon = icon;
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/com/android/internal/telephony/cat/CommandParamsFactory.java b/com/android/internal/telephony/cat/CommandParamsFactory.java
new file mode 100644
index 0000000..3dd5337
--- /dev/null
+++ b/com/android/internal/telephony/cat/CommandParamsFactory.java
@@ -0,0 +1,1063 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.uicc.IccFileHandler;
+
+import java.util.Iterator;
+import java.util.List;
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.USER_ACTIVITY_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.IDLE_SCREEN_AVAILABLE_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.LANGUAGE_SELECTION_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.BROWSER_TERMINATION_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.
+                   SetupEventListConstants.BROWSING_STATUS_EVENT;
+/**
+ * Factory class, used for decoding raw byte arrays, received from baseband,
+ * into a CommandParams object.
+ *
+ */
+class CommandParamsFactory extends Handler {
+    private static CommandParamsFactory sInstance = null;
+    private IconLoader mIconLoader;
+    private CommandParams mCmdParams = null;
+    private int mIconLoadState = LOAD_NO_ICON;
+    private RilMessageDecoder mCaller = null;
+    private boolean mloadIcon = false;
+
+    // constants
+    static final int MSG_ID_LOAD_ICON_DONE = 1;
+
+    // loading icons state parameters.
+    static final int LOAD_NO_ICON           = 0;
+    static final int LOAD_SINGLE_ICON       = 1;
+    static final int LOAD_MULTI_ICONS       = 2;
+
+    // Command Qualifier values for refresh command
+    static final int REFRESH_NAA_INIT_AND_FULL_FILE_CHANGE  = 0x00;
+    static final int REFRESH_NAA_INIT_AND_FILE_CHANGE       = 0x02;
+    static final int REFRESH_NAA_INIT                       = 0x03;
+    static final int REFRESH_UICC_RESET                     = 0x04;
+
+    // Command Qualifier values for PLI command
+    static final int DTTZ_SETTING                           = 0x03;
+    static final int LANGUAGE_SETTING                       = 0x04;
+
+    // As per TS 102.223 Annex C, Structure of CAT communications,
+    // the APDU length can be max 255 bytes. This leaves only 239 bytes for user
+    // input string. CMD details TLV + Device IDs TLV + Result TLV + Other
+    // details of TextString TLV not including user input take 16 bytes.
+    //
+    // If UCS2 encoding is used, maximum 118 UCS2 chars can be encoded in 238 bytes.
+    // Each UCS2 char takes 2 bytes. Byte Order Mask(BOM), 0xFEFF takes 2 bytes.
+    //
+    // If GSM 7 bit default(use 8 bits to represent a 7 bit char) format is used,
+    // maximum 239 chars can be encoded in 239 bytes since each char takes 1 byte.
+    //
+    // No issues for GSM 7 bit packed format encoding.
+
+    private static final int MAX_GSM7_DEFAULT_CHARS = 239;
+    private static final int MAX_UCS2_CHARS = 118;
+
+    static synchronized CommandParamsFactory getInstance(RilMessageDecoder caller,
+            IccFileHandler fh) {
+        if (sInstance != null) {
+            return sInstance;
+        }
+        if (fh != null) {
+            return new CommandParamsFactory(caller, fh);
+        }
+        return null;
+    }
+
+    private CommandParamsFactory(RilMessageDecoder caller, IccFileHandler fh) {
+        mCaller = caller;
+        mIconLoader = IconLoader.getInstance(this, fh);
+    }
+
+    private CommandDetails processCommandDetails(List<ComprehensionTlv> ctlvs) {
+        CommandDetails cmdDet = null;
+
+        if (ctlvs != null) {
+            // Search for the Command Details object.
+            ComprehensionTlv ctlvCmdDet = searchForTag(
+                    ComprehensionTlvTag.COMMAND_DETAILS, ctlvs);
+            if (ctlvCmdDet != null) {
+                try {
+                    cmdDet = ValueParser.retrieveCommandDetails(ctlvCmdDet);
+                } catch (ResultException e) {
+                    CatLog.d(this,
+                            "processCommandDetails: Failed to procees command details e=" + e);
+                }
+            }
+        }
+        return cmdDet;
+    }
+
+    void make(BerTlv berTlv) {
+        if (berTlv == null) {
+            return;
+        }
+        // reset global state parameters.
+        mCmdParams = null;
+        mIconLoadState = LOAD_NO_ICON;
+        // only proactive command messages are processed.
+        if (berTlv.getTag() != BerTlv.BER_PROACTIVE_COMMAND_TAG) {
+            sendCmdParams(ResultCode.CMD_TYPE_NOT_UNDERSTOOD);
+            return;
+        }
+        boolean cmdPending = false;
+        List<ComprehensionTlv> ctlvs = berTlv.getComprehensionTlvs();
+        // process command dtails from the tlv list.
+        CommandDetails cmdDet = processCommandDetails(ctlvs);
+        if (cmdDet == null) {
+            sendCmdParams(ResultCode.CMD_TYPE_NOT_UNDERSTOOD);
+            return;
+        }
+
+        // extract command type enumeration from the raw value stored inside
+        // the Command Details object.
+        AppInterface.CommandType cmdType = AppInterface.CommandType
+                .fromInt(cmdDet.typeOfCommand);
+        if (cmdType == null) {
+            // This PROACTIVE COMMAND is presently not handled. Hence set
+            // result code as BEYOND_TERMINAL_CAPABILITY in TR.
+            mCmdParams = new CommandParams(cmdDet);
+            sendCmdParams(ResultCode.BEYOND_TERMINAL_CAPABILITY);
+            return;
+        }
+
+        // proactive command length is incorrect.
+        if (!berTlv.isLengthValid()) {
+            mCmdParams = new CommandParams(cmdDet);
+            sendCmdParams(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+            return;
+        }
+
+        try {
+            switch (cmdType) {
+            case SET_UP_MENU:
+                cmdPending = processSelectItem(cmdDet, ctlvs);
+                break;
+            case SELECT_ITEM:
+                cmdPending = processSelectItem(cmdDet, ctlvs);
+                break;
+            case DISPLAY_TEXT:
+                cmdPending = processDisplayText(cmdDet, ctlvs);
+                break;
+             case SET_UP_IDLE_MODE_TEXT:
+                 cmdPending = processSetUpIdleModeText(cmdDet, ctlvs);
+                 break;
+             case GET_INKEY:
+                cmdPending = processGetInkey(cmdDet, ctlvs);
+                break;
+             case GET_INPUT:
+                 cmdPending = processGetInput(cmdDet, ctlvs);
+                 break;
+             case SEND_DTMF:
+             case SEND_SMS:
+             case SEND_SS:
+             case SEND_USSD:
+                 cmdPending = processEventNotify(cmdDet, ctlvs);
+                 break;
+             case GET_CHANNEL_STATUS:
+             case SET_UP_CALL:
+                 cmdPending = processSetupCall(cmdDet, ctlvs);
+                 break;
+             case REFRESH:
+                processRefresh(cmdDet, ctlvs);
+                cmdPending = false;
+                break;
+             case LAUNCH_BROWSER:
+                 cmdPending = processLaunchBrowser(cmdDet, ctlvs);
+                 break;
+             case PLAY_TONE:
+                cmdPending = processPlayTone(cmdDet, ctlvs);
+                break;
+             case SET_UP_EVENT_LIST:
+                 cmdPending = processSetUpEventList(cmdDet, ctlvs);
+                 break;
+             case PROVIDE_LOCAL_INFORMATION:
+                cmdPending = processProvideLocalInfo(cmdDet, ctlvs);
+                break;
+             case OPEN_CHANNEL:
+             case CLOSE_CHANNEL:
+             case RECEIVE_DATA:
+             case SEND_DATA:
+                 cmdPending = processBIPClient(cmdDet, ctlvs);
+                 break;
+            default:
+                // unsupported proactive commands
+                mCmdParams = new CommandParams(cmdDet);
+                sendCmdParams(ResultCode.BEYOND_TERMINAL_CAPABILITY);
+                return;
+            }
+        } catch (ResultException e) {
+            CatLog.d(this, "make: caught ResultException e=" + e);
+            mCmdParams = new CommandParams(cmdDet);
+            sendCmdParams(e.result());
+            return;
+        }
+        if (!cmdPending) {
+            sendCmdParams(ResultCode.OK);
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+        case MSG_ID_LOAD_ICON_DONE:
+            if (mIconLoader != null) {
+                sendCmdParams(setIcons(msg.obj));
+            }
+            break;
+        }
+    }
+
+    private ResultCode setIcons(Object data) {
+        Bitmap[] icons = null;
+        int iconIndex = 0;
+
+        if (data == null) {
+            CatLog.d(this, "Optional Icon data is NULL");
+            mCmdParams.mLoadIconFailed = true;
+            mloadIcon = false;
+            /** In case of icon load fail consider the
+            ** received proactive command as valid (sending RESULT OK) as
+            ** The result code, 'PRFRMD_ICON_NOT_DISPLAYED' will be added in the
+            ** terminal response by CatService/StkAppService if needed based on
+            ** the value of mLoadIconFailed.
+            */
+            return ResultCode.OK;
+        }
+        switch(mIconLoadState) {
+        case LOAD_SINGLE_ICON:
+            mCmdParams.setIcon((Bitmap) data);
+            break;
+        case LOAD_MULTI_ICONS:
+            icons = (Bitmap[]) data;
+            // set each item icon.
+            for (Bitmap icon : icons) {
+                mCmdParams.setIcon(icon);
+                if (icon == null && mloadIcon) {
+                    CatLog.d(this, "Optional Icon data is NULL while loading multi icons");
+                    mCmdParams.mLoadIconFailed = true;
+                }
+            }
+            break;
+        }
+        return ResultCode.OK;
+    }
+
+    private void sendCmdParams(ResultCode resCode) {
+        mCaller.sendMsgParamsDecoded(resCode, mCmdParams);
+    }
+
+    /**
+     * Search for a COMPREHENSION-TLV object with the given tag from a list
+     *
+     * @param tag A tag to search for
+     * @param ctlvs List of ComprehensionTlv objects used to search in
+     *
+     * @return A ComprehensionTlv object that has the tag value of {@code tag}.
+     *         If no object is found with the tag, null is returned.
+     */
+    private ComprehensionTlv searchForTag(ComprehensionTlvTag tag,
+            List<ComprehensionTlv> ctlvs) {
+        Iterator<ComprehensionTlv> iter = ctlvs.iterator();
+        return searchForNextTag(tag, iter);
+    }
+
+    /**
+     * Search for the next COMPREHENSION-TLV object with the given tag from a
+     * list iterated by {@code iter}. {@code iter} points to the object next to
+     * the found object when this method returns. Used for searching the same
+     * list for similar tags, usually item id.
+     *
+     * @param tag A tag to search for
+     * @param iter Iterator for ComprehensionTlv objects used for search
+     *
+     * @return A ComprehensionTlv object that has the tag value of {@code tag}.
+     *         If no object is found with the tag, null is returned.
+     */
+    private ComprehensionTlv searchForNextTag(ComprehensionTlvTag tag,
+            Iterator<ComprehensionTlv> iter) {
+        int tagValue = tag.value();
+        while (iter.hasNext()) {
+            ComprehensionTlv ctlv = iter.next();
+            if (ctlv.getTag() == tagValue) {
+                return ctlv;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Processes DISPLAY_TEXT proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     * @throws ResultException
+     */
+    private boolean processDisplayText(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs)
+            throws ResultException {
+
+        CatLog.d(this, "process DisplayText");
+
+        TextMessage textMsg = new TextMessage();
+        IconId iconId = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING,
+                ctlvs);
+        if (ctlv != null) {
+            textMsg.text = ValueParser.retrieveTextString(ctlv);
+        }
+        // If the tlv object doesn't exist or the it is a null object reply
+        // with command not understood.
+        if (textMsg.text == null) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.IMMEDIATE_RESPONSE, ctlvs);
+        if (ctlv != null) {
+            textMsg.responseNeeded = false;
+        }
+        // parse icon identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            textMsg.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+        // parse tone duration
+        ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs);
+        if (ctlv != null) {
+            textMsg.duration = ValueParser.retrieveDuration(ctlv);
+        }
+
+        // Parse command qualifier parameters.
+        textMsg.isHighPriority = (cmdDet.commandQualifier & 0x01) != 0;
+        textMsg.userClear = (cmdDet.commandQualifier & 0x80) != 0;
+
+        mCmdParams = new DisplayTextParams(cmdDet, textMsg);
+
+        if (iconId != null) {
+            mloadIcon = true;
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Processes SET_UP_IDLE_MODE_TEXT proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     * @throws ResultException
+     */
+    private boolean processSetUpIdleModeText(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process SetUpIdleModeText");
+
+        TextMessage textMsg = new TextMessage();
+        IconId iconId = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING,
+                ctlvs);
+        if (ctlv != null) {
+            textMsg.text = ValueParser.retrieveTextString(ctlv);
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            textMsg.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        /*
+         * If the tlv object doesn't contain text and the icon is not self
+         * explanatory then reply with command not understood.
+         */
+
+        if (textMsg.text == null && iconId != null && !textMsg.iconSelfExplanatory) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+        mCmdParams = new DisplayTextParams(cmdDet, textMsg);
+
+        if (iconId != null) {
+            mloadIcon = true;
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Processes GET_INKEY proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     * @throws ResultException
+     */
+    private boolean processGetInkey(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process GetInkey");
+
+        Input input = new Input();
+        IconId iconId = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING,
+                ctlvs);
+        if (ctlv != null) {
+            input.text = ValueParser.retrieveTextString(ctlv);
+        } else {
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING);
+        }
+        // parse icon identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            input.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        // parse duration
+        ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs);
+        if (ctlv != null) {
+            input.duration = ValueParser.retrieveDuration(ctlv);
+        }
+
+        input.minLen = 1;
+        input.maxLen = 1;
+
+        input.digitOnly = (cmdDet.commandQualifier & 0x01) == 0;
+        input.ucs2 = (cmdDet.commandQualifier & 0x02) != 0;
+        input.yesNo = (cmdDet.commandQualifier & 0x04) != 0;
+        input.helpAvailable = (cmdDet.commandQualifier & 0x80) != 0;
+        input.echo = true;
+
+        mCmdParams = new GetInputParams(cmdDet, input);
+
+        if (iconId != null) {
+            mloadIcon = true;
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Processes GET_INPUT proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     * @throws ResultException
+     */
+    private boolean processGetInput(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process GetInput");
+
+        Input input = new Input();
+        IconId iconId = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING,
+                ctlvs);
+        if (ctlv != null) {
+            input.text = ValueParser.retrieveTextString(ctlv);
+        } else {
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING);
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.RESPONSE_LENGTH, ctlvs);
+        if (ctlv != null) {
+            try {
+                byte[] rawValue = ctlv.getRawValue();
+                int valueIndex = ctlv.getValueIndex();
+                input.minLen = rawValue[valueIndex] & 0xff;
+                input.maxLen = rawValue[valueIndex + 1] & 0xff;
+            } catch (IndexOutOfBoundsException e) {
+                throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+            }
+        } else {
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING);
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.DEFAULT_TEXT, ctlvs);
+        if (ctlv != null) {
+            input.defaultText = ValueParser.retrieveTextString(ctlv);
+        }
+        // parse icon identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            input.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        input.digitOnly = (cmdDet.commandQualifier & 0x01) == 0;
+        input.ucs2 = (cmdDet.commandQualifier & 0x02) != 0;
+        input.echo = (cmdDet.commandQualifier & 0x04) == 0;
+        input.packed = (cmdDet.commandQualifier & 0x08) != 0;
+        input.helpAvailable = (cmdDet.commandQualifier & 0x80) != 0;
+
+        // Truncate the maxLen if it exceeds the max number of chars that can
+        // be encoded. Limit depends on DCS in Command Qualifier.
+        if (input.ucs2 && input.maxLen > MAX_UCS2_CHARS) {
+            CatLog.d(this, "UCS2: received maxLen = " + input.maxLen +
+                  ", truncating to " + MAX_UCS2_CHARS);
+            input.maxLen = MAX_UCS2_CHARS;
+        } else if (!input.packed && input.maxLen > MAX_GSM7_DEFAULT_CHARS) {
+            CatLog.d(this, "GSM 7Bit Default: received maxLen = " + input.maxLen +
+                  ", truncating to " + MAX_GSM7_DEFAULT_CHARS);
+            input.maxLen = MAX_GSM7_DEFAULT_CHARS;
+        }
+
+        mCmdParams = new GetInputParams(cmdDet, input);
+
+        if (iconId != null) {
+            mloadIcon = true;
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Processes REFRESH proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     */
+    private boolean processRefresh(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) {
+
+        CatLog.d(this, "process Refresh");
+
+        // REFRESH proactive command is rerouted by the baseband and handled by
+        // the telephony layer. IDLE TEXT should be removed for a REFRESH command
+        // with "initialization" or "reset"
+        switch (cmdDet.commandQualifier) {
+        case REFRESH_NAA_INIT_AND_FULL_FILE_CHANGE:
+        case REFRESH_NAA_INIT_AND_FILE_CHANGE:
+        case REFRESH_NAA_INIT:
+        case REFRESH_UICC_RESET:
+            mCmdParams = new DisplayTextParams(cmdDet, null);
+            break;
+        }
+        return false;
+    }
+
+    /**
+     * Processes SELECT_ITEM proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     * @throws ResultException
+     */
+    private boolean processSelectItem(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process SelectItem");
+
+        Menu menu = new Menu();
+        IconId titleIconId = null;
+        ItemsIconId itemsIconId = null;
+        Iterator<ComprehensionTlv> iter = ctlvs.iterator();
+
+        AppInterface.CommandType cmdType = AppInterface.CommandType
+                .fromInt(cmdDet.typeOfCommand);
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID,
+                ctlvs);
+        if (ctlv != null) {
+            menu.title = ValueParser.retrieveAlphaId(ctlv);
+        } else if (cmdType == AppInterface.CommandType.SET_UP_MENU) {
+            // According to spec ETSI TS 102 223 section 6.10.3, the
+            // Alpha ID is mandatory (and also part of minimum set of
+            // elements required) for SET_UP_MENU. If it is not received
+            // by ME, then ME should respond with "error: missing minimum
+            // information" and not "command performed successfully".
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING);
+        }
+
+        while (true) {
+            ctlv = searchForNextTag(ComprehensionTlvTag.ITEM, iter);
+            if (ctlv != null) {
+                menu.items.add(ValueParser.retrieveItem(ctlv));
+            } else {
+                break;
+            }
+        }
+
+        // We must have at least one menu item.
+        if (menu.items.size() == 0) {
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING);
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.ITEM_ID, ctlvs);
+        if (ctlv != null) {
+            // CAT items are listed 1...n while list start at 0, need to
+            // subtract one.
+            menu.defaultItem = ValueParser.retrieveItemId(ctlv) - 1;
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            mIconLoadState = LOAD_SINGLE_ICON;
+            titleIconId = ValueParser.retrieveIconId(ctlv);
+            menu.titleIconSelfExplanatory = titleIconId.selfExplanatory;
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.ITEM_ICON_ID_LIST, ctlvs);
+        if (ctlv != null) {
+            mIconLoadState = LOAD_MULTI_ICONS;
+            itemsIconId = ValueParser.retrieveItemsIconId(ctlv);
+            menu.itemsIconSelfExplanatory = itemsIconId.selfExplanatory;
+        }
+
+        boolean presentTypeSpecified = (cmdDet.commandQualifier & 0x01) != 0;
+        if (presentTypeSpecified) {
+            if ((cmdDet.commandQualifier & 0x02) == 0) {
+                menu.presentationType = PresentationType.DATA_VALUES;
+            } else {
+                menu.presentationType = PresentationType.NAVIGATION_OPTIONS;
+            }
+        }
+        menu.softKeyPreferred = (cmdDet.commandQualifier & 0x04) != 0;
+        menu.helpAvailable = (cmdDet.commandQualifier & 0x80) != 0;
+
+        mCmdParams = new SelectItemParams(cmdDet, menu, titleIconId != null);
+
+        // Load icons data if needed.
+        switch(mIconLoadState) {
+        case LOAD_NO_ICON:
+            return false;
+        case LOAD_SINGLE_ICON:
+            mloadIcon = true;
+            mIconLoader.loadIcon(titleIconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            break;
+        case LOAD_MULTI_ICONS:
+            int[] recordNumbers = itemsIconId.recordNumbers;
+            if (titleIconId != null) {
+                // Create a new array for all the icons (title and items).
+                recordNumbers = new int[itemsIconId.recordNumbers.length + 1];
+                recordNumbers[0] = titleIconId.recordNumber;
+                System.arraycopy(itemsIconId.recordNumbers, 0, recordNumbers,
+                        1, itemsIconId.recordNumbers.length);
+            }
+            mloadIcon = true;
+            mIconLoader.loadIcons(recordNumbers, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            break;
+        }
+        return true;
+    }
+
+    /**
+     * Processes EVENT_NOTIFY message from baseband.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     */
+    private boolean processEventNotify(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process EventNotify");
+
+        TextMessage textMsg = new TextMessage();
+        IconId iconId = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID,
+                ctlvs);
+        textMsg.text = ValueParser.retrieveAlphaId(ctlv);
+
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            textMsg.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        textMsg.responseNeeded = false;
+        mCmdParams = new DisplayTextParams(cmdDet, textMsg);
+
+        if (iconId != null) {
+            mloadIcon = true;
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Processes SET_UP_EVENT_LIST proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details object retrieved.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return false. This function always returns false meaning that the command
+     *         processing is  not pending and additional asynchronous processing
+     *         is not required.
+     */
+    private boolean processSetUpEventList(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) {
+
+        CatLog.d(this, "process SetUpEventList");
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.EVENT_LIST, ctlvs);
+        if (ctlv != null) {
+            try {
+                byte[] rawValue = ctlv.getRawValue();
+                int valueIndex = ctlv.getValueIndex();
+                int valueLen = ctlv.getLength();
+                int[] eventList = new int[valueLen];
+                int eventValue = -1;
+                int i = 0;
+                while (valueLen > 0) {
+                    eventValue = rawValue[valueIndex] & 0xff;
+                    valueIndex++;
+                    valueLen--;
+
+                    switch (eventValue) {
+                        case USER_ACTIVITY_EVENT:
+                        case IDLE_SCREEN_AVAILABLE_EVENT:
+                        case LANGUAGE_SELECTION_EVENT:
+                        case BROWSER_TERMINATION_EVENT:
+                        case BROWSING_STATUS_EVENT:
+                            eventList[i] = eventValue;
+                            i++;
+                            break;
+                        default:
+                            break;
+                    }
+
+                }
+                mCmdParams = new SetEventListParams(cmdDet, eventList);
+            } catch (IndexOutOfBoundsException e) {
+                CatLog.e(this, " IndexOutofBoundException in processSetUpEventList");
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Processes LAUNCH_BROWSER proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     * @throws ResultException
+     */
+    private boolean processLaunchBrowser(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process LaunchBrowser");
+
+        TextMessage confirmMsg = new TextMessage();
+        IconId iconId = null;
+        String url = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.URL, ctlvs);
+        if (ctlv != null) {
+            try {
+                byte[] rawValue = ctlv.getRawValue();
+                int valueIndex = ctlv.getValueIndex();
+                int valueLen = ctlv.getLength();
+                if (valueLen > 0) {
+                    url = GsmAlphabet.gsm8BitUnpackedToString(rawValue,
+                            valueIndex, valueLen);
+                } else {
+                    url = null;
+                }
+            } catch (IndexOutOfBoundsException e) {
+                throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+            }
+        }
+
+        // parse alpha identifier.
+        ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, ctlvs);
+        confirmMsg.text = ValueParser.retrieveAlphaId(ctlv);
+
+        // parse icon identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            confirmMsg.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        // parse command qualifier value.
+        LaunchBrowserMode mode;
+        switch (cmdDet.commandQualifier) {
+        case 0x00:
+        default:
+            mode = LaunchBrowserMode.LAUNCH_IF_NOT_ALREADY_LAUNCHED;
+            break;
+        case 0x02:
+            mode = LaunchBrowserMode.USE_EXISTING_BROWSER;
+            break;
+        case 0x03:
+            mode = LaunchBrowserMode.LAUNCH_NEW_BROWSER;
+            break;
+        }
+
+        mCmdParams = new LaunchBrowserParams(cmdDet, confirmMsg, url, mode);
+
+        if (iconId != null) {
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+     /**
+     * Processes PLAY_TONE proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details container object.
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.t
+     * @throws ResultException
+     */
+    private boolean processPlayTone(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+
+        CatLog.d(this, "process PlayTone");
+
+        Tone tone = null;
+        TextMessage textMsg = new TextMessage();
+        Duration duration = null;
+        IconId iconId = null;
+
+        ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TONE, ctlvs);
+        if (ctlv != null) {
+            // Nothing to do for null objects.
+            if (ctlv.getLength() > 0) {
+                try {
+                    byte[] rawValue = ctlv.getRawValue();
+                    int valueIndex = ctlv.getValueIndex();
+                    int toneVal = rawValue[valueIndex];
+                    tone = Tone.fromInt(toneVal);
+                } catch (IndexOutOfBoundsException e) {
+                    throw new ResultException(
+                            ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+                }
+            }
+        }
+        // parse alpha identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, ctlvs);
+        if (ctlv != null) {
+            textMsg.text = ValueParser.retrieveAlphaId(ctlv);
+        }
+        // parse tone duration
+        ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs);
+        if (ctlv != null) {
+            duration = ValueParser.retrieveDuration(ctlv);
+        }
+        // parse icon identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            textMsg.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        boolean vibrate = (cmdDet.commandQualifier & 0x01) != 0x00;
+
+        textMsg.responseNeeded = false;
+        mCmdParams = new PlayToneParams(cmdDet, textMsg, tone, duration, vibrate);
+
+        if (iconId != null) {
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Processes SETUP_CALL proactive command from the SIM card.
+     *
+     * @param cmdDet Command Details object retrieved from the proactive command
+     *        object
+     * @param ctlvs List of ComprehensionTlv objects following Command Details
+     *        object and Device Identities object within the proactive command
+     * @return true if the command is processing is pending and additional
+     *         asynchronous processing is required.
+     */
+    private boolean processSetupCall(CommandDetails cmdDet,
+            List<ComprehensionTlv> ctlvs) throws ResultException {
+        CatLog.d(this, "process SetupCall");
+
+        Iterator<ComprehensionTlv> iter = ctlvs.iterator();
+        ComprehensionTlv ctlv = null;
+        // User confirmation phase message.
+        TextMessage confirmMsg = new TextMessage();
+        // Call set up phase message.
+        TextMessage callMsg = new TextMessage();
+        IconId confirmIconId = null;
+        IconId callIconId = null;
+
+        // get confirmation message string.
+        ctlv = searchForNextTag(ComprehensionTlvTag.ALPHA_ID, iter);
+        confirmMsg.text = ValueParser.retrieveAlphaId(ctlv);
+
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            confirmIconId = ValueParser.retrieveIconId(ctlv);
+            confirmMsg.iconSelfExplanatory = confirmIconId.selfExplanatory;
+        }
+
+        // get call set up message string.
+        ctlv = searchForNextTag(ComprehensionTlvTag.ALPHA_ID, iter);
+        if (ctlv != null) {
+            callMsg.text = ValueParser.retrieveAlphaId(ctlv);
+        }
+
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            callIconId = ValueParser.retrieveIconId(ctlv);
+            callMsg.iconSelfExplanatory = callIconId.selfExplanatory;
+        }
+
+        mCmdParams = new CallSetupParams(cmdDet, confirmMsg, callMsg);
+
+        if (confirmIconId != null || callIconId != null) {
+            mIconLoadState = LOAD_MULTI_ICONS;
+            int[] recordNumbers = new int[2];
+            recordNumbers[0] = confirmIconId != null
+                    ? confirmIconId.recordNumber : -1;
+            recordNumbers[1] = callIconId != null ? callIconId.recordNumber
+                    : -1;
+
+            mIconLoader.loadIcons(recordNumbers, this
+                    .obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    private boolean processProvideLocalInfo(CommandDetails cmdDet, List<ComprehensionTlv> ctlvs)
+            throws ResultException {
+        CatLog.d(this, "process ProvideLocalInfo");
+        switch (cmdDet.commandQualifier) {
+            case DTTZ_SETTING:
+                CatLog.d(this, "PLI [DTTZ_SETTING]");
+                mCmdParams = new CommandParams(cmdDet);
+                break;
+            case LANGUAGE_SETTING:
+                CatLog.d(this, "PLI [LANGUAGE_SETTING]");
+                mCmdParams = new CommandParams(cmdDet);
+                break;
+            default:
+                CatLog.d(this, "PLI[" + cmdDet.commandQualifier + "] Command Not Supported");
+                mCmdParams = new CommandParams(cmdDet);
+                throw new ResultException(ResultCode.BEYOND_TERMINAL_CAPABILITY);
+        }
+        return false;
+    }
+
+    private boolean processBIPClient(CommandDetails cmdDet,
+                                     List<ComprehensionTlv> ctlvs) throws ResultException {
+        AppInterface.CommandType commandType =
+                                    AppInterface.CommandType.fromInt(cmdDet.typeOfCommand);
+        if (commandType != null) {
+            CatLog.d(this, "process "+ commandType.name());
+        }
+
+        TextMessage textMsg = new TextMessage();
+        IconId iconId = null;
+        ComprehensionTlv ctlv = null;
+        boolean has_alpha_id = false;
+
+        // parse alpha identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, ctlvs);
+        if (ctlv != null) {
+            textMsg.text = ValueParser.retrieveAlphaId(ctlv);
+            CatLog.d(this, "alpha TLV text=" + textMsg.text);
+            has_alpha_id = true;
+        }
+
+        // parse icon identifier
+        ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs);
+        if (ctlv != null) {
+            iconId = ValueParser.retrieveIconId(ctlv);
+            textMsg.iconSelfExplanatory = iconId.selfExplanatory;
+        }
+
+        textMsg.responseNeeded = false;
+        mCmdParams = new BIPClientParams(cmdDet, textMsg, has_alpha_id);
+
+        if (iconId != null) {
+            mIconLoadState = LOAD_SINGLE_ICON;
+            mIconLoader.loadIcon(iconId.recordNumber, obtainMessage(MSG_ID_LOAD_ICON_DONE));
+            return true;
+        }
+        return false;
+    }
+
+    public void dispose() {
+        mIconLoader.dispose();
+        mIconLoader = null;
+        mCmdParams = null;
+        mCaller = null;
+        sInstance = null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/ComprehensionTlv.java b/com/android/internal/telephony/cat/ComprehensionTlv.java
new file mode 100644
index 0000000..e2522a4
--- /dev/null
+++ b/com/android/internal/telephony/cat/ComprehensionTlv.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.cat;
+
+import android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Class for representing COMPREHENSION-TLV objects.
+ *
+ * @see "ETSI TS 101 220 subsection 7.1.1"
+ *
+ * {@hide}
+ */
+class ComprehensionTlv {
+    private static final String LOG_TAG = "ComprehensionTlv";
+    private int mTag;
+    private boolean mCr;
+    private int mLength;
+    private int mValueIndex;
+    private byte[] mRawValue;
+
+    /**
+     * Constructor. Private on purpose. Use
+     * {@link #decodeMany(byte[], int) decodeMany} or
+     * {@link #decode(byte[], int) decode} method.
+     *
+     * @param tag The tag for this object
+     * @param cr Comprehension Required flag
+     * @param length Length of the value
+     * @param data Byte array containing the value
+     * @param valueIndex Index in data at which the value starts
+     */
+    protected ComprehensionTlv(int tag, boolean cr, int length, byte[] data,
+            int valueIndex) {
+        mTag = tag;
+        mCr = cr;
+        mLength = length;
+        mValueIndex = valueIndex;
+        mRawValue = data;
+    }
+
+    public int getTag() {
+        return mTag;
+    }
+
+    public boolean isComprehensionRequired() {
+        return mCr;
+    }
+
+    public int getLength() {
+        return mLength;
+    }
+
+    public int getValueIndex() {
+        return mValueIndex;
+    }
+
+    public byte[] getRawValue() {
+        return mRawValue;
+    }
+
+    /**
+     * Parses a list of COMPREHENSION-TLV objects from a byte array.
+     *
+     * @param data A byte array containing data to be parsed
+     * @param startIndex Index in data at which to start parsing
+     * @return A list of COMPREHENSION-TLV objects parsed
+     * @throws ResultException
+     */
+    public static List<ComprehensionTlv> decodeMany(byte[] data, int startIndex)
+            throws ResultException {
+        ArrayList<ComprehensionTlv> items = new ArrayList<ComprehensionTlv>();
+        int endIndex = data.length;
+        while (startIndex < endIndex) {
+            ComprehensionTlv ctlv = ComprehensionTlv.decode(data, startIndex);
+            if (ctlv != null) {
+                items.add(ctlv);
+                startIndex = ctlv.mValueIndex + ctlv.mLength;
+            } else {
+                CatLog.d(LOG_TAG, "decodeMany: ctlv is null, stop decoding");
+                break;
+            }
+        }
+
+        return items;
+    }
+
+    /**
+     * Parses an COMPREHENSION-TLV object from a byte array.
+     *
+     * @param data A byte array containing data to be parsed
+     * @param startIndex Index in data at which to start parsing
+     * @return A COMPREHENSION-TLV object parsed
+     * @throws ResultException
+     */
+    public static ComprehensionTlv decode(byte[] data, int startIndex)
+            throws ResultException {
+        int curIndex = startIndex;
+        int endIndex = data.length;
+
+        try {
+            /* tag */
+            int tag;
+            boolean cr; // Comprehension required flag
+            int temp = data[curIndex++] & 0xff;
+            switch (temp) {
+            case 0:
+            case 0xff:
+            case 0x80:
+                Rlog.d("CAT     ", "decode: unexpected first tag byte=" + Integer.toHexString(temp) +
+                        ", startIndex=" + startIndex + " curIndex=" + curIndex +
+                        " endIndex=" + endIndex);
+                // Return null which will stop decoding, this has occurred
+                // with Ghana MTN simcard and JDI simcard.
+                return null;
+
+            case 0x7f: // tag is in three-byte format
+                tag = ((data[curIndex] & 0xff) << 8)
+                        | (data[curIndex + 1] & 0xff);
+                cr = (tag & 0x8000) != 0;
+                tag &= ~0x8000;
+                curIndex += 2;
+                break;
+
+            default: // tag is in single-byte format
+                tag = temp;
+                cr = (tag & 0x80) != 0;
+                tag &= ~0x80;
+                break;
+            }
+
+            /* length */
+            int length;
+            temp = data[curIndex++] & 0xff;
+            if (temp < 0x80) {
+                length = temp;
+            } else if (temp == 0x81) {
+                length = data[curIndex++] & 0xff;
+                if (length < 0x80) {
+                    throw new ResultException(
+                            ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                            "length < 0x80 length=" + Integer.toHexString(length) +
+                            " startIndex=" + startIndex + " curIndex=" + curIndex +
+                            " endIndex=" + endIndex);
+                }
+            } else if (temp == 0x82) {
+                length = ((data[curIndex] & 0xff) << 8)
+                        | (data[curIndex + 1] & 0xff);
+                curIndex += 2;
+                if (length < 0x100) {
+                    throw new ResultException(
+                            ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                            "two byte length < 0x100 length=" + Integer.toHexString(length) +
+                            " startIndex=" + startIndex + " curIndex=" + curIndex +
+                            " endIndex=" + endIndex);
+                }
+            } else if (temp == 0x83) {
+                length = ((data[curIndex] & 0xff) << 16)
+                        | ((data[curIndex + 1] & 0xff) << 8)
+                        | (data[curIndex + 2] & 0xff);
+                curIndex += 3;
+                if (length < 0x10000) {
+                    throw new ResultException(
+                            ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                            "three byte length < 0x10000 length=0x" + Integer.toHexString(length) +
+                            " startIndex=" + startIndex + " curIndex=" + curIndex +
+                            " endIndex=" + endIndex);
+                }
+            } else {
+                throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                        "Bad length modifer=" + temp +
+                        " startIndex=" + startIndex + " curIndex=" + curIndex +
+                        " endIndex=" + endIndex);
+
+            }
+
+            return new ComprehensionTlv(tag, cr, length, data, curIndex);
+
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD,
+                    "IndexOutOfBoundsException" + " startIndex=" + startIndex +
+                    " curIndex=" + curIndex + " endIndex=" + endIndex);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cat/ComprehensionTlvTag.java b/com/android/internal/telephony/cat/ComprehensionTlvTag.java
new file mode 100644
index 0000000..973dbc8
--- /dev/null
+++ b/com/android/internal/telephony/cat/ComprehensionTlvTag.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.cat;
+
+/**
+ * Enumeration for representing the tag value of COMPREHENSION-TLV objects. If
+ * you want to get the actual value, call {@link #value() value} method.
+ *
+ * {@hide}
+ */
+public enum ComprehensionTlvTag {
+    COMMAND_DETAILS(0x01),
+    DEVICE_IDENTITIES(0x02),
+    RESULT(0x03),
+    DURATION(0x04),
+    ALPHA_ID(0x05),
+    ADDRESS(0x06),
+    USSD_STRING(0x0a),
+    SMS_TPDU(0x0b),
+    TEXT_STRING(0x0d),
+    TONE(0x0e),
+    ITEM(0x0f),
+    ITEM_ID(0x10),
+    RESPONSE_LENGTH(0x11),
+    FILE_LIST(0x12),
+    HELP_REQUEST(0x15),
+    DEFAULT_TEXT(0x17),
+    EVENT_LIST(0x19),
+    ICON_ID(0x1e),
+    ITEM_ICON_ID_LIST(0x1f),
+    IMMEDIATE_RESPONSE(0x2b),
+    LANGUAGE(0x2d),
+    URL(0x31),
+    BROWSER_TERMINATION_CAUSE(0x34),
+    TEXT_ATTRIBUTE(0x50);
+
+    private int mValue;
+
+    ComprehensionTlvTag(int value) {
+        mValue = value;
+    }
+
+    /**
+     * Returns the actual value of this COMPREHENSION-TLV object.
+     *
+     * @return Actual tag value of this object
+     */
+    public int value() {
+        return mValue;
+    }
+
+    public static ComprehensionTlvTag fromInt(int value) {
+        for (ComprehensionTlvTag e : ComprehensionTlvTag.values()) {
+            if (e.mValue == value) {
+                return e;
+            }
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/Duration.java b/com/android/internal/telephony/cat/Duration.java
new file mode 100644
index 0000000..e437f6e
--- /dev/null
+++ b/com/android/internal/telephony/cat/Duration.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * Class for representing "Duration" object for CAT.
+ *
+ * {@hide}
+ */
+public class Duration implements Parcelable {
+    public int timeInterval;
+    public TimeUnit timeUnit;
+
+    public enum TimeUnit {
+        MINUTE(0x00),
+        SECOND(0x01),
+        TENTH_SECOND(0x02);
+
+        private int mValue;
+
+        TimeUnit(int value) {
+            mValue = value;
+        }
+
+        public int value() {
+            return mValue;
+        }
+    }
+
+    /**
+     * @param timeInterval Between 1 and 255 inclusive.
+     */
+    public Duration(int timeInterval, TimeUnit timeUnit) {
+        this.timeInterval = timeInterval;
+        this.timeUnit = timeUnit;
+    }
+
+    private Duration(Parcel in) {
+        timeInterval = in.readInt();
+        timeUnit = TimeUnit.values()[in.readInt()];
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(timeInterval);
+        dest.writeInt(timeUnit.ordinal());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<Duration> CREATOR = new Parcelable.Creator<Duration>() {
+        @Override
+        public Duration createFromParcel(Parcel in) {
+            return new Duration(in);
+        }
+
+        @Override
+        public Duration[] newArray(int size) {
+            return new Duration[size];
+        }
+    };
+}
diff --git a/com/android/internal/telephony/cat/FontSize.java b/com/android/internal/telephony/cat/FontSize.java
new file mode 100644
index 0000000..02c7ea0
--- /dev/null
+++ b/com/android/internal/telephony/cat/FontSize.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Enumeration for representing text font size.
+ *
+ * {@hide}
+ */
+public enum FontSize {
+    NORMAL(0x0),
+    LARGE(0x1),
+    SMALL(0x2);
+
+    private int mValue;
+
+    FontSize(int value) {
+        mValue = value;
+    }
+
+    /**
+     * Create a FontSize object.
+     * @param value Integer value to be converted to a FontSize object.
+     * @return FontSize object whose value is {@code value}. If no
+     *         FontSize object has that value, null is returned.
+     */
+    public static FontSize fromInt(int value) {
+        for (FontSize e : FontSize.values()) {
+            if (e.mValue == value) {
+                return e;
+            }
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/IconLoader.java b/com/android/internal/telephony/cat/IconLoader.java
new file mode 100644
index 0000000..6a668e0
--- /dev/null
+++ b/com/android/internal/telephony/cat/IconLoader.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import com.android.internal.telephony.uicc.IccFileHandler;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import java.util.HashMap;
+
+/**
+ * Class for loading icons from the SIM card. Has two states: single, for loading
+ * one icon. Multi, for loading icons list.
+ *
+ */
+class IconLoader extends Handler {
+    // members
+    private int mState = STATE_SINGLE_ICON;
+    private ImageDescriptor mId = null;
+    private Bitmap mCurrentIcon = null;
+    private int mRecordNumber;
+    private IccFileHandler mSimFH = null;
+    private Message mEndMsg = null;
+    private byte[] mIconData = null;
+    // multi icons state members
+    private int[] mRecordNumbers = null;
+    private int mCurrentRecordIndex = 0;
+    private Bitmap[] mIcons = null;
+    private HashMap<Integer, Bitmap> mIconsCache = null;
+
+    private static IconLoader sLoader = null;
+    private static HandlerThread sThread = null;
+
+    // Loader state values.
+    private static final int STATE_SINGLE_ICON = 1;
+    private static final int STATE_MULTI_ICONS = 2;
+
+    // Finished loading single record from a linear-fixed EF-IMG.
+    private static final int EVENT_READ_EF_IMG_RECOED_DONE  = 1;
+    // Finished loading single icon from a Transparent DF-Graphics.
+    private static final int EVENT_READ_ICON_DONE           = 2;
+    // Finished loading single colour icon lookup table.
+    private static final int EVENT_READ_CLUT_DONE           = 3;
+
+    // Color lookup table offset inside the EF.
+    private static final int CLUT_LOCATION_OFFSET = 4;
+    // CLUT entry size, {Red, Green, Black}
+    private static final int CLUT_ENTRY_SIZE = 3;
+
+
+    private IconLoader(Looper looper , IccFileHandler fh) {
+        super(looper);
+        mSimFH = fh;
+
+        mIconsCache = new HashMap<Integer, Bitmap>(50);
+    }
+
+    static IconLoader getInstance(Handler caller, IccFileHandler fh) {
+        if (sLoader != null) {
+            return sLoader;
+        }
+        if (fh != null) {
+            sThread = new HandlerThread("Cat Icon Loader");
+            sThread.start();
+            return new IconLoader(sThread.getLooper(), fh);
+        }
+        return null;
+    }
+
+    void loadIcons(int[] recordNumbers, Message msg) {
+        if (recordNumbers == null || recordNumbers.length == 0 || msg == null) {
+            return;
+        }
+        mEndMsg = msg;
+        // initialize multi icons load variables.
+        mIcons = new Bitmap[recordNumbers.length];
+        mRecordNumbers = recordNumbers;
+        mCurrentRecordIndex = 0;
+        mState = STATE_MULTI_ICONS;
+        startLoadingIcon(recordNumbers[0]);
+    }
+
+    void loadIcon(int recordNumber, Message msg) {
+        if (msg == null) {
+            return;
+        }
+        mEndMsg = msg;
+        mState = STATE_SINGLE_ICON;
+        startLoadingIcon(recordNumber);
+    }
+
+    private void startLoadingIcon(int recordNumber) {
+        // Reset the load variables.
+        mId = null;
+        mIconData = null;
+        mCurrentIcon = null;
+        mRecordNumber = recordNumber;
+
+        // make sure the icon was not already loaded and saved in the local cache.
+        if (mIconsCache.containsKey(recordNumber)) {
+            mCurrentIcon = mIconsCache.get(recordNumber);
+            postIcon();
+            return;
+        }
+
+        // start the first phase ==> loading Image Descriptor.
+        readId();
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        try {
+            switch (msg.what) {
+            case EVENT_READ_EF_IMG_RECOED_DONE:
+                ar = (AsyncResult) msg.obj;
+                if (handleImageDescriptor((byte[]) ar.result)) {
+                    readIconData();
+                } else {
+                    throw new Exception("Unable to parse image descriptor");
+                }
+                break;
+            case EVENT_READ_ICON_DONE:
+                CatLog.d(this, "load icon done");
+                ar = (AsyncResult) msg.obj;
+                byte[] rawData = ((byte[]) ar.result);
+                if (mId.mCodingScheme == ImageDescriptor.CODING_SCHEME_BASIC) {
+                    mCurrentIcon = parseToBnW(rawData, rawData.length);
+                    mIconsCache.put(mRecordNumber, mCurrentIcon);
+                    postIcon();
+                } else if (mId.mCodingScheme == ImageDescriptor.CODING_SCHEME_COLOUR) {
+                    mIconData = rawData;
+                    readClut();
+                } else {
+                    CatLog.d(this, "else  /postIcon ");
+                    postIcon();
+                }
+                break;
+            case EVENT_READ_CLUT_DONE:
+                ar = (AsyncResult) msg.obj;
+                byte [] clut = ((byte[]) ar.result);
+                mCurrentIcon = parseToRGB(mIconData, mIconData.length,
+                        false, clut);
+                mIconsCache.put(mRecordNumber, mCurrentIcon);
+                postIcon();
+                break;
+            }
+        } catch (Exception e) {
+            CatLog.d(this, "Icon load failed");
+            // post null icon back to the caller.
+            postIcon();
+        }
+    }
+
+    /**
+     * Handles Image descriptor parsing and required processing. This is the
+     * first step required to handle retrieving icons from the SIM.
+     *
+     * @param rawData byte [] containing Image Instance descriptor as defined in
+     * TS 51.011.
+     */
+    private boolean handleImageDescriptor(byte[] rawData) {
+        mId = ImageDescriptor.parse(rawData, 1);
+        if (mId == null) {
+            return false;
+        }
+        return true;
+    }
+
+    // Start reading color lookup table from SIM card.
+    private void readClut() {
+        int length = mIconData[3] * CLUT_ENTRY_SIZE;
+        Message msg = obtainMessage(EVENT_READ_CLUT_DONE);
+        mSimFH.loadEFImgTransparent(mId.mImageId,
+                mIconData[CLUT_LOCATION_OFFSET],
+                mIconData[CLUT_LOCATION_OFFSET + 1], length, msg);
+    }
+
+    // Start reading Image Descriptor from SIM card.
+    private void readId() {
+        if (mRecordNumber < 0) {
+            mCurrentIcon = null;
+            postIcon();
+            return;
+        }
+        Message msg = obtainMessage(EVENT_READ_EF_IMG_RECOED_DONE);
+        mSimFH.loadEFImgLinearFixed(mRecordNumber, msg);
+    }
+
+    // Start reading icon bytes array from SIM card.
+    private void readIconData() {
+        Message msg = obtainMessage(EVENT_READ_ICON_DONE);
+        mSimFH.loadEFImgTransparent(mId.mImageId, 0, 0, mId.mLength ,msg);
+    }
+
+    // When all is done pass icon back to caller.
+    private void postIcon() {
+        if (mState == STATE_SINGLE_ICON) {
+            mEndMsg.obj = mCurrentIcon;
+            mEndMsg.sendToTarget();
+        } else if (mState == STATE_MULTI_ICONS) {
+            mIcons[mCurrentRecordIndex++] = mCurrentIcon;
+            // If not all icons were loaded, start loading the next one.
+            if (mCurrentRecordIndex < mRecordNumbers.length) {
+                startLoadingIcon(mRecordNumbers[mCurrentRecordIndex]);
+            } else {
+                mEndMsg.obj = mIcons;
+                mEndMsg.sendToTarget();
+            }
+        }
+    }
+
+    /**
+     * Convert a TS 131.102 image instance of code scheme '11' into Bitmap
+     * @param data The raw data
+     * @param length The length of image body
+     * @return The bitmap
+     */
+    public static Bitmap parseToBnW(byte[] data, int length){
+        int valueIndex = 0;
+        int width = data[valueIndex++] & 0xFF;
+        int height = data[valueIndex++] & 0xFF;
+        int numOfPixels = width*height;
+
+        int[] pixels = new int[numOfPixels];
+
+        int pixelIndex = 0;
+        int bitIndex = 7;
+        byte currentByte = 0x00;
+        while (pixelIndex < numOfPixels) {
+            // reassign data and index for every byte (8 bits).
+            if (pixelIndex % 8 == 0) {
+                currentByte = data[valueIndex++];
+                bitIndex = 7;
+            }
+            pixels[pixelIndex++] = bitToBnW((currentByte >> bitIndex-- ) & 0x01);
+        }
+
+        if (pixelIndex != numOfPixels) {
+            CatLog.d("IconLoader", "parseToBnW; size error");
+        }
+        return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
+    }
+
+    /**
+     * Decode one bit to a black and white color:
+     * 0 is black
+     * 1 is white
+     * @param bit to decode
+     * @return RGB color
+     */
+    private static int bitToBnW(int bit){
+        if(bit == 1){
+            return Color.WHITE;
+        } else {
+            return Color.BLACK;
+        }
+    }
+
+    /**
+     * a TS 131.102 image instance of code scheme '11' into color Bitmap
+     *
+     * @param data The raw data
+     * @param length the length of image body
+     * @param transparency with or without transparency
+     * @param clut coulor lookup table
+     * @return The color bitmap
+     */
+    public static Bitmap parseToRGB(byte[] data, int length,
+            boolean transparency, byte[] clut) {
+        int valueIndex = 0;
+        int width = data[valueIndex++] & 0xFF;
+        int height = data[valueIndex++] & 0xFF;
+        int bitsPerImg = data[valueIndex++] & 0xFF;
+        int numOfClutEntries = data[valueIndex++] & 0xFF;
+
+        if (true == transparency) {
+            clut[numOfClutEntries - 1] = Color.TRANSPARENT;
+        }
+
+        int numOfPixels = width * height;
+        int[] pixels = new int[numOfPixels];
+
+        valueIndex = 6;
+        int pixelIndex = 0;
+        int bitsStartOffset = 8 - bitsPerImg;
+        int bitIndex = bitsStartOffset;
+        byte currentByte = data[valueIndex++];
+        int mask = getMask(bitsPerImg);
+        boolean bitsOverlaps = (8 % bitsPerImg == 0);
+        while (pixelIndex < numOfPixels) {
+            // reassign data and index for every byte (8 bits).
+            if (bitIndex < 0) {
+                currentByte = data[valueIndex++];
+                bitIndex = bitsOverlaps ? (bitsStartOffset) : (bitIndex * -1);
+            }
+            int clutEntry = ((currentByte >> bitIndex) & mask);
+            int clutIndex = clutEntry * CLUT_ENTRY_SIZE;
+            pixels[pixelIndex++] = Color.rgb(clut[clutIndex],
+                    clut[clutIndex + 1], clut[clutIndex + 2]);
+            bitIndex -= bitsPerImg;
+        }
+
+        return Bitmap.createBitmap(pixels, width, height,
+                Bitmap.Config.ARGB_8888);
+    }
+
+    /**
+     * Calculate bit mask for a given number of bits. The mask should enable to
+     * make a bitwise and to the given number of bits.
+     * @param numOfBits number of bits to calculate mask for.
+     * @return bit mask
+     */
+    private static int getMask(int numOfBits) {
+        int mask = 0x00;
+
+        switch (numOfBits) {
+        case 1:
+            mask = 0x01;
+            break;
+        case 2:
+            mask = 0x03;
+            break;
+        case 3:
+            mask = 0x07;
+            break;
+        case 4:
+            mask = 0x0F;
+            break;
+        case 5:
+            mask = 0x1F;
+            break;
+        case 6:
+            mask = 0x3F;
+            break;
+        case 7:
+            mask = 0x7F;
+            break;
+        case 8:
+            mask = 0xFF;
+            break;
+        }
+        return mask;
+    }
+    public void dispose() {
+        mSimFH = null;
+        if (sThread != null) {
+            sThread.quit();
+            sThread = null;
+        }
+        mIconsCache = null;
+        sLoader = null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/ImageDescriptor.java b/com/android/internal/telephony/cat/ImageDescriptor.java
new file mode 100644
index 0000000..29e6c76
--- /dev/null
+++ b/com/android/internal/telephony/cat/ImageDescriptor.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+/**
+ * {@hide}
+ */
+public class ImageDescriptor {
+    // members
+    int mWidth;
+    int mHeight;
+    int mCodingScheme;
+    int mImageId;
+    int mHighOffset;
+    int mLowOffset;
+    int mLength;
+
+    // constants
+    static final int CODING_SCHEME_BASIC = 0x11;
+    static final int CODING_SCHEME_COLOUR = 0x21;
+
+    // public static final int ID_LENGTH = 9;
+    // ID_LENGTH substituted by IccFileHandlerBase.GET_RESPONSE_EF_IMG_SIZE_BYTES
+
+    ImageDescriptor() {
+        mWidth = 0;
+        mHeight = 0;
+        mCodingScheme = 0;
+        mImageId = 0;
+        mHighOffset = 0;
+        mLowOffset = 0;
+        mLength = 0;
+    }
+
+    /**
+     * Extract descriptor information about image instance.
+     *
+     * @param rawData
+     * @param valueIndex
+     * @return ImageDescriptor
+     */
+    static ImageDescriptor parse(byte[] rawData, int valueIndex) {
+        ImageDescriptor d = new ImageDescriptor();
+        try {
+            d.mWidth = rawData[valueIndex++] & 0xff;
+            d.mHeight = rawData[valueIndex++] & 0xff;
+            d.mCodingScheme = rawData[valueIndex++] & 0xff;
+
+            // parse image id
+            d.mImageId = (rawData[valueIndex++] & 0xff) << 8;
+            d.mImageId |= rawData[valueIndex++] & 0xff;
+            // parse offset
+            d.mHighOffset = (rawData[valueIndex++] & 0xff); // high byte offset
+            d.mLowOffset = rawData[valueIndex++] & 0xff; // low byte offset
+
+            d.mLength = ((rawData[valueIndex++] & 0xff) << 8 | (rawData[valueIndex++] & 0xff));
+            CatLog.d("ImageDescriptor", "parse; Descriptor : " + d.mWidth + ", " + d.mHeight +
+                    ", " + d.mCodingScheme + ", 0x" + Integer.toHexString(d.mImageId) + ", " +
+                    d.mHighOffset + ", " + d.mLowOffset + ", " + d.mLength);
+        } catch (IndexOutOfBoundsException e) {
+            CatLog.d("ImageDescriptor", "parse; failed parsing image descriptor");
+            d = null;
+        }
+        return d;
+    }
+}
diff --git a/com/android/internal/telephony/cat/Input.java b/com/android/internal/telephony/cat/Input.java
new file mode 100644
index 0000000..aaaff43
--- /dev/null
+++ b/com/android/internal/telephony/cat/Input.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Container class for CAT GET INPUT, GET IN KEY commands parameters.
+ *
+ */
+public class Input implements Parcelable {
+    public String text;
+    public String defaultText;
+    public Bitmap icon;
+    public int minLen;
+    public int maxLen;
+    public boolean ucs2;
+    public boolean packed;
+    public boolean digitOnly;
+    public boolean echo;
+    public boolean yesNo;
+    public boolean helpAvailable;
+    public Duration duration;
+    public boolean iconSelfExplanatory;
+
+    Input() {
+        text = "";
+        defaultText = null;
+        icon = null;
+        minLen = 0;
+        maxLen = 1;
+        ucs2 = false;
+        packed = false;
+        digitOnly = false;
+        echo = false;
+        yesNo = false;
+        helpAvailable = false;
+        duration = null;
+        iconSelfExplanatory = false;
+    }
+
+    private Input(Parcel in) {
+        text = in.readString();
+        defaultText = in.readString();
+        icon = in.readParcelable(null);
+        minLen = in.readInt();
+        maxLen = in.readInt();
+        ucs2 = in.readInt() == 1 ? true : false;
+        packed = in.readInt() == 1 ? true : false;
+        digitOnly = in.readInt() == 1 ? true : false;
+        echo = in.readInt() == 1 ? true : false;
+        yesNo = in.readInt() == 1 ? true : false;
+        helpAvailable = in.readInt() == 1 ? true : false;
+        duration = in.readParcelable(null);
+        iconSelfExplanatory = in.readInt() == 1 ? true : false;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(text);
+        dest.writeString(defaultText);
+        dest.writeParcelable(icon, 0);
+        dest.writeInt(minLen);
+        dest.writeInt(maxLen);
+        dest.writeInt(ucs2 ? 1 : 0);
+        dest.writeInt(packed ? 1 : 0);
+        dest.writeInt(digitOnly ? 1 : 0);
+        dest.writeInt(echo ? 1 : 0);
+        dest.writeInt(yesNo ? 1 : 0);
+        dest.writeInt(helpAvailable ? 1 : 0);
+        dest.writeParcelable(duration, 0);
+        dest.writeInt(iconSelfExplanatory ? 1 : 0);
+    }
+
+    public static final Parcelable.Creator<Input> CREATOR = new Parcelable.Creator<Input>() {
+        @Override
+        public Input createFromParcel(Parcel in) {
+            return new Input(in);
+        }
+
+        @Override
+        public Input[] newArray(int size) {
+            return new Input[size];
+        }
+    };
+
+    boolean setIcon(Bitmap Icon) { return true; }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/cat/Item.java b/com/android/internal/telephony/cat/Item.java
new file mode 100644
index 0000000..456a46f
--- /dev/null
+++ b/com/android/internal/telephony/cat/Item.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents an Item COMPREHENSION-TLV object.
+ *
+ * {@hide}
+ */
+public class Item implements Parcelable {
+    /** Identifier of the item. */
+    public int id;
+    /** Text string of the item. */
+    public String text;
+    /** Icon of the item */
+    public Bitmap icon;
+
+    public Item(int id, String text) {
+        this(id, text, null);
+    }
+
+    public Item(int id, String text, Bitmap icon) {
+        this.id = id;
+        this.text = text;
+        this.icon = icon;
+    }
+
+    public Item(Parcel in) {
+        id = in.readInt();
+        text = in.readString();
+        icon = in.readParcelable(null);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(id);
+        dest.writeString(text);
+        dest.writeParcelable(icon, flags);
+    }
+
+    public static final Parcelable.Creator<Item> CREATOR = new Parcelable.Creator<Item>() {
+        @Override
+        public Item createFromParcel(Parcel in) {
+            return new Item(in);
+        }
+
+        @Override
+        public Item[] newArray(int size) {
+            return new Item[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return text;
+    }
+}
diff --git a/com/android/internal/telephony/cat/LaunchBrowserMode.java b/com/android/internal/telephony/cat/LaunchBrowserMode.java
new file mode 100644
index 0000000..af043d1
--- /dev/null
+++ b/com/android/internal/telephony/cat/LaunchBrowserMode.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Browser launch mode for LAUNCH BROWSER proactive command.
+ *
+ * {@hide}
+ */
+public enum LaunchBrowserMode {
+    /** Launch browser if not already launched. */
+    LAUNCH_IF_NOT_ALREADY_LAUNCHED,
+    /**
+     * Use the existing browser (the browser shall not use the active existing
+     * secured session).
+     */
+    USE_EXISTING_BROWSER,
+    /** Close the existing browser session and launch new browser session. */
+    LAUNCH_NEW_BROWSER;
+}
diff --git a/com/android/internal/telephony/cat/Menu.java b/com/android/internal/telephony/cat/Menu.java
new file mode 100644
index 0000000..a3cbbdf
--- /dev/null
+++ b/com/android/internal/telephony/cat/Menu.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container class for CAT menu (SET UP MENU, SELECT ITEM) parameters.
+ *
+ */
+public class Menu implements Parcelable {
+    public List<Item> items;
+    public List<TextAttribute> titleAttrs;
+    public PresentationType presentationType;
+    public String title;
+    public Bitmap titleIcon;
+    public int defaultItem;
+    public boolean softKeyPreferred;
+    public boolean helpAvailable;
+    public boolean titleIconSelfExplanatory;
+    public boolean itemsIconSelfExplanatory;
+
+    public Menu() {
+        // Create an empty list.
+        items = new ArrayList<Item>();
+        title = null;
+        titleAttrs = null;
+        defaultItem = 0;
+        softKeyPreferred = false;
+        helpAvailable = false;
+        titleIconSelfExplanatory = false;
+        itemsIconSelfExplanatory = false;
+        titleIcon = null;
+        // set default style to be navigation menu.
+        presentationType = PresentationType.NAVIGATION_OPTIONS;
+    }
+
+    private Menu(Parcel in) {
+        title = in.readString();
+        titleIcon = in.readParcelable(null);
+        // rebuild items list.
+        items = new ArrayList<Item>();
+        int size = in.readInt();
+        for (int i=0; i<size; i++) {
+            Item item = in.readParcelable(null);
+            items.add(item);
+        }
+        defaultItem = in.readInt();
+        softKeyPreferred = in.readInt() == 1 ? true : false;
+        helpAvailable = in.readInt() == 1 ? true : false;
+        titleIconSelfExplanatory = in.readInt() == 1 ? true : false;
+        itemsIconSelfExplanatory = in.readInt() == 1 ? true : false;
+        presentationType = PresentationType.values()[in.readInt()];
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(title);
+        dest.writeParcelable(titleIcon, flags);
+        // write items list to the parcel.
+        int size = items.size();
+        dest.writeInt(size);
+        for (int i=0; i<size; i++) {
+            dest.writeParcelable(items.get(i), flags);
+        }
+        dest.writeInt(defaultItem);
+        dest.writeInt(softKeyPreferred ? 1 : 0);
+        dest.writeInt(helpAvailable ? 1 : 0);
+        dest.writeInt(titleIconSelfExplanatory ? 1 : 0);
+        dest.writeInt(itemsIconSelfExplanatory ? 1 : 0);
+        dest.writeInt(presentationType.ordinal());
+    }
+
+    public static final Parcelable.Creator<Menu> CREATOR = new Parcelable.Creator<Menu>() {
+        @Override
+        public Menu createFromParcel(Parcel in) {
+            return new Menu(in);
+        }
+
+        @Override
+        public Menu[] newArray(int size) {
+            return new Menu[size];
+        }
+    };
+}
diff --git a/com/android/internal/telephony/cat/PresentationType.java b/com/android/internal/telephony/cat/PresentationType.java
new file mode 100644
index 0000000..7c8cd8c
--- /dev/null
+++ b/com/android/internal/telephony/cat/PresentationType.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Presentation types for SELECT TYPE proactive command.
+ *
+ * {@hide}
+ */
+public enum PresentationType {
+    /** Presentation type is not specified */
+    NOT_SPECIFIED,
+    /** Presentation as a choice of data values */
+    DATA_VALUES,
+    /** Presentation as a choice of navigation options */
+    NAVIGATION_OPTIONS;
+}
diff --git a/com/android/internal/telephony/cat/ResponseData.java b/com/android/internal/telephony/cat/ResponseData.java
new file mode 100644
index 0000000..7112bf1
--- /dev/null
+++ b/com/android/internal/telephony/cat/ResponseData.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2006-2007 Google Inc.
+ *
+ * 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.internal.telephony.cat;
+
+import com.android.internal.telephony.EncodeException;
+import com.android.internal.telephony.GsmAlphabet;
+import java.util.Calendar;
+import java.util.TimeZone;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.cat.AppInterface.CommandType;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+
+abstract class ResponseData {
+    /**
+     * Format the data appropriate for TERMINAL RESPONSE and write it into
+     * the ByteArrayOutputStream object.
+     */
+    public abstract void format(ByteArrayOutputStream buf);
+
+    public static void writeLength(ByteArrayOutputStream buf, int length) {
+        // As per ETSI 102.220 Sec7.1.2, if the total length is greater
+        // than 0x7F, it should be coded in two bytes and the first byte
+        // should be 0x81.
+        if (length > 0x7F) {
+            buf.write(0x81);
+        }
+        buf.write(length);
+    }
+}
+
+class SelectItemResponseData extends ResponseData {
+    // members
+    private int mId;
+
+    public SelectItemResponseData(int id) {
+        super();
+        mId = id;
+    }
+
+    @Override
+    public void format(ByteArrayOutputStream buf) {
+        // Item identifier object
+        int tag = 0x80 | ComprehensionTlvTag.ITEM_ID.value();
+        buf.write(tag); // tag
+        buf.write(1); // length
+        buf.write(mId); // identifier of item chosen
+    }
+}
+
+class GetInkeyInputResponseData extends ResponseData {
+    // members
+    private boolean mIsUcs2;
+    private boolean mIsPacked;
+    private boolean mIsYesNo;
+    private boolean mYesNoResponse;
+    public String mInData;
+
+    // GetInKey Yes/No response characters constants.
+    protected static final byte GET_INKEY_YES = 0x01;
+    protected static final byte GET_INKEY_NO = 0x00;
+
+    public GetInkeyInputResponseData(String inData, boolean ucs2, boolean packed) {
+        super();
+        mIsUcs2 = ucs2;
+        mIsPacked = packed;
+        mInData = inData;
+        mIsYesNo = false;
+    }
+
+    public GetInkeyInputResponseData(boolean yesNoResponse) {
+        super();
+        mIsUcs2 = false;
+        mIsPacked = false;
+        mInData = "";
+        mIsYesNo = true;
+        mYesNoResponse = yesNoResponse;
+    }
+
+    @Override
+    public void format(ByteArrayOutputStream buf) {
+        if (buf == null) {
+            return;
+        }
+
+        // Text string object
+        int tag = 0x80 | ComprehensionTlvTag.TEXT_STRING.value();
+        buf.write(tag); // tag
+
+        byte[] data;
+
+        if (mIsYesNo) {
+            data = new byte[1];
+            data[0] = mYesNoResponse ? GET_INKEY_YES : GET_INKEY_NO;
+        } else if (mInData != null && mInData.length() > 0) {
+            try {
+                // ETSI TS 102 223 8.15, should use the same format as in SMS messages
+                // on the network.
+                if (mIsUcs2) {
+                    // ucs2 is by definition big endian.
+                    data = mInData.getBytes("UTF-16BE");
+                } else if (mIsPacked) {
+                    byte[] tempData = GsmAlphabet
+                            .stringToGsm7BitPacked(mInData, 0, 0);
+                    // The size of the new buffer will be smaller than the original buffer
+                    // since 7-bit GSM packed only requires ((mInData.length * 7) + 7) / 8 bytes.
+                    // And we don't need to copy/store the first byte from the returned array
+                    // because it is used to store the count of septets used.
+                    data = new byte[tempData.length - 1];
+                    System.arraycopy(tempData, 1, data, 0, tempData.length - 1);
+                } else {
+                    data = GsmAlphabet.stringToGsm8BitPacked(mInData);
+                }
+            } catch (UnsupportedEncodingException e) {
+                data = new byte[0];
+            } catch (EncodeException e) {
+                data = new byte[0];
+            }
+        } else {
+            data = new byte[0];
+        }
+
+        // length - one more for data coding scheme.
+
+        // ETSI TS 102 223 Annex C (normative): Structure of CAT communications
+        // Any length within the APDU limits (up to 255 bytes) can thus be encoded on two bytes.
+        // This coding is chosen to remain compatible with TS 101.220.
+        // Note that we need to reserve one more byte for coding scheme thus the maximum APDU
+        // size would be 254 bytes.
+        if (data.length + 1 <= 255) {
+            writeLength(buf, data.length + 1);
+        }
+        else {
+            data = new byte[0];
+        }
+
+
+        // data coding scheme
+        if (mIsUcs2) {
+            buf.write(0x08); // UCS2
+        } else if (mIsPacked) {
+            buf.write(0x00); // 7 bit packed
+        } else {
+            buf.write(0x04); // 8 bit unpacked
+        }
+
+        for (byte b : data) {
+            buf.write(b);
+        }
+    }
+}
+
+// For "PROVIDE LOCAL INFORMATION" command.
+// See TS 31.111 section 6.4.15/ETSI TS 102 223
+// TS 31.124 section 27.22.4.15 for test spec
+class LanguageResponseData extends ResponseData {
+    private String mLang;
+
+    public LanguageResponseData(String lang) {
+        super();
+        mLang = lang;
+    }
+
+    @Override
+    public void format(ByteArrayOutputStream buf) {
+        if (buf == null) {
+            return;
+        }
+
+        // Text string object
+        int tag = 0x80 | ComprehensionTlvTag.LANGUAGE.value();
+        buf.write(tag); // tag
+
+        byte[] data;
+
+        if (mLang != null && mLang.length() > 0) {
+            data = GsmAlphabet.stringToGsm8BitPacked(mLang);
+        }
+        else {
+            data = new byte[0];
+        }
+
+        buf.write(data.length);
+
+        for (byte b : data) {
+            buf.write(b);
+        }
+    }
+}
+
+// For "PROVIDE LOCAL INFORMATION" command.
+// See TS 31.111 section 6.4.15/ETSI TS 102 223
+// TS 31.124 section 27.22.4.15 for test spec
+class DTTZResponseData extends ResponseData {
+    private Calendar mCalendar;
+
+    public DTTZResponseData(Calendar cal) {
+        super();
+        mCalendar = cal;
+    }
+
+    @Override
+    public void format(ByteArrayOutputStream buf) {
+        if (buf == null) {
+            return;
+        }
+
+        // DTTZ object
+        int tag = 0x80 | CommandType.PROVIDE_LOCAL_INFORMATION.value();
+        buf.write(tag); // tag
+
+        byte[] data = new byte[8];
+
+        data[0] = 0x07; // Write length of DTTZ data
+
+        if (mCalendar == null) {
+            mCalendar = Calendar.getInstance();
+        }
+        // Fill year byte
+        data[1] = byteToBCD(mCalendar.get(java.util.Calendar.YEAR) % 100);
+
+        // Fill month byte
+        data[2] = byteToBCD(mCalendar.get(java.util.Calendar.MONTH) + 1);
+
+        // Fill day byte
+        data[3] = byteToBCD(mCalendar.get(java.util.Calendar.DATE));
+
+        // Fill hour byte
+        data[4] = byteToBCD(mCalendar.get(java.util.Calendar.HOUR_OF_DAY));
+
+        // Fill minute byte
+        data[5] = byteToBCD(mCalendar.get(java.util.Calendar.MINUTE));
+
+        // Fill second byte
+        data[6] = byteToBCD(mCalendar.get(java.util.Calendar.SECOND));
+
+        String tz = SystemProperties.get("persist.sys.timezone", "");
+        if (TextUtils.isEmpty(tz)) {
+            data[7] = (byte) 0xFF;    // set FF in terminal response
+        } else {
+            TimeZone zone = TimeZone.getTimeZone(tz);
+            int zoneOffset = zone.getRawOffset() + zone.getDSTSavings();
+            data[7] = getTZOffSetByte(zoneOffset);
+        }
+
+        for (byte b : data) {
+            buf.write(b);
+        }
+    }
+
+    private byte byteToBCD(int value) {
+        if (value < 0 && value > 99) {
+            CatLog.d(this, "Err: byteToBCD conversion Value is " + value +
+                           " Value has to be between 0 and 99");
+            return 0;
+        }
+
+        return (byte) ((value / 10) | ((value % 10) << 4));
+    }
+
+    private byte getTZOffSetByte(long offSetVal) {
+        boolean isNegative = (offSetVal < 0);
+
+        /*
+         * The 'offSetVal' is in milliseconds. Convert it to hours and compute
+         * offset While sending T.R to UICC, offset is expressed is 'quarters of
+         * hours'
+         */
+
+         long tzOffset = offSetVal / (15 * 60 * 1000);
+         tzOffset = (isNegative ? -1 : 1) * tzOffset;
+         byte bcdVal = byteToBCD((int) tzOffset);
+         // For negative offsets, put '1' in the msb
+         return isNegative ?  (bcdVal |= 0x08) : bcdVal;
+    }
+
+}
+
diff --git a/com/android/internal/telephony/cat/ResultCode.java b/com/android/internal/telephony/cat/ResultCode.java
new file mode 100644
index 0000000..f001c7e
--- /dev/null
+++ b/com/android/internal/telephony/cat/ResultCode.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Enumeration for the return code in TERMINAL RESPONSE.
+ * To get the actual return code for each enum value, call {@link #value}
+ * method.
+ *
+ * {@hide}
+ */
+public enum ResultCode {
+
+    /*
+     * Results '0X' and '1X' indicate that the command has been performed.
+     */
+
+    /** Command performed successfully */
+    OK(0x00),
+
+    /** Command performed with partial comprehension */
+    PRFRMD_WITH_PARTIAL_COMPREHENSION(0x01),
+
+    /** Command performed, with missing information */
+    PRFRMD_WITH_MISSING_INFO(0x02),
+
+    /** REFRESH performed with additional EFs read */
+    PRFRMD_WITH_ADDITIONAL_EFS_READ(0x03),
+
+    /**
+     * Command performed successfully, but requested icon could not be
+     * displayed
+     */
+    PRFRMD_ICON_NOT_DISPLAYED(0x04),
+
+    /** Command performed, but modified by call control by NAA */
+    PRFRMD_MODIFIED_BY_NAA(0x05),
+
+    /** Command performed successfully, limited service */
+    PRFRMD_LIMITED_SERVICE(0x06),
+
+    /** Command performed with modification */
+    PRFRMD_WITH_MODIFICATION(0x07),
+
+    /** REFRESH performed but indicated NAA was not active */
+    PRFRMD_NAA_NOT_ACTIVE(0x08),
+
+    /** Command performed successfully, tone not played */
+    PRFRMD_TONE_NOT_PLAYED(0x09),
+
+    /** Proactive UICC session terminated by the user */
+    UICC_SESSION_TERM_BY_USER(0x10),
+
+    /** Backward move in the proactive UICC session requested by the user */
+    BACKWARD_MOVE_BY_USER(0x11),
+
+    /** No response from user */
+    NO_RESPONSE_FROM_USER(0x12),
+
+    /** Help information required by the user */
+    HELP_INFO_REQUIRED(0x13),
+
+    /** USSD or SS transaction terminated by the user */
+    USSD_SS_SESSION_TERM_BY_USER(0x14),
+
+
+    /*
+     * Results '2X' indicate to the UICC that it may be worth re-trying the
+     * command at a later opportunity.
+     */
+
+    /** Terminal currently unable to process command */
+    TERMINAL_CRNTLY_UNABLE_TO_PROCESS(0x20),
+
+    /** Network currently unable to process command */
+    NETWORK_CRNTLY_UNABLE_TO_PROCESS(0x21),
+
+    /** User did not accept the proactive command */
+    USER_NOT_ACCEPT(0x22),
+
+    /** User cleared down call before connection or network release */
+    USER_CLEAR_DOWN_CALL(0x23),
+
+    /** Action in contradiction with the current timer state */
+    CONTRADICTION_WITH_TIMER(0x24),
+
+    /** Interaction with call control by NAA, temporary problem */
+    NAA_CALL_CONTROL_TEMPORARY(0x25),
+
+    /** Launch browser generic error code */
+    LAUNCH_BROWSER_ERROR(0x26),
+
+    /** MMS temporary problem. */
+    MMS_TEMPORARY(0x27),
+
+
+    /*
+     * Results '3X' indicate that it is not worth the UICC re-trying with an
+     * identical command, as it will only get the same response. However, the
+     * decision to retry lies with the application.
+     */
+
+    /** Command beyond terminal's capabilities */
+    BEYOND_TERMINAL_CAPABILITY(0x30),
+
+    /** Command type not understood by terminal */
+    CMD_TYPE_NOT_UNDERSTOOD(0x31),
+
+    /** Command data not understood by terminal */
+    CMD_DATA_NOT_UNDERSTOOD(0x32),
+
+    /** Command number not known by terminal */
+    CMD_NUM_NOT_KNOWN(0x33),
+
+    /** SS Return Error */
+    SS_RETURN_ERROR(0x34),
+
+    /** SMS RP-ERROR */
+    SMS_RP_ERROR(0x35),
+
+    /** Error, required values are missing */
+    REQUIRED_VALUES_MISSING(0x36),
+
+    /** USSD Return Error */
+    USSD_RETURN_ERROR(0x37),
+
+    /** MultipleCard commands error */
+    MULTI_CARDS_CMD_ERROR(0x38),
+
+    /**
+     * Interaction with call control by USIM or MO short message control by
+     * USIM, permanent problem
+     */
+    USIM_CALL_CONTROL_PERMANENT(0x39),
+
+    /** Bearer Independent Protocol error */
+    BIP_ERROR(0x3a),
+
+    /** Access Technology unable to process command */
+    ACCESS_TECH_UNABLE_TO_PROCESS(0x3b),
+
+    /** Frames error */
+    FRAMES_ERROR(0x3c),
+
+    /** MMS Error */
+    MMS_ERROR(0x3d);
+
+
+    private int mCode;
+
+    ResultCode(int code) {
+        mCode = code;
+    }
+
+    /**
+     * Retrieves the actual result code that this object represents.
+     * @return Actual result code
+     */
+    public int value() {
+        return mCode;
+    }
+
+    public static ResultCode fromInt(int value) {
+        for (ResultCode r : ResultCode.values()) {
+            if (r.mCode == value) {
+                return r;
+            }
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/ResultException.java b/com/android/internal/telephony/cat/ResultException.java
new file mode 100644
index 0000000..5175b83
--- /dev/null
+++ b/com/android/internal/telephony/cat/ResultException.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Class for errors in the Result object.
+ *
+ * {@hide}
+ */
+public class ResultException extends CatException {
+    private ResultCode mResult;
+    private int mAdditionalInfo;
+    private String mExplanation;
+
+    public ResultException(ResultCode result) {
+        super();
+
+        // ETSI TS 102 223, 8.12 -- For the general results '20', '21', '26',
+        // '38', '39', '3A', '3C', and '3D', it is mandatory for the terminal
+        // to provide a specific cause value as additional information.
+        switch (result) {
+            case TERMINAL_CRNTLY_UNABLE_TO_PROCESS:    // 0x20
+            case NETWORK_CRNTLY_UNABLE_TO_PROCESS:     // 0x21
+            case LAUNCH_BROWSER_ERROR:                 // 0x26
+            case MULTI_CARDS_CMD_ERROR:                // 0x38
+            case USIM_CALL_CONTROL_PERMANENT:          // 0x39
+            case BIP_ERROR:                            // 0x3a
+            case FRAMES_ERROR:                         // 0x3c
+            case MMS_ERROR:                            // 0x3d
+                throw new AssertionError(
+                        "For result code, " + result +
+                        ", additional information must be given!");
+            default:
+                break;
+        }
+
+        mResult = result;
+        mAdditionalInfo = -1;
+        mExplanation = "";
+    }
+
+    public ResultException(ResultCode result, String explanation) {
+        this(result);
+        mExplanation = explanation;
+    }
+
+    public ResultException(ResultCode result, int additionalInfo) {
+        this(result);
+
+        if (additionalInfo < 0) {
+            throw new AssertionError(
+                    "Additional info must be greater than zero!");
+        }
+
+        mAdditionalInfo = additionalInfo;
+    }
+
+    public ResultException(ResultCode result, int additionalInfo, String explanation) {
+        this(result, additionalInfo);
+        mExplanation = explanation;
+    }
+
+    public ResultCode result() {
+        return mResult;
+    }
+
+    public boolean hasAdditionalInfo() {
+        return mAdditionalInfo >= 0;
+    }
+
+    public int additionalInfo() {
+        return mAdditionalInfo;
+    }
+
+    public String explanation() {
+        return mExplanation;
+    }
+
+    @Override
+    public String toString() {
+        return "result=" + mResult + " additionalInfo=" + mAdditionalInfo +
+                " explantion=" + mExplanation;
+    }
+}
diff --git a/com/android/internal/telephony/cat/RilMessageDecoder.java b/com/android/internal/telephony/cat/RilMessageDecoder.java
new file mode 100644
index 0000000..65b958c
--- /dev/null
+++ b/com/android/internal/telephony/cat/RilMessageDecoder.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+/**
+ * Class used for queuing raw ril messages, decoding them into CommanParams
+ * objects and sending the result back to the CAT Service.
+ */
+class RilMessageDecoder extends StateMachine {
+
+    // constants
+    private static final int CMD_START = 1;
+    private static final int CMD_PARAMS_READY = 2;
+
+    // members
+    private CommandParamsFactory mCmdParamsFactory = null;
+    private RilMessage mCurrentRilMessage = null;
+    private Handler mCaller = null;
+    private static int mSimCount = 0;
+    private static RilMessageDecoder[] mInstance = null;
+
+    // States
+    private StateStart mStateStart = new StateStart();
+    private StateCmdParamsReady mStateCmdParamsReady = new StateCmdParamsReady();
+
+    /**
+     * Get the singleton instance, constructing if necessary.
+     *
+     * @param caller
+     * @param fh
+     * @return RilMesssageDecoder
+     */
+    public static synchronized RilMessageDecoder getInstance(Handler caller, IccFileHandler fh,
+            int slotId) {
+        if (null == mInstance) {
+            mSimCount = TelephonyManager.getDefault().getSimCount();
+            mInstance = new RilMessageDecoder[mSimCount];
+            for (int i = 0; i < mSimCount; i++) {
+                mInstance[i] = null;
+            }
+        }
+
+        if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mSimCount) {
+            if (null == mInstance[slotId]) {
+                mInstance[slotId] = new RilMessageDecoder(caller, fh);
+            }
+        } else {
+            CatLog.d("RilMessageDecoder", "invaild slot id: " + slotId);
+            return null;
+        }
+
+        return mInstance[slotId];
+    }
+
+    /**
+     * Start decoding the message parameters,
+     * when complete MSG_ID_RIL_MSG_DECODED will be returned to caller.
+     *
+     * @param rilMsg
+     */
+    public void sendStartDecodingMessageParams(RilMessage rilMsg) {
+        Message msg = obtainMessage(CMD_START);
+        msg.obj = rilMsg;
+        sendMessage(msg);
+    }
+
+    /**
+     * The command parameters have been decoded.
+     *
+     * @param resCode
+     * @param cmdParams
+     */
+    public void sendMsgParamsDecoded(ResultCode resCode, CommandParams cmdParams) {
+        Message msg = obtainMessage(RilMessageDecoder.CMD_PARAMS_READY);
+        msg.arg1 = resCode.value();
+        msg.obj = cmdParams;
+        sendMessage(msg);
+    }
+
+    private void sendCmdForExecution(RilMessage rilMsg) {
+        Message msg = mCaller.obtainMessage(CatService.MSG_ID_RIL_MSG_DECODED,
+                new RilMessage(rilMsg));
+        msg.sendToTarget();
+    }
+
+    private RilMessageDecoder(Handler caller, IccFileHandler fh) {
+        super("RilMessageDecoder");
+
+        addState(mStateStart);
+        addState(mStateCmdParamsReady);
+        setInitialState(mStateStart);
+
+        mCaller = caller;
+        mCmdParamsFactory = CommandParamsFactory.getInstance(this, fh);
+    }
+
+    private RilMessageDecoder() {
+        super("RilMessageDecoder");
+    }
+
+    private class StateStart extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            if (msg.what == CMD_START) {
+                if (decodeMessageParams((RilMessage)msg.obj)) {
+                    transitionTo(mStateCmdParamsReady);
+                }
+            } else {
+                CatLog.d(this, "StateStart unexpected expecting START=" +
+                         CMD_START + " got " + msg.what);
+            }
+            return true;
+        }
+    }
+
+    private class StateCmdParamsReady extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            if (msg.what == CMD_PARAMS_READY) {
+                mCurrentRilMessage.mResCode = ResultCode.fromInt(msg.arg1);
+                mCurrentRilMessage.mData = msg.obj;
+                sendCmdForExecution(mCurrentRilMessage);
+                transitionTo(mStateStart);
+            } else {
+                CatLog.d(this, "StateCmdParamsReady expecting CMD_PARAMS_READY="
+                         + CMD_PARAMS_READY + " got " + msg.what);
+                deferMessage(msg);
+            }
+            return true;
+        }
+    }
+
+    private boolean decodeMessageParams(RilMessage rilMsg) {
+        boolean decodingStarted;
+
+        mCurrentRilMessage = rilMsg;
+        switch(rilMsg.mId) {
+        case CatService.MSG_ID_SESSION_END:
+        case CatService.MSG_ID_CALL_SETUP:
+            mCurrentRilMessage.mResCode = ResultCode.OK;
+            sendCmdForExecution(mCurrentRilMessage);
+            decodingStarted = false;
+            break;
+        case CatService.MSG_ID_PROACTIVE_COMMAND:
+        case CatService.MSG_ID_EVENT_NOTIFY:
+        case CatService.MSG_ID_REFRESH:
+            byte[] rawData = null;
+            try {
+                rawData = IccUtils.hexStringToBytes((String) rilMsg.mData);
+            } catch (Exception e) {
+                // zombie messages are dropped
+                CatLog.d(this, "decodeMessageParams dropping zombie messages");
+                decodingStarted = false;
+                break;
+            }
+            try {
+                // Start asynch parsing of the command parameters.
+                mCmdParamsFactory.make(BerTlv.decode(rawData));
+                decodingStarted = true;
+            } catch (ResultException e) {
+                // send to Service for proper RIL communication.
+                CatLog.d(this, "decodeMessageParams: caught ResultException e=" + e);
+                mCurrentRilMessage.mResCode = e.result();
+                sendCmdForExecution(mCurrentRilMessage);
+                decodingStarted = false;
+            }
+            break;
+        default:
+            decodingStarted = false;
+            break;
+        }
+        return decodingStarted;
+    }
+
+    public void dispose() {
+        quitNow();
+        mStateStart = null;
+        mStateCmdParamsReady = null;
+        mCmdParamsFactory.dispose();
+        mCmdParamsFactory = null;
+        mCurrentRilMessage = null;
+        mCaller = null;
+        mInstance = null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/TextAlignment.java b/com/android/internal/telephony/cat/TextAlignment.java
new file mode 100644
index 0000000..7fb58a5
--- /dev/null
+++ b/com/android/internal/telephony/cat/TextAlignment.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Enumeration for representing text alignment.
+ *
+ * {@hide}
+ */
+public enum TextAlignment {
+    LEFT(0x0),
+    CENTER(0x1),
+    RIGHT(0x2),
+    /** Language dependent (default) */
+    DEFAULT(0x3);
+
+    private int mValue;
+
+    TextAlignment(int value) {
+        mValue = value;
+    }
+
+    /**
+     * Create a TextAlignment object.
+     * @param value Integer value to be converted to a TextAlignment object.
+     * @return TextAlignment object whose value is {@code value}. If no
+     *         TextAlignment object has that value, null is returned.
+     */
+    public static TextAlignment fromInt(int value) {
+        for (TextAlignment e : TextAlignment.values()) {
+            if (e.mValue == value) {
+                return e;
+            }
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/TextAttribute.java b/com/android/internal/telephony/cat/TextAttribute.java
new file mode 100644
index 0000000..0dea640
--- /dev/null
+++ b/com/android/internal/telephony/cat/TextAttribute.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Class for representing text attributes for SIM Toolkit.
+ *
+ * {@hide}
+ */
+public class TextAttribute {
+    public int start;
+    public int length;
+    public TextAlignment align;
+    public FontSize size;
+    public boolean bold;
+    public boolean italic;
+    public boolean underlined;
+    public boolean strikeThrough;
+    public TextColor color;
+
+    public TextAttribute(int start, int length, TextAlignment align,
+            FontSize size, boolean bold, boolean italic, boolean underlined,
+            boolean strikeThrough, TextColor color) {
+        this.start = start;
+        this.length = length;
+        this.align = align;
+        this.size = size;
+        this.bold = bold;
+        this.italic = italic;
+        this.underlined = underlined;
+        this.strikeThrough = strikeThrough;
+        this.color = color;
+    }
+}
diff --git a/com/android/internal/telephony/cat/TextColor.java b/com/android/internal/telephony/cat/TextColor.java
new file mode 100644
index 0000000..6447e74
--- /dev/null
+++ b/com/android/internal/telephony/cat/TextColor.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+
+/**
+ * Enumeration for representing text color.
+ *
+ * {@hide}
+ */
+public enum TextColor {
+    BLACK(0x0),
+    DARK_GRAY(0x1),
+    DARK_RED(0x2),
+    DARK_YELLOW(0x3),
+    DARK_GREEN(0x4),
+    DARK_CYAN(0x5),
+    DARK_BLUE(0x6),
+    DARK_MAGENTA(0x7),
+    GRAY(0x8),
+    WHITE(0x9),
+    BRIGHT_RED(0xa),
+    BRIGHT_YELLOW(0xb),
+    BRIGHT_GREEN(0xc),
+    BRIGHT_CYAN(0xd),
+    BRIGHT_BLUE(0xe),
+    BRIGHT_MAGENTA(0xf);
+
+    private int mValue;
+
+    TextColor(int value) {
+        mValue = value;
+    }
+
+    /**
+     * Create a TextColor object.
+     * @param value Integer value to be converted to a TextColor object.
+     * @return TextColor object whose value is {@code value}. If no TextColor
+     *         object has that value, null is returned.
+     */
+    public static TextColor fromInt(int value) {
+        for (TextColor e : TextColor.values()) {
+            if (e.mValue == value) {
+                return e;
+            }
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cat/TextMessage.java b/com/android/internal/telephony/cat/TextMessage.java
new file mode 100644
index 0000000..926c927
--- /dev/null
+++ b/com/android/internal/telephony/cat/TextMessage.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class TextMessage implements Parcelable {
+    public String title = "";
+    public String text = null;
+    public Bitmap icon = null;
+    public boolean iconSelfExplanatory = false;
+    public boolean isHighPriority = false;
+    public boolean responseNeeded = true;
+    public boolean userClear = false;
+    public Duration duration = null;
+
+    TextMessage() {
+    }
+
+    private TextMessage(Parcel in) {
+        title = in.readString();
+        text = in.readString();
+        icon = in.readParcelable(null);
+        iconSelfExplanatory = in.readInt() == 1 ? true : false;
+        isHighPriority = in.readInt() == 1 ? true : false;
+        responseNeeded = in.readInt() == 1 ? true : false;
+        userClear = in.readInt() == 1 ? true : false;
+        duration = in.readParcelable(null);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(title);
+        dest.writeString(text);
+        dest.writeParcelable(icon, 0);
+        dest.writeInt(iconSelfExplanatory ? 1 : 0);
+        dest.writeInt(isHighPriority ? 1 : 0);
+        dest.writeInt(responseNeeded ? 1 : 0);
+        dest.writeInt(userClear ? 1 : 0);
+        dest.writeParcelable(duration, 0);
+    }
+
+    public static final Parcelable.Creator<TextMessage> CREATOR = new Parcelable.Creator<TextMessage>() {
+        @Override
+        public TextMessage createFromParcel(Parcel in) {
+            return new TextMessage(in);
+        }
+
+        @Override
+        public TextMessage[] newArray(int size) {
+            return new TextMessage[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return "title=" + title + " text=" + text + " icon=" + icon +
+            " iconSelfExplanatory=" + iconSelfExplanatory + " isHighPriority=" +
+            isHighPriority + " responseNeeded=" + responseNeeded + " userClear=" +
+            userClear + " duration=" + duration;
+    }
+}
diff --git a/com/android/internal/telephony/cat/Tone.java b/com/android/internal/telephony/cat/Tone.java
new file mode 100644
index 0000000..eea7fed
--- /dev/null
+++ b/com/android/internal/telephony/cat/Tone.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cat;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Enumeration for representing the tone values for use with PLAY TONE
+ * proactive commands.
+ *
+ * {@hide}
+ */
+public enum Tone implements Parcelable {
+    // Standard supervisory tones
+
+    /**
+     * Dial tone.
+     */
+    DIAL(0x01),
+
+    /**
+     * Called subscriber busy.
+     */
+    BUSY(0x02),
+
+    /**
+     * Congestion.
+     */
+    CONGESTION(0x03),
+
+    /**
+     * Radio path acknowledge.
+     */
+    RADIO_PATH_ACK(0x04),
+
+    /**
+     * Radio path not available / Call dropped.
+     */
+    RADIO_PATH_NOT_AVAILABLE(0x05),
+
+    /**
+     * Error/Special information.
+     */
+    ERROR_SPECIAL_INFO(0x06),
+
+    /**
+     * Call waiting tone.
+     */
+    CALL_WAITING(0x07),
+
+    /**
+     * Ringing tone.
+     */
+    RINGING(0x08),
+
+    // Terminal proprietary tones
+
+    /**
+     * General beep.
+     */
+    GENERAL_BEEP(0x10),
+
+    /**
+     * Positive acknowledgement tone.
+     */
+    POSITIVE_ACK(0x11),
+
+    /**
+     * Negative acknowledgement tone.
+     */
+    NEGATIVE_ACK(0x12),
+
+    /**
+     * Ringing tone as selected by the user for incoming speech call.
+     */
+    INCOMING_SPEECH_CALL(0x13),
+
+    /**
+     * Alert tone as selected by the user for incoming SMS.
+     */
+    INCOMING_SMS(0x14),
+
+    /**
+     * Critical alert.
+     * This tone is to be used in critical situations. The terminal shall make
+     * every effort to alert the user when this tone is indicated independent
+     * from the volume setting in the terminal.
+     */
+    CRITICAL_ALERT(0x15),
+
+    /**
+     * Vibrate only, if available.
+     */
+    VIBRATE_ONLY(0x20),
+
+    // Themed tones
+
+    /**
+     * Happy tone.
+     */
+    HAPPY(0x30),
+
+    /**
+     * Sad tone.
+     */
+    SAD(0x31),
+
+    /**
+     * Urgent action tone.
+     */
+    URGENT(0x32),
+
+    /**
+     * Question tone.
+     */
+    QUESTION(0x33),
+
+    /**
+     * Message received tone.
+     */
+    MESSAGE_RECEIVED(0x34),
+
+    // Melody tones
+    MELODY_1(0x40),
+    MELODY_2(0x41),
+    MELODY_3(0x42),
+    MELODY_4(0x43),
+    MELODY_5(0x44),
+    MELODY_6(0x45),
+    MELODY_7(0x46),
+    MELODY_8(0x47);
+
+    private int mValue;
+
+    Tone(int value) {
+        mValue = value;
+    }
+
+    /**
+     * Create a Tone object.
+     * @param value Integer value to be converted to a Tone object.
+     * @return Tone object whose value is {@code value}. If no Tone object has
+     *         that value, null is returned.
+     */
+    public static Tone fromInt(int value) {
+        for (Tone e : Tone.values()) {
+            if (e.mValue == value) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    Tone(Parcel in) {
+        mValue = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(ordinal());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<Tone> CREATOR = new Parcelable.Creator<Tone>() {
+        @Override
+        public Tone createFromParcel(Parcel in) {
+            return Tone.values()[in.readInt()];
+        }
+
+        @Override
+        public Tone[] newArray(int size) {
+            return new Tone[size];
+        }
+    };
+}
diff --git a/com/android/internal/telephony/cat/ToneSettings.java b/com/android/internal/telephony/cat/ToneSettings.java
new file mode 100644
index 0000000..61c1573
--- /dev/null
+++ b/com/android/internal/telephony/cat/ToneSettings.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.cat;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Container class for PlayTone commands parameters.
+ *
+ */
+public class ToneSettings implements Parcelable {
+    public Duration duration;
+    public Tone tone;
+    public boolean vibrate;
+
+    public ToneSettings(Duration duration, Tone tone, boolean vibrate) {
+        this.duration = duration;
+        this.tone = tone;
+        this.vibrate = vibrate;
+    }
+
+    private ToneSettings(Parcel in) {
+        duration = in.readParcelable(null);
+        tone = in.readParcelable(null);
+        vibrate = in.readInt() == 1;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(duration, 0);
+        dest.writeParcelable(tone, 0);
+        dest.writeInt(vibrate ? 1 : 0);
+    }
+
+    public static final Parcelable.Creator<ToneSettings> CREATOR = new Parcelable.Creator<ToneSettings>() {
+        @Override
+        public ToneSettings createFromParcel(Parcel in) {
+            return new ToneSettings(in);
+        }
+
+        @Override
+        public ToneSettings[] newArray(int size) {
+            return new ToneSettings[size];
+        }
+    };
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/cat/ValueParser.java b/com/android/internal/telephony/cat/ValueParser.java
new file mode 100644
index 0000000..91a6fd6
--- /dev/null
+++ b/com/android/internal/telephony/cat/ValueParser.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2006-2007 Google Inc.
+ *
+ * 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.internal.telephony.cat;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.cat.Duration.TimeUnit;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class ValueParser {
+
+    /**
+     * Search for a Command Details object from a list.
+     *
+     * @param ctlv List of ComprehensionTlv objects used for search
+     * @return An CtlvCommandDetails object found from the objects. If no
+     *         Command Details object is found, ResultException is thrown.
+     * @throws ResultException
+     */
+    static CommandDetails retrieveCommandDetails(ComprehensionTlv ctlv)
+            throws ResultException {
+
+        CommandDetails cmdDet = new CommandDetails();
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        try {
+            cmdDet.compRequired = ctlv.isComprehensionRequired();
+            cmdDet.commandNumber = rawValue[valueIndex] & 0xff;
+            cmdDet.typeOfCommand = rawValue[valueIndex + 1] & 0xff;
+            cmdDet.commandQualifier = rawValue[valueIndex + 2] & 0xff;
+            return cmdDet;
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+    }
+
+    /**
+     * Search for a Device Identities object from a list.
+     *
+     * @param ctlv List of ComprehensionTlv objects used for search
+     * @return An CtlvDeviceIdentities object found from the objects. If no
+     *         Command Details object is found, ResultException is thrown.
+     * @throws ResultException
+     */
+    static DeviceIdentities retrieveDeviceIdentities(ComprehensionTlv ctlv)
+            throws ResultException {
+
+        DeviceIdentities devIds = new DeviceIdentities();
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        try {
+            devIds.sourceId = rawValue[valueIndex] & 0xff;
+            devIds.destinationId = rawValue[valueIndex + 1] & 0xff;
+            return devIds;
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING);
+        }
+    }
+
+    /**
+     * Retrieves Duration information from the Duration COMPREHENSION-TLV
+     * object.
+     *
+     * @param ctlv A Text Attribute COMPREHENSION-TLV object
+     * @return A Duration object
+     * @throws ResultException
+     */
+    static Duration retrieveDuration(ComprehensionTlv ctlv) throws ResultException {
+        int timeInterval = 0;
+        TimeUnit timeUnit = TimeUnit.SECOND;
+
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+
+        try {
+            timeUnit = TimeUnit.values()[(rawValue[valueIndex] & 0xff)];
+            timeInterval = rawValue[valueIndex + 1] & 0xff;
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+        return new Duration(timeInterval, timeUnit);
+    }
+
+    /**
+     * Retrieves Item information from the COMPREHENSION-TLV object.
+     *
+     * @param ctlv A Text Attribute COMPREHENSION-TLV object
+     * @return An Item
+     * @throws ResultException
+     */
+    static Item retrieveItem(ComprehensionTlv ctlv) throws ResultException {
+        Item item = null;
+
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        int length = ctlv.getLength();
+
+        if (length != 0) {
+            int textLen = length - 1;
+
+            try {
+                int id = rawValue[valueIndex] & 0xff;
+                String text = IccUtils.adnStringFieldToString(rawValue,
+                        valueIndex + 1, textLen);
+                item = new Item(id, text);
+            } catch (IndexOutOfBoundsException e) {
+                throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+            }
+        }
+
+        return item;
+    }
+
+    /**
+     * Retrieves Item id information from the COMPREHENSION-TLV object.
+     *
+     * @param ctlv A Text Attribute COMPREHENSION-TLV object
+     * @return An Item id
+     * @throws ResultException
+     */
+    static int retrieveItemId(ComprehensionTlv ctlv) throws ResultException {
+        int id = 0;
+
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+
+        try {
+            id = rawValue[valueIndex] & 0xff;
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+
+        return id;
+    }
+
+    /**
+     * Retrieves icon id from an Icon Identifier COMPREHENSION-TLV object
+     *
+     * @param ctlv An Icon Identifier COMPREHENSION-TLV object
+     * @return IconId instance
+     * @throws ResultException
+     */
+    static IconId retrieveIconId(ComprehensionTlv ctlv) throws ResultException {
+        IconId id = new IconId();
+
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        try {
+            id.selfExplanatory = (rawValue[valueIndex++] & 0xff) == 0x00;
+            id.recordNumber = rawValue[valueIndex] & 0xff;
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+
+        return id;
+    }
+
+    /**
+     * Retrieves item icons id from an Icon Identifier List COMPREHENSION-TLV
+     * object
+     *
+     * @param ctlv An Item Icon List Identifier COMPREHENSION-TLV object
+     * @return ItemsIconId instance
+     * @throws ResultException
+     */
+    static ItemsIconId retrieveItemsIconId(ComprehensionTlv ctlv)
+            throws ResultException {
+        CatLog.d("ValueParser", "retrieveItemsIconId:");
+        ItemsIconId id = new ItemsIconId();
+
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        int numOfItems = ctlv.getLength() - 1;
+        id.recordNumbers = new int[numOfItems];
+
+        try {
+            // get icon self-explanatory
+            id.selfExplanatory = (rawValue[valueIndex++] & 0xff) == 0x00;
+
+            for (int index = 0; index < numOfItems;) {
+                id.recordNumbers[index++] = rawValue[valueIndex++];
+            }
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+        return id;
+    }
+
+    /**
+     * Retrieves text attribute information from the Text Attribute
+     * COMPREHENSION-TLV object.
+     *
+     * @param ctlv A Text Attribute COMPREHENSION-TLV object
+     * @return A list of TextAttribute objects
+     * @throws ResultException
+     */
+    static List<TextAttribute> retrieveTextAttribute(ComprehensionTlv ctlv)
+            throws ResultException {
+        ArrayList<TextAttribute> lst = new ArrayList<TextAttribute>();
+
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        int length = ctlv.getLength();
+
+        if (length != 0) {
+            // Each attribute is consisted of four bytes
+            int itemCount = length / 4;
+
+            try {
+                for (int i = 0; i < itemCount; i++, valueIndex += 4) {
+                    int start = rawValue[valueIndex] & 0xff;
+                    int textLength = rawValue[valueIndex + 1] & 0xff;
+                    int format = rawValue[valueIndex + 2] & 0xff;
+                    int colorValue = rawValue[valueIndex + 3] & 0xff;
+
+                    int alignValue = format & 0x03;
+                    TextAlignment align = TextAlignment.fromInt(alignValue);
+
+                    int sizeValue = (format >> 2) & 0x03;
+                    FontSize size = FontSize.fromInt(sizeValue);
+                    if (size == null) {
+                        // Font size value is not defined. Use default.
+                        size = FontSize.NORMAL;
+                    }
+
+                    boolean bold = (format & 0x10) != 0;
+                    boolean italic = (format & 0x20) != 0;
+                    boolean underlined = (format & 0x40) != 0;
+                    boolean strikeThrough = (format & 0x80) != 0;
+
+                    TextColor color = TextColor.fromInt(colorValue);
+
+                    TextAttribute attr = new TextAttribute(start, textLength,
+                            align, size, bold, italic, underlined,
+                            strikeThrough, color);
+                    lst.add(attr);
+                }
+
+                return lst;
+
+            } catch (IndexOutOfBoundsException e) {
+                throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * Retrieves alpha identifier from an Alpha Identifier COMPREHENSION-TLV
+     * object.
+     *
+     * @param ctlv An Alpha Identifier COMPREHENSION-TLV object
+     * @return String corresponding to the alpha identifier
+     * @throws ResultException
+     */
+    static String retrieveAlphaId(ComprehensionTlv ctlv) throws ResultException {
+
+        if (ctlv != null) {
+            byte[] rawValue = ctlv.getRawValue();
+            int valueIndex = ctlv.getValueIndex();
+            int length = ctlv.getLength();
+            if (length != 0) {
+                try {
+                    return IccUtils.adnStringFieldToString(rawValue, valueIndex,
+                            length);
+                } catch (IndexOutOfBoundsException e) {
+                    throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+                }
+            } else {
+                CatLog.d("ValueParser", "Alpha Id length=" + length);
+                return null;
+            }
+        } else {
+            /* Per 3GPP specification 102.223,
+             * if the alpha identifier is not provided by the UICC,
+             * the terminal MAY give information to the user
+             * noAlphaUsrCnf defines if you need to show user confirmation or not
+             */
+            boolean noAlphaUsrCnf = false;
+            Resources resource = Resources.getSystem();
+            try {
+                noAlphaUsrCnf = resource.getBoolean(
+                        com.android.internal.R.bool.config_stkNoAlphaUsrCnf);
+            } catch (NotFoundException e) {
+                noAlphaUsrCnf = false;
+            }
+            return (noAlphaUsrCnf ? null : CatService.STK_DEFAULT);
+        }
+    }
+
+    /**
+     * Retrieves text from the Text COMPREHENSION-TLV object, and decodes it
+     * into a Java String.
+     *
+     * @param ctlv A Text COMPREHENSION-TLV object
+     * @return A Java String object decoded from the Text object
+     * @throws ResultException
+     */
+    static String retrieveTextString(ComprehensionTlv ctlv) throws ResultException {
+        byte[] rawValue = ctlv.getRawValue();
+        int valueIndex = ctlv.getValueIndex();
+        byte codingScheme = 0x00;
+        String text = null;
+        int textLen = ctlv.getLength();
+
+        // In case the text length is 0, return a null string.
+        if (textLen == 0) {
+            return text;
+        } else {
+            // one byte is coding scheme
+            textLen -= 1;
+        }
+
+        try {
+            codingScheme = (byte) (rawValue[valueIndex] & 0x0c);
+
+            if (codingScheme == 0x00) { // GSM 7-bit packed
+                text = GsmAlphabet.gsm7BitPackedToString(rawValue,
+                        valueIndex + 1, (textLen * 8) / 7);
+            } else if (codingScheme == 0x04) { // GSM 8-bit unpacked
+                text = GsmAlphabet.gsm8BitUnpackedToString(rawValue,
+                        valueIndex + 1, textLen);
+            } else if (codingScheme == 0x08) { // UCS2
+                text = new String(rawValue, valueIndex + 1, textLen, "UTF-16");
+            } else {
+                throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+            }
+
+            return text;
+        } catch (IndexOutOfBoundsException e) {
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        } catch (UnsupportedEncodingException e) {
+            // This should never happen.
+            throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaCallWaitingNotification.java b/com/android/internal/telephony/cdma/CdmaCallWaitingNotification.java
new file mode 100644
index 0000000..81dec16
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaCallWaitingNotification.java
@@ -0,0 +1,69 @@
+/*
+ * 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.internal.telephony.cdma;
+
+import android.telephony.Rlog;
+import com.android.internal.telephony.PhoneConstants;
+
+/**
+ * Represents a Supplementary Service Notification received from the network.
+ *
+ * {@hide}
+ */
+public class CdmaCallWaitingNotification {
+    static final String LOG_TAG = "CdmaCallWaitingNotification";
+    public String number = null;
+    public int numberPresentation = 0;
+    public String name = null;
+    public int namePresentation = 0;
+    public int numberType = 0;
+    public int numberPlan = 0;
+    public int isPresent = 0;
+    public int signalType = 0;
+    public int alertPitch = 0;
+    public int signal = 0;
+
+    @Override
+    public String toString()
+    {
+        return super.toString() + "Call Waiting Notification  "
+            + " number: " + number
+            + " numberPresentation: " + numberPresentation
+            + " name: " + name
+            + " namePresentation: " + namePresentation
+            + " numberType: " + numberType
+            + " numberPlan: " + numberPlan
+            + " isPresent: " + isPresent
+            + " signalType: " + signalType
+            + " alertPitch: " + alertPitch
+            + " signal: " + signal ;
+    }
+
+    public static int
+    presentationFromCLIP(int cli)
+    {
+        switch(cli) {
+            case 0: return PhoneConstants.PRESENTATION_ALLOWED;
+            case 1: return PhoneConstants.PRESENTATION_RESTRICTED;
+            case 2: return PhoneConstants.PRESENTATION_UNKNOWN;
+            default:
+                // This shouldn't happen, just log an error and treat as Unknown
+                Rlog.d(LOG_TAG, "Unexpected presentation " + cli);
+                return PhoneConstants.PRESENTATION_UNKNOWN;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java b/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
new file mode 100644
index 0000000..e2c178a
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
@@ -0,0 +1,345 @@
+/*
+ * 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.internal.telephony.cdma;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Message;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.SmsCbMessage;
+
+import com.android.internal.telephony.CellBroadcastHandler;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.InboundSmsHandler;
+import com.android.internal.telephony.InboundSmsTracker;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.SmsStorageMonitor;
+import com.android.internal.telephony.TelephonyComponentFactory;
+import com.android.internal.telephony.WspTypeDecoder;
+import com.android.internal.telephony.cdma.sms.SmsEnvelope;
+import com.android.internal.util.HexDump;
+
+import java.util.Arrays;
+
+/**
+ * Subclass of {@link InboundSmsHandler} for 3GPP2 type messages.
+ */
+public class CdmaInboundSmsHandler extends InboundSmsHandler {
+
+    private final CdmaSMSDispatcher mSmsDispatcher;
+    private final CdmaServiceCategoryProgramHandler mServiceCategoryProgramHandler;
+
+    private byte[] mLastDispatchedSmsFingerprint;
+    private byte[] mLastAcknowledgedSmsFingerprint;
+
+    private final boolean mCheckForDuplicatePortsInOmadmWapPush = Resources.getSystem().getBoolean(
+            com.android.internal.R.bool.config_duplicate_port_omadm_wappush);
+
+    /**
+     * Create a new inbound SMS handler for CDMA.
+     */
+    private CdmaInboundSmsHandler(Context context, SmsStorageMonitor storageMonitor,
+            Phone phone, CdmaSMSDispatcher smsDispatcher) {
+        super("CdmaInboundSmsHandler", context, storageMonitor, phone,
+                CellBroadcastHandler.makeCellBroadcastHandler(context, phone));
+        mSmsDispatcher = smsDispatcher;
+        mServiceCategoryProgramHandler = CdmaServiceCategoryProgramHandler.makeScpHandler(context,
+                phone.mCi);
+        phone.mCi.setOnNewCdmaSms(getHandler(), EVENT_NEW_SMS, null);
+    }
+
+    /**
+     * Unregister for CDMA SMS.
+     */
+    @Override
+    protected void onQuitting() {
+        mPhone.mCi.unSetOnNewCdmaSms(getHandler());
+        mCellBroadcastHandler.dispose();
+
+        if (DBG) log("unregistered for 3GPP2 SMS");
+        super.onQuitting();
+    }
+
+    /**
+     * Wait for state machine to enter startup state. We can't send any messages until then.
+     */
+    public static CdmaInboundSmsHandler makeInboundSmsHandler(Context context,
+            SmsStorageMonitor storageMonitor, Phone phone, CdmaSMSDispatcher smsDispatcher) {
+        CdmaInboundSmsHandler handler = new CdmaInboundSmsHandler(context, storageMonitor,
+                phone, smsDispatcher);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Return true if this handler is for 3GPP2 messages; false for 3GPP format.
+     * @return true (3GPP2)
+     */
+    @Override
+    protected boolean is3gpp2() {
+        return true;
+    }
+
+    /**
+     * Process Cell Broadcast, Voicemail Notification, and other 3GPP/3GPP2-specific messages.
+     * @param smsb the SmsMessageBase object from the RIL
+     * @return true if the message was handled here; false to continue processing
+     */
+    @Override
+    protected int dispatchMessageRadioSpecific(SmsMessageBase smsb) {
+        SmsMessage sms = (SmsMessage) smsb;
+        boolean isBroadcastType = (SmsEnvelope.MESSAGE_TYPE_BROADCAST == sms.getMessageType());
+
+        // Handle CMAS emergency broadcast messages.
+        if (isBroadcastType) {
+            log("Broadcast type message");
+            SmsCbMessage cbMessage = sms.parseBroadcastSms();
+            if (cbMessage != null) {
+                mCellBroadcastHandler.dispatchSmsMessage(cbMessage);
+            } else {
+                loge("error trying to parse broadcast SMS");
+            }
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        // Initialize fingerprint field, and see if we have a network duplicate SMS.
+        mLastDispatchedSmsFingerprint = sms.getIncomingSmsFingerprint();
+        if (mLastAcknowledgedSmsFingerprint != null &&
+                Arrays.equals(mLastDispatchedSmsFingerprint, mLastAcknowledgedSmsFingerprint)) {
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        // Decode BD stream and set sms variables.
+        sms.parseSms();
+        int teleService = sms.getTeleService();
+
+        switch (teleService) {
+            case SmsEnvelope.TELESERVICE_VMN:
+            case SmsEnvelope.TELESERVICE_MWI:
+                // handle voicemail indication
+                handleVoicemailTeleservice(sms);
+                return Intents.RESULT_SMS_HANDLED;
+
+            case SmsEnvelope.TELESERVICE_WMT:
+            case SmsEnvelope.TELESERVICE_WEMT:
+                if (sms.isStatusReportMessage()) {
+                    mSmsDispatcher.sendStatusReportMessage(sms);
+                    return Intents.RESULT_SMS_HANDLED;
+                }
+                break;
+
+            case SmsEnvelope.TELESERVICE_SCPT:
+                mServiceCategoryProgramHandler.dispatchSmsMessage(sms);
+                return Intents.RESULT_SMS_HANDLED;
+
+            case SmsEnvelope.TELESERVICE_WAP:
+                // handled below, after storage check
+                break;
+
+            default:
+                loge("unsupported teleservice 0x" + Integer.toHexString(teleService));
+                return Intents.RESULT_SMS_UNSUPPORTED;
+        }
+
+        if (!mStorageMonitor.isStorageAvailable() &&
+                sms.getMessageClass() != SmsConstants.MessageClass.CLASS_0) {
+            // It's a storable message and there's no storage available.  Bail.
+            // (See C.S0015-B v2.0 for a description of "Immediate Display"
+            // messages, which we represent as CLASS_0.)
+            return Intents.RESULT_SMS_OUT_OF_MEMORY;
+        }
+
+        if (SmsEnvelope.TELESERVICE_WAP == teleService) {
+            return processCdmaWapPdu(sms.getUserData(), sms.mMessageRef,
+                    sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
+                    sms.getTimestampMillis());
+        }
+
+        return dispatchNormalMessage(smsb);
+    }
+
+    /**
+     * Send an acknowledge message.
+     * @param success indicates that last message was successfully received.
+     * @param result result code indicating any error
+     * @param response callback message sent when operation completes.
+     */
+    @Override
+    protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) {
+        int causeCode = resultToCause(result);
+        mPhone.mCi.acknowledgeLastIncomingCdmaSms(success, causeCode, response);
+
+        if (causeCode == 0) {
+            mLastAcknowledgedSmsFingerprint = mLastDispatchedSmsFingerprint;
+        }
+        mLastDispatchedSmsFingerprint = null;
+    }
+
+    /**
+     * Called when the phone changes the default method updates mPhone
+     * mStorageMonitor and mCellBroadcastHandler.updatePhoneObject.
+     * Override if different or other behavior is desired.
+     *
+     * @param phone
+     */
+    @Override
+    protected void onUpdatePhoneObject(Phone phone) {
+        super.onUpdatePhoneObject(phone);
+        mCellBroadcastHandler.updatePhoneObject(phone);
+    }
+
+    /**
+     * Convert Android result code to CDMA SMS failure cause.
+     * @param rc the Android SMS intent result value
+     * @return 0 for success, or a CDMA SMS failure cause value
+     */
+    private static int resultToCause(int rc) {
+        switch (rc) {
+        case Activity.RESULT_OK:
+        case Intents.RESULT_SMS_HANDLED:
+            // Cause code is ignored on success.
+            return 0;
+        case Intents.RESULT_SMS_OUT_OF_MEMORY:
+            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE;
+        case Intents.RESULT_SMS_UNSUPPORTED:
+            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID;
+        case Intents.RESULT_SMS_GENERIC_ERROR:
+        default:
+            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_OTHER_TERMINAL_PROBLEM;
+        }
+    }
+
+    /**
+     * Handle {@link SmsEnvelope#TELESERVICE_VMN} and {@link SmsEnvelope#TELESERVICE_MWI}.
+     * @param sms the message to process
+     */
+    private void handleVoicemailTeleservice(SmsMessage sms) {
+        int voicemailCount = sms.getNumOfVoicemails();
+        if (DBG) log("Voicemail count=" + voicemailCount);
+
+        // range check
+        if (voicemailCount < 0) {
+            voicemailCount = -1;
+        } else if (voicemailCount > 99) {
+            // C.S0015-B v2, 4.5.12
+            // range: 0-99
+            voicemailCount = 99;
+        }
+        // update voice mail count in phone
+        mPhone.setVoiceMessageCount(voicemailCount);
+    }
+
+    /**
+     * Processes inbound messages that are in the WAP-WDP PDU format. See
+     * wap-259-wdp-20010614-a section 6.5 for details on the WAP-WDP PDU format.
+     * WDP segments are gathered until a datagram completes and gets dispatched.
+     *
+     * @param pdu The WAP-WDP PDU segment
+     * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or
+     *         {@link Activity#RESULT_OK} if the message has been broadcast
+     *         to applications
+     */
+    private int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address, String dispAddr,
+            long timestamp) {
+        int index = 0;
+
+        int msgType = (0xFF & pdu[index++]);
+        if (msgType != 0) {
+            log("Received a WAP SMS which is not WDP. Discard.");
+            return Intents.RESULT_SMS_HANDLED;
+        }
+        int totalSegments = (0xFF & pdu[index++]);   // >= 1
+        int segment = (0xFF & pdu[index++]);         // >= 0
+
+        if (segment >= totalSegments) {
+            loge("WDP bad segment #" + segment + " expecting 0-" + (totalSegments - 1));
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        // Only the first segment contains sourcePort and destination Port
+        int sourcePort = 0;
+        int destinationPort = 0;
+        if (segment == 0) {
+            //process WDP segment
+            sourcePort = (0xFF & pdu[index++]) << 8;
+            sourcePort |= 0xFF & pdu[index++];
+            destinationPort = (0xFF & pdu[index++]) << 8;
+            destinationPort |= 0xFF & pdu[index++];
+            // Some carriers incorrectly send duplicate port fields in omadm wap pushes.
+            // If configured, check for that here
+            if (mCheckForDuplicatePortsInOmadmWapPush) {
+                if (checkDuplicatePortOmadmWapPush(pdu, index)) {
+                    index = index + 4; // skip duplicate port fields
+                }
+            }
+        }
+
+        // Lookup all other related parts
+        log("Received WAP PDU. Type = " + msgType + ", originator = " + address
+                + ", src-port = " + sourcePort + ", dst-port = " + destinationPort
+                + ", ID = " + referenceNumber + ", segment# = " + segment + '/' + totalSegments);
+
+        // pass the user data portion of the PDU to the shared handler in SMSDispatcher
+        byte[] userData = new byte[pdu.length - index];
+        System.arraycopy(pdu, index, userData, 0, pdu.length - index);
+
+        InboundSmsTracker tracker = TelephonyComponentFactory.getInstance().makeInboundSmsTracker(
+                userData, timestamp, destinationPort, true, address, dispAddr, referenceNumber,
+                segment, totalSegments, true, HexDump.toHexString(userData));
+
+        // de-duping is done only for text messages
+        return addTrackerToRawTableAndSendMessage(tracker, false /* don't de-dup */);
+    }
+
+    /**
+     * Optional check to see if the received WapPush is an OMADM notification with erroneous
+     * extra port fields.
+     * - Some carriers make this mistake.
+     * ex: MSGTYPE-TotalSegments-CurrentSegment
+     *       -SourcePortDestPort-SourcePortDestPort-OMADM PDU
+     * @param origPdu The WAP-WDP PDU segment
+     * @param index Current Index while parsing the PDU.
+     * @return True if OrigPdu is OmaDM Push Message which has duplicate ports.
+     *         False if OrigPdu is NOT OmaDM Push Message which has duplicate ports.
+     */
+    private static boolean checkDuplicatePortOmadmWapPush(byte[] origPdu, int index) {
+        index += 4;
+        byte[] omaPdu = new byte[origPdu.length - index];
+        System.arraycopy(origPdu, index, omaPdu, 0, omaPdu.length);
+
+        WspTypeDecoder pduDecoder = new WspTypeDecoder(omaPdu);
+        int wspIndex = 2;
+
+        // Process header length field
+        if (!pduDecoder.decodeUintvarInteger(wspIndex)) {
+            return false;
+        }
+
+        wspIndex += pduDecoder.getDecodedDataLength();  // advance to next field
+
+        // Process content type field
+        if (!pduDecoder.decodeContentType(wspIndex)) {
+            return false;
+        }
+
+        String mimeType = pduDecoder.getValueString();
+        return (WspTypeDecoder.CONTENT_TYPE_B_PUSH_SYNCML_NOTI.equals(mimeType));
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaInformationRecords.java b/com/android/internal/telephony/cdma/CdmaInformationRecords.java
new file mode 100644
index 0000000..4987e51
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaInformationRecords.java
@@ -0,0 +1,291 @@
+/*
+ * 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.internal.telephony.cdma;
+
+import android.os.Parcel;
+
+public final class CdmaInformationRecords {
+    public Object record;
+
+    /**
+     * Record type identifier
+     */
+    public static final int RIL_CDMA_DISPLAY_INFO_REC = 0;
+    public static final int RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC = 1;
+    public static final int RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC = 2;
+    public static final int RIL_CDMA_CONNECTED_NUMBER_INFO_REC = 3;
+    public static final int RIL_CDMA_SIGNAL_INFO_REC = 4;
+    public static final int RIL_CDMA_REDIRECTING_NUMBER_INFO_REC = 5;
+    public static final int RIL_CDMA_LINE_CONTROL_INFO_REC = 6;
+    public static final int RIL_CDMA_EXTENDED_DISPLAY_INFO_REC = 7;
+    public static final int RIL_CDMA_T53_CLIR_INFO_REC = 8;
+    public static final int RIL_CDMA_T53_RELEASE_INFO_REC = 9;
+    public static final int RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC = 10;
+
+    public CdmaInformationRecords(CdmaDisplayInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(CdmaNumberInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(CdmaSignalInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(CdmaRedirectingNumberInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(CdmaLineControlInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(CdmaT53ClirInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(CdmaT53AudioControlInfoRec obj) {
+        record = obj;
+    }
+
+    public CdmaInformationRecords(Parcel p) {
+        int id = p.readInt();
+        switch (id) {
+            case RIL_CDMA_DISPLAY_INFO_REC:
+            case RIL_CDMA_EXTENDED_DISPLAY_INFO_REC:
+                record  = new CdmaDisplayInfoRec(id, p.readString());
+                break;
+
+            case RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC:
+            case RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC:
+            case RIL_CDMA_CONNECTED_NUMBER_INFO_REC:
+                record = new CdmaNumberInfoRec(id, p.readString(), p.readInt(), p.readInt(),
+                        p.readInt(), p.readInt());
+                break;
+
+            case RIL_CDMA_SIGNAL_INFO_REC:
+                record = new CdmaSignalInfoRec(p.readInt(), p.readInt(), p.readInt(), p.readInt());
+                break;
+
+            case RIL_CDMA_REDIRECTING_NUMBER_INFO_REC:
+                record = new CdmaRedirectingNumberInfoRec(p.readString(), p.readInt(), p.readInt(),
+                        p.readInt(), p.readInt(), p.readInt());
+                break;
+
+            case RIL_CDMA_LINE_CONTROL_INFO_REC:
+                record = new CdmaLineControlInfoRec(p.readInt(), p.readInt(), p.readInt(),
+                        p.readInt());
+                break;
+
+            case RIL_CDMA_T53_CLIR_INFO_REC:
+                record = new CdmaT53ClirInfoRec(p.readInt());
+                break;
+
+            case RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC:
+                record = new CdmaT53AudioControlInfoRec(p.readInt(), p.readInt());
+                break;
+
+            case RIL_CDMA_T53_RELEASE_INFO_REC:
+                // TODO: WHAT to do, for now fall through and throw exception
+            default:
+                throw new RuntimeException("RIL_UNSOL_CDMA_INFO_REC: unsupported record. Got "
+                                            + CdmaInformationRecords.idToString(id) + " ");
+
+        }
+    }
+
+    public static String idToString(int id) {
+        switch(id) {
+        case RIL_CDMA_DISPLAY_INFO_REC: return "RIL_CDMA_DISPLAY_INFO_REC";
+        case RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC: return "RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC";
+        case RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC: return "RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC";
+        case RIL_CDMA_CONNECTED_NUMBER_INFO_REC: return "RIL_CDMA_CONNECTED_NUMBER_INFO_REC";
+        case RIL_CDMA_SIGNAL_INFO_REC: return "RIL_CDMA_SIGNAL_INFO_REC";
+        case RIL_CDMA_REDIRECTING_NUMBER_INFO_REC: return "RIL_CDMA_REDIRECTING_NUMBER_INFO_REC";
+        case RIL_CDMA_LINE_CONTROL_INFO_REC: return "RIL_CDMA_LINE_CONTROL_INFO_REC";
+        case RIL_CDMA_EXTENDED_DISPLAY_INFO_REC: return "RIL_CDMA_EXTENDED_DISPLAY_INFO_REC";
+        case RIL_CDMA_T53_CLIR_INFO_REC: return "RIL_CDMA_T53_CLIR_INFO_REC";
+        case RIL_CDMA_T53_RELEASE_INFO_REC: return "RIL_CDMA_T53_RELEASE_INFO_REC";
+        case RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC: return "RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC";
+        default: return "<unknown record>";
+        }
+    }
+
+    /**
+     * Signal Information record from 3GPP2 C.S005 3.7.5.5
+     */
+    public static class CdmaSignalInfoRec {
+        public boolean isPresent;   /* non-zero if signal information record is present */
+        public int signalType;
+        public int alertPitch;
+        public int signal;
+
+        public CdmaSignalInfoRec() {}
+
+        public CdmaSignalInfoRec(int isPresent, int signalType, int alertPitch, int signal) {
+            this.isPresent = isPresent != 0;
+            this.signalType = signalType;
+            this.alertPitch = alertPitch;
+            this.signal = signal;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaSignalInfo: {" +
+                    " isPresent: " + isPresent +
+                    ", signalType: " + signalType +
+                    ", alertPitch: " + alertPitch +
+                    ", signal: " + signal +
+                    " }";
+        }
+    }
+
+    public static class CdmaDisplayInfoRec {
+        public int id;
+        public String alpha;
+
+        public CdmaDisplayInfoRec(int id, String alpha) {
+            this.id = id;
+            this.alpha = alpha;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaDisplayInfoRec: {" +
+                    " id: " + CdmaInformationRecords.idToString(id) +
+                    ", alpha: " + alpha +
+                    " }";
+        }
+    }
+
+    public static class CdmaNumberInfoRec {
+        public int id;
+        public String number;
+        public byte numberType;
+        public byte numberPlan;
+        public byte pi;
+        public byte si;
+
+        public CdmaNumberInfoRec(int id, String number, int numberType, int numberPlan, int pi,
+                int si) {
+            this.number = number;
+            this.numberType = (byte)numberType;
+            this.numberPlan = (byte)numberPlan;
+            this.pi = (byte)pi;
+            this.si = (byte)si;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaNumberInfoRec: {" +
+                    " id: " + CdmaInformationRecords.idToString(id) +
+                    ", number: <MASKED>" +
+                    ", numberType: " + numberType +
+                    ", numberPlan: " + numberPlan +
+                    ", pi: " + pi +
+                    ", si: " + si +
+                    " }";
+        }
+    }
+
+    public static class CdmaRedirectingNumberInfoRec {
+        public static final int REASON_UNKNOWN = 0;
+        public static final int REASON_CALL_FORWARDING_BUSY = 1;
+        public static final int REASON_CALL_FORWARDING_NO_REPLY = 2;
+        public static final int REASON_CALLED_DTE_OUT_OF_ORDER = 9;
+        public static final int REASON_CALL_FORWARDING_BY_THE_CALLED_DTE = 10;
+        public static final int REASON_CALL_FORWARDING_UNCONDITIONAL = 15;
+
+        public CdmaNumberInfoRec numberInfoRec;
+        public int redirectingReason;
+
+        public CdmaRedirectingNumberInfoRec(String number, int numberType, int numberPlan,
+                int pi, int si, int reason) {
+            numberInfoRec = new CdmaNumberInfoRec(RIL_CDMA_REDIRECTING_NUMBER_INFO_REC,
+                                                  number, numberType, numberPlan, pi, si);
+            redirectingReason = reason;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaNumberInfoRec: {" +
+                    " numberInfoRec: " + numberInfoRec +
+                    ", redirectingReason: " + redirectingReason +
+                    " }";
+        }
+    }
+
+    public static class CdmaLineControlInfoRec {
+        public byte lineCtrlPolarityIncluded;
+        public byte lineCtrlToggle;
+        public byte lineCtrlReverse;
+        public byte lineCtrlPowerDenial;
+
+        public CdmaLineControlInfoRec(int lineCtrlPolarityIncluded, int lineCtrlToggle,
+                int lineCtrlReverse, int lineCtrlPowerDenial) {
+            this.lineCtrlPolarityIncluded = (byte)lineCtrlPolarityIncluded;
+            this.lineCtrlToggle = (byte)lineCtrlToggle;
+            this.lineCtrlReverse = (byte)lineCtrlReverse;
+            this.lineCtrlPowerDenial = (byte)lineCtrlPowerDenial;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaLineControlInfoRec: {" +
+                    " lineCtrlPolarityIncluded: " + lineCtrlPolarityIncluded +
+                    " lineCtrlToggle: " + lineCtrlToggle +
+                    " lineCtrlReverse: " + lineCtrlReverse +
+                    " lineCtrlPowerDenial: " + lineCtrlPowerDenial +
+                    " }";
+        }
+    }
+
+    public static class CdmaT53ClirInfoRec {
+        public byte cause;
+
+        public CdmaT53ClirInfoRec(int cause) {
+            this.cause = (byte)cause;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaT53ClirInfoRec: {" +
+                    " cause: " + cause +
+                    " }";
+        }
+    }
+
+    public static class CdmaT53AudioControlInfoRec {
+        public byte uplink;
+        public byte downlink;
+
+        public CdmaT53AudioControlInfoRec(int uplink, int downlink) {
+            this.uplink = (byte) uplink;
+            this.downlink = (byte) downlink;
+        }
+
+        @Override
+        public String toString() {
+            return "CdmaT53AudioControlInfoRec: {" +
+                    " uplink: " + uplink +
+                    " downlink: " + downlink +
+                    " }";
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaMmiCode.java b/com/android/internal/telephony/cdma/CdmaMmiCode.java
new file mode 100644
index 0000000..ab76f98
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaMmiCode.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.cdma;
+
+import android.content.Context;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.telephony.Rlog;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+/**
+ * This class can handle Puk code Mmi
+ *
+ * {@hide}
+ *
+ */
+public final class CdmaMmiCode  extends Handler implements MmiCode {
+    static final String LOG_TAG = "CdmaMmiCode";
+
+    // Constants
+
+    // From TS 22.030 6.5.2
+    static final String ACTION_REGISTER = "**";
+
+    // Supplementary Service codes for PIN/PIN2/PUK/PUK2 from TS 22.030 Annex B
+    static final String SC_PIN          = "04";
+    static final String SC_PIN2         = "042";
+    static final String SC_PUK          = "05";
+    static final String SC_PUK2         = "052";
+
+    // Event Constant
+
+    static final int EVENT_SET_COMPLETE = 1;
+
+    // Instance Variables
+
+    GsmCdmaPhone mPhone;
+    Context mContext;
+    UiccCardApplication mUiccApplication;
+
+    String mAction;              // ACTION_REGISTER
+    String mSc;                  // Service Code
+    String mSia, mSib, mSic;     // Service Info a,b,c
+    String mPoundString;         // Entire MMI string up to and including #
+    String mDialingNumber;
+    String mPwd;                 // For password registration
+
+    State mState = State.PENDING;
+    CharSequence mMessage;
+
+    // Class Variables
+
+    static Pattern sPatternSuppService = Pattern.compile(
+        "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)");
+/*       1  2                    3          4  5       6   7         8    9     10  11             12
+
+         1 = Full string up to and including #
+         2 = action
+         3 = service code
+         5 = SIA
+         7 = SIB
+         9 = SIC
+         10 = dialing number
+*/
+
+    static final int MATCH_GROUP_POUND_STRING = 1;
+    static final int MATCH_GROUP_ACTION = 2;
+    static final int MATCH_GROUP_SERVICE_CODE = 3;
+    static final int MATCH_GROUP_SIA = 5;
+    static final int MATCH_GROUP_SIB = 7;
+    static final int MATCH_GROUP_SIC = 9;
+    static final int MATCH_GROUP_PWD_CONFIRM = 11;
+    static final int MATCH_GROUP_DIALING_NUMBER = 12;
+
+
+    // Public Class methods
+
+    /**
+     * Check if provided string contains Mmi code in it and create corresponding
+     * Mmi if it does
+     */
+
+    public static CdmaMmiCode
+    newFromDialString(String dialString, GsmCdmaPhone phone, UiccCardApplication app) {
+        Matcher m;
+        CdmaMmiCode ret = null;
+
+        m = sPatternSuppService.matcher(dialString);
+
+        // Is this formatted like a standard supplementary service code?
+        if (m.matches()) {
+            ret = new CdmaMmiCode(phone,app);
+            ret.mPoundString = makeEmptyNull(m.group(MATCH_GROUP_POUND_STRING));
+            ret.mAction = makeEmptyNull(m.group(MATCH_GROUP_ACTION));
+            ret.mSc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE));
+            ret.mSia = makeEmptyNull(m.group(MATCH_GROUP_SIA));
+            ret.mSib = makeEmptyNull(m.group(MATCH_GROUP_SIB));
+            ret.mSic = makeEmptyNull(m.group(MATCH_GROUP_SIC));
+            ret.mPwd = makeEmptyNull(m.group(MATCH_GROUP_PWD_CONFIRM));
+            ret.mDialingNumber = makeEmptyNull(m.group(MATCH_GROUP_DIALING_NUMBER));
+
+        }
+
+        return ret;
+    }
+
+    // Private Class methods
+
+    /** make empty strings be null.
+     *  Regexp returns empty strings for empty groups
+     */
+    private static String
+    makeEmptyNull (String s) {
+        if (s != null && s.length() == 0) return null;
+
+        return s;
+    }
+
+    // Constructor
+
+    CdmaMmiCode (GsmCdmaPhone phone, UiccCardApplication app) {
+        super(phone.getHandler().getLooper());
+        mPhone = phone;
+        mContext = phone.getContext();
+        mUiccApplication = app;
+    }
+
+    // MmiCode implementation
+
+    @Override
+    public State
+    getState() {
+        return mState;
+    }
+
+    @Override
+    public CharSequence
+    getMessage() {
+        return mMessage;
+    }
+
+    public Phone
+    getPhone() {
+        return ((Phone) mPhone);
+    }
+
+    // inherited javadoc suffices
+    @Override
+    public void
+    cancel() {
+        // Complete or failed cannot be cancelled
+        if (mState == State.COMPLETE || mState == State.FAILED) {
+            return;
+        }
+
+        mState = State.CANCELLED;
+        mPhone.onMMIDone (this);
+    }
+
+    @Override
+    public boolean isCancelable() {
+        return false;
+    }
+
+    // Instance Methods
+
+    /**
+     * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related
+     */
+    public boolean isPinPukCommand() {
+        return mSc != null && (mSc.equals(SC_PIN) || mSc.equals(SC_PIN2)
+                              || mSc.equals(SC_PUK) || mSc.equals(SC_PUK2));
+    }
+
+    boolean isRegister() {
+        return mAction != null && mAction.equals(ACTION_REGISTER);
+    }
+
+    @Override
+    public boolean isUssdRequest() {
+        Rlog.w(LOG_TAG, "isUssdRequest is not implemented in CdmaMmiCode");
+        return false;
+    }
+
+    @Override
+    public String getDialString() {
+        return null;
+    }
+
+    /** Process a MMI PUK code */
+    public void
+    processCode() {
+        try {
+            if (isPinPukCommand()) {
+                // TODO: This is the same as the code in GsmMmiCode.java,
+                // MmiCode should be an abstract or base class and this and
+                // other common variables and code should be promoted.
+
+                // sia = old PIN or PUK
+                // sib = new PIN
+                // sic = new PIN
+                String oldPinOrPuk = mSia;
+                String newPinOrPuk = mSib;
+                int pinLen = newPinOrPuk.length();
+                if (isRegister()) {
+                    if (!newPinOrPuk.equals(mSic)) {
+                        // password mismatch; return error
+                        handlePasswordError(com.android.internal.R.string.mismatchPin);
+                    } else if (pinLen < 4 || pinLen > 8 ) {
+                        // invalid length
+                        handlePasswordError(com.android.internal.R.string.invalidPin);
+                    } else if (mSc.equals(SC_PIN)
+                            && mUiccApplication != null
+                            && mUiccApplication.getState() == AppState.APPSTATE_PUK) {
+                        // Sim is puk-locked
+                        handlePasswordError(com.android.internal.R.string.needPuk);
+                    } else if (mUiccApplication != null) {
+                        Rlog.d(LOG_TAG, "process mmi service code using UiccApp sc=" + mSc);
+
+                        // We have an app and the pre-checks are OK
+                        if (mSc.equals(SC_PIN)) {
+                            mUiccApplication.changeIccLockPassword(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else if (mSc.equals(SC_PIN2)) {
+                            mUiccApplication.changeIccFdnPassword(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else if (mSc.equals(SC_PUK)) {
+                            mUiccApplication.supplyPuk(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else if (mSc.equals(SC_PUK2)) {
+                            mUiccApplication.supplyPuk2(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else {
+                            throw new RuntimeException("Unsupported service code=" + mSc);
+                        }
+                    } else {
+                        throw new RuntimeException("No application mUiccApplicaiton is null");
+                    }
+                } else {
+                    throw new RuntimeException ("Ivalid register/action=" + mAction);
+                }
+            }
+        } catch (RuntimeException exc) {
+            mState = State.FAILED;
+            mMessage = mContext.getText(com.android.internal.R.string.mmiError);
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    private void handlePasswordError(int res) {
+        mState = State.FAILED;
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+        sb.append(mContext.getText(res));
+        mMessage = sb;
+        mPhone.onMMIDone(this);
+    }
+
+    @Override
+    public void
+    handleMessage (Message msg) {
+        AsyncResult ar;
+
+        if (msg.what == EVENT_SET_COMPLETE) {
+            ar = (AsyncResult) (msg.obj);
+            onSetComplete(msg, ar);
+        } else {
+            Rlog.e(LOG_TAG, "Unexpected reply");
+        }
+    }
+    // Private instance methods
+
+    private CharSequence getScString() {
+        if (mSc != null) {
+            if (isPinPukCommand()) {
+                return mContext.getText(com.android.internal.R.string.PinMmi);
+            }
+        }
+
+        return "";
+    }
+
+    private void
+    onSetComplete(Message msg, AsyncResult ar){
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+            if (ar.exception instanceof CommandException) {
+                CommandException.Error err = ((CommandException)(ar.exception)).getCommandError();
+                if (err == CommandException.Error.PASSWORD_INCORRECT) {
+                    if (isPinPukCommand()) {
+                        // look specifically for the PUK commands and adjust
+                        // the message accordingly.
+                        if (mSc.equals(SC_PUK) || mSc.equals(SC_PUK2)) {
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.badPuk));
+                        } else {
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.badPin));
+                        }
+                        // Get the No. of retries remaining to unlock PUK/PUK2
+                        int attemptsRemaining = msg.arg1;
+                        if (attemptsRemaining <= 0) {
+                            Rlog.d(LOG_TAG, "onSetComplete: PUK locked,"
+                                    + " cancel as lock screen will handle this");
+                            mState = State.CANCELLED;
+                        } else if (attemptsRemaining > 0) {
+                            Rlog.d(LOG_TAG, "onSetComplete: attemptsRemaining="+attemptsRemaining);
+                            sb.append(mContext.getResources().getQuantityString(
+                                    com.android.internal.R.plurals.pinpuk_attempts,
+                                    attemptsRemaining, attemptsRemaining));
+                        }
+                    } else {
+                        sb.append(mContext.getText(
+                                com.android.internal.R.string.passwordIncorrect));
+                    }
+                } else if (err == CommandException.Error.SIM_PUK2) {
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.badPin));
+                    sb.append("\n");
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.needPuk2));
+                } else if (err == CommandException.Error.REQUEST_NOT_SUPPORTED) {
+                    if (mSc.equals(SC_PIN)) {
+                        sb.append(mContext.getText(com.android.internal.R.string.enablePin));
+                    }
+                } else {
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.mmiError));
+                }
+            } else {
+                sb.append(mContext.getText(
+                        com.android.internal.R.string.mmiError));
+            }
+        } else if (isRegister()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceRegistered));
+        } else {
+            mState = State.FAILED;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.mmiError));
+        }
+
+        mMessage = sb;
+        mPhone.onMMIDone(this);
+    }
+
+    @Override
+    public ResultReceiver getUssdCallbackReceiver() {
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
new file mode 100644
index 0000000..1cfdc33
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.provider.Telephony.Sms;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.ImsSMSDispatcher;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.SMSDispatcher;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.cdma.sms.UserData;
+
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CdmaSMSDispatcher extends SMSDispatcher {
+    private static final String TAG = "CdmaSMSDispatcher";
+    private static final boolean VDBG = false;
+
+    public CdmaSMSDispatcher(Phone phone, SmsUsageMonitor usageMonitor,
+            ImsSMSDispatcher imsSMSDispatcher) {
+        super(phone, usageMonitor, imsSMSDispatcher);
+        Rlog.d(TAG, "CdmaSMSDispatcher created");
+    }
+
+    @Override
+    public String getFormat() {
+        return SmsConstants.FORMAT_3GPP2;
+    }
+
+    /**
+     * Send the SMS status report to the dispatcher thread to process.
+     * @param sms the CDMA SMS message containing the status report
+     */
+    public void sendStatusReportMessage(SmsMessage sms) {
+        if (VDBG) Rlog.d(TAG, "sending EVENT_HANDLE_STATUS_REPORT message");
+        sendMessage(obtainMessage(EVENT_HANDLE_STATUS_REPORT, sms));
+    }
+
+    @Override
+    protected void handleStatusReport(Object o) {
+        if (o instanceof SmsMessage) {
+            if (VDBG) Rlog.d(TAG, "calling handleCdmaStatusReport()");
+            handleCdmaStatusReport((SmsMessage) o);
+        } else {
+            Rlog.e(TAG, "handleStatusReport() called for object type " + o.getClass().getName());
+        }
+    }
+
+    /**
+     * Called from parent class to handle status report from {@code CdmaInboundSmsHandler}.
+     * @param sms the CDMA SMS message to process
+     */
+    private void handleCdmaStatusReport(SmsMessage sms) {
+        for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
+            SmsTracker tracker = deliveryPendingList.get(i);
+            if (tracker.mMessageRef == sms.mMessageRef) {
+                // Found it.  Remove from list and broadcast.
+                deliveryPendingList.remove(i);
+                // Update the message status (COMPLETE)
+                tracker.updateSentMessageStatus(mContext, Sms.STATUS_COMPLETE);
+
+                PendingIntent intent = tracker.mDeliveryIntent;
+                Intent fillIn = new Intent();
+                fillIn.putExtra("pdu", sms.getPdu());
+                fillIn.putExtra("format", getFormat());
+                try {
+                    intent.send(mContext, Activity.RESULT_OK, fillIn);
+                } catch (CanceledException ex) {}
+                break;  // Only expect to see one tracker matching this message.
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void sendData(String destAddr, String scAddr, int destPort,
+            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
+                scAddr, destAddr, destPort, data, (deliveryIntent != null));
+        if (pdu != null) {
+            HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
+            SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
+                    null /*messageUri*/, false /*isExpectMore*/, null /*fullMessageText*/,
+                    false /*isText*/, true /*persistMessage*/);
+
+            String carrierPackage = getCarrierAppPackageName();
+            if (carrierPackage != null) {
+                Rlog.d(TAG, "Found carrier package.");
+                DataSmsSender smsSender = new DataSmsSender(tracker);
+                smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
+            } else {
+                Rlog.v(TAG, "No carrier package.");
+                sendSubmitPdu(tracker);
+            }
+        } else {
+            Rlog.e(TAG, "CdmaSMSDispatcher.sendData(): getSubmitPdu() returned null");
+            if (sentIntent != null) {
+                try {
+                    sentIntent.send(SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+                } catch (CanceledException ex) {
+                    Rlog.e(TAG, "Intent has been canceled!");
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
+            boolean persistMessage) {
+        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
+                scAddr, destAddr, text, (deliveryIntent != null), null);
+        if (pdu != null) {
+            HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
+            SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
+                    messageUri, false /*isExpectMore*/, text, true /*isText*/, persistMessage);
+
+            String carrierPackage = getCarrierAppPackageName();
+            if (carrierPackage != null) {
+                Rlog.d(TAG, "Found carrier package.");
+                TextSmsSender smsSender = new TextSmsSender(tracker);
+                smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
+            } else {
+                Rlog.v(TAG, "No carrier package.");
+                sendSubmitPdu(tracker);
+            }
+        } else {
+            Rlog.e(TAG, "CdmaSMSDispatcher.sendText(): getSubmitPdu() returned null");
+            if (sentIntent != null) {
+                try {
+                    sentIntent.send(SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+                } catch (CanceledException ex) {
+                    Rlog.e(TAG, "Intent has been canceled!");
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
+        throw new IllegalStateException("This method must be called only on ImsSMSDispatcher");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody,
+            boolean use7bitOnly) {
+        return SmsMessage.calculateLength(messageBody, use7bitOnly, false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
+            String message, SmsHeader smsHeader, int encoding,
+            PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart,
+            AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
+            String fullMessageText) {
+        UserData uData = new UserData();
+        uData.payloadStr = message;
+        uData.userDataHeader = smsHeader;
+        if (encoding == SmsConstants.ENCODING_7BIT) {
+            uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
+        } else { // assume UTF-16
+            uData.msgEncoding = UserData.ENCODING_UNICODE_16;
+        }
+        uData.msgEncodingSet = true;
+
+        /* By setting the statusReportRequested bit only for the
+         * last message fragment, this will result in only one
+         * callback to the sender when that last fragment delivery
+         * has been acknowledged. */
+        SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destinationAddress,
+                uData, (deliveryIntent != null) && lastPart);
+
+        HashMap map = getSmsTrackerMap(destinationAddress, scAddress,
+                message, submitPdu);
+        return getSmsTracker(map, sentIntent, deliveryIntent,
+                getFormat(), unsentPartCount, anyPartFailed, messageUri, smsHeader,
+                false /*isExpextMore*/, fullMessageText, true /*isText*/,
+                true /*persistMessage*/);
+    }
+
+    @Override
+    protected void sendSubmitPdu(SmsTracker tracker) {
+        if (mPhone.isInEcm()) {
+            if (VDBG) {
+                Rlog.d(TAG, "Block SMS in Emergency Callback mode");
+            }
+            tracker.onFailed(mContext, SmsManager.RESULT_ERROR_NO_SERVICE, 0/*errorCode*/);
+            return;
+        }
+        sendRawPdu(tracker);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void sendSms(SmsTracker tracker) {
+        Rlog.d(TAG, "sendSms: "
+                + " isIms()=" + isIms()
+                + " mRetryCount=" + tracker.mRetryCount
+                + " mImsRetry=" + tracker.mImsRetry
+                + " mMessageRef=" + tracker.mMessageRef
+                + " SS=" + mPhone.getServiceState().getState());
+
+        sendSmsByPstn(tracker);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void sendSmsByPstn(SmsTracker tracker) {
+        int ss = mPhone.getServiceState().getState();
+        // if sms over IMS is not supported on data and voice is not available...
+        if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
+            tracker.onFailed(mContext, getNotInServiceError(ss), 0/*errorCode*/);
+            return;
+        }
+
+        Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);
+        byte[] pdu = (byte[]) tracker.getData().get("pdu");
+
+        int currentDataNetwork = mPhone.getServiceState().getDataNetworkType();
+        boolean imsSmsDisabled = (currentDataNetwork == TelephonyManager.NETWORK_TYPE_EHRPD
+                    || (ServiceState.isLte(currentDataNetwork)
+                    && !mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()))
+                    && mPhone.getServiceState().getVoiceNetworkType()
+                    == TelephonyManager.NETWORK_TYPE_1xRTT
+                    && ((GsmCdmaPhone) mPhone).mCT.mState != PhoneConstants.State.IDLE;
+
+        // sms over cdma is used:
+        //   if sms over IMS is not supported AND
+        //   this is not a retry case after sms over IMS failed
+        //     indicated by mImsRetry > 0
+        if (0 == tracker.mImsRetry && !isIms() || imsSmsDisabled) {
+            mCi.sendCdmaSms(pdu, reply);
+        } else {
+            mCi.sendImsCdmaSms(pdu, tracker.mImsRetry, tracker.mMessageRef, reply);
+            // increment it here, so in case of SMS_FAIL_RETRY over IMS
+            // next retry will be sent using IMS request again.
+            tracker.mImsRetry++;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java b/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java
new file mode 100644
index 0000000..bcbce53
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java
@@ -0,0 +1,189 @@
+/*
+ * 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.internal.telephony.cdma;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionManager;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.telephony.cdma.CdmaSmsCbProgramResults;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.WakeLockStateMachine;
+import com.android.internal.telephony.cdma.sms.BearerData;
+import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
+import com.android.internal.telephony.cdma.sms.SmsEnvelope;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Handle CDMA Service Category Program Data requests and responses.
+ */
+public final class CdmaServiceCategoryProgramHandler extends WakeLockStateMachine {
+
+    final CommandsInterface mCi;
+
+    /**
+     * Create a new CDMA inbound SMS handler.
+     */
+    CdmaServiceCategoryProgramHandler(Context context, CommandsInterface commandsInterface) {
+        super("CdmaServiceCategoryProgramHandler", context, null);
+        mContext = context;
+        mCi = commandsInterface;
+    }
+
+    /**
+     * Create a new State machine for SCPD requests.
+     * @param context the context to use
+     * @param commandsInterface the radio commands interface
+     * @return the new SCPD handler
+     */
+    static CdmaServiceCategoryProgramHandler makeScpHandler(Context context,
+            CommandsInterface commandsInterface) {
+        CdmaServiceCategoryProgramHandler handler = new CdmaServiceCategoryProgramHandler(
+                context, commandsInterface);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}.
+     * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass.
+     *
+     * @param message the message to process
+     * @return true if an ordered broadcast was sent; false on failure
+     */
+    @Override
+    protected boolean handleSmsMessage(Message message) {
+        if (message.obj instanceof SmsMessage) {
+            return handleServiceCategoryProgramData((SmsMessage) message.obj);
+        } else {
+            loge("handleMessage got object of type: " + message.obj.getClass().getName());
+            return false;
+        }
+    }
+
+
+    /**
+     * Send SCPD request to CellBroadcastReceiver as an ordered broadcast.
+     * @param sms the CDMA SmsMessage containing the SCPD request
+     * @return true if an ordered broadcast was sent; false on failure
+     */
+    private boolean handleServiceCategoryProgramData(SmsMessage sms) {
+        ArrayList<CdmaSmsCbProgramData> programDataList = sms.getSmsCbProgramData();
+        if (programDataList == null) {
+            loge("handleServiceCategoryProgramData: program data list is null!");
+            return false;
+        }
+
+        Intent intent = new Intent(Intents.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION);
+        intent.setPackage(mContext.getResources().getString(
+                com.android.internal.R.string.config_defaultCellBroadcastReceiverPkg));
+        intent.putExtra("sender", sms.getOriginatingAddress());
+        intent.putParcelableArrayListExtra("program_data", programDataList);
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
+        mContext.sendOrderedBroadcast(intent, Manifest.permission.RECEIVE_SMS,
+                AppOpsManager.OP_RECEIVE_SMS, mScpResultsReceiver,
+                getHandler(), Activity.RESULT_OK, null, null);
+        return true;
+    }
+
+    /**
+     * Broadcast receiver to handle results of ordered broadcast. Sends the SCPD results
+     * as a reply SMS, then sends a message to state machine to transition to idle.
+     */
+    private final BroadcastReceiver mScpResultsReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            sendScpResults();
+            if (DBG) log("mScpResultsReceiver finished");
+            sendMessage(EVENT_BROADCAST_COMPLETE);
+        }
+
+        private void sendScpResults() {
+            int resultCode = getResultCode();
+            if ((resultCode != Activity.RESULT_OK) && (resultCode != Intents.RESULT_SMS_HANDLED)) {
+                loge("SCP results error: result code = " + resultCode);
+                return;
+            }
+            Bundle extras = getResultExtras(false);
+            if (extras == null) {
+                loge("SCP results error: missing extras");
+                return;
+            }
+            String sender = extras.getString("sender");
+            if (sender == null) {
+                loge("SCP results error: missing sender extra.");
+                return;
+            }
+            ArrayList<CdmaSmsCbProgramResults> results
+                    = extras.getParcelableArrayList("results");
+            if (results == null) {
+                loge("SCP results error: missing results extra.");
+                return;
+            }
+
+            BearerData bData = new BearerData();
+            bData.messageType = BearerData.MESSAGE_TYPE_SUBMIT;
+            bData.messageId = SmsMessage.getNextMessageId();
+            bData.serviceCategoryProgramResults = results;
+            byte[] encodedBearerData = BearerData.encode(bData);
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
+            DataOutputStream dos = new DataOutputStream(baos);
+            try {
+                dos.writeInt(SmsEnvelope.TELESERVICE_SCPT);
+                dos.writeInt(0); //servicePresent
+                dos.writeInt(0); //serviceCategory
+                CdmaSmsAddress destAddr = CdmaSmsAddress.parse(
+                        PhoneNumberUtils.cdmaCheckAndProcessPlusCodeForSms(sender));
+                dos.write(destAddr.digitMode);
+                dos.write(destAddr.numberMode);
+                dos.write(destAddr.ton); // number_type
+                dos.write(destAddr.numberPlan);
+                dos.write(destAddr.numberOfDigits);
+                dos.write(destAddr.origBytes, 0, destAddr.origBytes.length); // digits
+                // Subaddress is not supported.
+                dos.write(0); //subaddressType
+                dos.write(0); //subaddr_odd
+                dos.write(0); //subaddr_nbr_of_digits
+                dos.write(encodedBearerData.length);
+                dos.write(encodedBearerData, 0, encodedBearerData.length);
+                // Ignore the RIL response. TODO: implement retry if SMS send fails.
+                mCi.sendCdmaSms(baos.toByteArray(), null);
+            } catch (IOException e) {
+                loge("exception creating SCP results PDU", e);
+            } finally {
+                try {
+                    dos.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    };
+}
diff --git a/com/android/internal/telephony/cdma/CdmaSmsBroadcastConfigInfo.java b/com/android/internal/telephony/cdma/CdmaSmsBroadcastConfigInfo.java
new file mode 100644
index 0000000..b31df59
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaSmsBroadcastConfigInfo.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2011-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.internal.telephony.cdma;
+
+/**
+ * CdmaSmsBroadcastConfigInfo defines one configuration of Cdma Broadcast
+ * Message to be received by the ME
+ *
+ * serviceCategory defines a Broadcast message identifier
+ * whose value is 0x0000 - 0xFFFF as defined in C.R1001G 9.3.1 and 9.3.2.
+ * All other values can be treated as empty message ID.
+ *
+ * language defines a language code of Broadcast Message
+ * whose value is 0x00 - 0x07 as defined in C.R1001G 9.2.
+ * All other values can be treated as empty language code.
+ *
+ * selected false means message types specified in serviceCategory
+ * are not accepted, while true means accepted.
+ *
+ */
+public class CdmaSmsBroadcastConfigInfo {
+    private int mFromServiceCategory;
+    private int mToServiceCategory;
+    private int mLanguage;
+    private boolean mSelected;
+
+    /**
+     * Initialize the object from rssi and cid.
+     */
+    public CdmaSmsBroadcastConfigInfo(int fromServiceCategory, int toServiceCategory,
+            int language, boolean selected) {
+        mFromServiceCategory = fromServiceCategory;
+        mToServiceCategory = toServiceCategory;
+        mLanguage = language;
+        mSelected = selected;
+    }
+
+    /**
+     * @return the mFromServiceCategory
+     */
+    public int getFromServiceCategory() {
+        return mFromServiceCategory;
+    }
+
+    /**
+     * @return the mToServiceCategory
+     */
+    public int getToServiceCategory() {
+        return mToServiceCategory;
+    }
+
+    /**
+     * @return the mLanguage
+     */
+    public int getLanguage() {
+        return mLanguage;
+    }
+
+    /**
+     * @return the selected
+     */
+    public boolean isSelected() {
+        return mSelected;
+    }
+
+    @Override
+    public String toString() {
+        return "CdmaSmsBroadcastConfigInfo: Id [" +
+            mFromServiceCategory + ", " + mToServiceCategory + "] " +
+            (isSelected() ? "ENABLED" : "DISABLED");
+    }
+}
diff --git a/com/android/internal/telephony/cdma/CdmaSubscriptionSourceManager.java b/com/android/internal/telephony/cdma/CdmaSubscriptionSourceManager.java
new file mode 100644
index 0000000..25ad3af
--- /dev/null
+++ b/com/android/internal/telephony/cdma/CdmaSubscriptionSourceManager.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2011 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.internal.telephony.cdma;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Phone;
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.provider.Settings;
+import android.telephony.Rlog;
+
+/**
+ * Class that handles the CDMA subscription source changed events from RIL
+ */
+public class CdmaSubscriptionSourceManager extends Handler {
+    static final String LOG_TAG = "CdmaSSM";
+    private static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 1;
+    private static final int EVENT_GET_CDMA_SUBSCRIPTION_SOURCE     = 2;
+    private static final int EVENT_RADIO_ON                         = 3;
+    private static final int EVENT_SUBSCRIPTION_STATUS_CHANGED      = 4;
+
+    // To know subscription is activated
+    private static final int SUBSCRIPTION_ACTIVATED                 = 1;
+
+    public static final int SUBSCRIPTION_SOURCE_UNKNOWN = -1;
+    public static final int SUBSCRIPTION_FROM_RUIM      = 0; /* CDMA subscription from RUIM */
+    public static final int SUBSCRIPTION_FROM_NV        = 1; /* CDMA subscription from NV */
+
+    private static CdmaSubscriptionSourceManager sInstance;
+    private static final Object sReferenceCountMonitor = new Object();
+    private static int sReferenceCount = 0;
+
+    // ***** Instance Variables
+    private CommandsInterface mCi;
+    private RegistrantList mCdmaSubscriptionSourceChangedRegistrants = new RegistrantList();
+
+    // Type of CDMA subscription source
+    private AtomicInteger mCdmaSubscriptionSource =
+            new AtomicInteger(Phone.PREFERRED_CDMA_SUBSCRIPTION);
+
+    // Constructor
+    private CdmaSubscriptionSourceManager(Context context, CommandsInterface ci) {
+        mCi = ci;
+        mCi.registerForCdmaSubscriptionChanged(this, EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null);
+        mCi.registerForOn(this, EVENT_RADIO_ON, null);
+        int subscriptionSource = getDefault(context);
+        log("cdmaSSM constructor: " + subscriptionSource);
+        mCdmaSubscriptionSource.set(subscriptionSource);
+        mCi.registerForSubscriptionStatusChanged(this, EVENT_SUBSCRIPTION_STATUS_CHANGED, null);
+    }
+
+    /**
+     * This function creates a single instance of this class
+     *
+     * @return object of type CdmaSubscriptionSourceManager
+     */
+    public static CdmaSubscriptionSourceManager getInstance(Context context,
+            CommandsInterface ci, Handler h, int what, Object obj) {
+        synchronized (sReferenceCountMonitor) {
+            if (null == sInstance) {
+                sInstance = new CdmaSubscriptionSourceManager(context, ci);
+            }
+            CdmaSubscriptionSourceManager.sReferenceCount++;
+        }
+        sInstance.registerForCdmaSubscriptionSourceChanged(h, what, obj);
+        return sInstance;
+    }
+
+    /**
+     * Unregisters for the registered event with RIL
+     */
+    public void dispose(Handler h) {
+        mCdmaSubscriptionSourceChangedRegistrants.remove(h);
+        synchronized (sReferenceCountMonitor) {
+            sReferenceCount--;
+            if (sReferenceCount <= 0) {
+                mCi.unregisterForCdmaSubscriptionChanged(this);
+                mCi.unregisterForOn(this);
+                mCi.unregisterForSubscriptionStatusChanged(this);
+                sInstance = null;
+            }
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.os.Handler#handleMessage(android.os.Message)
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+        switch (msg.what) {
+            case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED:
+            case EVENT_GET_CDMA_SUBSCRIPTION_SOURCE:
+            {
+                log("CDMA_SUBSCRIPTION_SOURCE event = " + msg.what);
+                ar = (AsyncResult) msg.obj;
+                handleGetCdmaSubscriptionSource(ar);
+            }
+            break;
+            case EVENT_RADIO_ON: {
+                mCi.getCdmaSubscriptionSource(obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_SOURCE));
+            }
+            break;
+            case EVENT_SUBSCRIPTION_STATUS_CHANGED: {
+                log("EVENT_SUBSCRIPTION_STATUS_CHANGED");
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    int actStatus = ((int[])ar.result)[0];
+                    log("actStatus = " + actStatus);
+                    if (actStatus == SUBSCRIPTION_ACTIVATED) { // Subscription Activated
+                        // In case of multi-SIM, framework should wait for the subscription ready
+                        // to send any request to RIL.  Otherwise it will return failure.
+                        Rlog.v(LOG_TAG,"get Cdma Subscription Source");
+                        mCi.getCdmaSubscriptionSource(
+                                obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_SOURCE));
+                    }
+                } else {
+                    logw("EVENT_SUBSCRIPTION_STATUS_CHANGED, Exception:" + ar.exception);
+                }
+            }
+            break;
+            default:
+                super.handleMessage(msg);
+        }
+    }
+
+    /**
+     * Returns the current CDMA subscription source value
+     * @return CDMA subscription source value
+     */
+    public int getCdmaSubscriptionSource() {
+        log("getcdmasubscriptionSource: " + mCdmaSubscriptionSource.get());
+        return mCdmaSubscriptionSource.get();
+    }
+
+    /**
+     * Gets the default CDMA subscription source
+     *
+     * @return Default CDMA subscription source from Settings DB if present.
+     */
+    public static int getDefault(Context context) {
+        // Get the default value from the Settings
+        int subscriptionSource = Settings.Global.getInt(context.getContentResolver(),
+                Settings.Global.CDMA_SUBSCRIPTION_MODE, Phone.PREFERRED_CDMA_SUBSCRIPTION);
+        Rlog.d(LOG_TAG, "subscriptionSource from settings: " + subscriptionSource);
+        return subscriptionSource;
+    }
+
+    /**
+     * Clients automatically register for CDMA subscription source changed event
+     * when they get an instance of this object.
+     */
+    private void registerForCdmaSubscriptionSourceChanged(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mCdmaSubscriptionSourceChangedRegistrants.add(r);
+    }
+
+    /**
+     * Handles the call to get the subscription source
+     *
+     * @param ar AsyncResult object that contains the result of get CDMA
+     *            subscription source call
+     */
+    private void handleGetCdmaSubscriptionSource(AsyncResult ar) {
+        if ((ar.exception == null) && (ar.result != null)) {
+            int newSubscriptionSource = ((int[]) ar.result)[0];
+
+            if (newSubscriptionSource != mCdmaSubscriptionSource.get()) {
+                log("Subscription Source Changed : " + mCdmaSubscriptionSource + " >> "
+                        + newSubscriptionSource);
+                mCdmaSubscriptionSource.set(newSubscriptionSource);
+
+                // Notify registrants of the new CDMA subscription source
+                mCdmaSubscriptionSourceChangedRegistrants.notifyRegistrants(new AsyncResult(null,
+                        null, null));
+            }
+        } else {
+            // GET_CDMA_SUBSCRIPTION is returning Failure. Probably because modem created GSM Phone.
+            logw("Unable to get CDMA Subscription Source, Exception: " + ar.exception
+                    + ", result: " + ar.result);
+        }
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private void logw(String s) {
+        Rlog.w(LOG_TAG, s);
+    }
+
+}
diff --git a/com/android/internal/telephony/cdma/EriInfo.java b/com/android/internal/telephony/cdma/EriInfo.java
new file mode 100644
index 0000000..3e9680b
--- /dev/null
+++ b/com/android/internal/telephony/cdma/EriInfo.java
@@ -0,0 +1,45 @@
+/*
+ * 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.internal.telephony.cdma;
+
+public final class EriInfo {
+
+    public static final int ROAMING_INDICATOR_ON    = 0;
+    public static final int ROAMING_INDICATOR_OFF   = 1;
+    public static final int ROAMING_INDICATOR_FLASH = 2;
+
+    public static final int ROAMING_ICON_MODE_NORMAL    = 0;
+    public static final int ROAMING_ICON_MODE_FLASH     = 1;
+
+    public int roamingIndicator;
+    public int iconIndex;
+    public int iconMode;
+    public String eriText;
+    public int callPromptId;
+    public int alertId;
+
+    public EriInfo (int roamingIndicator, int iconIndex, int iconMode, String eriText,
+            int callPromptId, int alertId) {
+
+        this.roamingIndicator = roamingIndicator;
+        this.iconIndex = iconIndex;
+        this.iconMode = iconMode;
+        this.eriText = eriText;
+        this.callPromptId = callPromptId;
+        this.alertId = alertId;
+    }
+}
diff --git a/com/android/internal/telephony/cdma/EriManager.java b/com/android/internal/telephony/cdma/EriManager.java
new file mode 100644
index 0000000..81bc8fe
--- /dev/null
+++ b/com/android/internal/telephony/cdma/EriManager.java
@@ -0,0 +1,517 @@
+/*
+ * 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.internal.telephony.cdma;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.util.Xml;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.util.XmlUtils;
+
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * EriManager loads the ERI file definitions and manages the CDMA roaming information.
+ *
+ */
+public class EriManager {
+
+    class EriFile {
+
+        int mVersionNumber;                      // File version number
+        int mNumberOfEriEntries;                 // Number of entries
+        int mEriFileType;                        // Eri Phase 0/1
+        //int mNumberOfIconImages;               // reserved for future use
+        //int mIconImageType;                    // reserved for future use
+        String[] mCallPromptId;                  // reserved for future use
+        HashMap<Integer, EriInfo> mRoamIndTable; // Roaming Indicator Table
+
+        EriFile() {
+            mVersionNumber = -1;
+            mNumberOfEriEntries = 0;
+            mEriFileType = -1;
+            mCallPromptId = new String[] { "", "", "" };
+            mRoamIndTable = new HashMap<Integer, EriInfo>();
+        }
+    }
+
+    class EriDisplayInformation {
+        int mEriIconIndex;
+        int mEriIconMode;
+        String mEriIconText;
+
+        EriDisplayInformation(int eriIconIndex, int eriIconMode, String eriIconText) {
+            mEriIconIndex = eriIconIndex;
+            mEriIconMode = eriIconMode;
+            mEriIconText = eriIconText;
+        }
+
+//        public void setParameters(int eriIconIndex, int eriIconMode, String eriIconText){
+//            mEriIconIndex = eriIconIndex;
+//            mEriIconMode = eriIconMode;
+//            mEriIconText = eriIconText;
+//        }
+
+        @Override
+        public String toString() {
+            return "EriDisplayInformation: {" + " IconIndex: " + mEriIconIndex + " EriIconMode: "
+                    + mEriIconMode + " EriIconText: " + mEriIconText + " }";
+        }
+    }
+
+    private static final String LOG_TAG = "EriManager";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    public static final int ERI_FROM_XML   = 0;
+    static final int ERI_FROM_FILE_SYSTEM  = 1;
+    static final int ERI_FROM_MODEM        = 2;
+
+    private Context mContext;
+    private int mEriFileSource = ERI_FROM_XML;
+    private boolean mIsEriFileLoaded;
+    private EriFile mEriFile;
+    private final Phone mPhone;
+
+    public EriManager(Phone phone, Context context, int eriFileSource) {
+        mPhone = phone;
+        mContext = context;
+        mEriFileSource = eriFileSource;
+        mEriFile = new EriFile();
+    }
+
+    public void dispose() {
+        mEriFile = new EriFile();
+        mIsEriFileLoaded = false;
+    }
+
+
+    public void loadEriFile() {
+        switch (mEriFileSource) {
+        case ERI_FROM_MODEM:
+            loadEriFileFromModem();
+            break;
+
+        case ERI_FROM_FILE_SYSTEM:
+            loadEriFileFromFileSystem();
+            break;
+
+        case ERI_FROM_XML:
+        default:
+            loadEriFileFromXml();
+            break;
+        }
+    }
+
+    /**
+     * Load the ERI file from the MODEM through chipset specific RIL_REQUEST_OEM_HOOK
+     *
+     * In this case the ERI file can be updated from the Phone Support Tool available
+     * from the Chipset vendor
+     */
+    private void loadEriFileFromModem() {
+        // NOT IMPLEMENTED, Chipset vendor/Operator specific
+    }
+
+    /**
+     * Load the ERI file from a File System file
+     *
+     * In this case the a Phone Support Tool to update the ERI file must be provided
+     * to the Operator
+     */
+    private void loadEriFileFromFileSystem() {
+        // NOT IMPLEMENTED, Chipset vendor/Operator specific
+    }
+
+    /**
+     * Load the ERI file from the application framework resources encoded in XML
+     *
+     */
+    private void loadEriFileFromXml() {
+        XmlPullParser parser = null;
+        FileInputStream stream = null;
+        Resources r = mContext.getResources();
+
+        try {
+            if (DBG) Rlog.d(LOG_TAG, "loadEriFileFromXml: check for alternate file");
+            stream = new FileInputStream(
+                            r.getString(com.android.internal.R.string.alternate_eri_file));
+            parser = Xml.newPullParser();
+            parser.setInput(stream, null);
+            if (DBG) Rlog.d(LOG_TAG, "loadEriFileFromXml: opened alternate file");
+        } catch (FileNotFoundException e) {
+            if (DBG) Rlog.d(LOG_TAG, "loadEriFileFromXml: no alternate file");
+            parser = null;
+        } catch (XmlPullParserException e) {
+            if (DBG) Rlog.d(LOG_TAG, "loadEriFileFromXml: no parser for alternate file");
+            parser = null;
+        }
+
+        if (parser == null) {
+            String eriFile = null;
+
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configManager != null) {
+                PersistableBundle b = configManager.getConfigForSubId(mPhone.getSubId());
+                if (b != null) {
+                    eriFile = b.getString(CarrierConfigManager.KEY_CARRIER_ERI_FILE_NAME_STRING);
+                }
+            }
+
+            Rlog.d(LOG_TAG, "eriFile = " + eriFile);
+
+            if (eriFile == null) {
+                if (DBG) Rlog.e(LOG_TAG, "loadEriFileFromXml: Can't find ERI file to load");
+                return;
+            }
+
+            try {
+                parser = Xml.newPullParser();
+                parser.setInput(mContext.getAssets().open(eriFile), null);
+            } catch (IOException | XmlPullParserException e) {
+                if (DBG) Rlog.e(LOG_TAG, "loadEriFileFromXml: no parser for " + eriFile +
+                        ". Exception = " + e.toString());
+            }
+        }
+
+        try {
+            XmlUtils.beginDocument(parser, "EriFile");
+            mEriFile.mVersionNumber = Integer.parseInt(
+                    parser.getAttributeValue(null, "VersionNumber"));
+            mEriFile.mNumberOfEriEntries = Integer.parseInt(
+                    parser.getAttributeValue(null, "NumberOfEriEntries"));
+            mEriFile.mEriFileType = Integer.parseInt(
+                    parser.getAttributeValue(null, "EriFileType"));
+
+            int parsedEriEntries = 0;
+            while(true) {
+                XmlUtils.nextElement(parser);
+                String name = parser.getName();
+                if (name == null) {
+                    if (parsedEriEntries != mEriFile.mNumberOfEriEntries)
+                        Rlog.e(LOG_TAG, "Error Parsing ERI file: " +  mEriFile.mNumberOfEriEntries
+                                + " defined, " + parsedEriEntries + " parsed!");
+                    break;
+                } else if (name.equals("CallPromptId")) {
+                    int id = Integer.parseInt(parser.getAttributeValue(null, "Id"));
+                    String text = parser.getAttributeValue(null, "CallPromptText");
+                    if (id >= 0 && id <= 2) {
+                        mEriFile.mCallPromptId[id] = text;
+                    } else {
+                        Rlog.e(LOG_TAG, "Error Parsing ERI file: found" + id + " CallPromptId");
+                    }
+
+                } else if (name.equals("EriInfo")) {
+                    int roamingIndicator = Integer.parseInt(
+                            parser.getAttributeValue(null, "RoamingIndicator"));
+                    int iconIndex = Integer.parseInt(parser.getAttributeValue(null, "IconIndex"));
+                    int iconMode = Integer.parseInt(parser.getAttributeValue(null, "IconMode"));
+                    String eriText = parser.getAttributeValue(null, "EriText");
+                    int callPromptId = Integer.parseInt(
+                            parser.getAttributeValue(null, "CallPromptId"));
+                    int alertId = Integer.parseInt(parser.getAttributeValue(null, "AlertId"));
+                    parsedEriEntries++;
+                    mEriFile.mRoamIndTable.put(roamingIndicator, new EriInfo (roamingIndicator,
+                            iconIndex, iconMode, eriText, callPromptId, alertId));
+                }
+            }
+
+            Rlog.d(LOG_TAG, "loadEriFileFromXml: eri parsing successful, file loaded. ver = " +
+                    mEriFile.mVersionNumber + ", # of entries = " + mEriFile.mNumberOfEriEntries);
+
+            mIsEriFileLoaded = true;
+
+        } catch (Exception e) {
+            Rlog.e(LOG_TAG, "Got exception while loading ERI file.", e);
+        } finally {
+            if (parser instanceof XmlResourceParser) {
+                ((XmlResourceParser)parser).close();
+            }
+            try {
+                if (stream != null) {
+                    stream.close();
+                }
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+    }
+
+    /**
+     * Returns the version of the ERI file
+     *
+     */
+    public int getEriFileVersion() {
+        return mEriFile.mVersionNumber;
+    }
+
+    /**
+     * Returns the number of ERI entries parsed
+     *
+     */
+    public int getEriNumberOfEntries() {
+        return mEriFile.mNumberOfEriEntries;
+    }
+
+    /**
+     * Returns the ERI file type value ( 0 for Phase 0, 1 for Phase 1)
+     *
+     */
+    public int getEriFileType() {
+        return mEriFile.mEriFileType;
+    }
+
+    /**
+     * Returns if the ERI file has been loaded
+     *
+     */
+    public boolean isEriFileLoaded() {
+        return mIsEriFileLoaded;
+    }
+
+    /**
+     * Returns the EriInfo record associated with roamingIndicator
+     * or null if the entry is not found
+     */
+    private EriInfo getEriInfo(int roamingIndicator) {
+        if (mEriFile.mRoamIndTable.containsKey(roamingIndicator)) {
+            return mEriFile.mRoamIndTable.get(roamingIndicator);
+        } else {
+            return null;
+        }
+    }
+
+    private EriDisplayInformation getEriDisplayInformation(int roamInd, int defRoamInd){
+        EriDisplayInformation ret;
+
+        // Carrier can use carrier config to customize any built-in roaming display indications
+        if (mIsEriFileLoaded) {
+            EriInfo eriInfo = getEriInfo(roamInd);
+            if (eriInfo != null) {
+                if (VDBG) Rlog.v(LOG_TAG, "ERI roamInd " + roamInd + " found in ERI file");
+                ret = new EriDisplayInformation(
+                        eriInfo.iconIndex,
+                        eriInfo.iconMode,
+                        eriInfo.eriText);
+                return ret;
+            }
+        }
+
+        switch (roamInd) {
+        // Handling the standard roaming indicator (non-ERI)
+        case EriInfo.ROAMING_INDICATOR_ON:
+            ret = new EriDisplayInformation(
+                    EriInfo.ROAMING_INDICATOR_ON,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText0).toString());
+            break;
+
+        case EriInfo.ROAMING_INDICATOR_OFF:
+            ret = new EriDisplayInformation(
+                    EriInfo.ROAMING_INDICATOR_OFF,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText1).toString());
+            break;
+
+        case EriInfo.ROAMING_INDICATOR_FLASH:
+            ret = new EriDisplayInformation(
+                    EriInfo.ROAMING_INDICATOR_FLASH,
+                    EriInfo.ROAMING_ICON_MODE_FLASH,
+                    mContext.getText(com.android.internal.R.string.roamingText2).toString());
+            break;
+
+
+        // Handling the standard ERI
+        case 3:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText3).toString());
+            break;
+
+        case 4:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText4).toString());
+            break;
+
+        case 5:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText5).toString());
+            break;
+
+        case 6:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText6).toString());
+            break;
+
+        case 7:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText7).toString());
+            break;
+
+        case 8:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText8).toString());
+            break;
+
+        case 9:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText9).toString());
+            break;
+
+        case 10:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText10).toString());
+            break;
+
+        case 11:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText11).toString());
+            break;
+
+        case 12:
+            ret = new EriDisplayInformation(
+                    roamInd,
+                    EriInfo.ROAMING_ICON_MODE_NORMAL,
+                    mContext.getText(com.android.internal.R.string.roamingText12).toString());
+            break;
+
+        // Handling the non standard Enhanced Roaming Indicator (roamInd > 63)
+        default:
+            if (!mIsEriFileLoaded) {
+                // ERI file NOT loaded
+                if (DBG) Rlog.d(LOG_TAG, "ERI File not loaded");
+                if(defRoamInd > 2) {
+                    if (VDBG) Rlog.v(LOG_TAG, "ERI defRoamInd > 2 ...flashing");
+                    ret = new EriDisplayInformation(
+                            EriInfo.ROAMING_INDICATOR_FLASH,
+                            EriInfo.ROAMING_ICON_MODE_FLASH,
+                            mContext.getText(com.android.internal
+                                                            .R.string.roamingText2).toString());
+                } else {
+                    if (VDBG) Rlog.v(LOG_TAG, "ERI defRoamInd <= 2");
+                    switch (defRoamInd) {
+                    case EriInfo.ROAMING_INDICATOR_ON:
+                        ret = new EriDisplayInformation(
+                                EriInfo.ROAMING_INDICATOR_ON,
+                                EriInfo.ROAMING_ICON_MODE_NORMAL,
+                                mContext.getText(com.android.internal
+                                                            .R.string.roamingText0).toString());
+                        break;
+
+                    case EriInfo.ROAMING_INDICATOR_OFF:
+                        ret = new EriDisplayInformation(
+                                EriInfo.ROAMING_INDICATOR_OFF,
+                                EriInfo.ROAMING_ICON_MODE_NORMAL,
+                                mContext.getText(com.android.internal
+                                                            .R.string.roamingText1).toString());
+                        break;
+
+                    case EriInfo.ROAMING_INDICATOR_FLASH:
+                        ret = new EriDisplayInformation(
+                                EriInfo.ROAMING_INDICATOR_FLASH,
+                                EriInfo.ROAMING_ICON_MODE_FLASH,
+                                mContext.getText(com.android.internal
+                                                            .R.string.roamingText2).toString());
+                        break;
+
+                    default:
+                        ret = new EriDisplayInformation(-1, -1, "ERI text");
+                    }
+                }
+            } else {
+                // ERI file loaded
+                EriInfo eriInfo = getEriInfo(roamInd);
+                EriInfo defEriInfo = getEriInfo(defRoamInd);
+                if (eriInfo == null) {
+                    if (VDBG) {
+                        Rlog.v(LOG_TAG, "ERI roamInd " + roamInd
+                            + " not found in ERI file ...using defRoamInd " + defRoamInd);
+                    }
+                    if(defEriInfo == null) {
+                        Rlog.e(LOG_TAG, "ERI defRoamInd " + defRoamInd
+                                + " not found in ERI file ...on");
+                        ret = new EriDisplayInformation(
+                                EriInfo.ROAMING_INDICATOR_ON,
+                                EriInfo.ROAMING_ICON_MODE_NORMAL,
+                                mContext.getText(com.android.internal
+                                                             .R.string.roamingText0).toString());
+
+                    } else {
+                        if (VDBG) {
+                            Rlog.v(LOG_TAG, "ERI defRoamInd " + defRoamInd + " found in ERI file");
+                        }
+                        ret = new EriDisplayInformation(
+                                defEriInfo.iconIndex,
+                                defEriInfo.iconMode,
+                                defEriInfo.eriText);
+                    }
+                } else {
+                    if (VDBG) Rlog.v(LOG_TAG, "ERI roamInd " + roamInd + " found in ERI file");
+                    ret = new EriDisplayInformation(
+                            eriInfo.iconIndex,
+                            eriInfo.iconMode,
+                            eriInfo.eriText);
+                }
+            }
+            break;
+        }
+        if (VDBG) Rlog.v(LOG_TAG, "Displaying ERI " + ret.toString());
+        return ret;
+    }
+
+    public int getCdmaEriIconIndex(int roamInd, int defRoamInd){
+        return getEriDisplayInformation(roamInd, defRoamInd).mEriIconIndex;
+    }
+
+    public int getCdmaEriIconMode(int roamInd, int defRoamInd){
+        return getEriDisplayInformation(roamInd, defRoamInd).mEriIconMode;
+    }
+
+    public String getCdmaEriText(int roamInd, int defRoamInd){
+        return getEriDisplayInformation(roamInd, defRoamInd).mEriIconText;
+    }
+}
diff --git a/com/android/internal/telephony/cdma/SignalToneUtil.java b/com/android/internal/telephony/cdma/SignalToneUtil.java
new file mode 100644
index 0000000..f702a09
--- /dev/null
+++ b/com/android/internal/telephony/cdma/SignalToneUtil.java
@@ -0,0 +1,292 @@
+/*
+ * 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.internal.telephony.cdma;
+
+import java.util.HashMap;
+import android.media.ToneGenerator;
+
+public class SignalToneUtil {
+    /** A marker that isn't a valid TONE */
+    public static final int CDMA_INVALID_TONE = -1;
+
+    // public final int int IS95_CONST_IR_SIGNAL_TYPE_TYPE;
+    static public final int IS95_CONST_IR_SIGNAL_TONE = 0;
+    static public final int IS95_CONST_IR_SIGNAL_ISDN = 1;
+    static public final int IS95_CONST_IR_SIGNAL_IS54B = 2;
+    static public final int IS95_CONST_IR_SIGNAL_USR_DEFD_ALERT = 4;
+
+    // public final int int IS95_CONST_IR_ALERT_PITCH_TYPE;
+    static public final int IS95_CONST_IR_ALERT_MED = 0;
+    static public final int IS95_CONST_IR_ALERT_HIGH = 1;
+    static public final int IS95_CONST_IR_ALERT_LOW = 2;
+
+    // Based on 3GPP2 C.S0005-E, section 3.7.5.5 Signal,
+    // set TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN to 0 to avoid
+    // the alert pitch to be involved in hash calculation for
+    // signal type other than IS54B.
+    static public final int TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN = 0;
+
+    // public final int int IS95_CONST_IR_SIGNAL_TYPE;
+    static public final int IS95_CONST_IR_SIG_ISDN_NORMAL = 0;
+    static public final int IS95_CONST_IR_SIG_ISDN_INTGRP = 1;
+    static public final int IS95_CONST_IR_SIG_ISDN_SP_PRI = 2;
+    static public final int IS95_CONST_IR_SIG_ISDN_PAT_3 = 3;
+    static public final int IS95_CONST_IR_SIG_ISDN_PING = 4;
+    static public final int IS95_CONST_IR_SIG_ISDN_PAT_5 = 5;
+    static public final int IS95_CONST_IR_SIG_ISDN_PAT_6 = 6;
+    static public final int IS95_CONST_IR_SIG_ISDN_PAT_7 = 7;
+    static public final int IS95_CONST_IR_SIG_ISDN_OFF = 15;
+    static public final int IS95_CONST_IR_SIG_TONE_DIAL = 0;
+    static public final int IS95_CONST_IR_SIG_TONE_RING = 1;
+    static public final int IS95_CONST_IR_SIG_TONE_INT = 2;
+    static public final int IS95_CONST_IR_SIG_TONE_ABB_INT = 3;
+    static public final int IS95_CONST_IR_SIG_TONE_REORDER = 4;
+    static public final int IS95_CONST_IR_SIG_TONE_ABB_RE = 5;
+    static public final int IS95_CONST_IR_SIG_TONE_BUSY = 6;
+    static public final int IS95_CONST_IR_SIG_TONE_CONFIRM = 7;
+    static public final int IS95_CONST_IR_SIG_TONE_ANSWER = 8;
+    static public final int IS95_CONST_IR_SIG_TONE_CALL_W = 9;
+    static public final int IS95_CONST_IR_SIG_TONE_PIP = 10;
+    static public final int IS95_CONST_IR_SIG_TONE_NO_TONE = 63;
+    static public final int IS95_CONST_IR_SIG_IS54B_NO_TONE = 0;
+    static public final int IS95_CONST_IR_SIG_IS54B_L = 1;
+    static public final int IS95_CONST_IR_SIG_IS54B_SS = 2;
+    static public final int IS95_CONST_IR_SIG_IS54B_SSL = 3;
+    static public final int IS95_CONST_IR_SIG_IS54B_SS_2 = 4;
+    static public final int IS95_CONST_IR_SIG_IS54B_SLS = 5;
+    static public final int IS95_CONST_IR_SIG_IS54B_S_X4 = 6;
+    static public final int IS95_CONST_IR_SIG_IS54B_PBX_L = 7;
+    static public final int IS95_CONST_IR_SIG_IS54B_PBX_SS = 8;
+    static public final int IS95_CONST_IR_SIG_IS54B_PBX_SSL = 9;
+    static public final int IS95_CONST_IR_SIG_IS54B_PBX_SLS = 10;
+    static public final int IS95_CONST_IR_SIG_IS54B_PBX_S_X4 = 11;
+    static public final int IS95_CONST_IR_SIG_TONE_ABBR_ALRT = 0;
+
+    // Hashmap to map signalInfo To AudioTone
+    static private HashMap<Integer, Integer> mHm = new HashMap<Integer, Integer>();
+
+    private static Integer signalParamHash(int signalType, int alertPitch, int signal) {
+        if ((signalType < 0) || (signalType > 256) || (alertPitch > 256) ||
+                (alertPitch < 0) || (signal > 256) || (signal < 0)) {
+            return new Integer(CDMA_INVALID_TONE);
+        }
+        // Based on 3GPP2 C.S0005-E, seciton 3.7.5.5 Signal,
+        // the alert pitch field is ignored by the mobile station unless
+        // SIGNAL_TYPE is '10',IS-54B Alerting.
+        // Set alert pitch to TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN
+        // so the alert pitch is not involved in hash calculation
+        // when signal type is not IS-54B.
+        if (signalType != IS95_CONST_IR_SIGNAL_IS54B) {
+            alertPitch = TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN;
+        }
+        return new Integer(signalType * 256 * 256 + alertPitch * 256 + signal);
+    }
+
+    public static int getAudioToneFromSignalInfo(int signalType, int alertPitch, int signal) {
+        Integer result = mHm.get(signalParamHash(signalType, alertPitch, signal));
+        if (result == null) {
+            return CDMA_INVALID_TONE;
+        }
+        return result;
+    }
+
+    static {
+
+        /* SIGNAL_TYPE_ISDN */
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_NORMAL), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_NORMAL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                        IS95_CONST_IR_SIG_ISDN_INTGRP),
+                        ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_INTERGROUP);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_SP_PRI), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_SP_PRI);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_PAT_3), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT3);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_PING), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PING_RING);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_PAT_5), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT5);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_PAT_6), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT6);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_PAT_7), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT7);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_ISDN_OFF), ToneGenerator.TONE_CDMA_SIGNAL_OFF);
+
+        /* SIGNAL_TYPE_TONE */
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_DIAL), ToneGenerator.TONE_CDMA_DIAL_TONE_LITE);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_RING), ToneGenerator.TONE_CDMA_NETWORK_USA_RINGBACK);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_INT), ToneGenerator.TONE_SUP_INTERCEPT);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_ABB_INT), ToneGenerator.TONE_SUP_INTERCEPT_ABBREV);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_REORDER), ToneGenerator.TONE_CDMA_REORDER);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_ABB_RE), ToneGenerator.TONE_CDMA_ABBR_REORDER);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_BUSY), ToneGenerator.TONE_CDMA_NETWORK_BUSY);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_CONFIRM), ToneGenerator.TONE_SUP_CONFIRM);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_ANSWER), ToneGenerator.TONE_CDMA_ANSWER);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_CALL_W), ToneGenerator.TONE_CDMA_NETWORK_CALLWAITING);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_PIP), ToneGenerator.TONE_CDMA_PIP);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_TONE_NO_TONE), ToneGenerator.TONE_CDMA_SIGNAL_OFF);
+
+        /* SIGNAL_TYPE_IS54B */
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_L), ToneGenerator.TONE_CDMA_HIGH_L);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_L), ToneGenerator.TONE_CDMA_MED_L);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_L), ToneGenerator.TONE_CDMA_LOW_L);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_SS), ToneGenerator.TONE_CDMA_HIGH_SS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_SS), ToneGenerator.TONE_CDMA_MED_SS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_SS), ToneGenerator.TONE_CDMA_LOW_SS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_SSL), ToneGenerator.TONE_CDMA_HIGH_SSL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_SSL), ToneGenerator.TONE_CDMA_MED_SSL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_SSL), ToneGenerator.TONE_CDMA_LOW_SSL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_SS_2), ToneGenerator.TONE_CDMA_HIGH_SS_2);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_SS_2), ToneGenerator.TONE_CDMA_MED_SS_2);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_SS_2), ToneGenerator.TONE_CDMA_LOW_SS_2);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_SLS), ToneGenerator.TONE_CDMA_HIGH_SLS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_SLS), ToneGenerator.TONE_CDMA_MED_SLS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_SLS), ToneGenerator.TONE_CDMA_LOW_SLS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_S_X4), ToneGenerator.TONE_CDMA_HIGH_S_X4);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_S_X4), ToneGenerator.TONE_CDMA_MED_S_X4);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_S_X4), ToneGenerator.TONE_CDMA_LOW_S_X4);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_PBX_L), ToneGenerator.TONE_CDMA_HIGH_PBX_L);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_PBX_L), ToneGenerator.TONE_CDMA_MED_PBX_L);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_PBX_L), ToneGenerator.TONE_CDMA_LOW_PBX_L);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_PBX_SS), ToneGenerator.TONE_CDMA_HIGH_PBX_SS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_PBX_SS), ToneGenerator.TONE_CDMA_MED_PBX_SS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_PBX_SS), ToneGenerator.TONE_CDMA_LOW_PBX_SS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_PBX_SSL), ToneGenerator.TONE_CDMA_HIGH_PBX_SSL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_PBX_SSL), ToneGenerator.TONE_CDMA_MED_PBX_SSL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_PBX_SSL), ToneGenerator.TONE_CDMA_LOW_PBX_SSL);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_PBX_SLS), ToneGenerator.TONE_CDMA_HIGH_PBX_SLS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_PBX_SLS), ToneGenerator.TONE_CDMA_MED_PBX_SLS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_PBX_SLS), ToneGenerator.TONE_CDMA_LOW_PBX_SLS);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH,
+                IS95_CONST_IR_SIG_IS54B_PBX_S_X4), ToneGenerator.TONE_CDMA_HIGH_PBX_S_X4);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED,
+                IS95_CONST_IR_SIG_IS54B_PBX_S_X4), ToneGenerator.TONE_CDMA_MED_PBX_S_X4);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW,
+                IS95_CONST_IR_SIG_IS54B_PBX_S_X4), ToneGenerator.TONE_CDMA_LOW_PBX_S_X4);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN,
+                IS95_CONST_IR_SIG_IS54B_NO_TONE), ToneGenerator.TONE_CDMA_SIGNAL_OFF);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_USR_DEFD_ALERT,
+                TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, IS95_CONST_IR_SIG_TONE_ABBR_ALRT),
+                ToneGenerator.TONE_CDMA_ABBR_ALERT);
+
+        mHm.put(signalParamHash(IS95_CONST_IR_SIGNAL_USR_DEFD_ALERT,
+                TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, IS95_CONST_IR_SIG_TONE_NO_TONE),
+                ToneGenerator.TONE_CDMA_ABBR_ALERT);
+
+    }
+
+    // suppress default constructor for noninstantiability
+    private SignalToneUtil() {
+    }
+}
diff --git a/com/android/internal/telephony/cdma/SmsMessage.java b/com/android/internal/telephony/cdma/SmsMessage.java
new file mode 100644
index 0000000..629173d
--- /dev/null
+++ b/com/android/internal/telephony/cdma/SmsMessage.java
@@ -0,0 +1,968 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma;
+
+import android.os.Parcel;
+import android.os.SystemProperties;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.telephony.Rlog;
+import android.util.Log;
+import android.text.TextUtils;
+import android.content.res.Resources;
+
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.SmsAddress;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.cdma.sms.BearerData;
+import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
+import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress;
+import com.android.internal.telephony.cdma.sms.SmsEnvelope;
+import com.android.internal.telephony.cdma.sms.UserData;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.util.BitwiseInputStream;
+import com.android.internal.util.HexDump;
+import com.android.internal.telephony.Sms7BitEncodingTranslator;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * TODO(cleanup): these constants are disturbing... are they not just
+ * different interpretations on one number?  And if we did not have
+ * terrible class name overlap, they would not need to be directly
+ * imported like this.  The class in this file could just as well be
+ * named CdmaSmsMessage, could it not?
+ */
+
+/**
+ * TODO(cleanup): internally returning null in many places makes
+ * debugging very hard (among many other reasons) and should be made
+ * more meaningful (replaced with exceptions for example).  Null
+ * returns should only occur at the very outside of the module/class
+ * scope.
+ */
+
+/**
+ * A Short Message Service message.
+ *
+ */
+public class SmsMessage extends SmsMessageBase {
+    static final String LOG_TAG = "SmsMessage";
+    static private final String LOGGABLE_TAG = "CDMA:SMS";
+    private static final boolean VDBG = false;
+
+    private final static byte TELESERVICE_IDENTIFIER                    = 0x00;
+    private final static byte SERVICE_CATEGORY                          = 0x01;
+    private final static byte ORIGINATING_ADDRESS                       = 0x02;
+    private final static byte ORIGINATING_SUB_ADDRESS                   = 0x03;
+    private final static byte DESTINATION_ADDRESS                       = 0x04;
+    private final static byte DESTINATION_SUB_ADDRESS                   = 0x05;
+    private final static byte BEARER_REPLY_OPTION                       = 0x06;
+    private final static byte CAUSE_CODES                               = 0x07;
+    private final static byte BEARER_DATA                               = 0x08;
+
+    /**
+     *  Status of a previously submitted SMS.
+     *  This field applies to SMS Delivery Acknowledge messages. 0 indicates success;
+     *  Here, the error class is defined by the bits from 9-8, the status code by the bits from 7-0.
+     *  See C.S0015-B, v2.0, 4.5.21 for a detailed description of possible values.
+     */
+    private int status;
+
+    /** Specifies if a return of an acknowledgment is requested for send SMS */
+    private static final int RETURN_NO_ACK  = 0;
+    private static final int RETURN_ACK     = 1;
+
+    private SmsEnvelope mEnvelope;
+    private BearerData mBearerData;
+
+    /** @hide */
+    public SmsMessage(SmsAddress addr, SmsEnvelope env) {
+        mOriginatingAddress = addr;
+        mEnvelope = env;
+        createPdu();
+    }
+
+    public SmsMessage() {}
+
+    public static class SubmitPdu extends SubmitPduBase {
+    }
+
+    /**
+     * Create an SmsMessage from a raw PDU.
+     * Note: In CDMA the PDU is just a byte representation of the received Sms.
+     */
+    public static SmsMessage createFromPdu(byte[] pdu) {
+        SmsMessage msg = new SmsMessage();
+
+        try {
+            msg.parsePdu(pdu);
+            return msg;
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
+            return null;
+        } catch (OutOfMemoryError e) {
+            Log.e(LOG_TAG, "SMS PDU parsing failed with out of memory: ", e);
+            return null;
+        }
+    }
+
+    /**
+     * Create an SmsMessage from an SMS EF record.
+     *
+     * @param index Index of SMS record. This should be index in ArrayList
+     *              returned by RuimSmsInterfaceManager.getAllMessagesFromIcc + 1.
+     * @param data Record data.
+     * @return An SmsMessage representing the record.
+     *
+     * @hide
+     */
+    public static SmsMessage createFromEfRecord(int index, byte[] data) {
+        try {
+            SmsMessage msg = new SmsMessage();
+
+            msg.mIndexOnIcc = index;
+
+            // First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT,
+            // or STORED_UNSENT
+            // See 3GPP2 C.S0023 3.4.27
+            if ((data[0] & 1) == 0) {
+                Rlog.w(LOG_TAG, "SMS parsing failed: Trying to parse a free record");
+                return null;
+            } else {
+                msg.mStatusOnIcc = data[0] & 0x07;
+            }
+
+            // Second byte is the MSG_LEN, length of the message
+            // See 3GPP2 C.S0023 3.4.27
+            int size = data[1];
+
+            // Note: Data may include trailing FF's.  That's OK; message
+            // should still parse correctly.
+            byte[] pdu = new byte[size];
+            System.arraycopy(data, 2, pdu, 0, size);
+            // the message has to be parsed before it can be displayed
+            // see gsm.SmsMessage
+            msg.parsePduFromEfRecord(pdu);
+            return msg;
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
+            return null;
+        }
+
+    }
+
+    /**
+     * Note: This function is a GSM specific functionality which is not supported in CDMA mode.
+     */
+    public static int getTPLayerLengthForPDU(String pdu) {
+        Rlog.w(LOG_TAG, "getTPLayerLengthForPDU: is not supported in CDMA mode.");
+        return 0;
+    }
+
+    /**
+     * TODO(cleanup): why do getSubmitPdu methods take an scAddr input
+     * and do nothing with it?  GSM allows us to specify a SC (eg,
+     * when responding to an SMS that explicitly requests the response
+     * is sent to a specific SC), or pass null to use the default
+     * value.  Is there no similar notion in CDMA? Or do we just not
+     * have it hooked up?
+     */
+
+    /**
+     * Get an SMS-SUBMIT PDU for a destination address and a message
+     *
+     * @param scAddr                Service Centre address.  Null means use default.
+     * @param destAddr              Address of the recipient.
+     * @param message               String representation of the message payload.
+     * @param statusReportRequested Indicates whether a report is requested for this message.
+     * @param smsHeader             Array containing the data for the User Data Header, preceded
+     *                              by the Element Identifiers.
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     * @hide
+     */
+    public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, String message,
+            boolean statusReportRequested, SmsHeader smsHeader) {
+
+        /**
+         * TODO(cleanup): Do we really want silent failure like this?
+         * Would it not be much more reasonable to make sure we don't
+         * call this function if we really want nothing done?
+         */
+        if (message == null || destAddr == null) {
+            return null;
+        }
+
+        UserData uData = new UserData();
+        uData.payloadStr = message;
+        uData.userDataHeader = smsHeader;
+        return privateGetSubmitPdu(destAddr, statusReportRequested, uData);
+    }
+
+    /**
+     * Get an SMS-SUBMIT PDU for a data message to a destination address and port.
+     *
+     * @param scAddr Service Centre address. null == use default
+     * @param destAddr the address of the destination for the message
+     * @param destPort the port to deliver the message to at the
+     *        destination
+     * @param data the data for the message
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     */
+    public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, int destPort,
+            byte[] data, boolean statusReportRequested) {
+
+        /**
+         * TODO(cleanup): this is not a general-purpose SMS creation
+         * method, but rather something specialized to messages
+         * containing OCTET encoded (meaning non-human-readable) user
+         * data.  The name should reflect that, and not just overload.
+         */
+
+        SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs();
+        portAddrs.destPort = destPort;
+        portAddrs.origPort = 0;
+        portAddrs.areEightBits = false;
+
+        SmsHeader smsHeader = new SmsHeader();
+        smsHeader.portAddrs = portAddrs;
+
+        UserData uData = new UserData();
+        uData.userDataHeader = smsHeader;
+        uData.msgEncoding = UserData.ENCODING_OCTET;
+        uData.msgEncodingSet = true;
+        uData.payload = data;
+
+        return privateGetSubmitPdu(destAddr, statusReportRequested, uData);
+    }
+
+    /**
+     * Get an SMS-SUBMIT PDU for a data message to a destination address &amp; port
+     *
+     * @param destAddr the address of the destination for the message
+     * @param userData the data for the message
+     * @param statusReportRequested Indicates whether a report is requested for this message.
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     */
+    public static SubmitPdu getSubmitPdu(String destAddr, UserData userData,
+            boolean statusReportRequested) {
+        return privateGetSubmitPdu(destAddr, statusReportRequested, userData);
+    }
+
+    /**
+     * Note: This function is a GSM specific functionality which is not supported in CDMA mode.
+     */
+    @Override
+    public int getProtocolIdentifier() {
+        Rlog.w(LOG_TAG, "getProtocolIdentifier: is not supported in CDMA mode.");
+        // (3GPP TS 23.040): "no interworking, but SME to SME protocol":
+        return 0;
+    }
+
+    /**
+     * Note: This function is a GSM specific functionality which is not supported in CDMA mode.
+     */
+    @Override
+    public boolean isReplace() {
+        Rlog.w(LOG_TAG, "isReplace: is not supported in CDMA mode.");
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     * Note: This function is a GSM specific functionality which is not supported in CDMA mode.
+     */
+    @Override
+    public boolean isCphsMwiMessage() {
+        Rlog.w(LOG_TAG, "isCphsMwiMessage: is not supported in CDMA mode.");
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isMWIClearMessage() {
+        return ((mBearerData != null) && (mBearerData.numberOfMessages == 0));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isMWISetMessage() {
+        return ((mBearerData != null) && (mBearerData.numberOfMessages > 0));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isMwiDontStore() {
+        return ((mBearerData != null) &&
+                (mBearerData.numberOfMessages > 0) &&
+                (mBearerData.userData == null));
+    }
+
+    /**
+     * Returns the status for a previously submitted message.
+     * For not interfering with status codes from GSM, this status code is
+     * shifted to the bits 31-16.
+     */
+    @Override
+    public int getStatus() {
+        return (status << 16);
+    }
+
+    /** Return true iff the bearer data message type is DELIVERY_ACK. */
+    @Override
+    public boolean isStatusReportMessage() {
+        return (mBearerData.messageType == BearerData.MESSAGE_TYPE_DELIVERY_ACK);
+    }
+
+    /**
+     * Note: This function is a GSM specific functionality which is not supported in CDMA mode.
+     */
+    @Override
+    public boolean isReplyPathPresent() {
+        Rlog.w(LOG_TAG, "isReplyPathPresent: is not supported in CDMA mode.");
+        return false;
+    }
+
+    /**
+     * Calculate the number of septets needed to encode the message.
+     *
+     * @param messageBody the message to encode
+     * @param use7bitOnly ignore (but still count) illegal characters if true
+     * @param isEntireMsg indicates if this is entire msg or a segment in multipart msg
+     * @return TextEncodingDetails
+     */
+    public static TextEncodingDetails calculateLength(CharSequence messageBody,
+            boolean use7bitOnly, boolean isEntireMsg) {
+        CharSequence newMsgBody = null;
+        Resources r = Resources.getSystem();
+        if (r.getBoolean(com.android.internal.R.bool.config_sms_force_7bit_encoding)) {
+            newMsgBody  = Sms7BitEncodingTranslator.translate(messageBody);
+        }
+        if (TextUtils.isEmpty(newMsgBody)) {
+            newMsgBody = messageBody;
+        }
+        return BearerData.calcTextEncodingDetails(newMsgBody, use7bitOnly, isEntireMsg);
+    }
+
+    /**
+     * Returns the teleservice type of the message.
+     * @return the teleservice:
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_NOT_SET},
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WMT},
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WEMT},
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_VMN},
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WAP}
+    */
+    public int getTeleService() {
+        return mEnvelope.teleService;
+    }
+
+    /**
+     * Returns the message type of the message.
+     * @return the message type:
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_POINT_TO_POINT},
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_BROADCAST},
+     *  {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_ACKNOWLEDGE},
+    */
+    public int getMessageType() {
+        // NOTE: mEnvelope.messageType is not set correctly for cell broadcasts with some RILs.
+        // Use the service category parameter to detect CMAS and other cell broadcast messages.
+        if (mEnvelope.serviceCategory != 0) {
+            return SmsEnvelope.MESSAGE_TYPE_BROADCAST;
+        } else {
+            return SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT;
+        }
+    }
+
+    /**
+     * Decodes pdu to an empty SMS object.
+     * In the CDMA case the pdu is just an internal byte stream representation
+     * of the SMS Java-object.
+     * @see #createPdu()
+     */
+    private void parsePdu(byte[] pdu) {
+        ByteArrayInputStream bais = new ByteArrayInputStream(pdu);
+        DataInputStream dis = new DataInputStream(bais);
+        int length;
+        int bearerDataLength;
+        SmsEnvelope env = new SmsEnvelope();
+        CdmaSmsAddress addr = new CdmaSmsAddress();
+
+        try {
+            env.messageType = dis.readInt();
+            env.teleService = dis.readInt();
+            env.serviceCategory = dis.readInt();
+
+            addr.digitMode = dis.readByte();
+            addr.numberMode = dis.readByte();
+            addr.ton = dis.readByte();
+            addr.numberPlan = dis.readByte();
+
+            length = dis.readUnsignedByte();
+            addr.numberOfDigits = length;
+
+            // sanity check on the length
+            if (length > pdu.length) {
+                throw new RuntimeException(
+                        "createFromPdu: Invalid pdu, addr.numberOfDigits " + length
+                        + " > pdu len " + pdu.length);
+            }
+            addr.origBytes = new byte[length];
+            dis.read(addr.origBytes, 0, length); // digits
+
+            env.bearerReply = dis.readInt();
+            // CauseCode values:
+            env.replySeqNo = dis.readByte();
+            env.errorClass = dis.readByte();
+            env.causeCode = dis.readByte();
+
+            //encoded BearerData:
+            bearerDataLength = dis.readInt();
+            // sanity check on the length
+            if (bearerDataLength > pdu.length) {
+                throw new RuntimeException(
+                        "createFromPdu: Invalid pdu, bearerDataLength " + bearerDataLength
+                        + " > pdu len " + pdu.length);
+            }
+            env.bearerData = new byte[bearerDataLength];
+            dis.read(env.bearerData, 0, bearerDataLength);
+            dis.close();
+        } catch (IOException ex) {
+            throw new RuntimeException(
+                    "createFromPdu: conversion from byte array to object failed: " + ex, ex);
+        } catch (Exception ex) {
+            Rlog.e(LOG_TAG, "createFromPdu: conversion from byte array to object failed: " + ex);
+        }
+
+        // link the filled objects to this SMS
+        mOriginatingAddress = addr;
+        env.origAddress = addr;
+        mEnvelope = env;
+        mPdu = pdu;
+
+        parseSms();
+    }
+
+    /**
+     * Decodes 3GPP2 sms stored in CSIM/RUIM cards As per 3GPP2 C.S0015-0
+     */
+    private void parsePduFromEfRecord(byte[] pdu) {
+        ByteArrayInputStream bais = new ByteArrayInputStream(pdu);
+        DataInputStream dis = new DataInputStream(bais);
+        SmsEnvelope env = new SmsEnvelope();
+        CdmaSmsAddress addr = new CdmaSmsAddress();
+        CdmaSmsSubaddress subAddr = new CdmaSmsSubaddress();
+
+        try {
+            env.messageType = dis.readByte();
+
+            while (dis.available() > 0) {
+                int parameterId = dis.readByte();
+                int parameterLen = dis.readUnsignedByte();
+                byte[] parameterData = new byte[parameterLen];
+
+                switch (parameterId) {
+                    case TELESERVICE_IDENTIFIER:
+                        /*
+                         * 16 bit parameter that identifies which upper layer
+                         * service access point is sending or should receive
+                         * this message
+                         */
+                        env.teleService = dis.readUnsignedShort();
+                        Rlog.i(LOG_TAG, "teleservice = " + env.teleService);
+                        break;
+                    case SERVICE_CATEGORY:
+                        /*
+                         * 16 bit parameter that identifies type of service as
+                         * in 3GPP2 C.S0015-0 Table 3.4.3.2-1
+                         */
+                        env.serviceCategory = dis.readUnsignedShort();
+                        break;
+                    case ORIGINATING_ADDRESS:
+                    case DESTINATION_ADDRESS:
+                        dis.read(parameterData, 0, parameterLen);
+                        BitwiseInputStream addrBis = new BitwiseInputStream(parameterData);
+                        addr.digitMode = addrBis.read(1);
+                        addr.numberMode = addrBis.read(1);
+                        int numberType = 0;
+                        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+                            numberType = addrBis.read(3);
+                            addr.ton = numberType;
+
+                            if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK)
+                                addr.numberPlan = addrBis.read(4);
+                        }
+
+                        addr.numberOfDigits = addrBis.read(8);
+
+                        byte[] data = new byte[addr.numberOfDigits];
+                        byte b = 0x00;
+
+                        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF) {
+                            /* As per 3GPP2 C.S0005-0 Table 2.7.1.3.2.4-4 */
+                            for (int index = 0; index < addr.numberOfDigits; index++) {
+                                b = (byte) (0xF & addrBis.read(4));
+                                // convert the value if it is 4-bit DTMF to 8
+                                // bit
+                                data[index] = convertDtmfToAscii(b);
+                            }
+                        } else if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+                            if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK) {
+                                for (int index = 0; index < addr.numberOfDigits; index++) {
+                                    b = (byte) (0xFF & addrBis.read(8));
+                                    data[index] = b;
+                                }
+
+                            } else if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_DATA_NETWORK) {
+                                if (numberType == 2)
+                                    Rlog.e(LOG_TAG, "TODO: Originating Addr is email id");
+                                else
+                                    Rlog.e(LOG_TAG,
+                                          "TODO: Originating Addr is data network address");
+                            } else {
+                                Rlog.e(LOG_TAG, "Originating Addr is of incorrect type");
+                            }
+                        } else {
+                            Rlog.e(LOG_TAG, "Incorrect Digit mode");
+                        }
+                        addr.origBytes = data;
+                        Rlog.i(LOG_TAG, "Originating Addr=" + addr.toString());
+                        break;
+                    case ORIGINATING_SUB_ADDRESS:
+                    case DESTINATION_SUB_ADDRESS:
+                        dis.read(parameterData, 0, parameterLen);
+                        BitwiseInputStream subAddrBis = new BitwiseInputStream(parameterData);
+                        subAddr.type = subAddrBis.read(3);
+                        subAddr.odd = subAddrBis.readByteArray(1)[0];
+                        int subAddrLen = subAddrBis.read(8);
+                        byte[] subdata = new byte[subAddrLen];
+                        for (int index = 0; index < subAddrLen; index++) {
+                            b = (byte) (0xFF & subAddrBis.read(4));
+                            // convert the value if it is 4-bit DTMF to 8 bit
+                            subdata[index] = convertDtmfToAscii(b);
+                        }
+                        subAddr.origBytes = subdata;
+                        break;
+                    case BEARER_REPLY_OPTION:
+                        dis.read(parameterData, 0, parameterLen);
+                        BitwiseInputStream replyOptBis = new BitwiseInputStream(parameterData);
+                        env.bearerReply = replyOptBis.read(6);
+                        break;
+                    case CAUSE_CODES:
+                        dis.read(parameterData, 0, parameterLen);
+                        BitwiseInputStream ccBis = new BitwiseInputStream(parameterData);
+                        env.replySeqNo = ccBis.readByteArray(6)[0];
+                        env.errorClass = ccBis.readByteArray(2)[0];
+                        if (env.errorClass != 0x00)
+                            env.causeCode = ccBis.readByteArray(8)[0];
+                        break;
+                    case BEARER_DATA:
+                        dis.read(parameterData, 0, parameterLen);
+                        env.bearerData = parameterData;
+                        break;
+                    default:
+                        throw new Exception("unsupported parameterId (" + parameterId + ")");
+                }
+            }
+            bais.close();
+            dis.close();
+        } catch (Exception ex) {
+            Rlog.e(LOG_TAG, "parsePduFromEfRecord: conversion from pdu to SmsMessage failed" + ex);
+        }
+
+        // link the filled objects to this SMS
+        mOriginatingAddress = addr;
+        env.origAddress = addr;
+        env.origSubaddress = subAddr;
+        mEnvelope = env;
+        mPdu = pdu;
+
+        parseSms();
+    }
+
+    /**
+     * Parses a SMS message from its BearerData stream. (mobile-terminated only)
+     */
+    public void parseSms() {
+        // Message Waiting Info Record defined in 3GPP2 C.S-0005, 3.7.5.6
+        // It contains only an 8-bit number with the number of messages waiting
+        if (mEnvelope.teleService == SmsEnvelope.TELESERVICE_MWI) {
+            mBearerData = new BearerData();
+            if (mEnvelope.bearerData != null) {
+                mBearerData.numberOfMessages = 0x000000FF & mEnvelope.bearerData[0];
+            }
+            if (VDBG) {
+                Rlog.d(LOG_TAG, "parseSms: get MWI " +
+                      Integer.toString(mBearerData.numberOfMessages));
+            }
+            return;
+        }
+        mBearerData = BearerData.decode(mEnvelope.bearerData);
+        if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) {
+            Rlog.d(LOG_TAG, "MT raw BearerData = '" +
+                      HexDump.toHexString(mEnvelope.bearerData) + "'");
+            Rlog.d(LOG_TAG, "MT (decoded) BearerData = " + mBearerData);
+        }
+        mMessageRef = mBearerData.messageId;
+        if (mBearerData.userData != null) {
+            mUserData = mBearerData.userData.payload;
+            mUserDataHeader = mBearerData.userData.userDataHeader;
+            mMessageBody = mBearerData.userData.payloadStr;
+        }
+
+        if (mOriginatingAddress != null) {
+            mOriginatingAddress.address = new String(mOriginatingAddress.origBytes);
+            if (mOriginatingAddress.ton == CdmaSmsAddress.TON_INTERNATIONAL_OR_IP) {
+                if (mOriginatingAddress.address.charAt(0) != '+') {
+                    mOriginatingAddress.address = "+" + mOriginatingAddress.address;
+                }
+            }
+            if (VDBG) Rlog.v(LOG_TAG, "SMS originating address: "
+                    + mOriginatingAddress.address);
+        }
+
+        if (mBearerData.msgCenterTimeStamp != null) {
+            mScTimeMillis = mBearerData.msgCenterTimeStamp.toMillis(true);
+        }
+
+        if (VDBG) Rlog.d(LOG_TAG, "SMS SC timestamp: " + mScTimeMillis);
+
+        // Message Type (See 3GPP2 C.S0015-B, v2, 4.5.1)
+        if (mBearerData.messageType == BearerData.MESSAGE_TYPE_DELIVERY_ACK) {
+            // The BearerData MsgStatus subparameter should only be
+            // included for DELIVERY_ACK messages.  If it occurred for
+            // other messages, it would be unclear what the status
+            // being reported refers to.  The MsgStatus subparameter
+            // is primarily useful to indicate error conditions -- a
+            // message without this subparameter is assumed to
+            // indicate successful delivery (status == 0).
+            if (! mBearerData.messageStatusSet) {
+                Rlog.d(LOG_TAG, "DELIVERY_ACK message without msgStatus (" +
+                        (mUserData == null ? "also missing" : "does have") +
+                        " userData).");
+                status = 0;
+            } else {
+                status = mBearerData.errorClass << 8;
+                status |= mBearerData.messageStatus;
+            }
+        } else if (mBearerData.messageType != BearerData.MESSAGE_TYPE_DELIVER) {
+            throw new RuntimeException("Unsupported message type: " + mBearerData.messageType);
+        }
+
+        if (mMessageBody != null) {
+            if (VDBG) Rlog.v(LOG_TAG, "SMS message body: '" + mMessageBody + "'");
+            parseMessageBody();
+        } else if ((mUserData != null) && VDBG) {
+            Rlog.v(LOG_TAG, "SMS payload: '" + IccUtils.bytesToHexString(mUserData) + "'");
+        }
+    }
+
+    /**
+     * Parses a broadcast SMS, possibly containing a CMAS alert.
+     */
+    public SmsCbMessage parseBroadcastSms() {
+        BearerData bData = BearerData.decode(mEnvelope.bearerData, mEnvelope.serviceCategory);
+        if (bData == null) {
+            Rlog.w(LOG_TAG, "BearerData.decode() returned null");
+            return null;
+        }
+
+        if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) {
+            Rlog.d(LOG_TAG, "MT raw BearerData = " + HexDump.toHexString(mEnvelope.bearerData));
+        }
+
+        String plmn = TelephonyManager.getDefault().getNetworkOperator();
+        SmsCbLocation location = new SmsCbLocation(plmn);
+
+        return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2,
+                SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, bData.messageId, location,
+                mEnvelope.serviceCategory, bData.getLanguage(), bData.userData.payloadStr,
+                bData.priority, null, bData.cmasWarningInfo);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public SmsConstants.MessageClass getMessageClass() {
+        if (BearerData.DISPLAY_MODE_IMMEDIATE == mBearerData.displayMode ) {
+            return SmsConstants.MessageClass.CLASS_0;
+        } else {
+            return SmsConstants.MessageClass.UNKNOWN;
+        }
+    }
+
+    /**
+     * Calculate the next message id, starting at 1 and iteratively
+     * incrementing within the range 1..65535 remembering the state
+     * via a persistent system property.  (See C.S0015-B, v2.0,
+     * 4.3.1.5) Since this routine is expected to be accessed via via
+     * binder-call, and hence should be thread-safe, it has been
+     * synchronized.
+     */
+    public synchronized static int getNextMessageId() {
+        // Testing and dialog with partners has indicated that
+        // msgId==0 is (sometimes?) treated specially by lower levels.
+        // Specifically, the ID is not preserved for delivery ACKs.
+        // Hence, avoid 0 -- constraining the range to 1..65535.
+        int msgId = SystemProperties.getInt(TelephonyProperties.PROPERTY_CDMA_MSG_ID, 1);
+        String nextMsgId = Integer.toString((msgId % 0xFFFF) + 1);
+        try{
+            SystemProperties.set(TelephonyProperties.PROPERTY_CDMA_MSG_ID, nextMsgId);
+            if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) {
+                Rlog.d(LOG_TAG, "next " + TelephonyProperties.PROPERTY_CDMA_MSG_ID + " = " + nextMsgId);
+                Rlog.d(LOG_TAG, "readback gets " +
+                        SystemProperties.get(TelephonyProperties.PROPERTY_CDMA_MSG_ID));
+            }
+        } catch(RuntimeException ex) {
+            Rlog.e(LOG_TAG, "set nextMessage ID failed: " + ex);
+        }
+        return msgId;
+    }
+
+    /**
+     * Creates BearerData and Envelope from parameters for a Submit SMS.
+     * @return byte stream for SubmitPdu.
+     */
+    private static SubmitPdu privateGetSubmitPdu(String destAddrStr, boolean statusReportRequested,
+            UserData userData) {
+
+        /**
+         * TODO(cleanup): give this function a more meaningful name.
+         */
+
+        /**
+         * TODO(cleanup): Make returning null from the getSubmitPdu
+         * variations meaningful -- clean up the error feedback
+         * mechanism, and avoid null pointer exceptions.
+         */
+
+        /**
+         * North America Plus Code :
+         * Convert + code to 011 and dial out for international SMS
+         */
+        CdmaSmsAddress destAddr = CdmaSmsAddress.parse(
+                PhoneNumberUtils.cdmaCheckAndProcessPlusCodeForSms(destAddrStr));
+        if (destAddr == null) return null;
+
+        BearerData bearerData = new BearerData();
+        bearerData.messageType = BearerData.MESSAGE_TYPE_SUBMIT;
+
+        bearerData.messageId = getNextMessageId();
+
+        bearerData.deliveryAckReq = statusReportRequested;
+        bearerData.userAckReq = false;
+        bearerData.readAckReq = false;
+        bearerData.reportReq = false;
+
+        bearerData.userData = userData;
+
+        byte[] encodedBearerData = BearerData.encode(bearerData);
+        if (Rlog.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) {
+            Rlog.d(LOG_TAG, "MO (encoded) BearerData = " + bearerData);
+            Rlog.d(LOG_TAG, "MO raw BearerData = '" + HexDump.toHexString(encodedBearerData) + "'");
+        }
+        if (encodedBearerData == null) return null;
+
+        int teleservice = bearerData.hasUserDataHeader ?
+                SmsEnvelope.TELESERVICE_WEMT : SmsEnvelope.TELESERVICE_WMT;
+
+        SmsEnvelope envelope = new SmsEnvelope();
+        envelope.messageType = SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT;
+        envelope.teleService = teleservice;
+        envelope.destAddress = destAddr;
+        envelope.bearerReply = RETURN_ACK;
+        envelope.bearerData = encodedBearerData;
+
+        /**
+         * TODO(cleanup): envelope looks to be a pointless class, get
+         * rid of it.  Also -- most of the envelope fields set here
+         * are ignored, why?
+         */
+
+        try {
+            /**
+             * TODO(cleanup): reference a spec and get rid of the ugly comments
+             */
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
+            DataOutputStream dos = new DataOutputStream(baos);
+            dos.writeInt(envelope.teleService);
+            dos.writeInt(0); //servicePresent
+            dos.writeInt(0); //serviceCategory
+            dos.write(destAddr.digitMode);
+            dos.write(destAddr.numberMode);
+            dos.write(destAddr.ton); // number_type
+            dos.write(destAddr.numberPlan);
+            dos.write(destAddr.numberOfDigits);
+            dos.write(destAddr.origBytes, 0, destAddr.origBytes.length); // digits
+            // Subaddress is not supported.
+            dos.write(0); //subaddressType
+            dos.write(0); //subaddr_odd
+            dos.write(0); //subaddr_nbr_of_digits
+            dos.write(encodedBearerData.length);
+            dos.write(encodedBearerData, 0, encodedBearerData.length);
+            dos.close();
+
+            SubmitPdu pdu = new SubmitPdu();
+            pdu.encodedMessage = baos.toByteArray();
+            pdu.encodedScAddress = null;
+            return pdu;
+        } catch(IOException ex) {
+            Rlog.e(LOG_TAG, "creating SubmitPdu failed: " + ex);
+        }
+        return null;
+    }
+
+    /**
+     * Creates byte array (pseudo pdu) from SMS object.
+     * Note: Do not call this method more than once per object!
+     * @hide
+     */
+    public void createPdu() {
+        SmsEnvelope env = mEnvelope;
+        CdmaSmsAddress addr = env.origAddress;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
+        DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(baos));
+
+        try {
+            dos.writeInt(env.messageType);
+            dos.writeInt(env.teleService);
+            dos.writeInt(env.serviceCategory);
+
+            dos.writeByte(addr.digitMode);
+            dos.writeByte(addr.numberMode);
+            dos.writeByte(addr.ton);
+            dos.writeByte(addr.numberPlan);
+            dos.writeByte(addr.numberOfDigits);
+            dos.write(addr.origBytes, 0, addr.origBytes.length); // digits
+
+            dos.writeInt(env.bearerReply);
+            // CauseCode values:
+            dos.writeByte(env.replySeqNo);
+            dos.writeByte(env.errorClass);
+            dos.writeByte(env.causeCode);
+            //encoded BearerData:
+            dos.writeInt(env.bearerData.length);
+            dos.write(env.bearerData, 0, env.bearerData.length);
+            dos.close();
+
+            /**
+             * TODO(cleanup) -- The mPdu field is managed in
+             * a fragile manner, and it would be much nicer if
+             * accessing the serialized representation used a less
+             * fragile mechanism.  Maybe the getPdu method could
+             * generate a representation if there was not yet one?
+             */
+
+            mPdu = baos.toByteArray();
+        } catch (IOException ex) {
+            Rlog.e(LOG_TAG, "createPdu: conversion from object to byte array failed: " + ex);
+        }
+    }
+
+    /**
+     * Converts a 4-Bit DTMF encoded symbol from the calling address number to ASCII character
+     * @hide
+     */
+    public static byte convertDtmfToAscii(byte dtmfDigit) {
+        byte asciiDigit;
+
+        switch (dtmfDigit) {
+        case  0: asciiDigit = 68; break; // 'D'
+        case  1: asciiDigit = 49; break; // '1'
+        case  2: asciiDigit = 50; break; // '2'
+        case  3: asciiDigit = 51; break; // '3'
+        case  4: asciiDigit = 52; break; // '4'
+        case  5: asciiDigit = 53; break; // '5'
+        case  6: asciiDigit = 54; break; // '6'
+        case  7: asciiDigit = 55; break; // '7'
+        case  8: asciiDigit = 56; break; // '8'
+        case  9: asciiDigit = 57; break; // '9'
+        case 10: asciiDigit = 48; break; // '0'
+        case 11: asciiDigit = 42; break; // '*'
+        case 12: asciiDigit = 35; break; // '#'
+        case 13: asciiDigit = 65; break; // 'A'
+        case 14: asciiDigit = 66; break; // 'B'
+        case 15: asciiDigit = 67; break; // 'C'
+        default:
+            asciiDigit = 32; // Invalid DTMF code
+            break;
+        }
+
+        return asciiDigit;
+    }
+
+    /** This function  shall be called to get the number of voicemails.
+     * @hide
+     */
+    public int getNumOfVoicemails() {
+        return mBearerData.numberOfMessages;
+    }
+
+    /**
+     * Returns a byte array that can be use to uniquely identify a received SMS message.
+     * C.S0015-B  4.3.1.6 Unique Message Identification.
+     *
+     * @return byte array uniquely identifying the message.
+     * @hide
+     */
+    public byte[] getIncomingSmsFingerprint() {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        output.write(mEnvelope.serviceCategory);
+        output.write(mEnvelope.teleService);
+        output.write(mEnvelope.origAddress.origBytes, 0, mEnvelope.origAddress.origBytes.length);
+        output.write(mEnvelope.bearerData, 0, mEnvelope.bearerData.length);
+        output.write(mEnvelope.origSubaddress.origBytes, 0,
+                mEnvelope.origSubaddress.origBytes.length);
+
+        return output.toByteArray();
+    }
+
+    /**
+     * Returns the list of service category program data, if present.
+     * @return a list of CdmaSmsCbProgramData objects, or null if not present
+     * @hide
+     */
+    public ArrayList<CdmaSmsCbProgramData> getSmsCbProgramData() {
+        return mBearerData.serviceCategoryProgramData;
+    }
+}
diff --git a/com/android/internal/telephony/cdma/SmsMessageConverter.java b/com/android/internal/telephony/cdma/SmsMessageConverter.java
new file mode 100644
index 0000000..4ac2dea
--- /dev/null
+++ b/com/android/internal/telephony/cdma/SmsMessageConverter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma;
+
+import android.hardware.radio.V1_0.CdmaSmsMessage;
+import android.os.Parcel;
+import android.os.SystemProperties;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.telephony.Rlog;
+import android.util.Log;
+import android.text.TextUtils;
+import android.content.res.Resources;
+
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.cdma.sms.BearerData;
+import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
+import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress;
+import com.android.internal.telephony.cdma.sms.SmsEnvelope;
+import com.android.internal.telephony.cdma.sms.UserData;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.util.BitwiseInputStream;
+import com.android.internal.util.HexDump;
+import com.android.internal.telephony.Sms7BitEncodingTranslator;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * A Factory class to convert from RIL to Framework SMS
+ *
+ */
+public class SmsMessageConverter {
+    static final String LOG_TAG = "SmsMessageConverter";
+    static private final String LOGGABLE_TAG = "CDMA:SMS";
+    private static final boolean VDBG = false;
+
+    /**
+     *  Create a "raw" CDMA SmsMessage from a Parcel that was forged in ril.cpp.
+     *  Note: Only primitive fields are set.
+     */
+    public static SmsMessage newCdmaSmsMessageFromRil(
+            CdmaSmsMessage cdmaSmsMessage) {
+        // Note: Parcel.readByte actually reads one Int and masks to byte
+        SmsEnvelope env = new SmsEnvelope();
+        CdmaSmsAddress addr = new CdmaSmsAddress();
+        CdmaSmsSubaddress subaddr = new CdmaSmsSubaddress();
+        byte[] data;
+        byte count;
+        int countInt;
+        int addressDigitMode;
+
+        //currently not supported by the modem-lib: env.mMessageType
+        env.teleService = cdmaSmsMessage.teleserviceId;
+
+        if (cdmaSmsMessage.isServicePresent) {
+            env.messageType = SmsEnvelope.MESSAGE_TYPE_BROADCAST;
+        }
+        else {
+            if (SmsEnvelope.TELESERVICE_NOT_SET == env.teleService) {
+                // assume type ACK
+                env.messageType = SmsEnvelope.MESSAGE_TYPE_ACKNOWLEDGE;
+            } else {
+                env.messageType = SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT;
+            }
+        }
+        env.serviceCategory = cdmaSmsMessage.serviceCategory;
+
+        // address
+        addressDigitMode = cdmaSmsMessage.address.digitMode;
+        addr.digitMode = (byte) (0xFF & addressDigitMode);
+        addr.numberMode = (byte) (0xFF & cdmaSmsMessage.address.numberMode);
+        addr.ton = cdmaSmsMessage.address.numberType;
+        addr.numberPlan = (byte) (0xFF & cdmaSmsMessage.address.numberPlan);
+        count = (byte) cdmaSmsMessage.address.digits.size();
+        addr.numberOfDigits = count;
+        data = new byte[count];
+        for (int index=0; index < count; index++) {
+            data[index] = cdmaSmsMessage.address.digits.get(index);
+
+            // convert the value if it is 4-bit DTMF to 8 bit
+            if (addressDigitMode == CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF) {
+                data[index] = SmsMessage.convertDtmfToAscii(data[index]);
+            }
+        }
+
+        addr.origBytes = data;
+
+        subaddr.type = cdmaSmsMessage.subAddress.subaddressType;
+        subaddr.odd = (byte) (cdmaSmsMessage.subAddress.odd ? 1 : 0);
+        count = (byte) cdmaSmsMessage.subAddress.digits.size();
+
+        if (count < 0) {
+            count = 0;
+        }
+
+        // p_cur->sSubAddress.digits[digitCount] :
+
+        data = new byte[count];
+
+        for (int index = 0; index < count; ++index) {
+            data[index] = cdmaSmsMessage.subAddress.digits.get(index);
+        }
+
+        subaddr.origBytes = data;
+
+        /* currently not supported by the modem-lib:
+            env.bearerReply
+            env.replySeqNo
+            env.errorClass
+            env.causeCode
+        */
+
+        // bearer data
+        countInt = cdmaSmsMessage.bearerData.size();
+        if (countInt < 0) {
+            countInt = 0;
+        }
+
+        data = new byte[countInt];
+        for (int index=0; index < countInt; index++) {
+            data[index] = cdmaSmsMessage.bearerData.get(index);
+        }
+        // BD gets further decoded when accessed in SMSDispatcher
+        env.bearerData = data;
+
+        // link the the filled objects to the SMS
+        env.origAddress = addr;
+        env.origSubaddress = subaddr;
+
+        SmsMessage msg = new SmsMessage(addr, env);
+
+        return msg;
+    }
+
+    public static android.telephony.SmsMessage newSmsMessageFromCdmaSmsMessage(
+            CdmaSmsMessage msg) {
+        return new android.telephony.SmsMessage((SmsMessageBase)newCdmaSmsMessageFromRil(msg));
+    }
+}
diff --git a/com/android/internal/telephony/cdma/sms/BearerData.java b/com/android/internal/telephony/cdma/sms/BearerData.java
new file mode 100644
index 0000000..a4cd56b
--- /dev/null
+++ b/com/android/internal/telephony/cdma/sms/BearerData.java
@@ -0,0 +1,2025 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma.sms;
+
+import android.content.res.Resources;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.telephony.cdma.CdmaSmsCbProgramResults;
+import android.text.format.Time;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.util.BitwiseInputStream;
+import com.android.internal.util.BitwiseOutputStream;
+
+import java.util.ArrayList;
+import java.util.TimeZone;
+
+/**
+ * An object to encode and decode CDMA SMS bearer data.
+ */
+public final class BearerData {
+    private final static String LOG_TAG = "BearerData";
+
+    /**
+     * Bearer Data Subparameter Identifiers
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5-1)
+     * NOTE: Commented subparameter types are not implemented.
+     */
+    private final static byte SUBPARAM_MESSAGE_IDENTIFIER               = 0x00;
+    private final static byte SUBPARAM_USER_DATA                        = 0x01;
+    private final static byte SUBPARAM_USER_RESPONSE_CODE               = 0x02;
+    private final static byte SUBPARAM_MESSAGE_CENTER_TIME_STAMP        = 0x03;
+    private final static byte SUBPARAM_VALIDITY_PERIOD_ABSOLUTE         = 0x04;
+    private final static byte SUBPARAM_VALIDITY_PERIOD_RELATIVE         = 0x05;
+    private final static byte SUBPARAM_DEFERRED_DELIVERY_TIME_ABSOLUTE  = 0x06;
+    private final static byte SUBPARAM_DEFERRED_DELIVERY_TIME_RELATIVE  = 0x07;
+    private final static byte SUBPARAM_PRIORITY_INDICATOR               = 0x08;
+    private final static byte SUBPARAM_PRIVACY_INDICATOR                = 0x09;
+    private final static byte SUBPARAM_REPLY_OPTION                     = 0x0A;
+    private final static byte SUBPARAM_NUMBER_OF_MESSAGES               = 0x0B;
+    private final static byte SUBPARAM_ALERT_ON_MESSAGE_DELIVERY        = 0x0C;
+    private final static byte SUBPARAM_LANGUAGE_INDICATOR               = 0x0D;
+    private final static byte SUBPARAM_CALLBACK_NUMBER                  = 0x0E;
+    private final static byte SUBPARAM_MESSAGE_DISPLAY_MODE             = 0x0F;
+    //private final static byte SUBPARAM_MULTIPLE_ENCODING_USER_DATA      = 0x10;
+    private final static byte SUBPARAM_MESSAGE_DEPOSIT_INDEX            = 0x11;
+    private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA    = 0x12;
+    private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS = 0x13;
+    private final static byte SUBPARAM_MESSAGE_STATUS                   = 0x14;
+    //private final static byte SUBPARAM_TP_FAILURE_CAUSE                 = 0x15;
+    //private final static byte SUBPARAM_ENHANCED_VMN                     = 0x16;
+    //private final static byte SUBPARAM_ENHANCED_VMN_ACK                 = 0x17;
+
+    // All other values after this are reserved.
+    private final static byte SUBPARAM_ID_LAST_DEFINED                    = 0x17;
+
+    /**
+     * Supported message types for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1)
+     */
+    public static final int MESSAGE_TYPE_DELIVER        = 0x01;
+    public static final int MESSAGE_TYPE_SUBMIT         = 0x02;
+    public static final int MESSAGE_TYPE_CANCELLATION   = 0x03;
+    public static final int MESSAGE_TYPE_DELIVERY_ACK   = 0x04;
+    public static final int MESSAGE_TYPE_USER_ACK       = 0x05;
+    public static final int MESSAGE_TYPE_READ_ACK       = 0x06;
+    public static final int MESSAGE_TYPE_DELIVER_REPORT = 0x07;
+    public static final int MESSAGE_TYPE_SUBMIT_REPORT  = 0x08;
+
+    public int messageType;
+
+    /**
+     * 16-bit value indicating the message ID, which increments modulo 65536.
+     * (Special rules apply for WAP-messages.)
+     * (See 3GPP2 C.S0015-B, v2, 4.5.1)
+     */
+    public int messageId;
+
+    /**
+     * Supported priority modes for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1)
+     */
+    public static final int PRIORITY_NORMAL        = 0x0;
+    public static final int PRIORITY_INTERACTIVE   = 0x1;
+    public static final int PRIORITY_URGENT        = 0x2;
+    public static final int PRIORITY_EMERGENCY     = 0x3;
+
+    public boolean priorityIndicatorSet = false;
+    public int priority = PRIORITY_NORMAL;
+
+    /**
+     * Supported privacy modes for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.10-1)
+     */
+    public static final int PRIVACY_NOT_RESTRICTED = 0x0;
+    public static final int PRIVACY_RESTRICTED     = 0x1;
+    public static final int PRIVACY_CONFIDENTIAL   = 0x2;
+    public static final int PRIVACY_SECRET         = 0x3;
+
+    public boolean privacyIndicatorSet = false;
+    public int privacy = PRIVACY_NOT_RESTRICTED;
+
+    /**
+     * Supported alert priority modes for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.13-1)
+     */
+    public static final int ALERT_DEFAULT          = 0x0;
+    public static final int ALERT_LOW_PRIO         = 0x1;
+    public static final int ALERT_MEDIUM_PRIO      = 0x2;
+    public static final int ALERT_HIGH_PRIO        = 0x3;
+
+    public boolean alertIndicatorSet = false;
+    public int alert = ALERT_DEFAULT;
+
+    /**
+     * Supported display modes for CDMA SMS messages.  Display mode is
+     * a 2-bit value used to indicate to the mobile station when to
+     * display the received message.  (See 3GPP2 C.S0015-B, v2,
+     * 4.5.16)
+     */
+    public static final int DISPLAY_MODE_IMMEDIATE      = 0x0;
+    public static final int DISPLAY_MODE_DEFAULT        = 0x1;
+    public static final int DISPLAY_MODE_USER           = 0x2;
+
+    public boolean displayModeSet = false;
+    public int displayMode = DISPLAY_MODE_DEFAULT;
+
+    /**
+     * Language Indicator values.  NOTE: the spec (3GPP2 C.S0015-B,
+     * v2, 4.5.14) is ambiguous as to the meaning of this field, as it
+     * refers to C.R1001-D but that reference has been crossed out.
+     * It would seem reasonable to assume the values from C.R1001-F
+     * (table 9.2-1) are to be used instead.
+     */
+    public static final int LANGUAGE_UNKNOWN  = 0x00;
+    public static final int LANGUAGE_ENGLISH  = 0x01;
+    public static final int LANGUAGE_FRENCH   = 0x02;
+    public static final int LANGUAGE_SPANISH  = 0x03;
+    public static final int LANGUAGE_JAPANESE = 0x04;
+    public static final int LANGUAGE_KOREAN   = 0x05;
+    public static final int LANGUAGE_CHINESE  = 0x06;
+    public static final int LANGUAGE_HEBREW   = 0x07;
+
+    public boolean languageIndicatorSet = false;
+    public int language = LANGUAGE_UNKNOWN;
+
+    /**
+     * SMS Message Status Codes.  The first component of the Message
+     * status indicates if an error has occurred and whether the error
+     * is considered permanent or temporary.  The second component of
+     * the Message status indicates the cause of the error (if any).
+     * (See 3GPP2 C.S0015-B, v2.0, 4.5.21)
+     */
+    /* no-error codes */
+    public static final int ERROR_NONE                   = 0x00;
+    public static final int STATUS_ACCEPTED              = 0x00;
+    public static final int STATUS_DEPOSITED_TO_INTERNET = 0x01;
+    public static final int STATUS_DELIVERED             = 0x02;
+    public static final int STATUS_CANCELLED             = 0x03;
+    /* temporary-error and permanent-error codes */
+    public static final int ERROR_TEMPORARY              = 0x02;
+    public static final int STATUS_NETWORK_CONGESTION    = 0x04;
+    public static final int STATUS_NETWORK_ERROR         = 0x05;
+    public static final int STATUS_UNKNOWN_ERROR         = 0x1F;
+    /* permanent-error codes */
+    public static final int ERROR_PERMANENT              = 0x03;
+    public static final int STATUS_CANCEL_FAILED         = 0x06;
+    public static final int STATUS_BLOCKED_DESTINATION   = 0x07;
+    public static final int STATUS_TEXT_TOO_LONG         = 0x08;
+    public static final int STATUS_DUPLICATE_MESSAGE     = 0x09;
+    public static final int STATUS_INVALID_DESTINATION   = 0x0A;
+    public static final int STATUS_MESSAGE_EXPIRED       = 0x0D;
+    /* undefined-status codes */
+    public static final int ERROR_UNDEFINED              = 0xFF;
+    public static final int STATUS_UNDEFINED             = 0xFF;
+
+    public boolean messageStatusSet = false;
+    public int errorClass = ERROR_UNDEFINED;
+    public int messageStatus = STATUS_UNDEFINED;
+
+    /**
+     * 1-bit value that indicates whether a User Data Header (UDH) is present.
+     * (See 3GPP2 C.S0015-B, v2, 4.5.1)
+     *
+     * NOTE: during encoding, this value will be set based on the
+     * presence of a UDH in the structured data, any existing setting
+     * will be overwritten.
+     */
+    public boolean hasUserDataHeader;
+
+    /**
+     * provides the information for the user data
+     * (e.g. padding bits, user data, user data header, etc)
+     * (See 3GPP2 C.S.0015-B, v2, 4.5.2)
+     */
+    public UserData userData;
+
+    /**
+     * The User Response Code subparameter is used in the SMS User
+     * Acknowledgment Message to respond to previously received short
+     * messages. This message center-specific element carries the
+     * identifier of a predefined response. (See 3GPP2 C.S.0015-B, v2,
+     * 4.5.3)
+     */
+    public boolean userResponseCodeSet = false;
+    public int userResponseCode;
+
+    /**
+     * 6-byte-field, see 3GPP2 C.S0015-B, v2, 4.5.4
+     */
+    public static class TimeStamp extends Time {
+
+        public TimeStamp() {
+            super(TimeZone.getDefault().getID());   // 3GPP2 timestamps use the local timezone
+        }
+
+        public static TimeStamp fromByteArray(byte[] data) {
+            TimeStamp ts = new TimeStamp();
+            // C.S0015-B v2.0, 4.5.4: range is 1996-2095
+            int year = IccUtils.cdmaBcdByteToInt(data[0]);
+            if (year > 99 || year < 0) return null;
+            ts.year = year >= 96 ? year + 1900 : year + 2000;
+            int month = IccUtils.cdmaBcdByteToInt(data[1]);
+            if (month < 1 || month > 12) return null;
+            ts.month = month - 1;
+            int day = IccUtils.cdmaBcdByteToInt(data[2]);
+            if (day < 1 || day > 31) return null;
+            ts.monthDay = day;
+            int hour = IccUtils.cdmaBcdByteToInt(data[3]);
+            if (hour < 0 || hour > 23) return null;
+            ts.hour = hour;
+            int minute = IccUtils.cdmaBcdByteToInt(data[4]);
+            if (minute < 0 || minute > 59) return null;
+            ts.minute = minute;
+            int second = IccUtils.cdmaBcdByteToInt(data[5]);
+            if (second < 0 || second > 59) return null;
+            ts.second = second;
+            return ts;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("TimeStamp ");
+            builder.append("{ year=" + year);
+            builder.append(", month=" + month);
+            builder.append(", day=" + monthDay);
+            builder.append(", hour=" + hour);
+            builder.append(", minute=" + minute);
+            builder.append(", second=" + second);
+            builder.append(" }");
+            return builder.toString();
+        }
+    }
+
+    public TimeStamp msgCenterTimeStamp;
+    public TimeStamp validityPeriodAbsolute;
+    public TimeStamp deferredDeliveryTimeAbsolute;
+
+    /**
+     * Relative time is specified as one byte, the value of which
+     * falls into a series of ranges, as specified below.  The idea is
+     * that shorter time intervals allow greater precision -- the
+     * value means minutes from zero until the MINS_LIMIT (inclusive),
+     * upon which it means hours until the HOURS_LIMIT, and so
+     * forth. (See 3GPP2 C.S0015-B, v2, 4.5.6-1)
+     */
+    public static final int RELATIVE_TIME_MINS_LIMIT      = 143;
+    public static final int RELATIVE_TIME_HOURS_LIMIT     = 167;
+    public static final int RELATIVE_TIME_DAYS_LIMIT      = 196;
+    public static final int RELATIVE_TIME_WEEKS_LIMIT     = 244;
+    public static final int RELATIVE_TIME_INDEFINITE      = 245;
+    public static final int RELATIVE_TIME_NOW             = 246;
+    public static final int RELATIVE_TIME_MOBILE_INACTIVE = 247;
+    public static final int RELATIVE_TIME_RESERVED        = 248;
+
+    public boolean validityPeriodRelativeSet;
+    public int validityPeriodRelative;
+    public boolean deferredDeliveryTimeRelativeSet;
+    public int deferredDeliveryTimeRelative;
+
+    /**
+     * The Reply Option subparameter contains 1-bit values which
+     * indicate whether SMS acknowledgment is requested or not.  (See
+     * 3GPP2 C.S0015-B, v2, 4.5.11)
+     */
+    public boolean userAckReq;
+    public boolean deliveryAckReq;
+    public boolean readAckReq;
+    public boolean reportReq;
+
+    /**
+     * The Number of Messages subparameter (8-bit value) is a decimal
+     * number in the 0 to 99 range representing the number of messages
+     * stored at the Voice Mail System. This element is used by the
+     * Voice Mail Notification service.  (See 3GPP2 C.S0015-B, v2,
+     * 4.5.12)
+     */
+    public int numberOfMessages;
+
+    /**
+     * The Message Deposit Index subparameter is assigned by the
+     * message center as a unique index to the contents of the User
+     * Data subparameter in each message sent to a particular mobile
+     * station. The mobile station, when replying to a previously
+     * received short message which included a Message Deposit Index
+     * subparameter, may include the Message Deposit Index of the
+     * received message to indicate to the message center that the
+     * original contents of the message are to be included in the
+     * reply.  (See 3GPP2 C.S0015-B, v2, 4.5.18)
+     */
+    public int depositIndex;
+
+    /**
+     * 4-bit or 8-bit value that indicates the number to be dialed in reply to a
+     * received SMS message.
+     * (See 3GPP2 C.S0015-B, v2, 4.5.15)
+     */
+    public CdmaSmsAddress callbackNumber;
+
+    /**
+     * CMAS warning notification information.
+     * @see #decodeCmasUserData(BearerData, int)
+     */
+    public SmsCbCmasInfo cmasWarningInfo;
+
+    /**
+     * The Service Category Program Data subparameter is used to enable and disable
+     * SMS broadcast service categories to display. If this subparameter is present,
+     * this field will contain a list of one or more
+     * {@link android.telephony.cdma.CdmaSmsCbProgramData} objects containing the
+     * operation(s) to perform.
+     */
+    public ArrayList<CdmaSmsCbProgramData> serviceCategoryProgramData;
+
+    /**
+     * The Service Category Program Results subparameter informs the message center
+     * of the results of a Service Category Program Data request.
+     */
+    public ArrayList<CdmaSmsCbProgramResults> serviceCategoryProgramResults;
+
+
+    private static class CodingException extends Exception {
+        public CodingException(String s) {
+            super(s);
+        }
+    }
+
+    /**
+     * Returns the language indicator as a two-character ISO 639 string.
+     * @return a two character ISO 639 language code
+     */
+    public String getLanguage() {
+        return getLanguageCodeForValue(language);
+    }
+
+    /**
+     * Converts a CDMA language indicator value to an ISO 639 two character language code.
+     * @param languageValue the CDMA language value to convert
+     * @return the two character ISO 639 language code for the specified value, or null if unknown
+     */
+    private static String getLanguageCodeForValue(int languageValue) {
+        switch (languageValue) {
+            case LANGUAGE_ENGLISH:
+                return "en";
+
+            case LANGUAGE_FRENCH:
+                return "fr";
+
+            case LANGUAGE_SPANISH:
+                return "es";
+
+            case LANGUAGE_JAPANESE:
+                return "ja";
+
+            case LANGUAGE_KOREAN:
+                return "ko";
+
+            case LANGUAGE_CHINESE:
+                return "zh";
+
+            case LANGUAGE_HEBREW:
+                return "he";
+
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("BearerData ");
+        builder.append("{ messageType=" + messageType);
+        builder.append(", messageId=" + messageId);
+        builder.append(", priority=" + (priorityIndicatorSet ? priority : "unset"));
+        builder.append(", privacy=" + (privacyIndicatorSet ? privacy : "unset"));
+        builder.append(", alert=" + (alertIndicatorSet ? alert : "unset"));
+        builder.append(", displayMode=" + (displayModeSet ? displayMode : "unset"));
+        builder.append(", language=" + (languageIndicatorSet ? language : "unset"));
+        builder.append(", errorClass=" + (messageStatusSet ? errorClass : "unset"));
+        builder.append(", msgStatus=" + (messageStatusSet ? messageStatus : "unset"));
+        builder.append(", msgCenterTimeStamp=" +
+                ((msgCenterTimeStamp != null) ? msgCenterTimeStamp : "unset"));
+        builder.append(", validityPeriodAbsolute=" +
+                ((validityPeriodAbsolute != null) ? validityPeriodAbsolute : "unset"));
+        builder.append(", validityPeriodRelative=" +
+                ((validityPeriodRelativeSet) ? validityPeriodRelative : "unset"));
+        builder.append(", deferredDeliveryTimeAbsolute=" +
+                ((deferredDeliveryTimeAbsolute != null) ? deferredDeliveryTimeAbsolute : "unset"));
+        builder.append(", deferredDeliveryTimeRelative=" +
+                ((deferredDeliveryTimeRelativeSet) ? deferredDeliveryTimeRelative : "unset"));
+        builder.append(", userAckReq=" + userAckReq);
+        builder.append(", deliveryAckReq=" + deliveryAckReq);
+        builder.append(", readAckReq=" + readAckReq);
+        builder.append(", reportReq=" + reportReq);
+        builder.append(", numberOfMessages=" + numberOfMessages);
+        builder.append(", callbackNumber=" + Rlog.pii(LOG_TAG, callbackNumber));
+        builder.append(", depositIndex=" + depositIndex);
+        builder.append(", hasUserDataHeader=" + hasUserDataHeader);
+        builder.append(", userData=" + userData);
+        builder.append(" }");
+        return builder.toString();
+    }
+
+    private static void encodeMessageId(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 3);
+        outStream.write(4, bData.messageType);
+        outStream.write(8, bData.messageId >> 8);
+        outStream.write(8, bData.messageId);
+        outStream.write(1, bData.hasUserDataHeader ? 1 : 0);
+        outStream.skip(3);
+    }
+
+    private static int countAsciiSeptets(CharSequence msg, boolean force) {
+        int msgLen = msg.length();
+        if (force) return msgLen;
+        for (int i = 0; i < msgLen; i++) {
+            if (UserData.charToAscii.get(msg.charAt(i), -1) == -1) {
+                return -1;
+            }
+        }
+        return msgLen;
+    }
+
+    /**
+     * Calculate the message text encoding length, fragmentation, and other details.
+     *
+     * @param msg message text
+     * @param force7BitEncoding ignore (but still count) illegal characters if true
+     * @param isEntireMsg indicates if this is entire msg or a segment in multipart msg
+     * @return septet count, or -1 on failure
+     */
+    public static TextEncodingDetails calcTextEncodingDetails(CharSequence msg,
+            boolean force7BitEncoding, boolean isEntireMsg) {
+        TextEncodingDetails ted;
+        int septets = countAsciiSeptets(msg, force7BitEncoding);
+        if (septets != -1 && septets <= SmsConstants.MAX_USER_DATA_SEPTETS) {
+            ted = new TextEncodingDetails();
+            ted.msgCount = 1;
+            ted.codeUnitCount = septets;
+            ted.codeUnitsRemaining = SmsConstants.MAX_USER_DATA_SEPTETS - septets;
+            ted.codeUnitSize = SmsConstants.ENCODING_7BIT;
+        } else {
+            ted = com.android.internal.telephony.gsm.SmsMessage.calculateLength(
+                    msg, force7BitEncoding);
+            if (ted.msgCount == 1 && ted.codeUnitSize == SmsConstants.ENCODING_7BIT &&
+                    isEntireMsg) {
+                // We don't support single-segment EMS, so calculate for 16-bit
+                // TODO: Consider supporting single-segment EMS
+                return SmsMessageBase.calcUnicodeEncodingDetails(msg);
+            }
+        }
+        return ted;
+    }
+
+    private static byte[] encode7bitAscii(String msg, boolean force)
+        throws CodingException
+    {
+        try {
+            BitwiseOutputStream outStream = new BitwiseOutputStream(msg.length());
+            int msgLen = msg.length();
+            for (int i = 0; i < msgLen; i++) {
+                int charCode = UserData.charToAscii.get(msg.charAt(i), -1);
+                if (charCode == -1) {
+                    if (force) {
+                        outStream.write(7, UserData.UNENCODABLE_7_BIT_CHAR);
+                    } else {
+                        throw new CodingException("cannot ASCII encode (" + msg.charAt(i) + ")");
+                    }
+                } else {
+                    outStream.write(7, charCode);
+                }
+            }
+            return outStream.toByteArray();
+        } catch (BitwiseOutputStream.AccessException ex) {
+            throw new CodingException("7bit ASCII encode failed: " + ex);
+        }
+    }
+
+    private static byte[] encodeUtf16(String msg)
+        throws CodingException
+    {
+        try {
+            return msg.getBytes("utf-16be");
+        } catch (java.io.UnsupportedEncodingException ex) {
+            throw new CodingException("UTF-16 encode failed: " + ex);
+        }
+    }
+
+    private static class Gsm7bitCodingResult {
+        int septets;
+        byte[] data;
+    }
+
+    private static Gsm7bitCodingResult encode7bitGsm(String msg, int septetOffset, boolean force)
+        throws CodingException
+    {
+        try {
+            /*
+             * TODO(cleanup): It would be nice if GsmAlphabet provided
+             * an option to produce just the data without prepending
+             * the septet count, as this function is really just a
+             * wrapper to strip that off.  Not to mention that the
+             * septet count is generally known prior to invocation of
+             * the encoder.  Note that it cannot be derived from the
+             * resulting array length, since that cannot distinguish
+             * if the last contains either 1 or 8 valid bits.
+             *
+             * TODO(cleanup): The BitwiseXStreams could also be
+             * extended with byte-wise reversed endianness read/write
+             * routines to allow a corresponding implementation of
+             * stringToGsm7BitPacked, and potentially directly support
+             * access to the main bitwise stream from encode/decode.
+             */
+            byte[] fullData = GsmAlphabet.stringToGsm7BitPacked(msg, septetOffset, !force, 0, 0);
+            Gsm7bitCodingResult result = new Gsm7bitCodingResult();
+            result.data = new byte[fullData.length - 1];
+            System.arraycopy(fullData, 1, result.data, 0, fullData.length - 1);
+            result.septets = fullData[0] & 0x00FF;
+            return result;
+        } catch (com.android.internal.telephony.EncodeException ex) {
+            throw new CodingException("7bit GSM encode failed: " + ex);
+        }
+    }
+
+    private static void encode7bitEms(UserData uData, byte[] udhData, boolean force)
+        throws CodingException
+    {
+        int udhBytes = udhData.length + 1;  // Add length octet.
+        int udhSeptets = ((udhBytes * 8) + 6) / 7;
+        Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, udhSeptets, force);
+        uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
+        uData.msgEncodingSet = true;
+        uData.numFields = gcr.septets;
+        uData.payload = gcr.data;
+        uData.payload[0] = (byte)udhData.length;
+        System.arraycopy(udhData, 0, uData.payload, 1, udhData.length);
+    }
+
+    private static void encode16bitEms(UserData uData, byte[] udhData)
+        throws CodingException
+    {
+        byte[] payload = encodeUtf16(uData.payloadStr);
+        int udhBytes = udhData.length + 1;  // Add length octet.
+        int udhCodeUnits = (udhBytes + 1) / 2;
+        int payloadCodeUnits = payload.length / 2;
+        uData.msgEncoding = UserData.ENCODING_UNICODE_16;
+        uData.msgEncodingSet = true;
+        uData.numFields = udhCodeUnits + payloadCodeUnits;
+        uData.payload = new byte[uData.numFields * 2];
+        uData.payload[0] = (byte)udhData.length;
+        System.arraycopy(udhData, 0, uData.payload, 1, udhData.length);
+        System.arraycopy(payload, 0, uData.payload, udhBytes, payload.length);
+    }
+
+    private static void encodeEmsUserDataPayload(UserData uData)
+        throws CodingException
+    {
+        byte[] headerData = SmsHeader.toByteArray(uData.userDataHeader);
+        if (uData.msgEncodingSet) {
+            if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) {
+                encode7bitEms(uData, headerData, true);
+            } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) {
+                encode16bitEms(uData, headerData);
+            } else {
+                throw new CodingException("unsupported EMS user data encoding (" +
+                                          uData.msgEncoding + ")");
+            }
+        } else {
+            try {
+                encode7bitEms(uData, headerData, false);
+            } catch (CodingException ex) {
+                encode16bitEms(uData, headerData);
+            }
+        }
+    }
+
+    private static byte[] encodeShiftJis(String msg) throws CodingException {
+        try {
+            return msg.getBytes("Shift_JIS");
+        } catch (java.io.UnsupportedEncodingException ex) {
+            throw new CodingException("Shift-JIS encode failed: " + ex);
+        }
+    }
+
+    private static void encodeUserDataPayload(UserData uData)
+        throws CodingException
+    {
+        if ((uData.payloadStr == null) && (uData.msgEncoding != UserData.ENCODING_OCTET)) {
+            Rlog.e(LOG_TAG, "user data with null payloadStr");
+            uData.payloadStr = "";
+        }
+
+        if (uData.userDataHeader != null) {
+            encodeEmsUserDataPayload(uData);
+            return;
+        }
+
+        if (uData.msgEncodingSet) {
+            if (uData.msgEncoding == UserData.ENCODING_OCTET) {
+                if (uData.payload == null) {
+                    Rlog.e(LOG_TAG, "user data with octet encoding but null payload");
+                    uData.payload = new byte[0];
+                    uData.numFields = 0;
+                } else {
+                    uData.numFields = uData.payload.length;
+                }
+            } else {
+                if (uData.payloadStr == null) {
+                    Rlog.e(LOG_TAG, "non-octet user data with null payloadStr");
+                    uData.payloadStr = "";
+                }
+                if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) {
+                    Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, 0, true);
+                    uData.payload = gcr.data;
+                    uData.numFields = gcr.septets;
+                } else if (uData.msgEncoding == UserData.ENCODING_7BIT_ASCII) {
+                    uData.payload = encode7bitAscii(uData.payloadStr, true);
+                    uData.numFields = uData.payloadStr.length();
+                } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) {
+                    uData.payload = encodeUtf16(uData.payloadStr);
+                    uData.numFields = uData.payloadStr.length();
+                } else if (uData.msgEncoding == UserData.ENCODING_SHIFT_JIS) {
+                    uData.payload = encodeShiftJis(uData.payloadStr);
+                    uData.numFields = uData.payload.length;
+                } else {
+                    throw new CodingException("unsupported user data encoding (" +
+                                              uData.msgEncoding + ")");
+                }
+            }
+        } else {
+            try {
+                uData.payload = encode7bitAscii(uData.payloadStr, false);
+                uData.msgEncoding = UserData.ENCODING_7BIT_ASCII;
+            } catch (CodingException ex) {
+                uData.payload = encodeUtf16(uData.payloadStr);
+                uData.msgEncoding = UserData.ENCODING_UNICODE_16;
+            }
+            uData.numFields = uData.payloadStr.length();
+            uData.msgEncodingSet = true;
+        }
+    }
+
+    private static void encodeUserData(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException, CodingException
+    {
+        /*
+         * TODO(cleanup): Do we really need to set userData.payload as
+         * a side effect of encoding?  If not, we could avoid data
+         * copies by passing outStream directly.
+         */
+        encodeUserDataPayload(bData.userData);
+        bData.hasUserDataHeader = bData.userData.userDataHeader != null;
+
+        if (bData.userData.payload.length > SmsConstants.MAX_USER_DATA_BYTES) {
+            throw new CodingException("encoded user data too large (" +
+                                      bData.userData.payload.length +
+                                      " > " + SmsConstants.MAX_USER_DATA_BYTES + " bytes)");
+        }
+
+        /*
+         * TODO(cleanup): figure out what the right answer is WRT paddingBits field
+         *
+         *   userData.paddingBits = (userData.payload.length * 8) - (userData.numFields * 7);
+         *   userData.paddingBits = 0; // XXX this seems better, but why?
+         *
+         */
+        int dataBits = (bData.userData.payload.length * 8) - bData.userData.paddingBits;
+        int paramBits = dataBits + 13;
+        if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) ||
+            (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) {
+            paramBits += 8;
+        }
+        int paramBytes = (paramBits / 8) + ((paramBits % 8) > 0 ? 1 : 0);
+        int paddingBits = (paramBytes * 8) - paramBits;
+        outStream.write(8, paramBytes);
+        outStream.write(5, bData.userData.msgEncoding);
+        if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) ||
+            (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) {
+            outStream.write(8, bData.userData.msgType);
+        }
+        outStream.write(8, bData.userData.numFields);
+        outStream.writeByteArray(dataBits, bData.userData.payload);
+        if (paddingBits > 0) outStream.write(paddingBits, 0);
+    }
+
+    private static void encodeReplyOption(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(1, bData.userAckReq     ? 1 : 0);
+        outStream.write(1, bData.deliveryAckReq ? 1 : 0);
+        outStream.write(1, bData.readAckReq     ? 1 : 0);
+        outStream.write(1, bData.reportReq      ? 1 : 0);
+        outStream.write(4, 0);
+    }
+
+    private static byte[] encodeDtmfSmsAddress(String address) {
+        int digits = address.length();
+        int dataBits = digits * 4;
+        int dataBytes = (dataBits / 8);
+        dataBytes += (dataBits % 8) > 0 ? 1 : 0;
+        byte[] rawData = new byte[dataBytes];
+        for (int i = 0; i < digits; i++) {
+            char c = address.charAt(i);
+            int val = 0;
+            if ((c >= '1') && (c <= '9')) val = c - '0';
+            else if (c == '0') val = 10;
+            else if (c == '*') val = 11;
+            else if (c == '#') val = 12;
+            else return null;
+            rawData[i / 2] |= val << (4 - ((i % 2) * 4));
+        }
+        return rawData;
+    }
+
+    /*
+     * TODO(cleanup): CdmaSmsAddress encoding should make use of
+     * CdmaSmsAddress.parse provided that DTMF encoding is unified,
+     * and the difference in 4-bit vs. 8-bit is resolved.
+     */
+
+    private static void encodeCdmaSmsAddress(CdmaSmsAddress addr) throws CodingException {
+        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+            try {
+                addr.origBytes = addr.address.getBytes("US-ASCII");
+            } catch (java.io.UnsupportedEncodingException ex) {
+                throw new CodingException("invalid SMS address, cannot convert to ASCII");
+            }
+        } else {
+            addr.origBytes = encodeDtmfSmsAddress(addr.address);
+        }
+    }
+
+    private static void encodeCallbackNumber(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException, CodingException
+    {
+        CdmaSmsAddress addr = bData.callbackNumber;
+        encodeCdmaSmsAddress(addr);
+        int paramBits = 9;
+        int dataBits = 0;
+        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+            paramBits += 7;
+            dataBits = addr.numberOfDigits * 8;
+        } else {
+            dataBits = addr.numberOfDigits * 4;
+        }
+        paramBits += dataBits;
+        int paramBytes = (paramBits / 8) + ((paramBits % 8) > 0 ? 1 : 0);
+        int paddingBits = (paramBytes * 8) - paramBits;
+        outStream.write(8, paramBytes);
+        outStream.write(1, addr.digitMode);
+        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+            outStream.write(3, addr.ton);
+            outStream.write(4, addr.numberPlan);
+        }
+        outStream.write(8, addr.numberOfDigits);
+        outStream.writeByteArray(dataBits, addr.origBytes);
+        if (paddingBits > 0) outStream.write(paddingBits, 0);
+    }
+
+    private static void encodeMsgStatus(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(2, bData.errorClass);
+        outStream.write(6, bData.messageStatus);
+    }
+
+    private static void encodeMsgCount(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(8, bData.numberOfMessages);
+    }
+
+    private static void encodeValidityPeriodRel(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(8, bData.validityPeriodRelative);
+    }
+
+    private static void encodePrivacyIndicator(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(2, bData.privacy);
+        outStream.skip(6);
+    }
+
+    private static void encodeLanguageIndicator(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(8, bData.language);
+    }
+
+    private static void encodeDisplayMode(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(2, bData.displayMode);
+        outStream.skip(6);
+    }
+
+    private static void encodePriorityIndicator(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(2, bData.priority);
+        outStream.skip(6);
+    }
+
+    private static void encodeMsgDeliveryAlert(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        outStream.write(8, 1);
+        outStream.write(2, bData.alert);
+        outStream.skip(6);
+    }
+
+    private static void encodeScpResults(BearerData bData, BitwiseOutputStream outStream)
+        throws BitwiseOutputStream.AccessException
+    {
+        ArrayList<CdmaSmsCbProgramResults> results = bData.serviceCategoryProgramResults;
+        outStream.write(8, (results.size() * 4));   // 4 octets per program result
+        for (CdmaSmsCbProgramResults result : results) {
+            int category = result.getCategory();
+            outStream.write(8, category >> 8);
+            outStream.write(8, category);
+            outStream.write(8, result.getLanguage());
+            outStream.write(4, result.getCategoryResult());
+            outStream.skip(4);
+        }
+    }
+
+    /**
+     * Create serialized representation for BearerData object.
+     * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
+     *
+     * @param bData an instance of BearerData.
+     *
+     * @return byte array of raw encoded SMS bearer data.
+     */
+    public static byte[] encode(BearerData bData) {
+        bData.hasUserDataHeader = ((bData.userData != null) &&
+                (bData.userData.userDataHeader != null));
+        try {
+            BitwiseOutputStream outStream = new BitwiseOutputStream(200);
+            outStream.write(8, SUBPARAM_MESSAGE_IDENTIFIER);
+            encodeMessageId(bData, outStream);
+            if (bData.userData != null) {
+                outStream.write(8, SUBPARAM_USER_DATA);
+                encodeUserData(bData, outStream);
+            }
+            if (bData.callbackNumber != null) {
+                outStream.write(8, SUBPARAM_CALLBACK_NUMBER);
+                encodeCallbackNumber(bData, outStream);
+            }
+            if (bData.userAckReq || bData.deliveryAckReq || bData.readAckReq || bData.reportReq) {
+                outStream.write(8, SUBPARAM_REPLY_OPTION);
+                encodeReplyOption(bData, outStream);
+            }
+            if (bData.numberOfMessages != 0) {
+                outStream.write(8, SUBPARAM_NUMBER_OF_MESSAGES);
+                encodeMsgCount(bData, outStream);
+            }
+            if (bData.validityPeriodRelativeSet) {
+                outStream.write(8, SUBPARAM_VALIDITY_PERIOD_RELATIVE);
+                encodeValidityPeriodRel(bData, outStream);
+            }
+            if (bData.privacyIndicatorSet) {
+                outStream.write(8, SUBPARAM_PRIVACY_INDICATOR);
+                encodePrivacyIndicator(bData, outStream);
+            }
+            if (bData.languageIndicatorSet) {
+                outStream.write(8, SUBPARAM_LANGUAGE_INDICATOR);
+                encodeLanguageIndicator(bData, outStream);
+            }
+            if (bData.displayModeSet) {
+                outStream.write(8, SUBPARAM_MESSAGE_DISPLAY_MODE);
+                encodeDisplayMode(bData, outStream);
+            }
+            if (bData.priorityIndicatorSet) {
+                outStream.write(8, SUBPARAM_PRIORITY_INDICATOR);
+                encodePriorityIndicator(bData, outStream);
+            }
+            if (bData.alertIndicatorSet) {
+                outStream.write(8, SUBPARAM_ALERT_ON_MESSAGE_DELIVERY);
+                encodeMsgDeliveryAlert(bData, outStream);
+            }
+            if (bData.messageStatusSet) {
+                outStream.write(8, SUBPARAM_MESSAGE_STATUS);
+                encodeMsgStatus(bData, outStream);
+            }
+            if (bData.serviceCategoryProgramResults != null) {
+                outStream.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS);
+                encodeScpResults(bData, outStream);
+            }
+            return outStream.toByteArray();
+        } catch (BitwiseOutputStream.AccessException ex) {
+            Rlog.e(LOG_TAG, "BearerData encode failed: " + ex);
+        } catch (CodingException ex) {
+            Rlog.e(LOG_TAG, "BearerData encode failed: " + ex);
+        }
+        return null;
+   }
+
+    private static boolean decodeMessageId(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 3 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.messageType = inStream.read(4);
+            bData.messageId = inStream.read(8) << 8;
+            bData.messageId |= inStream.read(8);
+            bData.hasUserDataHeader = (inStream.read(1) == 1);
+            inStream.skip(3);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "MESSAGE_IDENTIFIER decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeReserved(
+            BearerData bData, BitwiseInputStream inStream, int subparamId)
+        throws BitwiseInputStream.AccessException, CodingException
+    {
+        boolean decodeSuccess = false;
+        int subparamLen = inStream.read(8); // SUBPARAM_LEN
+        int paramBits = subparamLen * 8;
+        if (paramBits <= inStream.available()) {
+            decodeSuccess = true;
+            inStream.skip(paramBits);
+        }
+        Rlog.d(LOG_TAG, "RESERVED bearer data subparameter " + subparamId + " decode "
+                + (decodeSuccess ? "succeeded" : "failed") + " (param bits = " + paramBits + ")");
+        if (!decodeSuccess) {
+            throw new CodingException("RESERVED bearer data subparameter " + subparamId
+                    + " had invalid SUBPARAM_LEN " + subparamLen);
+        }
+
+        return decodeSuccess;
+    }
+
+    private static boolean decodeUserData(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException
+    {
+        int paramBits = inStream.read(8) * 8;
+        bData.userData = new UserData();
+        bData.userData.msgEncoding = inStream.read(5);
+        bData.userData.msgEncodingSet = true;
+        bData.userData.msgType = 0;
+        int consumedBits = 5;
+        if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) ||
+            (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) {
+            bData.userData.msgType = inStream.read(8);
+            consumedBits += 8;
+        }
+        bData.userData.numFields = inStream.read(8);
+        consumedBits += 8;
+        int dataBits = paramBits - consumedBits;
+        bData.userData.payload = inStream.readByteArray(dataBits);
+        return true;
+    }
+
+    private static String decodeUtf8(byte[] data, int offset, int numFields)
+        throws CodingException
+    {
+        return decodeCharset(data, offset, numFields, 1, "UTF-8");
+    }
+
+    private static String decodeUtf16(byte[] data, int offset, int numFields)
+        throws CodingException
+    {
+        // Subtract header and possible padding byte (at end) from num fields.
+        int padding = offset % 2;
+        numFields -= (offset + padding) / 2;
+        return decodeCharset(data, offset, numFields, 2, "utf-16be");
+    }
+
+    private static String decodeCharset(byte[] data, int offset, int numFields, int width,
+            String charset) throws CodingException
+    {
+        if (numFields < 0 || (numFields * width + offset) > data.length) {
+            // Try to decode the max number of characters in payload
+            int padding = offset % width;
+            int maxNumFields = (data.length - offset - padding) / width;
+            if (maxNumFields < 0) {
+                throw new CodingException(charset + " decode failed: offset out of range");
+            }
+            Rlog.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = "
+                    + numFields + " data.length = " + data.length + " maxNumFields = "
+                    + maxNumFields);
+            numFields = maxNumFields;
+        }
+        try {
+            return new String(data, offset, numFields * width, charset);
+        } catch (java.io.UnsupportedEncodingException ex) {
+            throw new CodingException(charset + " decode failed: " + ex);
+        }
+    }
+
+    private static String decode7bitAscii(byte[] data, int offset, int numFields)
+        throws CodingException
+    {
+        try {
+            offset *= 8;
+            StringBuffer strBuf = new StringBuffer(numFields);
+            BitwiseInputStream inStream = new BitwiseInputStream(data);
+            int wantedBits = (offset * 8) + (numFields * 7);
+            if (inStream.available() < wantedBits) {
+                throw new CodingException("insufficient data (wanted " + wantedBits +
+                                          " bits, but only have " + inStream.available() + ")");
+            }
+            inStream.skip(offset);
+            for (int i = 0; i < numFields; i++) {
+                int charCode = inStream.read(7);
+                if ((charCode >= UserData.ASCII_MAP_BASE_INDEX) &&
+                        (charCode <= UserData.ASCII_MAP_MAX_INDEX)) {
+                    strBuf.append(UserData.ASCII_MAP[charCode - UserData.ASCII_MAP_BASE_INDEX]);
+                } else if (charCode == UserData.ASCII_NL_INDEX) {
+                    strBuf.append('\n');
+                } else if (charCode == UserData.ASCII_CR_INDEX) {
+                    strBuf.append('\r');
+                } else {
+                    /* For other charCodes, they are unprintable, and so simply use SPACE. */
+                    strBuf.append(' ');
+                }
+            }
+            return strBuf.toString();
+        } catch (BitwiseInputStream.AccessException ex) {
+            throw new CodingException("7bit ASCII decode failed: " + ex);
+        }
+    }
+
+    private static String decode7bitGsm(byte[] data, int offset, int numFields)
+        throws CodingException
+    {
+        // Start reading from the next 7-bit aligned boundary after offset.
+        int offsetBits = offset * 8;
+        int offsetSeptets = (offsetBits + 6) / 7;
+        numFields -= offsetSeptets;
+        int paddingBits = (offsetSeptets * 7) - offsetBits;
+        String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits,
+                0, 0);
+        if (result == null) {
+            throw new CodingException("7bit GSM decoding failed");
+        }
+        return result;
+    }
+
+    private static String decodeLatin(byte[] data, int offset, int numFields)
+        throws CodingException
+    {
+        return decodeCharset(data, offset, numFields, 1, "ISO-8859-1");
+    }
+
+    private static String decodeShiftJis(byte[] data, int offset, int numFields)
+        throws CodingException
+    {
+        return decodeCharset(data, offset, numFields, 1, "Shift_JIS");
+    }
+
+    private static String decodeGsmDcs(byte[] data, int offset, int numFields, int msgType)
+            throws CodingException
+    {
+        if ((msgType & 0xC0) != 0) {
+            throw new CodingException("unsupported coding group ("
+                    + msgType + ")");
+        }
+
+        switch ((msgType >> 2) & 0x3) {
+        case UserData.ENCODING_GSM_DCS_7BIT:
+            return decode7bitGsm(data, offset, numFields);
+        case UserData.ENCODING_GSM_DCS_8BIT:
+            return decodeUtf8(data, offset, numFields);
+        case UserData.ENCODING_GSM_DCS_16BIT:
+            return decodeUtf16(data, offset, numFields);
+        default:
+            throw new CodingException("unsupported user msgType encoding ("
+                    + msgType + ")");
+        }
+    }
+
+    private static void decodeUserDataPayload(UserData userData, boolean hasUserDataHeader)
+        throws CodingException
+    {
+        int offset = 0;
+        if (hasUserDataHeader) {
+            int udhLen = userData.payload[0] & 0x00FF;
+            offset += udhLen + 1;
+            byte[] headerData = new byte[udhLen];
+            System.arraycopy(userData.payload, 1, headerData, 0, udhLen);
+            userData.userDataHeader = SmsHeader.fromByteArray(headerData);
+        }
+        switch (userData.msgEncoding) {
+        case UserData.ENCODING_OCTET:
+            /*
+            *  Octet decoding depends on the carrier service.
+            */
+            boolean decodingtypeUTF8 = Resources.getSystem()
+                    .getBoolean(com.android.internal.R.bool.config_sms_utf8_support);
+
+            // Strip off any padding bytes, meaning any differences between the length of the
+            // array and the target length specified by numFields.  This is to avoid any
+            // confusion by code elsewhere that only considers the payload array length.
+            byte[] payload = new byte[userData.numFields];
+            int copyLen = userData.numFields < userData.payload.length
+                    ? userData.numFields : userData.payload.length;
+
+            System.arraycopy(userData.payload, 0, payload, 0, copyLen);
+            userData.payload = payload;
+
+            if (!decodingtypeUTF8) {
+                // There are many devices in the market that send 8bit text sms (latin encoded) as
+                // octet encoded.
+                userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
+            } else {
+                userData.payloadStr = decodeUtf8(userData.payload, offset, userData.numFields);
+            }
+            break;
+
+        case UserData.ENCODING_IA5:
+        case UserData.ENCODING_7BIT_ASCII:
+            userData.payloadStr = decode7bitAscii(userData.payload, offset, userData.numFields);
+            break;
+        case UserData.ENCODING_UNICODE_16:
+            userData.payloadStr = decodeUtf16(userData.payload, offset, userData.numFields);
+            break;
+        case UserData.ENCODING_GSM_7BIT_ALPHABET:
+            userData.payloadStr = decode7bitGsm(userData.payload, offset, userData.numFields);
+            break;
+        case UserData.ENCODING_LATIN:
+            userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
+            break;
+        case UserData.ENCODING_SHIFT_JIS:
+            userData.payloadStr = decodeShiftJis(userData.payload, offset, userData.numFields);
+            break;
+        case UserData.ENCODING_GSM_DCS:
+            userData.payloadStr = decodeGsmDcs(userData.payload, offset,
+                    userData.numFields, userData.msgType);
+            break;
+        default:
+            throw new CodingException("unsupported user data encoding ("
+                                      + userData.msgEncoding + ")");
+        }
+    }
+
+    /**
+     * IS-91 Voice Mail message decoding
+     * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1)
+     * (For character encodings, see TIA/EIA/IS-91, Annex B)
+     *
+     * Protocol Summary: The user data payload may contain 3-14
+     * characters.  The first two characters are parsed as a number
+     * and indicate the number of voicemails.  The third character is
+     * either a SPACE or '!' to indicate normal or urgent priority,
+     * respectively.  Any following characters are treated as normal
+     * text user data payload.
+     *
+     * Note that the characters encoding is 6-bit packed.
+     */
+    private static void decodeIs91VoicemailStatus(BearerData bData)
+        throws BitwiseInputStream.AccessException, CodingException
+    {
+        BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
+        int dataLen = inStream.available() / 6;  // 6-bit packed character encoding.
+        int numFields = bData.userData.numFields;
+        if ((dataLen > 14) || (dataLen < 3) || (dataLen < numFields)) {
+            throw new CodingException("IS-91 voicemail status decoding failed");
+        }
+        try {
+            StringBuffer strbuf = new StringBuffer(dataLen);
+            while (inStream.available() >= 6) {
+                strbuf.append(UserData.ASCII_MAP[inStream.read(6)]);
+            }
+            String data = strbuf.toString();
+            bData.numberOfMessages = Integer.parseInt(data.substring(0, 2));
+            char prioCode = data.charAt(2);
+            if (prioCode == ' ') {
+                bData.priority = PRIORITY_NORMAL;
+            } else if (prioCode == '!') {
+                bData.priority = PRIORITY_URGENT;
+            } else {
+                throw new CodingException("IS-91 voicemail status decoding failed: " +
+                        "illegal priority setting (" + prioCode + ")");
+            }
+            bData.priorityIndicatorSet = true;
+            bData.userData.payloadStr = data.substring(3, numFields - 3);
+       } catch (java.lang.NumberFormatException ex) {
+            throw new CodingException("IS-91 voicemail status decoding failed: " + ex);
+        } catch (java.lang.IndexOutOfBoundsException ex) {
+            throw new CodingException("IS-91 voicemail status decoding failed: " + ex);
+        }
+    }
+
+    /**
+     * IS-91 Short Message decoding
+     * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1)
+     * (For character encodings, see TIA/EIA/IS-91, Annex B)
+     *
+     * Protocol Summary: The user data payload may contain 1-14
+     * characters, which are treated as normal text user data payload.
+     * Note that the characters encoding is 6-bit packed.
+     */
+    private static void decodeIs91ShortMessage(BearerData bData)
+        throws BitwiseInputStream.AccessException, CodingException
+    {
+        BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
+        int dataLen = inStream.available() / 6;  // 6-bit packed character encoding.
+        int numFields = bData.userData.numFields;
+        // dataLen may be > 14 characters due to octet padding
+        if ((numFields > 14) || (dataLen < numFields)) {
+            throw new CodingException("IS-91 short message decoding failed");
+        }
+        StringBuffer strbuf = new StringBuffer(dataLen);
+        for (int i = 0; i < numFields; i++) {
+            strbuf.append(UserData.ASCII_MAP[inStream.read(6)]);
+        }
+        bData.userData.payloadStr = strbuf.toString();
+    }
+
+    /**
+     * IS-91 CLI message (callback number) decoding
+     * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1)
+     *
+     * Protocol Summary: The data payload may contain 1-32 digits,
+     * encoded using standard 4-bit DTMF, which are treated as a
+     * callback number.
+     */
+    private static void decodeIs91Cli(BearerData bData) throws CodingException {
+        BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
+        int dataLen = inStream.available() / 4;  // 4-bit packed DTMF digit encoding.
+        int numFields = bData.userData.numFields;
+        if ((dataLen > 14) || (dataLen < 3) || (dataLen < numFields)) {
+            throw new CodingException("IS-91 voicemail status decoding failed");
+        }
+        CdmaSmsAddress addr = new CdmaSmsAddress();
+        addr.digitMode = CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF;
+        addr.origBytes = bData.userData.payload;
+        addr.numberOfDigits = (byte)numFields;
+        decodeSmsAddress(addr);
+        bData.callbackNumber = addr;
+    }
+
+    private static void decodeIs91(BearerData bData)
+        throws BitwiseInputStream.AccessException, CodingException
+    {
+        switch (bData.userData.msgType) {
+        case UserData.IS91_MSG_TYPE_VOICEMAIL_STATUS:
+            decodeIs91VoicemailStatus(bData);
+            break;
+        case UserData.IS91_MSG_TYPE_CLI:
+            decodeIs91Cli(bData);
+            break;
+        case UserData.IS91_MSG_TYPE_SHORT_MESSAGE_FULL:
+        case UserData.IS91_MSG_TYPE_SHORT_MESSAGE:
+            decodeIs91ShortMessage(bData);
+            break;
+        default:
+            throw new CodingException("unsupported IS-91 message type (" +
+                    bData.userData.msgType + ")");
+        }
+    }
+
+    private static boolean decodeReplyOption(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.userAckReq     = (inStream.read(1) == 1);
+            bData.deliveryAckReq = (inStream.read(1) == 1);
+            bData.readAckReq     = (inStream.read(1) == 1);
+            bData.reportReq      = (inStream.read(1) == 1);
+            inStream.skip(4);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "REPLY_OPTION decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeMsgCount(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.numberOfMessages = IccUtils.cdmaBcdByteToInt((byte)inStream.read(8));
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "NUMBER_OF_MESSAGES decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeDepositIndex(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 2 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.depositIndex = (inStream.read(8) << 8) | inStream.read(8);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "MESSAGE_DEPOSIT_INDEX decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static String decodeDtmfSmsAddress(byte[] rawData, int numFields)
+        throws CodingException
+    {
+        /* DTMF 4-bit digit encoding, defined in at
+         * 3GPP2 C.S005-D, v2.0, table 2.7.1.3.2.4-4 */
+        StringBuffer strBuf = new StringBuffer(numFields);
+        for (int i = 0; i < numFields; i++) {
+            int val = 0x0F & (rawData[i / 2] >>> (4 - ((i % 2) * 4)));
+            if ((val >= 1) && (val <= 9)) strBuf.append(Integer.toString(val, 10));
+            else if (val == 10) strBuf.append('0');
+            else if (val == 11) strBuf.append('*');
+            else if (val == 12) strBuf.append('#');
+            else throw new CodingException("invalid SMS address DTMF code (" + val + ")");
+        }
+        return strBuf.toString();
+    }
+
+    private static void decodeSmsAddress(CdmaSmsAddress addr) throws CodingException {
+        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+            try {
+                /* As specified in 3GPP2 C.S0015-B, v2, 4.5.15 -- actually
+                 * just 7-bit ASCII encoding, with the MSB being zero. */
+                addr.address = new String(addr.origBytes, 0, addr.origBytes.length, "US-ASCII");
+            } catch (java.io.UnsupportedEncodingException ex) {
+                throw new CodingException("invalid SMS address ASCII code");
+            }
+        } else {
+            addr.address = decodeDtmfSmsAddress(addr.origBytes, addr.numberOfDigits);
+        }
+    }
+
+    private static boolean decodeCallbackNumber(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException, CodingException
+    {
+        final int EXPECTED_PARAM_SIZE = 1 * 8; //at least
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits < EXPECTED_PARAM_SIZE) {
+            inStream.skip(paramBits);
+            return false;
+        }
+        CdmaSmsAddress addr = new CdmaSmsAddress();
+        addr.digitMode = inStream.read(1);
+        byte fieldBits = 4;
+        byte consumedBits = 1;
+        if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) {
+            addr.ton = inStream.read(3);
+            addr.numberPlan = inStream.read(4);
+            fieldBits = 8;
+            consumedBits += 7;
+        }
+        addr.numberOfDigits = inStream.read(8);
+        consumedBits += 8;
+        int remainingBits = paramBits - consumedBits;
+        int dataBits = addr.numberOfDigits * fieldBits;
+        int paddingBits = remainingBits - dataBits;
+        if (remainingBits < dataBits) {
+            throw new CodingException("CALLBACK_NUMBER subparam encoding size error (" +
+                                      "remainingBits + " + remainingBits + ", dataBits + " +
+                                      dataBits + ", paddingBits + " + paddingBits + ")");
+        }
+        addr.origBytes = inStream.readByteArray(dataBits);
+        inStream.skip(paddingBits);
+        decodeSmsAddress(addr);
+        bData.callbackNumber = addr;
+        return true;
+    }
+
+    private static boolean decodeMsgStatus(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.errorClass = inStream.read(2);
+            bData.messageStatus = inStream.read(6);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "MESSAGE_STATUS decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.messageStatusSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeMsgCenterTimeStamp(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 6 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.msgCenterTimeStamp = TimeStamp.fromByteArray(inStream.readByteArray(6 * 8));
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "MESSAGE_CENTER_TIME_STAMP decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeValidityAbs(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 6 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.validityPeriodAbsolute = TimeStamp.fromByteArray(inStream.readByteArray(6 * 8));
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "VALIDITY_PERIOD_ABSOLUTE decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeDeferredDeliveryAbs(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 6 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.deferredDeliveryTimeAbsolute = TimeStamp.fromByteArray(
+                    inStream.readByteArray(6 * 8));
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "DEFERRED_DELIVERY_TIME_ABSOLUTE decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeValidityRel(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.deferredDeliveryTimeRelative = inStream.read(8);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "VALIDITY_PERIOD_RELATIVE decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.deferredDeliveryTimeRelativeSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeDeferredDeliveryRel(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.validityPeriodRelative = inStream.read(8);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "DEFERRED_DELIVERY_TIME_RELATIVE decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.validityPeriodRelativeSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodePrivacyIndicator(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.privacy = inStream.read(2);
+            inStream.skip(6);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "PRIVACY_INDICATOR decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.privacyIndicatorSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeLanguageIndicator(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.language = inStream.read(8);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "LANGUAGE_INDICATOR decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.languageIndicatorSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeDisplayMode(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.displayMode = inStream.read(2);
+            inStream.skip(6);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "DISPLAY_MODE decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.displayModeSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodePriorityIndicator(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.priority = inStream.read(2);
+            inStream.skip(6);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "PRIORITY_INDICATOR decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.priorityIndicatorSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeMsgDeliveryAlert(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.alert = inStream.read(2);
+            inStream.skip(6);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "ALERT_ON_MESSAGE_DELIVERY decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.alertIndicatorSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeUserResponseCode(BearerData bData, BitwiseInputStream inStream)
+        throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.userResponseCode = inStream.read(8);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "USER_RESPONSE_CODE decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        bData.userResponseCodeSet = decodeSuccess;
+        return decodeSuccess;
+    }
+
+    private static boolean decodeServiceCategoryProgramData(BearerData bData,
+            BitwiseInputStream inStream) throws BitwiseInputStream.AccessException, CodingException
+    {
+        if (inStream.available() < 13) {
+            throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only "
+                    + inStream.available() + " bits available");
+        }
+
+        int paramBits = inStream.read(8) * 8;
+        int msgEncoding = inStream.read(5);
+        paramBits -= 5;
+
+        if (inStream.available() < paramBits) {
+            throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only "
+                    + inStream.available() + " bits available (" + paramBits + " bits expected)");
+        }
+
+        ArrayList<CdmaSmsCbProgramData> programDataList = new ArrayList<CdmaSmsCbProgramData>();
+
+        final int CATEGORY_FIELD_MIN_SIZE = 6 * 8;
+        boolean decodeSuccess = false;
+        while (paramBits >= CATEGORY_FIELD_MIN_SIZE) {
+            int operation = inStream.read(4);
+            int category = (inStream.read(8) << 8) | inStream.read(8);
+            int language = inStream.read(8);
+            int maxMessages = inStream.read(8);
+            int alertOption = inStream.read(4);
+            int numFields = inStream.read(8);
+            paramBits -= CATEGORY_FIELD_MIN_SIZE;
+
+            int textBits = getBitsForNumFields(msgEncoding, numFields);
+            if (paramBits < textBits) {
+                throw new CodingException("category name is " + textBits + " bits in length,"
+                        + " but there are only " + paramBits + " bits available");
+            }
+
+            UserData userData = new UserData();
+            userData.msgEncoding = msgEncoding;
+            userData.msgEncodingSet = true;
+            userData.numFields = numFields;
+            userData.payload = inStream.readByteArray(textBits);
+            paramBits -= textBits;
+
+            decodeUserDataPayload(userData, false);
+            String categoryName = userData.payloadStr;
+            CdmaSmsCbProgramData programData = new CdmaSmsCbProgramData(operation, category,
+                    language, maxMessages, alertOption, categoryName);
+            programDataList.add(programData);
+
+            decodeSuccess = true;
+        }
+
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "SERVICE_CATEGORY_PROGRAM_DATA decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ')');
+        }
+
+        inStream.skip(paramBits);
+        bData.serviceCategoryProgramData = programDataList;
+        return decodeSuccess;
+    }
+
+    private static int serviceCategoryToCmasMessageClass(int serviceCategory) {
+        switch (serviceCategory) {
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT:
+                return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT:
+                return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT:
+                return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY:
+                return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
+        }
+    }
+
+    /**
+     * Calculates the number of bits to read for the specified number of encoded characters.
+     * @param msgEncoding the message encoding to use
+     * @param numFields the number of characters to read. For Shift-JIS and Korean encodings,
+     *  this is the number of bytes to read.
+     * @return the number of bits to read from the stream
+     * @throws CodingException if the specified encoding is not supported
+     */
+    private static int getBitsForNumFields(int msgEncoding, int numFields)
+            throws CodingException {
+        switch (msgEncoding) {
+            case UserData.ENCODING_OCTET:
+            case UserData.ENCODING_SHIFT_JIS:
+            case UserData.ENCODING_KOREAN:
+            case UserData.ENCODING_LATIN:
+            case UserData.ENCODING_LATIN_HEBREW:
+                return numFields * 8;
+
+            case UserData.ENCODING_IA5:
+            case UserData.ENCODING_7BIT_ASCII:
+            case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                return numFields * 7;
+
+            case UserData.ENCODING_UNICODE_16:
+                return numFields * 16;
+
+            default:
+                throw new CodingException("unsupported message encoding (" + msgEncoding + ')');
+        }
+    }
+
+    /**
+     * CMAS message decoding.
+     * (See TIA-1149-0-1, CMAS over CDMA)
+     *
+     * @param serviceCategory is the service category from the SMS envelope
+     */
+    private static void decodeCmasUserData(BearerData bData, int serviceCategory)
+            throws BitwiseInputStream.AccessException, CodingException {
+        BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
+        if (inStream.available() < 8) {
+            throw new CodingException("emergency CB with no CMAE_protocol_version");
+        }
+        int protocolVersion = inStream.read(8);
+        if (protocolVersion != 0) {
+            throw new CodingException("unsupported CMAE_protocol_version " + protocolVersion);
+        }
+
+        int messageClass = serviceCategoryToCmasMessageClass(serviceCategory);
+        int category = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN;
+        int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN;
+        int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
+        int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
+        int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
+
+        while (inStream.available() >= 16) {
+            int recordType = inStream.read(8);
+            int recordLen = inStream.read(8);
+            switch (recordType) {
+                case 0:     // Type 0 elements (Alert text)
+                    UserData alertUserData = new UserData();
+                    alertUserData.msgEncoding = inStream.read(5);
+                    alertUserData.msgEncodingSet = true;
+                    alertUserData.msgType = 0;
+
+                    int numFields;                          // number of chars to decode
+                    switch (alertUserData.msgEncoding) {
+                        case UserData.ENCODING_OCTET:
+                        case UserData.ENCODING_LATIN:
+                            numFields = recordLen - 1;      // subtract 1 byte for encoding
+                            break;
+
+                        case UserData.ENCODING_IA5:
+                        case UserData.ENCODING_7BIT_ASCII:
+                        case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                            numFields = ((recordLen * 8) - 5) / 7;  // subtract 5 bits for encoding
+                            break;
+
+                        case UserData.ENCODING_UNICODE_16:
+                            numFields = (recordLen - 1) / 2;
+                            break;
+
+                        default:
+                            numFields = 0;      // unsupported encoding
+                    }
+
+                    alertUserData.numFields = numFields;
+                    alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5);
+                    decodeUserDataPayload(alertUserData, false);
+                    bData.userData = alertUserData;
+                    break;
+
+                case 1:     // Type 1 elements
+                    category = inStream.read(8);
+                    responseType = inStream.read(8);
+                    severity = inStream.read(4);
+                    urgency = inStream.read(4);
+                    certainty = inStream.read(4);
+                    inStream.skip(recordLen * 8 - 28);
+                    break;
+
+                default:
+                    Rlog.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType);
+                    inStream.skip(recordLen * 8);
+                    break;
+            }
+        }
+
+        bData.cmasWarningInfo = new SmsCbCmasInfo(messageClass, category, responseType, severity,
+                urgency, certainty);
+    }
+
+    /**
+     * Create BearerData object from serialized representation.
+     * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
+     *
+     * @param smsData byte array of raw encoded SMS bearer data.
+     * @return an instance of BearerData.
+     */
+    public static BearerData decode(byte[] smsData) {
+        return decode(smsData, 0);
+    }
+
+    private static boolean isCmasAlertCategory(int category) {
+        return category >= SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT
+                && category <= SmsEnvelope.SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE;
+    }
+
+    /**
+     * Create BearerData object from serialized representation.
+     * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
+     *
+     * @param smsData byte array of raw encoded SMS bearer data.
+     * @param serviceCategory the envelope service category (for CMAS alert handling)
+     * @return an instance of BearerData.
+     */
+    public static BearerData decode(byte[] smsData, int serviceCategory) {
+        try {
+            BitwiseInputStream inStream = new BitwiseInputStream(smsData);
+            BearerData bData = new BearerData();
+            int foundSubparamMask = 0;
+            while (inStream.available() > 0) {
+                int subparamId = inStream.read(8);
+                int subparamIdBit = 1 << subparamId;
+                // int is 4 bytes. This duplicate check has a limit to Id number up to 32 (4*8)
+                // as 32th bit is the max bit in int.
+                // Per 3GPP2 C.S0015-B Table 4.5-1 Bearer Data Subparameter Identifiers:
+                // last defined subparam ID is 23 (00010111 = 0x17 = 23).
+                // Only do duplicate subparam ID check if subparam is within defined value as
+                // reserved subparams are just skipped.
+                if ((foundSubparamMask & subparamIdBit) != 0 &&
+                        (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER &&
+                        subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
+                    throw new CodingException("illegal duplicate subparameter (" +
+                                              subparamId + ")");
+                }
+                boolean decodeSuccess;
+                switch (subparamId) {
+                case SUBPARAM_MESSAGE_IDENTIFIER:
+                    decodeSuccess = decodeMessageId(bData, inStream);
+                    break;
+                case SUBPARAM_USER_DATA:
+                    decodeSuccess = decodeUserData(bData, inStream);
+                    break;
+                case SUBPARAM_USER_RESPONSE_CODE:
+                    decodeSuccess = decodeUserResponseCode(bData, inStream);
+                    break;
+                case SUBPARAM_REPLY_OPTION:
+                    decodeSuccess = decodeReplyOption(bData, inStream);
+                    break;
+                case SUBPARAM_NUMBER_OF_MESSAGES:
+                    decodeSuccess = decodeMsgCount(bData, inStream);
+                    break;
+                case SUBPARAM_CALLBACK_NUMBER:
+                    decodeSuccess = decodeCallbackNumber(bData, inStream);
+                    break;
+                case SUBPARAM_MESSAGE_STATUS:
+                    decodeSuccess = decodeMsgStatus(bData, inStream);
+                    break;
+                case SUBPARAM_MESSAGE_CENTER_TIME_STAMP:
+                    decodeSuccess = decodeMsgCenterTimeStamp(bData, inStream);
+                    break;
+                case SUBPARAM_VALIDITY_PERIOD_ABSOLUTE:
+                    decodeSuccess = decodeValidityAbs(bData, inStream);
+                    break;
+                case SUBPARAM_VALIDITY_PERIOD_RELATIVE:
+                    decodeSuccess = decodeValidityRel(bData, inStream);
+                    break;
+                case SUBPARAM_DEFERRED_DELIVERY_TIME_ABSOLUTE:
+                    decodeSuccess = decodeDeferredDeliveryAbs(bData, inStream);
+                    break;
+                case SUBPARAM_DEFERRED_DELIVERY_TIME_RELATIVE:
+                    decodeSuccess = decodeDeferredDeliveryRel(bData, inStream);
+                    break;
+                case SUBPARAM_PRIVACY_INDICATOR:
+                    decodeSuccess = decodePrivacyIndicator(bData, inStream);
+                    break;
+                case SUBPARAM_LANGUAGE_INDICATOR:
+                    decodeSuccess = decodeLanguageIndicator(bData, inStream);
+                    break;
+                case SUBPARAM_MESSAGE_DISPLAY_MODE:
+                    decodeSuccess = decodeDisplayMode(bData, inStream);
+                    break;
+                case SUBPARAM_PRIORITY_INDICATOR:
+                    decodeSuccess = decodePriorityIndicator(bData, inStream);
+                    break;
+                case SUBPARAM_ALERT_ON_MESSAGE_DELIVERY:
+                    decodeSuccess = decodeMsgDeliveryAlert(bData, inStream);
+                    break;
+                case SUBPARAM_MESSAGE_DEPOSIT_INDEX:
+                    decodeSuccess = decodeDepositIndex(bData, inStream);
+                    break;
+                case SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA:
+                    decodeSuccess = decodeServiceCategoryProgramData(bData, inStream);
+                    break;
+                default:
+                    decodeSuccess = decodeReserved(bData, inStream, subparamId);
+                }
+                if (decodeSuccess &&
+                        (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER &&
+                        subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
+                    foundSubparamMask |= subparamIdBit;
+                }
+            }
+            if ((foundSubparamMask & (1 << SUBPARAM_MESSAGE_IDENTIFIER)) == 0) {
+                throw new CodingException("missing MESSAGE_IDENTIFIER subparam");
+            }
+            if (bData.userData != null) {
+                if (isCmasAlertCategory(serviceCategory)) {
+                    decodeCmasUserData(bData, serviceCategory);
+                } else if (bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) {
+                    if ((foundSubparamMask ^
+                             (1 << SUBPARAM_MESSAGE_IDENTIFIER) ^
+                             (1 << SUBPARAM_USER_DATA))
+                            != 0) {
+                        Rlog.e(LOG_TAG, "IS-91 must occur without extra subparams (" +
+                              foundSubparamMask + ")");
+                    }
+                    decodeIs91(bData);
+                } else {
+                    decodeUserDataPayload(bData.userData, bData.hasUserDataHeader);
+                }
+            }
+            return bData;
+        } catch (BitwiseInputStream.AccessException ex) {
+            Rlog.e(LOG_TAG, "BearerData decode failed: " + ex);
+        } catch (CodingException ex) {
+            Rlog.e(LOG_TAG, "BearerData decode failed: " + ex);
+        }
+        return null;
+    }
+}
diff --git a/com/android/internal/telephony/cdma/sms/CdmaSmsAddress.java b/com/android/internal/telephony/cdma/sms/CdmaSmsAddress.java
new file mode 100644
index 0000000..5f2e561
--- /dev/null
+++ b/com/android/internal/telephony/cdma/sms/CdmaSmsAddress.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma.sms;
+
+import android.util.SparseBooleanArray;
+
+import com.android.internal.telephony.SmsAddress;
+import com.android.internal.telephony.cdma.sms.UserData;
+import com.android.internal.util.HexDump;
+
+public class CdmaSmsAddress extends SmsAddress {
+
+    /**
+     * Digit Mode Indicator is a 1-bit value that indicates whether
+     * the address digits are 4-bit DTMF codes or 8-bit codes.  (See
+     * 3GPP2 C.S0015-B, v2, 3.4.3.3)
+     */
+    static public final int DIGIT_MODE_4BIT_DTMF              = 0x00;
+    static public final int DIGIT_MODE_8BIT_CHAR              = 0x01;
+
+    public int digitMode;
+
+    /**
+     * Number Mode Indicator is 1-bit value that indicates whether the
+     * address type is a data network address or not.  (See 3GPP2
+     * C.S0015-B, v2, 3.4.3.3)
+     */
+    static public final int NUMBER_MODE_NOT_DATA_NETWORK      = 0x00;
+    static public final int NUMBER_MODE_DATA_NETWORK          = 0x01;
+
+    public int numberMode;
+
+    /**
+     * Number Types for data networks.
+     * (See 3GPP2 C.S005-D, table2.7.1.3.2.4-2 for complete table)
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.3 for data network subset)
+     * NOTE: value is stored in the parent class ton field.
+     */
+    static public final int TON_UNKNOWN                   = 0x00;
+    static public final int TON_INTERNATIONAL_OR_IP       = 0x01;
+    static public final int TON_NATIONAL_OR_EMAIL         = 0x02;
+    static public final int TON_NETWORK                   = 0x03;
+    static public final int TON_SUBSCRIBER                = 0x04;
+    static public final int TON_ALPHANUMERIC              = 0x05;
+    static public final int TON_ABBREVIATED               = 0x06;
+    static public final int TON_RESERVED                  = 0x07;
+
+    /**
+     * Maximum lengths for fields as defined in ril_cdma_sms.h.
+     */
+    static public final int SMS_ADDRESS_MAX          =  36;
+    static public final int SMS_SUBADDRESS_MAX       =  36;
+
+    /**
+     * This field shall be set to the number of address digits
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.3)
+     */
+    public int numberOfDigits;
+
+    /**
+     * Numbering Plan identification is a 0 or 4-bit value that
+     * indicates which numbering plan identification is set.  (See
+     * 3GPP2, C.S0015-B, v2, 3.4.3.3 and C.S005-D, table2.7.1.3.2.4-3)
+     */
+    static public final int NUMBERING_PLAN_UNKNOWN           = 0x0;
+    static public final int NUMBERING_PLAN_ISDN_TELEPHONY    = 0x1;
+    //static protected final int NUMBERING_PLAN_DATA              = 0x3;
+    //static protected final int NUMBERING_PLAN_TELEX             = 0x4;
+    //static protected final int NUMBERING_PLAN_PRIVATE           = 0x9;
+
+    public int numberPlan;
+
+    /**
+     * NOTE: the parsed string address and the raw byte array values
+     * are stored in the parent class address and origBytes fields,
+     * respectively.
+     */
+
+    public CdmaSmsAddress(){
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("CdmaSmsAddress ");
+        builder.append("{ digitMode=" + digitMode);
+        builder.append(", numberMode=" + numberMode);
+        builder.append(", numberPlan=" + numberPlan);
+        builder.append(", numberOfDigits=" + numberOfDigits);
+        builder.append(", ton=" + ton);
+        builder.append(", address=\"" + address + "\"");
+        builder.append(", origBytes=" + HexDump.toHexString(origBytes));
+        builder.append(" }");
+        return builder.toString();
+    }
+
+    /*
+     * TODO(cleanup): Refactor the parsing for addresses to better
+     * share code and logic with GSM.  Also, gather all DTMF/BCD
+     * processing code in one place.
+     */
+
+    private static byte[] parseToDtmf(String address) {
+        int digits = address.length();
+        byte[] result = new byte[digits];
+        for (int i = 0; i < digits; i++) {
+            char c = address.charAt(i);
+            int val = 0;
+            if ((c >= '1') && (c <= '9')) val = c - '0';
+            else if (c == '0') val = 10;
+            else if (c == '*') val = 11;
+            else if (c == '#') val = 12;
+            else return null;
+            result[i] = (byte)val;
+        }
+        return result;
+    }
+
+    private static final char[] numericCharsDialable = {
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#'
+    };
+
+    private static final char[] numericCharsSugar = {
+        '(', ')', ' ', '-', '+', '.', '/', '\\'
+    };
+
+    private static final SparseBooleanArray numericCharDialableMap = new SparseBooleanArray (
+            numericCharsDialable.length + numericCharsSugar.length);
+    static {
+        for (int i = 0; i < numericCharsDialable.length; i++) {
+            numericCharDialableMap.put(numericCharsDialable[i], true);
+        }
+        for (int i = 0; i < numericCharsSugar.length; i++) {
+            numericCharDialableMap.put(numericCharsSugar[i], false);
+        }
+    }
+
+    /**
+     * Given a numeric address string, return the string without
+     * syntactic sugar, meaning parens, spaces, hyphens/minuses, or
+     * plus signs.  If the input string contains non-numeric
+     * non-punctuation characters, return null.
+     */
+    private static String filterNumericSugar(String address) {
+        StringBuilder builder = new StringBuilder();
+        int len = address.length();
+        for (int i = 0; i < len; i++) {
+            char c = address.charAt(i);
+            int mapIndex = numericCharDialableMap.indexOfKey(c);
+            if (mapIndex < 0) return null;
+            if (! numericCharDialableMap.valueAt(mapIndex)) continue;
+            builder.append(c);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Given a string, return the string without whitespace,
+     * including CR/LF.
+     */
+    private static String filterWhitespace(String address) {
+        StringBuilder builder = new StringBuilder();
+        int len = address.length();
+        for (int i = 0; i < len; i++) {
+            char c = address.charAt(i);
+            if ((c == ' ') || (c == '\r') || (c == '\n') || (c == '\t')) continue;
+            builder.append(c);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Given a string, create a corresponding CdmaSmsAddress object.
+     *
+     * The result will be null if the input string is not
+     * representable using printable ASCII.
+     *
+     * For numeric addresses, the string is cleaned up by removing
+     * common punctuation.  For alpha addresses, the string is cleaned
+     * up by removing whitespace.
+     */
+    public static CdmaSmsAddress parse(String address) {
+        CdmaSmsAddress addr = new CdmaSmsAddress();
+        addr.address = address;
+        addr.ton = CdmaSmsAddress.TON_UNKNOWN;
+        byte[] origBytes = null;
+        String filteredAddr = filterNumericSugar(address);
+        if (filteredAddr != null) {
+            origBytes = parseToDtmf(filteredAddr);
+        }
+        if (origBytes != null) {
+            addr.digitMode = DIGIT_MODE_4BIT_DTMF;
+            addr.numberMode = NUMBER_MODE_NOT_DATA_NETWORK;
+            if (address.indexOf('+') != -1) {
+                addr.ton = TON_INTERNATIONAL_OR_IP;
+            }
+        } else {
+            filteredAddr = filterWhitespace(address);
+            origBytes = UserData.stringToAscii(filteredAddr);
+            if (origBytes == null) {
+                return null;
+            }
+            addr.digitMode = DIGIT_MODE_8BIT_CHAR;
+            addr.numberMode = NUMBER_MODE_DATA_NETWORK;
+            if (address.indexOf('@') != -1) {
+                addr.ton = TON_NATIONAL_OR_EMAIL;
+            }
+        }
+        addr.origBytes = origBytes;
+        addr.numberOfDigits = origBytes.length;
+        return addr;
+    }
+
+}
diff --git a/com/android/internal/telephony/cdma/sms/CdmaSmsSubaddress.java b/com/android/internal/telephony/cdma/sms/CdmaSmsSubaddress.java
new file mode 100644
index 0000000..0d5b502
--- /dev/null
+++ b/com/android/internal/telephony/cdma/sms/CdmaSmsSubaddress.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project. All rights reserved.
+ *
+ * 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.internal.telephony.cdma.sms;
+
+public class CdmaSmsSubaddress {
+    public int type;
+
+    public byte odd;
+
+    public byte[] origBytes;
+}
+
diff --git a/com/android/internal/telephony/cdma/sms/SmsEnvelope.java b/com/android/internal/telephony/cdma/sms/SmsEnvelope.java
new file mode 100644
index 0000000..f73df56
--- /dev/null
+++ b/com/android/internal/telephony/cdma/sms/SmsEnvelope.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma.sms;
+
+
+import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress;
+
+public final class SmsEnvelope {
+    /**
+     * Message Types
+     * (See 3GPP2 C.S0015-B 3.4.1)
+     */
+    static public final int MESSAGE_TYPE_POINT_TO_POINT   = 0x00;
+    static public final int MESSAGE_TYPE_BROADCAST        = 0x01;
+    static public final int MESSAGE_TYPE_ACKNOWLEDGE      = 0x02;
+
+    /**
+     * Supported Teleservices
+     * (See 3GPP2 N.S0005 and TIA-41)
+     */
+    static public final int TELESERVICE_NOT_SET           = 0x0000;
+    static public final int TELESERVICE_WMT               = 0x1002;
+    static public final int TELESERVICE_VMN               = 0x1003;
+    static public final int TELESERVICE_WAP               = 0x1004;
+    static public final int TELESERVICE_WEMT              = 0x1005;
+    static public final int TELESERVICE_SCPT              = 0x1006;
+
+    /**
+     * The following are defined as extensions to the standard teleservices
+     */
+    // Voice mail notification through Message Waiting Indication in CDMA mode or Analog mode.
+    // Defined in 3GPP2 C.S-0005, 3.7.5.6, an Info Record containing an 8-bit number with the
+    // number of messages waiting, it's used by some CDMA carriers for a voice mail count.
+    static public final int TELESERVICE_MWI               = 0x40000;
+
+    // Service Categories for Cell Broadcast, see 3GPP2 C.R1001 table 9.3.1-1
+    // static final int SERVICE_CATEGORY_EMERGENCY      = 0x0001;
+    //...
+
+    // CMAS alert service category assignments, see 3GPP2 C.R1001 table 9.3.3-1
+    public static final int SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT  = 0x1000;
+    public static final int SERVICE_CATEGORY_CMAS_EXTREME_THREAT            = 0x1001;
+    public static final int SERVICE_CATEGORY_CMAS_SEVERE_THREAT             = 0x1002;
+    public static final int SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY = 0x1003;
+    public static final int SERVICE_CATEGORY_CMAS_TEST_MESSAGE              = 0x1004;
+    public static final int SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE       = 0x10ff;
+
+    /**
+     * Provides the type of a SMS message like point to point, broadcast or acknowledge
+     */
+    public int messageType;
+
+    /**
+     * The 16-bit Teleservice parameter identifies which upper layer service access point is sending
+     * or receiving the message.
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.1)
+     */
+    public int teleService = TELESERVICE_NOT_SET;
+
+    /**
+     * The 16-bit service category parameter identifies the type of service provided
+     * by the SMS message.
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.2)
+     */
+    public int serviceCategory;
+
+    /**
+     * The origination address identifies the originator of the SMS message.
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.3)
+     */
+    public CdmaSmsAddress origAddress;
+
+    /**
+     * The destination address identifies the target of the SMS message.
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.3)
+     */
+    public CdmaSmsAddress destAddress;
+
+    /**
+     * The origination subaddress identifies the originator of the SMS message.
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.4)
+     */
+    public CdmaSmsSubaddress origSubaddress;
+
+    /**
+     * The 6-bit bearer reply parameter is used to request the return of a
+     * SMS Acknowledge Message.
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.5)
+     */
+    public int bearerReply;
+
+    /**
+     * Cause Code values:
+     * The cause code parameters are an indication whether an SMS error has occurred and if so,
+     * whether the condition is considered temporary or permanent.
+     * ReplySeqNo 6-bit value,
+     * ErrorClass 2-bit value,
+     * CauseCode 0-bit or 8-bit value
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.6)
+     */
+    public byte replySeqNo;
+    public byte errorClass;
+    public byte causeCode;
+
+    /**
+     * encoded bearer data
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.7)
+     */
+    public byte[] bearerData;
+
+    public SmsEnvelope() {
+        // nothing to see here
+    }
+
+}
+
diff --git a/com/android/internal/telephony/cdma/sms/UserData.java b/com/android/internal/telephony/cdma/sms/UserData.java
new file mode 100644
index 0000000..f879560
--- /dev/null
+++ b/com/android/internal/telephony/cdma/sms/UserData.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.cdma.sms;
+
+import android.util.SparseIntArray;
+
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.util.HexDump;
+
+public class UserData {
+
+    /**
+     * User data encoding types.
+     * (See 3GPP2 C.R1001-F, v1.0, table 9.1-1)
+     */
+    public static final int ENCODING_OCTET                      = 0x00;
+    public static final int ENCODING_IS91_EXTENDED_PROTOCOL     = 0x01;
+    public static final int ENCODING_7BIT_ASCII                 = 0x02;
+    public static final int ENCODING_IA5                        = 0x03;
+    public static final int ENCODING_UNICODE_16                 = 0x04;
+    public static final int ENCODING_SHIFT_JIS                  = 0x05;
+    public static final int ENCODING_KOREAN                     = 0x06;
+    public static final int ENCODING_LATIN_HEBREW               = 0x07;
+    public static final int ENCODING_LATIN                      = 0x08;
+    public static final int ENCODING_GSM_7BIT_ALPHABET          = 0x09;
+    public static final int ENCODING_GSM_DCS                    = 0x0A;
+
+    /**
+     * User data message type encoding types.
+     * (See 3GPP2 C.S0015-B, 4.5.2 and 3GPP 23.038, Section 4)
+     */
+    public static final int ENCODING_GSM_DCS_7BIT               = 0x00;
+    public static final int ENCODING_GSM_DCS_8BIT               = 0x01;
+    public static final int ENCODING_GSM_DCS_16BIT              = 0x02;
+
+    /**
+     * IS-91 message types.
+     * (See TIA/EIS/IS-91-A-ENGL 1999, table 3.7.1.1-3)
+     */
+    public static final int IS91_MSG_TYPE_VOICEMAIL_STATUS   = 0x82;
+    public static final int IS91_MSG_TYPE_SHORT_MESSAGE_FULL = 0x83;
+    public static final int IS91_MSG_TYPE_CLI                = 0x84;
+    public static final int IS91_MSG_TYPE_SHORT_MESSAGE      = 0x85;
+
+    /**
+     * US ASCII character mapping table.
+     *
+     * This table contains only the printable ASCII characters, with a
+     * 0x20 offset, meaning that the ASCII SPACE character is at index
+     * 0, with the resulting code of 0x20.
+     *
+     * Note this mapping is also equivalent to that used by both the
+     * IA5 and the IS-91 encodings.  For the former this is defined
+     * using CCITT Rec. T.50 Tables 1 and 3.  For the latter IS 637 B,
+     * Table 4.3.1.4.1-1 -- and note the encoding uses only 6 bits,
+     * and hence only maps entries up to the '_' character.
+     *
+     */
+    public static final char[] ASCII_MAP = {
+        ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
+        '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+        'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_',
+        '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'};
+
+    /**
+     * Character to use when forced to encode otherwise unencodable
+     * characters, meaning those not in the respective ASCII or GSM
+     * 7-bit encoding tables.  Current choice is SPACE, which is 0x20
+     * in both the GSM-7bit and ASCII-7bit encodings.
+     */
+    static final byte UNENCODABLE_7_BIT_CHAR = 0x20;
+
+    /**
+     * Only elements between these indices in the ASCII table are printable.
+     */
+    public static final int PRINTABLE_ASCII_MIN_INDEX = 0x20;
+    public static final int ASCII_NL_INDEX = 0x0A;
+    public static final int ASCII_CR_INDEX = 0x0D;
+    public static final SparseIntArray charToAscii = new SparseIntArray();
+    static {
+        for (int i = 0; i < ASCII_MAP.length; i++) {
+            charToAscii.put(ASCII_MAP[i], PRINTABLE_ASCII_MIN_INDEX + i);
+        }
+        charToAscii.put('\n', ASCII_NL_INDEX);
+        charToAscii.put('\r', ASCII_CR_INDEX);
+    }
+
+    /*
+     * TODO(cleanup): Move this very generic functionality somewhere
+     * more general.
+     */
+    /**
+     * Given a string generate a corresponding ASCII-encoded byte
+     * array, but limited to printable characters.  If the input
+     * contains unprintable characters, return null.
+     */
+    public static byte[] stringToAscii(String str) {
+        int len = str.length();
+        byte[] result = new byte[len];
+        for (int i = 0; i < len; i++) {
+            int charCode = charToAscii.get(str.charAt(i), -1);
+            if (charCode == -1) return null;
+            result[i] = (byte)charCode;
+        }
+        return result;
+    }
+
+    /**
+     * Mapping for ASCII values less than 32 are flow control signals
+     * and not used here.
+     */
+    public static final int ASCII_MAP_BASE_INDEX = 0x20;
+    public static final int ASCII_MAP_MAX_INDEX = ASCII_MAP_BASE_INDEX + ASCII_MAP.length - 1;
+
+    /**
+     * Contains the data header of the user data
+     */
+    public SmsHeader userDataHeader;
+
+    /**
+     * Contains the data encoding type for the SMS message
+     */
+    public int msgEncoding;
+    public boolean msgEncodingSet = false;
+
+    public int msgType;
+
+    /**
+     * Number of invalid bits in the last byte of data.
+     */
+    public int paddingBits;
+
+    public int numFields;
+
+    /**
+     * Contains the user data of a SMS message
+     * (See 3GPP2 C.S0015-B, v2, 4.5.2)
+     */
+    public byte[] payload;
+    public String payloadStr;
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("UserData ");
+        builder.append("{ msgEncoding=" + (msgEncodingSet ? msgEncoding : "unset"));
+        builder.append(", msgType=" + msgType);
+        builder.append(", paddingBits=" + paddingBits);
+        builder.append(", numFields=" + numFields);
+        builder.append(", userDataHeader=" + userDataHeader);
+        builder.append(", payload='" + HexDump.toHexString(payload) + "'");
+        builder.append(", payloadStr='" + payloadStr + "'");
+        builder.append(" }");
+        return builder.toString();
+    }
+
+}
diff --git a/com/android/internal/telephony/dataconnection/ApnContext.java b/com/android/internal/telephony/dataconnection/ApnContext.java
new file mode 100644
index 0000000..3cd804d
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/ApnContext.java
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.dataconnection;
+
+import android.app.PendingIntent;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.NetworkConfig;
+import android.net.NetworkRequest;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.SparseIntArray;
+
+import com.android.internal.R;
+import com.android.internal.telephony.DctConstants;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RetryManager;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Maintain the Apn context
+ */
+public class ApnContext {
+
+    public final String LOG_TAG;
+    private final static String SLOG_TAG = "ApnContext";
+
+    protected static final boolean DBG = false;
+
+    private final Phone mPhone;
+
+    private final String mApnType;
+
+    private DctConstants.State mState;
+
+    public final int priority;
+
+    private ApnSetting mApnSetting;
+
+    DcAsyncChannel mDcAc;
+
+    String mReason;
+
+    PendingIntent mReconnectAlarmIntent;
+
+    /**
+     * user/app requested connection on this APN
+     */
+    AtomicBoolean mDataEnabled;
+
+    private final Object mRefCountLock = new Object();
+    private int mRefCount = 0;
+
+    /**
+     * carrier requirements met
+     */
+    AtomicBoolean mDependencyMet;
+
+    private final DcTracker mDcTracker;
+
+    /**
+     * Remember this as a change in this value to a more permissive state
+     * should cause us to retry even permanent failures
+     */
+    private boolean mConcurrentVoiceAndDataAllowed;
+
+    /**
+     * used to track a single connection request so disconnects can get ignored if
+     * obsolete.
+     */
+    private final AtomicInteger mConnectionGeneration = new AtomicInteger(0);
+
+    /**
+     * Retry manager that handles the APN retry and delays.
+     */
+    private final RetryManager mRetryManager;
+
+    /**
+     * AonContext constructor
+     * @param phone phone object
+     * @param apnType APN type (e.g. default, supl, mms, etc...)
+     * @param logTag Tag for logging
+     * @param config Network configuration
+     * @param tracker Data call tracker
+     */
+    public ApnContext(Phone phone, String apnType, String logTag, NetworkConfig config,
+            DcTracker tracker) {
+        mPhone = phone;
+        mApnType = apnType;
+        mState = DctConstants.State.IDLE;
+        setReason(Phone.REASON_DATA_ENABLED);
+        mDataEnabled = new AtomicBoolean(false);
+        mDependencyMet = new AtomicBoolean(config.dependencyMet);
+        priority = config.priority;
+        LOG_TAG = logTag;
+        mDcTracker = tracker;
+        mRetryManager = new RetryManager(phone, apnType);
+    }
+
+    /**
+     * Get the APN type
+     * @return The APN type
+     */
+    public String getApnType() {
+        return mApnType;
+    }
+
+    /**
+     * Get the data call async channel.
+     * @return The data call async channel
+     */
+    public synchronized DcAsyncChannel getDcAc() {
+        return mDcAc;
+    }
+
+    /**
+     * Set the data call async channel.
+     * @param dcac The data call async channel
+     */
+    public synchronized void setDataConnectionAc(DcAsyncChannel dcac) {
+        if (DBG) {
+            log("setDataConnectionAc: old dcac=" + mDcAc + " new dcac=" + dcac
+                    + " this=" + this);
+        }
+        mDcAc = dcac;
+    }
+
+    /**
+     * Release data connection.
+     * @param reason The reason of releasing data connection
+     */
+    public synchronized void releaseDataConnection(String reason) {
+        if (mDcAc != null) {
+            mDcAc.tearDown(this, reason, null);
+            mDcAc = null;
+        }
+        setState(DctConstants.State.IDLE);
+    }
+
+    /**
+     * Get the reconnect intent.
+     * @return The reconnect intent
+     */
+    public synchronized PendingIntent getReconnectIntent() {
+        return mReconnectAlarmIntent;
+    }
+
+    /**
+     * Save the reconnect intent which can be used for cancelling later.
+     * @param intent The reconnect intent
+     */
+    public synchronized void setReconnectIntent(PendingIntent intent) {
+        mReconnectAlarmIntent = intent;
+    }
+
+    /**
+     * Get the current APN setting.
+     * @return APN setting
+     */
+    public synchronized ApnSetting getApnSetting() {
+        if (DBG) log("getApnSetting: apnSetting=" + mApnSetting);
+        return mApnSetting;
+    }
+
+    /**
+     * Set the APN setting.
+     * @param apnSetting APN setting
+     */
+    public synchronized void setApnSetting(ApnSetting apnSetting) {
+        if (DBG) log("setApnSetting: apnSetting=" + apnSetting);
+        mApnSetting = apnSetting;
+    }
+
+    /**
+     * Set the list of APN candidates which will be used for data call setup later.
+     * @param waitingApns List of APN candidates
+     */
+    public synchronized void setWaitingApns(ArrayList<ApnSetting> waitingApns) {
+        mRetryManager.setWaitingApns(waitingApns);
+    }
+
+    /**
+     * Get the next available APN to try.
+     * @return APN setting which will be used for data call setup. Return null if there is no
+     * APN can be retried.
+     */
+    public ApnSetting getNextApnSetting() {
+        return mRetryManager.getNextApnSetting();
+    }
+
+    /**
+     * Save the modem suggested delay for retrying the current APN.
+     * This method is called when we get the suggested delay from RIL.
+     * @param delay The delay in milliseconds
+     */
+    public void setModemSuggestedDelay(long delay) {
+        mRetryManager.setModemSuggestedDelay(delay);
+    }
+
+    /**
+     * Get the delay for trying the next APN setting if the current one failed.
+     * @param failFastEnabled True if fail fast mode enabled. In this case we'll use a shorter
+     *                        delay.
+     * @return The delay in milliseconds
+     */
+    public long getDelayForNextApn(boolean failFastEnabled) {
+        return mRetryManager.getDelayForNextApn(failFastEnabled || isFastRetryReason());
+    }
+
+    /**
+     * Mark the current APN setting permanently failed, which means it will not be retried anymore.
+     * @param apn APN setting
+     */
+    public void markApnPermanentFailed(ApnSetting apn) {
+        mRetryManager.markApnPermanentFailed(apn);
+    }
+
+    /**
+     * Get the list of waiting APNs.
+     * @return the list of waiting APNs
+     */
+    public ArrayList<ApnSetting> getWaitingApns() {
+        return mRetryManager.getWaitingApns();
+    }
+
+    /**
+     * Save the state indicating concurrent voice/data allowed.
+     * @param allowed True if concurrent voice/data is allowed
+     */
+    public synchronized void setConcurrentVoiceAndDataAllowed(boolean allowed) {
+        mConcurrentVoiceAndDataAllowed = allowed;
+    }
+
+    /**
+     * Get the state indicating concurrent voice/data allowed.
+     * @return True if concurrent voice/data is allowed
+     */
+    public synchronized boolean isConcurrentVoiceAndDataAllowed() {
+        return mConcurrentVoiceAndDataAllowed;
+    }
+
+    /**
+     * Set the current data call state.
+     * @param s Current data call state
+     */
+    public synchronized void setState(DctConstants.State s) {
+        if (DBG) {
+            log("setState: " + s + ", previous state:" + mState);
+        }
+
+        mState = s;
+
+        if (mState == DctConstants.State.FAILED) {
+            if (mRetryManager.getWaitingApns() != null) {
+                mRetryManager.getWaitingApns().clear(); // when teardown the connection and set to IDLE
+            }
+        }
+    }
+
+    /**
+     * Get the current data call state.
+     * @return The current data call state
+     */
+    public synchronized DctConstants.State getState() {
+        return mState;
+    }
+
+    /**
+     * Check whether the data call is disconnected or not.
+     * @return True if the data call is disconnected
+     */
+    public boolean isDisconnected() {
+        DctConstants.State currentState = getState();
+        return ((currentState == DctConstants.State.IDLE) ||
+                    currentState == DctConstants.State.FAILED);
+    }
+
+    /**
+     * Set the reason for data call connection.
+     * @param reason Reason for data call connection
+     */
+    public synchronized void setReason(String reason) {
+        if (DBG) {
+            log("set reason as " + reason + ",current state " + mState);
+        }
+        mReason = reason;
+    }
+
+    /**
+     * Get the reason for data call connection.
+     * @return The reason for data call connection
+     */
+    public synchronized String getReason() {
+        return mReason;
+    }
+
+    /**
+     * Check if ready for data call connection
+     * @return True if ready, otherwise false.
+     */
+    public boolean isReady() {
+        return mDataEnabled.get() && mDependencyMet.get();
+    }
+
+    /**
+     * Check if the data call is in the state which allow connecting.
+     * @return True if allowed, otherwise false.
+     */
+    public boolean isConnectable() {
+        return isReady() && ((mState == DctConstants.State.IDLE)
+                                || (mState == DctConstants.State.SCANNING)
+                                || (mState == DctConstants.State.RETRYING)
+                                || (mState == DctConstants.State.FAILED));
+    }
+
+    /**
+     * Check if apn reason is fast retry reason which should apply shorter delay between apn re-try.
+     * @return True if it is fast retry reason, otherwise false.
+     */
+    private boolean isFastRetryReason() {
+        return Phone.REASON_NW_TYPE_CHANGED.equals(mReason) ||
+                Phone.REASON_APN_CHANGED.equals(mReason);
+    }
+
+    /** Check if the data call is in connected or connecting state.
+     * @return True if the data call is in connected or connecting state
+     */
+    public boolean isConnectedOrConnecting() {
+        return isReady() && ((mState == DctConstants.State.CONNECTED)
+                                || (mState == DctConstants.State.CONNECTING)
+                                || (mState == DctConstants.State.SCANNING)
+                                || (mState == DctConstants.State.RETRYING));
+    }
+
+    /**
+     * Set data call enabled/disabled state.
+     * @param enabled True if data call is enabled
+     */
+    public void setEnabled(boolean enabled) {
+        if (DBG) {
+            log("set enabled as " + enabled + ", current state is " + mDataEnabled.get());
+        }
+        mDataEnabled.set(enabled);
+    }
+
+    /**
+     * Check if the data call is enabled or not.
+     * @return True if enabled
+     */
+    public boolean isEnabled() {
+        return mDataEnabled.get();
+    }
+
+    public void setDependencyMet(boolean met) {
+        if (DBG) {
+            log("set mDependencyMet as " + met + " current state is " + mDependencyMet.get());
+        }
+        mDependencyMet.set(met);
+    }
+
+    public boolean getDependencyMet() {
+       return mDependencyMet.get();
+    }
+
+    public boolean isProvisioningApn() {
+        String provisioningApn = mPhone.getContext().getResources()
+                .getString(R.string.mobile_provisioning_apn);
+        if (!TextUtils.isEmpty(provisioningApn) &&
+                (mApnSetting != null) && (mApnSetting.apn != null)) {
+            return (mApnSetting.apn.equals(provisioningApn));
+        } else {
+            return false;
+        }
+    }
+
+    private final ArrayList<LocalLog> mLocalLogs = new ArrayList<>();
+    private final ArrayList<NetworkRequest> mNetworkRequests = new ArrayList<>();
+    private final ArrayDeque<LocalLog> mHistoryLogs = new ArrayDeque<>();
+    private final static int MAX_HISTORY_LOG_COUNT = 4;
+
+    public void requestLog(String str) {
+        synchronized (mRefCountLock) {
+            for (LocalLog l : mLocalLogs) {
+                l.log(str);
+            }
+        }
+    }
+
+    public void requestNetwork(NetworkRequest networkRequest, LocalLog log) {
+        synchronized (mRefCountLock) {
+            if (mLocalLogs.contains(log) || mNetworkRequests.contains(networkRequest)) {
+                log.log("ApnContext.requestNetwork has duplicate add - " + mNetworkRequests.size());
+            } else {
+                mLocalLogs.add(log);
+                mNetworkRequests.add(networkRequest);
+                mDcTracker.setEnabled(apnIdForApnName(mApnType), true);
+            }
+        }
+    }
+
+    public void releaseNetwork(NetworkRequest networkRequest, LocalLog log) {
+        synchronized (mRefCountLock) {
+            if (mLocalLogs.contains(log) == false) {
+                log.log("ApnContext.releaseNetwork can't find this log");
+            } else {
+                mLocalLogs.remove(log);
+            }
+            if (mNetworkRequests.contains(networkRequest) == false) {
+                log.log("ApnContext.releaseNetwork can't find this request ("
+                        + networkRequest + ")");
+            } else {
+                mNetworkRequests.remove(networkRequest);
+                log.log("ApnContext.releaseNetwork left with " + mNetworkRequests.size() +
+                        " requests.");
+                if (mNetworkRequests.size() == 0) {
+                    mDcTracker.setEnabled(apnIdForApnName(mApnType), false);
+                }
+            }
+        }
+    }
+
+    public List<NetworkRequest> getNetworkRequests() {
+        synchronized (mRefCountLock) {
+            return new ArrayList<NetworkRequest>(mNetworkRequests);
+        }
+    }
+
+    public boolean hasNoRestrictedRequests(boolean excludeDun) {
+        synchronized (mRefCountLock) {
+            for (NetworkRequest nr : mNetworkRequests) {
+                if (excludeDun &&
+                        nr.networkCapabilities.hasCapability(
+                        NetworkCapabilities.NET_CAPABILITY_DUN)) {
+                    continue;
+                }
+                if (nr.networkCapabilities.hasCapability(
+                        NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) == false) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private final SparseIntArray mRetriesLeftPerErrorCode = new SparseIntArray();
+
+    public void resetErrorCodeRetries() {
+        requestLog("ApnContext.resetErrorCodeRetries");
+        if (DBG) log("ApnContext.resetErrorCodeRetries");
+
+        String[] config = mPhone.getContext().getResources().getStringArray(
+                com.android.internal.R.array.config_cell_retries_per_error_code);
+        synchronized (mRetriesLeftPerErrorCode) {
+            mRetriesLeftPerErrorCode.clear();
+
+            for (String c : config) {
+                String errorValue[] = c.split(",");
+                if (errorValue != null && errorValue.length == 2) {
+                    int count = 0;
+                    int errorCode = 0;
+                    try {
+                        errorCode = Integer.parseInt(errorValue[0]);
+                        count = Integer.parseInt(errorValue[1]);
+                    } catch (NumberFormatException e) {
+                        log("Exception parsing config_retries_per_error_code: " + e);
+                        continue;
+                    }
+                    if (count > 0 && errorCode > 0) {
+                        mRetriesLeftPerErrorCode.put(errorCode, count);
+                    }
+                } else {
+                    log("Exception parsing config_retries_per_error_code: " + c);
+                }
+            }
+        }
+    }
+
+    public boolean restartOnError(int errorCode) {
+        boolean result = false;
+        int retriesLeft = 0;
+        synchronized(mRetriesLeftPerErrorCode) {
+            retriesLeft = mRetriesLeftPerErrorCode.get(errorCode);
+            switch (retriesLeft) {
+                case 0: {
+                    // not set, never restart modem
+                    break;
+                }
+                case 1: {
+                    resetErrorCodeRetries();
+                    result = true;
+                    break;
+                }
+                default: {
+                    mRetriesLeftPerErrorCode.put(errorCode, retriesLeft - 1);
+                    result = false;
+                }
+            }
+        }
+        String str = "ApnContext.restartOnError(" + errorCode + ") found " + retriesLeft +
+                " and returned " + result;
+        if (DBG) log(str);
+        requestLog(str);
+        return result;
+    }
+
+    public int incAndGetConnectionGeneration() {
+        return mConnectionGeneration.incrementAndGet();
+    }
+
+    public int getConnectionGeneration() {
+        return mConnectionGeneration.get();
+    }
+
+    long getRetryAfterDisconnectDelay() {
+        return mRetryManager.getRetryAfterDisconnectDelay();
+    }
+
+    public static int apnIdForType(int networkType) {
+        switch (networkType) {
+        case ConnectivityManager.TYPE_MOBILE:
+            return DctConstants.APN_DEFAULT_ID;
+        case ConnectivityManager.TYPE_MOBILE_MMS:
+            return DctConstants.APN_MMS_ID;
+        case ConnectivityManager.TYPE_MOBILE_SUPL:
+            return DctConstants.APN_SUPL_ID;
+        case ConnectivityManager.TYPE_MOBILE_DUN:
+            return DctConstants.APN_DUN_ID;
+        case ConnectivityManager.TYPE_MOBILE_FOTA:
+            return DctConstants.APN_FOTA_ID;
+        case ConnectivityManager.TYPE_MOBILE_IMS:
+            return DctConstants.APN_IMS_ID;
+        case ConnectivityManager.TYPE_MOBILE_CBS:
+            return DctConstants.APN_CBS_ID;
+        case ConnectivityManager.TYPE_MOBILE_IA:
+            return DctConstants.APN_IA_ID;
+        case ConnectivityManager.TYPE_MOBILE_EMERGENCY:
+            return DctConstants.APN_EMERGENCY_ID;
+        default:
+            return DctConstants.APN_INVALID_ID;
+        }
+    }
+
+    public static int apnIdForNetworkRequest(NetworkRequest nr) {
+        NetworkCapabilities nc = nr.networkCapabilities;
+        // For now, ignore the bandwidth stuff
+        if (nc.getTransportTypes().length > 0 &&
+                nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == false) {
+            return DctConstants.APN_INVALID_ID;
+        }
+
+        // in the near term just do 1-1 matches.
+        // TODO - actually try to match the set of capabilities
+        int apnId = DctConstants.APN_INVALID_ID;
+        boolean error = false;
+
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            apnId = DctConstants.APN_DEFAULT_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_MMS_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_SUPL_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_DUN_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_FOTA_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_IMS_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_CBS_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_IA)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_IA_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_RCS)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+
+            Rlog.d(SLOG_TAG, "RCS APN type not yet supported");
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_XCAP)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+
+            Rlog.d(SLOG_TAG, "XCAP APN type not yet supported");
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_EIMS)) {
+            if (apnId != DctConstants.APN_INVALID_ID) error = true;
+            apnId = DctConstants.APN_EMERGENCY_ID;
+        }
+        if (error) {
+            // TODO: If this error condition is removed, the framework's handling of
+            // NET_CAPABILITY_NOT_RESTRICTED will need to be updated so requests for
+            // say FOTA and INTERNET are marked as restricted.  This is not how
+            // NetworkCapabilities.maybeMarkCapabilitiesRestricted currently works.
+            Rlog.d(SLOG_TAG, "Multiple apn types specified in request - result is unspecified!");
+        }
+        if (apnId == DctConstants.APN_INVALID_ID) {
+            Rlog.d(SLOG_TAG, "Unsupported NetworkRequest in Telephony: nr=" + nr);
+        }
+        return apnId;
+    }
+
+    // TODO - kill The use of these strings
+    public static int apnIdForApnName(String type) {
+        switch (type) {
+            case PhoneConstants.APN_TYPE_DEFAULT:
+                return DctConstants.APN_DEFAULT_ID;
+            case PhoneConstants.APN_TYPE_MMS:
+                return DctConstants.APN_MMS_ID;
+            case PhoneConstants.APN_TYPE_SUPL:
+                return DctConstants.APN_SUPL_ID;
+            case PhoneConstants.APN_TYPE_DUN:
+                return DctConstants.APN_DUN_ID;
+            case PhoneConstants.APN_TYPE_HIPRI:
+                return DctConstants.APN_HIPRI_ID;
+            case PhoneConstants.APN_TYPE_IMS:
+                return DctConstants.APN_IMS_ID;
+            case PhoneConstants.APN_TYPE_FOTA:
+                return DctConstants.APN_FOTA_ID;
+            case PhoneConstants.APN_TYPE_CBS:
+                return DctConstants.APN_CBS_ID;
+            case PhoneConstants.APN_TYPE_IA:
+                return DctConstants.APN_IA_ID;
+            case PhoneConstants.APN_TYPE_EMERGENCY:
+                return DctConstants.APN_EMERGENCY_ID;
+            default:
+                return DctConstants.APN_INVALID_ID;
+        }
+    }
+
+    private static String apnNameForApnId(int id) {
+        switch (id) {
+            case DctConstants.APN_DEFAULT_ID:
+                return PhoneConstants.APN_TYPE_DEFAULT;
+            case DctConstants.APN_MMS_ID:
+                return PhoneConstants.APN_TYPE_MMS;
+            case DctConstants.APN_SUPL_ID:
+                return PhoneConstants.APN_TYPE_SUPL;
+            case DctConstants.APN_DUN_ID:
+                return PhoneConstants.APN_TYPE_DUN;
+            case DctConstants.APN_HIPRI_ID:
+                return PhoneConstants.APN_TYPE_HIPRI;
+            case DctConstants.APN_IMS_ID:
+                return PhoneConstants.APN_TYPE_IMS;
+            case DctConstants.APN_FOTA_ID:
+                return PhoneConstants.APN_TYPE_FOTA;
+            case DctConstants.APN_CBS_ID:
+                return PhoneConstants.APN_TYPE_CBS;
+            case DctConstants.APN_IA_ID:
+                return PhoneConstants.APN_TYPE_IA;
+            case DctConstants.APN_EMERGENCY_ID:
+                return PhoneConstants.APN_TYPE_EMERGENCY;
+            default:
+                Rlog.d(SLOG_TAG, "Unknown id (" + id + ") in apnIdToType");
+                return PhoneConstants.APN_TYPE_DEFAULT;
+        }
+    }
+
+    @Override
+    public synchronized String toString() {
+        // We don't print mDataConnection because its recursive.
+        return "{mApnType=" + mApnType + " mState=" + getState() + " mWaitingApns={" +
+                mRetryManager.getWaitingApns() + "}" + " mApnSetting={" + mApnSetting +
+                "} mReason=" + mReason + " mDataEnabled=" + mDataEnabled + " mDependencyMet=" +
+                mDependencyMet + "}";
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[ApnContext:" + mApnType + "] " + s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        synchronized (mRefCountLock) {
+            pw.println(toString());
+            if (mNetworkRequests.size() > 0) {
+                pw.println("NetworkRequests:");
+                pw.increaseIndent();
+                for (NetworkRequest nr : mNetworkRequests) {
+                    pw.println(nr);
+                }
+                pw.decreaseIndent();
+            }
+            pw.increaseIndent();
+            for (LocalLog l : mLocalLogs) {
+                l.dump(fd, pw, args);
+            }
+            if (mHistoryLogs.size() > 0) pw.println("Historical Logs:");
+            for (LocalLog l : mHistoryLogs) {
+                l.dump(fd, pw, args);
+            }
+            pw.decreaseIndent();
+            pw.println(mRetryManager);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/ApnSetting.java b/com/android/internal/telephony/dataconnection/ApnSetting.java
new file mode 100644
index 0000000..ce8318d
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/ApnSetting.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.dataconnection;
+
+import android.content.Context;
+import android.hardware.radio.V1_0.ApnTypes;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.uicc.IccRecords;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class represents a apn setting for create PDP link
+ */
+public class ApnSetting {
+
+    static final String LOG_TAG = "ApnSetting";
+
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    static final String V2_FORMAT_REGEX = "^\\[ApnSettingV2\\]\\s*";
+    static final String V3_FORMAT_REGEX = "^\\[ApnSettingV3\\]\\s*";
+
+    public final String carrier;
+    public final String apn;
+    public final String proxy;
+    public final String port;
+    public final String mmsc;
+    public final String mmsProxy;
+    public final String mmsPort;
+    public final String user;
+    public final String password;
+    public final int authType;
+    public final String[] types;
+    public final int typesBitmap;
+    public final int id;
+    public final String numeric;
+    public final String protocol;
+    public final String roamingProtocol;
+    public final int mtu;
+
+    /**
+      * Current status of APN
+      * true : enabled APN, false : disabled APN.
+      */
+    public final boolean carrierEnabled;
+    /**
+     * Radio Access Technology info
+     * To check what values can hold, refer to ServiceState.java.
+     * This should be spread to other technologies,
+     * but currently only used for LTE(14) and EHRPD(13).
+     */
+    private final int bearer;
+    /**
+      * Radio Access Technology info
+      * To check what values can hold, refer to ServiceState.java. This is a bitmask of radio
+      * technologies in ServiceState.
+      * This should be spread to other technologies,
+      * but currently only used for LTE(14) and EHRPD(13).
+      */
+    public final int bearerBitmask;
+
+    /* ID of the profile in the modem */
+    public final int profileId;
+    public final boolean modemCognitive;
+    public final int maxConns;
+    public final int waitTime;
+    public final int maxConnsTime;
+
+    /**
+      * MVNO match type. Possible values:
+      *   "spn": Service provider name.
+      *   "imsi": IMSI.
+      *   "gid": Group identifier level 1.
+      */
+    public final String mvnoType;
+    /**
+      * MVNO data. Examples:
+      *   "spn": A MOBILE, BEN NL
+      *   "imsi": 302720x94, 2060188
+      *   "gid": 4E, 33
+      */
+    public final String mvnoMatchData;
+
+    /**
+     * Indicates this APN setting is permanently failed and cannot be
+     * retried by the retry manager anymore.
+     * */
+    public boolean permanentFailed = false;
+
+    public ApnSetting(int id, String numeric, String carrier, String apn,
+                      String proxy, String port,
+                      String mmsc, String mmsProxy, String mmsPort,
+                      String user, String password, int authType, String[] types,
+                      String protocol, String roamingProtocol, boolean carrierEnabled, int bearer,
+                      int bearerBitmask, int profileId, boolean modemCognitive, int maxConns,
+                      int waitTime, int maxConnsTime, int mtu, String mvnoType,
+                      String mvnoMatchData) {
+        this.id = id;
+        this.numeric = numeric;
+        this.carrier = carrier;
+        this.apn = apn;
+        this.proxy = proxy;
+        this.port = port;
+        this.mmsc = mmsc;
+        this.mmsProxy = mmsProxy;
+        this.mmsPort = mmsPort;
+        this.user = user;
+        this.password = password;
+        this.authType = authType;
+        this.types = new String[types.length];
+        int apnBitmap = 0;
+        for (int i = 0; i < types.length; i++) {
+            this.types[i] = types[i].toLowerCase();
+            apnBitmap |= getApnBitmask(this.types[i]);
+        }
+        this.typesBitmap = apnBitmap;
+        this.protocol = protocol;
+        this.roamingProtocol = roamingProtocol;
+        this.carrierEnabled = carrierEnabled;
+        this.bearer = bearer;
+        this.bearerBitmask = (bearerBitmask | ServiceState.getBitmaskForTech(bearer));
+        this.profileId = profileId;
+        this.modemCognitive = modemCognitive;
+        this.maxConns = maxConns;
+        this.waitTime = waitTime;
+        this.maxConnsTime = maxConnsTime;
+        this.mtu = mtu;
+        this.mvnoType = mvnoType;
+        this.mvnoMatchData = mvnoMatchData;
+
+    }
+
+    public ApnSetting(ApnSetting apn) {
+        this(apn.id, apn.numeric, apn.carrier, apn.apn, apn.proxy, apn.port, apn.mmsc, apn.mmsProxy,
+                apn.mmsPort, apn.user, apn.password, apn.authType, apn.types, apn.protocol,
+                apn.roamingProtocol, apn.carrierEnabled, apn.bearer, apn.bearerBitmask,
+                apn.profileId, apn.modemCognitive, apn.maxConns, apn.waitTime, apn.maxConnsTime,
+                apn.mtu, apn.mvnoType, apn.mvnoMatchData);
+    }
+
+    /**
+     * Creates an ApnSetting object from a string.
+     *
+     * @param data the string to read.
+     *
+     * The string must be in one of two formats (newlines added for clarity,
+     * spaces are optional):
+     *
+     * v1 format:
+     *   <carrier>, <apn>, <proxy>, <port>, <user>, <password>, <server>,
+     *   <mmsc>, <mmsproxy>, <mmsport>, <mcc>, <mnc>, <authtype>,
+     *   <type>[| <type>...],
+     *
+     * v2 format:
+     *   [ApnSettingV2] <carrier>, <apn>, <proxy>, <port>, <user>, <password>, <server>,
+     *   <mmsc>, <mmsproxy>, <mmsport>, <mcc>, <mnc>, <authtype>,
+     *   <type>[| <type>...], <protocol>, <roaming_protocol>, <carrierEnabled>, <bearerBitmask>,
+     *
+     * v3 format:
+     *   [ApnSettingV3] <carrier>, <apn>, <proxy>, <port>, <user>, <password>, <server>,
+     *   <mmsc>, <mmsproxy>, <mmsport>, <mcc>, <mnc>, <authtype>,
+     *   <type>[| <type>...], <protocol>, <roaming_protocol>, <carrierEnabled>, <bearerBitmask>,
+     *   <profileId>, <modemCognitive>, <maxConns>, <waitTime>, <maxConnsTime>, <mtu>,
+     *   <mvnoType>, <mvnoMatchData>
+     *
+     * Note that the strings generated by toString() do not contain the username
+     * and password and thus cannot be read by this method.
+     */
+    public static ApnSetting fromString(String data) {
+        if (data == null) return null;
+
+        int version;
+        // matches() operates on the whole string, so append .* to the regex.
+        if (data.matches(V3_FORMAT_REGEX + ".*")) {
+            version = 3;
+            data = data.replaceFirst(V3_FORMAT_REGEX, "");
+        } else if (data.matches(V2_FORMAT_REGEX + ".*")) {
+            version = 2;
+            data = data.replaceFirst(V2_FORMAT_REGEX, "");
+        } else {
+            version = 1;
+        }
+
+        String[] a = data.split("\\s*,\\s*");
+        if (a.length < 14) {
+            return null;
+        }
+
+        int authType;
+        try {
+            authType = Integer.parseInt(a[12]);
+        } catch (NumberFormatException e) {
+            authType = 0;
+        }
+
+        String[] typeArray;
+        String protocol, roamingProtocol;
+        boolean carrierEnabled;
+        int bearerBitmask = 0;
+        int profileId = 0;
+        boolean modemCognitive = false;
+        int maxConns = 0;
+        int waitTime = 0;
+        int maxConnsTime = 0;
+        int mtu = PhoneConstants.UNSET_MTU;
+        String mvnoType = "";
+        String mvnoMatchData = "";
+        if (version == 1) {
+            typeArray = new String[a.length - 13];
+            System.arraycopy(a, 13, typeArray, 0, a.length - 13);
+            protocol = RILConstants.SETUP_DATA_PROTOCOL_IP;
+            roamingProtocol = RILConstants.SETUP_DATA_PROTOCOL_IP;
+            carrierEnabled = true;
+        } else {
+            if (a.length < 18) {
+                return null;
+            }
+            typeArray = a[13].split("\\s*\\|\\s*");
+            protocol = a[14];
+            roamingProtocol = a[15];
+            carrierEnabled = Boolean.parseBoolean(a[16]);
+
+            bearerBitmask = ServiceState.getBitmaskFromString(a[17]);
+
+            if (a.length > 22) {
+                modemCognitive = Boolean.parseBoolean(a[19]);
+                try {
+                    profileId = Integer.parseInt(a[18]);
+                    maxConns = Integer.parseInt(a[20]);
+                    waitTime = Integer.parseInt(a[21]);
+                    maxConnsTime = Integer.parseInt(a[22]);
+                } catch (NumberFormatException e) {
+                }
+            }
+            if (a.length > 23) {
+                try {
+                    mtu = Integer.parseInt(a[23]);
+                } catch (NumberFormatException e) {
+                }
+            }
+            if (a.length > 25) {
+                mvnoType = a[24];
+                mvnoMatchData = a[25];
+            }
+        }
+
+        return new ApnSetting(-1,a[10]+a[11],a[0],a[1],a[2],a[3],a[7],a[8],
+                a[9],a[4],a[5],authType,typeArray,protocol,roamingProtocol,carrierEnabled,0,
+                bearerBitmask, profileId, modemCognitive, maxConns, waitTime, maxConnsTime, mtu,
+                mvnoType, mvnoMatchData);
+    }
+
+    /**
+     * Creates an array of ApnSetting objects from a string.
+     *
+     * @param data the string to read.
+     *
+     * Builds on top of the same format used by fromString, but allows for multiple entries
+     * separated by "; ".
+     */
+    public static List<ApnSetting> arrayFromString(String data) {
+        List<ApnSetting> retVal = new ArrayList<ApnSetting>();
+        if (TextUtils.isEmpty(data)) {
+            return retVal;
+        }
+        String[] apnStrings = data.split("\\s*;\\s*");
+        for (String apnString : apnStrings) {
+            ApnSetting apn = fromString(apnString);
+            if (apn != null) {
+                retVal.add(apn);
+            }
+        }
+        return retVal;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[ApnSettingV3] ")
+        .append(carrier)
+        .append(", ").append(id)
+        .append(", ").append(numeric)
+        .append(", ").append(apn)
+        .append(", ").append(proxy)
+        .append(", ").append(mmsc)
+        .append(", ").append(mmsProxy)
+        .append(", ").append(mmsPort)
+        .append(", ").append(port)
+        .append(", ").append(authType).append(", ");
+        for (int i = 0; i < types.length; i++) {
+            sb.append(types[i]);
+            if (i < types.length - 1) {
+                sb.append(" | ");
+            }
+        }
+        sb.append(", ").append(protocol);
+        sb.append(", ").append(roamingProtocol);
+        sb.append(", ").append(carrierEnabled);
+        sb.append(", ").append(bearer);
+        sb.append(", ").append(bearerBitmask);
+        sb.append(", ").append(profileId);
+        sb.append(", ").append(modemCognitive);
+        sb.append(", ").append(maxConns);
+        sb.append(", ").append(waitTime);
+        sb.append(", ").append(maxConnsTime);
+        sb.append(", ").append(mtu);
+        sb.append(", ").append(mvnoType);
+        sb.append(", ").append(mvnoMatchData);
+        sb.append(", ").append(permanentFailed);
+        return sb.toString();
+    }
+
+    /**
+     * Returns true if there are MVNO params specified.
+     */
+    public boolean hasMvnoParams() {
+        return !TextUtils.isEmpty(mvnoType) && !TextUtils.isEmpty(mvnoMatchData);
+    }
+
+    public boolean canHandleType(String type) {
+        if (!carrierEnabled) return false;
+        boolean wildcardable = true;
+        if (PhoneConstants.APN_TYPE_IA.equalsIgnoreCase(type)) wildcardable = false;
+        for (String t : types) {
+            // DEFAULT handles all, and HIPRI is handled by DEFAULT
+            if (t.equalsIgnoreCase(type) ||
+                    (wildcardable && t.equalsIgnoreCase(PhoneConstants.APN_TYPE_ALL)) ||
+                    (t.equalsIgnoreCase(PhoneConstants.APN_TYPE_DEFAULT) &&
+                    type.equalsIgnoreCase(PhoneConstants.APN_TYPE_HIPRI))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean imsiMatches(String imsiDB, String imsiSIM) {
+        // Note: imsiDB value has digit number or 'x' character for seperating USIM information
+        // for MVNO operator. And then digit number is matched at same order and 'x' character
+        // could replace by any digit number.
+        // ex) if imsiDB inserted '310260x10xxxxxx' for GG Operator,
+        //     that means first 6 digits, 8th and 9th digit
+        //     should be set in USIM for GG Operator.
+        int len = imsiDB.length();
+        int idxCompare = 0;
+
+        if (len <= 0) return false;
+        if (len > imsiSIM.length()) return false;
+
+        for (int idx=0; idx<len; idx++) {
+            char c = imsiDB.charAt(idx);
+            if ((c == 'x') || (c == 'X') || (c == imsiSIM.charAt(idx))) {
+                continue;
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean mvnoMatches(IccRecords r, String mvnoType, String mvnoMatchData) {
+        if (mvnoType.equalsIgnoreCase("spn")) {
+            if ((r.getServiceProviderName() != null) &&
+                    r.getServiceProviderName().equalsIgnoreCase(mvnoMatchData)) {
+                return true;
+            }
+        } else if (mvnoType.equalsIgnoreCase("imsi")) {
+            String imsiSIM = r.getIMSI();
+            if ((imsiSIM != null) && imsiMatches(mvnoMatchData, imsiSIM)) {
+                return true;
+            }
+        } else if (mvnoType.equalsIgnoreCase("gid")) {
+            String gid1 = r.getGid1();
+            int mvno_match_data_length = mvnoMatchData.length();
+            if ((gid1 != null) && (gid1.length() >= mvno_match_data_length) &&
+                    gid1.substring(0, mvno_match_data_length).equalsIgnoreCase(mvnoMatchData)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check if this APN type is metered.
+     *
+     * @param type The APN type
+     * @param phone The phone object
+     * @return True if the APN type is metered, otherwise false.
+     */
+    public static boolean isMeteredApnType(String type, Phone phone) {
+        if (phone == null) {
+            return true;
+        }
+
+        boolean isRoaming = phone.getServiceState().getDataRoaming();
+        boolean isIwlan = phone.getServiceState().getRilDataRadioTechnology()
+                == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
+        int subId = phone.getSubId();
+
+        String carrierConfig;
+        // First check if the device is in IWLAN mode. If yes, use the IWLAN metered APN list. Then
+        // check if the device is roaming. If yes, use the roaming metered APN list. Otherwise, use
+        // the normal metered APN list.
+        if (isIwlan) {
+            carrierConfig = CarrierConfigManager.KEY_CARRIER_METERED_IWLAN_APN_TYPES_STRINGS;
+        } else if (isRoaming) {
+            carrierConfig = CarrierConfigManager.KEY_CARRIER_METERED_ROAMING_APN_TYPES_STRINGS;
+        } else {
+            carrierConfig = CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS;
+        }
+
+        if (DBG) {
+            Rlog.d(LOG_TAG, "isMeteredApnType: isRoaming=" + isRoaming + ", isIwlan=" + isIwlan);
+        }
+
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) {
+            Rlog.e(LOG_TAG, "Carrier config service is not available");
+            return true;
+        }
+
+        PersistableBundle b = configManager.getConfigForSubId(subId);
+        if (b == null) {
+            Rlog.e(LOG_TAG, "Can't get the config. subId = " + subId);
+            return true;
+        }
+
+        String[] meteredApnTypes = b.getStringArray(carrierConfig);
+        if (meteredApnTypes == null) {
+            Rlog.e(LOG_TAG, carrierConfig +  " is not available. " + "subId = " + subId);
+            return true;
+        }
+
+        HashSet<String> meteredApnSet = new HashSet<>(Arrays.asList(meteredApnTypes));
+        if (DBG) {
+            Rlog.d(LOG_TAG, "For subId = " + subId + ", metered APN types are "
+                    + Arrays.toString(meteredApnSet.toArray()));
+        }
+
+        // If all types of APN are metered, then this APN setting must be metered.
+        if (meteredApnSet.contains(PhoneConstants.APN_TYPE_ALL)) {
+            if (DBG) Rlog.d(LOG_TAG, "All APN types are metered.");
+            return true;
+        }
+
+        if (meteredApnSet.contains(type)) {
+            if (DBG) Rlog.d(LOG_TAG, type + " is metered.");
+            return true;
+        } else if (type.equals(PhoneConstants.APN_TYPE_ALL)) {
+            // Assuming no configuration error, if at least one APN type is
+            // metered, then this APN setting is metered.
+            if (meteredApnSet.size() > 0) {
+                if (DBG) Rlog.d(LOG_TAG, "APN_TYPE_ALL APN is metered.");
+                return true;
+            }
+        }
+
+        if (DBG) Rlog.d(LOG_TAG, type + " is not metered.");
+        return false;
+    }
+
+    /**
+     * Check if this APN setting is metered.
+     *
+     * @param phone The phone object
+     * @return True if this APN setting is metered, otherwise false.
+     */
+    public boolean isMetered(Phone phone) {
+        if (phone == null) {
+            return true;
+        }
+
+        for (String type : types) {
+            // If one of the APN type is metered, then this APN setting is metered.
+            if (isMeteredApnType(type, phone)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // TODO - if we have this function we should also have hashCode.
+    // Also should handle changes in type order and perhaps case-insensitivity
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof ApnSetting == false) {
+            return false;
+        }
+
+        ApnSetting other = (ApnSetting) o;
+
+        return carrier.equals(other.carrier)
+                && id == other.id
+                && numeric.equals(other.numeric)
+                && apn.equals(other.apn)
+                && proxy.equals(other.proxy)
+                && mmsc.equals(other.mmsc)
+                && mmsProxy.equals(other.mmsProxy)
+                && TextUtils.equals(mmsPort, other.mmsPort)
+                && port.equals(other.port)
+                && TextUtils.equals(user, other.user)
+                && TextUtils.equals(password, other.password)
+                && authType == other.authType
+                && Arrays.deepEquals(types, other.types)
+                && typesBitmap == other.typesBitmap
+                && protocol.equals(other.protocol)
+                && roamingProtocol.equals(other.roamingProtocol)
+                && carrierEnabled == other.carrierEnabled
+                && bearer == other.bearer
+                && bearerBitmask == other.bearerBitmask
+                && profileId == other.profileId
+                && modemCognitive == other.modemCognitive
+                && maxConns == other.maxConns
+                && waitTime == other.waitTime
+                && maxConnsTime == other.maxConnsTime
+                && mtu == other.mtu
+                && mvnoType.equals(other.mvnoType)
+                && mvnoMatchData.equals(other.mvnoMatchData);
+    }
+
+    /**
+     * Compare two APN settings
+     *
+     * Note: This method does not compare 'id', 'bearer', 'bearerBitmask'. We only use this for
+     * determining if tearing a data call is needed when conditions change. See
+     * cleanUpConnectionsOnUpdatedApns in DcTracker.
+     *
+     * @param o the other object to compare
+     * @param isDataRoaming True if the device is on data roaming
+     * @return True if the two APN settings are same
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public boolean equals(Object o, boolean isDataRoaming) {
+        if (!(o instanceof ApnSetting)) {
+            return false;
+        }
+
+        ApnSetting other = (ApnSetting) o;
+
+        return carrier.equals(other.carrier)
+                && numeric.equals(other.numeric)
+                && apn.equals(other.apn)
+                && proxy.equals(other.proxy)
+                && mmsc.equals(other.mmsc)
+                && mmsProxy.equals(other.mmsProxy)
+                && TextUtils.equals(mmsPort, other.mmsPort)
+                && port.equals(other.port)
+                && TextUtils.equals(user, other.user)
+                && TextUtils.equals(password, other.password)
+                && authType == other.authType
+                && Arrays.deepEquals(types, other.types)
+                && typesBitmap == other.typesBitmap
+                && (isDataRoaming || protocol.equals(other.protocol))
+                && (!isDataRoaming || roamingProtocol.equals(other.roamingProtocol))
+                && carrierEnabled == other.carrierEnabled
+                && profileId == other.profileId
+                && modemCognitive == other.modemCognitive
+                && maxConns == other.maxConns
+                && waitTime == other.waitTime
+                && maxConnsTime == other.maxConnsTime
+                && mtu == other.mtu
+                && mvnoType.equals(other.mvnoType)
+                && mvnoMatchData.equals(other.mvnoMatchData);
+    }
+
+    /**
+     * Check if neither mention DUN and are substantially similar
+     *
+     * @param other The other APN settings to compare
+     * @return True if two APN settings are similar
+     */
+    public boolean similar(ApnSetting other) {
+        return (!this.canHandleType(PhoneConstants.APN_TYPE_DUN)
+                && !other.canHandleType(PhoneConstants.APN_TYPE_DUN)
+                && Objects.equals(this.apn, other.apn)
+                && !typeSameAny(this, other)
+                && xorEquals(this.proxy, other.proxy)
+                && xorEquals(this.port, other.port)
+                && xorEquals(this.protocol, other.protocol)
+                && xorEquals(this.roamingProtocol, other.roamingProtocol)
+                && this.carrierEnabled == other.carrierEnabled
+                && this.bearerBitmask == other.bearerBitmask
+                && this.profileId == other.profileId
+                && Objects.equals(this.mvnoType, other.mvnoType)
+                && Objects.equals(this.mvnoMatchData, other.mvnoMatchData)
+                && xorEquals(this.mmsc, other.mmsc)
+                && xorEquals(this.mmsProxy, other.mmsProxy)
+                && xorEquals(this.mmsPort, other.mmsPort));
+    }
+
+    // check whether the types of two APN same (even only one type of each APN is same)
+    private boolean typeSameAny(ApnSetting first, ApnSetting second) {
+        if (VDBG) {
+            StringBuilder apnType1 = new StringBuilder(first.apn + ": ");
+            for (int index1 = 0; index1 < first.types.length; index1++) {
+                apnType1.append(first.types[index1]);
+                apnType1.append(",");
+            }
+
+            StringBuilder apnType2 = new StringBuilder(second.apn + ": ");
+            for (int index1 = 0; index1 < second.types.length; index1++) {
+                apnType2.append(second.types[index1]);
+                apnType2.append(",");
+            }
+            Rlog.d(LOG_TAG, "APN1: is " + apnType1);
+            Rlog.d(LOG_TAG, "APN2: is " + apnType2);
+        }
+
+        for (int index1 = 0; index1 < first.types.length; index1++) {
+            for (int index2 = 0; index2 < second.types.length; index2++) {
+                if (first.types[index1].equals(PhoneConstants.APN_TYPE_ALL)
+                        || second.types[index2].equals(PhoneConstants.APN_TYPE_ALL)
+                        || first.types[index1].equals(second.types[index2])) {
+                    if (VDBG) Rlog.d(LOG_TAG, "typeSameAny: return true");
+                    return true;
+                }
+            }
+        }
+
+        if (VDBG) Rlog.d(LOG_TAG, "typeSameAny: return false");
+        return false;
+    }
+
+    // equal or one is not specified
+    private boolean xorEquals(String first, String second) {
+        return (Objects.equals(first, second)
+                || TextUtils.isEmpty(first)
+                || TextUtils.isEmpty(second));
+    }
+
+    // Helper function to convert APN string into a 32-bit bitmask.
+    private static int getApnBitmask(String apn) {
+        switch (apn) {
+            case PhoneConstants.APN_TYPE_DEFAULT: return ApnTypes.DEFAULT;
+            case PhoneConstants.APN_TYPE_MMS: return ApnTypes.MMS;
+            case PhoneConstants.APN_TYPE_SUPL: return ApnTypes.SUPL;
+            case PhoneConstants.APN_TYPE_DUN: return ApnTypes.DUN;
+            case PhoneConstants.APN_TYPE_HIPRI: return ApnTypes.HIPRI;
+            case PhoneConstants.APN_TYPE_FOTA: return ApnTypes.FOTA;
+            case PhoneConstants.APN_TYPE_IMS: return ApnTypes.IMS;
+            case PhoneConstants.APN_TYPE_CBS: return ApnTypes.CBS;
+            case PhoneConstants.APN_TYPE_IA: return ApnTypes.IA;
+            case PhoneConstants.APN_TYPE_EMERGENCY: return ApnTypes.EMERGENCY;
+            case PhoneConstants.APN_TYPE_ALL: return ApnTypes.ALL;
+            default: return ApnTypes.NONE;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DataCallResponse.java b/com/android/internal/telephony/dataconnection/DataCallResponse.java
new file mode 100644
index 0000000..bc02b97
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DataCallResponse.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2009 Qualcomm Innovation Center, Inc.  All Rights Reserved.
+ * 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.internal.telephony.dataconnection;
+
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkUtils;
+import android.net.RouteInfo;
+import android.os.SystemProperties;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * This is RIL_Data_Call_Response_v5 from ril.h
+ */
+public class DataCallResponse {
+    private final boolean DBG = true;
+    private final String LOG_TAG = "DataCallResponse";
+
+    public final int status;
+    public final int suggestedRetryTime;
+    public final int cid;
+    public final int active;
+    public final String type;
+    public final String ifname;
+    public final String [] addresses;
+    public final String [] dnses;
+    // TODO: Change this to final if possible.
+    public String[] gateways;
+    public final String [] pcscf;
+    public final int mtu;
+
+    /**
+     * Class returned by onSetupConnectionCompleted.
+     */
+    public enum SetupResult {
+        SUCCESS,
+        ERR_BadCommand,
+        ERR_UnacceptableParameter,
+        ERR_GetLastErrorFromRil,
+        ERR_Stale,
+        ERR_RilError;
+
+        public DcFailCause mFailCause;
+
+        SetupResult() {
+            mFailCause = DcFailCause.fromInt(0);
+        }
+
+        @Override
+        public String toString() {
+            return name() + "  SetupResult.mFailCause=" + mFailCause;
+        }
+    }
+
+    public DataCallResponse(int status, int suggestedRetryTime, int cid, int active, String type,
+                            String ifname, String addresses, String dnses, String gateways,
+                            String pcscf, int mtu) {
+        this.status = status;
+        this.suggestedRetryTime = suggestedRetryTime;
+        this.cid = cid;
+        this.active = active;
+        this.type = (type == null) ? "" : type;
+        this.ifname = (ifname == null) ? "" : ifname;
+        if ((status == DcFailCause.NONE.getErrorCode()) && TextUtils.isEmpty(ifname)) {
+            throw new RuntimeException("DataCallResponse, no ifname");
+        }
+        this.addresses = TextUtils.isEmpty(addresses) ? new String[0] : addresses.split(" ");
+        this.dnses = TextUtils.isEmpty(dnses) ? new String[0] : dnses.split(" ");
+        this.gateways = TextUtils.isEmpty(gateways) ? new String[0] : gateways.split(" ");
+        this.pcscf = TextUtils.isEmpty(pcscf) ? new String[0] : pcscf.split(" ");
+        this.mtu = mtu;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("DataCallResponse: {")
+           .append(" status=").append(status)
+           .append(" retry=").append(suggestedRetryTime)
+           .append(" cid=").append(cid)
+           .append(" active=").append(active)
+           .append(" type=").append(type)
+           .append(" ifname=").append(ifname)
+           .append(" mtu=").append(mtu)
+           .append(" addresses=[");
+        for (String addr : addresses) {
+            sb.append(addr);
+            sb.append(",");
+        }
+        if (addresses.length > 0) sb.deleteCharAt(sb.length()-1);
+        sb.append("] dnses=[");
+        for (String addr : dnses) {
+            sb.append(addr);
+            sb.append(",");
+        }
+        if (dnses.length > 0) sb.deleteCharAt(sb.length()-1);
+        sb.append("] gateways=[");
+        for (String addr : gateways) {
+            sb.append(addr);
+            sb.append(",");
+        }
+        if (gateways.length > 0) sb.deleteCharAt(sb.length()-1);
+        sb.append("] pcscf=[");
+        for (String addr : pcscf) {
+            sb.append(addr);
+            sb.append(",");
+        }
+        if (pcscf.length > 0) sb.deleteCharAt(sb.length()-1);
+        sb.append("]}");
+        return sb.toString();
+    }
+
+    public SetupResult setLinkProperties(LinkProperties linkProperties,
+            boolean okToUseSystemPropertyDns) {
+        SetupResult result;
+
+        // Start with clean network properties and if we have
+        // a failure we'll clear again at the bottom of this code.
+        if (linkProperties == null)
+            linkProperties = new LinkProperties();
+        else
+            linkProperties.clear();
+
+        if (status == DcFailCause.NONE.getErrorCode()) {
+            String propertyPrefix = "net." + ifname + ".";
+
+            try {
+                // set interface name
+                linkProperties.setInterfaceName(ifname);
+
+                // set link addresses
+                if (addresses != null && addresses.length > 0) {
+                    for (String addr : addresses) {
+                        addr = addr.trim();
+                        if (addr.isEmpty()) continue;
+                        LinkAddress la;
+                        int addrPrefixLen;
+
+                        String [] ap = addr.split("/");
+                        if (ap.length == 2) {
+                            addr = ap[0];
+                            addrPrefixLen = Integer.parseInt(ap[1]);
+                        } else {
+                            addrPrefixLen = 0;
+                        }
+                        InetAddress ia;
+                        try {
+                            ia = NetworkUtils.numericToInetAddress(addr);
+                        } catch (IllegalArgumentException e) {
+                            throw new UnknownHostException("Non-numeric ip addr=" + addr);
+                        }
+                        if (! ia.isAnyLocalAddress()) {
+                            if (addrPrefixLen == 0) {
+                                // Assume point to point
+                                addrPrefixLen = (ia instanceof Inet4Address) ? 32 : 128;
+                            }
+                            if (DBG) Rlog.d(LOG_TAG, "addr/pl=" + addr + "/" + addrPrefixLen);
+                            try {
+                                la = new LinkAddress(ia, addrPrefixLen);
+                            } catch (IllegalArgumentException e) {
+                                throw new UnknownHostException("Bad parameter for LinkAddress, ia="
+                                        + ia.getHostAddress() + "/" + addrPrefixLen);
+                            }
+
+                            linkProperties.addLinkAddress(la);
+                        }
+                    }
+                } else {
+                    throw new UnknownHostException("no address for ifname=" + ifname);
+                }
+
+                // set dns servers
+                if (dnses != null && dnses.length > 0) {
+                    for (String addr : dnses) {
+                        addr = addr.trim();
+                        if (addr.isEmpty()) continue;
+                        InetAddress ia;
+                        try {
+                            ia = NetworkUtils.numericToInetAddress(addr);
+                        } catch (IllegalArgumentException e) {
+                            throw new UnknownHostException("Non-numeric dns addr=" + addr);
+                        }
+                        if (! ia.isAnyLocalAddress()) {
+                            linkProperties.addDnsServer(ia);
+                        }
+                    }
+                } else if (okToUseSystemPropertyDns){
+                    String dnsServers[] = new String[2];
+                    dnsServers[0] = SystemProperties.get(propertyPrefix + "dns1");
+                    dnsServers[1] = SystemProperties.get(propertyPrefix + "dns2");
+                    for (String dnsAddr : dnsServers) {
+                        dnsAddr = dnsAddr.trim();
+                        if (dnsAddr.isEmpty()) continue;
+                        InetAddress ia;
+                        try {
+                            ia = NetworkUtils.numericToInetAddress(dnsAddr);
+                        } catch (IllegalArgumentException e) {
+                            throw new UnknownHostException("Non-numeric dns addr=" + dnsAddr);
+                        }
+                        if (! ia.isAnyLocalAddress()) {
+                            linkProperties.addDnsServer(ia);
+                        }
+                    }
+                } else {
+                    throw new UnknownHostException("Empty dns response and no system default dns");
+                }
+
+                // set gateways
+                if ((gateways == null) || (gateways.length == 0)) {
+                    String sysGateways = SystemProperties.get(propertyPrefix + "gw");
+                    if (sysGateways != null) {
+                        gateways = sysGateways.split(" ");
+                    } else {
+                        gateways = new String[0];
+                    }
+                }
+                for (String addr : gateways) {
+                    addr = addr.trim();
+                    if (addr.isEmpty()) continue;
+                    InetAddress ia;
+                    try {
+                        ia = NetworkUtils.numericToInetAddress(addr);
+                    } catch (IllegalArgumentException e) {
+                        throw new UnknownHostException("Non-numeric gateway addr=" + addr);
+                    }
+                    // Allow 0.0.0.0 or :: as a gateway; this indicates a point-to-point interface.
+                    linkProperties.addRoute(new RouteInfo(ia));
+                }
+
+                // set interface MTU
+                // this may clobber the setting read from the APN db, but that's ok
+                linkProperties.setMtu(mtu);
+
+                result = SetupResult.SUCCESS;
+            } catch (UnknownHostException e) {
+                Rlog.d(LOG_TAG, "setLinkProperties: UnknownHostException " + e);
+                e.printStackTrace();
+                result = SetupResult.ERR_UnacceptableParameter;
+            }
+        } else {
+            result = SetupResult.ERR_RilError;
+        }
+
+        // An error occurred so clear properties
+        if (result != SetupResult.SUCCESS) {
+            if(DBG) {
+                Rlog.d(LOG_TAG, "setLinkProperties: error clearing LinkProperties " +
+                        "status=" + status + " result=" + result);
+            }
+            linkProperties.clear();
+        }
+
+        return result;
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DataConnection.java b/com/android/internal/telephony/dataconnection/DataConnection.java
new file mode 100644
index 0000000..33d0b1d
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DataConnection.java
@@ -0,0 +1,2122 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.dataconnection;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkMisc;
+import android.net.ProxyInfo;
+import android.net.StringNetworkSpecifier;
+import android.os.AsyncResult;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.CallTracker;
+import com.android.internal.telephony.CarrierSignalAgent;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.DctConstants;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.RetryManager;
+import com.android.internal.telephony.ServiceStateTracker;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * {@hide}
+ *
+ * DataConnection StateMachine.
+ *
+ * This a class for representing a single data connection, with instances of this
+ * class representing a connection via the cellular network. There may be multiple
+ * data connections and all of them are managed by the <code>DataConnectionTracker</code>.
+ *
+ * NOTE: All DataConnection objects must be running on the same looper, which is the default
+ * as the coordinator has members which are used without synchronization.
+ */
+public class DataConnection extends StateMachine {
+    private static final boolean DBG = true;
+    private static final boolean VDBG = true;
+
+    private static final String NETWORK_TYPE = "MOBILE";
+
+    // The data connection controller
+    private DcController mDcController;
+
+    // The Tester for failing all bringup's
+    private DcTesterFailBringUpAll mDcTesterFailBringUpAll;
+
+    private static AtomicInteger mInstanceNumber = new AtomicInteger(0);
+    private AsyncChannel mAc;
+
+    // The DCT that's talking to us, we only support one!
+    private DcTracker mDct = null;
+
+    protected String[] mPcscfAddr;
+
+    /**
+     * Used internally for saving connecting parameters.
+     */
+    public static class ConnectionParams {
+        int mTag;
+        ApnContext mApnContext;
+        int mProfileId;
+        int mRilRat;
+        final boolean mUnmeteredUseOnly;
+        Message mOnCompletedMsg;
+        final int mConnectionGeneration;
+
+        ConnectionParams(ApnContext apnContext, int profileId, int rilRadioTechnology,
+                         boolean unmeteredUseOnly,  Message onCompletedMsg,
+                         int connectionGeneration) {
+            mApnContext = apnContext;
+            mProfileId = profileId;
+            mRilRat = rilRadioTechnology;
+            mUnmeteredUseOnly = unmeteredUseOnly;
+            mOnCompletedMsg = onCompletedMsg;
+            mConnectionGeneration = connectionGeneration;
+        }
+
+        @Override
+        public String toString() {
+            return "{mTag=" + mTag + " mApnContext=" + mApnContext
+                    + " mProfileId=" + mProfileId
+                    + " mRat=" + mRilRat
+                    + " mUnmeteredUseOnly=" + mUnmeteredUseOnly
+                    + " mOnCompletedMsg=" + msgToString(mOnCompletedMsg) + "}";
+        }
+    }
+
+    /**
+     * Used internally for saving disconnecting parameters.
+     */
+    public static class DisconnectParams {
+        int mTag;
+        public ApnContext mApnContext;
+        String mReason;
+        Message mOnCompletedMsg;
+
+        DisconnectParams(ApnContext apnContext, String reason, Message onCompletedMsg) {
+            mApnContext = apnContext;
+            mReason = reason;
+            mOnCompletedMsg = onCompletedMsg;
+        }
+
+        @Override
+        public String toString() {
+            return "{mTag=" + mTag + " mApnContext=" + mApnContext
+                    + " mReason=" + mReason
+                    + " mOnCompletedMsg=" + msgToString(mOnCompletedMsg) + "}";
+        }
+    }
+
+    private ApnSetting mApnSetting;
+    private ConnectionParams mConnectionParams;
+    private DisconnectParams mDisconnectParams;
+    private DcFailCause mDcFailCause;
+
+    private Phone mPhone;
+    private LinkProperties mLinkProperties = new LinkProperties();
+    private long mCreateTime;
+    private long mLastFailTime;
+    private DcFailCause mLastFailCause;
+    private static final String NULL_IP = "0.0.0.0";
+    private Object mUserData;
+    private int mRilRat = Integer.MAX_VALUE;
+    private int mDataRegState = Integer.MAX_VALUE;
+    private NetworkInfo mNetworkInfo;
+    private NetworkAgent mNetworkAgent;
+
+    int mTag;
+    public int mCid;
+    public HashMap<ApnContext, ConnectionParams> mApnContexts = null;
+    PendingIntent mReconnectIntent = null;
+
+
+    // ***** Event codes for driving the state machine, package visible for Dcc
+    static final int BASE = Protocol.BASE_DATA_CONNECTION;
+    static final int EVENT_CONNECT = BASE + 0;
+    static final int EVENT_SETUP_DATA_CONNECTION_DONE = BASE + 1;
+    static final int EVENT_GET_LAST_FAIL_DONE = BASE + 2;
+    static final int EVENT_DEACTIVATE_DONE = BASE + 3;
+    static final int EVENT_DISCONNECT = BASE + 4;
+    static final int EVENT_RIL_CONNECTED = BASE + 5;
+    static final int EVENT_DISCONNECT_ALL = BASE + 6;
+    static final int EVENT_DATA_STATE_CHANGED = BASE + 7;
+    static final int EVENT_TEAR_DOWN_NOW = BASE + 8;
+    static final int EVENT_LOST_CONNECTION = BASE + 9;
+    static final int EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED = BASE + 11;
+    static final int EVENT_DATA_CONNECTION_ROAM_ON = BASE + 12;
+    static final int EVENT_DATA_CONNECTION_ROAM_OFF = BASE + 13;
+    static final int EVENT_BW_REFRESH_RESPONSE = BASE + 14;
+    static final int EVENT_DATA_CONNECTION_VOICE_CALL_STARTED = BASE + 15;
+    static final int EVENT_DATA_CONNECTION_VOICE_CALL_ENDED = BASE + 16;
+
+    private static final int CMD_TO_STRING_COUNT =
+            EVENT_DATA_CONNECTION_VOICE_CALL_ENDED - BASE + 1;
+
+    private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT];
+    static {
+        sCmdToString[EVENT_CONNECT - BASE] = "EVENT_CONNECT";
+        sCmdToString[EVENT_SETUP_DATA_CONNECTION_DONE - BASE] =
+                "EVENT_SETUP_DATA_CONNECTION_DONE";
+        sCmdToString[EVENT_GET_LAST_FAIL_DONE - BASE] = "EVENT_GET_LAST_FAIL_DONE";
+        sCmdToString[EVENT_DEACTIVATE_DONE - BASE] = "EVENT_DEACTIVATE_DONE";
+        sCmdToString[EVENT_DISCONNECT - BASE] = "EVENT_DISCONNECT";
+        sCmdToString[EVENT_RIL_CONNECTED - BASE] = "EVENT_RIL_CONNECTED";
+        sCmdToString[EVENT_DISCONNECT_ALL - BASE] = "EVENT_DISCONNECT_ALL";
+        sCmdToString[EVENT_DATA_STATE_CHANGED - BASE] = "EVENT_DATA_STATE_CHANGED";
+        sCmdToString[EVENT_TEAR_DOWN_NOW - BASE] = "EVENT_TEAR_DOWN_NOW";
+        sCmdToString[EVENT_LOST_CONNECTION - BASE] = "EVENT_LOST_CONNECTION";
+        sCmdToString[EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED - BASE] =
+                "EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED";
+        sCmdToString[EVENT_DATA_CONNECTION_ROAM_ON - BASE] = "EVENT_DATA_CONNECTION_ROAM_ON";
+        sCmdToString[EVENT_DATA_CONNECTION_ROAM_OFF - BASE] = "EVENT_DATA_CONNECTION_ROAM_OFF";
+        sCmdToString[EVENT_BW_REFRESH_RESPONSE - BASE] = "EVENT_BW_REFRESH_RESPONSE";
+        sCmdToString[EVENT_DATA_CONNECTION_VOICE_CALL_STARTED - BASE] =
+                "EVENT_DATA_CONNECTION_VOICE_CALL_STARTED";
+        sCmdToString[EVENT_DATA_CONNECTION_VOICE_CALL_ENDED - BASE] =
+                "EVENT_DATA_CONNECTION_VOICE_CALL_ENDED";
+    }
+    // Convert cmd to string or null if unknown
+    static String cmdToString(int cmd) {
+        String value;
+        cmd -= BASE;
+        if ((cmd >= 0) && (cmd < sCmdToString.length)) {
+            value = sCmdToString[cmd];
+        } else {
+            value = DcAsyncChannel.cmdToString(cmd + BASE);
+        }
+        if (value == null) {
+            value = "0x" + Integer.toHexString(cmd + BASE);
+        }
+        return value;
+    }
+
+    /**
+     * Create the connection object
+     *
+     * @param phone the Phone
+     * @param id the connection id
+     * @return DataConnection that was created.
+     */
+    public static DataConnection makeDataConnection(Phone phone, int id,
+            DcTracker dct, DcTesterFailBringUpAll failBringUpAll,
+            DcController dcc) {
+        DataConnection dc = new DataConnection(phone,
+                "DC-" + mInstanceNumber.incrementAndGet(), id, dct, failBringUpAll, dcc);
+        dc.start();
+        if (DBG) dc.log("Made " + dc.getName());
+        return dc;
+    }
+
+    void dispose() {
+        log("dispose: call quiteNow()");
+        quitNow();
+    }
+
+    /* Getter functions */
+
+    LinkProperties getCopyLinkProperties() {
+        return new LinkProperties(mLinkProperties);
+    }
+
+    boolean getIsInactive() {
+        return getCurrentState() == mInactiveState;
+    }
+
+    int getCid() {
+        return mCid;
+    }
+
+    ApnSetting getApnSetting() {
+        return mApnSetting;
+    }
+
+    void setLinkPropertiesHttpProxy(ProxyInfo proxy) {
+        mLinkProperties.setHttpProxy(proxy);
+    }
+
+    public static class UpdateLinkPropertyResult {
+        public DataCallResponse.SetupResult setupResult = DataCallResponse.SetupResult.SUCCESS;
+        public LinkProperties oldLp;
+        public LinkProperties newLp;
+        public UpdateLinkPropertyResult(LinkProperties curLp) {
+            oldLp = curLp;
+            newLp = curLp;
+        }
+    }
+
+    public boolean isIpv4Connected() {
+        boolean ret = false;
+        Collection <InetAddress> addresses = mLinkProperties.getAddresses();
+
+        for (InetAddress addr: addresses) {
+            if (addr instanceof java.net.Inet4Address) {
+                java.net.Inet4Address i4addr = (java.net.Inet4Address) addr;
+                if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress() &&
+                        !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) {
+                    ret = true;
+                    break;
+                }
+            }
+        }
+        return ret;
+    }
+
+    public boolean isIpv6Connected() {
+        boolean ret = false;
+        Collection <InetAddress> addresses = mLinkProperties.getAddresses();
+
+        for (InetAddress addr: addresses) {
+            if (addr instanceof java.net.Inet6Address) {
+                java.net.Inet6Address i6addr = (java.net.Inet6Address) addr;
+                if (!i6addr.isAnyLocalAddress() && !i6addr.isLinkLocalAddress() &&
+                        !i6addr.isLoopbackAddress() && !i6addr.isMulticastAddress()) {
+                    ret = true;
+                    break;
+                }
+            }
+        }
+        return ret;
+    }
+
+    public UpdateLinkPropertyResult updateLinkProperty(DataCallResponse newState) {
+        UpdateLinkPropertyResult result = new UpdateLinkPropertyResult(mLinkProperties);
+
+        if (newState == null) return result;
+
+        DataCallResponse.SetupResult setupResult;
+        result.newLp = new LinkProperties();
+
+        // set link properties based on data call response
+        result.setupResult = setLinkProperties(newState, result.newLp);
+        if (result.setupResult != DataCallResponse.SetupResult.SUCCESS) {
+            if (DBG) log("updateLinkProperty failed : " + result.setupResult);
+            return result;
+        }
+        // copy HTTP proxy as it is not part DataCallResponse.
+        result.newLp.setHttpProxy(mLinkProperties.getHttpProxy());
+
+        checkSetMtu(mApnSetting, result.newLp);
+
+        mLinkProperties = result.newLp;
+
+        updateTcpBufferSizes(mRilRat);
+
+        if (DBG && (! result.oldLp.equals(result.newLp))) {
+            log("updateLinkProperty old LP=" + result.oldLp);
+            log("updateLinkProperty new LP=" + result.newLp);
+        }
+
+        if (result.newLp.equals(result.oldLp) == false &&
+                mNetworkAgent != null) {
+            mNetworkAgent.sendLinkProperties(mLinkProperties);
+        }
+
+        return result;
+    }
+
+    /**
+     * Read the MTU value from link properties where it can be set from network. In case
+     * not set by the network, set it again using the mtu szie value defined in the APN
+     * database for the connected APN
+     */
+    private void checkSetMtu(ApnSetting apn, LinkProperties lp) {
+        if (lp == null) return;
+
+        if (apn == null || lp == null) return;
+
+        if (lp.getMtu() != PhoneConstants.UNSET_MTU) {
+            if (DBG) log("MTU set by call response to: " + lp.getMtu());
+            return;
+        }
+
+        if (apn != null && apn.mtu != PhoneConstants.UNSET_MTU) {
+            lp.setMtu(apn.mtu);
+            if (DBG) log("MTU set by APN to: " + apn.mtu);
+            return;
+        }
+
+        int mtu = mPhone.getContext().getResources().getInteger(
+                com.android.internal.R.integer.config_mobile_mtu);
+        if (mtu != PhoneConstants.UNSET_MTU) {
+            lp.setMtu(mtu);
+            if (DBG) log("MTU set by config resource to: " + mtu);
+        }
+    }
+
+    //***** Constructor (NOTE: uses dcc.getHandler() as its Handler)
+    private DataConnection(Phone phone, String name, int id,
+                DcTracker dct, DcTesterFailBringUpAll failBringUpAll,
+                DcController dcc) {
+        super(name, dcc.getHandler());
+        setLogRecSize(300);
+        setLogOnlyTransitions(true);
+        if (DBG) log("DataConnection created");
+
+        mPhone = phone;
+        mDct = dct;
+        mDcTesterFailBringUpAll = failBringUpAll;
+        mDcController = dcc;
+        mId = id;
+        mCid = -1;
+        ServiceState ss = mPhone.getServiceState();
+        mRilRat = ss.getRilDataRadioTechnology();
+        mDataRegState = mPhone.getServiceState().getDataRegState();
+        int networkType = ss.getDataNetworkType();
+        mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_MOBILE,
+                networkType, NETWORK_TYPE, TelephonyManager.getNetworkTypeName(networkType));
+        mNetworkInfo.setRoaming(ss.getDataRoaming());
+        mNetworkInfo.setIsAvailable(true);
+
+        addState(mDefaultState);
+            addState(mInactiveState, mDefaultState);
+            addState(mActivatingState, mDefaultState);
+            addState(mActiveState, mDefaultState);
+            addState(mDisconnectingState, mDefaultState);
+            addState(mDisconnectingErrorCreatingConnection, mDefaultState);
+        setInitialState(mInactiveState);
+
+        mApnContexts = new HashMap<ApnContext, ConnectionParams>();
+    }
+
+    /**
+     * Begin setting up a data connection, calls setupDataCall
+     * and the ConnectionParams will be returned with the
+     * EVENT_SETUP_DATA_CONNECTION_DONE AsyncResul.userObj.
+     *
+     * @param cp is the connection parameters
+     */
+    private void onConnect(ConnectionParams cp) {
+        if (DBG) log("onConnect: carrier='" + mApnSetting.carrier
+                + "' APN='" + mApnSetting.apn
+                + "' proxy='" + mApnSetting.proxy + "' port='" + mApnSetting.port + "'");
+        if (cp.mApnContext != null) cp.mApnContext.requestLog("DataConnection.onConnect");
+
+        // Check if we should fake an error.
+        if (mDcTesterFailBringUpAll.getDcFailBringUp().mCounter  > 0) {
+            DataCallResponse response = new DataCallResponse(
+                    mDcTesterFailBringUpAll.getDcFailBringUp().mFailCause.getErrorCode(),
+                    mDcTesterFailBringUpAll.getDcFailBringUp().mSuggestedRetryTime, 0, 0, "", "",
+                    "", "", "", "", PhoneConstants.UNSET_MTU);
+
+            Message msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp);
+            AsyncResult.forMessage(msg, response, null);
+            sendMessage(msg);
+            if (DBG) {
+                log("onConnect: FailBringUpAll=" + mDcTesterFailBringUpAll.getDcFailBringUp()
+                        + " send error response=" + response);
+            }
+            mDcTesterFailBringUpAll.getDcFailBringUp().mCounter -= 1;
+            return;
+        }
+
+        mCreateTime = -1;
+        mLastFailTime = -1;
+        mLastFailCause = DcFailCause.NONE;
+
+        // msg.obj will be returned in AsyncResult.userObj;
+        Message msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp);
+        msg.obj = cp;
+
+        DataProfile dp = new DataProfile(mApnSetting, cp.mProfileId);
+
+        // We need to use the actual modem roaming state instead of the framework roaming state
+        // here. This flag is only passed down to ril_service for picking the correct protocol (for
+        // old modem backward compatibility).
+        boolean isModemRoaming = mPhone.getServiceState().getDataRoamingFromRegistration();
+
+        // Set this flag to true if the user turns on data roaming. Or if we override the roaming
+        // state in framework, we should set this flag to true as well so the modem will not reject
+        // the data call setup (because the modem actually thinks the device is roaming).
+        boolean allowRoaming = mPhone.getDataRoamingEnabled()
+                || (isModemRoaming && !mPhone.getServiceState().getDataRoaming());
+
+        mPhone.mCi.setupDataCall(cp.mRilRat, dp, isModemRoaming, allowRoaming, msg);
+    }
+
+    /**
+     * TearDown the data connection when the deactivation is complete a Message with
+     * msg.what == EVENT_DEACTIVATE_DONE and msg.obj == AsyncResult with AsyncResult.obj
+     * containing the parameter o.
+     *
+     * @param o is the object returned in the AsyncResult.obj.
+     */
+    private void tearDownData(Object o) {
+        int discReason = RILConstants.DEACTIVATE_REASON_NONE;
+        ApnContext apnContext = null;
+        if ((o != null) && (o instanceof DisconnectParams)) {
+            DisconnectParams dp = (DisconnectParams)o;
+            apnContext = dp.mApnContext;
+            if (TextUtils.equals(dp.mReason, Phone.REASON_RADIO_TURNED_OFF)) {
+                discReason = RILConstants.DEACTIVATE_REASON_RADIO_OFF;
+            } else if (TextUtils.equals(dp.mReason, Phone.REASON_PDP_RESET)) {
+                discReason = RILConstants.DEACTIVATE_REASON_PDP_RESET;
+            }
+        }
+
+        String str = "tearDownData. mCid=" + mCid + ", reason=" + discReason;
+        if (DBG) log(str);
+        if (apnContext != null) apnContext.requestLog(str);
+        mPhone.mCi.deactivateDataCall(mCid, discReason,
+                obtainMessage(EVENT_DEACTIVATE_DONE, mTag, 0, o));
+    }
+
+    private void notifyAllWithEvent(ApnContext alreadySent, int event, String reason) {
+        mNetworkInfo.setDetailedState(mNetworkInfo.getDetailedState(), reason,
+                mNetworkInfo.getExtraInfo());
+        for (ConnectionParams cp : mApnContexts.values()) {
+            ApnContext apnContext = cp.mApnContext;
+            if (apnContext == alreadySent) continue;
+            if (reason != null) apnContext.setReason(reason);
+            Pair<ApnContext, Integer> pair =
+                    new Pair<ApnContext, Integer>(apnContext, cp.mConnectionGeneration);
+            Message msg = mDct.obtainMessage(event, pair);
+            AsyncResult.forMessage(msg);
+            msg.sendToTarget();
+        }
+    }
+
+    private void notifyAllOfConnected(String reason) {
+        notifyAllWithEvent(null, DctConstants.EVENT_DATA_SETUP_COMPLETE, reason);
+    }
+
+    private void notifyAllOfDisconnectDcRetrying(String reason) {
+        notifyAllWithEvent(null, DctConstants.EVENT_DISCONNECT_DC_RETRYING, reason);
+    }
+    private void notifyAllDisconnectCompleted(DcFailCause cause) {
+        notifyAllWithEvent(null, DctConstants.EVENT_DISCONNECT_DONE, cause.toString());
+    }
+
+
+    /**
+     * Send the connectionCompletedMsg.
+     *
+     * @param cp is the ConnectionParams
+     * @param cause and if no error the cause is DcFailCause.NONE
+     * @param sendAll is true if all contexts are to be notified
+     */
+    private void notifyConnectCompleted(ConnectionParams cp, DcFailCause cause, boolean sendAll) {
+        ApnContext alreadySent = null;
+
+        if (cp != null && cp.mOnCompletedMsg != null) {
+            // Get the completed message but only use it once
+            Message connectionCompletedMsg = cp.mOnCompletedMsg;
+            cp.mOnCompletedMsg = null;
+            alreadySent = cp.mApnContext;
+
+            long timeStamp = System.currentTimeMillis();
+            connectionCompletedMsg.arg1 = mCid;
+
+            if (cause == DcFailCause.NONE) {
+                mCreateTime = timeStamp;
+                AsyncResult.forMessage(connectionCompletedMsg);
+            } else {
+                mLastFailCause = cause;
+                mLastFailTime = timeStamp;
+
+                // Return message with a Throwable exception to signify an error.
+                if (cause == null) cause = DcFailCause.UNKNOWN;
+                AsyncResult.forMessage(connectionCompletedMsg, cause,
+                        new Throwable(cause.toString()));
+            }
+            if (DBG) {
+                log("notifyConnectCompleted at " + timeStamp + " cause=" + cause
+                        + " connectionCompletedMsg=" + msgToString(connectionCompletedMsg));
+            }
+
+            connectionCompletedMsg.sendToTarget();
+        }
+        if (sendAll) {
+            log("Send to all. " + alreadySent + " " + cause.toString());
+            notifyAllWithEvent(alreadySent, DctConstants.EVENT_DATA_SETUP_COMPLETE_ERROR,
+                    cause.toString());
+        }
+    }
+
+    /**
+     * Send ar.userObj if its a message, which is should be back to originator.
+     *
+     * @param dp is the DisconnectParams.
+     */
+    private void notifyDisconnectCompleted(DisconnectParams dp, boolean sendAll) {
+        if (VDBG) log("NotifyDisconnectCompleted");
+
+        ApnContext alreadySent = null;
+        String reason = null;
+
+        if (dp != null && dp.mOnCompletedMsg != null) {
+            // Get the completed message but only use it once
+            Message msg = dp.mOnCompletedMsg;
+            dp.mOnCompletedMsg = null;
+            if (msg.obj instanceof ApnContext) {
+                alreadySent = (ApnContext)msg.obj;
+            }
+            reason = dp.mReason;
+            if (VDBG) {
+                log(String.format("msg=%s msg.obj=%s", msg.toString(),
+                    ((msg.obj instanceof String) ? (String) msg.obj : "<no-reason>")));
+            }
+            AsyncResult.forMessage(msg);
+            msg.sendToTarget();
+        }
+        if (sendAll) {
+            if (reason == null) {
+                reason = DcFailCause.UNKNOWN.toString();
+            }
+            notifyAllWithEvent(alreadySent, DctConstants.EVENT_DISCONNECT_DONE, reason);
+        }
+        if (DBG) log("NotifyDisconnectCompleted DisconnectParams=" + dp);
+    }
+
+    /*
+     * **************************************************************************
+     * Begin Members and methods owned by DataConnectionTracker but stored
+     * in a DataConnection because there is one per connection.
+     * **************************************************************************
+     */
+
+    /*
+     * The id is owned by DataConnectionTracker.
+     */
+    private int mId;
+
+    /**
+     * Get the DataConnection ID
+     */
+    public int getDataConnectionId() {
+        return mId;
+    }
+
+    /*
+     * **************************************************************************
+     * End members owned by DataConnectionTracker
+     * **************************************************************************
+     */
+
+    /**
+     * Clear all settings called when entering mInactiveState.
+     */
+    private void clearSettings() {
+        if (DBG) log("clearSettings");
+
+        mCreateTime = -1;
+        mLastFailTime = -1;
+        mLastFailCause = DcFailCause.NONE;
+        mCid = -1;
+
+        mPcscfAddr = new String[5];
+
+        mLinkProperties = new LinkProperties();
+        mApnContexts.clear();
+        mApnSetting = null;
+        mDcFailCause = null;
+    }
+
+    /**
+     * Process setup completion.
+     *
+     * @param ar is the result
+     * @return SetupResult.
+     */
+    private DataCallResponse.SetupResult onSetupConnectionCompleted(AsyncResult ar) {
+        DataCallResponse response = (DataCallResponse) ar.result;
+        ConnectionParams cp = (ConnectionParams) ar.userObj;
+        DataCallResponse.SetupResult result;
+
+        if (cp.mTag != mTag) {
+            if (DBG) {
+                log("onSetupConnectionCompleted stale cp.tag=" + cp.mTag + ", mtag=" + mTag);
+            }
+            result = DataCallResponse.SetupResult.ERR_Stale;
+        } else if (ar.exception != null) {
+            if (DBG) {
+                log("onSetupConnectionCompleted failed, ar.exception=" + ar.exception +
+                    " response=" + response);
+            }
+
+            if (ar.exception instanceof CommandException
+                    && ((CommandException) (ar.exception)).getCommandError()
+                    == CommandException.Error.RADIO_NOT_AVAILABLE) {
+                result = DataCallResponse.SetupResult.ERR_BadCommand;
+                result.mFailCause = DcFailCause.RADIO_NOT_AVAILABLE;
+            } else {
+                result = DataCallResponse.SetupResult.ERR_RilError;
+                result.mFailCause = DcFailCause.fromInt(response.status);
+            }
+        } else if (response.status != 0) {
+            result = DataCallResponse.SetupResult.ERR_RilError;
+            result.mFailCause = DcFailCause.fromInt(response.status);
+        } else {
+            if (DBG) log("onSetupConnectionCompleted received successful DataCallResponse");
+            mCid = response.cid;
+
+            mPcscfAddr = response.pcscf;
+
+            result = updateLinkProperty(response).setupResult;
+        }
+
+        return result;
+    }
+
+    private boolean isDnsOk(String[] domainNameServers) {
+        if (NULL_IP.equals(domainNameServers[0]) && NULL_IP.equals(domainNameServers[1])
+                && !mPhone.isDnsCheckDisabled()) {
+            // Work around a race condition where QMI does not fill in DNS:
+            // Deactivate PDP and let DataConnectionTracker retry.
+            // Do not apply the race condition workaround for MMS APN
+            // if Proxy is an IP-address.
+            // Otherwise, the default APN will not be restored anymore.
+            if (!mApnSetting.types[0].equals(PhoneConstants.APN_TYPE_MMS)
+                || !isIpAddress(mApnSetting.mmsProxy)) {
+                log(String.format(
+                        "isDnsOk: return false apn.types[0]=%s APN_TYPE_MMS=%s isIpAddress(%s)=%s",
+                        mApnSetting.types[0], PhoneConstants.APN_TYPE_MMS, mApnSetting.mmsProxy,
+                        isIpAddress(mApnSetting.mmsProxy)));
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static final String TCP_BUFFER_SIZES_GPRS = "4092,8760,48000,4096,8760,48000";
+    private static final String TCP_BUFFER_SIZES_EDGE = "4093,26280,70800,4096,16384,70800";
+    private static final String TCP_BUFFER_SIZES_UMTS = "58254,349525,1048576,58254,349525,1048576";
+    private static final String TCP_BUFFER_SIZES_1XRTT= "16384,32768,131072,4096,16384,102400";
+    private static final String TCP_BUFFER_SIZES_EVDO = "4094,87380,262144,4096,16384,262144";
+    private static final String TCP_BUFFER_SIZES_EHRPD= "131072,262144,1048576,4096,16384,524288";
+    private static final String TCP_BUFFER_SIZES_HSDPA= "61167,367002,1101005,8738,52429,262114";
+    private static final String TCP_BUFFER_SIZES_HSPA = "40778,244668,734003,16777,100663,301990";
+    private static final String TCP_BUFFER_SIZES_LTE  =
+            "524288,1048576,2097152,262144,524288,1048576";
+    private static final String TCP_BUFFER_SIZES_HSPAP= "122334,734003,2202010,32040,192239,576717";
+
+    private void updateTcpBufferSizes(int rilRat) {
+        String sizes = null;
+        if (rilRat == ServiceState.RIL_RADIO_TECHNOLOGY_LTE_CA) {
+            // for now treat CA as LTE.  Plan to surface the extra bandwith in a more
+            // precise manner which should affect buffer sizes
+            rilRat = ServiceState.RIL_RADIO_TECHNOLOGY_LTE;
+        }
+        String ratName = ServiceState.rilRadioTechnologyToString(rilRat).toLowerCase(Locale.ROOT);
+        // ServiceState gives slightly different names for EVDO tech ("evdo-rev.0" for ex)
+        // - patch it up:
+        if (rilRat == ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_0 ||
+                rilRat == ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_A ||
+                rilRat == ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_B) {
+            ratName = "evdo";
+        }
+
+        // in the form: "ratname:rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max"
+        String[] configOverride = mPhone.getContext().getResources().getStringArray(
+                com.android.internal.R.array.config_mobile_tcp_buffers);
+        for (int i = 0; i < configOverride.length; i++) {
+            String[] split = configOverride[i].split(":");
+            if (ratName.equals(split[0]) && split.length == 2) {
+                sizes = split[1];
+                break;
+            }
+        }
+
+        if (sizes == null) {
+            // no override - use telephony defaults
+            // doing it this way allows device or carrier to just override the types they
+            // care about and inherit the defaults for the others.
+            switch (rilRat) {
+                case ServiceState.RIL_RADIO_TECHNOLOGY_GPRS:
+                    sizes = TCP_BUFFER_SIZES_GPRS;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_EDGE:
+                    sizes = TCP_BUFFER_SIZES_EDGE;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_UMTS:
+                    sizes = TCP_BUFFER_SIZES_UMTS;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT:
+                    sizes = TCP_BUFFER_SIZES_1XRTT;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_0:
+                case ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_A:
+                case ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_B:
+                    sizes = TCP_BUFFER_SIZES_EVDO;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD:
+                    sizes = TCP_BUFFER_SIZES_EHRPD;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_HSDPA:
+                    sizes = TCP_BUFFER_SIZES_HSDPA;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_HSPA:
+                case ServiceState.RIL_RADIO_TECHNOLOGY_HSUPA:
+                    sizes = TCP_BUFFER_SIZES_HSPA;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_LTE:
+                case ServiceState.RIL_RADIO_TECHNOLOGY_LTE_CA:
+                    sizes = TCP_BUFFER_SIZES_LTE;
+                    break;
+                case ServiceState.RIL_RADIO_TECHNOLOGY_HSPAP:
+                    sizes = TCP_BUFFER_SIZES_HSPAP;
+                    break;
+                default:
+                    // Leave empty - this will let ConnectivityService use the system default.
+                    break;
+            }
+        }
+        mLinkProperties.setTcpBufferSizes(sizes);
+    }
+
+    /**
+     * Indicates if when this connection was established we had a restricted/privileged
+     * NetworkRequest and needed it to overcome data-enabled limitations.
+     *
+     * This gets set once per connection setup and is based on conditions at that time.
+     * We could theoretically have dynamic capabilities but now is not a good time to
+     * experiment with that.
+     *
+     * This flag overrides the APN-based restriction capability, restricting the network
+     * based on both having a NetworkRequest with restricted AND needing a restricted
+     * bit to overcome user-disabled status.  This allows us to handle the common case
+     * of having both restricted requests and unrestricted requests for the same apn:
+     * if conditions require a restricted network to overcome user-disabled then it must
+     * be restricted, otherwise it is unrestricted (or restricted based on APN type).
+     *
+     * Because we're not supporting dynamic capabilities, if conditions change and we go from
+     * data-enabled to not or vice-versa we will need to tear down networks to deal with it
+     * at connection setup time with the new state.
+     *
+     * This supports a privileged app bringing up a network without general apps having access
+     * to it when the network is otherwise unavailable (hipri).  The first use case is
+     * pre-paid SIM reprovisioning over internet, where the carrier insists on no traffic
+     * other than from the privileged carrier-app.
+     */
+    private boolean mRestrictedNetworkOverride = false;
+
+    // Should be called once when the call goes active to examine the state of things and
+    // declare the restriction override for the life of the connection
+    private void setNetworkRestriction() {
+        mRestrictedNetworkOverride = false;
+        // first, if we have no restricted requests, this override can stay FALSE:
+        boolean noRestrictedRequests = true;
+        for (ApnContext apnContext : mApnContexts.keySet()) {
+            noRestrictedRequests &= apnContext.hasNoRestrictedRequests(true /* exclude DUN */);
+        }
+        if (noRestrictedRequests) {
+            return;
+        }
+
+        // Do we need a restricted network to satisfy the request?
+        // Is this network metered?  If not, then don't add restricted
+        if (!mApnSetting.isMetered(mPhone)) {
+            return;
+        }
+
+        // Is data disabled?
+        mRestrictedNetworkOverride = !mDct.isDataEnabled();
+    }
+
+    NetworkCapabilities getNetworkCapabilities() {
+        NetworkCapabilities result = new NetworkCapabilities();
+        result.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+
+        if (mApnSetting != null) {
+            ApnSetting securedDunApn = mDct.fetchDunApn();
+            for (String type : mApnSetting.types) {
+                if (!mRestrictedNetworkOverride
+                        && (mConnectionParams != null && mConnectionParams.mUnmeteredUseOnly)
+                        && ApnSetting.isMeteredApnType(type, mPhone)) {
+                    log("Dropped the metered " + type + " for the unmetered data call.");
+                    continue;
+                }
+                switch (type) {
+                    case PhoneConstants.APN_TYPE_ALL: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_SUPL);
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_FOTA);
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_CBS);
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_IA);
+                        // check if this is the DUN apn as well as returned by fetchDunApn().
+                        // If yes, add DUN capability too.
+                        if (mApnSetting.equals(securedDunApn)) {
+                            result.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+                        }
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_DEFAULT: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_MMS: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_SUPL: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_SUPL);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_DUN: {
+                        if (securedDunApn == null || securedDunApn.equals(mApnSetting)) {
+                            result.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+                        }
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_FOTA: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_FOTA);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_IMS: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_CBS: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_CBS);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_IA: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_IA);
+                        break;
+                    }
+                    case PhoneConstants.APN_TYPE_EMERGENCY: {
+                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_EIMS);
+                        break;
+                    }
+                    default:
+                }
+            }
+
+            // Mark NOT_METERED in the following cases,
+            // 1. All APNs in APN settings are unmetered.
+            // 2. The non-restricted data and is intended for unmetered use only.
+            if (((mConnectionParams != null && mConnectionParams.mUnmeteredUseOnly)
+                    && !mRestrictedNetworkOverride)
+                    || !mApnSetting.isMetered(mPhone)) {
+                result.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+            } else {
+                result.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+            }
+
+            result.maybeMarkCapabilitiesRestricted();
+        }
+        if (mRestrictedNetworkOverride) {
+            result.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+            // don't use dun on restriction-overriden networks.
+            result.removeCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+        }
+
+        int up = 14;
+        int down = 14;
+        switch (mRilRat) {
+            case ServiceState.RIL_RADIO_TECHNOLOGY_GPRS: up = 80; down = 80; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_EDGE: up = 59; down = 236; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_UMTS: up = 384; down = 384; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_IS95A: // fall through
+            case ServiceState.RIL_RADIO_TECHNOLOGY_IS95B: up = 14; down = 14; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_0: up = 153; down = 2457; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_A: up = 1843; down = 3174; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT: up = 100; down = 100; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_HSDPA: up = 2048; down = 14336; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_HSUPA: up = 5898; down = 14336; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_HSPA: up = 5898; down = 14336; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_B: up = 1843; down = 5017; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_LTE: up = 51200; down = 102400; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_LTE_CA: up = 51200; down = 102400; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD: up = 153; down = 2516; break;
+            case ServiceState.RIL_RADIO_TECHNOLOGY_HSPAP: up = 11264; down = 43008; break;
+            default:
+        }
+        result.setLinkUpstreamBandwidthKbps(up);
+        result.setLinkDownstreamBandwidthKbps(down);
+
+        result.setNetworkSpecifier(new StringNetworkSpecifier(Integer.toString(mPhone.getSubId())));
+
+        return result;
+    }
+
+    /**
+     * @return {@code} true iff. {@code address} is a literal IPv4 or IPv6 address.
+     */
+    @VisibleForTesting
+    public static boolean isIpAddress(String address) {
+        if (address == null) return false;
+
+        return InetAddress.isNumeric(address);
+    }
+
+    private DataCallResponse.SetupResult setLinkProperties(DataCallResponse response,
+            LinkProperties lp) {
+        // Check if system property dns usable
+        boolean okToUseSystemPropertyDns = false;
+        String propertyPrefix = "net." + response.ifname + ".";
+        String dnsServers[] = new String[2];
+        dnsServers[0] = SystemProperties.get(propertyPrefix + "dns1");
+        dnsServers[1] = SystemProperties.get(propertyPrefix + "dns2");
+        okToUseSystemPropertyDns = isDnsOk(dnsServers);
+
+        // set link properties based on data call response
+        return response.setLinkProperties(lp, okToUseSystemPropertyDns);
+    }
+
+    /**
+     * Initialize connection, this will fail if the
+     * apnSettings are not compatible.
+     *
+     * @param cp the Connection parameters
+     * @return true if initialization was successful.
+     */
+    private boolean initConnection(ConnectionParams cp) {
+        ApnContext apnContext = cp.mApnContext;
+        if (mApnSetting == null) {
+            // Only change apn setting if it isn't set, it will
+            // only NOT be set only if we're in DcInactiveState.
+            mApnSetting = apnContext.getApnSetting();
+        }
+        if (mApnSetting == null || !mApnSetting.canHandleType(apnContext.getApnType())) {
+            if (DBG) {
+                log("initConnection: incompatible apnSetting in ConnectionParams cp=" + cp
+                        + " dc=" + DataConnection.this);
+            }
+            return false;
+        }
+        mTag += 1;
+        mConnectionParams = cp;
+        mConnectionParams.mTag = mTag;
+
+        // always update the ConnectionParams with the latest or the
+        // connectionGeneration gets stale
+        mApnContexts.put(apnContext, cp);
+
+        if (DBG) {
+            log("initConnection: "
+                    + " RefCount=" + mApnContexts.size()
+                    + " mApnList=" + mApnContexts
+                    + " mConnectionParams=" + mConnectionParams);
+        }
+        return true;
+    }
+
+    /**
+     * The parent state for all other states.
+     */
+    private class DcDefaultState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log("DcDefaultState: enter");
+
+            // Register for DRS or RAT change
+            mPhone.getServiceStateTracker().registerForDataRegStateOrRatChanged(getHandler(),
+                    DataConnection.EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED, null);
+
+            mPhone.getServiceStateTracker().registerForDataRoamingOn(getHandler(),
+                    DataConnection.EVENT_DATA_CONNECTION_ROAM_ON, null);
+            mPhone.getServiceStateTracker().registerForDataRoamingOff(getHandler(),
+                    DataConnection.EVENT_DATA_CONNECTION_ROAM_OFF, null, true);
+
+            // Add ourselves to the list of data connections
+            mDcController.addDc(DataConnection.this);
+        }
+        @Override
+        public void exit() {
+            if (DBG) log("DcDefaultState: exit");
+
+            // Unregister for DRS or RAT change.
+            mPhone.getServiceStateTracker().unregisterForDataRegStateOrRatChanged(getHandler());
+
+            mPhone.getServiceStateTracker().unregisterForDataRoamingOn(getHandler());
+            mPhone.getServiceStateTracker().unregisterForDataRoamingOff(getHandler());
+
+            // Remove ourselves from the DC lists
+            mDcController.removeDc(DataConnection.this);
+
+            if (mAc != null) {
+                mAc.disconnected();
+                mAc = null;
+            }
+            mApnContexts = null;
+            mReconnectIntent = null;
+            mDct = null;
+            mApnSetting = null;
+            mPhone = null;
+            mLinkProperties = null;
+            mLastFailCause = null;
+            mUserData = null;
+            mDcController = null;
+            mDcTesterFailBringUpAll = null;
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            boolean retVal = HANDLED;
+
+            if (VDBG) {
+                log("DcDefault msg=" + getWhatToString(msg.what)
+                        + " RefCount=" + mApnContexts.size());
+            }
+            switch (msg.what) {
+                case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
+                    if (mAc != null) {
+                        if (VDBG) log("Disconnecting to previous connection mAc=" + mAc);
+                        mAc.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                                AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED);
+                    } else {
+                        mAc = new AsyncChannel();
+                        mAc.connected(null, getHandler(), msg.replyTo);
+                        if (VDBG) log("DcDefaultState: FULL_CONNECTION reply connected");
+                        mAc.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                                AsyncChannel.STATUS_SUCCESSFUL, mId, "hi");
+                    }
+                    break;
+                }
+                case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
+                    if (DBG) {
+                        log("DcDefault: CMD_CHANNEL_DISCONNECTED before quiting call dump");
+                        dumpToLog();
+                    }
+
+                    quit();
+                    break;
+                }
+                case DcAsyncChannel.REQ_IS_INACTIVE: {
+                    boolean val = getIsInactive();
+                    if (VDBG) log("REQ_IS_INACTIVE  isInactive=" + val);
+                    mAc.replyToMessage(msg, DcAsyncChannel.RSP_IS_INACTIVE, val ? 1 : 0);
+                    break;
+                }
+                case DcAsyncChannel.REQ_GET_CID: {
+                    int cid = getCid();
+                    if (VDBG) log("REQ_GET_CID  cid=" + cid);
+                    mAc.replyToMessage(msg, DcAsyncChannel.RSP_GET_CID, cid);
+                    break;
+                }
+                case DcAsyncChannel.REQ_GET_APNSETTING: {
+                    ApnSetting apnSetting = getApnSetting();
+                    if (VDBG) log("REQ_GET_APNSETTING  mApnSetting=" + apnSetting);
+                    mAc.replyToMessage(msg, DcAsyncChannel.RSP_GET_APNSETTING, apnSetting);
+                    break;
+                }
+                case DcAsyncChannel.REQ_GET_LINK_PROPERTIES: {
+                    LinkProperties lp = getCopyLinkProperties();
+                    if (VDBG) log("REQ_GET_LINK_PROPERTIES linkProperties" + lp);
+                    mAc.replyToMessage(msg, DcAsyncChannel.RSP_GET_LINK_PROPERTIES, lp);
+                    break;
+                }
+                case DcAsyncChannel.REQ_SET_LINK_PROPERTIES_HTTP_PROXY: {
+                    ProxyInfo proxy = (ProxyInfo) msg.obj;
+                    if (VDBG) log("REQ_SET_LINK_PROPERTIES_HTTP_PROXY proxy=" + proxy);
+                    setLinkPropertiesHttpProxy(proxy);
+                    mAc.replyToMessage(msg, DcAsyncChannel.RSP_SET_LINK_PROPERTIES_HTTP_PROXY);
+                    if (mNetworkAgent != null) {
+                        mNetworkAgent.sendLinkProperties(mLinkProperties);
+                    }
+                    break;
+                }
+                case DcAsyncChannel.REQ_GET_NETWORK_CAPABILITIES: {
+                    NetworkCapabilities nc = getNetworkCapabilities();
+                    if (VDBG) log("REQ_GET_NETWORK_CAPABILITIES networkCapabilities" + nc);
+                    mAc.replyToMessage(msg, DcAsyncChannel.RSP_GET_NETWORK_CAPABILITIES, nc);
+                    break;
+                }
+                case DcAsyncChannel.REQ_RESET:
+                    if (VDBG) log("DcDefaultState: msg.what=REQ_RESET");
+                    transitionTo(mInactiveState);
+                    break;
+                case EVENT_CONNECT:
+                    if (DBG) log("DcDefaultState: msg.what=EVENT_CONNECT, fail not expected");
+                    ConnectionParams cp = (ConnectionParams) msg.obj;
+                    notifyConnectCompleted(cp, DcFailCause.UNKNOWN, false);
+                    break;
+
+                case EVENT_DISCONNECT:
+                    if (DBG) {
+                        log("DcDefaultState deferring msg.what=EVENT_DISCONNECT RefCount="
+                                + mApnContexts.size());
+                    }
+                    deferMessage(msg);
+                    break;
+
+                case EVENT_DISCONNECT_ALL:
+                    if (DBG) {
+                        log("DcDefaultState deferring msg.what=EVENT_DISCONNECT_ALL RefCount="
+                                + mApnContexts.size());
+                    }
+                    deferMessage(msg);
+                    break;
+
+                case EVENT_TEAR_DOWN_NOW:
+                    if (DBG) log("DcDefaultState EVENT_TEAR_DOWN_NOW");
+                    mPhone.mCi.deactivateDataCall(mCid, 0,  null);
+                    break;
+
+                case EVENT_LOST_CONNECTION:
+                    if (DBG) {
+                        String s = "DcDefaultState ignore EVENT_LOST_CONNECTION"
+                                + " tag=" + msg.arg1 + ":mTag=" + mTag;
+                        logAndAddLogRec(s);
+                    }
+                    break;
+                case EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED:
+                    AsyncResult ar = (AsyncResult)msg.obj;
+                    Pair<Integer, Integer> drsRatPair = (Pair<Integer, Integer>)ar.result;
+                    mDataRegState = drsRatPair.first;
+                    if (mRilRat != drsRatPair.second) {
+                        updateTcpBufferSizes(drsRatPair.second);
+                    }
+                    mRilRat = drsRatPair.second;
+                    if (DBG) {
+                        log("DcDefaultState: EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED"
+                                + " drs=" + mDataRegState
+                                + " mRilRat=" + mRilRat);
+                    }
+                    ServiceState ss = mPhone.getServiceState();
+                    int networkType = ss.getDataNetworkType();
+                    mNetworkInfo.setSubtype(networkType,
+                            TelephonyManager.getNetworkTypeName(networkType));
+                    if (mNetworkAgent != null) {
+                        updateNetworkInfoSuspendState();
+                        mNetworkAgent.sendNetworkCapabilities(getNetworkCapabilities());
+                        mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+                        mNetworkAgent.sendLinkProperties(mLinkProperties);
+                    }
+                    break;
+
+                case EVENT_DATA_CONNECTION_ROAM_ON:
+                    mNetworkInfo.setRoaming(true);
+                    break;
+
+                case EVENT_DATA_CONNECTION_ROAM_OFF:
+                    mNetworkInfo.setRoaming(false);
+                    break;
+
+                default:
+                    if (DBG) {
+                        log("DcDefaultState: shouldn't happen but ignore msg.what="
+                                + getWhatToString(msg.what));
+                    }
+                    break;
+            }
+
+            return retVal;
+        }
+    }
+
+    private boolean updateNetworkInfoSuspendState() {
+        final NetworkInfo.DetailedState oldState = mNetworkInfo.getDetailedState();
+
+        // this is only called when we are either connected or suspended.  Decide which.
+        if (mNetworkAgent == null) {
+            Rlog.e(getName(), "Setting suspend state without a NetworkAgent");
+        }
+
+        // if we are not in-service change to SUSPENDED
+        final ServiceStateTracker sst = mPhone.getServiceStateTracker();
+        if (sst.getCurrentDataConnectionState() != ServiceState.STATE_IN_SERVICE) {
+            mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.SUSPENDED, null,
+                    mNetworkInfo.getExtraInfo());
+        } else {
+            // check for voice call and concurrency issues
+            if (sst.isConcurrentVoiceAndDataAllowed() == false) {
+                final CallTracker ct = mPhone.getCallTracker();
+                if (ct.getState() != PhoneConstants.State.IDLE) {
+                    mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.SUSPENDED, null,
+                            mNetworkInfo.getExtraInfo());
+                    return (oldState != NetworkInfo.DetailedState.SUSPENDED);
+                }
+            }
+            mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null,
+                    mNetworkInfo.getExtraInfo());
+        }
+        return (oldState != mNetworkInfo.getDetailedState());
+    }
+
+    private DcDefaultState mDefaultState = new DcDefaultState();
+
+    /**
+     * The state machine is inactive and expects a EVENT_CONNECT.
+     */
+    private class DcInactiveState extends State {
+        // Inform all contexts we've failed connecting
+        public void setEnterNotificationParams(ConnectionParams cp, DcFailCause cause) {
+            if (VDBG) log("DcInactiveState: setEnterNotificationParams cp,cause");
+            mConnectionParams = cp;
+            mDisconnectParams = null;
+            mDcFailCause = cause;
+        }
+
+        // Inform all contexts we've failed disconnected
+        public void setEnterNotificationParams(DisconnectParams dp) {
+            if (VDBG) log("DcInactiveState: setEnterNotificationParams dp");
+            mConnectionParams = null;
+            mDisconnectParams = dp;
+            mDcFailCause = DcFailCause.NONE;
+        }
+
+        // Inform all contexts of the failure cause
+        public void setEnterNotificationParams(DcFailCause cause) {
+            mConnectionParams = null;
+            mDisconnectParams = null;
+            mDcFailCause = cause;
+        }
+
+        @Override
+        public void enter() {
+            mTag += 1;
+            if (DBG) log("DcInactiveState: enter() mTag=" + mTag);
+
+            if (mConnectionParams != null) {
+                if (DBG) {
+                    log("DcInactiveState: enter notifyConnectCompleted +ALL failCause="
+                            + mDcFailCause);
+                }
+                notifyConnectCompleted(mConnectionParams, mDcFailCause, true);
+            }
+            if (mDisconnectParams != null) {
+                if (DBG) {
+                    log("DcInactiveState: enter notifyDisconnectCompleted +ALL failCause="
+                            + mDcFailCause);
+                }
+                notifyDisconnectCompleted(mDisconnectParams, true);
+            }
+            if (mDisconnectParams == null && mConnectionParams == null && mDcFailCause != null) {
+                if (DBG) {
+                    log("DcInactiveState: enter notifyAllDisconnectCompleted failCause="
+                            + mDcFailCause);
+                }
+                notifyAllDisconnectCompleted(mDcFailCause);
+            }
+
+            // Remove ourselves from cid mapping, before clearSettings
+            mDcController.removeActiveDcByCid(DataConnection.this);
+
+            clearSettings();
+        }
+
+        @Override
+        public void exit() {
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            boolean retVal;
+
+            switch (msg.what) {
+                case DcAsyncChannel.REQ_RESET:
+                    if (DBG) {
+                        log("DcInactiveState: msg.what=RSP_RESET, ignore we're already reset");
+                    }
+                    retVal = HANDLED;
+                    break;
+
+                case EVENT_CONNECT:
+                    if (DBG) log("DcInactiveState: mag.what=EVENT_CONNECT");
+                    ConnectionParams cp = (ConnectionParams) msg.obj;
+                    if (initConnection(cp)) {
+                        onConnect(mConnectionParams);
+                        transitionTo(mActivatingState);
+                    } else {
+                        if (DBG) {
+                            log("DcInactiveState: msg.what=EVENT_CONNECT initConnection failed");
+                        }
+                        notifyConnectCompleted(cp, DcFailCause.UNACCEPTABLE_NETWORK_PARAMETER,
+                                false);
+                    }
+                    retVal = HANDLED;
+                    break;
+
+                case EVENT_DISCONNECT:
+                    if (DBG) log("DcInactiveState: msg.what=EVENT_DISCONNECT");
+                    notifyDisconnectCompleted((DisconnectParams)msg.obj, false);
+                    retVal = HANDLED;
+                    break;
+
+                case EVENT_DISCONNECT_ALL:
+                    if (DBG) log("DcInactiveState: msg.what=EVENT_DISCONNECT_ALL");
+                    notifyDisconnectCompleted((DisconnectParams)msg.obj, false);
+                    retVal = HANDLED;
+                    break;
+
+                default:
+                    if (VDBG) {
+                        log("DcInactiveState nothandled msg.what=" + getWhatToString(msg.what));
+                    }
+                    retVal = NOT_HANDLED;
+                    break;
+            }
+            return retVal;
+        }
+    }
+    private DcInactiveState mInactiveState = new DcInactiveState();
+
+    /**
+     * The state machine is activating a connection.
+     */
+    private class DcActivatingState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            boolean retVal;
+            AsyncResult ar;
+            ConnectionParams cp;
+
+            if (DBG) log("DcActivatingState: msg=" + msgToString(msg));
+            switch (msg.what) {
+                case EVENT_DATA_CONNECTION_DRS_OR_RAT_CHANGED:
+                case EVENT_CONNECT:
+                    // Activating can't process until we're done.
+                    deferMessage(msg);
+                    retVal = HANDLED;
+                    break;
+
+                case EVENT_SETUP_DATA_CONNECTION_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    cp = (ConnectionParams) ar.userObj;
+
+                    DataCallResponse.SetupResult result = onSetupConnectionCompleted(ar);
+                    if (result != DataCallResponse.SetupResult.ERR_Stale) {
+                        if (mConnectionParams != cp) {
+                            loge("DcActivatingState: WEIRD mConnectionsParams:"+ mConnectionParams
+                                    + " != cp:" + cp);
+                        }
+                    }
+                    if (DBG) {
+                        log("DcActivatingState onSetupConnectionCompleted result=" + result
+                                + " dc=" + DataConnection.this);
+                    }
+                    if (cp.mApnContext != null) {
+                        cp.mApnContext.requestLog("onSetupConnectionCompleted result=" + result);
+                    }
+                    switch (result) {
+                        case SUCCESS:
+                            // All is well
+                            mDcFailCause = DcFailCause.NONE;
+                            transitionTo(mActiveState);
+                            break;
+                        case ERR_BadCommand:
+                            // Vendor ril rejected the command and didn't connect.
+                            // Transition to inactive but send notifications after
+                            // we've entered the mInactive state.
+                            mInactiveState.setEnterNotificationParams(cp, result.mFailCause);
+                            transitionTo(mInactiveState);
+                            break;
+                        case ERR_UnacceptableParameter:
+                            // The addresses given from the RIL are bad
+                            tearDownData(cp);
+                            transitionTo(mDisconnectingErrorCreatingConnection);
+                            break;
+                        case ERR_RilError:
+
+                            // Retrieve the suggested retry delay from the modem and save it.
+                            // If the modem want us to retry the current APN again, it will
+                            // suggest a positive delay value (in milliseconds). Otherwise we'll get
+                            // NO_SUGGESTED_RETRY_DELAY here.
+                            long delay = getSuggestedRetryDelay(ar);
+                            cp.mApnContext.setModemSuggestedDelay(delay);
+
+                            String str = "DcActivatingState: ERR_RilError "
+                                    + " delay=" + delay
+                                    + " result=" + result
+                                    + " result.isRestartRadioFail=" +
+                                    result.mFailCause.isRestartRadioFail(mPhone.getContext(),
+                                            mPhone.getSubId())
+                                    + " isPermanentFailure=" +
+                                    mDct.isPermanentFailure(result.mFailCause);
+                            if (DBG) log(str);
+                            if (cp.mApnContext != null) cp.mApnContext.requestLog(str);
+
+                            // Save the cause. DcTracker.onDataSetupComplete will check this
+                            // failure cause and determine if we need to retry this APN later
+                            // or not.
+                            mInactiveState.setEnterNotificationParams(cp, result.mFailCause);
+                            transitionTo(mInactiveState);
+                            break;
+                        case ERR_Stale:
+                            loge("DcActivatingState: stale EVENT_SETUP_DATA_CONNECTION_DONE"
+                                    + " tag:" + cp.mTag + " != mTag:" + mTag);
+                            break;
+                        default:
+                            throw new RuntimeException("Unknown SetupResult, should not happen");
+                    }
+                    retVal = HANDLED;
+                    break;
+
+                case EVENT_GET_LAST_FAIL_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    cp = (ConnectionParams) ar.userObj;
+                    if (cp.mTag == mTag) {
+                        if (mConnectionParams != cp) {
+                            loge("DcActivatingState: WEIRD mConnectionsParams:" + mConnectionParams
+                                    + " != cp:" + cp);
+                        }
+
+                        DcFailCause cause = DcFailCause.UNKNOWN;
+
+                        if (ar.exception == null) {
+                            int rilFailCause = ((int[]) (ar.result))[0];
+                            cause = DcFailCause.fromInt(rilFailCause);
+                            if (cause == DcFailCause.NONE) {
+                                if (DBG) {
+                                    log("DcActivatingState msg.what=EVENT_GET_LAST_FAIL_DONE"
+                                            + " BAD: error was NONE, change to UNKNOWN");
+                                }
+                                cause = DcFailCause.UNKNOWN;
+                            }
+                        }
+                        mDcFailCause = cause;
+
+                        if (DBG) {
+                            log("DcActivatingState msg.what=EVENT_GET_LAST_FAIL_DONE"
+                                    + " cause=" + cause + " dc=" + DataConnection.this);
+                        }
+
+                        mInactiveState.setEnterNotificationParams(cp, cause);
+                        transitionTo(mInactiveState);
+                    } else {
+                        loge("DcActivatingState: stale EVENT_GET_LAST_FAIL_DONE"
+                                + " tag:" + cp.mTag + " != mTag:" + mTag);
+                    }
+
+                    retVal = HANDLED;
+                    break;
+
+                default:
+                    if (VDBG) {
+                        log("DcActivatingState not handled msg.what=" +
+                                getWhatToString(msg.what) + " RefCount=" + mApnContexts.size());
+                    }
+                    retVal = NOT_HANDLED;
+                    break;
+            }
+            return retVal;
+        }
+    }
+    private DcActivatingState mActivatingState = new DcActivatingState();
+
+    /**
+     * The state machine is connected, expecting an EVENT_DISCONNECT.
+     */
+    private class DcActiveState extends State {
+        @Override public void enter() {
+            if (DBG) log("DcActiveState: enter dc=" + DataConnection.this);
+
+            // verify and get updated information in case these things
+            // are obsolete
+            ServiceState ss = mPhone.getServiceState();
+            final int networkType = ss.getDataNetworkType();
+            if (mNetworkInfo.getSubtype() != networkType) {
+                log("DcActiveState with incorrect subtype (" + mNetworkInfo.getSubtype()
+                        + ", " + networkType + "), updating.");
+            }
+            mNetworkInfo.setSubtype(networkType, TelephonyManager.getNetworkTypeName(networkType));
+            final boolean roaming = ss.getDataRoaming();
+            if (roaming != mNetworkInfo.isRoaming()) {
+                log("DcActiveState with incorrect roaming (" + mNetworkInfo.isRoaming()
+                        + ", " + roaming + "), updating.");
+            }
+
+            mNetworkInfo.setRoaming(roaming);
+
+            // If we were retrying there maybe more than one, otherwise they'll only be one.
+            notifyAllOfConnected(Phone.REASON_CONNECTED);
+
+            mPhone.getCallTracker().registerForVoiceCallStarted(getHandler(),
+                    DataConnection.EVENT_DATA_CONNECTION_VOICE_CALL_STARTED, null);
+            mPhone.getCallTracker().registerForVoiceCallEnded(getHandler(),
+                    DataConnection.EVENT_DATA_CONNECTION_VOICE_CALL_ENDED, null);
+
+            // If the EVENT_CONNECT set the current max retry restore it here
+            // if it didn't then this is effectively a NOP.
+            mDcController.addActiveDcByCid(DataConnection.this);
+
+            mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED,
+                    mNetworkInfo.getReason(), null);
+            mNetworkInfo.setExtraInfo(mApnSetting.apn);
+            updateTcpBufferSizes(mRilRat);
+
+            final NetworkMisc misc = new NetworkMisc();
+            final CarrierSignalAgent carrierSignalAgent = mPhone.getCarrierSignalAgent();
+            if (carrierSignalAgent.hasRegisteredReceivers(TelephonyIntents
+                    .ACTION_CARRIER_SIGNAL_REDIRECTED)) {
+                // carrierSignal Receivers will place the carrier-specific provisioning notification
+                misc.provisioningNotificationDisabled = true;
+            }
+            misc.subscriberId = mPhone.getSubscriberId();
+
+            setNetworkRestriction();
+            mNetworkAgent = new DcNetworkAgent(getHandler().getLooper(), mPhone.getContext(),
+                    "DcNetworkAgent", mNetworkInfo, getNetworkCapabilities(), mLinkProperties,
+                    50, misc);
+        }
+
+        @Override
+        public void exit() {
+            if (DBG) log("DcActiveState: exit dc=" + this);
+            String reason = mNetworkInfo.getReason();
+            if(mDcController.isExecutingCarrierChange()) {
+                reason = Phone.REASON_CARRIER_CHANGE;
+            } else if (mDisconnectParams != null && mDisconnectParams.mReason != null) {
+                reason = mDisconnectParams.mReason;
+            } else if (mDcFailCause != null) {
+                reason = mDcFailCause.toString();
+            }
+            mPhone.getCallTracker().unregisterForVoiceCallStarted(getHandler());
+            mPhone.getCallTracker().unregisterForVoiceCallEnded(getHandler());
+
+            mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
+                    reason, mNetworkInfo.getExtraInfo());
+            if (mNetworkAgent != null) {
+                mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+                mNetworkAgent = null;
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            boolean retVal;
+
+            switch (msg.what) {
+                case EVENT_CONNECT: {
+                    ConnectionParams cp = (ConnectionParams) msg.obj;
+                    // either add this new apn context to our set or
+                    // update the existing cp with the latest connection generation number
+                    mApnContexts.put(cp.mApnContext, cp);
+                    if (DBG) {
+                        log("DcActiveState: EVENT_CONNECT cp=" + cp + " dc=" + DataConnection.this);
+                    }
+                    notifyConnectCompleted(cp, DcFailCause.NONE, false);
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_DISCONNECT: {
+                    DisconnectParams dp = (DisconnectParams) msg.obj;
+                    if (DBG) {
+                        log("DcActiveState: EVENT_DISCONNECT dp=" + dp
+                                + " dc=" + DataConnection.this);
+                    }
+                    if (mApnContexts.containsKey(dp.mApnContext)) {
+                        if (DBG) {
+                            log("DcActiveState msg.what=EVENT_DISCONNECT RefCount="
+                                    + mApnContexts.size());
+                        }
+
+                        if (mApnContexts.size() == 1) {
+                            mApnContexts.clear();
+                            mDisconnectParams = dp;
+                            mConnectionParams = null;
+                            dp.mTag = mTag;
+                            tearDownData(dp);
+                            transitionTo(mDisconnectingState);
+                        } else {
+                            mApnContexts.remove(dp.mApnContext);
+                            notifyDisconnectCompleted(dp, false);
+                        }
+                    } else {
+                        log("DcActiveState ERROR no such apnContext=" + dp.mApnContext
+                                + " in this dc=" + DataConnection.this);
+                        notifyDisconnectCompleted(dp, false);
+                    }
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_DISCONNECT_ALL: {
+                    if (DBG) {
+                        log("DcActiveState EVENT_DISCONNECT clearing apn contexts,"
+                                + " dc=" + DataConnection.this);
+                    }
+                    DisconnectParams dp = (DisconnectParams) msg.obj;
+                    mDisconnectParams = dp;
+                    mConnectionParams = null;
+                    dp.mTag = mTag;
+                    tearDownData(dp);
+                    transitionTo(mDisconnectingState);
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_LOST_CONNECTION: {
+                    if (DBG) {
+                        log("DcActiveState EVENT_LOST_CONNECTION dc=" + DataConnection.this);
+                    }
+
+                    mInactiveState.setEnterNotificationParams(DcFailCause.LOST_CONNECTION);
+                    transitionTo(mInactiveState);
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_DATA_CONNECTION_ROAM_ON: {
+                    mNetworkInfo.setRoaming(true);
+                    if (mNetworkAgent != null) {
+                        mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+                    }
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_DATA_CONNECTION_ROAM_OFF: {
+                    mNetworkInfo.setRoaming(false);
+                    if (mNetworkAgent != null) {
+                        mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+                    }
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_BW_REFRESH_RESPONSE: {
+                    AsyncResult ar = (AsyncResult)msg.obj;
+                    if (ar.exception != null) {
+                        log("EVENT_BW_REFRESH_RESPONSE: error ignoring, e=" + ar.exception);
+                    } else {
+                        final ArrayList<Integer> capInfo = (ArrayList<Integer>)ar.result;
+                        final int lceBwDownKbps = capInfo.get(0);
+                        NetworkCapabilities nc = getNetworkCapabilities();
+                        if (mPhone.getLceStatus() == RILConstants.LCE_ACTIVE) {
+                            nc.setLinkDownstreamBandwidthKbps(lceBwDownKbps);
+                            if (mNetworkAgent != null) {
+                                mNetworkAgent.sendNetworkCapabilities(nc);
+                            }
+                        }
+                    }
+                    retVal = HANDLED;
+                    break;
+                }
+                case EVENT_DATA_CONNECTION_VOICE_CALL_STARTED:
+                case EVENT_DATA_CONNECTION_VOICE_CALL_ENDED: {
+                    if (updateNetworkInfoSuspendState() && mNetworkAgent != null) {
+                        // state changed
+                        mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+                    }
+                    retVal = HANDLED;
+                    break;
+                }
+                default:
+                    if (VDBG) {
+                        log("DcActiveState not handled msg.what=" + getWhatToString(msg.what));
+                    }
+                    retVal = NOT_HANDLED;
+                    break;
+            }
+            return retVal;
+        }
+    }
+    private DcActiveState mActiveState = new DcActiveState();
+
+    /**
+     * The state machine is disconnecting.
+     */
+    private class DcDisconnectingState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            boolean retVal;
+
+            switch (msg.what) {
+                case EVENT_CONNECT:
+                    if (DBG) log("DcDisconnectingState msg.what=EVENT_CONNECT. Defer. RefCount = "
+                            + mApnContexts.size());
+                    deferMessage(msg);
+                    retVal = HANDLED;
+                    break;
+
+                case EVENT_DEACTIVATE_DONE:
+                    AsyncResult ar = (AsyncResult) msg.obj;
+                    DisconnectParams dp = (DisconnectParams) ar.userObj;
+
+                    String str = "DcDisconnectingState msg.what=EVENT_DEACTIVATE_DONE RefCount="
+                            + mApnContexts.size();
+                    if (DBG) log(str);
+                    if (dp.mApnContext != null) dp.mApnContext.requestLog(str);
+
+                    if (dp.mTag == mTag) {
+                        // Transition to inactive but send notifications after
+                        // we've entered the mInactive state.
+                        mInactiveState.setEnterNotificationParams((DisconnectParams) ar.userObj);
+                        transitionTo(mInactiveState);
+                    } else {
+                        if (DBG) log("DcDisconnectState stale EVENT_DEACTIVATE_DONE"
+                                + " dp.tag=" + dp.mTag + " mTag=" + mTag);
+                    }
+                    retVal = HANDLED;
+                    break;
+
+                default:
+                    if (VDBG) {
+                        log("DcDisconnectingState not handled msg.what="
+                                + getWhatToString(msg.what));
+                    }
+                    retVal = NOT_HANDLED;
+                    break;
+            }
+            return retVal;
+        }
+    }
+    private DcDisconnectingState mDisconnectingState = new DcDisconnectingState();
+
+    /**
+     * The state machine is disconnecting after an creating a connection.
+     */
+    private class DcDisconnectionErrorCreatingConnection extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            boolean retVal;
+
+            switch (msg.what) {
+                case EVENT_DEACTIVATE_DONE:
+                    AsyncResult ar = (AsyncResult) msg.obj;
+                    ConnectionParams cp = (ConnectionParams) ar.userObj;
+                    if (cp.mTag == mTag) {
+                        String str = "DcDisconnectionErrorCreatingConnection" +
+                                " msg.what=EVENT_DEACTIVATE_DONE";
+                        if (DBG) log(str);
+                        if (cp.mApnContext != null) cp.mApnContext.requestLog(str);
+
+                        // Transition to inactive but send notifications after
+                        // we've entered the mInactive state.
+                        mInactiveState.setEnterNotificationParams(cp,
+                                DcFailCause.UNACCEPTABLE_NETWORK_PARAMETER);
+                        transitionTo(mInactiveState);
+                    } else {
+                        if (DBG) {
+                            log("DcDisconnectionErrorCreatingConnection stale EVENT_DEACTIVATE_DONE"
+                                    + " dp.tag=" + cp.mTag + ", mTag=" + mTag);
+                        }
+                    }
+                    retVal = HANDLED;
+                    break;
+
+                default:
+                    if (VDBG) {
+                        log("DcDisconnectionErrorCreatingConnection not handled msg.what="
+                                + getWhatToString(msg.what));
+                    }
+                    retVal = NOT_HANDLED;
+                    break;
+            }
+            return retVal;
+        }
+    }
+    private DcDisconnectionErrorCreatingConnection mDisconnectingErrorCreatingConnection =
+                new DcDisconnectionErrorCreatingConnection();
+
+
+    private class DcNetworkAgent extends NetworkAgent {
+        public DcNetworkAgent(Looper l, Context c, String TAG, NetworkInfo ni,
+                NetworkCapabilities nc, LinkProperties lp, int score, NetworkMisc misc) {
+            super(l, c, TAG, ni, nc, lp, score, misc);
+        }
+
+        @Override
+        protected void unwanted() {
+            if (mNetworkAgent != this) {
+                log("DcNetworkAgent: unwanted found mNetworkAgent=" + mNetworkAgent +
+                        ", which isn't me.  Aborting unwanted");
+                return;
+            }
+            // this can only happen if our exit has been called - we're already disconnected
+            if (mApnContexts == null) return;
+            for (ConnectionParams cp : mApnContexts.values()) {
+                final ApnContext apnContext = cp.mApnContext;
+                final Pair<ApnContext, Integer> pair =
+                        new Pair<ApnContext, Integer>(apnContext, cp.mConnectionGeneration);
+                log("DcNetworkAgent: [unwanted]: disconnect apnContext=" + apnContext);
+                Message msg = mDct.obtainMessage(DctConstants.EVENT_DISCONNECT_DONE, pair);
+                DisconnectParams dp = new DisconnectParams(apnContext, apnContext.getReason(), msg);
+                DataConnection.this.sendMessage(DataConnection.this.
+                        obtainMessage(EVENT_DISCONNECT, dp));
+            }
+        }
+
+        @Override
+        protected void pollLceData() {
+            if(mPhone.getLceStatus() == RILConstants.LCE_ACTIVE) {  // active LCE service
+                mPhone.mCi.pullLceData(DataConnection.this.obtainMessage(EVENT_BW_REFRESH_RESPONSE));
+            }
+        }
+
+        @Override
+        protected void networkStatus(int status, String redirectUrl) {
+            if(!TextUtils.isEmpty(redirectUrl)) {
+                log("validation status: " + status + " with redirection URL: " + redirectUrl);
+                /* its possible that we have multiple DataConnection with INTERNET_CAPABILITY
+                   all fail the validation with the same redirection url, send CMD back to DCTracker
+                   and let DcTracker to make the decision */
+                Message msg = mDct.obtainMessage(DctConstants.EVENT_REDIRECTION_DETECTED,
+                        redirectUrl);
+                msg.sendToTarget();
+            }
+        }
+    }
+
+    // ******* "public" interface
+
+    /**
+     * Used for testing purposes.
+     */
+    /* package */ void tearDownNow() {
+        if (DBG) log("tearDownNow()");
+        sendMessage(obtainMessage(EVENT_TEAR_DOWN_NOW));
+    }
+
+    /**
+     * Using the result of the SETUP_DATA_CALL determine the retry delay.
+     *
+     * @param ar is the result from SETUP_DATA_CALL
+     * @return NO_SUGGESTED_RETRY_DELAY if no retry is needed otherwise the delay to the
+     *         next SETUP_DATA_CALL
+     */
+    private long getSuggestedRetryDelay(AsyncResult ar) {
+
+        DataCallResponse response = (DataCallResponse) ar.result;
+
+        /** According to ril.h
+         * The value < 0 means no value is suggested
+         * The value 0 means retry should be done ASAP.
+         * The value of Integer.MAX_VALUE(0x7fffffff) means no retry.
+         */
+
+        // The value < 0 means no value is suggested
+        if (response.suggestedRetryTime < 0) {
+            if (DBG) log("No suggested retry delay.");
+            return RetryManager.NO_SUGGESTED_RETRY_DELAY;
+        }
+        // The value of Integer.MAX_VALUE(0x7fffffff) means no retry.
+        else if (response.suggestedRetryTime == Integer.MAX_VALUE) {
+            if (DBG) log("Modem suggested not retrying.");
+            return RetryManager.NO_RETRY;
+        }
+
+        // We need to cast it to long because the value returned from RIL is a 32-bit integer,
+        // but the time values used in AlarmManager are all 64-bit long.
+        return (long) response.suggestedRetryTime;
+    }
+
+    /**
+     * @return the string for msg.what as our info.
+     */
+    @Override
+    protected String getWhatToString(int what) {
+        return cmdToString(what);
+    }
+
+    private static String msgToString(Message msg) {
+        String retVal;
+        if (msg == null) {
+            retVal = "null";
+        } else {
+            StringBuilder   b = new StringBuilder();
+
+            b.append("{what=");
+            b.append(cmdToString(msg.what));
+
+            b.append(" when=");
+            TimeUtils.formatDuration(msg.getWhen() - SystemClock.uptimeMillis(), b);
+
+            if (msg.arg1 != 0) {
+                b.append(" arg1=");
+                b.append(msg.arg1);
+            }
+
+            if (msg.arg2 != 0) {
+                b.append(" arg2=");
+                b.append(msg.arg2);
+            }
+
+            if (msg.obj != null) {
+                b.append(" obj=");
+                b.append(msg.obj);
+            }
+
+            b.append(" target=");
+            b.append(msg.getTarget());
+
+            b.append(" replyTo=");
+            b.append(msg.replyTo);
+
+            b.append("}");
+
+            retVal = b.toString();
+        }
+        return retVal;
+    }
+
+    static void slog(String s) {
+        Rlog.d("DC", s);
+    }
+
+    /**
+     * Log with debug
+     *
+     * @param s is string log
+     */
+    @Override
+    protected void log(String s) {
+        Rlog.d(getName(), s);
+    }
+
+    /**
+     * Log with debug attribute
+     *
+     * @param s is string log
+     */
+    @Override
+    protected void logd(String s) {
+        Rlog.d(getName(), s);
+    }
+
+    /**
+     * Log with verbose attribute
+     *
+     * @param s is string log
+     */
+    @Override
+    protected void logv(String s) {
+        Rlog.v(getName(), s);
+    }
+
+    /**
+     * Log with info attribute
+     *
+     * @param s is string log
+     */
+    @Override
+    protected void logi(String s) {
+        Rlog.i(getName(), s);
+    }
+
+    /**
+     * Log with warning attribute
+     *
+     * @param s is string log
+     */
+    @Override
+    protected void logw(String s) {
+        Rlog.w(getName(), s);
+    }
+
+    /**
+     * Log with error attribute
+     *
+     * @param s is string log
+     */
+    @Override
+    protected void loge(String s) {
+        Rlog.e(getName(), s);
+    }
+
+    /**
+     * Log with error attribute
+     *
+     * @param s is string log
+     * @param e is a Throwable which logs additional information.
+     */
+    @Override
+    protected void loge(String s, Throwable e) {
+        Rlog.e(getName(), s, e);
+    }
+
+    /** Doesn't print mApnList of ApnContext's which would be recursive */
+    public String toStringSimple() {
+        return getName() + ": State=" + getCurrentState().getName()
+                + " mApnSetting=" + mApnSetting + " RefCount=" + mApnContexts.size()
+                + " mCid=" + mCid + " mCreateTime=" + mCreateTime
+                + " mLastastFailTime=" + mLastFailTime
+                + " mLastFailCause=" + mLastFailCause
+                + " mTag=" + mTag
+                + " mLinkProperties=" + mLinkProperties
+                + " linkCapabilities=" + getNetworkCapabilities()
+                + " mRestrictedNetworkOverride=" + mRestrictedNetworkOverride;
+    }
+
+    @Override
+    public String toString() {
+        return "{" + toStringSimple() + " mApnContexts=" + mApnContexts + "}";
+    }
+
+    private void dumpToLog() {
+        dump(null, new PrintWriter(new StringWriter(0)) {
+            @Override
+            public void println(String s) {
+                DataConnection.this.logd(s);
+            }
+
+            @Override
+            public void flush() {
+            }
+        }, null);
+    }
+
+    /**
+     * Dump the current state.
+     *
+     * @param fd
+     * @param pw
+     * @param args
+     */
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.print("DataConnection ");
+        super.dump(fd, pw, args);
+        pw.println(" mApnContexts.size=" + mApnContexts.size());
+        pw.println(" mApnContexts=" + mApnContexts);
+        pw.flush();
+        pw.println(" mDataConnectionTracker=" + mDct);
+        pw.println(" mApnSetting=" + mApnSetting);
+        pw.println(" mTag=" + mTag);
+        pw.println(" mCid=" + mCid);
+        pw.println(" mConnectionParams=" + mConnectionParams);
+        pw.println(" mDisconnectParams=" + mDisconnectParams);
+        pw.println(" mDcFailCause=" + mDcFailCause);
+        pw.flush();
+        pw.println(" mPhone=" + mPhone);
+        pw.flush();
+        pw.println(" mLinkProperties=" + mLinkProperties);
+        pw.flush();
+        pw.println(" mDataRegState=" + mDataRegState);
+        pw.println(" mRilRat=" + mRilRat);
+        pw.println(" mNetworkCapabilities=" + getNetworkCapabilities());
+        pw.println(" mCreateTime=" + TimeUtils.logTimeOfDay(mCreateTime));
+        pw.println(" mLastFailTime=" + TimeUtils.logTimeOfDay(mLastFailTime));
+        pw.println(" mLastFailCause=" + mLastFailCause);
+        pw.flush();
+        pw.println(" mUserData=" + mUserData);
+        pw.println(" mInstanceNumber=" + mInstanceNumber);
+        pw.println(" mAc=" + mAc);
+        pw.flush();
+    }
+}
+
diff --git a/com/android/internal/telephony/dataconnection/DataConnectionReasons.java b/com/android/internal/telephony/dataconnection/DataConnectionReasons.java
new file mode 100644
index 0000000..e7afdff
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DataConnectionReasons.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.dataconnection;
+
+import java.util.HashSet;
+
+/**
+ * The class to describe the reasons of allowing or disallowing to establish a data connection.
+ */
+public class DataConnectionReasons {
+    private HashSet<DataDisallowedReasonType> mDataDisallowedReasonSet = new HashSet<>();
+    private DataAllowedReasonType mDataAllowedReason = DataAllowedReasonType.NONE;
+
+    public DataConnectionReasons() {}
+
+    void add(DataDisallowedReasonType reason) {
+        // Adding a disallowed reason will clean up the allowed reason because they are
+        // mutual exclusive.
+        mDataAllowedReason = DataAllowedReasonType.NONE;
+        mDataDisallowedReasonSet.add(reason);
+    }
+
+    void add(DataAllowedReasonType reason) {
+        // Adding an allowed reason will clean up the disallowed reasons because they are
+        // mutual exclusive.
+        mDataDisallowedReasonSet.clear();
+
+        // Only higher priority allowed reason can overwrite the old one. See
+        // DataAllowedReasonType for the oder.
+        if (reason.ordinal() > mDataAllowedReason.ordinal()) {
+            mDataAllowedReason = reason;
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder reasonStr = new StringBuilder();
+        if (mDataDisallowedReasonSet.size() > 0) {
+            reasonStr.append("Data disallowed, reasons:");
+            for (DataDisallowedReasonType reason : mDataDisallowedReasonSet) {
+                reasonStr.append(" ").append(reason);
+            }
+        } else {
+            reasonStr.append("Data allowed, reason:");
+            reasonStr.append(" ").append(mDataAllowedReason);
+        }
+        return reasonStr.toString();
+    }
+
+    void copyFrom(DataConnectionReasons reasons) {
+        this.mDataDisallowedReasonSet = reasons.mDataDisallowedReasonSet;
+        this.mDataAllowedReason = reasons.mDataAllowedReason;
+    }
+
+    boolean allowed() {
+        return mDataDisallowedReasonSet.size() == 0;
+    }
+
+    boolean contains(DataDisallowedReasonType reason) {
+        return mDataDisallowedReasonSet.contains(reason);
+    }
+
+    /**
+     * Check if only one disallowed reason prevent data connection.
+     *
+     * @param reason The given reason to check
+     * @return True if the given reason is the only one that prevents data connection
+     */
+    public boolean containsOnly(DataDisallowedReasonType reason) {
+        return mDataDisallowedReasonSet.size() == 1 && contains(reason);
+    }
+
+    boolean contains(DataAllowedReasonType reason) {
+        return reason == mDataAllowedReason;
+    }
+
+    boolean containsHardDisallowedReasons() {
+        for (DataDisallowedReasonType reason : mDataDisallowedReasonSet) {
+            if (reason.isHardReason()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // Disallowed reasons. There could be multiple reasons if data connection is not allowed.
+    public enum DataDisallowedReasonType {
+        // Soft failure reasons. Normally the reasons from users or policy settings.
+        DATA_DISABLED(false),                   // Data is disabled by the user or policy.
+        ROAMING_DISABLED(false),                // Data roaming is disabled by the user.
+
+        // Belows are all hard failure reasons.
+        NOT_ATTACHED(true),
+        RECORD_NOT_LOADED(true),
+        INVALID_PHONE_STATE(true),
+        CONCURRENT_VOICE_DATA_NOT_ALLOWED(true),
+        PS_RESTRICTED(true),
+        UNDESIRED_POWER_STATE(true),
+        INTERNAL_DATA_DISABLED(true),
+        DEFAULT_DATA_UNSELECTED(true),
+        RADIO_DISABLED_BY_CARRIER(true),
+        APN_NOT_CONNECTABLE(true),
+        ON_IWLAN(true),
+        IN_ECBM(true);
+
+        private boolean mIsHardReason;
+
+        boolean isHardReason() {
+            return mIsHardReason;
+        }
+
+        DataDisallowedReasonType(boolean isHardReason) {
+            mIsHardReason = isHardReason;
+        }
+    }
+
+    // Data allowed reasons. There will be only one reason if data is allowed.
+    enum DataAllowedReasonType {
+        // Note that unlike disallowed reasons, we only have one allowed reason every time
+        // when we check data is allowed or not. The order of these allowed reasons is very
+        // important. The lower ones take precedence over the upper ones.
+        NONE,
+        NORMAL,
+        UNMETERED_APN,
+        RESTRICTED_REQUEST,
+        EMERGENCY_APN,
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DataEnabledSettings.java b/com/android/internal/telephony/dataconnection/DataEnabledSettings.java
new file mode 100644
index 0000000..3b856a7
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DataEnabledSettings.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.dataconnection;
+
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.os.RegistrantList;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.LocalLog;
+import android.util.Pair;
+
+import com.android.internal.telephony.Phone;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * The class to hold different data enabled/disabled settings. Also it allows clients to register
+ * for overall data enabled setting changed event.
+ * @hide
+ */
+public class DataEnabledSettings {
+
+    private static final String LOG_TAG = "DataEnabledSettings";
+
+    public static final int REASON_REGISTERED = 0;
+
+    public static final int REASON_INTERNAL_DATA_ENABLED = 1;
+
+    public static final int REASON_USER_DATA_ENABLED = 2;
+
+    public static final int REASON_POLICY_DATA_ENABLED = 3;
+
+    public static final int REASON_DATA_ENABLED_BY_CARRIER = 4;
+
+    /**
+     * responds to the setInternalDataEnabled call - used internally to turn off data.
+     * For example during emergency calls
+     */
+    private boolean mInternalDataEnabled = true;
+
+    /**
+     * Flag indicating data allowed by network policy manager or not.
+     */
+    private boolean mPolicyDataEnabled = true;
+
+    /**
+     * Indicate if metered APNs are enabled by the carrier. set false to block all the metered APNs
+     * from continuously sending requests, which causes undesired network load.
+     */
+    private boolean mCarrierDataEnabled = true;
+
+    private Phone mPhone = null;
+    private ContentResolver mResolver = null;
+
+    private final RegistrantList mDataEnabledChangedRegistrants = new RegistrantList();
+
+    private final LocalLog mSettingChangeLocalLog = new LocalLog(50);
+
+    @Override
+    public String toString() {
+        return "[mInternalDataEnabled=" + mInternalDataEnabled
+                + ", isUserDataEnabled=" + isUserDataEnabled()
+                + ", isProvisioningDataEnabled=" + isProvisioningDataEnabled()
+                + ", mPolicyDataEnabled=" + mPolicyDataEnabled
+                + ", mCarrierDataEnabled=" + mCarrierDataEnabled + "]";
+    }
+
+    public DataEnabledSettings(Phone phone) {
+        mPhone = phone;
+        mResolver = mPhone.getContext().getContentResolver();
+    }
+
+    public synchronized void setInternalDataEnabled(boolean enabled) {
+        localLog("InternalDataEnabled", enabled);
+        boolean prevDataEnabled = isDataEnabled();
+        mInternalDataEnabled = enabled;
+        if (prevDataEnabled != isDataEnabled()) {
+            notifyDataEnabledChanged(!prevDataEnabled, REASON_INTERNAL_DATA_ENABLED);
+        }
+    }
+    public synchronized boolean isInternalDataEnabled() {
+        return mInternalDataEnabled;
+    }
+
+    public synchronized void setUserDataEnabled(boolean enabled) {
+        localLog("UserDataEnabled", enabled);
+        boolean prevDataEnabled = isDataEnabled();
+
+        Settings.Global.putInt(mResolver, getMobileDataSettingName(), enabled ? 1 : 0);
+
+        if (prevDataEnabled != isDataEnabled()) {
+            notifyDataEnabledChanged(!prevDataEnabled, REASON_USER_DATA_ENABLED);
+        }
+    }
+    public synchronized boolean isUserDataEnabled() {
+        boolean defaultVal = "true".equalsIgnoreCase(SystemProperties.get(
+                "ro.com.android.mobiledata", "true"));
+
+        return (Settings.Global.getInt(mResolver, getMobileDataSettingName(),
+                defaultVal ? 1 : 0) != 0);
+    }
+
+    private String getMobileDataSettingName() {
+        // For single SIM phones, this is a per phone property. Or if it's invalid subId, we
+        // read default setting.
+        int subId = mPhone.getSubId();
+        if (TelephonyManager.getDefault().getSimCount() == 1
+                || !SubscriptionManager.isValidSubscriptionId(subId)) {
+            return Settings.Global.MOBILE_DATA;
+        } else {
+            return Settings.Global.MOBILE_DATA + mPhone.getSubId();
+        }
+    }
+
+    public synchronized void setPolicyDataEnabled(boolean enabled) {
+        localLog("PolicyDataEnabled", enabled);
+        boolean prevDataEnabled = isDataEnabled();
+        mPolicyDataEnabled = enabled;
+        if (prevDataEnabled != isDataEnabled()) {
+            notifyDataEnabledChanged(!prevDataEnabled, REASON_POLICY_DATA_ENABLED);
+        }
+    }
+    public synchronized boolean isPolicyDataEnabled() {
+        return mPolicyDataEnabled;
+    }
+
+    public synchronized void setCarrierDataEnabled(boolean enabled) {
+        localLog("CarrierDataEnabled", enabled);
+        boolean prevDataEnabled = isDataEnabled();
+        mCarrierDataEnabled = enabled;
+        if (prevDataEnabled != isDataEnabled()) {
+            notifyDataEnabledChanged(!prevDataEnabled, REASON_DATA_ENABLED_BY_CARRIER);
+        }
+    }
+    public synchronized boolean isCarrierDataEnabled() {
+        return mCarrierDataEnabled;
+    }
+
+    public synchronized boolean isDataEnabled() {
+        if (isProvisioning()) {
+            return isProvisioningDataEnabled();
+        } else {
+            return mInternalDataEnabled && isUserDataEnabled()
+                    && mPolicyDataEnabled && mCarrierDataEnabled;
+        }
+    }
+
+    public boolean isProvisioning() {
+        return Settings.Global.getInt(mResolver, Settings.Global.DEVICE_PROVISIONED, 0) == 0;
+    }
+    /**
+     * In provisioning, we might want to have enable mobile data during provisioning. It depends
+     * on value of Settings.Global.DEVICE_PROVISIONING_MOBILE_DATA_ENABLED which is set by
+     * setupwizard. It only matters if it's in provisioning stage.
+     * @return whether we are enabling userData during provisioning stage.
+     */
+    public boolean isProvisioningDataEnabled() {
+        final String prov_property = SystemProperties.get("ro.com.android.prov_mobiledata",
+                "false");
+        boolean retVal = "true".equalsIgnoreCase(prov_property);
+
+        final int prov_mobile_data = Settings.Global.getInt(mResolver,
+                Settings.Global.DEVICE_PROVISIONING_MOBILE_DATA_ENABLED,
+                retVal ? 1 : 0);
+        retVal = prov_mobile_data != 0;
+        log("getDataEnabled during provisioning retVal=" + retVal + " - (" + prov_property
+                + ", " + prov_mobile_data + ")");
+
+        return retVal;
+    }
+
+    private void notifyDataEnabledChanged(boolean enabled, int reason) {
+        mDataEnabledChangedRegistrants.notifyResult(new Pair<>(enabled, reason));
+    }
+
+    public void registerForDataEnabledChanged(Handler h, int what, Object obj) {
+        mDataEnabledChangedRegistrants.addUnique(h, what, obj);
+        notifyDataEnabledChanged(isDataEnabled(), REASON_REGISTERED);
+    }
+
+    public void unregisterForDataEnabledChanged(Handler h) {
+        mDataEnabledChangedRegistrants.remove(h);
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void localLog(String name, boolean value) {
+        mSettingChangeLocalLog.log(name + " change to " + value);
+    }
+
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(" DataEnabledSettings=");
+        mSettingChangeLocalLog.dump(fd, pw, args);
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DataProfile.java b/com/android/internal/telephony/dataconnection/DataProfile.java
new file mode 100644
index 0000000..48a8107
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DataProfile.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony.dataconnection;
+
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.RILConstants;
+
+public class DataProfile {
+
+    static final int TYPE_COMMON = 0;
+    static final int TYPE_3GPP = 1;
+    static final int TYPE_3GPP2 = 2;
+
+    //id of the data profile
+    public final int profileId;
+    //the APN to connect to
+    public final String apn;
+    //one of the PDP_type values in TS 27.007 section 10.1.1.
+    //For example, "IP", "IPV6", "IPV4V6", or "PPP".
+    public final String protocol;
+    //authentication protocol used for this PDP context
+    //(None: 0, PAP: 1, CHAP: 2, PAP&CHAP: 3)
+    public final int authType;
+    //the username for APN, or NULL
+    public final String user;
+    //the password for APN, or NULL
+    public final String password;
+    //the profile type, TYPE_COMMON, TYPE_3GPP, TYPE_3GPP2
+    public final int type;
+    //the period in seconds to limit the maximum connections
+    public final int maxConnsTime;
+    //the maximum connections during maxConnsTime
+    public final int maxConns;
+    //the required wait time in seconds after a successful UE initiated
+    //disconnect of a given PDN connection before the device can send
+    //a new PDN connection request for that given PDN
+    public final int waitTime;
+    //true to enable the profile, false to disable
+    public final boolean enabled;
+    //supported APN types bitmap. See RIL_ApnTypes for the value of each bit.
+    public final int supportedApnTypesBitmap;
+    //one of the PDP_type values in TS 27.007 section 10.1.1 used on roaming network.
+    //For example, "IP", "IPV6", "IPV4V6", or "PPP".
+    public final String roamingProtocol;
+    //The bearer bitmap. See RIL_RadioAccessFamily for the value of each bit.
+    public final int bearerBitmap;
+    //maximum transmission unit (MTU) size in bytes
+    public final int mtu;
+    //the MVNO type: possible values are "imsi", "gid", "spn"
+    public final String mvnoType;
+    //MVNO match data. For example, SPN: A MOBILE, BEN NL, ...
+    //IMSI: 302720x94, 2060188, ...
+    //GID: 4E, 33, ...
+    public final String mvnoMatchData;
+    //indicating the data profile was sent to the modem through setDataProfile earlier.
+    public final boolean modemCognitive;
+
+    DataProfile(int profileId, String apn, String protocol, int authType,
+                String user, String password, int type, int maxConnsTime, int maxConns,
+                int waitTime, boolean enabled, int supportedApnTypesBitmap, String roamingProtocol,
+                int bearerBitmap, int mtu, String mvnoType, String mvnoMatchData,
+                boolean modemCognitive) {
+
+        this.profileId = profileId;
+        this.apn = apn;
+        this.protocol = protocol;
+        if (authType == -1) {
+            authType = TextUtils.isEmpty(user) ? RILConstants.SETUP_DATA_AUTH_NONE
+                    : RILConstants.SETUP_DATA_AUTH_PAP_CHAP;
+        }
+        this.authType = authType;
+        this.user = user;
+        this.password = password;
+        this.type = type;
+        this.maxConnsTime = maxConnsTime;
+        this.maxConns = maxConns;
+        this.waitTime = waitTime;
+        this.enabled = enabled;
+
+        this.supportedApnTypesBitmap = supportedApnTypesBitmap;
+        this.roamingProtocol = roamingProtocol;
+        this.bearerBitmap = bearerBitmap;
+        this.mtu = mtu;
+        this.mvnoType = mvnoType;
+        this.mvnoMatchData = mvnoMatchData;
+        this.modemCognitive = modemCognitive;
+    }
+
+    public DataProfile(ApnSetting apn) {
+        this(apn, apn.profileId);
+    }
+
+    public DataProfile(ApnSetting apn, int profileId) {
+        this(profileId, apn.apn, apn.protocol,
+                apn.authType, apn.user, apn.password, apn.bearerBitmask == 0
+                        ? TYPE_COMMON : (ServiceState.bearerBitmapHasCdma(apn.bearerBitmask)
+                        ? TYPE_3GPP2 : TYPE_3GPP),
+                apn.maxConnsTime, apn.maxConns, apn.waitTime, apn.carrierEnabled, apn.typesBitmap,
+                apn.roamingProtocol, apn.bearerBitmask, apn.mtu, apn.mvnoType, apn.mvnoMatchData,
+                apn.modemCognitive);
+    }
+
+    @Override
+    public String toString() {
+        return "DataProfile=" + profileId + "/" + apn + "/" + protocol + "/" + authType
+                + "/" + user + "/" + password + "/" + type + "/" + maxConnsTime
+                + "/" + maxConns + "/" + waitTime + "/" + enabled + "/" + supportedApnTypesBitmap
+                + "/" + roamingProtocol + "/" + bearerBitmap + "/" + mtu + "/" + mvnoType + "/"
+                + mvnoMatchData + "/" + modemCognitive;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof DataProfile == false) return false;
+        return (o == this || toString().equals(o.toString()));
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcAsyncChannel.java b/com/android/internal/telephony/dataconnection/DcAsyncChannel.java
new file mode 100644
index 0000000..8273dee
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcAsyncChannel.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.dataconnection;
+
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.ProxyInfo;
+import android.os.Message;
+
+import com.android.internal.telephony.dataconnection.DataConnection.ConnectionParams;
+import com.android.internal.telephony.dataconnection.DataConnection.DisconnectParams;
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+
+/**
+ * AsyncChannel to a DataConnection
+ */
+public class DcAsyncChannel extends AsyncChannel {
+    private static final boolean DBG = false;
+    private String mLogTag;
+
+    private DataConnection mDc;
+    private long mDcThreadId;
+
+    public static final int BASE = Protocol.BASE_DATA_CONNECTION_AC;
+
+    public static final int REQ_IS_INACTIVE = BASE + 0;
+    public static final int RSP_IS_INACTIVE = BASE + 1;
+
+    public static final int REQ_GET_CID = BASE + 2;
+    public static final int RSP_GET_CID = BASE + 3;
+
+    public static final int REQ_GET_APNSETTING = BASE + 4;
+    public static final int RSP_GET_APNSETTING = BASE + 5;
+
+    public static final int REQ_GET_LINK_PROPERTIES = BASE + 6;
+    public static final int RSP_GET_LINK_PROPERTIES = BASE + 7;
+
+    public static final int REQ_SET_LINK_PROPERTIES_HTTP_PROXY = BASE + 8;
+    public static final int RSP_SET_LINK_PROPERTIES_HTTP_PROXY = BASE + 9;
+
+    public static final int REQ_GET_NETWORK_CAPABILITIES = BASE + 10;
+    public static final int RSP_GET_NETWORK_CAPABILITIES = BASE + 11;
+
+    public static final int REQ_RESET = BASE + 12;
+    public static final int RSP_RESET = BASE + 13;
+
+    private static final int CMD_TO_STRING_COUNT = RSP_RESET - BASE + 1;
+    private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT];
+    static {
+        sCmdToString[REQ_IS_INACTIVE - BASE] = "REQ_IS_INACTIVE";
+        sCmdToString[RSP_IS_INACTIVE - BASE] = "RSP_IS_INACTIVE";
+        sCmdToString[REQ_GET_CID - BASE] = "REQ_GET_CID";
+        sCmdToString[RSP_GET_CID - BASE] = "RSP_GET_CID";
+        sCmdToString[REQ_GET_APNSETTING - BASE] = "REQ_GET_APNSETTING";
+        sCmdToString[RSP_GET_APNSETTING - BASE] = "RSP_GET_APNSETTING";
+        sCmdToString[REQ_GET_LINK_PROPERTIES - BASE] = "REQ_GET_LINK_PROPERTIES";
+        sCmdToString[RSP_GET_LINK_PROPERTIES - BASE] = "RSP_GET_LINK_PROPERTIES";
+        sCmdToString[REQ_SET_LINK_PROPERTIES_HTTP_PROXY - BASE] =
+                "REQ_SET_LINK_PROPERTIES_HTTP_PROXY";
+        sCmdToString[RSP_SET_LINK_PROPERTIES_HTTP_PROXY - BASE] =
+                "RSP_SET_LINK_PROPERTIES_HTTP_PROXY";
+        sCmdToString[REQ_GET_NETWORK_CAPABILITIES - BASE] = "REQ_GET_NETWORK_CAPABILITIES";
+        sCmdToString[RSP_GET_NETWORK_CAPABILITIES - BASE] = "RSP_GET_NETWORK_CAPABILITIES";
+        sCmdToString[REQ_RESET - BASE] = "REQ_RESET";
+        sCmdToString[RSP_RESET - BASE] = "RSP_RESET";
+    }
+
+    ConnectionParams mLastConnectionParams;
+
+    // Convert cmd to string or null if unknown
+    protected static String cmdToString(int cmd) {
+        cmd -= BASE;
+        if ((cmd >= 0) && (cmd < sCmdToString.length)) {
+            return sCmdToString[cmd];
+        } else {
+            return AsyncChannel.cmdToString(cmd + BASE);
+        }
+    }
+
+    /**
+     * enum used to notify action taken or necessary to be
+     * taken after the link property is changed.
+     */
+    public enum LinkPropertyChangeAction {
+        NONE, CHANGED, RESET;
+
+        public static LinkPropertyChangeAction fromInt(int value) {
+            if (value == NONE.ordinal()) {
+                return NONE;
+            } else if (value == CHANGED.ordinal()) {
+                return CHANGED;
+            } else if (value == RESET.ordinal()) {
+                return RESET;
+            } else {
+                throw new RuntimeException("LinkPropertyChangeAction.fromInt: bad value=" + value);
+            }
+        }
+    }
+
+    public DcAsyncChannel(DataConnection dc, String logTag) {
+        mDc = dc;
+        mDcThreadId = mDc.getHandler().getLooper().getThread().getId();
+        mLogTag = logTag;
+    }
+
+    /**
+     * Request if the state machine is in the inactive state.
+     * Response {@link #rspIsInactive}
+     */
+    public void reqIsInactive() {
+        sendMessage(REQ_IS_INACTIVE);
+        if (DBG) log("reqIsInactive");
+    }
+
+    /**
+     * Evaluate RSP_IS_INACTIVE.
+     *
+     * @return true if the state machine is in the inactive state.
+     */
+    public boolean rspIsInactive(Message response) {
+        boolean retVal = response.arg1 == 1;
+        if (DBG) log("rspIsInactive=" + retVal);
+        return retVal;
+    }
+
+    /**
+     * @return true if the state machine is in the inactive state
+     * and can be used for a new connection.
+     */
+    public boolean isInactiveSync() {
+        boolean value;
+        if (isCallerOnDifferentThread()) {
+            Message response = sendMessageSynchronously(REQ_IS_INACTIVE);
+            if ((response != null) && (response.what == RSP_IS_INACTIVE)) {
+                value = rspIsInactive(response);
+            } else {
+                log("rspIsInactive error response=" + response);
+                value = false;
+            }
+        } else {
+            value = mDc.getIsInactive();
+        }
+        return value;
+    }
+
+    /**
+     * Request the Connection ID.
+     * Response {@link #rspCid}
+     */
+    public void reqCid() {
+        sendMessage(REQ_GET_CID);
+        if (DBG) log("reqCid");
+    }
+
+    /**
+     * Evaluate a RSP_GET_CID message and return the cid.
+     *
+     * @param response Message
+     * @return connection id or -1 if an error
+     */
+    public int rspCid(Message response) {
+        int retVal = response.arg1;
+        if (DBG) log("rspCid=" + retVal);
+        return retVal;
+    }
+
+    /**
+     * @return connection id or -1 if an error
+     */
+    public int getCidSync() {
+        int value;
+        if (isCallerOnDifferentThread()) {
+            Message response = sendMessageSynchronously(REQ_GET_CID);
+            if ((response != null) && (response.what == RSP_GET_CID)) {
+                value = rspCid(response);
+            } else {
+                log("rspCid error response=" + response);
+                value = -1;
+            }
+        } else {
+            value = mDc.getCid();
+        }
+        return value;
+    }
+
+    /**
+     * Request the connections ApnSetting.
+     * Response {@link #rspApnSetting}
+     */
+    public void reqApnSetting() {
+        sendMessage(REQ_GET_APNSETTING);
+        if (DBG) log("reqApnSetting");
+    }
+
+    /**
+     * Evaluate a RSP_APN_SETTING message and return the ApnSetting.
+     *
+     * @param response Message
+     * @return ApnSetting, maybe null
+     */
+    public ApnSetting rspApnSetting(Message response) {
+        ApnSetting retVal = (ApnSetting) response.obj;
+        if (DBG) log("rspApnSetting=" + retVal);
+        return retVal;
+    }
+
+    /**
+     * Get the connections ApnSetting.
+     *
+     * @return ApnSetting or null if an error
+     */
+    public ApnSetting getApnSettingSync() {
+        ApnSetting value;
+        if (isCallerOnDifferentThread()) {
+            Message response = sendMessageSynchronously(REQ_GET_APNSETTING);
+            if ((response != null) && (response.what == RSP_GET_APNSETTING)) {
+                value = rspApnSetting(response);
+            } else {
+                log("getApnSetting error response=" + response);
+                value = null;
+            }
+        } else {
+            value = mDc.getApnSetting();
+        }
+        return value;
+    }
+
+    /**
+     * Request the connections LinkProperties.
+     * Response {@link #rspLinkProperties}
+     */
+    public void reqLinkProperties() {
+        sendMessage(REQ_GET_LINK_PROPERTIES);
+        if (DBG) log("reqLinkProperties");
+    }
+
+    /**
+     * Evaluate RSP_GET_LINK_PROPERTIES
+     *
+     * @param response
+     * @return LinkProperties, maybe null.
+     */
+    public LinkProperties rspLinkProperties(Message response) {
+        LinkProperties retVal = (LinkProperties) response.obj;
+        if (DBG) log("rspLinkProperties=" + retVal);
+        return retVal;
+    }
+
+    /**
+     * Get the connections LinkProperties.
+     *
+     * @return LinkProperties or null if an error
+     */
+    public LinkProperties getLinkPropertiesSync() {
+        LinkProperties value;
+        if (isCallerOnDifferentThread()) {
+            Message response = sendMessageSynchronously(REQ_GET_LINK_PROPERTIES);
+            if ((response != null) && (response.what == RSP_GET_LINK_PROPERTIES)) {
+                value = rspLinkProperties(response);
+            } else {
+                log("getLinkProperties error response=" + response);
+                value = null;
+            }
+        } else {
+            value = mDc.getCopyLinkProperties();
+        }
+        return value;
+    }
+
+    /**
+     * Request setting the connections LinkProperties.HttpProxy.
+     * Response RSP_SET_LINK_PROPERTIES when complete.
+     */
+    public void reqSetLinkPropertiesHttpProxy(ProxyInfo proxy) {
+        sendMessage(REQ_SET_LINK_PROPERTIES_HTTP_PROXY, proxy);
+        if (DBG) log("reqSetLinkPropertiesHttpProxy proxy=" + proxy);
+    }
+
+    /**
+     * Set the connections LinkProperties.HttpProxy
+     */
+    public void setLinkPropertiesHttpProxySync(ProxyInfo proxy) {
+        if (isCallerOnDifferentThread()) {
+            Message response =
+                sendMessageSynchronously(REQ_SET_LINK_PROPERTIES_HTTP_PROXY, proxy);
+            if ((response != null) && (response.what == RSP_SET_LINK_PROPERTIES_HTTP_PROXY)) {
+                if (DBG) log("setLinkPropertiesHttpPoxy ok");
+            } else {
+                log("setLinkPropertiesHttpPoxy error response=" + response);
+            }
+        } else {
+            mDc.setLinkPropertiesHttpProxy(proxy);
+        }
+    }
+
+    /**
+     * Request the connections NetworkCapabilities.
+     * Response {@link #rspNetworkCapabilities}
+     */
+    public void reqNetworkCapabilities() {
+        sendMessage(REQ_GET_NETWORK_CAPABILITIES);
+        if (DBG) log("reqNetworkCapabilities");
+    }
+
+    /**
+     * Evaluate RSP_GET_NETWORK_CAPABILITIES
+     *
+     * @param response
+     * @return NetworkCapabilities, maybe null.
+     */
+    public NetworkCapabilities rspNetworkCapabilities(Message response) {
+        NetworkCapabilities retVal = (NetworkCapabilities) response.obj;
+        if (DBG) log("rspNetworkCapabilities=" + retVal);
+        return retVal;
+    }
+
+    /**
+     * Get the connections NetworkCapabilities.
+     *
+     * @return NetworkCapabilities or null if an error
+     */
+    public NetworkCapabilities getNetworkCapabilitiesSync() {
+        NetworkCapabilities value;
+        if (isCallerOnDifferentThread()) {
+            Message response = sendMessageSynchronously(REQ_GET_NETWORK_CAPABILITIES);
+            if ((response != null) && (response.what == RSP_GET_NETWORK_CAPABILITIES)) {
+                value = rspNetworkCapabilities(response);
+            } else {
+                value = null;
+            }
+        } else {
+            value = mDc.getNetworkCapabilities();
+        }
+        return value;
+    }
+
+    /**
+     * Response RSP_RESET when complete
+     */
+    public void reqReset() {
+        sendMessage(REQ_RESET);
+        if (DBG) log("reqReset");
+    }
+
+    /**
+     * Bring up a connection to the apn and return an AsyncResult in onCompletedMsg.
+     * Used for cellular networks that use Access Point Names (APN) such
+     * as GSM networks.
+     *
+     * @param apnContext is the Access Point Name to bring up a connection to
+     * @param profileId for the connection
+     * @param rilRadioTechnology Radio technology for the data connection
+     * @param unmeteredUseOnly Indicates the data connection can only used for unmetered purposes
+     * @param onCompletedMsg is sent with its msg.obj as an AsyncResult object.
+     *                       With AsyncResult.userObj set to the original msg.obj,
+     *                       AsyncResult.result = FailCause and AsyncResult.exception = Exception().
+     * @param connectionGeneration used to track a single connection request so disconnects can get
+     *                             ignored if obsolete.
+     */
+    public void bringUp(ApnContext apnContext, int profileId, int rilRadioTechnology,
+                        boolean unmeteredUseOnly, Message onCompletedMsg,
+                        int connectionGeneration) {
+        if (DBG) {
+            log("bringUp: apnContext=" + apnContext + "unmeteredUseOnly=" + unmeteredUseOnly
+                    + " onCompletedMsg=" + onCompletedMsg);
+        }
+        mLastConnectionParams = new ConnectionParams(apnContext, profileId, rilRadioTechnology,
+                unmeteredUseOnly, onCompletedMsg, connectionGeneration);
+        sendMessage(DataConnection.EVENT_CONNECT, mLastConnectionParams);
+    }
+
+    /**
+     * Tear down the connection through the apn on the network.
+     *
+     * @param onCompletedMsg is sent with its msg.obj as an AsyncResult object.
+     *        With AsyncResult.userObj set to the original msg.obj.
+     */
+    public void tearDown(ApnContext apnContext, String reason, Message onCompletedMsg) {
+        if (DBG) {
+            log("tearDown: apnContext=" + apnContext
+                    + " reason=" + reason + " onCompletedMsg=" + onCompletedMsg);
+        }
+        sendMessage(DataConnection.EVENT_DISCONNECT,
+                        new DisconnectParams(apnContext, reason, onCompletedMsg));
+    }
+
+    /**
+     * Tear down the connection through the apn on the network.  Ignores refcount and
+     * and always tears down.
+     *
+     * @param onCompletedMsg is sent with its msg.obj as an AsyncResult object.
+     *        With AsyncResult.userObj set to the original msg.obj.
+     */
+    public void tearDownAll(String reason, Message onCompletedMsg) {
+        if (DBG) log("tearDownAll: reason=" + reason + " onCompletedMsg=" + onCompletedMsg);
+        sendMessage(DataConnection.EVENT_DISCONNECT_ALL,
+                new DisconnectParams(null, reason, onCompletedMsg));
+    }
+
+    /**
+     * @return connection id
+     */
+    public int getDataConnectionIdSync() {
+        // Safe because this is owned by the caller.
+        return mDc.getDataConnectionId();
+    }
+
+    @Override
+    public String toString() {
+        return mDc.getName();
+    }
+
+    private boolean isCallerOnDifferentThread() {
+        long curThreadId = Thread.currentThread().getId();
+        boolean value = mDcThreadId != curThreadId;
+        if (DBG) log("isCallerOnDifferentThread: " + value);
+        return value;
+    }
+
+    private void log(String s) {
+        android.telephony.Rlog.d(mLogTag, "DataConnectionAc " + s);
+    }
+
+    public String[] getPcscfAddr() {
+        return mDc.mPcscfAddr;
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcController.java b/com/android/internal/telephony/dataconnection/DcController.java
new file mode 100644
index 0000000..291d6f5
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcController.java
@@ -0,0 +1,447 @@
+/*
+ * 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.internal.telephony.dataconnection;
+
+import android.content.Context;
+import android.net.LinkAddress;
+import android.net.LinkProperties.CompareResult;
+import android.net.NetworkUtils;
+import android.os.AsyncResult;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.PhoneStateListener;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.DctConstants;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.dataconnection.DataConnection.UpdateLinkPropertyResult;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Data Connection Controller which is a package visible class and controls
+ * multiple data connections. For instance listening for unsolicited messages
+ * and then demultiplexing them to the appropriate DC.
+ */
+public class DcController extends StateMachine {
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    private Phone mPhone;
+    private DcTracker mDct;
+    private DcTesterDeactivateAll mDcTesterDeactivateAll;
+
+    // package as its used by Testing code
+    ArrayList<DataConnection> mDcListAll = new ArrayList<DataConnection>();
+    private HashMap<Integer, DataConnection> mDcListActiveByCid =
+            new HashMap<Integer, DataConnection>();
+
+    /**
+     * Constants for the data connection activity:
+     * physical link down/up
+     *
+     * TODO: Move to RILConstants.java
+     */
+    static final int DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE = 0;
+    static final int DATA_CONNECTION_ACTIVE_PH_LINK_DORMANT = 1;
+    static final int DATA_CONNECTION_ACTIVE_PH_LINK_UP = 2;
+    static final int DATA_CONNECTION_ACTIVE_UNKNOWN = Integer.MAX_VALUE;
+
+    private DccDefaultState mDccDefaultState = new DccDefaultState();
+
+    TelephonyManager mTelephonyManager;
+    private PhoneStateListener mPhoneStateListener;
+
+    //mExecutingCarrierChange tracks whether the phone is currently executing
+    //carrier network change
+    private volatile boolean mExecutingCarrierChange;
+
+    /**
+     * Constructor.
+     *
+     * @param name to be used for the Controller
+     * @param phone the phone associated with Dcc and Dct
+     * @param dct the DataConnectionTracker associated with Dcc
+     * @param handler defines the thread/looper to be used with Dcc
+     */
+    private DcController(String name, Phone phone, DcTracker dct,
+            Handler handler) {
+        super(name, handler);
+        setLogRecSize(300);
+        log("E ctor");
+        mPhone = phone;
+        mDct = dct;
+        addState(mDccDefaultState);
+        setInitialState(mDccDefaultState);
+        log("X ctor");
+
+        mPhoneStateListener = new PhoneStateListener(handler.getLooper()) {
+            @Override
+            public void onCarrierNetworkChange(boolean active) {
+                mExecutingCarrierChange = active;
+            }
+        };
+
+        mTelephonyManager = (TelephonyManager) phone.getContext().getSystemService(Context.TELEPHONY_SERVICE);
+        if(mTelephonyManager != null) {
+            mTelephonyManager.listen(mPhoneStateListener,
+                    PhoneStateListener.LISTEN_CARRIER_NETWORK_CHANGE);
+        }
+    }
+
+    public static DcController makeDcc(Phone phone, DcTracker dct, Handler handler) {
+        DcController dcc = new DcController("Dcc", phone, dct, handler);
+        dcc.start();
+        return dcc;
+    }
+
+    void dispose() {
+        log("dispose: call quiteNow()");
+        if(mTelephonyManager != null) mTelephonyManager.listen(mPhoneStateListener, 0);
+        quitNow();
+    }
+
+    void addDc(DataConnection dc) {
+        mDcListAll.add(dc);
+    }
+
+    void removeDc(DataConnection dc) {
+        mDcListActiveByCid.remove(dc.mCid);
+        mDcListAll.remove(dc);
+    }
+
+    public void addActiveDcByCid(DataConnection dc) {
+        if (DBG && dc.mCid < 0) {
+            log("addActiveDcByCid dc.mCid < 0 dc=" + dc);
+        }
+        mDcListActiveByCid.put(dc.mCid, dc);
+    }
+
+    public DataConnection getActiveDcByCid(int cid) {
+        return mDcListActiveByCid.get(cid);
+    }
+
+    void removeActiveDcByCid(DataConnection dc) {
+        DataConnection removedDc = mDcListActiveByCid.remove(dc.mCid);
+        if (DBG && removedDc == null) {
+            log("removeActiveDcByCid removedDc=null dc=" + dc);
+        }
+    }
+
+    boolean isExecutingCarrierChange() {
+        return mExecutingCarrierChange;
+    }
+
+    private class DccDefaultState extends State {
+        @Override
+        public void enter() {
+            mPhone.mCi.registerForRilConnected(getHandler(),
+                    DataConnection.EVENT_RIL_CONNECTED, null);
+            mPhone.mCi.registerForDataCallListChanged(getHandler(),
+                    DataConnection.EVENT_DATA_STATE_CHANGED, null);
+            if (Build.IS_DEBUGGABLE) {
+                mDcTesterDeactivateAll =
+                        new DcTesterDeactivateAll(mPhone, DcController.this, getHandler());
+            }
+        }
+
+        @Override
+        public void exit() {
+            if (mPhone != null) {
+                mPhone.mCi.unregisterForRilConnected(getHandler());
+                mPhone.mCi.unregisterForDataCallListChanged(getHandler());
+            }
+            if (mDcTesterDeactivateAll != null) {
+                mDcTesterDeactivateAll.dispose();
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            AsyncResult ar;
+
+            switch (msg.what) {
+                case DataConnection.EVENT_RIL_CONNECTED:
+                    ar = (AsyncResult)msg.obj;
+                    if (ar.exception == null) {
+                        if (DBG) {
+                            log("DccDefaultState: msg.what=EVENT_RIL_CONNECTED mRilVersion=" +
+                                ar.result);
+                        }
+                    } else {
+                        log("DccDefaultState: Unexpected exception on EVENT_RIL_CONNECTED");
+                    }
+                    break;
+
+                case DataConnection.EVENT_DATA_STATE_CHANGED:
+                    ar = (AsyncResult)msg.obj;
+                    if (ar.exception == null) {
+                        onDataStateChanged((ArrayList<DataCallResponse>)ar.result);
+                    } else {
+                        log("DccDefaultState: EVENT_DATA_STATE_CHANGED:" +
+                                    " exception; likely radio not available, ignore");
+                    }
+                    break;
+            }
+            return HANDLED;
+        }
+
+        /**
+         * Process the new list of "known" Data Calls
+         * @param dcsList as sent by RIL_UNSOL_DATA_CALL_LIST_CHANGED
+         */
+        private void onDataStateChanged(ArrayList<DataCallResponse> dcsList) {
+            if (DBG) {
+                lr("onDataStateChanged: dcsList=" + dcsList
+                        + " mDcListActiveByCid=" + mDcListActiveByCid);
+            }
+            if (VDBG) {
+                log("onDataStateChanged: mDcListAll=" + mDcListAll);
+            }
+
+            // Create hashmap of cid to DataCallResponse
+            HashMap<Integer, DataCallResponse> dataCallResponseListByCid =
+                    new HashMap<Integer, DataCallResponse>();
+            for (DataCallResponse dcs : dcsList) {
+                dataCallResponseListByCid.put(dcs.cid, dcs);
+            }
+
+            // Add a DC that is active but not in the
+            // dcsList to the list of DC's to retry
+            ArrayList<DataConnection> dcsToRetry = new ArrayList<DataConnection>();
+            for (DataConnection dc : mDcListActiveByCid.values()) {
+                if (dataCallResponseListByCid.get(dc.mCid) == null) {
+                    if (DBG) log("onDataStateChanged: add to retry dc=" + dc);
+                    dcsToRetry.add(dc);
+                }
+            }
+            if (DBG) log("onDataStateChanged: dcsToRetry=" + dcsToRetry);
+
+            // Find which connections have changed state and send a notification or cleanup
+            // and any that are in active need to be retried.
+            ArrayList<ApnContext> apnsToCleanup = new ArrayList<ApnContext>();
+
+            boolean isAnyDataCallDormant = false;
+            boolean isAnyDataCallActive = false;
+
+            for (DataCallResponse newState : dcsList) {
+
+                DataConnection dc = mDcListActiveByCid.get(newState.cid);
+                if (dc == null) {
+                    // UNSOL_DATA_CALL_LIST_CHANGED arrived before SETUP_DATA_CALL completed.
+                    loge("onDataStateChanged: no associated DC yet, ignore");
+                    continue;
+                }
+
+                if (dc.mApnContexts.size() == 0) {
+                    if (DBG) loge("onDataStateChanged: no connected apns, ignore");
+                } else {
+                    // Determine if the connection/apnContext should be cleaned up
+                    // or just a notification should be sent out.
+                    if (DBG) log("onDataStateChanged: Found ConnId=" + newState.cid
+                            + " newState=" + newState.toString());
+                    if (newState.active == DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE) {
+                        if (mDct.isCleanupRequired.get()) {
+                            apnsToCleanup.addAll(dc.mApnContexts.keySet());
+                            mDct.isCleanupRequired.set(false);
+                        } else {
+                            DcFailCause failCause = DcFailCause.fromInt(newState.status);
+                            if (failCause.isRestartRadioFail(mPhone.getContext(),
+                                        mPhone.getSubId())) {
+                                if (DBG) {
+                                    log("onDataStateChanged: X restart radio, failCause="
+                                            + failCause);
+                                }
+                                mDct.sendRestartRadio();
+                            } else if (mDct.isPermanentFailure(failCause)) {
+                                if (DBG) {
+                                    log("onDataStateChanged: inactive, add to cleanup list. "
+                                            + "failCause=" + failCause);
+                                }
+                                apnsToCleanup.addAll(dc.mApnContexts.keySet());
+                            } else {
+                                if (DBG) {
+                                    log("onDataStateChanged: inactive, add to retry list. "
+                                            + "failCause=" + failCause);
+                                }
+                                dcsToRetry.add(dc);
+                            }
+                        }
+                    } else {
+                        // Its active so update the DataConnections link properties
+                        UpdateLinkPropertyResult result = dc.updateLinkProperty(newState);
+                        if (result.oldLp.equals(result.newLp)) {
+                            if (DBG) log("onDataStateChanged: no change");
+                        } else {
+                            if (result.oldLp.isIdenticalInterfaceName(result.newLp)) {
+                                if (! result.oldLp.isIdenticalDnses(result.newLp) ||
+                                        ! result.oldLp.isIdenticalRoutes(result.newLp) ||
+                                        ! result.oldLp.isIdenticalHttpProxy(result.newLp) ||
+                                        ! result.oldLp.isIdenticalAddresses(result.newLp)) {
+                                    // If the same address type was removed and
+                                    // added we need to cleanup
+                                    CompareResult<LinkAddress> car =
+                                        result.oldLp.compareAddresses(result.newLp);
+                                    if (DBG) {
+                                        log("onDataStateChanged: oldLp=" + result.oldLp +
+                                                " newLp=" + result.newLp + " car=" + car);
+                                    }
+                                    boolean needToClean = false;
+                                    for (LinkAddress added : car.added) {
+                                        for (LinkAddress removed : car.removed) {
+                                            if (NetworkUtils.addressTypeMatches(
+                                                    removed.getAddress(),
+                                                    added.getAddress())) {
+                                                needToClean = true;
+                                                break;
+                                            }
+                                        }
+                                    }
+                                    if (needToClean) {
+                                        if (DBG) {
+                                            log("onDataStateChanged: addr change," +
+                                                    " cleanup apns=" + dc.mApnContexts +
+                                                    " oldLp=" + result.oldLp +
+                                                    " newLp=" + result.newLp);
+                                        }
+                                        apnsToCleanup.addAll(dc.mApnContexts.keySet());
+                                    } else {
+                                        if (DBG) log("onDataStateChanged: simple change");
+
+                                        for (ApnContext apnContext : dc.mApnContexts.keySet()) {
+                                             mPhone.notifyDataConnection(
+                                                 PhoneConstants.REASON_LINK_PROPERTIES_CHANGED,
+                                                 apnContext.getApnType());
+                                        }
+                                    }
+                                } else {
+                                    if (DBG) {
+                                        log("onDataStateChanged: no changes");
+                                    }
+                                }
+                            } else {
+                                apnsToCleanup.addAll(dc.mApnContexts.keySet());
+                                if (DBG) {
+                                    log("onDataStateChanged: interface change, cleanup apns="
+                                            + dc.mApnContexts);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (newState.active == DATA_CONNECTION_ACTIVE_PH_LINK_UP) {
+                    isAnyDataCallActive = true;
+                }
+                if (newState.active == DATA_CONNECTION_ACTIVE_PH_LINK_DORMANT) {
+                    isAnyDataCallDormant = true;
+                }
+            }
+
+            if (isAnyDataCallDormant && !isAnyDataCallActive) {
+                // There is no way to indicate link activity per APN right now. So
+                // Link Activity will be considered dormant only when all data calls
+                // are dormant.
+                // If a single data call is in dormant state and none of the data
+                // calls are active broadcast overall link state as dormant.
+                if (DBG) {
+                    log("onDataStateChanged: Data Activity updated to DORMANT. stopNetStatePoll");
+                }
+                mDct.sendStopNetStatPoll(DctConstants.Activity.DORMANT);
+            } else {
+                if (DBG) {
+                    log("onDataStateChanged: Data Activity updated to NONE. " +
+                            "isAnyDataCallActive = " + isAnyDataCallActive +
+                            " isAnyDataCallDormant = " + isAnyDataCallDormant);
+                }
+                if (isAnyDataCallActive) {
+                    mDct.sendStartNetStatPoll(DctConstants.Activity.NONE);
+                }
+            }
+
+            if (DBG) {
+                lr("onDataStateChanged: dcsToRetry=" + dcsToRetry
+                        + " apnsToCleanup=" + apnsToCleanup);
+            }
+
+            // Cleanup connections that have changed
+            for (ApnContext apnContext : apnsToCleanup) {
+               mDct.sendCleanUpConnection(true, apnContext);
+            }
+
+            // Retry connections that have disappeared
+            for (DataConnection dc : dcsToRetry) {
+                if (DBG) log("onDataStateChanged: send EVENT_LOST_CONNECTION dc.mTag=" + dc.mTag);
+                dc.sendMessage(DataConnection.EVENT_LOST_CONNECTION, dc.mTag);
+            }
+
+            if (VDBG) log("onDataStateChanged: X");
+        }
+    }
+
+    /**
+     * lr is short name for logAndAddLogRec
+     * @param s
+     */
+    private void lr(String s) {
+        logAndAddLogRec(s);
+    }
+
+    @Override
+    protected void log(String s) {
+        Rlog.d(getName(), s);
+    }
+
+    @Override
+    protected void loge(String s) {
+        Rlog.e(getName(), s);
+    }
+
+    /**
+     * @return the string for msg.what as our info.
+     */
+    @Override
+    protected String getWhatToString(int what) {
+        String info = null;
+        info = DataConnection.cmdToString(what);
+        if (info == null) {
+            info = DcAsyncChannel.cmdToString(what);
+        }
+        return info;
+    }
+
+    @Override
+    public String toString() {
+        return "mDcListAll=" + mDcListAll + " mDcListActiveByCid=" + mDcListActiveByCid;
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        super.dump(fd, pw, args);
+        pw.println(" mPhone=" + mPhone);
+        pw.println(" mDcListAll=" + mDcListAll);
+        pw.println(" mDcListActiveByCid=" + mDcListActiveByCid);
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcFailBringUp.java b/com/android/internal/telephony/dataconnection/DcFailBringUp.java
new file mode 100644
index 0000000..c24d0bf
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcFailBringUp.java
@@ -0,0 +1,80 @@
+/*
+ * 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.internal.telephony.dataconnection;
+
+import android.content.Intent;
+import android.telephony.Rlog;
+
+/**
+ * A package visible class for supporting testing failing bringUp commands. This
+ * saves the parameters from a action_fail_bringup intent. See
+ * {@link DataConnection#doOnConnect} and {@see DcTesterFailBringUpAll} for more info.
+ */
+public class DcFailBringUp {
+    private static final String LOG_TAG = "DcFailBringUp";
+    private static final boolean DBG = true;
+
+    static final String INTENT_BASE = DataConnection.class.getPackage().getName();
+
+    static final String ACTION_FAIL_BRINGUP = "action_fail_bringup";
+
+    // counter with its --ei option name and default value
+    static final String COUNTER = "counter";
+    static final int DEFAULT_COUNTER = 2;
+    int mCounter;
+
+    // failCause with its --ei option name and default value
+    static final String FAIL_CAUSE = "fail_cause";
+    static final DcFailCause DEFAULT_FAIL_CAUSE = DcFailCause.ERROR_UNSPECIFIED;
+    DcFailCause mFailCause;
+
+    // suggestedRetryTime with its --ei option name and default value
+    static final String SUGGESTED_RETRY_TIME = "suggested_retry_time";
+    static final int DEFAULT_SUGGESTED_RETRY_TIME = -1;
+    int mSuggestedRetryTime;
+
+    // Get the Extra Intent parameters
+    void saveParameters(Intent intent, String s) {
+        if (DBG) log(s + ".saveParameters: action=" + intent.getAction());
+        mCounter = intent.getIntExtra(COUNTER, DEFAULT_COUNTER);
+        mFailCause = DcFailCause.fromInt(
+                intent.getIntExtra(FAIL_CAUSE, DEFAULT_FAIL_CAUSE.getErrorCode()));
+        mSuggestedRetryTime =
+                intent.getIntExtra(SUGGESTED_RETRY_TIME, DEFAULT_SUGGESTED_RETRY_TIME);
+        if (DBG) {
+            log(s + ".saveParameters: " + this);
+        }
+    }
+
+    public void saveParameters(int counter, int failCause, int suggestedRetryTime) {
+        mCounter = counter;
+        mFailCause = DcFailCause.fromInt(failCause);
+        mSuggestedRetryTime = suggestedRetryTime;
+    }
+
+    @Override
+    public String toString() {
+        return "{mCounter=" + mCounter +
+                " mFailCause=" + mFailCause +
+                " mSuggestedRetryTime=" + mSuggestedRetryTime + "}";
+
+    }
+
+    private static void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcFailCause.java b/com/android/internal/telephony/dataconnection/DcFailCause.java
new file mode 100644
index 0000000..b3762e5
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcFailCause.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.dataconnection;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Returned as the reason for a connection failure as defined
+ * by RIL_DataCallFailCause in ril.h and some local errors.
+ */
+public enum DcFailCause {
+    NONE(0),
+
+    // This series of errors as specified by the standards
+    // specified in ril.h
+    OPERATOR_BARRED(0x08),                  /* no retry */
+    NAS_SIGNALLING(0x0E),
+    LLC_SNDCP(0x19),
+    INSUFFICIENT_RESOURCES(0x1A),
+    MISSING_UNKNOWN_APN(0x1B),              /* no retry */
+    UNKNOWN_PDP_ADDRESS_TYPE(0x1C),         /* no retry */
+    USER_AUTHENTICATION(0x1D),              /* no retry */
+    ACTIVATION_REJECT_GGSN(0x1E),           /* no retry */
+    ACTIVATION_REJECT_UNSPECIFIED(0x1F),
+    SERVICE_OPTION_NOT_SUPPORTED(0x20),     /* no retry */
+    SERVICE_OPTION_NOT_SUBSCRIBED(0x21),    /* no retry */
+    SERVICE_OPTION_OUT_OF_ORDER(0x22),
+    NSAPI_IN_USE(0x23),                     /* no retry */
+    REGULAR_DEACTIVATION(0x24),             /* possibly restart radio, based on config */
+    QOS_NOT_ACCEPTED(0x25),
+    NETWORK_FAILURE(0x26),
+    UMTS_REACTIVATION_REQ(0x27),
+    FEATURE_NOT_SUPP(0x28),
+    TFT_SEMANTIC_ERROR(0x29),
+    TFT_SYTAX_ERROR(0x2A),
+    UNKNOWN_PDP_CONTEXT(0x2B),
+    FILTER_SEMANTIC_ERROR(0x2C),
+    FILTER_SYTAX_ERROR(0x2D),
+    PDP_WITHOUT_ACTIVE_TFT(0x2E),
+    ONLY_IPV4_ALLOWED(0x32),                /* no retry */
+    ONLY_IPV6_ALLOWED(0x33),                /* no retry */
+    ONLY_SINGLE_BEARER_ALLOWED(0x34),
+    ESM_INFO_NOT_RECEIVED(0x35),
+    PDN_CONN_DOES_NOT_EXIST(0x36),
+    MULTI_CONN_TO_SAME_PDN_NOT_ALLOWED(0x37),
+    MAX_ACTIVE_PDP_CONTEXT_REACHED(0x41),
+    UNSUPPORTED_APN_IN_CURRENT_PLMN(0x42),
+    INVALID_TRANSACTION_ID(0x51),
+    MESSAGE_INCORRECT_SEMANTIC(0x5F),
+    INVALID_MANDATORY_INFO(0x60),
+    MESSAGE_TYPE_UNSUPPORTED(0x61),
+    MSG_TYPE_NONCOMPATIBLE_STATE(0x62),
+    UNKNOWN_INFO_ELEMENT(0x63),
+    CONDITIONAL_IE_ERROR(0x64),
+    MSG_AND_PROTOCOL_STATE_UNCOMPATIBLE(0x65),
+    PROTOCOL_ERRORS(0x6F),                  /* no retry */
+    APN_TYPE_CONFLICT(0x70),
+    INVALID_PCSCF_ADDR(0x71),
+    INTERNAL_CALL_PREEMPT_BY_HIGH_PRIO_APN(0x72),
+    EMM_ACCESS_BARRED(0x73),
+    EMERGENCY_IFACE_ONLY(0x74),
+    IFACE_MISMATCH(0x75),
+    COMPANION_IFACE_IN_USE(0x76),
+    IP_ADDRESS_MISMATCH(0x77),
+    IFACE_AND_POL_FAMILY_MISMATCH(0x78),
+    EMM_ACCESS_BARRED_INFINITE_RETRY(0x79),
+    AUTH_FAILURE_ON_EMERGENCY_CALL(0x7A),
+
+    // OEM sepecific error codes. To be used by OEMs when they don't
+    // want to reveal error code which would be replaced by ERROR_UNSPECIFIED
+    OEM_DCFAILCAUSE_1(0x1001),
+    OEM_DCFAILCAUSE_2(0x1002),
+    OEM_DCFAILCAUSE_3(0x1003),
+    OEM_DCFAILCAUSE_4(0x1004),
+    OEM_DCFAILCAUSE_5(0x1005),
+    OEM_DCFAILCAUSE_6(0x1006),
+    OEM_DCFAILCAUSE_7(0x1007),
+    OEM_DCFAILCAUSE_8(0x1008),
+    OEM_DCFAILCAUSE_9(0x1009),
+    OEM_DCFAILCAUSE_10(0x100A),
+    OEM_DCFAILCAUSE_11(0x100B),
+    OEM_DCFAILCAUSE_12(0x100C),
+    OEM_DCFAILCAUSE_13(0x100D),
+    OEM_DCFAILCAUSE_14(0x100E),
+    OEM_DCFAILCAUSE_15(0x100F),
+
+    // Local errors generated by Vendor RIL
+    // specified in ril.h
+    REGISTRATION_FAIL(-1),
+    GPRS_REGISTRATION_FAIL(-2),
+    SIGNAL_LOST(-3),                        /* no retry */
+    PREF_RADIO_TECH_CHANGED(-4),
+    RADIO_POWER_OFF(-5),                    /* no retry */
+    TETHERED_CALL_ACTIVE(-6),               /* no retry */
+    ERROR_UNSPECIFIED(0xFFFF),
+
+    // Errors generated by the Framework
+    // specified here
+    UNKNOWN(0x10000),
+    RADIO_NOT_AVAILABLE(0x10001),                   /* no retry */
+    UNACCEPTABLE_NETWORK_PARAMETER(0x10002),        /* no retry */
+    CONNECTION_TO_DATACONNECTIONAC_BROKEN(0x10003),
+    LOST_CONNECTION(0x10004),
+    RESET_BY_FRAMEWORK(0x10005);
+
+    private final int mErrorCode;
+    private static final HashMap<Integer, DcFailCause> sErrorCodeToFailCauseMap;
+    static {
+        sErrorCodeToFailCauseMap = new HashMap<Integer, DcFailCause>();
+        for (DcFailCause fc : values()) {
+            sErrorCodeToFailCauseMap.put(fc.getErrorCode(), fc);
+        }
+    }
+
+    /**
+     * Map of subId -> set of data call setup permanent failure for the carrier.
+     */
+    private static final HashMap<Integer, HashSet<DcFailCause>> sPermanentFailureCache =
+            new HashMap<>();
+
+    DcFailCause(int errorCode) {
+        mErrorCode = errorCode;
+    }
+
+    public int getErrorCode() {
+        return mErrorCode;
+    }
+
+    /**
+     * Returns whether or not the radio has failed and also needs to be restarted.
+     * By default, we do not restart radio on REGULAR_DEACTIVATION.
+     *
+     * @param context device context
+     * @param subId subscription id
+     * @return true if the radio has failed and the carrier requres restart, otherwise false
+     */
+    public boolean isRestartRadioFail(Context context, int subId) {
+        if (this == REGULAR_DEACTIVATION) {
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configManager != null) {
+                PersistableBundle b = configManager.getConfigForSubId(subId);
+                if (b != null) {
+                    return b.getBoolean(CarrierConfigManager.
+                            KEY_RESTART_RADIO_ON_PDP_FAIL_REGULAR_DEACTIVATION_BOOL);
+                }
+            }
+        }
+        return false;
+    }
+
+    public boolean isPermanentFailure(Context context, int subId) {
+
+        synchronized (sPermanentFailureCache) {
+
+            HashSet<DcFailCause> permanentFailureSet = sPermanentFailureCache.get(subId);
+
+            // In case of cache miss, we need to look up the settings from carrier config.
+            if (permanentFailureSet == null) {
+                // Retrieve the permanent failure from carrier config
+                CarrierConfigManager configManager = (CarrierConfigManager)
+                        context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+                if (configManager != null) {
+                    PersistableBundle b = configManager.getConfigForSubId(subId);
+                    if (b != null) {
+                        String[] permanentFailureStrings = b.getStringArray(CarrierConfigManager.
+                                KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS);
+
+                        if (permanentFailureStrings != null) {
+                            permanentFailureSet = new HashSet<>();
+                            for (String failure : permanentFailureStrings) {
+                                permanentFailureSet.add(DcFailCause.valueOf(failure));
+                            }
+                        }
+                    }
+                }
+
+                // If we are not able to find the configuration from carrier config, use the default
+                // ones.
+                if (permanentFailureSet == null) {
+                    permanentFailureSet = new HashSet<DcFailCause>() {
+                        {
+                            add(OPERATOR_BARRED);
+                            add(MISSING_UNKNOWN_APN);
+                            add(UNKNOWN_PDP_ADDRESS_TYPE);
+                            add(USER_AUTHENTICATION);
+                            add(ACTIVATION_REJECT_GGSN);
+                            add(SERVICE_OPTION_NOT_SUPPORTED);
+                            add(SERVICE_OPTION_NOT_SUBSCRIBED);
+                            add(NSAPI_IN_USE);
+                            add(ONLY_IPV4_ALLOWED);
+                            add(ONLY_IPV6_ALLOWED);
+                            add(PROTOCOL_ERRORS);
+                            add(RADIO_POWER_OFF);
+                            add(TETHERED_CALL_ACTIVE);
+                            add(RADIO_NOT_AVAILABLE);
+                            add(UNACCEPTABLE_NETWORK_PARAMETER);
+                            add(SIGNAL_LOST);
+                        }
+                    };
+                }
+
+                sPermanentFailureCache.put(subId, permanentFailureSet);
+            }
+
+            return permanentFailureSet.contains(this);
+        }
+    }
+
+    public boolean isEventLoggable() {
+        return (this == OPERATOR_BARRED) || (this == INSUFFICIENT_RESOURCES) ||
+                (this == UNKNOWN_PDP_ADDRESS_TYPE) || (this == USER_AUTHENTICATION) ||
+                (this == ACTIVATION_REJECT_GGSN) || (this == ACTIVATION_REJECT_UNSPECIFIED) ||
+                (this == SERVICE_OPTION_NOT_SUBSCRIBED) ||
+                (this == SERVICE_OPTION_NOT_SUPPORTED) ||
+                (this == SERVICE_OPTION_OUT_OF_ORDER) || (this == NSAPI_IN_USE) ||
+                (this == ONLY_IPV4_ALLOWED) || (this == ONLY_IPV6_ALLOWED) ||
+                (this == PROTOCOL_ERRORS) || (this == SIGNAL_LOST) ||
+                (this == RADIO_POWER_OFF) || (this == TETHERED_CALL_ACTIVE) ||
+                (this == UNACCEPTABLE_NETWORK_PARAMETER);
+    }
+
+    public static DcFailCause fromInt(int errorCode) {
+        DcFailCause fc = sErrorCodeToFailCauseMap.get(errorCode);
+        if (fc == null) {
+            fc = UNKNOWN;
+        }
+        return fc;
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcRequest.java b/com/android/internal/telephony/dataconnection/DcRequest.java
new file mode 100644
index 0000000..79cfcec
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcRequest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.dataconnection;
+
+import static com.android.internal.telephony.DctConstants.APN_CBS_ID;
+import static com.android.internal.telephony.DctConstants.APN_DEFAULT_ID;
+import static com.android.internal.telephony.DctConstants.APN_DUN_ID;
+import static com.android.internal.telephony.DctConstants.APN_EMERGENCY_ID;
+import static com.android.internal.telephony.DctConstants.APN_FOTA_ID;
+import static com.android.internal.telephony.DctConstants.APN_IA_ID;
+import static com.android.internal.telephony.DctConstants.APN_IMS_ID;
+import static com.android.internal.telephony.DctConstants.APN_INVALID_ID;
+import static com.android.internal.telephony.DctConstants.APN_MMS_ID;
+import static com.android.internal.telephony.DctConstants.APN_SUPL_ID;
+
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkConfig;
+import android.net.NetworkRequest;
+import android.telephony.Rlog;
+
+import java.util.HashMap;
+
+public class DcRequest implements Comparable<DcRequest> {
+    private static final String LOG_TAG = "DcRequest";
+
+    public final NetworkRequest networkRequest;
+    public final int priority;
+    public final int apnId;
+
+    public DcRequest(NetworkRequest nr, Context context) {
+        initApnPriorities(context);
+        networkRequest = nr;
+        apnId = apnIdForNetworkRequest(networkRequest);
+        priority = priorityForApnId(apnId);
+    }
+
+    public String toString() {
+        return networkRequest.toString() + ", priority=" + priority + ", apnId=" + apnId;
+    }
+
+    public int hashCode() {
+        return networkRequest.hashCode();
+    }
+
+    public boolean equals(Object o) {
+        if (o instanceof DcRequest) {
+            return networkRequest.equals(((DcRequest)o).networkRequest);
+        }
+        return false;
+    }
+
+    public int compareTo(DcRequest o) {
+        return o.priority - priority;
+    }
+
+    private int apnIdForNetworkRequest(NetworkRequest nr) {
+        NetworkCapabilities nc = nr.networkCapabilities;
+        // For now, ignore the bandwidth stuff
+        if (nc.getTransportTypes().length > 0 &&
+                nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == false) {
+            return APN_INVALID_ID;
+        }
+
+        // in the near term just do 1-1 matches.
+        // TODO - actually try to match the set of capabilities
+        int apnId = APN_INVALID_ID;
+
+        boolean error = false;
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_DEFAULT_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_MMS_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_SUPL_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_DUN_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_FOTA_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_IMS_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_CBS_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_IA)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_IA_ID;
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_RCS)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_INVALID_ID;
+            loge("RCS APN type not yet supported");
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_XCAP)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_INVALID_ID;
+            loge("XCAP APN type not yet supported");
+        }
+        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_EIMS)) {
+            if (apnId != APN_INVALID_ID) error = true;
+            apnId = APN_EMERGENCY_ID;
+        }
+        if (error) {
+            // TODO: If this error condition is removed, the framework's handling of
+            // NET_CAPABILITY_NOT_RESTRICTED will need to be updated so requests for
+            // say FOTA and INTERNET are marked as restricted.  This is not how
+            // NetworkCapabilities.maybeMarkCapabilitiesRestricted currently works.
+            loge("Multiple apn types specified in request - result is unspecified!");
+        }
+        if (apnId == APN_INVALID_ID) {
+            loge("Unsupported NetworkRequest in Telephony: nr=" + nr);
+        }
+        return apnId;
+    }
+
+    private static final HashMap<Integer, Integer> sApnPriorityMap =
+            new HashMap<Integer, Integer>();
+
+    private void initApnPriorities(Context context) {
+        synchronized (sApnPriorityMap) {
+            if (sApnPriorityMap.isEmpty()) {
+                String[] networkConfigStrings = context.getResources().getStringArray(
+                        com.android.internal.R.array.networkAttributes);
+                for (String networkConfigString : networkConfigStrings) {
+                    NetworkConfig networkConfig = new NetworkConfig(networkConfigString);
+                    final int apnId = ApnContext.apnIdForType(networkConfig.type);
+                    sApnPriorityMap.put(apnId, networkConfig.priority);
+                }
+            }
+        }
+    }
+
+    private int priorityForApnId(int apnId) {
+        Integer priority = sApnPriorityMap.get(apnId);
+        return (priority != null ? priority.intValue() : 0);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcTesterDeactivateAll.java b/com/android/internal/telephony/dataconnection/DcTesterDeactivateAll.java
new file mode 100644
index 0000000..cda4836
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcTesterDeactivateAll.java
@@ -0,0 +1,94 @@
+/*
+ * 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.internal.telephony.dataconnection;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Handler;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.Phone;
+
+/**
+ * To bring down all DC's send the following intent:
+ *
+ * adb shell am broadcast -a com.android.internal.telephony.dataconnection.action_deactivate_all
+ */
+public class DcTesterDeactivateAll {
+    private static final String LOG_TAG = "DcTesterDeacativateAll";
+    private static final boolean DBG = true;
+
+    private Phone mPhone;
+    private DcController mDcc;
+
+    public static String sActionDcTesterDeactivateAll =
+            "com.android.internal.telephony.dataconnection.action_deactivate_all";
+
+
+    // The static intent receiver one for all instances and we assume this
+    // is running on the same thread as Dcc.
+    protected BroadcastReceiver sIntentReceiver = new BroadcastReceiver() {
+            @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (DBG) log("sIntentReceiver.onReceive: action=" + action);
+            if (action.equals(sActionDcTesterDeactivateAll)
+                    || action.equals(mPhone.getActionDetached())) {
+                log("Send DEACTIVATE to all Dcc's");
+                if (mDcc != null) {
+                    for (DataConnection dc : mDcc.mDcListAll) {
+                        dc.tearDownNow();
+                    }
+                } else {
+                    if (DBG) log("onReceive: mDcc is null, ignoring");
+                }
+            } else {
+                if (DBG) log("onReceive: unknown action=" + action);
+            }
+        }
+    };
+
+    DcTesterDeactivateAll(Phone phone, DcController dcc, Handler handler) {
+        mPhone = phone;
+        mDcc = dcc;
+
+        if (Build.IS_DEBUGGABLE) {
+            IntentFilter filter = new IntentFilter();
+
+            filter.addAction(sActionDcTesterDeactivateAll);
+            log("register for intent action=" + sActionDcTesterDeactivateAll);
+
+            filter.addAction(mPhone.getActionDetached());
+            log("register for intent action=" + mPhone.getActionDetached());
+
+            phone.getContext().registerReceiver(sIntentReceiver, filter, null, handler);
+        }
+    }
+
+    void dispose() {
+        if (Build.IS_DEBUGGABLE) {
+            mPhone.getContext().unregisterReceiver(sIntentReceiver);
+        }
+    }
+
+    private static void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcTesterFailBringUpAll.java b/com/android/internal/telephony/dataconnection/DcTesterFailBringUpAll.java
new file mode 100644
index 0000000..dede71e
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcTesterFailBringUpAll.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal.telephony.dataconnection;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Handler;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.Phone;
+
+/**
+ * A package level call that causes all DataConnection bringUp calls to fail a specific
+ * number of times. Here is an example that sets counter to 2 and cause to -3 for all instances:
+ *    adb shell am broadcast -a com.android.internal.telephony.dataconnection.action_fail_bringup \
+ *     --ei counter 2 --ei fail_cause -3
+ *
+ * Also you can add a suggested retry time if desired:
+ *     --ei suggested_retry_time 5000
+ *
+ * The fail_cause is one of {@link DcFailCause}
+ */
+public class DcTesterFailBringUpAll {
+    private static final String LOG_TAG = "DcTesterFailBrinupAll";
+    private static final boolean DBG = true;
+
+    private Phone mPhone;
+
+    private String mActionFailBringUp = DcFailBringUp.INTENT_BASE + "."
+            + DcFailBringUp.ACTION_FAIL_BRINGUP;
+
+    // The saved FailBringUp data from the intent
+    private DcFailBringUp mFailBringUp = new DcFailBringUp();
+
+    // The static intent receiver one for all instances.
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+            @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (DBG) log("sIntentReceiver.onReceive: action=" + action);
+            if (action.equals(mActionFailBringUp)) {
+                mFailBringUp.saveParameters(intent, "sFailBringUp");
+            } else if (action.equals(mPhone.getActionDetached())) {
+                // Counter is MAX, bringUp/retry will always fail
+                log("simulate detaching");
+                mFailBringUp.saveParameters(Integer.MAX_VALUE,
+                        DcFailCause.LOST_CONNECTION.getErrorCode(),
+                        DcFailBringUp.DEFAULT_SUGGESTED_RETRY_TIME);
+            } else if (action.equals(mPhone.getActionAttached())) {
+                // Counter is 0 next bringUp/retry will succeed
+                log("simulate attaching");
+                mFailBringUp.saveParameters(0, DcFailCause.NONE.getErrorCode(),
+                        DcFailBringUp.DEFAULT_SUGGESTED_RETRY_TIME);
+            } else {
+                if (DBG) log("onReceive: unknown action=" + action);
+            }
+        }
+    };
+
+    DcTesterFailBringUpAll(Phone phone, Handler handler) {
+        mPhone = phone;
+        if (Build.IS_DEBUGGABLE) {
+            IntentFilter filter = new IntentFilter();
+
+            filter.addAction(mActionFailBringUp);
+            log("register for intent action=" + mActionFailBringUp);
+
+            filter.addAction(mPhone.getActionDetached());
+            log("register for intent action=" + mPhone.getActionDetached());
+
+            filter.addAction(mPhone.getActionAttached());
+            log("register for intent action=" + mPhone.getActionAttached());
+
+            phone.getContext().registerReceiver(mIntentReceiver, filter, null, handler);
+        }
+    }
+
+    void dispose() {
+        if (Build.IS_DEBUGGABLE) {
+            mPhone.getContext().unregisterReceiver(mIntentReceiver);
+        }
+    }
+
+    public DcFailBringUp getDcFailBringUp() {
+        return mFailBringUp;
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+}
diff --git a/com/android/internal/telephony/dataconnection/DcTracker.java b/com/android/internal/telephony/dataconnection/DcTracker.java
new file mode 100644
index 0000000..f9b0017
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -0,0 +1,4824 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.dataconnection;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.ProgressDialog;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.NetworkConfig;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.net.NetworkUtils;
+import android.net.ProxyInfo;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.AsyncResult;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.RegistrantList;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.provider.Telephony;
+import android.telephony.CarrierConfigManager;
+import android.telephony.CellLocation;
+import android.telephony.PcoData;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaCellLocation;
+import android.telephony.gsm.GsmCellLocation;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.LocalLog;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.CarrierActionAgent;
+import com.android.internal.telephony.DctConstants;
+import com.android.internal.telephony.EventLogTags;
+import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.SettingsObserver;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.dataconnection.DataConnectionReasons.DataAllowedReasonType;
+import com.android.internal.telephony.dataconnection.DataConnectionReasons.DataDisallowedReasonType;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.AsyncChannel;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.PriorityQueue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+/**
+ * {@hide}
+ */
+public class DcTracker extends Handler {
+    private static final String LOG_TAG = "DCT";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+    private static final boolean VDBG_STALL = false; // STOPSHIP if true
+    private static final boolean RADIO_TESTS = false;
+
+    public AtomicBoolean isCleanupRequired = new AtomicBoolean(false);
+
+    private final AlarmManager mAlarmManager;
+
+    /* Currently requested APN type (TODO: This should probably be a parameter not a member) */
+    private String mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT;
+
+    // All data enabling/disabling related settings
+    private final DataEnabledSettings mDataEnabledSettings;
+
+
+    /**
+     * After detecting a potential connection problem, this is the max number
+     * of subsequent polls before attempting recovery.
+     */
+    // 1 sec. default polling interval when screen is on.
+    private static final int POLL_NETSTAT_MILLIS = 1000;
+    // 10 min. default polling interval when screen is off.
+    private static final int POLL_NETSTAT_SCREEN_OFF_MILLIS = 1000*60*10;
+    // Default sent packets without ack which triggers initial recovery steps
+    private static final int NUMBER_SENT_PACKETS_OF_HANG = 10;
+
+    // Default for the data stall alarm while non-aggressive stall detection
+    private static final int DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS_DEFAULT = 1000 * 60 * 6;
+    // Default for the data stall alarm for aggressive stall detection
+    private static final int DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS_DEFAULT = 1000 * 60;
+    // Tag for tracking stale alarms
+    private static final String DATA_STALL_ALARM_TAG_EXTRA = "data.stall.alram.tag";
+
+    private static final boolean DATA_STALL_SUSPECTED = true;
+    private static final boolean DATA_STALL_NOT_SUSPECTED = false;
+
+    private String RADIO_RESET_PROPERTY = "gsm.radioreset";
+
+    private static final String INTENT_RECONNECT_ALARM =
+            "com.android.internal.telephony.data-reconnect";
+    private static final String INTENT_RECONNECT_ALARM_EXTRA_TYPE = "reconnect_alarm_extra_type";
+    private static final String INTENT_RECONNECT_ALARM_EXTRA_REASON =
+            "reconnect_alarm_extra_reason";
+
+    private static final String INTENT_DATA_STALL_ALARM =
+            "com.android.internal.telephony.data-stall";
+
+    private DcTesterFailBringUpAll mDcTesterFailBringUpAll;
+    private DcController mDcc;
+
+    /** kept in sync with mApnContexts
+     * Higher numbers are higher priority and sorted so highest priority is first */
+    private final PriorityQueue<ApnContext>mPrioritySortedApnContexts =
+            new PriorityQueue<ApnContext>(5,
+            new Comparator<ApnContext>() {
+                public int compare(ApnContext c1, ApnContext c2) {
+                    return c2.priority - c1.priority;
+                }
+            } );
+
+    /** allApns holds all apns */
+    private ArrayList<ApnSetting> mAllApnSettings = null;
+
+    /** preferred apn */
+    private ApnSetting mPreferredApn = null;
+
+    /** Is packet service restricted by network */
+    private boolean mIsPsRestricted = false;
+
+    /** emergency apn Setting*/
+    private ApnSetting mEmergencyApn = null;
+
+    /* Once disposed dont handle any messages */
+    private boolean mIsDisposed = false;
+
+    private ContentResolver mResolver;
+
+    /* Set to true with CMD_ENABLE_MOBILE_PROVISIONING */
+    private boolean mIsProvisioning = false;
+
+    /* The Url passed as object parameter in CMD_ENABLE_MOBILE_PROVISIONING */
+    private String mProvisioningUrl = null;
+
+    /* Intent for the provisioning apn alarm */
+    private static final String INTENT_PROVISIONING_APN_ALARM =
+            "com.android.internal.telephony.provisioning_apn_alarm";
+
+    /* Tag for tracking stale alarms */
+    private static final String PROVISIONING_APN_ALARM_TAG_EXTRA = "provisioning.apn.alarm.tag";
+
+    /* Debug property for overriding the PROVISIONING_APN_ALARM_DELAY_IN_MS */
+    private static final String DEBUG_PROV_APN_ALARM = "persist.debug.prov_apn_alarm";
+
+    /* Default for the provisioning apn alarm timeout */
+    private static final int PROVISIONING_APN_ALARM_DELAY_IN_MS_DEFAULT = 1000 * 60 * 15;
+
+    /* The provision apn alarm intent used to disable the provisioning apn */
+    private PendingIntent mProvisioningApnAlarmIntent = null;
+
+    /* Used to track stale provisioning apn alarms */
+    private int mProvisioningApnAlarmTag = (int) SystemClock.elapsedRealtime();
+
+    private AsyncChannel mReplyAc = new AsyncChannel();
+
+    private final LocalLog mDataRoamingLeakageLog = new LocalLog(50);
+
+    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver () {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (action.equals(Intent.ACTION_SCREEN_ON)) {
+                // TODO: Evaluate hooking this up with DeviceStateMonitor
+                if (DBG) log("screen on");
+                mIsScreenOn = true;
+                stopNetStatPoll();
+                startNetStatPoll();
+                restartDataStallAlarm();
+            } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
+                if (DBG) log("screen off");
+                mIsScreenOn = false;
+                stopNetStatPoll();
+                startNetStatPoll();
+                restartDataStallAlarm();
+            } else if (action.startsWith(INTENT_RECONNECT_ALARM)) {
+                if (DBG) log("Reconnect alarm. Previous state was " + mState);
+                onActionIntentReconnectAlarm(intent);
+            } else if (action.equals(INTENT_DATA_STALL_ALARM)) {
+                if (DBG) log("Data stall alarm");
+                onActionIntentDataStallAlarm(intent);
+            } else if (action.equals(INTENT_PROVISIONING_APN_ALARM)) {
+                if (DBG) log("Provisioning apn alarm");
+                onActionIntentProvisioningApnAlarm(intent);
+            } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+                final android.net.NetworkInfo networkInfo = (NetworkInfo)
+                intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+                mIsWifiConnected = (networkInfo != null && networkInfo.isConnected());
+                if (DBG) log("NETWORK_STATE_CHANGED_ACTION: mIsWifiConnected=" + mIsWifiConnected);
+            } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
+                if (DBG) log("Wifi state changed");
+                final boolean enabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
+                        WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED;
+                if (!enabled) {
+                    // when WiFi got disabled, the NETWORK_STATE_CHANGED_ACTION
+                    // quit and won't report disconnected until next enabling.
+                    mIsWifiConnected = false;
+                }
+                if (DBG) {
+                    log("WIFI_STATE_CHANGED_ACTION: enabled=" + enabled
+                            + " mIsWifiConnected=" + mIsWifiConnected);
+                }
+            } else if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                if (mIccRecords.get() != null && mIccRecords.get().getRecordsLoaded()) {
+                    setDefaultDataRoamingEnabled();
+                }
+            } else {
+                if (DBG) log("onReceive: Unknown action=" + action);
+            }
+        }
+    };
+
+    private final Runnable mPollNetStat = new Runnable() {
+        @Override
+        public void run() {
+            updateDataActivity();
+
+            if (mIsScreenOn) {
+                mNetStatPollPeriod = Settings.Global.getInt(mResolver,
+                        Settings.Global.PDP_WATCHDOG_POLL_INTERVAL_MS, POLL_NETSTAT_MILLIS);
+            } else {
+                mNetStatPollPeriod = Settings.Global.getInt(mResolver,
+                        Settings.Global.PDP_WATCHDOG_LONG_POLL_INTERVAL_MS,
+                        POLL_NETSTAT_SCREEN_OFF_MILLIS);
+            }
+
+            if (mNetStatPollEnabled) {
+                mDataConnectionTracker.postDelayed(this, mNetStatPollPeriod);
+            }
+        }
+    };
+
+    private SubscriptionManager mSubscriptionManager;
+    private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener =
+            new OnSubscriptionsChangedListener() {
+                public final AtomicInteger mPreviousSubId =
+                        new AtomicInteger(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+                /**
+                 * Callback invoked when there is any change to any SubscriptionInfo. Typically
+                 * this method invokes {@link SubscriptionManager#getActiveSubscriptionInfoList}
+                 */
+                @Override
+                public void onSubscriptionsChanged() {
+                    if (DBG) log("SubscriptionListener.onSubscriptionInfoChanged");
+                    // Set the network type, in case the radio does not restore it.
+                    int subId = mPhone.getSubId();
+                    if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                        registerSettingsObserver();
+                    }
+                    if (mPreviousSubId.getAndSet(subId) != subId &&
+                            SubscriptionManager.isValidSubscriptionId(subId)) {
+                        onRecordsLoadedOrSubIdChanged();
+                    }
+                }
+            };
+
+    private final SettingsObserver mSettingsObserver;
+
+    private void registerSettingsObserver() {
+        mSettingsObserver.unobserve();
+        String simSuffix = "";
+        if (TelephonyManager.getDefault().getSimCount() > 1) {
+            simSuffix = Integer.toString(mPhone.getSubId());
+        }
+
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(Settings.Global.DATA_ROAMING + simSuffix),
+                DctConstants.EVENT_ROAMING_SETTING_CHANGE);
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+                DctConstants.EVENT_DEVICE_PROVISIONED_CHANGE);
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONING_MOBILE_DATA_ENABLED),
+                DctConstants.EVENT_DEVICE_PROVISIONED_CHANGE);
+    }
+
+    /**
+     * Maintain the sum of transmit and receive packets.
+     *
+     * The packet counts are initialized and reset to -1 and
+     * remain -1 until they can be updated.
+     */
+    public static class TxRxSum {
+        public long txPkts;
+        public long rxPkts;
+
+        public TxRxSum() {
+            reset();
+        }
+
+        public TxRxSum(long txPkts, long rxPkts) {
+            this.txPkts = txPkts;
+            this.rxPkts = rxPkts;
+        }
+
+        public TxRxSum(TxRxSum sum) {
+            txPkts = sum.txPkts;
+            rxPkts = sum.rxPkts;
+        }
+
+        public void reset() {
+            txPkts = -1;
+            rxPkts = -1;
+        }
+
+        @Override
+        public String toString() {
+            return "{txSum=" + txPkts + " rxSum=" + rxPkts + "}";
+        }
+
+        public void updateTxRxSum() {
+            this.txPkts = TrafficStats.getMobileTcpTxPackets();
+            this.rxPkts = TrafficStats.getMobileTcpRxPackets();
+        }
+    }
+
+    private void onActionIntentReconnectAlarm(Intent intent) {
+        Message msg = obtainMessage(DctConstants.EVENT_DATA_RECONNECT);
+        msg.setData(intent.getExtras());
+        sendMessage(msg);
+    }
+
+    private void onDataReconnect(Bundle bundle) {
+        String reason = bundle.getString(INTENT_RECONNECT_ALARM_EXTRA_REASON);
+        String apnType = bundle.getString(INTENT_RECONNECT_ALARM_EXTRA_TYPE);
+
+        int phoneSubId = mPhone.getSubId();
+        int currSubId = bundle.getInt(PhoneConstants.SUBSCRIPTION_KEY,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        log("onDataReconnect: currSubId = " + currSubId + " phoneSubId=" + phoneSubId);
+
+        // Stop reconnect if not current subId is not correct.
+        // FIXME STOPSHIP - phoneSubId is coming up as -1 way after boot and failing this?
+        if (!SubscriptionManager.isValidSubscriptionId(currSubId) || (currSubId != phoneSubId)) {
+            log("receive ReconnectAlarm but subId incorrect, ignore");
+            return;
+        }
+
+        ApnContext apnContext = mApnContexts.get(apnType);
+
+        if (DBG) {
+            log("onDataReconnect: mState=" + mState + " reason=" + reason + " apnType=" + apnType
+                    + " apnContext=" + apnContext + " mDataConnectionAsyncChannels="
+                    + mDataConnectionAcHashMap);
+        }
+
+        if ((apnContext != null) && (apnContext.isEnabled())) {
+            apnContext.setReason(reason);
+            DctConstants.State apnContextState = apnContext.getState();
+            if (DBG) {
+                log("onDataReconnect: apnContext state=" + apnContextState);
+            }
+            if ((apnContextState == DctConstants.State.FAILED)
+                    || (apnContextState == DctConstants.State.IDLE)) {
+                if (DBG) {
+                    log("onDataReconnect: state is FAILED|IDLE, disassociate");
+                }
+                DcAsyncChannel dcac = apnContext.getDcAc();
+                if (dcac != null) {
+                    if (DBG) {
+                        log("onDataReconnect: tearDown apnContext=" + apnContext);
+                    }
+                    dcac.tearDown(apnContext, "", null);
+                }
+                apnContext.setDataConnectionAc(null);
+                apnContext.setState(DctConstants.State.IDLE);
+            } else {
+                if (DBG) log("onDataReconnect: keep associated");
+            }
+            // TODO: IF already associated should we send the EVENT_TRY_SETUP_DATA???
+            sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA, apnContext));
+
+            apnContext.setReconnectIntent(null);
+        }
+    }
+
+    private void onActionIntentDataStallAlarm(Intent intent) {
+        if (VDBG_STALL) log("onActionIntentDataStallAlarm: action=" + intent.getAction());
+        Message msg = obtainMessage(DctConstants.EVENT_DATA_STALL_ALARM,
+                intent.getAction());
+        msg.arg1 = intent.getIntExtra(DATA_STALL_ALARM_TAG_EXTRA, 0);
+        sendMessage(msg);
+    }
+
+    private final ConnectivityManager mCm;
+
+    /**
+     * List of messages that are waiting to be posted, when data call disconnect
+     * is complete
+     */
+    private ArrayList<Message> mDisconnectAllCompleteMsgList = new ArrayList<Message>();
+
+    private RegistrantList mAllDataDisconnectedRegistrants = new RegistrantList();
+
+    // member variables
+    private final Phone mPhone;
+    private final UiccController mUiccController;
+    private final AtomicReference<IccRecords> mIccRecords = new AtomicReference<IccRecords>();
+    private DctConstants.Activity mActivity = DctConstants.Activity.NONE;
+    private DctConstants.State mState = DctConstants.State.IDLE;
+    private final Handler mDataConnectionTracker;
+
+    private long mTxPkts;
+    private long mRxPkts;
+    private int mNetStatPollPeriod;
+    private boolean mNetStatPollEnabled = false;
+
+    private TxRxSum mDataStallTxRxSum = new TxRxSum(0, 0);
+    // Used to track stale data stall alarms.
+    private int mDataStallAlarmTag = (int) SystemClock.elapsedRealtime();
+    // The current data stall alarm intent
+    private PendingIntent mDataStallAlarmIntent = null;
+    // Number of packets sent since the last received packet
+    private long mSentSinceLastRecv;
+    // Controls when a simple recovery attempt it to be tried
+    private int mNoRecvPollCount = 0;
+    // Reference counter for enabling fail fast
+    private static int sEnableFailFastRefCounter = 0;
+    // True if data stall detection is enabled
+    private volatile boolean mDataStallDetectionEnabled = true;
+
+    private volatile boolean mFailFast = false;
+
+    // True when in voice call
+    private boolean mInVoiceCall = false;
+
+    // wifi connection status will be updated by sticky intent
+    private boolean mIsWifiConnected = false;
+
+    /** Intent sent when the reconnect alarm fires. */
+    private PendingIntent mReconnectIntent = null;
+
+    // When false we will not auto attach and manually attaching is required.
+    private boolean mAutoAttachOnCreationConfig = false;
+    private AtomicBoolean mAutoAttachOnCreation = new AtomicBoolean(false);
+
+    // State of screen
+    // (TODO: Reconsider tying directly to screen, maybe this is
+    //        really a lower power mode")
+    private boolean mIsScreenOn = true;
+
+    // Indicates if we found mvno-specific APNs in the full APN list.
+    // used to determine if we can accept mno-specific APN for tethering.
+    private boolean mMvnoMatched = false;
+
+    /** Allows the generation of unique Id's for DataConnection objects */
+    private AtomicInteger mUniqueIdGenerator = new AtomicInteger(0);
+
+    /** The data connections. */
+    private HashMap<Integer, DataConnection> mDataConnections =
+            new HashMap<Integer, DataConnection>();
+
+    /** The data connection async channels */
+    private HashMap<Integer, DcAsyncChannel> mDataConnectionAcHashMap =
+            new HashMap<Integer, DcAsyncChannel>();
+
+    /** Convert an ApnType string to Id (TODO: Use "enumeration" instead of String for ApnType) */
+    private HashMap<String, Integer> mApnToDataConnectionId = new HashMap<String, Integer>();
+
+    /** Phone.APN_TYPE_* ===> ApnContext */
+    private final ConcurrentHashMap<String, ApnContext> mApnContexts =
+            new ConcurrentHashMap<String, ApnContext>();
+
+    private final SparseArray<ApnContext> mApnContextsById = new SparseArray<ApnContext>();
+
+    private int mDisconnectPendingCount = 0;
+
+    /** Indicate if metered APNs are disabled.
+     *  set to block all the metered APNs from continuously sending requests, which causes
+     *  undesired network load */
+    private boolean mMeteredApnDisabled = false;
+
+    /**
+     * int to remember whether has setDataProfiles and with roaming or not.
+     * 0: default, has never set data profile
+     * 1: has set data profile with home protocol
+     * 2: has set data profile with roaming protocol
+     * This is not needed once RIL command is updated to support both home and roaming protocol.
+     */
+    private int mSetDataProfileStatus = 0;
+
+    /**
+     * Handles changes to the APN db.
+     */
+    private class ApnChangeObserver extends ContentObserver {
+        public ApnChangeObserver () {
+            super(mDataConnectionTracker);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            sendMessage(obtainMessage(DctConstants.EVENT_APN_CHANGED));
+        }
+    }
+
+    //***** Instance Variables
+
+    private boolean mReregisterOnReconnectFailure = false;
+
+
+    //***** Constants
+
+    // Used by puppetmaster/*/radio_stress.py
+    private static final String PUPPET_MASTER_RADIO_STRESS_TEST = "gsm.defaultpdpcontext.active";
+
+    private static final int POLL_PDP_MILLIS = 5 * 1000;
+
+    private static final int PROVISIONING_SPINNER_TIMEOUT_MILLIS = 120 * 1000;
+
+    static final Uri PREFERAPN_NO_UPDATE_URI_USING_SUBID =
+                        Uri.parse("content://telephony/carriers/preferapn_no_update/subId/");
+    static final String APN_ID = "apn_id";
+
+    private boolean mCanSetPreferApn = false;
+
+    private AtomicBoolean mAttached = new AtomicBoolean(false);
+
+    /** Watches for changes to the APN db. */
+    private ApnChangeObserver mApnObserver;
+
+    private final String mProvisionActionName;
+    private BroadcastReceiver mProvisionBroadcastReceiver;
+    private ProgressDialog mProvisioningSpinner;
+
+    public boolean mImsRegistrationState = false;
+
+    //***** Constructor
+    public DcTracker(Phone phone) {
+        super();
+        mPhone = phone;
+
+        if (DBG) log("DCT.constructor");
+
+        mResolver = mPhone.getContext().getContentResolver();
+        mUiccController = UiccController.getInstance();
+        mUiccController.registerForIccChanged(this, DctConstants.EVENT_ICC_CHANGED, null);
+        mAlarmManager =
+                (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE);
+        mCm = (ConnectivityManager) mPhone.getContext().getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_SCREEN_ON);
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        filter.addAction(INTENT_DATA_STALL_ALARM);
+        filter.addAction(INTENT_PROVISIONING_APN_ALARM);
+        filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+
+        mDataEnabledSettings = new DataEnabledSettings(phone);
+
+        mPhone.getContext().registerReceiver(mIntentReceiver, filter, null, mPhone);
+
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mPhone.getContext());
+        mAutoAttachOnCreation.set(sp.getBoolean(Phone.DATA_DISABLED_ON_BOOT_KEY, false));
+
+        mSubscriptionManager = SubscriptionManager.from(mPhone.getContext());
+        mSubscriptionManager.addOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
+
+        HandlerThread dcHandlerThread = new HandlerThread("DcHandlerThread");
+        dcHandlerThread.start();
+        Handler dcHandler = new Handler(dcHandlerThread.getLooper());
+        mDcc = DcController.makeDcc(mPhone, this, dcHandler);
+        mDcTesterFailBringUpAll = new DcTesterFailBringUpAll(mPhone, dcHandler);
+
+        mDataConnectionTracker = this;
+        registerForAllEvents();
+        update();
+        mApnObserver = new ApnChangeObserver();
+        phone.getContext().getContentResolver().registerContentObserver(
+                Telephony.Carriers.CONTENT_URI, true, mApnObserver);
+
+        initApnContexts();
+
+        for (ApnContext apnContext : mApnContexts.values()) {
+            // Register the reconnect and restart actions.
+            filter = new IntentFilter();
+            filter.addAction(INTENT_RECONNECT_ALARM + '.' + apnContext.getApnType());
+            mPhone.getContext().registerReceiver(mIntentReceiver, filter, null, mPhone);
+        }
+
+        // Add Emergency APN to APN setting list by default to support EPDN in sim absent cases
+        initEmergencyApnSetting();
+        addEmergencyApnSetting();
+
+        mProvisionActionName = "com.android.internal.telephony.PROVISION" + phone.getPhoneId();
+
+        mSettingsObserver = new SettingsObserver(mPhone.getContext(), this);
+        registerSettingsObserver();
+    }
+
+    @VisibleForTesting
+    public DcTracker() {
+        mAlarmManager = null;
+        mCm = null;
+        mPhone = null;
+        mUiccController = null;
+        mDataConnectionTracker = null;
+        mProvisionActionName = null;
+        mSettingsObserver = new SettingsObserver(null, this);
+        mDataEnabledSettings = null;
+    }
+
+    public void registerServiceStateTrackerEvents() {
+        mPhone.getServiceStateTracker().registerForDataConnectionAttached(this,
+                DctConstants.EVENT_DATA_CONNECTION_ATTACHED, null);
+        mPhone.getServiceStateTracker().registerForDataConnectionDetached(this,
+                DctConstants.EVENT_DATA_CONNECTION_DETACHED, null);
+        mPhone.getServiceStateTracker().registerForDataRoamingOn(this,
+                DctConstants.EVENT_ROAMING_ON, null);
+        mPhone.getServiceStateTracker().registerForDataRoamingOff(this,
+                DctConstants.EVENT_ROAMING_OFF, null, true);
+        mPhone.getServiceStateTracker().registerForPsRestrictedEnabled(this,
+                DctConstants.EVENT_PS_RESTRICT_ENABLED, null);
+        mPhone.getServiceStateTracker().registerForPsRestrictedDisabled(this,
+                DctConstants.EVENT_PS_RESTRICT_DISABLED, null);
+        mPhone.getServiceStateTracker().registerForDataRegStateOrRatChanged(this,
+                DctConstants.EVENT_DATA_RAT_CHANGED, null);
+    }
+
+    public void unregisterServiceStateTrackerEvents() {
+        mPhone.getServiceStateTracker().unregisterForDataConnectionAttached(this);
+        mPhone.getServiceStateTracker().unregisterForDataConnectionDetached(this);
+        mPhone.getServiceStateTracker().unregisterForDataRoamingOn(this);
+        mPhone.getServiceStateTracker().unregisterForDataRoamingOff(this);
+        mPhone.getServiceStateTracker().unregisterForPsRestrictedEnabled(this);
+        mPhone.getServiceStateTracker().unregisterForPsRestrictedDisabled(this);
+        mPhone.getServiceStateTracker().unregisterForDataRegStateOrRatChanged(this);
+    }
+
+    private void registerForAllEvents() {
+        mPhone.mCi.registerForAvailable(this, DctConstants.EVENT_RADIO_AVAILABLE, null);
+        mPhone.mCi.registerForOffOrNotAvailable(this,
+                DctConstants.EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null);
+        mPhone.mCi.registerForDataCallListChanged(this,
+                DctConstants.EVENT_DATA_STATE_CHANGED, null);
+        // Note, this is fragile - the Phone is now presenting a merged picture
+        // of PS (volte) & CS and by diving into its internals you're just seeing
+        // the CS data.  This works well for the purposes this is currently used for
+        // but that may not always be the case.  Should probably be redesigned to
+        // accurately reflect what we're really interested in (registerForCSVoiceCallEnded).
+        mPhone.getCallTracker().registerForVoiceCallEnded(this,
+                DctConstants.EVENT_VOICE_CALL_ENDED, null);
+        mPhone.getCallTracker().registerForVoiceCallStarted(this,
+                DctConstants.EVENT_VOICE_CALL_STARTED, null);
+        registerServiceStateTrackerEvents();
+     //   SubscriptionManager.registerForDdsSwitch(this,
+     //          DctConstants.EVENT_CLEAN_UP_ALL_CONNECTIONS, null);
+        mPhone.mCi.registerForPcoData(this, DctConstants.EVENT_PCO_DATA_RECEIVED, null);
+        mPhone.getCarrierActionAgent().registerForCarrierAction(
+                CarrierActionAgent.CARRIER_ACTION_SET_METERED_APNS_ENABLED, this,
+                DctConstants.EVENT_SET_CARRIER_DATA_ENABLED, null, false);
+    }
+
+    public void dispose() {
+        if (DBG) log("DCT.dispose");
+
+        if (mProvisionBroadcastReceiver != null) {
+            mPhone.getContext().unregisterReceiver(mProvisionBroadcastReceiver);
+            mProvisionBroadcastReceiver = null;
+        }
+        if (mProvisioningSpinner != null) {
+            mProvisioningSpinner.dismiss();
+            mProvisioningSpinner = null;
+        }
+
+        cleanUpAllConnections(true, null);
+
+        for (DcAsyncChannel dcac : mDataConnectionAcHashMap.values()) {
+            dcac.disconnect();
+        }
+        mDataConnectionAcHashMap.clear();
+        mIsDisposed = true;
+        mPhone.getContext().unregisterReceiver(mIntentReceiver);
+        mUiccController.unregisterForIccChanged(this);
+        mSettingsObserver.unobserve();
+
+        mSubscriptionManager
+                .removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
+        mDcc.dispose();
+        mDcTesterFailBringUpAll.dispose();
+
+        mPhone.getContext().getContentResolver().unregisterContentObserver(mApnObserver);
+        mApnContexts.clear();
+        mApnContextsById.clear();
+        mPrioritySortedApnContexts.clear();
+        unregisterForAllEvents();
+
+        destroyDataConnections();
+    }
+
+    private void unregisterForAllEvents() {
+         //Unregister for all events
+        mPhone.mCi.unregisterForAvailable(this);
+        mPhone.mCi.unregisterForOffOrNotAvailable(this);
+        IccRecords r = mIccRecords.get();
+        if (r != null) {
+            r.unregisterForRecordsLoaded(this);
+            mIccRecords.set(null);
+        }
+        mPhone.mCi.unregisterForDataCallListChanged(this);
+        mPhone.getCallTracker().unregisterForVoiceCallEnded(this);
+        mPhone.getCallTracker().unregisterForVoiceCallStarted(this);
+        unregisterServiceStateTrackerEvents();
+        //SubscriptionManager.unregisterForDdsSwitch(this);
+        mPhone.mCi.unregisterForPcoData(this);
+        mPhone.getCarrierActionAgent().unregisterForCarrierAction(this,
+                CarrierActionAgent.CARRIER_ACTION_SET_METERED_APNS_ENABLED);
+    }
+
+    /**
+     * Called when EVENT_RESET_DONE is received so goto
+     * IDLE state and send notifications to those interested.
+     *
+     * TODO - currently unused.  Needs to be hooked into DataConnection cleanup
+     * TODO - needs to pass some notion of which connection is reset..
+     */
+    private void onResetDone(AsyncResult ar) {
+        if (DBG) log("EVENT_RESET_DONE");
+        String reason = null;
+        if (ar.userObj instanceof String) {
+            reason = (String) ar.userObj;
+        }
+        gotoIdleAndNotifyDataConnection(reason);
+    }
+
+    /**
+     * Modify {@link android.provider.Settings.Global#MOBILE_DATA} value.
+     */
+    public void setDataEnabled(boolean enable) {
+        Message msg = obtainMessage(DctConstants.CMD_SET_USER_DATA_ENABLE);
+        msg.arg1 = enable ? 1 : 0;
+        if (DBG) log("setDataEnabled: sendMessage: enable=" + enable);
+        sendMessage(msg);
+    }
+
+    private void onSetUserDataEnabled(boolean enabled) {
+        synchronized (mDataEnabledSettings) {
+            if (mDataEnabledSettings.isUserDataEnabled() != enabled) {
+                mDataEnabledSettings.setUserDataEnabled(enabled);
+                if (!getDataRoamingEnabled() && mPhone.getServiceState().getDataRoaming()) {
+                    if (enabled) {
+                        notifyOffApnsOfAvailability(Phone.REASON_ROAMING_ON);
+                    } else {
+                        notifyOffApnsOfAvailability(Phone.REASON_DATA_DISABLED);
+                    }
+                }
+
+                // TODO: We should register for DataEnabledSetting's data enabled/disabled event and
+                // handle the rest from there.
+                if (enabled) {
+                    reevaluateDataConnections();
+                    onTrySetupData(Phone.REASON_DATA_ENABLED);
+                } else {
+                    onCleanUpAllConnections(Phone.REASON_DATA_SPECIFIC_DISABLED);
+                }
+            }
+        }
+    }
+
+    /**
+     * Reevaluate existing data connections when conditions change.
+     *
+     * For example, handle reverting restricted networks back to unrestricted. If we're changing
+     * user data to enabled and this makes data truly enabled (not disabled by other factors) we
+     * need to tear down any metered apn type that was enabled anyway by a privileged request.
+     * This allows us to reconnect to it in an unrestricted way.
+     *
+     * Or when we brought up a unmetered data connection while data is off, we only limit this
+     * data connection for unmetered use only. When data is turned back on, we need to tear that
+     * down so a full capable data connection can be re-established.
+     */
+    private void reevaluateDataConnections() {
+        if (mDataEnabledSettings.isDataEnabled()) {
+            for (ApnContext apnContext : mApnContexts.values()) {
+                if (apnContext.isConnectedOrConnecting()) {
+                    final DcAsyncChannel dcac = apnContext.getDcAc();
+                    if (dcac != null) {
+                        final NetworkCapabilities netCaps = dcac.getNetworkCapabilitiesSync();
+                        if (netCaps != null && !netCaps.hasCapability(NetworkCapabilities
+                                .NET_CAPABILITY_NOT_RESTRICTED) && !netCaps.hasCapability(
+                                NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
+                            if (DBG) {
+                                log("Tearing down restricted metered net:" + apnContext);
+                            }
+                            // Tearing down the restricted metered data call when
+                            // conditions change. This will allow reestablishing a new unrestricted
+                            // data connection.
+                            apnContext.setReason(Phone.REASON_DATA_ENABLED);
+                            cleanUpConnection(true, apnContext);
+                        } else if (apnContext.getApnSetting().isMetered(mPhone)
+                                && (netCaps != null && netCaps.hasCapability(
+                                        NetworkCapabilities.NET_CAPABILITY_NOT_METERED))) {
+                            if (DBG) {
+                                log("Tearing down unmetered net:" + apnContext);
+                            }
+                            // The APN settings is metered, but the data was still marked as
+                            // unmetered data, must be the unmetered data connection brought up when
+                            // data is off. We need to tear that down when data is enabled again.
+                            // This will allow reestablishing a new full capability data connection.
+                            apnContext.setReason(Phone.REASON_DATA_ENABLED);
+                            cleanUpConnection(true, apnContext);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void onDeviceProvisionedChange() {
+        if (isDataEnabled()) {
+            reevaluateDataConnections();
+            onTrySetupData(Phone.REASON_DATA_ENABLED);
+        } else {
+            onCleanUpAllConnections(Phone.REASON_DATA_SPECIFIC_DISABLED);
+        }
+    }
+
+
+    public long getSubId() {
+        return mPhone.getSubId();
+    }
+
+    public DctConstants.Activity getActivity() {
+        return mActivity;
+    }
+
+    private void setActivity(DctConstants.Activity activity) {
+        log("setActivity = " + activity);
+        mActivity = activity;
+        mPhone.notifyDataActivity();
+    }
+
+    public void requestNetwork(NetworkRequest networkRequest, LocalLog log) {
+        final int apnId = ApnContext.apnIdForNetworkRequest(networkRequest);
+        final ApnContext apnContext = mApnContextsById.get(apnId);
+        log.log("DcTracker.requestNetwork for " + networkRequest + " found " + apnContext);
+        if (apnContext != null) apnContext.requestNetwork(networkRequest, log);
+    }
+
+    public void releaseNetwork(NetworkRequest networkRequest, LocalLog log) {
+        final int apnId = ApnContext.apnIdForNetworkRequest(networkRequest);
+        final ApnContext apnContext = mApnContextsById.get(apnId);
+        log.log("DcTracker.releaseNetwork for " + networkRequest + " found " + apnContext);
+        if (apnContext != null) apnContext.releaseNetwork(networkRequest, log);
+    }
+
+    public boolean isApnSupported(String name) {
+        if (name == null) {
+            loge("isApnSupported: name=null");
+            return false;
+        }
+        ApnContext apnContext = mApnContexts.get(name);
+        if (apnContext == null) {
+            loge("Request for unsupported mobile name: " + name);
+            return false;
+        }
+        return true;
+    }
+
+    public int getApnPriority(String name) {
+        ApnContext apnContext = mApnContexts.get(name);
+        if (apnContext == null) {
+            loge("Request for unsupported mobile name: " + name);
+        }
+        return apnContext.priority;
+    }
+
+    // Turn telephony radio on or off.
+    private void setRadio(boolean on) {
+        final ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
+        try {
+            phone.setRadio(on);
+        } catch (Exception e) {
+            // Ignore.
+        }
+    }
+
+    // Class to handle Intent dispatched with user selects the "Sign-in to network"
+    // notification.
+    private class ProvisionNotificationBroadcastReceiver extends BroadcastReceiver {
+        private final String mNetworkOperator;
+        // Mobile provisioning URL.  Valid while provisioning notification is up.
+        // Set prior to notification being posted as URL contains ICCID which
+        // disappears when radio is off (which is the case when notification is up).
+        private final String mProvisionUrl;
+
+        public ProvisionNotificationBroadcastReceiver(String provisionUrl, String networkOperator) {
+            mNetworkOperator = networkOperator;
+            mProvisionUrl = provisionUrl;
+        }
+
+        private void setEnableFailFastMobileData(int enabled) {
+            sendMessage(obtainMessage(DctConstants.CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA, enabled, 0));
+        }
+
+        private void enableMobileProvisioning() {
+            final Message msg = obtainMessage(DctConstants.CMD_ENABLE_MOBILE_PROVISIONING);
+            msg.setData(Bundle.forPair(DctConstants.PROVISIONING_URL_KEY, mProvisionUrl));
+            sendMessage(msg);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // Turning back on the radio can take time on the order of a minute, so show user a
+            // spinner so they know something is going on.
+            log("onReceive : ProvisionNotificationBroadcastReceiver");
+            mProvisioningSpinner = new ProgressDialog(context);
+            mProvisioningSpinner.setTitle(mNetworkOperator);
+            mProvisioningSpinner.setMessage(
+                    // TODO: Don't borrow "Connecting..." i18n string; give Telephony a version.
+                    context.getText(com.android.internal.R.string.media_route_status_connecting));
+            mProvisioningSpinner.setIndeterminate(true);
+            mProvisioningSpinner.setCancelable(true);
+            // Allow non-Activity Service Context to create a View.
+            mProvisioningSpinner.getWindow().setType(
+                    WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+            mProvisioningSpinner.show();
+            // After timeout, hide spinner so user can at least use their device.
+            // TODO: Indicate to user that it is taking an unusually long time to connect?
+            sendMessageDelayed(obtainMessage(DctConstants.CMD_CLEAR_PROVISIONING_SPINNER,
+                    mProvisioningSpinner), PROVISIONING_SPINNER_TIMEOUT_MILLIS);
+            // This code is almost identical to the old
+            // ConnectivityService.handleMobileProvisioningAction code.
+            setRadio(true);
+            setEnableFailFastMobileData(DctConstants.ENABLED);
+            enableMobileProvisioning();
+        }
+    }
+
+    @Override
+    protected void finalize() {
+        if(DBG && mPhone != null) log("finalize");
+    }
+
+    private ApnContext addApnContext(String type, NetworkConfig networkConfig) {
+        ApnContext apnContext = new ApnContext(mPhone, type, LOG_TAG, networkConfig, this);
+        mApnContexts.put(type, apnContext);
+        mApnContextsById.put(ApnContext.apnIdForApnName(type), apnContext);
+        mPrioritySortedApnContexts.add(apnContext);
+        return apnContext;
+    }
+
+    private void initApnContexts() {
+        log("initApnContexts: E");
+        // Load device network attributes from resources
+        String[] networkConfigStrings = mPhone.getContext().getResources().getStringArray(
+                com.android.internal.R.array.networkAttributes);
+        for (String networkConfigString : networkConfigStrings) {
+            NetworkConfig networkConfig = new NetworkConfig(networkConfigString);
+            ApnContext apnContext = null;
+
+            switch (networkConfig.type) {
+            case ConnectivityManager.TYPE_MOBILE:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_DEFAULT, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_MMS:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_MMS, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_SUPL:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_SUPL, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_DUN:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_DUN, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_HIPRI:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_HIPRI, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_FOTA:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_FOTA, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_IMS:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_IMS, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_CBS:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_CBS, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_IA:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_IA, networkConfig);
+                break;
+            case ConnectivityManager.TYPE_MOBILE_EMERGENCY:
+                apnContext = addApnContext(PhoneConstants.APN_TYPE_EMERGENCY, networkConfig);
+                break;
+            default:
+                log("initApnContexts: skipping unknown type=" + networkConfig.type);
+                continue;
+            }
+            log("initApnContexts: apnContext=" + apnContext);
+        }
+
+        if (VDBG) log("initApnContexts: X mApnContexts=" + mApnContexts);
+    }
+
+    public LinkProperties getLinkProperties(String apnType) {
+        ApnContext apnContext = mApnContexts.get(apnType);
+        if (apnContext != null) {
+            DcAsyncChannel dcac = apnContext.getDcAc();
+            if (dcac != null) {
+                if (DBG) log("return link properites for " + apnType);
+                return dcac.getLinkPropertiesSync();
+            }
+        }
+        if (DBG) log("return new LinkProperties");
+        return new LinkProperties();
+    }
+
+    public NetworkCapabilities getNetworkCapabilities(String apnType) {
+        ApnContext apnContext = mApnContexts.get(apnType);
+        if (apnContext!=null) {
+            DcAsyncChannel dataConnectionAc = apnContext.getDcAc();
+            if (dataConnectionAc != null) {
+                if (DBG) {
+                    log("get active pdp is not null, return NetworkCapabilities for " + apnType);
+                }
+                return dataConnectionAc.getNetworkCapabilitiesSync();
+            }
+        }
+        if (DBG) log("return new NetworkCapabilities");
+        return new NetworkCapabilities();
+    }
+
+    // Return all active apn types
+    public String[] getActiveApnTypes() {
+        if (DBG) log("get all active apn types");
+        ArrayList<String> result = new ArrayList<String>();
+
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (mAttached.get() && apnContext.isReady()) {
+                result.add(apnContext.getApnType());
+            }
+        }
+
+        return result.toArray(new String[0]);
+    }
+
+    // Return active apn of specific apn type
+    public String getActiveApnString(String apnType) {
+        if (VDBG) log( "get active apn string for type:" + apnType);
+        ApnContext apnContext = mApnContexts.get(apnType);
+        if (apnContext != null) {
+            ApnSetting apnSetting = apnContext.getApnSetting();
+            if (apnSetting != null) {
+                return apnSetting.apn;
+            }
+        }
+        return null;
+    }
+
+    // Return state of specific apn type
+    public DctConstants.State getState(String apnType) {
+        ApnContext apnContext = mApnContexts.get(apnType);
+        if (apnContext != null) {
+            return apnContext.getState();
+        }
+        return DctConstants.State.FAILED;
+    }
+
+    // Return if apn type is a provisioning apn.
+    private boolean isProvisioningApn(String apnType) {
+        ApnContext apnContext = mApnContexts.get(apnType);
+        if (apnContext != null) {
+            return apnContext.isProvisioningApn();
+        }
+        return false;
+    }
+
+    // Return state of overall
+    public DctConstants.State getOverallState() {
+        boolean isConnecting = false;
+        boolean isFailed = true; // All enabled Apns should be FAILED.
+        boolean isAnyEnabled = false;
+
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (apnContext.isEnabled()) {
+                isAnyEnabled = true;
+                switch (apnContext.getState()) {
+                case CONNECTED:
+                case DISCONNECTING:
+                    if (VDBG) log("overall state is CONNECTED");
+                    return DctConstants.State.CONNECTED;
+                case RETRYING:
+                case CONNECTING:
+                    isConnecting = true;
+                    isFailed = false;
+                    break;
+                case IDLE:
+                case SCANNING:
+                    isFailed = false;
+                    break;
+                default:
+                    isAnyEnabled = true;
+                    break;
+                }
+            }
+        }
+
+        if (!isAnyEnabled) { // Nothing enabled. return IDLE.
+            if (VDBG) log( "overall state is IDLE");
+            return DctConstants.State.IDLE;
+        }
+
+        if (isConnecting) {
+            if (VDBG) log( "overall state is CONNECTING");
+            return DctConstants.State.CONNECTING;
+        } else if (!isFailed) {
+            if (VDBG) log( "overall state is IDLE");
+            return DctConstants.State.IDLE;
+        } else {
+            if (VDBG) log( "overall state is FAILED");
+            return DctConstants.State.FAILED;
+        }
+    }
+
+    @VisibleForTesting
+    public boolean isDataEnabled() {
+        return mDataEnabledSettings.isDataEnabled();
+    }
+
+    //****** Called from ServiceStateTracker
+    /**
+     * Invoked when ServiceStateTracker observes a transition from GPRS
+     * attach to detach.
+     */
+    private void onDataConnectionDetached() {
+        /*
+         * We presently believe it is unnecessary to tear down the PDP context
+         * when GPRS detaches, but we should stop the network polling.
+         */
+        if (DBG) log ("onDataConnectionDetached: stop polling and notify detached");
+        stopNetStatPoll();
+        stopDataStallAlarm();
+        notifyDataConnection(Phone.REASON_DATA_DETACHED);
+        mAttached.set(false);
+    }
+
+    private void onDataConnectionAttached() {
+        if (DBG) log("onDataConnectionAttached");
+        mAttached.set(true);
+        if (getOverallState() == DctConstants.State.CONNECTED) {
+            if (DBG) log("onDataConnectionAttached: start polling notify attached");
+            startNetStatPoll();
+            startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+            notifyDataConnection(Phone.REASON_DATA_ATTACHED);
+        } else {
+            // update APN availability so that APN can be enabled.
+            notifyOffApnsOfAvailability(Phone.REASON_DATA_ATTACHED);
+        }
+        if (mAutoAttachOnCreationConfig) {
+            mAutoAttachOnCreation.set(true);
+        }
+        setupDataOnConnectableApns(Phone.REASON_DATA_ATTACHED);
+    }
+
+    /**
+     * Check if it is allowed to make a data connection (without checking APN context specific
+     * conditions).
+     *
+     * @param dataConnectionReasons Data connection allowed or disallowed reasons as the output
+     *                              param. It's okay to pass null here and no reasons will be
+     *                              provided.
+     * @return True if data connection is allowed, otherwise false.
+     */
+    public boolean isDataAllowed(DataConnectionReasons dataConnectionReasons) {
+        return isDataAllowed(null, dataConnectionReasons);
+    }
+
+    /**
+     * Check if it is allowed to make a data connection for a given APN type.
+     *
+     * @param apnContext APN context. If passing null, then will only check general but not APN
+     *                   specific conditions (e.g. APN state, metered/unmetered APN).
+     * @param dataConnectionReasons Data connection allowed or disallowed reasons as the output
+     *                              param. It's okay to pass null here and no reasons will be
+     *                              provided.
+     * @return True if data connection is allowed, otherwise false.
+     */
+    boolean isDataAllowed(ApnContext apnContext, DataConnectionReasons dataConnectionReasons) {
+        // Step 1: Get all environment conditions.
+        // Step 2: Special handling for emergency APN.
+        // Step 3. Build disallowed reasons.
+        // Step 4: Determine if data should be allowed in some special conditions.
+
+        DataConnectionReasons reasons = new DataConnectionReasons();
+
+        // Step 1: Get all environment conditions.
+        final boolean internalDataEnabled = mDataEnabledSettings.isInternalDataEnabled();
+        boolean attachedState = mAttached.get();
+        boolean desiredPowerState = mPhone.getServiceStateTracker().getDesiredPowerState();
+        boolean radioStateFromCarrier = mPhone.getServiceStateTracker().getPowerStateFromCarrier();
+        // TODO: Remove this hack added by ag/641832.
+        int radioTech = mPhone.getServiceState().getRilDataRadioTechnology();
+        if (radioTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN) {
+            desiredPowerState = true;
+            radioStateFromCarrier = true;
+        }
+
+        boolean recordsLoaded = mIccRecords.get() != null && mIccRecords.get().getRecordsLoaded();
+
+        boolean defaultDataSelected = SubscriptionManager.isValidSubscriptionId(
+                SubscriptionManager.getDefaultDataSubscriptionId());
+
+        boolean isMeteredApnType = apnContext == null
+                || ApnSetting.isMeteredApnType(apnContext.getApnType(), mPhone);
+
+        PhoneConstants.State phoneState = PhoneConstants.State.IDLE;
+        // Note this is explicitly not using mPhone.getState.  See b/19090488.
+        // mPhone.getState reports the merge of CS and PS (volte) voice call state
+        // but we only care about CS calls here for data/voice concurrency issues.
+        // Calling getCallTracker currently gives you just the CS side where the
+        // ImsCallTracker is held internally where applicable.
+        // This should be redesigned to ask explicitly what we want:
+        // voiceCallStateAllowDataCall, or dataCallAllowed or something similar.
+        if (mPhone.getCallTracker() != null) {
+            phoneState = mPhone.getCallTracker().getState();
+        }
+
+        // Step 2: Special handling for emergency APN.
+        if (apnContext != null
+                && apnContext.getApnType().equals(PhoneConstants.APN_TYPE_EMERGENCY)
+                && apnContext.isConnectable()) {
+            // If this is an emergency APN, as long as the APN is connectable, we
+            // should allow it.
+            if (dataConnectionReasons != null) {
+                dataConnectionReasons.add(DataAllowedReasonType.EMERGENCY_APN);
+            }
+            // Bail out without further checks.
+            return true;
+        }
+
+        // Step 3. Build disallowed reasons.
+        if (apnContext != null && !apnContext.isConnectable()) {
+            reasons.add(DataDisallowedReasonType.APN_NOT_CONNECTABLE);
+        }
+
+        // If RAT is IWLAN then don't allow default/IA PDP at all.
+        // Rest of APN types can be evaluated for remaining conditions.
+        if ((apnContext != null && (apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT)
+                || apnContext.getApnType().equals(PhoneConstants.APN_TYPE_IA)))
+                && (radioTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN)) {
+            reasons.add(DataDisallowedReasonType.ON_IWLAN);
+        }
+
+        if (isEmergency()) {
+            reasons.add(DataDisallowedReasonType.IN_ECBM);
+        }
+
+        if (!(attachedState || mAutoAttachOnCreation.get())) {
+            reasons.add(DataDisallowedReasonType.NOT_ATTACHED);
+        }
+        if (!recordsLoaded) {
+            reasons.add(DataDisallowedReasonType.RECORD_NOT_LOADED);
+        }
+        if (phoneState != PhoneConstants.State.IDLE
+                && !mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) {
+            reasons.add(DataDisallowedReasonType.INVALID_PHONE_STATE);
+            reasons.add(DataDisallowedReasonType.CONCURRENT_VOICE_DATA_NOT_ALLOWED);
+        }
+        if (!internalDataEnabled) {
+            reasons.add(DataDisallowedReasonType.INTERNAL_DATA_DISABLED);
+        }
+        if (!defaultDataSelected) {
+            reasons.add(DataDisallowedReasonType.DEFAULT_DATA_UNSELECTED);
+        }
+        if (mPhone.getServiceState().getDataRoaming() && !getDataRoamingEnabled()) {
+            reasons.add(DataDisallowedReasonType.ROAMING_DISABLED);
+        }
+        if (mIsPsRestricted) {
+            reasons.add(DataDisallowedReasonType.PS_RESTRICTED);
+        }
+        if (!desiredPowerState) {
+            reasons.add(DataDisallowedReasonType.UNDESIRED_POWER_STATE);
+        }
+        if (!radioStateFromCarrier) {
+            reasons.add(DataDisallowedReasonType.RADIO_DISABLED_BY_CARRIER);
+        }
+        if (!mDataEnabledSettings.isDataEnabled()) {
+            reasons.add(DataDisallowedReasonType.DATA_DISABLED);
+        }
+
+        // If there are hard disallowed reasons, we should not allow data connection no matter what.
+        if (reasons.containsHardDisallowedReasons()) {
+            if (dataConnectionReasons != null) {
+                dataConnectionReasons.copyFrom(reasons);
+            }
+            return false;
+        }
+
+        // Step 4: Determine if data should be allowed in some special conditions.
+
+        // At this point, if data is not allowed, it must be because of the soft reasons. We
+        // should start to check some special conditions that data will be allowed.
+
+        // If the request APN type is unmetered and there are soft disallowed reasons (e.g. data
+        // disabled, data roaming disabled) existing, we should allow the data because the user
+        // won't be charged anyway.
+        if (!isMeteredApnType && !reasons.allowed()) {
+            reasons.add(DataAllowedReasonType.UNMETERED_APN);
+        }
+
+        // If the request is restricted and there are only soft disallowed reasons (e.g. data
+        // disabled, data roaming disabled) existing, we should allow the data.
+        if (apnContext != null
+                && !apnContext.hasNoRestrictedRequests(true)
+                && !reasons.allowed()) {
+            reasons.add(DataAllowedReasonType.RESTRICTED_REQUEST);
+        }
+
+        // If at this point, we still haven't built any disallowed reasons, we should allow data.
+        if (reasons.allowed()) {
+            reasons.add(DataAllowedReasonType.NORMAL);
+        }
+
+        if (dataConnectionReasons != null) {
+            dataConnectionReasons.copyFrom(reasons);
+        }
+
+        return reasons.allowed();
+    }
+
+    // arg for setupDataOnConnectableApns
+    private enum RetryFailures {
+        // retry failed networks always (the old default)
+        ALWAYS,
+        // retry only when a substantial change has occurred.  Either:
+        // 1) we were restricted by voice/data concurrency and aren't anymore
+        // 2) our apn list has change
+        ONLY_ON_CHANGE
+    };
+
+    private void setupDataOnConnectableApns(String reason) {
+        setupDataOnConnectableApns(reason, RetryFailures.ALWAYS);
+    }
+
+    private void setupDataOnConnectableApns(String reason, RetryFailures retryFailures) {
+        if (VDBG) log("setupDataOnConnectableApns: " + reason);
+
+        if (DBG && !VDBG) {
+            StringBuilder sb = new StringBuilder(120);
+            for (ApnContext apnContext : mPrioritySortedApnContexts) {
+                sb.append(apnContext.getApnType());
+                sb.append(":[state=");
+                sb.append(apnContext.getState());
+                sb.append(",enabled=");
+                sb.append(apnContext.isEnabled());
+                sb.append("] ");
+            }
+            log("setupDataOnConnectableApns: " + reason + " " + sb);
+        }
+
+        for (ApnContext apnContext : mPrioritySortedApnContexts) {
+            if (VDBG) log("setupDataOnConnectableApns: apnContext " + apnContext);
+
+            if (apnContext.getState() == DctConstants.State.FAILED
+                    || apnContext.getState() == DctConstants.State.SCANNING) {
+                if (retryFailures == RetryFailures.ALWAYS) {
+                    apnContext.releaseDataConnection(reason);
+                } else if (apnContext.isConcurrentVoiceAndDataAllowed() == false &&
+                        mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) {
+                    // RetryFailures.ONLY_ON_CHANGE - check if voice concurrency has changed
+                    apnContext.releaseDataConnection(reason);
+                }
+            }
+            if (apnContext.isConnectable()) {
+                log("isConnectable() call trySetupData");
+                apnContext.setReason(reason);
+                trySetupData(apnContext);
+            }
+        }
+    }
+
+    boolean isEmergency() {
+        final boolean result = mPhone.isInEcm() || mPhone.isInEmergencyCall();
+        log("isEmergency: result=" + result);
+        return result;
+    }
+
+    private boolean trySetupData(ApnContext apnContext) {
+
+        if (mPhone.getSimulatedRadioControl() != null) {
+            // Assume data is connected on the simulator
+            // FIXME  this can be improved
+            apnContext.setState(DctConstants.State.CONNECTED);
+            mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());
+
+            log("trySetupData: X We're on the simulator; assuming connected retValue=true");
+            return true;
+        }
+
+        DataConnectionReasons dataConnectionReasons = new DataConnectionReasons();
+        boolean isDataAllowed = isDataAllowed(apnContext, dataConnectionReasons);
+        String logStr = "trySetupData for APN type " + apnContext.getApnType() + ", reason: "
+                + apnContext.getReason() + ". " + dataConnectionReasons.toString();
+        if (DBG) log(logStr);
+        apnContext.requestLog(logStr);
+        if (isDataAllowed) {
+            if (apnContext.getState() == DctConstants.State.FAILED) {
+                String str = "trySetupData: make a FAILED ApnContext IDLE so its reusable";
+                if (DBG) log(str);
+                apnContext.requestLog(str);
+                apnContext.setState(DctConstants.State.IDLE);
+            }
+            int radioTech = mPhone.getServiceState().getRilDataRadioTechnology();
+            apnContext.setConcurrentVoiceAndDataAllowed(mPhone.getServiceStateTracker()
+                    .isConcurrentVoiceAndDataAllowed());
+            if (apnContext.getState() == DctConstants.State.IDLE) {
+                ArrayList<ApnSetting> waitingApns =
+                        buildWaitingApns(apnContext.getApnType(), radioTech);
+                if (waitingApns.isEmpty()) {
+                    notifyNoData(DcFailCause.MISSING_UNKNOWN_APN, apnContext);
+                    notifyOffApnsOfAvailability(apnContext.getReason());
+                    String str = "trySetupData: X No APN found retValue=false";
+                    if (DBG) log(str);
+                    apnContext.requestLog(str);
+                    return false;
+                } else {
+                    apnContext.setWaitingApns(waitingApns);
+                    if (DBG) {
+                        log ("trySetupData: Create from mAllApnSettings : "
+                                    + apnListToString(mAllApnSettings));
+                    }
+                }
+            }
+
+            boolean retValue = setupData(apnContext, radioTech, dataConnectionReasons.contains(
+                    DataAllowedReasonType.UNMETERED_APN));
+            notifyOffApnsOfAvailability(apnContext.getReason());
+
+            if (DBG) log("trySetupData: X retValue=" + retValue);
+            return retValue;
+        } else {
+            if (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT)
+                    && apnContext.isConnectable()) {
+                mPhone.notifyDataConnectionFailed(apnContext.getReason(), apnContext.getApnType());
+            }
+            notifyOffApnsOfAvailability(apnContext.getReason());
+
+            StringBuilder str = new StringBuilder();
+
+            str.append("trySetupData failed. apnContext = [type=" + apnContext.getApnType()
+                    + ", mState=" + apnContext.getState() + ", apnEnabled="
+                    + apnContext.isEnabled() + ", mDependencyMet="
+                    + apnContext.getDependencyMet() + "] ");
+
+            if (!mDataEnabledSettings.isDataEnabled()) {
+                str.append("isDataEnabled() = false. " + mDataEnabledSettings);
+            }
+
+            // If this is a data retry, we should set the APN state to FAILED so it won't stay
+            // in SCANNING forever.
+            if (apnContext.getState() == DctConstants.State.SCANNING) {
+                apnContext.setState(DctConstants.State.FAILED);
+                str.append(" Stop retrying.");
+            }
+
+            if (DBG) log(str.toString());
+            apnContext.requestLog(str.toString());
+            return false;
+        }
+    }
+
+    // Disabled apn's still need avail/unavail notifications - send them out
+    private void notifyOffApnsOfAvailability(String reason) {
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (!mAttached.get() || !apnContext.isReady()) {
+                if (VDBG) log("notifyOffApnOfAvailability type:" + apnContext.getApnType());
+                mPhone.notifyDataConnection(reason != null ? reason : apnContext.getReason(),
+                                            apnContext.getApnType(),
+                                            PhoneConstants.DataState.DISCONNECTED);
+            } else {
+                if (VDBG) {
+                    log("notifyOffApnsOfAvailability skipped apn due to attached && isReady " +
+                            apnContext.toString());
+                }
+            }
+        }
+    }
+
+    /**
+     * If tearDown is true, this only tears down a CONNECTED session. Presently,
+     * there is no mechanism for abandoning an CONNECTING session,
+     * but would likely involve cancelling pending async requests or
+     * setting a flag or new state to ignore them when they came in
+     * @param tearDown true if the underlying DataConnection should be
+     * disconnected.
+     * @param reason reason for the clean up.
+     * @return boolean - true if we did cleanup any connections, false if they
+     *                   were already all disconnected.
+     */
+    private boolean cleanUpAllConnections(boolean tearDown, String reason) {
+        if (DBG) log("cleanUpAllConnections: tearDown=" + tearDown + " reason=" + reason);
+        boolean didDisconnect = false;
+        boolean disableMeteredOnly = false;
+
+        // reasons that only metered apn will be torn down
+        if (!TextUtils.isEmpty(reason)) {
+            disableMeteredOnly = reason.equals(Phone.REASON_DATA_SPECIFIC_DISABLED) ||
+                    reason.equals(Phone.REASON_ROAMING_ON) ||
+                    reason.equals(Phone.REASON_CARRIER_ACTION_DISABLE_METERED_APN);
+        }
+
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (apnContext.isDisconnected() == false) didDisconnect = true;
+            if (disableMeteredOnly) {
+                // Use ApnSetting to decide metered or non-metered.
+                // Tear down all metered data connections.
+                ApnSetting apnSetting = apnContext.getApnSetting();
+                if (apnSetting != null && apnSetting.isMetered(mPhone)) {
+                    if (DBG) log("clean up metered ApnContext Type: " + apnContext.getApnType());
+                    apnContext.setReason(reason);
+                    cleanUpConnection(tearDown, apnContext);
+                }
+            } else {
+                // TODO - only do cleanup if not disconnected
+                apnContext.setReason(reason);
+                cleanUpConnection(tearDown, apnContext);
+            }
+        }
+
+        stopNetStatPoll();
+        stopDataStallAlarm();
+
+        // TODO: Do we need mRequestedApnType?
+        mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT;
+
+        log("cleanUpConnection: mDisconnectPendingCount = " + mDisconnectPendingCount);
+        if (tearDown && mDisconnectPendingCount == 0) {
+            notifyDataDisconnectComplete();
+            notifyAllDataDisconnected();
+        }
+
+        return didDisconnect;
+    }
+
+    /**
+     * Cleanup all connections.
+     *
+     * TODO: Cleanup only a specified connection passed as a parameter.
+     *       Also, make sure when you clean up a conn, if it is last apply
+     *       logic as though it is cleanupAllConnections
+     *
+     * @param cause for the clean up.
+     */
+    private void onCleanUpAllConnections(String cause) {
+        cleanUpAllConnections(true, cause);
+    }
+
+    void sendCleanUpConnection(boolean tearDown, ApnContext apnContext) {
+        if (DBG) log("sendCleanUpConnection: tearDown=" + tearDown + " apnContext=" + apnContext);
+        Message msg = obtainMessage(DctConstants.EVENT_CLEAN_UP_CONNECTION);
+        msg.arg1 = tearDown ? 1 : 0;
+        msg.arg2 = 0;
+        msg.obj = apnContext;
+        sendMessage(msg);
+    }
+
+    private void cleanUpConnection(boolean tearDown, ApnContext apnContext) {
+        if (apnContext == null) {
+            if (DBG) log("cleanUpConnection: apn context is null");
+            return;
+        }
+
+        DcAsyncChannel dcac = apnContext.getDcAc();
+        String str = "cleanUpConnection: tearDown=" + tearDown + " reason=" +
+                apnContext.getReason();
+        if (VDBG) log(str + " apnContext=" + apnContext);
+        apnContext.requestLog(str);
+        if (tearDown) {
+            if (apnContext.isDisconnected()) {
+                // The request is tearDown and but ApnContext is not connected.
+                // If apnContext is not enabled anymore, break the linkage to the DCAC/DC.
+                apnContext.setState(DctConstants.State.IDLE);
+                if (!apnContext.isReady()) {
+                    if (dcac != null) {
+                        str = "cleanUpConnection: teardown, disconnected, !ready";
+                        if (DBG) log(str + " apnContext=" + apnContext);
+                        apnContext.requestLog(str);
+                        dcac.tearDown(apnContext, "", null);
+                    }
+                    apnContext.setDataConnectionAc(null);
+                }
+            } else {
+                // Connection is still there. Try to clean up.
+                if (dcac != null) {
+                    if (apnContext.getState() != DctConstants.State.DISCONNECTING) {
+                        boolean disconnectAll = false;
+                        if (PhoneConstants.APN_TYPE_DUN.equals(apnContext.getApnType())) {
+                            // CAF_MSIM is this below condition required.
+                            // if (PhoneConstants.APN_TYPE_DUN.equals(PhoneConstants.APN_TYPE_DEFAULT)) {
+                            if (teardownForDun()) {
+                                if (DBG) {
+                                    log("cleanUpConnection: disconnectAll DUN connection");
+                                }
+                                // we need to tear it down - we brought it up just for dun and
+                                // other people are camped on it and now dun is done.  We need
+                                // to stop using it and let the normal apn list get used to find
+                                // connections for the remaining desired connections
+                                disconnectAll = true;
+                            }
+                        }
+                        final int generation = apnContext.getConnectionGeneration();
+                        str = "cleanUpConnection: tearing down" + (disconnectAll ? " all" : "") +
+                                " using gen#" + generation;
+                        if (DBG) log(str + "apnContext=" + apnContext);
+                        apnContext.requestLog(str);
+                        Pair<ApnContext, Integer> pair =
+                                new Pair<ApnContext, Integer>(apnContext, generation);
+                        Message msg = obtainMessage(DctConstants.EVENT_DISCONNECT_DONE, pair);
+                        if (disconnectAll) {
+                            apnContext.getDcAc().tearDownAll(apnContext.getReason(), msg);
+                        } else {
+                            apnContext.getDcAc()
+                                .tearDown(apnContext, apnContext.getReason(), msg);
+                        }
+                        apnContext.setState(DctConstants.State.DISCONNECTING);
+                        mDisconnectPendingCount++;
+                    }
+                } else {
+                    // apn is connected but no reference to dcac.
+                    // Should not be happen, but reset the state in case.
+                    apnContext.setState(DctConstants.State.IDLE);
+                    apnContext.requestLog("cleanUpConnection: connected, bug no DCAC");
+                    mPhone.notifyDataConnection(apnContext.getReason(),
+                                                apnContext.getApnType());
+                }
+            }
+        } else {
+            // force clean up the data connection.
+            if (dcac != null) dcac.reqReset();
+            apnContext.setState(DctConstants.State.IDLE);
+            mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());
+            apnContext.setDataConnectionAc(null);
+        }
+
+        // Make sure reconnection alarm is cleaned up if there is no ApnContext
+        // associated to the connection.
+        if (dcac != null) {
+            cancelReconnectAlarm(apnContext);
+        }
+        str = "cleanUpConnection: X tearDown=" + tearDown + " reason=" + apnContext.getReason();
+        if (DBG) log(str + " apnContext=" + apnContext + " dcac=" + apnContext.getDcAc());
+        apnContext.requestLog(str);
+    }
+
+    /**
+     * Fetch dun apn
+     * @return ApnSetting to be used for dun
+     */
+    @VisibleForTesting
+    public ApnSetting fetchDunApn() {
+        if (SystemProperties.getBoolean("net.tethering.noprovisioning", false)) {
+            log("fetchDunApn: net.tethering.noprovisioning=true ret: null");
+            return null;
+        }
+        int bearer = mPhone.getServiceState().getRilDataRadioTechnology();
+        IccRecords r = mIccRecords.get();
+        String operator = (r != null) ? r.getOperatorNumeric() : "";
+        ArrayList<ApnSetting> dunCandidates = new ArrayList<ApnSetting>();
+        ApnSetting retDunSetting = null;
+
+        // Places to look for tether APN in order: TETHER_DUN_APN setting (to be deprecated soon),
+        // APN database, and config_tether_apndata resource (to be deprecated soon).
+        String apnData = Settings.Global.getString(mResolver, Settings.Global.TETHER_DUN_APN);
+        if (!TextUtils.isEmpty(apnData)) {
+            dunCandidates.addAll(ApnSetting.arrayFromString(apnData));
+            if (VDBG) log("fetchDunApn: dunCandidates from Setting: " + dunCandidates);
+        }
+
+        // todo: remove this and config_tether_apndata after APNs are moved from overlay to apns xml
+        // If TETHER_DUN_APN isn't set or APN database doesn't have dun APN,
+        // try the resource as last resort.
+        if (dunCandidates.isEmpty()) {
+            String[] apnArrayData = mPhone.getContext().getResources()
+                .getStringArray(R.array.config_tether_apndata);
+            if (!ArrayUtils.isEmpty(apnArrayData)) {
+                for (String apnString : apnArrayData) {
+                    ApnSetting apn = ApnSetting.fromString(apnString);
+                    // apn may be null if apnString isn't valid or has error parsing
+                    if (apn != null) dunCandidates.add(apn);
+                }
+                if (VDBG) log("fetchDunApn: dunCandidates from resource: " + dunCandidates);
+            }
+        }
+
+        if (dunCandidates.isEmpty()) {
+            if (!ArrayUtils.isEmpty(mAllApnSettings)) {
+                for (ApnSetting apn : mAllApnSettings) {
+                    if (apn.canHandleType(PhoneConstants.APN_TYPE_DUN)) {
+                        dunCandidates.add(apn);
+                    }
+                }
+                if (VDBG) log("fetchDunApn: dunCandidates from database: " + dunCandidates);
+            }
+        }
+
+        for (ApnSetting dunSetting : dunCandidates) {
+            if (!ServiceState.bitmaskHasTech(dunSetting.bearerBitmask, bearer)) continue;
+            if (dunSetting.numeric.equals(operator)) {
+                if (dunSetting.hasMvnoParams()) {
+                    if (r != null && ApnSetting.mvnoMatches(r, dunSetting.mvnoType,
+                            dunSetting.mvnoMatchData)) {
+                        retDunSetting = dunSetting;
+                        break;
+                    }
+                } else if (mMvnoMatched == false) {
+                    retDunSetting = dunSetting;
+                    break;
+                }
+            }
+        }
+
+        if (VDBG) log("fetchDunApn: dunSetting=" + retDunSetting);
+        return retDunSetting;
+    }
+
+    public boolean hasMatchedTetherApnSetting() {
+        ApnSetting matched = fetchDunApn();
+        log("hasMatchedTetherApnSetting: APN=" + matched);
+        return matched != null;
+    }
+
+    /**
+     * Determine if DUN connection is special and we need to teardown on start/stop
+     */
+    private boolean teardownForDun() {
+        // CDMA always needs to do this the profile id is correct
+        final int rilRat = mPhone.getServiceState().getRilDataRadioTechnology();
+        if (ServiceState.isCdma(rilRat)) return true;
+
+        return (fetchDunApn() != null);
+    }
+
+    /**
+     * Cancels the alarm associated with apnContext.
+     *
+     * @param apnContext on which the alarm should be stopped.
+     */
+    private void cancelReconnectAlarm(ApnContext apnContext) {
+        if (apnContext == null) return;
+
+        PendingIntent intent = apnContext.getReconnectIntent();
+
+        if (intent != null) {
+                AlarmManager am =
+                    (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE);
+                am.cancel(intent);
+                apnContext.setReconnectIntent(null);
+        }
+    }
+
+    /**
+     * @param types comma delimited list of APN types
+     * @return array of APN types
+     */
+    private String[] parseTypes(String types) {
+        String[] result;
+        // If unset, set to DEFAULT.
+        if (types == null || types.equals("")) {
+            result = new String[1];
+            result[0] = PhoneConstants.APN_TYPE_ALL;
+        } else {
+            result = types.split(",");
+        }
+        return result;
+    }
+
+    boolean isPermanentFailure(DcFailCause dcFailCause) {
+        return (dcFailCause.isPermanentFailure(mPhone.getContext(), mPhone.getSubId()) &&
+                (mAttached.get() == false || dcFailCause != DcFailCause.SIGNAL_LOST));
+    }
+
+    private ApnSetting makeApnSetting(Cursor cursor) {
+        String[] types = parseTypes(
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.TYPE)));
+        ApnSetting apn = new ApnSetting(
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NUMERIC)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NAME)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.APN)),
+                NetworkUtils.trimV4AddrZeros(
+                        cursor.getString(
+                        cursor.getColumnIndexOrThrow(Telephony.Carriers.PROXY))),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PORT)),
+                NetworkUtils.trimV4AddrZeros(
+                        cursor.getString(
+                        cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSC))),
+                NetworkUtils.trimV4AddrZeros(
+                        cursor.getString(
+                        cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPROXY))),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPORT)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.USER)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PASSWORD)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.AUTH_TYPE)),
+                types,
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROTOCOL)),
+                cursor.getString(cursor.getColumnIndexOrThrow(
+                        Telephony.Carriers.ROAMING_PROTOCOL)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(
+                        Telephony.Carriers.CARRIER_ENABLED)) == 1,
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.BEARER)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.BEARER_BITMASK)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROFILE_ID)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(
+                        Telephony.Carriers.MODEM_COGNITIVE)) == 1,
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MAX_CONNS)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(
+                        Telephony.Carriers.WAIT_TIME)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MAX_CONNS_TIME)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MTU)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.MVNO_TYPE)),
+                cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.MVNO_MATCH_DATA)));
+        return apn;
+    }
+
+    private ArrayList<ApnSetting> createApnList(Cursor cursor) {
+        ArrayList<ApnSetting> mnoApns = new ArrayList<ApnSetting>();
+        ArrayList<ApnSetting> mvnoApns = new ArrayList<ApnSetting>();
+        IccRecords r = mIccRecords.get();
+
+        if (cursor.moveToFirst()) {
+            do {
+                ApnSetting apn = makeApnSetting(cursor);
+                if (apn == null) {
+                    continue;
+                }
+
+                if (apn.hasMvnoParams()) {
+                    if (r != null && ApnSetting.mvnoMatches(r, apn.mvnoType, apn.mvnoMatchData)) {
+                        mvnoApns.add(apn);
+                    }
+                } else {
+                    mnoApns.add(apn);
+                }
+            } while (cursor.moveToNext());
+        }
+
+        ArrayList<ApnSetting> result;
+        if (mvnoApns.isEmpty()) {
+            result = mnoApns;
+            mMvnoMatched = false;
+        } else {
+            result = mvnoApns;
+            mMvnoMatched = true;
+        }
+        if (DBG) log("createApnList: X result=" + result);
+        return result;
+    }
+
+    private boolean dataConnectionNotInUse(DcAsyncChannel dcac) {
+        if (DBG) log("dataConnectionNotInUse: check if dcac is inuse dcac=" + dcac);
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (apnContext.getDcAc() == dcac) {
+                if (DBG) log("dataConnectionNotInUse: in use by apnContext=" + apnContext);
+                return false;
+            }
+        }
+        // TODO: Fix retry handling so free DataConnections have empty apnlists.
+        // Probably move retry handling into DataConnections and reduce complexity
+        // of DCT.
+        if (DBG) log("dataConnectionNotInUse: tearDownAll");
+        dcac.tearDownAll("No connection", null);
+        if (DBG) log("dataConnectionNotInUse: not in use return true");
+        return true;
+    }
+
+    private DcAsyncChannel findFreeDataConnection() {
+        for (DcAsyncChannel dcac : mDataConnectionAcHashMap.values()) {
+            if (dcac.isInactiveSync() && dataConnectionNotInUse(dcac)) {
+                if (DBG) {
+                    log("findFreeDataConnection: found free DataConnection=" +
+                        " dcac=" + dcac);
+                }
+                return dcac;
+            }
+        }
+        log("findFreeDataConnection: NO free DataConnection");
+        return null;
+    }
+
+    /**
+     * Setup a data connection based on given APN type.
+     *
+     * @param apnContext APN context
+     * @param radioTech RAT of the data connection
+     * @param unmeteredUseOnly True if this data connection should be only used for unmetered
+     *                         purposes only.
+     * @return True if successful, otherwise false.
+     */
+    private boolean setupData(ApnContext apnContext, int radioTech, boolean unmeteredUseOnly) {
+        if (DBG) log("setupData: apnContext=" + apnContext);
+        apnContext.requestLog("setupData");
+        ApnSetting apnSetting;
+        DcAsyncChannel dcac = null;
+
+        apnSetting = apnContext.getNextApnSetting();
+
+        if (apnSetting == null) {
+            if (DBG) log("setupData: return for no apn found!");
+            return false;
+        }
+
+        int profileId = apnSetting.profileId;
+        if (profileId == 0) {
+            profileId = getApnProfileID(apnContext.getApnType());
+        }
+
+        // On CDMA, if we're explicitly asking for DUN, we need have
+        // a dun-profiled connection so we can't share an existing one
+        // On GSM/LTE we can share existing apn connections provided they support
+        // this type.
+        if (apnContext.getApnType() != PhoneConstants.APN_TYPE_DUN ||
+                teardownForDun() == false) {
+            dcac = checkForCompatibleConnectedApnContext(apnContext);
+            if (dcac != null) {
+                // Get the dcacApnSetting for the connection we want to share.
+                ApnSetting dcacApnSetting = dcac.getApnSettingSync();
+                if (dcacApnSetting != null) {
+                    // Setting is good, so use it.
+                    apnSetting = dcacApnSetting;
+                }
+            }
+        }
+        if (dcac == null) {
+            if (isOnlySingleDcAllowed(radioTech)) {
+                if (isHigherPriorityApnContextActive(apnContext)) {
+                    if (DBG) {
+                        log("setupData: Higher priority ApnContext active.  Ignoring call");
+                    }
+                    return false;
+                }
+
+                // Only lower priority calls left.  Disconnect them all in this single PDP case
+                // so that we can bring up the requested higher priority call (once we receive
+                // response for deactivate request for the calls we are about to disconnect
+                if (cleanUpAllConnections(true, Phone.REASON_SINGLE_PDN_ARBITRATION)) {
+                    // If any call actually requested to be disconnected, means we can't
+                    // bring up this connection yet as we need to wait for those data calls
+                    // to be disconnected.
+                    if (DBG) log("setupData: Some calls are disconnecting first.  Wait and retry");
+                    return false;
+                }
+
+                // No other calls are active, so proceed
+                if (DBG) log("setupData: Single pdp. Continue setting up data call.");
+            }
+
+            dcac = findFreeDataConnection();
+
+            if (dcac == null) {
+                dcac = createDataConnection();
+            }
+
+            if (dcac == null) {
+                if (DBG) log("setupData: No free DataConnection and couldn't create one, WEIRD");
+                return false;
+            }
+        }
+        final int generation = apnContext.incAndGetConnectionGeneration();
+        if (DBG) {
+            log("setupData: dcac=" + dcac + " apnSetting=" + apnSetting + " gen#=" + generation);
+        }
+
+        apnContext.setDataConnectionAc(dcac);
+        apnContext.setApnSetting(apnSetting);
+        apnContext.setState(DctConstants.State.CONNECTING);
+        mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());
+
+        Message msg = obtainMessage();
+        msg.what = DctConstants.EVENT_DATA_SETUP_COMPLETE;
+        msg.obj = new Pair<ApnContext, Integer>(apnContext, generation);
+        dcac.bringUp(apnContext, profileId, radioTech, unmeteredUseOnly, msg, generation);
+
+        if (DBG) log("setupData: initing!");
+        return true;
+    }
+
+    private void setInitialAttachApn() {
+        ApnSetting iaApnSetting = null;
+        ApnSetting defaultApnSetting = null;
+        ApnSetting firstApnSetting = null;
+
+        log("setInitialApn: E mPreferredApn=" + mPreferredApn);
+
+        if (mPreferredApn != null && mPreferredApn.canHandleType(PhoneConstants.APN_TYPE_IA)) {
+              iaApnSetting = mPreferredApn;
+        } else if (mAllApnSettings != null && !mAllApnSettings.isEmpty()) {
+            firstApnSetting = mAllApnSettings.get(0);
+            log("setInitialApn: firstApnSetting=" + firstApnSetting);
+
+            // Search for Initial APN setting and the first apn that can handle default
+            for (ApnSetting apn : mAllApnSettings) {
+                if (apn.canHandleType(PhoneConstants.APN_TYPE_IA)) {
+                    // The Initial Attach APN is highest priority so use it if there is one
+                    log("setInitialApn: iaApnSetting=" + apn);
+                    iaApnSetting = apn;
+                    break;
+                } else if ((defaultApnSetting == null)
+                        && (apn.canHandleType(PhoneConstants.APN_TYPE_DEFAULT))) {
+                    // Use the first default apn if no better choice
+                    log("setInitialApn: defaultApnSetting=" + apn);
+                    defaultApnSetting = apn;
+                }
+            }
+        }
+
+        // The priority of apn candidates from highest to lowest is:
+        //   1) APN_TYPE_IA (Initial Attach)
+        //   2) mPreferredApn, i.e. the current preferred apn
+        //   3) The first apn that than handle APN_TYPE_DEFAULT
+        //   4) The first APN we can find.
+
+        ApnSetting initialAttachApnSetting = null;
+        if (iaApnSetting != null) {
+            if (DBG) log("setInitialAttachApn: using iaApnSetting");
+            initialAttachApnSetting = iaApnSetting;
+        } else if (mPreferredApn != null) {
+            if (DBG) log("setInitialAttachApn: using mPreferredApn");
+            initialAttachApnSetting = mPreferredApn;
+        } else if (defaultApnSetting != null) {
+            if (DBG) log("setInitialAttachApn: using defaultApnSetting");
+            initialAttachApnSetting = defaultApnSetting;
+        } else if (firstApnSetting != null) {
+            if (DBG) log("setInitialAttachApn: using firstApnSetting");
+            initialAttachApnSetting = firstApnSetting;
+        }
+
+        if (initialAttachApnSetting == null) {
+            if (DBG) log("setInitialAttachApn: X There in no available apn");
+        } else {
+            if (DBG) log("setInitialAttachApn: X selected Apn=" + initialAttachApnSetting);
+
+            mPhone.mCi.setInitialAttachApn(new DataProfile(initialAttachApnSetting),
+                    mPhone.getServiceState().getDataRoamingFromRegistration(), null);
+        }
+    }
+
+    /**
+     * Handles changes to the APN database.
+     */
+    private void onApnChanged() {
+        DctConstants.State overallState = getOverallState();
+        boolean isDisconnected = (overallState == DctConstants.State.IDLE ||
+                overallState == DctConstants.State.FAILED);
+
+        if (mPhone instanceof GsmCdmaPhone) {
+            // The "current" may no longer be valid.  MMS depends on this to send properly. TBD
+            ((GsmCdmaPhone)mPhone).updateCurrentCarrierInProvider();
+        }
+
+        // TODO: It'd be nice to only do this if the changed entrie(s)
+        // match the current operator.
+        if (DBG) log("onApnChanged: createAllApnList and cleanUpAllConnections");
+        createAllApnList();
+        setInitialAttachApn();
+        cleanUpConnectionsOnUpdatedApns(!isDisconnected, Phone.REASON_APN_CHANGED);
+
+        // FIXME: See bug 17426028 maybe no conditional is needed.
+        if (mPhone.getSubId() == SubscriptionManager.getDefaultDataSubscriptionId()) {
+            setupDataOnConnectableApns(Phone.REASON_APN_CHANGED);
+        }
+    }
+
+    /**
+     * @param cid Connection id provided from RIL.
+     * @return DataConnectionAc associated with specified cid.
+     */
+    private DcAsyncChannel findDataConnectionAcByCid(int cid) {
+        for (DcAsyncChannel dcac : mDataConnectionAcHashMap.values()) {
+            if (dcac.getCidSync() == cid) {
+                return dcac;
+            }
+        }
+        return null;
+    }
+
+    // TODO: For multiple Active APNs not exactly sure how to do this.
+    private void gotoIdleAndNotifyDataConnection(String reason) {
+        if (DBG) log("gotoIdleAndNotifyDataConnection: reason=" + reason);
+        notifyDataConnection(reason);
+    }
+
+    /**
+     * "Active" here means ApnContext isEnabled() and not in FAILED state
+     * @param apnContext to compare with
+     * @return true if higher priority active apn found
+     */
+    private boolean isHigherPriorityApnContextActive(ApnContext apnContext) {
+        for (ApnContext otherContext : mPrioritySortedApnContexts) {
+            if (apnContext.getApnType().equalsIgnoreCase(otherContext.getApnType())) return false;
+            if (otherContext.isEnabled() && otherContext.getState() != DctConstants.State.FAILED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Reports if we support multiple connections or not.
+     * This is a combination of factors, based on carrier and RAT.
+     * @param rilRadioTech the RIL Radio Tech currently in use
+     * @return true if only single DataConnection is allowed
+     */
+    private boolean isOnlySingleDcAllowed(int rilRadioTech) {
+        // Default single dc rats with no knowledge of carrier
+        int[] singleDcRats = null;
+        // get the carrier specific value, if it exists, from CarrierConfigManager.
+        // generally configManager and bundle should not be null, but if they are it should be okay
+        // to leave singleDcRats null as well
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager != null) {
+            PersistableBundle bundle = configManager.getConfig();
+            if (bundle != null) {
+                singleDcRats = bundle.getIntArray(
+                        CarrierConfigManager.KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY);
+            }
+        }
+        boolean onlySingleDcAllowed = false;
+        if (Build.IS_DEBUGGABLE &&
+                SystemProperties.getBoolean("persist.telephony.test.singleDc", false)) {
+            onlySingleDcAllowed = true;
+        }
+        if (singleDcRats != null) {
+            for (int i=0; i < singleDcRats.length && onlySingleDcAllowed == false; i++) {
+                if (rilRadioTech == singleDcRats[i]) onlySingleDcAllowed = true;
+            }
+        }
+
+        if (DBG) log("isOnlySingleDcAllowed(" + rilRadioTech + "): " + onlySingleDcAllowed);
+        return onlySingleDcAllowed;
+    }
+
+    void sendRestartRadio() {
+        if (DBG)log("sendRestartRadio:");
+        Message msg = obtainMessage(DctConstants.EVENT_RESTART_RADIO);
+        sendMessage(msg);
+    }
+
+    private void restartRadio() {
+        if (DBG) log("restartRadio: ************TURN OFF RADIO**************");
+        cleanUpAllConnections(true, Phone.REASON_RADIO_TURNED_OFF);
+        mPhone.getServiceStateTracker().powerOffRadioSafely(this);
+        /* Note: no need to call setRadioPower(true).  Assuming the desired
+         * radio power state is still ON (as tracked by ServiceStateTracker),
+         * ServiceStateTracker will call setRadioPower when it receives the
+         * RADIO_STATE_CHANGED notification for the power off.  And if the
+         * desired power state has changed in the interim, we don't want to
+         * override it with an unconditional power on.
+         */
+
+        int reset = Integer.parseInt(SystemProperties.get("net.ppp.reset-by-timeout", "0"));
+        SystemProperties.set("net.ppp.reset-by-timeout", String.valueOf(reset + 1));
+    }
+
+    /**
+     * Return true if data connection need to be setup after disconnected due to
+     * reason.
+     *
+     * @param apnContext APN context
+     * @return true if try setup data connection is need for this reason
+     */
+    private boolean retryAfterDisconnected(ApnContext apnContext) {
+        boolean retry = true;
+        String reason = apnContext.getReason();
+
+        if ( Phone.REASON_RADIO_TURNED_OFF.equals(reason) ||
+                (isOnlySingleDcAllowed(mPhone.getServiceState().getRilDataRadioTechnology())
+                 && isHigherPriorityApnContextActive(apnContext))) {
+            retry = false;
+        }
+        return retry;
+    }
+
+    private void startAlarmForReconnect(long delay, ApnContext apnContext) {
+        String apnType = apnContext.getApnType();
+
+        Intent intent = new Intent(INTENT_RECONNECT_ALARM + "." + apnType);
+        intent.putExtra(INTENT_RECONNECT_ALARM_EXTRA_REASON, apnContext.getReason());
+        intent.putExtra(INTENT_RECONNECT_ALARM_EXTRA_TYPE, apnType);
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+
+        // Get current sub id.
+        int subId = SubscriptionManager.getDefaultDataSubscriptionId();
+        intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, subId);
+
+        if (DBG) {
+            log("startAlarmForReconnect: delay=" + delay + " action=" + intent.getAction()
+                    + " apn=" + apnContext);
+        }
+
+        PendingIntent alarmIntent = PendingIntent.getBroadcast(mPhone.getContext(), 0,
+                                        intent, PendingIntent.FLAG_UPDATE_CURRENT);
+        apnContext.setReconnectIntent(alarmIntent);
+
+        // Use the exact timer instead of the inexact one to provide better user experience.
+        // In some extreme cases, we saw the retry was delayed for few minutes.
+        // Note that if the stated trigger time is in the past, the alarm will be triggered
+        // immediately.
+        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                SystemClock.elapsedRealtime() + delay, alarmIntent);
+    }
+
+    private void notifyNoData(DcFailCause lastFailCauseCode,
+                              ApnContext apnContext) {
+        if (DBG) log( "notifyNoData: type=" + apnContext.getApnType());
+        if (isPermanentFailure(lastFailCauseCode)
+            && (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT))) {
+            mPhone.notifyDataConnectionFailed(apnContext.getReason(), apnContext.getApnType());
+        }
+    }
+
+    public boolean getAutoAttachOnCreation() {
+        return mAutoAttachOnCreation.get();
+    }
+
+    private void onRecordsLoadedOrSubIdChanged() {
+        if (DBG) log("onRecordsLoadedOrSubIdChanged: createAllApnList");
+        mAutoAttachOnCreationConfig = mPhone.getContext().getResources()
+                .getBoolean(com.android.internal.R.bool.config_auto_attach_data_on_creation);
+
+        createAllApnList();
+        setInitialAttachApn();
+        if (mPhone.mCi.getRadioState().isOn()) {
+            if (DBG) log("onRecordsLoadedOrSubIdChanged: notifying data availability");
+            notifyOffApnsOfAvailability(Phone.REASON_SIM_LOADED);
+        }
+        setupDataOnConnectableApns(Phone.REASON_SIM_LOADED);
+    }
+
+    /**
+     * Action set from carrier signalling broadcast receivers to enable/disable metered apns.
+     */
+    private void onSetCarrierDataEnabled(AsyncResult ar) {
+        if (ar.exception != null) {
+            Rlog.e(LOG_TAG, "CarrierDataEnable exception: " + ar.exception);
+            return;
+        }
+        synchronized (mDataEnabledSettings) {
+            boolean enabled = (boolean) ar.result;
+            if (enabled != mDataEnabledSettings.isCarrierDataEnabled()) {
+                if (DBG) {
+                    log("carrier Action: set metered apns enabled: " + enabled);
+                }
+
+                // Disable/enable all metered apns
+                mDataEnabledSettings.setCarrierDataEnabled(enabled);
+
+                if (!enabled) {
+                    // Send otasp_sim_unprovisioned so that SuW is able to proceed and notify users
+                    mPhone.notifyOtaspChanged(TelephonyManager.OTASP_SIM_UNPROVISIONED);
+                    // Tear down all metered apns
+                    cleanUpAllConnections(true, Phone.REASON_CARRIER_ACTION_DISABLE_METERED_APN);
+                } else {
+                    // Re-evaluate Otasp state
+                    int otaspState = mPhone.getServiceStateTracker().getOtasp();
+                    mPhone.notifyOtaspChanged(otaspState);
+
+                    reevaluateDataConnections();
+                    setupDataOnConnectableApns(Phone.REASON_DATA_ENABLED);
+                }
+            }
+        }
+    }
+
+    private void onSimNotReady() {
+        if (DBG) log("onSimNotReady");
+
+        cleanUpAllConnections(true, Phone.REASON_SIM_NOT_READY);
+        mAllApnSettings = null;
+        mAutoAttachOnCreationConfig = false;
+        // Clear auto attach as modem is expected to do a new attach once SIM is ready
+        mAutoAttachOnCreation.set(false);
+    }
+
+    private void onSetDependencyMet(String apnType, boolean met) {
+        // don't allow users to tweak hipri to work around default dependency not met
+        if (PhoneConstants.APN_TYPE_HIPRI.equals(apnType)) return;
+
+        ApnContext apnContext = mApnContexts.get(apnType);
+        if (apnContext == null) {
+            loge("onSetDependencyMet: ApnContext not found in onSetDependencyMet(" +
+                    apnType + ", " + met + ")");
+            return;
+        }
+        applyNewState(apnContext, apnContext.isEnabled(), met);
+        if (PhoneConstants.APN_TYPE_DEFAULT.equals(apnType)) {
+            // tie actions on default to similar actions on HIPRI regarding dependencyMet
+            apnContext = mApnContexts.get(PhoneConstants.APN_TYPE_HIPRI);
+            if (apnContext != null) applyNewState(apnContext, apnContext.isEnabled(), met);
+        }
+    }
+
+    public void setPolicyDataEnabled(boolean enabled) {
+        if (DBG) log("setPolicyDataEnabled: " + enabled);
+        Message msg = obtainMessage(DctConstants.CMD_SET_POLICY_DATA_ENABLE);
+        msg.arg1 = (enabled ? DctConstants.ENABLED : DctConstants.DISABLED);
+        sendMessage(msg);
+    }
+
+    private void onSetPolicyDataEnabled(boolean enabled) {
+        synchronized (mDataEnabledSettings) {
+            final boolean prevEnabled = isDataEnabled();
+            if (mDataEnabledSettings.isPolicyDataEnabled() != enabled) {
+                mDataEnabledSettings.setPolicyDataEnabled(enabled);
+                // TODO: We should register for DataEnabledSetting's data enabled/disabled event and
+                // handle the rest from there.
+                if (prevEnabled != isDataEnabled()) {
+                    if (!prevEnabled) {
+                        reevaluateDataConnections();
+                        onTrySetupData(Phone.REASON_DATA_ENABLED);
+                    } else {
+                        onCleanUpAllConnections(Phone.REASON_DATA_SPECIFIC_DISABLED);
+                    }
+                }
+            }
+        }
+    }
+
+    private void applyNewState(ApnContext apnContext, boolean enabled, boolean met) {
+        boolean cleanup = false;
+        boolean trySetup = false;
+        String str ="applyNewState(" + apnContext.getApnType() + ", " + enabled +
+                "(" + apnContext.isEnabled() + "), " + met + "(" +
+                apnContext.getDependencyMet() +"))";
+        if (DBG) log(str);
+        apnContext.requestLog(str);
+
+        if (apnContext.isReady()) {
+            cleanup = true;
+            if (enabled && met) {
+                DctConstants.State state = apnContext.getState();
+                switch(state) {
+                    case CONNECTING:
+                    case CONNECTED:
+                    case DISCONNECTING:
+                        // We're "READY" and active so just return
+                        if (DBG) log("applyNewState: 'ready' so return");
+                        apnContext.requestLog("applyNewState state=" + state + ", so return");
+                        return;
+                    case IDLE:
+                        // fall through: this is unexpected but if it happens cleanup and try setup
+                    case FAILED:
+                    case SCANNING:
+                    case RETRYING: {
+                        // We're "READY" but not active so disconnect (cleanup = true) and
+                        // connect (trySetup = true) to be sure we retry the connection.
+                        trySetup = true;
+                        apnContext.setReason(Phone.REASON_DATA_ENABLED);
+                        break;
+                    }
+                }
+            } else if (met) {
+                apnContext.setReason(Phone.REASON_DATA_DISABLED);
+                // If ConnectivityService has disabled this network, stop trying to bring
+                // it up, but do not tear it down - ConnectivityService will do that
+                // directly by talking with the DataConnection.
+                //
+                // This doesn't apply to DUN, however.  Those connections have special
+                // requirements from carriers and we need stop using them when the dun
+                // request goes away.  This applies to both CDMA and GSM because they both
+                // can declare the DUN APN sharable by default traffic, thus still satisfying
+                // those requests and not torn down organically.
+                if ((apnContext.getApnType() == PhoneConstants.APN_TYPE_DUN && teardownForDun())
+                        || apnContext.getState() != DctConstants.State.CONNECTED) {
+                    str = "Clean up the connection. Apn type = " + apnContext.getApnType()
+                            + ", state = " + apnContext.getState();
+                    if (DBG) log(str);
+                    apnContext.requestLog(str);
+                    cleanup = true;
+                } else {
+                    cleanup = false;
+                }
+            } else {
+                apnContext.setReason(Phone.REASON_DATA_DEPENDENCY_UNMET);
+            }
+        } else {
+            if (enabled && met) {
+                if (apnContext.isEnabled()) {
+                    apnContext.setReason(Phone.REASON_DATA_DEPENDENCY_MET);
+                } else {
+                    apnContext.setReason(Phone.REASON_DATA_ENABLED);
+                }
+                if (apnContext.getState() == DctConstants.State.FAILED) {
+                    apnContext.setState(DctConstants.State.IDLE);
+                }
+                trySetup = true;
+            }
+        }
+        apnContext.setEnabled(enabled);
+        apnContext.setDependencyMet(met);
+        if (cleanup) cleanUpConnection(true, apnContext);
+        if (trySetup) {
+            apnContext.resetErrorCodeRetries();
+            trySetupData(apnContext);
+        }
+    }
+
+    private DcAsyncChannel checkForCompatibleConnectedApnContext(ApnContext apnContext) {
+        String apnType = apnContext.getApnType();
+        ApnSetting dunSetting = null;
+
+        if (PhoneConstants.APN_TYPE_DUN.equals(apnType)) {
+            dunSetting = fetchDunApn();
+        }
+        if (DBG) {
+            log("checkForCompatibleConnectedApnContext: apnContext=" + apnContext );
+        }
+
+        DcAsyncChannel potentialDcac = null;
+        ApnContext potentialApnCtx = null;
+        for (ApnContext curApnCtx : mApnContexts.values()) {
+            DcAsyncChannel curDcac = curApnCtx.getDcAc();
+            if (curDcac != null) {
+                ApnSetting apnSetting = curApnCtx.getApnSetting();
+                log("apnSetting: " + apnSetting);
+                if (dunSetting != null) {
+                    if (dunSetting.equals(apnSetting)) {
+                        switch (curApnCtx.getState()) {
+                            case CONNECTED:
+                                if (DBG) {
+                                    log("checkForCompatibleConnectedApnContext:"
+                                            + " found dun conn=" + curDcac
+                                            + " curApnCtx=" + curApnCtx);
+                                }
+                                return curDcac;
+                            case RETRYING:
+                            case CONNECTING:
+                                potentialDcac = curDcac;
+                                potentialApnCtx = curApnCtx;
+                            default:
+                                // Not connected, potential unchanged
+                                break;
+                        }
+                    }
+                } else if (apnSetting != null && apnSetting.canHandleType(apnType)) {
+                    switch (curApnCtx.getState()) {
+                        case CONNECTED:
+                            if (DBG) {
+                                log("checkForCompatibleConnectedApnContext:"
+                                        + " found canHandle conn=" + curDcac
+                                        + " curApnCtx=" + curApnCtx);
+                            }
+                            return curDcac;
+                        case RETRYING:
+                        case CONNECTING:
+                            potentialDcac = curDcac;
+                            potentialApnCtx = curApnCtx;
+                        default:
+                            // Not connected, potential unchanged
+                            break;
+                    }
+                }
+            } else {
+                if (VDBG) {
+                    log("checkForCompatibleConnectedApnContext: not conn curApnCtx=" + curApnCtx);
+                }
+            }
+        }
+        if (potentialDcac != null) {
+            if (DBG) {
+                log("checkForCompatibleConnectedApnContext: found potential conn=" + potentialDcac
+                        + " curApnCtx=" + potentialApnCtx);
+            }
+            return potentialDcac;
+        }
+
+        if (DBG) log("checkForCompatibleConnectedApnContext: NO conn apnContext=" + apnContext);
+        return null;
+    }
+
+    public void setEnabled(int id, boolean enable) {
+        Message msg = obtainMessage(DctConstants.EVENT_ENABLE_NEW_APN);
+        msg.arg1 = id;
+        msg.arg2 = (enable ? DctConstants.ENABLED : DctConstants.DISABLED);
+        sendMessage(msg);
+    }
+
+    private void onEnableApn(int apnId, int enabled) {
+        ApnContext apnContext = mApnContextsById.get(apnId);
+        if (apnContext == null) {
+            loge("onEnableApn(" + apnId + ", " + enabled + "): NO ApnContext");
+            return;
+        }
+        // TODO change our retry manager to use the appropriate numbers for the new APN
+        if (DBG) log("onEnableApn: apnContext=" + apnContext + " call applyNewState");
+        applyNewState(apnContext, enabled == DctConstants.ENABLED, apnContext.getDependencyMet());
+
+        if ((enabled == DctConstants.DISABLED) &&
+            isOnlySingleDcAllowed(mPhone.getServiceState().getRilDataRadioTechnology()) &&
+            !isHigherPriorityApnContextActive(apnContext)) {
+
+            if(DBG) log("onEnableApn: isOnlySingleDcAllowed true & higher priority APN disabled");
+            // If the highest priority APN is disabled and only single
+            // data call is allowed, try to setup data call on other connectable APN.
+            setupDataOnConnectableApns(Phone.REASON_SINGLE_PDN_ARBITRATION);
+        }
+    }
+
+    // TODO: We shouldnt need this.
+    private boolean onTrySetupData(String reason) {
+        if (DBG) log("onTrySetupData: reason=" + reason);
+        setupDataOnConnectableApns(reason);
+        return true;
+    }
+
+    private boolean onTrySetupData(ApnContext apnContext) {
+        if (DBG) log("onTrySetupData: apnContext=" + apnContext);
+        return trySetupData(apnContext);
+    }
+
+    /**
+     * Return current {@link android.provider.Settings.Global#MOBILE_DATA} value.
+     */
+    public boolean getDataEnabled() {
+        if (mDataEnabledSettings.isProvisioning()) {
+            return mDataEnabledSettings.isProvisioningDataEnabled();
+        } else {
+            return mDataEnabledSettings.isUserDataEnabled();
+        }
+    }
+
+    /**
+     * Modify {@link android.provider.Settings.Global#DATA_ROAMING} value for user modification only
+     */
+    public void setDataRoamingEnabledByUser(boolean enabled) {
+        final int phoneSubId = mPhone.getSubId();
+        if (getDataRoamingEnabled() != enabled) {
+            int roaming = enabled ? 1 : 0;
+
+            // For single SIM phones, this is a per phone property.
+            if (TelephonyManager.getDefault().getSimCount() == 1) {
+                Settings.Global.putInt(mResolver, Settings.Global.DATA_ROAMING, roaming);
+                setDataRoamingFromUserAction(true);
+            } else {
+                Settings.Global.putInt(mResolver, Settings.Global.DATA_ROAMING +
+                         phoneSubId, roaming);
+            }
+
+            mSubscriptionManager.setDataRoaming(roaming, phoneSubId);
+            // will trigger handleDataOnRoamingChange() through observer
+            if (DBG) {
+                log("setDataRoamingEnabledByUser: set phoneSubId=" + phoneSubId
+                        + " isRoaming=" + enabled);
+            }
+        } else {
+            if (DBG) {
+                log("setDataRoamingEnabledByUser: unchanged phoneSubId=" + phoneSubId
+                        + " isRoaming=" + enabled);
+             }
+        }
+    }
+
+    /**
+     * Return current {@link android.provider.Settings.Global#DATA_ROAMING} value.
+     */
+    public boolean getDataRoamingEnabled() {
+        boolean isDataRoamingEnabled;
+        final int phoneSubId = mPhone.getSubId();
+
+        // For single SIM phones, this is a per phone property.
+        if (TelephonyManager.getDefault().getSimCount() == 1) {
+            isDataRoamingEnabled = Settings.Global.getInt(mResolver,
+                    Settings.Global.DATA_ROAMING,
+                    getDefaultDataRoamingEnabled() ? 1 : 0) != 0;
+        } else {
+            isDataRoamingEnabled = Settings.Global.getInt(mResolver,
+                    Settings.Global.DATA_ROAMING + phoneSubId,
+                    getDefaultDataRoamingEnabled() ? 1 : 0) != 0;
+        }
+
+        if (VDBG) {
+            log("getDataRoamingEnabled: phoneSubId=" + phoneSubId
+                    + " isDataRoamingEnabled=" + isDataRoamingEnabled);
+        }
+        return isDataRoamingEnabled;
+    }
+
+    /**
+     * get default values for {@link Settings.Global#DATA_ROAMING}
+     * return {@code true} if either
+     * {@link CarrierConfigManager#KEY_CARRIER_DEFAULT_DATA_ROAMING_ENABLED_BOOL} or
+     * system property ro.com.android.dataroaming is set to true. otherwise return {@code false}
+     */
+    private boolean getDefaultDataRoamingEnabled() {
+        final CarrierConfigManager configMgr = (CarrierConfigManager)
+                mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        boolean isDataRoamingEnabled = "true".equalsIgnoreCase(SystemProperties.get(
+                "ro.com.android.dataroaming", "false"));
+        isDataRoamingEnabled |= configMgr.getConfigForSubId(mPhone.getSubId()).getBoolean(
+                CarrierConfigManager.KEY_CARRIER_DEFAULT_DATA_ROAMING_ENABLED_BOOL);
+        return isDataRoamingEnabled;
+    }
+
+    /**
+     * Set default value for {@link android.provider.Settings.Global#DATA_ROAMING}
+     * if the setting is not from user actions. default value is based on carrier config and system
+     * properties.
+     */
+    private void setDefaultDataRoamingEnabled() {
+        // For single SIM phones, this is a per phone property.
+        String setting = Settings.Global.DATA_ROAMING;
+        boolean useCarrierSpecificDefault = false;
+        if (TelephonyManager.getDefault().getSimCount() != 1) {
+            setting = setting + mPhone.getSubId();
+            try {
+                Settings.Global.getInt(mResolver, setting);
+            } catch (SettingNotFoundException ex) {
+                // For msim, update to carrier default if uninitialized.
+                useCarrierSpecificDefault = true;
+            }
+        } else if (!isDataRoamingFromUserAction()) {
+            // for single sim device, update to carrier default if user action is not set
+            useCarrierSpecificDefault = true;
+        }
+        if (useCarrierSpecificDefault) {
+            boolean defaultVal = getDefaultDataRoamingEnabled();
+            log("setDefaultDataRoamingEnabled: " + setting + "default value: " + defaultVal);
+            Settings.Global.putInt(mResolver, setting, defaultVal ? 1 : 0);
+            mSubscriptionManager.setDataRoaming(defaultVal ? 1 : 0, mPhone.getSubId());
+        }
+    }
+
+    private boolean isDataRoamingFromUserAction() {
+        final SharedPreferences sp = PreferenceManager
+                .getDefaultSharedPreferences(mPhone.getContext());
+        // since we don't want to unset user preference from system update, pass true as the default
+        // value if shared pref does not exist and set shared pref to false explicitly from factory
+        // reset.
+        if (!sp.contains(Phone.DATA_ROAMING_IS_USER_SETTING_KEY)
+                && Settings.Global.getInt(mResolver, Settings.Global.DEVICE_PROVISIONED, 0) == 0) {
+            sp.edit().putBoolean(Phone.DATA_ROAMING_IS_USER_SETTING_KEY, false).commit();
+        }
+        return sp.getBoolean(Phone.DATA_ROAMING_IS_USER_SETTING_KEY, true);
+    }
+
+    private void setDataRoamingFromUserAction(boolean isUserAction) {
+        final SharedPreferences.Editor sp = PreferenceManager
+                .getDefaultSharedPreferences(mPhone.getContext()).edit();
+        sp.putBoolean(Phone.DATA_ROAMING_IS_USER_SETTING_KEY, isUserAction).commit();
+    }
+
+    // When the data roaming status changes from roaming to non-roaming.
+    private void onDataRoamingOff() {
+        if (DBG) log("onDataRoamingOff");
+
+        if (!getDataRoamingEnabled()) {
+            // TODO: Remove this once all old vendor RILs are gone. We don't need to set initial apn
+            // attach and send the data profile again as the modem should have both roaming and
+            // non-roaming protocol in place. Modem should choose the right protocol based on the
+            // roaming condition.
+            setInitialAttachApn();
+            setDataProfilesAsNeeded();
+
+            // If the user did not enable data roaming, now when we transit from roaming to
+            // non-roaming, we should try to reestablish the data connection.
+
+            notifyOffApnsOfAvailability(Phone.REASON_ROAMING_OFF);
+            setupDataOnConnectableApns(Phone.REASON_ROAMING_OFF);
+        } else {
+            notifyDataConnection(Phone.REASON_ROAMING_OFF);
+        }
+    }
+
+    // This method is called
+    // 1. When the data roaming status changes from non-roaming to roaming.
+    // 2. When allowed data roaming settings is changed by the user.
+    private void onDataRoamingOnOrSettingsChanged(int messageType) {
+        if (DBG) log("onDataRoamingOnOrSettingsChanged");
+        // Used to differentiate data roaming turned on vs settings changed.
+        boolean settingChanged = (messageType == DctConstants.EVENT_ROAMING_SETTING_CHANGE);
+
+        // Check if the device is actually data roaming
+        if (!mPhone.getServiceState().getDataRoaming()) {
+            if (DBG) log("device is not roaming. ignored the request.");
+            return;
+        }
+
+        checkDataRoamingStatus(settingChanged);
+
+        if (getDataRoamingEnabled()) {
+            if (DBG) log("onDataRoamingOnOrSettingsChanged: setup data on roaming");
+
+            setupDataOnConnectableApns(Phone.REASON_ROAMING_ON);
+            notifyDataConnection(Phone.REASON_ROAMING_ON);
+        } else {
+            // If the user does not turn on data roaming, when we transit from non-roaming to
+            // roaming, we need to tear down the data connection otherwise the user might be
+            // charged for data roaming usage.
+            if (DBG) log("onDataRoamingOnOrSettingsChanged: Tear down data connection on roaming.");
+            cleanUpAllConnections(true, Phone.REASON_ROAMING_ON);
+            notifyOffApnsOfAvailability(Phone.REASON_ROAMING_ON);
+        }
+    }
+
+    // We want to track possible roaming data leakage. Which is, if roaming setting
+    // is disabled, yet we still setup a roaming data connection or have a connected ApnContext
+    // switched to roaming. When this happens, we log it in a local log.
+    private void checkDataRoamingStatus(boolean settingChanged) {
+        if (!settingChanged && !getDataRoamingEnabled()
+                && mPhone.getServiceState().getDataRoaming()) {
+            for (ApnContext apnContext : mApnContexts.values()) {
+                if (apnContext.getState() == DctConstants.State.CONNECTED) {
+                    mDataRoamingLeakageLog.log("PossibleRoamingLeakage "
+                            + " connection params: " + (apnContext.getDcAc() != null
+                            ? apnContext.getDcAc().mLastConnectionParams : ""));
+                }
+            }
+        }
+    }
+
+    private void onRadioAvailable() {
+        if (DBG) log("onRadioAvailable");
+        if (mPhone.getSimulatedRadioControl() != null) {
+            // Assume data is connected on the simulator
+            // FIXME  this can be improved
+            // setState(DctConstants.State.CONNECTED);
+            notifyDataConnection(null);
+
+            log("onRadioAvailable: We're on the simulator; assuming data is connected");
+        }
+
+        IccRecords r = mIccRecords.get();
+        if (r != null && r.getRecordsLoaded()) {
+            notifyOffApnsOfAvailability(null);
+        }
+
+        if (getOverallState() != DctConstants.State.IDLE) {
+            cleanUpConnection(true, null);
+        }
+    }
+
+    private void onRadioOffOrNotAvailable() {
+        // Make sure our reconnect delay starts at the initial value
+        // next time the radio comes on
+
+        mReregisterOnReconnectFailure = false;
+
+        // Clear auto attach as modem is expected to do a new attach
+        mAutoAttachOnCreation.set(false);
+
+        if (mPhone.getSimulatedRadioControl() != null) {
+            // Assume data is connected on the simulator
+            // FIXME  this can be improved
+            log("We're on the simulator; assuming radio off is meaningless");
+        } else {
+            if (DBG) log("onRadioOffOrNotAvailable: is off and clean up all connections");
+            cleanUpAllConnections(false, Phone.REASON_RADIO_TURNED_OFF);
+        }
+        notifyOffApnsOfAvailability(null);
+    }
+
+    private void completeConnection(ApnContext apnContext) {
+
+        if (DBG) log("completeConnection: successful, notify the world apnContext=" + apnContext);
+
+        if (mIsProvisioning && !TextUtils.isEmpty(mProvisioningUrl)) {
+            if (DBG) {
+                log("completeConnection: MOBILE_PROVISIONING_ACTION url="
+                        + mProvisioningUrl);
+            }
+            Intent newIntent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN,
+                    Intent.CATEGORY_APP_BROWSER);
+            newIntent.setData(Uri.parse(mProvisioningUrl));
+            newIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                    Intent.FLAG_ACTIVITY_NEW_TASK);
+            try {
+                mPhone.getContext().startActivity(newIntent);
+            } catch (ActivityNotFoundException e) {
+                loge("completeConnection: startActivityAsUser failed" + e);
+            }
+        }
+        mIsProvisioning = false;
+        mProvisioningUrl = null;
+        if (mProvisioningSpinner != null) {
+            sendMessage(obtainMessage(DctConstants.CMD_CLEAR_PROVISIONING_SPINNER,
+                    mProvisioningSpinner));
+        }
+
+        mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());
+        startNetStatPoll();
+        startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+    }
+
+    /**
+     * A SETUP (aka bringUp) has completed, possibly with an error. If
+     * there is an error this method will call {@link #onDataSetupCompleteError}.
+     */
+    private void onDataSetupComplete(AsyncResult ar) {
+
+        DcFailCause cause = DcFailCause.UNKNOWN;
+        boolean handleError = false;
+        ApnContext apnContext = getValidApnContext(ar, "onDataSetupComplete");
+
+        if (apnContext == null) return;
+
+        if (ar.exception == null) {
+            DcAsyncChannel dcac = apnContext.getDcAc();
+
+            if (RADIO_TESTS) {
+                // Note: To change radio.test.onDSC.null.dcac from command line you need to
+                // adb root and adb remount and from the command line you can only change the
+                // value to 1 once. To change it a second time you can reboot or execute
+                // adb shell stop and then adb shell start. The command line to set the value is:
+                // adb shell sqlite3 /data/data/com.android.providers.settings/databases/settings.db "insert into system (name,value) values ('radio.test.onDSC.null.dcac', '1');"
+                ContentResolver cr = mPhone.getContext().getContentResolver();
+                String radioTestProperty = "radio.test.onDSC.null.dcac";
+                if (Settings.System.getInt(cr, radioTestProperty, 0) == 1) {
+                    log("onDataSetupComplete: " + radioTestProperty +
+                            " is true, set dcac to null and reset property to false");
+                    dcac = null;
+                    Settings.System.putInt(cr, radioTestProperty, 0);
+                    log("onDataSetupComplete: " + radioTestProperty + "=" +
+                            Settings.System.getInt(mPhone.getContext().getContentResolver(),
+                                    radioTestProperty, -1));
+                }
+            }
+            if (dcac == null) {
+                log("onDataSetupComplete: no connection to DC, handle as error");
+                cause = DcFailCause.CONNECTION_TO_DATACONNECTIONAC_BROKEN;
+                handleError = true;
+            } else {
+                ApnSetting apn = apnContext.getApnSetting();
+                if (DBG) {
+                    log("onDataSetupComplete: success apn=" + (apn == null ? "unknown" : apn.apn));
+                }
+                if (apn != null && apn.proxy != null && apn.proxy.length() != 0) {
+                    try {
+                        String port = apn.port;
+                        if (TextUtils.isEmpty(port)) port = "8080";
+                        ProxyInfo proxy = new ProxyInfo(apn.proxy,
+                                Integer.parseInt(port), null);
+                        dcac.setLinkPropertiesHttpProxySync(proxy);
+                    } catch (NumberFormatException e) {
+                        loge("onDataSetupComplete: NumberFormatException making ProxyProperties (" +
+                                apn.port + "): " + e);
+                    }
+                }
+
+                // everything is setup
+                if (TextUtils.equals(apnContext.getApnType(), PhoneConstants.APN_TYPE_DEFAULT)) {
+                    try {
+                        SystemProperties.set(PUPPET_MASTER_RADIO_STRESS_TEST, "true");
+                    } catch (RuntimeException ex) {
+                        log("Failed to set PUPPET_MASTER_RADIO_STRESS_TEST to true");
+                    }
+                    if (mCanSetPreferApn && mPreferredApn == null) {
+                        if (DBG) log("onDataSetupComplete: PREFERRED APN is null");
+                        mPreferredApn = apn;
+                        if (mPreferredApn != null) {
+                            setPreferredApn(mPreferredApn.id);
+                        }
+                    }
+                } else {
+                    try {
+                        SystemProperties.set(PUPPET_MASTER_RADIO_STRESS_TEST, "false");
+                    } catch (RuntimeException ex) {
+                        log("Failed to set PUPPET_MASTER_RADIO_STRESS_TEST to false");
+                    }
+                }
+
+                // A connection is setup
+                apnContext.setState(DctConstants.State.CONNECTED);
+
+                checkDataRoamingStatus(false);
+
+                boolean isProvApn = apnContext.isProvisioningApn();
+                final ConnectivityManager cm = ConnectivityManager.from(mPhone.getContext());
+                if (mProvisionBroadcastReceiver != null) {
+                    mPhone.getContext().unregisterReceiver(mProvisionBroadcastReceiver);
+                    mProvisionBroadcastReceiver = null;
+                }
+                if ((!isProvApn) || mIsProvisioning) {
+                    // Hide any provisioning notification.
+                    cm.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_MOBILE,
+                            mProvisionActionName);
+                    // Complete the connection normally notifying the world we're connected.
+                    // We do this if this isn't a special provisioning apn or if we've been
+                    // told its time to provision.
+                    completeConnection(apnContext);
+                } else {
+                    // This is a provisioning APN that we're reporting as connected. Later
+                    // when the user desires to upgrade this to a "default" connection,
+                    // mIsProvisioning == true, we'll go through the code path above.
+                    // mIsProvisioning becomes true when CMD_ENABLE_MOBILE_PROVISIONING
+                    // is sent to the DCT.
+                    if (DBG) {
+                        log("onDataSetupComplete: successful, BUT send connected to prov apn as"
+                                + " mIsProvisioning:" + mIsProvisioning + " == false"
+                                + " && (isProvisioningApn:" + isProvApn + " == true");
+                    }
+
+                    // While radio is up, grab provisioning URL.  The URL contains ICCID which
+                    // disappears when radio is off.
+                    mProvisionBroadcastReceiver = new ProvisionNotificationBroadcastReceiver(
+                            cm.getMobileProvisioningUrl(),
+                            TelephonyManager.getDefault().getNetworkOperatorName());
+                    mPhone.getContext().registerReceiver(mProvisionBroadcastReceiver,
+                            new IntentFilter(mProvisionActionName));
+                    // Put up user notification that sign-in is required.
+                    cm.setProvisioningNotificationVisible(true, ConnectivityManager.TYPE_MOBILE,
+                            mProvisionActionName);
+                    // Turn off radio to save battery and avoid wasting carrier resources.
+                    // The network isn't usable and network validation will just fail anyhow.
+                    setRadio(false);
+                }
+                if (DBG) {
+                    log("onDataSetupComplete: SETUP complete type=" + apnContext.getApnType()
+                        + ", reason:" + apnContext.getReason());
+                }
+                if (Build.IS_DEBUGGABLE) {
+                    // adb shell setprop persist.radio.test.pco [pco_val]
+                    String radioTestProperty = "persist.radio.test.pco";
+                    int pcoVal = SystemProperties.getInt(radioTestProperty, -1);
+                    if (pcoVal != -1) {
+                        log("PCO testing: read pco value from persist.radio.test.pco " + pcoVal);
+                        final byte[] value = new byte[1];
+                        value[0] = (byte) pcoVal;
+                        final Intent intent =
+                                new Intent(TelephonyIntents.ACTION_CARRIER_SIGNAL_PCO_VALUE);
+                        intent.putExtra(TelephonyIntents.EXTRA_APN_TYPE_KEY, "default");
+                        intent.putExtra(TelephonyIntents.EXTRA_APN_PROTO_KEY, "IPV4V6");
+                        intent.putExtra(TelephonyIntents.EXTRA_PCO_ID_KEY, 0xFF00);
+                        intent.putExtra(TelephonyIntents.EXTRA_PCO_VALUE_KEY, value);
+                        mPhone.getCarrierSignalAgent().notifyCarrierSignalReceivers(intent);
+                    }
+                }
+            }
+        } else {
+            cause = (DcFailCause) (ar.result);
+            if (DBG) {
+                ApnSetting apn = apnContext.getApnSetting();
+                log(String.format("onDataSetupComplete: error apn=%s cause=%s",
+                        (apn == null ? "unknown" : apn.apn), cause));
+            }
+            if (cause.isEventLoggable()) {
+                // Log this failure to the Event Logs.
+                int cid = getCellLocationId();
+                EventLog.writeEvent(EventLogTags.PDP_SETUP_FAIL,
+                        cause.ordinal(), cid, TelephonyManager.getDefault().getNetworkType());
+            }
+            ApnSetting apn = apnContext.getApnSetting();
+            mPhone.notifyPreciseDataConnectionFailed(apnContext.getReason(),
+                    apnContext.getApnType(), apn != null ? apn.apn : "unknown", cause.toString());
+
+            // Compose broadcast intent send to the specific carrier signaling receivers
+            Intent intent = new Intent(TelephonyIntents
+                    .ACTION_CARRIER_SIGNAL_REQUEST_NETWORK_FAILED);
+            intent.putExtra(TelephonyIntents.EXTRA_ERROR_CODE_KEY, cause.getErrorCode());
+            intent.putExtra(TelephonyIntents.EXTRA_APN_TYPE_KEY, apnContext.getApnType());
+            mPhone.getCarrierSignalAgent().notifyCarrierSignalReceivers(intent);
+
+            if (cause.isRestartRadioFail(mPhone.getContext(), mPhone.getSubId()) ||
+                    apnContext.restartOnError(cause.getErrorCode())) {
+                if (DBG) log("Modem restarted.");
+                sendRestartRadio();
+            }
+
+            // If the data call failure cause is a permanent failure, we mark the APN as permanent
+            // failed.
+            if (isPermanentFailure(cause)) {
+                log("cause = " + cause + ", mark apn as permanent failed. apn = " + apn);
+                apnContext.markApnPermanentFailed(apn);
+            }
+
+            handleError = true;
+        }
+
+        if (handleError) {
+            onDataSetupCompleteError(ar);
+        }
+
+        /* If flag is set to false after SETUP_DATA_CALL is invoked, we need
+         * to clean data connections.
+         */
+        if (!mDataEnabledSettings.isInternalDataEnabled()) {
+            cleanUpAllConnections(Phone.REASON_DATA_DISABLED);
+        }
+
+    }
+
+    /**
+     * check for obsolete messages.  Return ApnContext if valid, null if not
+     */
+    private ApnContext getValidApnContext(AsyncResult ar, String logString) {
+        if (ar != null && ar.userObj instanceof Pair) {
+            Pair<ApnContext, Integer>pair = (Pair<ApnContext, Integer>)ar.userObj;
+            ApnContext apnContext = pair.first;
+            if (apnContext != null) {
+                final int generation = apnContext.getConnectionGeneration();
+                if (DBG) {
+                    log("getValidApnContext (" + logString + ") on " + apnContext + " got " +
+                            generation + " vs " + pair.second);
+                }
+                if (generation == pair.second) {
+                    return apnContext;
+                } else {
+                    log("ignoring obsolete " + logString);
+                    return null;
+                }
+            }
+        }
+        throw new RuntimeException(logString + ": No apnContext");
+    }
+
+    /**
+     * Error has occurred during the SETUP {aka bringUP} request and the DCT
+     * should either try the next waiting APN or start over from the
+     * beginning if the list is empty. Between each SETUP request there will
+     * be a delay defined by {@link #getApnDelay()}.
+     */
+    private void onDataSetupCompleteError(AsyncResult ar) {
+
+        ApnContext apnContext = getValidApnContext(ar, "onDataSetupCompleteError");
+
+        if (apnContext == null) return;
+
+        long delay = apnContext.getDelayForNextApn(mFailFast);
+
+        // Check if we need to retry or not.
+        if (delay >= 0) {
+            if (DBG) log("onDataSetupCompleteError: Try next APN. delay = " + delay);
+            apnContext.setState(DctConstants.State.SCANNING);
+            // Wait a bit before trying the next APN, so that
+            // we're not tying up the RIL command channel
+            startAlarmForReconnect(delay, apnContext);
+        } else {
+            // If we are not going to retry any APN, set this APN context to failed state.
+            // This would be the final state of a data connection.
+            apnContext.setState(DctConstants.State.FAILED);
+            mPhone.notifyDataConnection(Phone.REASON_APN_FAILED, apnContext.getApnType());
+            apnContext.setDataConnectionAc(null);
+            log("onDataSetupCompleteError: Stop retrying APNs.");
+        }
+    }
+
+    /**
+     * Called when EVENT_REDIRECTION_DETECTED is received.
+     */
+    private void onDataConnectionRedirected(String redirectUrl) {
+        if (!TextUtils.isEmpty(redirectUrl)) {
+            Intent intent = new Intent(TelephonyIntents.ACTION_CARRIER_SIGNAL_REDIRECTED);
+            intent.putExtra(TelephonyIntents.EXTRA_REDIRECTION_URL_KEY, redirectUrl);
+            mPhone.getCarrierSignalAgent().notifyCarrierSignalReceivers(intent);
+            log("Notify carrier signal receivers with redirectUrl: " + redirectUrl);
+        }
+    }
+
+    /**
+     * Called when EVENT_DISCONNECT_DONE is received.
+     */
+    private void onDisconnectDone(AsyncResult ar) {
+        ApnContext apnContext = getValidApnContext(ar, "onDisconnectDone");
+        if (apnContext == null) return;
+
+        if(DBG) log("onDisconnectDone: EVENT_DISCONNECT_DONE apnContext=" + apnContext);
+        apnContext.setState(DctConstants.State.IDLE);
+
+        mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());
+
+        // if all data connection are gone, check whether Airplane mode request was
+        // pending.
+        if (isDisconnected()) {
+            if (mPhone.getServiceStateTracker().processPendingRadioPowerOffAfterDataOff()) {
+                if (DBG) log("onDisconnectDone: radio will be turned off, no retries");
+                // Radio will be turned off. No need to retry data setup
+                apnContext.setApnSetting(null);
+                apnContext.setDataConnectionAc(null);
+
+                // Need to notify disconnect as well, in the case of switching Airplane mode.
+                // Otherwise, it would cause 30s delayed to turn on Airplane mode.
+                if (mDisconnectPendingCount > 0) {
+                    mDisconnectPendingCount--;
+                }
+
+                if (mDisconnectPendingCount == 0) {
+                    notifyDataDisconnectComplete();
+                    notifyAllDataDisconnected();
+                }
+                return;
+            }
+        }
+        // If APN is still enabled, try to bring it back up automatically
+        if (mAttached.get() && apnContext.isReady() && retryAfterDisconnected(apnContext)) {
+            try {
+                SystemProperties.set(PUPPET_MASTER_RADIO_STRESS_TEST, "false");
+            } catch (RuntimeException ex) {
+                log("Failed to set PUPPET_MASTER_RADIO_STRESS_TEST to false");
+            }
+            // Wait a bit before trying the next APN, so that
+            // we're not tying up the RIL command channel.
+            // This also helps in any external dependency to turn off the context.
+            if (DBG) log("onDisconnectDone: attached, ready and retry after disconnect");
+            long delay = apnContext.getRetryAfterDisconnectDelay();
+            if (delay > 0) {
+                // Data connection is in IDLE state, so when we reconnect later, we'll rebuild
+                // the waiting APN list, which will also reset/reconfigure the retry manager.
+                startAlarmForReconnect(delay, apnContext);
+            }
+        } else {
+            boolean restartRadioAfterProvisioning = mPhone.getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.config_restartRadioAfterProvisioning);
+
+            if (apnContext.isProvisioningApn() && restartRadioAfterProvisioning) {
+                log("onDisconnectDone: restartRadio after provisioning");
+                restartRadio();
+            }
+            apnContext.setApnSetting(null);
+            apnContext.setDataConnectionAc(null);
+            if (isOnlySingleDcAllowed(mPhone.getServiceState().getRilDataRadioTechnology())) {
+                if(DBG) log("onDisconnectDone: isOnlySigneDcAllowed true so setup single apn");
+                setupDataOnConnectableApns(Phone.REASON_SINGLE_PDN_ARBITRATION);
+            } else {
+                if(DBG) log("onDisconnectDone: not retrying");
+            }
+        }
+
+        if (mDisconnectPendingCount > 0)
+            mDisconnectPendingCount--;
+
+        if (mDisconnectPendingCount == 0) {
+            apnContext.setConcurrentVoiceAndDataAllowed(
+                    mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed());
+            notifyDataDisconnectComplete();
+            notifyAllDataDisconnected();
+        }
+
+    }
+
+    /**
+     * Called when EVENT_DISCONNECT_DC_RETRYING is received.
+     */
+    private void onDisconnectDcRetrying(AsyncResult ar) {
+        // We could just do this in DC!!!
+        ApnContext apnContext = getValidApnContext(ar, "onDisconnectDcRetrying");
+        if (apnContext == null) return;
+
+        apnContext.setState(DctConstants.State.RETRYING);
+        if(DBG) log("onDisconnectDcRetrying: apnContext=" + apnContext);
+
+        mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());
+    }
+
+    private void onVoiceCallStarted() {
+        if (DBG) log("onVoiceCallStarted");
+        mInVoiceCall = true;
+        if (isConnected() && ! mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) {
+            if (DBG) log("onVoiceCallStarted stop polling");
+            stopNetStatPoll();
+            stopDataStallAlarm();
+            notifyDataConnection(Phone.REASON_VOICE_CALL_STARTED);
+        }
+    }
+
+    private void onVoiceCallEnded() {
+        if (DBG) log("onVoiceCallEnded");
+        mInVoiceCall = false;
+        if (isConnected()) {
+            if (!mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) {
+                startNetStatPoll();
+                startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+                notifyDataConnection(Phone.REASON_VOICE_CALL_ENDED);
+            } else {
+                // clean slate after call end.
+                resetPollStats();
+            }
+        }
+        // reset reconnect timer
+        setupDataOnConnectableApns(Phone.REASON_VOICE_CALL_ENDED);
+    }
+
+    private void onCleanUpConnection(boolean tearDown, int apnId, String reason) {
+        if (DBG) log("onCleanUpConnection");
+        ApnContext apnContext = mApnContextsById.get(apnId);
+        if (apnContext != null) {
+            apnContext.setReason(reason);
+            cleanUpConnection(tearDown, apnContext);
+        }
+    }
+
+    private boolean isConnected() {
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (apnContext.getState() == DctConstants.State.CONNECTED) {
+                // At least one context is connected, return true
+                return true;
+            }
+        }
+        // There are not any contexts connected, return false
+        return false;
+    }
+
+    public boolean isDisconnected() {
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (!apnContext.isDisconnected()) {
+                // At least one context was not disconnected return false
+                return false;
+            }
+        }
+        // All contexts were disconnected so return true
+        return true;
+    }
+
+    private void notifyDataConnection(String reason) {
+        if (DBG) log("notifyDataConnection: reason=" + reason);
+        for (ApnContext apnContext : mApnContexts.values()) {
+            if (mAttached.get() && apnContext.isReady()) {
+                if (DBG) log("notifyDataConnection: type:" + apnContext.getApnType());
+                mPhone.notifyDataConnection(reason != null ? reason : apnContext.getReason(),
+                        apnContext.getApnType());
+            }
+        }
+        notifyOffApnsOfAvailability(reason);
+    }
+
+    private void setDataProfilesAsNeeded() {
+        if (DBG) log("setDataProfilesAsNeeded");
+        if (mAllApnSettings != null && !mAllApnSettings.isEmpty()) {
+            ArrayList<DataProfile> dps = new ArrayList<DataProfile>();
+            for (ApnSetting apn : mAllApnSettings) {
+                if (apn.modemCognitive) {
+                    DataProfile dp = new DataProfile(apn);
+                    if (!dps.contains(dp)) {
+                        dps.add(dp);
+                    }
+                }
+            }
+            if (dps.size() > 0) {
+                mPhone.mCi.setDataProfile(dps.toArray(new DataProfile[0]),
+                        mPhone.getServiceState().getDataRoamingFromRegistration(), null);
+            }
+        }
+    }
+
+    /**
+     * Based on the sim operator numeric, create a list for all possible
+     * Data Connections and setup the preferredApn.
+     */
+    private void createAllApnList() {
+        mMvnoMatched = false;
+        mAllApnSettings = new ArrayList<>();
+        IccRecords r = mIccRecords.get();
+        String operator = (r != null) ? r.getOperatorNumeric() : "";
+        if (operator != null) {
+            String selection = Telephony.Carriers.NUMERIC + " = '" + operator + "'";
+            // query only enabled apn.
+            // carrier_enabled : 1 means enabled apn, 0 disabled apn.
+            // selection += " and carrier_enabled = 1";
+            if (DBG) log("createAllApnList: selection=" + selection);
+
+            // ORDER BY Telephony.Carriers._ID ("_id")
+            Cursor cursor = mPhone.getContext().getContentResolver().query(
+                    Telephony.Carriers.CONTENT_URI, null, selection, null, Telephony.Carriers._ID);
+
+            if (cursor != null) {
+                if (cursor.getCount() > 0) {
+                    mAllApnSettings = createApnList(cursor);
+                }
+                cursor.close();
+            }
+        }
+
+        addEmergencyApnSetting();
+
+        dedupeApnSettings();
+
+        if (mAllApnSettings.isEmpty()) {
+            if (DBG) log("createAllApnList: No APN found for carrier: " + operator);
+            mPreferredApn = null;
+            // TODO: What is the right behavior?
+            //notifyNoData(DataConnection.FailCause.MISSING_UNKNOWN_APN);
+        } else {
+            mPreferredApn = getPreferredApn();
+            if (mPreferredApn != null && !mPreferredApn.numeric.equals(operator)) {
+                mPreferredApn = null;
+                setPreferredApn(-1);
+            }
+            if (DBG) log("createAllApnList: mPreferredApn=" + mPreferredApn);
+        }
+        if (DBG) log("createAllApnList: X mAllApnSettings=" + mAllApnSettings);
+
+        setDataProfilesAsNeeded();
+    }
+
+    private void dedupeApnSettings() {
+        ArrayList<ApnSetting> resultApns = new ArrayList<ApnSetting>();
+
+        // coalesce APNs if they are similar enough to prevent
+        // us from bringing up two data calls with the same interface
+        int i = 0;
+        while (i < mAllApnSettings.size() - 1) {
+            ApnSetting first = mAllApnSettings.get(i);
+            ApnSetting second = null;
+            int j = i + 1;
+            while (j < mAllApnSettings.size()) {
+                second = mAllApnSettings.get(j);
+                if (first.similar(second)) {
+                    ApnSetting newApn = mergeApns(first, second);
+                    mAllApnSettings.set(i, newApn);
+                    first = newApn;
+                    mAllApnSettings.remove(j);
+                } else {
+                    j++;
+                }
+            }
+            i++;
+        }
+    }
+
+    private ApnSetting mergeApns(ApnSetting dest, ApnSetting src) {
+        int id = dest.id;
+        ArrayList<String> resultTypes = new ArrayList<String>();
+        resultTypes.addAll(Arrays.asList(dest.types));
+        for (String srcType : src.types) {
+            if (resultTypes.contains(srcType) == false) resultTypes.add(srcType);
+            if (srcType.equals(PhoneConstants.APN_TYPE_DEFAULT)) id = src.id;
+        }
+        String mmsc = (TextUtils.isEmpty(dest.mmsc) ? src.mmsc : dest.mmsc);
+        String mmsProxy = (TextUtils.isEmpty(dest.mmsProxy) ? src.mmsProxy : dest.mmsProxy);
+        String mmsPort = (TextUtils.isEmpty(dest.mmsPort) ? src.mmsPort : dest.mmsPort);
+        String proxy = (TextUtils.isEmpty(dest.proxy) ? src.proxy : dest.proxy);
+        String port = (TextUtils.isEmpty(dest.port) ? src.port : dest.port);
+        String protocol = src.protocol.equals("IPV4V6") ? src.protocol : dest.protocol;
+        String roamingProtocol = src.roamingProtocol.equals("IPV4V6") ? src.roamingProtocol :
+                dest.roamingProtocol;
+        int bearerBitmask = (dest.bearerBitmask == 0 || src.bearerBitmask == 0) ?
+                0 : (dest.bearerBitmask | src.bearerBitmask);
+
+        return new ApnSetting(id, dest.numeric, dest.carrier, dest.apn,
+                proxy, port, mmsc, mmsProxy, mmsPort, dest.user, dest.password,
+                dest.authType, resultTypes.toArray(new String[0]), protocol,
+                roamingProtocol, dest.carrierEnabled, 0, bearerBitmask, dest.profileId,
+                (dest.modemCognitive || src.modemCognitive), dest.maxConns, dest.waitTime,
+                dest.maxConnsTime, dest.mtu, dest.mvnoType, dest.mvnoMatchData);
+    }
+
+    /** Return the DC AsyncChannel for the new data connection */
+    private DcAsyncChannel createDataConnection() {
+        if (DBG) log("createDataConnection E");
+
+        int id = mUniqueIdGenerator.getAndIncrement();
+        DataConnection conn = DataConnection.makeDataConnection(mPhone, id,
+                                                this, mDcTesterFailBringUpAll, mDcc);
+        mDataConnections.put(id, conn);
+        DcAsyncChannel dcac = new DcAsyncChannel(conn, LOG_TAG);
+        int status = dcac.fullyConnectSync(mPhone.getContext(), this, conn.getHandler());
+        if (status == AsyncChannel.STATUS_SUCCESSFUL) {
+            mDataConnectionAcHashMap.put(dcac.getDataConnectionIdSync(), dcac);
+        } else {
+            loge("createDataConnection: Could not connect to dcac=" + dcac + " status=" + status);
+        }
+
+        if (DBG) log("createDataConnection() X id=" + id + " dc=" + conn);
+        return dcac;
+    }
+
+    private void destroyDataConnections() {
+        if(mDataConnections != null) {
+            if (DBG) log("destroyDataConnections: clear mDataConnectionList");
+            mDataConnections.clear();
+        } else {
+            if (DBG) log("destroyDataConnections: mDataConnecitonList is empty, ignore");
+        }
+    }
+
+    /**
+     * Build a list of APNs to be used to create PDP's.
+     *
+     * @param requestedApnType
+     * @return waitingApns list to be used to create PDP
+     *          error when waitingApns.isEmpty()
+     */
+    private ArrayList<ApnSetting> buildWaitingApns(String requestedApnType, int radioTech) {
+        if (DBG) log("buildWaitingApns: E requestedApnType=" + requestedApnType);
+        ArrayList<ApnSetting> apnList = new ArrayList<ApnSetting>();
+
+        if (requestedApnType.equals(PhoneConstants.APN_TYPE_DUN)) {
+            ApnSetting dun = fetchDunApn();
+            if (dun != null) {
+                apnList.add(dun);
+                if (DBG) log("buildWaitingApns: X added APN_TYPE_DUN apnList=" + apnList);
+                return apnList;
+            }
+        }
+
+        IccRecords r = mIccRecords.get();
+        String operator = (r != null) ? r.getOperatorNumeric() : "";
+
+        // This is a workaround for a bug (7305641) where we don't failover to other
+        // suitable APNs if our preferred APN fails.  On prepaid ATT sims we need to
+        // failover to a provisioning APN, but once we've used their default data
+        // connection we are locked to it for life.  This change allows ATT devices
+        // to say they don't want to use preferred at all.
+        boolean usePreferred = true;
+        try {
+            usePreferred = ! mPhone.getContext().getResources().getBoolean(com.android.
+                    internal.R.bool.config_dontPreferApn);
+        } catch (Resources.NotFoundException e) {
+            if (DBG) log("buildWaitingApns: usePreferred NotFoundException set to true");
+            usePreferred = true;
+        }
+        if (usePreferred) {
+            mPreferredApn = getPreferredApn();
+        }
+        if (DBG) {
+            log("buildWaitingApns: usePreferred=" + usePreferred
+                    + " canSetPreferApn=" + mCanSetPreferApn
+                    + " mPreferredApn=" + mPreferredApn
+                    + " operator=" + operator + " radioTech=" + radioTech
+                    + " IccRecords r=" + r);
+        }
+
+        if (usePreferred && mCanSetPreferApn && mPreferredApn != null &&
+                mPreferredApn.canHandleType(requestedApnType)) {
+            if (DBG) {
+                log("buildWaitingApns: Preferred APN:" + operator + ":"
+                        + mPreferredApn.numeric + ":" + mPreferredApn);
+            }
+            if (mPreferredApn.numeric.equals(operator)) {
+                if (ServiceState.bitmaskHasTech(mPreferredApn.bearerBitmask, radioTech)) {
+                    apnList.add(mPreferredApn);
+                    if (DBG) log("buildWaitingApns: X added preferred apnList=" + apnList);
+                    return apnList;
+                } else {
+                    if (DBG) log("buildWaitingApns: no preferred APN");
+                    setPreferredApn(-1);
+                    mPreferredApn = null;
+                }
+            } else {
+                if (DBG) log("buildWaitingApns: no preferred APN");
+                setPreferredApn(-1);
+                mPreferredApn = null;
+            }
+        }
+        if (mAllApnSettings != null) {
+            if (DBG) log("buildWaitingApns: mAllApnSettings=" + mAllApnSettings);
+            for (ApnSetting apn : mAllApnSettings) {
+                if (apn.canHandleType(requestedApnType)) {
+                    if (ServiceState.bitmaskHasTech(apn.bearerBitmask, radioTech)) {
+                        if (DBG) log("buildWaitingApns: adding apn=" + apn);
+                        apnList.add(apn);
+                    } else {
+                        if (DBG) {
+                            log("buildWaitingApns: bearerBitmask:" + apn.bearerBitmask + " does " +
+                                    "not include radioTech:" + radioTech);
+                        }
+                    }
+                } else if (DBG) {
+                    log("buildWaitingApns: couldn't handle requested ApnType="
+                            + requestedApnType);
+                }
+            }
+        } else {
+            loge("mAllApnSettings is null!");
+        }
+        if (DBG) log("buildWaitingApns: " + apnList.size() + " APNs in the list: " + apnList);
+        return apnList;
+    }
+
+    private String apnListToString (ArrayList<ApnSetting> apns) {
+        StringBuilder result = new StringBuilder();
+        for (int i = 0, size = apns.size(); i < size; i++) {
+            result.append('[')
+                  .append(apns.get(i).toString())
+                  .append(']');
+        }
+        return result.toString();
+    }
+
+    private void setPreferredApn(int pos) {
+        if (!mCanSetPreferApn) {
+            log("setPreferredApn: X !canSEtPreferApn");
+            return;
+        }
+
+        String subId = Long.toString(mPhone.getSubId());
+        Uri uri = Uri.withAppendedPath(PREFERAPN_NO_UPDATE_URI_USING_SUBID, subId);
+        log("setPreferredApn: delete");
+        ContentResolver resolver = mPhone.getContext().getContentResolver();
+        resolver.delete(uri, null, null);
+
+        if (pos >= 0) {
+            log("setPreferredApn: insert");
+            ContentValues values = new ContentValues();
+            values.put(APN_ID, pos);
+            resolver.insert(uri, values);
+        }
+    }
+
+    private ApnSetting getPreferredApn() {
+        if (mAllApnSettings == null || mAllApnSettings.isEmpty()) {
+            log("getPreferredApn: mAllApnSettings is " + ((mAllApnSettings == null)?"null":"empty"));
+            return null;
+        }
+
+        String subId = Long.toString(mPhone.getSubId());
+        Uri uri = Uri.withAppendedPath(PREFERAPN_NO_UPDATE_URI_USING_SUBID, subId);
+        Cursor cursor = mPhone.getContext().getContentResolver().query(
+                uri, new String[] { "_id", "name", "apn" },
+                null, null, Telephony.Carriers.DEFAULT_SORT_ORDER);
+
+        if (cursor != null) {
+            mCanSetPreferApn = true;
+        } else {
+            mCanSetPreferApn = false;
+        }
+        log("getPreferredApn: mRequestedApnType=" + mRequestedApnType + " cursor=" + cursor
+                + " cursor.count=" + ((cursor != null) ? cursor.getCount() : 0));
+
+        if (mCanSetPreferApn && cursor.getCount() > 0) {
+            int pos;
+            cursor.moveToFirst();
+            pos = cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID));
+            for(ApnSetting p : mAllApnSettings) {
+                log("getPreferredApn: apnSetting=" + p);
+                if (p.id == pos && p.canHandleType(mRequestedApnType)) {
+                    log("getPreferredApn: X found apnSetting" + p);
+                    cursor.close();
+                    return p;
+                }
+            }
+        }
+
+        if (cursor != null) {
+            cursor.close();
+        }
+
+        log("getPreferredApn: X not found");
+        return null;
+    }
+
+    @Override
+    public void handleMessage (Message msg) {
+        if (VDBG) log("handleMessage msg=" + msg);
+
+        switch (msg.what) {
+            case DctConstants.EVENT_RECORDS_LOADED:
+                // If onRecordsLoadedOrSubIdChanged() is not called here, it should be called on
+                // onSubscriptionsChanged() when a valid subId is available.
+                int subId = mPhone.getSubId();
+                if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                    onRecordsLoadedOrSubIdChanged();
+                } else {
+                    log("Ignoring EVENT_RECORDS_LOADED as subId is not valid: " + subId);
+                }
+                break;
+
+            case DctConstants.EVENT_DATA_CONNECTION_DETACHED:
+                onDataConnectionDetached();
+                break;
+
+            case DctConstants.EVENT_DATA_CONNECTION_ATTACHED:
+                onDataConnectionAttached();
+                break;
+
+            case DctConstants.EVENT_DO_RECOVERY:
+                doRecovery();
+                break;
+
+            case DctConstants.EVENT_APN_CHANGED:
+                onApnChanged();
+                break;
+
+            case DctConstants.EVENT_PS_RESTRICT_ENABLED:
+                /**
+                 * We don't need to explicitly to tear down the PDP context
+                 * when PS restricted is enabled. The base band will deactive
+                 * PDP context and notify us with PDP_CONTEXT_CHANGED.
+                 * But we should stop the network polling and prevent reset PDP.
+                 */
+                if (DBG) log("EVENT_PS_RESTRICT_ENABLED " + mIsPsRestricted);
+                stopNetStatPoll();
+                stopDataStallAlarm();
+                mIsPsRestricted = true;
+                break;
+
+            case DctConstants.EVENT_PS_RESTRICT_DISABLED:
+                /**
+                 * When PS restrict is removed, we need setup PDP connection if
+                 * PDP connection is down.
+                 */
+                if (DBG) log("EVENT_PS_RESTRICT_DISABLED " + mIsPsRestricted);
+                mIsPsRestricted  = false;
+                if (isConnected()) {
+                    startNetStatPoll();
+                    startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+                } else {
+                    // TODO: Should all PDN states be checked to fail?
+                    if (mState == DctConstants.State.FAILED) {
+                        cleanUpAllConnections(false, Phone.REASON_PS_RESTRICT_ENABLED);
+                        mReregisterOnReconnectFailure = false;
+                    }
+                    ApnContext apnContext = mApnContextsById.get(DctConstants.APN_DEFAULT_ID);
+                    if (apnContext != null) {
+                        apnContext.setReason(Phone.REASON_PS_RESTRICT_ENABLED);
+                        trySetupData(apnContext);
+                    } else {
+                        loge("**** Default ApnContext not found ****");
+                        if (Build.IS_DEBUGGABLE) {
+                            throw new RuntimeException("Default ApnContext not found");
+                        }
+                    }
+                }
+                break;
+
+            case DctConstants.EVENT_TRY_SETUP_DATA:
+                if (msg.obj instanceof ApnContext) {
+                    onTrySetupData((ApnContext)msg.obj);
+                } else if (msg.obj instanceof String) {
+                    onTrySetupData((String)msg.obj);
+                } else {
+                    loge("EVENT_TRY_SETUP request w/o apnContext or String");
+                }
+                break;
+
+            case DctConstants.EVENT_CLEAN_UP_CONNECTION:
+                boolean tearDown = (msg.arg1 == 0) ? false : true;
+                if (DBG) log("EVENT_CLEAN_UP_CONNECTION tearDown=" + tearDown);
+                if (msg.obj instanceof ApnContext) {
+                    cleanUpConnection(tearDown, (ApnContext)msg.obj);
+                } else {
+                    onCleanUpConnection(tearDown, msg.arg2, (String) msg.obj);
+                }
+                break;
+            case DctConstants.EVENT_SET_INTERNAL_DATA_ENABLE: {
+                final boolean enabled = (msg.arg1 == DctConstants.ENABLED) ? true : false;
+                onSetInternalDataEnabled(enabled, (Message) msg.obj);
+                break;
+            }
+            case DctConstants.EVENT_CLEAN_UP_ALL_CONNECTIONS:
+                if ((msg.obj != null) && (msg.obj instanceof String == false)) {
+                    msg.obj = null;
+                }
+                onCleanUpAllConnections((String) msg.obj);
+                break;
+
+            case DctConstants.EVENT_DATA_RAT_CHANGED:
+                if (mPhone.getServiceState().getRilDataRadioTechnology()
+                        == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN) {
+                    // unknown rat is an exception for data rat change. It's only received when out
+                    // of service and is not applicable for apn bearer bitmask. We should bypass the
+                    // check of waiting apn list and keep the data connection on, and no need to
+                    // setup a new one.
+                    break;
+                }
+                cleanUpConnectionsOnUpdatedApns(false, Phone.REASON_NW_TYPE_CHANGED);
+                //May new Network allow setupData, so try it here
+                setupDataOnConnectableApns(Phone.REASON_NW_TYPE_CHANGED,
+                        RetryFailures.ONLY_ON_CHANGE);
+                break;
+
+            case DctConstants.CMD_CLEAR_PROVISIONING_SPINNER:
+                // Check message sender intended to clear the current spinner.
+                if (mProvisioningSpinner == msg.obj) {
+                    mProvisioningSpinner.dismiss();
+                    mProvisioningSpinner = null;
+                }
+                break;
+            case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
+                log("DISCONNECTED_CONNECTED: msg=" + msg);
+                DcAsyncChannel dcac = (DcAsyncChannel) msg.obj;
+                mDataConnectionAcHashMap.remove(dcac.getDataConnectionIdSync());
+                dcac.disconnected();
+                break;
+            }
+            case DctConstants.EVENT_ENABLE_NEW_APN:
+                onEnableApn(msg.arg1, msg.arg2);
+                break;
+
+            case DctConstants.EVENT_DATA_STALL_ALARM:
+                onDataStallAlarm(msg.arg1);
+                break;
+
+            case DctConstants.EVENT_ROAMING_OFF:
+                onDataRoamingOff();
+                break;
+
+            case DctConstants.EVENT_ROAMING_ON:
+            case DctConstants.EVENT_ROAMING_SETTING_CHANGE:
+                onDataRoamingOnOrSettingsChanged(msg.what);
+                break;
+
+            case DctConstants.EVENT_DEVICE_PROVISIONED_CHANGE:
+                onDeviceProvisionedChange();
+                break;
+
+            case DctConstants.EVENT_REDIRECTION_DETECTED:
+                String url = (String) msg.obj;
+                log("dataConnectionTracker.handleMessage: EVENT_REDIRECTION_DETECTED=" + url);
+                onDataConnectionRedirected(url);
+
+            case DctConstants.EVENT_RADIO_AVAILABLE:
+                onRadioAvailable();
+                break;
+
+            case DctConstants.EVENT_RADIO_OFF_OR_NOT_AVAILABLE:
+                onRadioOffOrNotAvailable();
+                break;
+
+            case DctConstants.EVENT_DATA_SETUP_COMPLETE:
+                onDataSetupComplete((AsyncResult) msg.obj);
+                break;
+
+            case DctConstants.EVENT_DATA_SETUP_COMPLETE_ERROR:
+                onDataSetupCompleteError((AsyncResult) msg.obj);
+                break;
+
+            case DctConstants.EVENT_DISCONNECT_DONE:
+                log("DataConnectionTracker.handleMessage: EVENT_DISCONNECT_DONE msg=" + msg);
+                onDisconnectDone((AsyncResult) msg.obj);
+                break;
+
+            case DctConstants.EVENT_DISCONNECT_DC_RETRYING:
+                log("DataConnectionTracker.handleMessage: EVENT_DISCONNECT_DC_RETRYING msg=" + msg);
+                onDisconnectDcRetrying((AsyncResult) msg.obj);
+                break;
+
+            case DctConstants.EVENT_VOICE_CALL_STARTED:
+                onVoiceCallStarted();
+                break;
+
+            case DctConstants.EVENT_VOICE_CALL_ENDED:
+                onVoiceCallEnded();
+                break;
+
+            case DctConstants.EVENT_RESET_DONE: {
+                if (DBG) log("EVENT_RESET_DONE");
+                onResetDone((AsyncResult) msg.obj);
+                break;
+            }
+            case DctConstants.CMD_SET_USER_DATA_ENABLE: {
+                final boolean enabled = (msg.arg1 == DctConstants.ENABLED) ? true : false;
+                if (DBG) log("CMD_SET_USER_DATA_ENABLE enabled=" + enabled);
+                onSetUserDataEnabled(enabled);
+                break;
+            }
+            // TODO - remove
+            case DctConstants.CMD_SET_DEPENDENCY_MET: {
+                boolean met = (msg.arg1 == DctConstants.ENABLED) ? true : false;
+                if (DBG) log("CMD_SET_DEPENDENCY_MET met=" + met);
+                Bundle bundle = msg.getData();
+                if (bundle != null) {
+                    String apnType = (String)bundle.get(DctConstants.APN_TYPE_KEY);
+                    if (apnType != null) {
+                        onSetDependencyMet(apnType, met);
+                    }
+                }
+                break;
+            }
+            case DctConstants.CMD_SET_POLICY_DATA_ENABLE: {
+                final boolean enabled = (msg.arg1 == DctConstants.ENABLED) ? true : false;
+                onSetPolicyDataEnabled(enabled);
+                break;
+            }
+            case DctConstants.CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA: {
+                sEnableFailFastRefCounter += (msg.arg1 == DctConstants.ENABLED) ? 1 : -1;
+                if (DBG) {
+                    log("CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA: "
+                            + " sEnableFailFastRefCounter=" + sEnableFailFastRefCounter);
+                }
+                if (sEnableFailFastRefCounter < 0) {
+                    final String s = "CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA: "
+                            + "sEnableFailFastRefCounter:" + sEnableFailFastRefCounter + " < 0";
+                    loge(s);
+                    sEnableFailFastRefCounter = 0;
+                }
+                final boolean enabled = sEnableFailFastRefCounter > 0;
+                if (DBG) {
+                    log("CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA: enabled=" + enabled
+                            + " sEnableFailFastRefCounter=" + sEnableFailFastRefCounter);
+                }
+                if (mFailFast != enabled) {
+                    mFailFast = enabled;
+
+                    mDataStallDetectionEnabled = !enabled;
+                    if (mDataStallDetectionEnabled
+                            && (getOverallState() == DctConstants.State.CONNECTED)
+                            && (!mInVoiceCall ||
+                                    mPhone.getServiceStateTracker()
+                                        .isConcurrentVoiceAndDataAllowed())) {
+                        if (DBG) log("CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA: start data stall");
+                        stopDataStallAlarm();
+                        startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+                    } else {
+                        if (DBG) log("CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA: stop data stall");
+                        stopDataStallAlarm();
+                    }
+                }
+
+                break;
+            }
+            case DctConstants.CMD_ENABLE_MOBILE_PROVISIONING: {
+                Bundle bundle = msg.getData();
+                if (bundle != null) {
+                    try {
+                        mProvisioningUrl = (String)bundle.get(DctConstants.PROVISIONING_URL_KEY);
+                    } catch(ClassCastException e) {
+                        loge("CMD_ENABLE_MOBILE_PROVISIONING: provisioning url not a string" + e);
+                        mProvisioningUrl = null;
+                    }
+                }
+                if (TextUtils.isEmpty(mProvisioningUrl)) {
+                    loge("CMD_ENABLE_MOBILE_PROVISIONING: provisioning url is empty, ignoring");
+                    mIsProvisioning = false;
+                    mProvisioningUrl = null;
+                } else {
+                    loge("CMD_ENABLE_MOBILE_PROVISIONING: provisioningUrl=" + mProvisioningUrl);
+                    mIsProvisioning = true;
+                    startProvisioningApnAlarm();
+                }
+                break;
+            }
+            case DctConstants.EVENT_PROVISIONING_APN_ALARM: {
+                if (DBG) log("EVENT_PROVISIONING_APN_ALARM");
+                ApnContext apnCtx = mApnContextsById.get(DctConstants.APN_DEFAULT_ID);
+                if (apnCtx.isProvisioningApn() && apnCtx.isConnectedOrConnecting()) {
+                    if (mProvisioningApnAlarmTag == msg.arg1) {
+                        if (DBG) log("EVENT_PROVISIONING_APN_ALARM: Disconnecting");
+                        mIsProvisioning = false;
+                        mProvisioningUrl = null;
+                        stopProvisioningApnAlarm();
+                        sendCleanUpConnection(true, apnCtx);
+                    } else {
+                        if (DBG) {
+                            log("EVENT_PROVISIONING_APN_ALARM: ignore stale tag,"
+                                    + " mProvisioningApnAlarmTag:" + mProvisioningApnAlarmTag
+                                    + " != arg1:" + msg.arg1);
+                        }
+                    }
+                } else {
+                    if (DBG) log("EVENT_PROVISIONING_APN_ALARM: Not connected ignore");
+                }
+                break;
+            }
+            case DctConstants.CMD_IS_PROVISIONING_APN: {
+                if (DBG) log("CMD_IS_PROVISIONING_APN");
+                boolean isProvApn;
+                try {
+                    String apnType = null;
+                    Bundle bundle = msg.getData();
+                    if (bundle != null) {
+                        apnType = (String)bundle.get(DctConstants.APN_TYPE_KEY);
+                    }
+                    if (TextUtils.isEmpty(apnType)) {
+                        loge("CMD_IS_PROVISIONING_APN: apnType is empty");
+                        isProvApn = false;
+                    } else {
+                        isProvApn = isProvisioningApn(apnType);
+                    }
+                } catch (ClassCastException e) {
+                    loge("CMD_IS_PROVISIONING_APN: NO provisioning url ignoring");
+                    isProvApn = false;
+                }
+                if (DBG) log("CMD_IS_PROVISIONING_APN: ret=" + isProvApn);
+                mReplyAc.replyToMessage(msg, DctConstants.CMD_IS_PROVISIONING_APN,
+                        isProvApn ? DctConstants.ENABLED : DctConstants.DISABLED);
+                break;
+            }
+            case DctConstants.EVENT_ICC_CHANGED: {
+                onUpdateIcc();
+                break;
+            }
+            case DctConstants.EVENT_RESTART_RADIO: {
+                restartRadio();
+                break;
+            }
+            case DctConstants.CMD_NET_STAT_POLL: {
+                if (msg.arg1 == DctConstants.ENABLED) {
+                    handleStartNetStatPoll((DctConstants.Activity)msg.obj);
+                } else if (msg.arg1 == DctConstants.DISABLED) {
+                    handleStopNetStatPoll((DctConstants.Activity)msg.obj);
+                }
+                break;
+            }
+            case DctConstants.EVENT_DATA_STATE_CHANGED: {
+                // no longer do anything, but still registered - clean up log
+                // TODO - why are we still registering?
+                break;
+            }
+            case DctConstants.EVENT_PCO_DATA_RECEIVED: {
+                handlePcoData((AsyncResult)msg.obj);
+                break;
+            }
+            case DctConstants.EVENT_SET_CARRIER_DATA_ENABLED:
+                onSetCarrierDataEnabled((AsyncResult) msg.obj);
+                break;
+            case DctConstants.EVENT_DATA_RECONNECT:
+                onDataReconnect(msg.getData());
+                break;
+            default:
+                Rlog.e("DcTracker", "Unhandled event=" + msg);
+                break;
+
+        }
+    }
+
+    private int getApnProfileID(String apnType) {
+        if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_IMS)) {
+            return RILConstants.DATA_PROFILE_IMS;
+        } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_FOTA)) {
+            return RILConstants.DATA_PROFILE_FOTA;
+        } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_CBS)) {
+            return RILConstants.DATA_PROFILE_CBS;
+        } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_IA)) {
+            return RILConstants.DATA_PROFILE_DEFAULT; // DEFAULT for now
+        } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_DUN)) {
+            return RILConstants.DATA_PROFILE_TETHERED;
+        } else {
+            return RILConstants.DATA_PROFILE_DEFAULT;
+        }
+    }
+
+    private int getCellLocationId() {
+        int cid = -1;
+        CellLocation loc = mPhone.getCellLocation();
+
+        if (loc != null) {
+            if (loc instanceof GsmCellLocation) {
+                cid = ((GsmCellLocation)loc).getCid();
+            } else if (loc instanceof CdmaCellLocation) {
+                cid = ((CdmaCellLocation)loc).getBaseStationId();
+            }
+        }
+        return cid;
+    }
+
+    private IccRecords getUiccRecords(int appFamily) {
+        return mUiccController.getIccRecords(mPhone.getPhoneId(), appFamily);
+    }
+
+
+    private void onUpdateIcc() {
+        if (mUiccController == null ) {
+            return;
+        }
+
+        IccRecords newIccRecords = getUiccRecords(UiccController.APP_FAM_3GPP);
+
+        IccRecords r = mIccRecords.get();
+        if (r != newIccRecords) {
+            if (r != null) {
+                log("Removing stale icc objects.");
+                r.unregisterForRecordsLoaded(this);
+                mIccRecords.set(null);
+            }
+            if (newIccRecords != null) {
+                if (SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
+                    log("New records found.");
+                    mIccRecords.set(newIccRecords);
+                    newIccRecords.registerForRecordsLoaded(
+                            this, DctConstants.EVENT_RECORDS_LOADED, null);
+                }
+            } else {
+                onSimNotReady();
+            }
+        }
+    }
+
+    public void update() {
+        log("update sub = " + mPhone.getSubId());
+        log("update(): Active DDS, register for all events now!");
+        onUpdateIcc();
+
+        mAutoAttachOnCreation.set(false);
+
+        ((GsmCdmaPhone)mPhone).updateCurrentCarrierInProvider();
+    }
+
+    public void cleanUpAllConnections(String cause) {
+        cleanUpAllConnections(cause, null);
+    }
+
+    public void updateRecords() {
+        onUpdateIcc();
+    }
+
+    public void cleanUpAllConnections(String cause, Message disconnectAllCompleteMsg) {
+        log("cleanUpAllConnections");
+        if (disconnectAllCompleteMsg != null) {
+            mDisconnectAllCompleteMsgList.add(disconnectAllCompleteMsg);
+        }
+
+        Message msg = obtainMessage(DctConstants.EVENT_CLEAN_UP_ALL_CONNECTIONS);
+        msg.obj = cause;
+        sendMessage(msg);
+    }
+
+    private void notifyDataDisconnectComplete() {
+        log("notifyDataDisconnectComplete");
+        for (Message m: mDisconnectAllCompleteMsgList) {
+            m.sendToTarget();
+        }
+        mDisconnectAllCompleteMsgList.clear();
+    }
+
+
+    private void notifyAllDataDisconnected() {
+        sEnableFailFastRefCounter = 0;
+        mFailFast = false;
+        mAllDataDisconnectedRegistrants.notifyRegistrants();
+    }
+
+    public void registerForAllDataDisconnected(Handler h, int what, Object obj) {
+        mAllDataDisconnectedRegistrants.addUnique(h, what, obj);
+
+        if (isDisconnected()) {
+            log("notify All Data Disconnected");
+            notifyAllDataDisconnected();
+        }
+    }
+
+    public void unregisterForAllDataDisconnected(Handler h) {
+        mAllDataDisconnectedRegistrants.remove(h);
+    }
+
+    public void registerForDataEnabledChanged(Handler h, int what, Object obj) {
+        mDataEnabledSettings.registerForDataEnabledChanged(h, what, obj);
+    }
+
+    public void unregisterForDataEnabledChanged(Handler h) {
+        mDataEnabledSettings.unregisterForDataEnabledChanged(h);
+    }
+
+    private void onSetInternalDataEnabled(boolean enabled, Message onCompleteMsg) {
+        synchronized (mDataEnabledSettings) {
+            if (DBG) log("onSetInternalDataEnabled: enabled=" + enabled);
+            boolean sendOnComplete = true;
+
+            mDataEnabledSettings.setInternalDataEnabled(enabled);
+            if (enabled) {
+                log("onSetInternalDataEnabled: changed to enabled, try to setup data call");
+                onTrySetupData(Phone.REASON_DATA_ENABLED);
+            } else {
+                sendOnComplete = false;
+                log("onSetInternalDataEnabled: changed to disabled, cleanUpAllConnections");
+                cleanUpAllConnections(Phone.REASON_DATA_DISABLED, onCompleteMsg);
+            }
+
+            if (sendOnComplete) {
+                if (onCompleteMsg != null) {
+                    onCompleteMsg.sendToTarget();
+                }
+            }
+        }
+    }
+
+    public boolean setInternalDataEnabled(boolean enable) {
+        return setInternalDataEnabled(enable, null);
+    }
+
+    public boolean setInternalDataEnabled(boolean enable, Message onCompleteMsg) {
+        if (DBG) log("setInternalDataEnabled(" + enable + ")");
+
+        Message msg = obtainMessage(DctConstants.EVENT_SET_INTERNAL_DATA_ENABLE, onCompleteMsg);
+        msg.arg1 = (enable ? DctConstants.ENABLED : DctConstants.DISABLED);
+        sendMessage(msg);
+        return true;
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, "[" + mPhone.getPhoneId() + "]" + s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("DcTracker:");
+        pw.println(" RADIO_TESTS=" + RADIO_TESTS);
+        pw.println(" mDataEnabledSettings=" + mDataEnabledSettings);
+        pw.println(" isDataAllowed=" + isDataAllowed(null));
+        pw.flush();
+        pw.println(" mRequestedApnType=" + mRequestedApnType);
+        pw.println(" mPhone=" + mPhone.getPhoneName());
+        pw.println(" mActivity=" + mActivity);
+        pw.println(" mState=" + mState);
+        pw.println(" mTxPkts=" + mTxPkts);
+        pw.println(" mRxPkts=" + mRxPkts);
+        pw.println(" mNetStatPollPeriod=" + mNetStatPollPeriod);
+        pw.println(" mNetStatPollEnabled=" + mNetStatPollEnabled);
+        pw.println(" mDataStallTxRxSum=" + mDataStallTxRxSum);
+        pw.println(" mDataStallAlarmTag=" + mDataStallAlarmTag);
+        pw.println(" mDataStallDetectionEnabled=" + mDataStallDetectionEnabled);
+        pw.println(" mSentSinceLastRecv=" + mSentSinceLastRecv);
+        pw.println(" mNoRecvPollCount=" + mNoRecvPollCount);
+        pw.println(" mResolver=" + mResolver);
+        pw.println(" mIsWifiConnected=" + mIsWifiConnected);
+        pw.println(" mReconnectIntent=" + mReconnectIntent);
+        pw.println(" mAutoAttachOnCreation=" + mAutoAttachOnCreation.get());
+        pw.println(" mIsScreenOn=" + mIsScreenOn);
+        pw.println(" mUniqueIdGenerator=" + mUniqueIdGenerator);
+        pw.println(" mDataRoamingLeakageLog= ");
+        mDataRoamingLeakageLog.dump(fd, pw, args);
+        pw.flush();
+        pw.println(" ***************************************");
+        DcController dcc = mDcc;
+        if (dcc != null) {
+            dcc.dump(fd, pw, args);
+        } else {
+            pw.println(" mDcc=null");
+        }
+        pw.println(" ***************************************");
+        HashMap<Integer, DataConnection> dcs = mDataConnections;
+        if (dcs != null) {
+            Set<Entry<Integer, DataConnection> > mDcSet = mDataConnections.entrySet();
+            pw.println(" mDataConnections: count=" + mDcSet.size());
+            for (Entry<Integer, DataConnection> entry : mDcSet) {
+                pw.printf(" *** mDataConnection[%d] \n", entry.getKey());
+                entry.getValue().dump(fd, pw, args);
+            }
+        } else {
+            pw.println("mDataConnections=null");
+        }
+        pw.println(" ***************************************");
+        pw.flush();
+        HashMap<String, Integer> apnToDcId = mApnToDataConnectionId;
+        if (apnToDcId != null) {
+            Set<Entry<String, Integer>> apnToDcIdSet = apnToDcId.entrySet();
+            pw.println(" mApnToDataConnectonId size=" + apnToDcIdSet.size());
+            for (Entry<String, Integer> entry : apnToDcIdSet) {
+                pw.printf(" mApnToDataConnectonId[%s]=%d\n", entry.getKey(), entry.getValue());
+            }
+        } else {
+            pw.println("mApnToDataConnectionId=null");
+        }
+        pw.println(" ***************************************");
+        pw.flush();
+        ConcurrentHashMap<String, ApnContext> apnCtxs = mApnContexts;
+        if (apnCtxs != null) {
+            Set<Entry<String, ApnContext>> apnCtxsSet = apnCtxs.entrySet();
+            pw.println(" mApnContexts size=" + apnCtxsSet.size());
+            for (Entry<String, ApnContext> entry : apnCtxsSet) {
+                entry.getValue().dump(fd, pw, args);
+            }
+            pw.println(" ***************************************");
+        } else {
+            pw.println(" mApnContexts=null");
+        }
+        pw.flush();
+        ArrayList<ApnSetting> apnSettings = mAllApnSettings;
+        if (apnSettings != null) {
+            pw.println(" mAllApnSettings size=" + apnSettings.size());
+            for (int i=0; i < apnSettings.size(); i++) {
+                pw.printf(" mAllApnSettings[%d]: %s\n", i, apnSettings.get(i));
+            }
+            pw.flush();
+        } else {
+            pw.println(" mAllApnSettings=null");
+        }
+        pw.println(" mPreferredApn=" + mPreferredApn);
+        pw.println(" mIsPsRestricted=" + mIsPsRestricted);
+        pw.println(" mIsDisposed=" + mIsDisposed);
+        pw.println(" mIntentReceiver=" + mIntentReceiver);
+        pw.println(" mReregisterOnReconnectFailure=" + mReregisterOnReconnectFailure);
+        pw.println(" canSetPreferApn=" + mCanSetPreferApn);
+        pw.println(" mApnObserver=" + mApnObserver);
+        pw.println(" getOverallState=" + getOverallState());
+        pw.println(" mDataConnectionAsyncChannels=%s\n" + mDataConnectionAcHashMap);
+        pw.println(" mAttached=" + mAttached.get());
+        mDataEnabledSettings.dump(fd, pw, args);
+        pw.flush();
+    }
+
+    public String[] getPcscfAddress(String apnType) {
+        log("getPcscfAddress()");
+        ApnContext apnContext = null;
+
+        if(apnType == null){
+            log("apnType is null, return null");
+            return null;
+        }
+
+        if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_EMERGENCY)) {
+            apnContext = mApnContextsById.get(DctConstants.APN_EMERGENCY_ID);
+        } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_IMS)) {
+            apnContext = mApnContextsById.get(DctConstants.APN_IMS_ID);
+        } else {
+            log("apnType is invalid, return null");
+            return null;
+        }
+
+        if (apnContext == null) {
+            log("apnContext is null, return null");
+            return null;
+        }
+
+        DcAsyncChannel dcac = apnContext.getDcAc();
+        String[] result = null;
+
+        if (dcac != null) {
+            result = dcac.getPcscfAddr();
+
+            for (int i = 0; i < result.length; i++) {
+                log("Pcscf[" + i + "]: " + result[i]);
+            }
+            return result;
+        }
+        return null;
+    }
+
+    /**
+     * Read APN configuration from Telephony.db for Emergency APN
+     * All opertors recognize the connection request for EPDN based on APN type
+     * PLMN name,APN name are not mandatory parameters
+     */
+    private void initEmergencyApnSetting() {
+        // Operator Numeric is not available when sim records are not loaded.
+        // Query Telephony.db with APN type as EPDN request does not
+        // require APN name, plmn and all operators support same APN config.
+        // DB will contain only one entry for Emergency APN
+        String selection = "type=\"emergency\"";
+        Cursor cursor = mPhone.getContext().getContentResolver().query(
+                Telephony.Carriers.CONTENT_URI, null, selection, null, null);
+
+        if (cursor != null) {
+            if (cursor.getCount() > 0) {
+                if (cursor.moveToFirst()) {
+                    mEmergencyApn = makeApnSetting(cursor);
+                }
+            }
+            cursor.close();
+        }
+    }
+
+    /**
+     * Add the Emergency APN settings to APN settings list
+     */
+    private void addEmergencyApnSetting() {
+        if(mEmergencyApn != null) {
+            if(mAllApnSettings == null) {
+                mAllApnSettings = new ArrayList<ApnSetting>();
+            } else {
+                boolean hasEmergencyApn = false;
+                for (ApnSetting apn : mAllApnSettings) {
+                    if (ArrayUtils.contains(apn.types, PhoneConstants.APN_TYPE_EMERGENCY)) {
+                        hasEmergencyApn = true;
+                        break;
+                    }
+                }
+
+                if(hasEmergencyApn == false) {
+                    mAllApnSettings.add(mEmergencyApn);
+                } else {
+                    log("addEmergencyApnSetting - E-APN setting is already present");
+                }
+            }
+        }
+    }
+
+    private boolean containsAllApns(ArrayList<ApnSetting> oldApnList,
+                                    ArrayList<ApnSetting> newApnList) {
+        for (ApnSetting newApnSetting : newApnList) {
+            boolean canHandle = false;
+            for (ApnSetting oldApnSetting : oldApnList) {
+                // Make sure at least one of the APN from old list can cover the new APN
+                if (oldApnSetting.equals(newApnSetting,
+                        mPhone.getServiceState().getDataRoamingFromRegistration())) {
+                    canHandle = true;
+                    break;
+                }
+            }
+            if (!canHandle) return false;
+        }
+        return true;
+    }
+
+    private void cleanUpConnectionsOnUpdatedApns(boolean tearDown, String reason) {
+        if (DBG) log("cleanUpConnectionsOnUpdatedApns: tearDown=" + tearDown);
+        if (mAllApnSettings != null && mAllApnSettings.isEmpty()) {
+            cleanUpAllConnections(tearDown, Phone.REASON_APN_CHANGED);
+        } else {
+            int radioTech = mPhone.getServiceState().getRilDataRadioTechnology();
+            if (radioTech == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN) {
+                // unknown rat is an exception for data rat change. Its only received when out of
+                // service and is not applicable for apn bearer bitmask. We should bypass the check
+                // of waiting apn list and keep the data connection on.
+                return;
+            }
+            for (ApnContext apnContext : mApnContexts.values()) {
+                ArrayList<ApnSetting> currentWaitingApns = apnContext.getWaitingApns();
+                ArrayList<ApnSetting> waitingApns = buildWaitingApns(
+                        apnContext.getApnType(),
+                        mPhone.getServiceState().getRilDataRadioTechnology());
+                if (VDBG) log("new waitingApns:" + waitingApns);
+                if ((currentWaitingApns != null)
+                        && ((waitingApns.size() != currentWaitingApns.size())
+                        // Check if the existing waiting APN list can cover the newly built APN
+                        // list. If yes, then we don't need to tear down the existing data call.
+                        // TODO: We probably need to rebuild APN list when roaming status changes.
+                        || !containsAllApns(currentWaitingApns, waitingApns))) {
+                    if (VDBG) log("new waiting apn is different for " + apnContext);
+                    apnContext.setWaitingApns(waitingApns);
+                    if (!apnContext.isDisconnected()) {
+                        if (VDBG) log("cleanUpConnectionsOnUpdatedApns for " + apnContext);
+                        apnContext.setReason(reason);
+                        cleanUpConnection(true, apnContext);
+                    }
+                }
+            }
+        }
+
+        if (!isConnected()) {
+            stopNetStatPoll();
+            stopDataStallAlarm();
+        }
+
+        mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT;
+
+        if (DBG) log("mDisconnectPendingCount = " + mDisconnectPendingCount);
+        if (tearDown && mDisconnectPendingCount == 0) {
+            notifyDataDisconnectComplete();
+            notifyAllDataDisconnected();
+        }
+    }
+
+    /**
+     * Polling stuff
+     */
+    private void resetPollStats() {
+        mTxPkts = -1;
+        mRxPkts = -1;
+        mNetStatPollPeriod = POLL_NETSTAT_MILLIS;
+    }
+
+    private void startNetStatPoll() {
+        if (getOverallState() == DctConstants.State.CONNECTED
+                && mNetStatPollEnabled == false) {
+            if (DBG) {
+                log("startNetStatPoll");
+            }
+            resetPollStats();
+            mNetStatPollEnabled = true;
+            mPollNetStat.run();
+        }
+        if (mPhone != null) {
+            mPhone.notifyDataActivity();
+        }
+    }
+
+    private void stopNetStatPoll() {
+        mNetStatPollEnabled = false;
+        removeCallbacks(mPollNetStat);
+        if (DBG) {
+            log("stopNetStatPoll");
+        }
+
+        // To sync data activity icon in the case of switching data connection to send MMS.
+        if (mPhone != null) {
+            mPhone.notifyDataActivity();
+        }
+    }
+
+    public void sendStartNetStatPoll(DctConstants.Activity activity) {
+        Message msg = obtainMessage(DctConstants.CMD_NET_STAT_POLL);
+        msg.arg1 = DctConstants.ENABLED;
+        msg.obj = activity;
+        sendMessage(msg);
+    }
+
+    private void handleStartNetStatPoll(DctConstants.Activity activity) {
+        startNetStatPoll();
+        startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+        setActivity(activity);
+    }
+
+    public void sendStopNetStatPoll(DctConstants.Activity activity) {
+        Message msg = obtainMessage(DctConstants.CMD_NET_STAT_POLL);
+        msg.arg1 = DctConstants.DISABLED;
+        msg.obj = activity;
+        sendMessage(msg);
+    }
+
+    private void handleStopNetStatPoll(DctConstants.Activity activity) {
+        stopNetStatPoll();
+        stopDataStallAlarm();
+        setActivity(activity);
+    }
+
+    private void updateDataActivity() {
+        long sent, received;
+
+        DctConstants.Activity newActivity;
+
+        TxRxSum preTxRxSum = new TxRxSum(mTxPkts, mRxPkts);
+        TxRxSum curTxRxSum = new TxRxSum();
+        curTxRxSum.updateTxRxSum();
+        mTxPkts = curTxRxSum.txPkts;
+        mRxPkts = curTxRxSum.rxPkts;
+
+        if (VDBG) {
+            log("updateDataActivity: curTxRxSum=" + curTxRxSum + " preTxRxSum=" + preTxRxSum);
+        }
+
+        if (mNetStatPollEnabled && (preTxRxSum.txPkts > 0 || preTxRxSum.rxPkts > 0)) {
+            sent = mTxPkts - preTxRxSum.txPkts;
+            received = mRxPkts - preTxRxSum.rxPkts;
+
+            if (VDBG)
+                log("updateDataActivity: sent=" + sent + " received=" + received);
+            if (sent > 0 && received > 0) {
+                newActivity = DctConstants.Activity.DATAINANDOUT;
+            } else if (sent > 0 && received == 0) {
+                newActivity = DctConstants.Activity.DATAOUT;
+            } else if (sent == 0 && received > 0) {
+                newActivity = DctConstants.Activity.DATAIN;
+            } else {
+                newActivity = (mActivity == DctConstants.Activity.DORMANT) ?
+                        mActivity : DctConstants.Activity.NONE;
+            }
+
+            if (mActivity != newActivity && mIsScreenOn) {
+                if (VDBG)
+                    log("updateDataActivity: newActivity=" + newActivity);
+                mActivity = newActivity;
+                mPhone.notifyDataActivity();
+            }
+        }
+    }
+
+    private void handlePcoData(AsyncResult ar) {
+        if (ar.exception != null) {
+            Rlog.e(LOG_TAG, "PCO_DATA exception: " + ar.exception);
+            return;
+        }
+        PcoData pcoData = (PcoData)(ar.result);
+        ArrayList<DataConnection> dcList = new ArrayList<>();
+        DataConnection temp = mDcc.getActiveDcByCid(pcoData.cid);
+        if (temp != null) {
+            dcList.add(temp);
+        }
+        if (dcList.size() == 0) {
+            Rlog.e(LOG_TAG, "PCO_DATA for unknown cid: " + pcoData.cid + ", inferring");
+            for (DataConnection dc : mDataConnections.values()) {
+                final int cid = dc.getCid();
+                if (cid == pcoData.cid) {
+                    if (VDBG) Rlog.d(LOG_TAG, "  found " + dc);
+                    dcList.clear();
+                    dcList.add(dc);
+                    break;
+                }
+                // check if this dc is still connecting
+                if (cid == -1) {
+                    for (ApnContext apnContext : dc.mApnContexts.keySet()) {
+                        if (apnContext.getState() == DctConstants.State.CONNECTING) {
+                            if (VDBG) Rlog.d(LOG_TAG, "  found potential " + dc);
+                            dcList.add(dc);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        if (dcList.size() == 0) {
+            Rlog.e(LOG_TAG, "PCO_DATA - couldn't infer cid");
+            return;
+        }
+        for (DataConnection dc : dcList) {
+            if (dc.mApnContexts.size() == 0) {
+                break;
+            }
+            // send one out for each apn type in play
+            for (ApnContext apnContext : dc.mApnContexts.keySet()) {
+                String apnType = apnContext.getApnType();
+
+                final Intent intent = new Intent(TelephonyIntents.ACTION_CARRIER_SIGNAL_PCO_VALUE);
+                intent.putExtra(TelephonyIntents.EXTRA_APN_TYPE_KEY, apnType);
+                intent.putExtra(TelephonyIntents.EXTRA_APN_PROTO_KEY, pcoData.bearerProto);
+                intent.putExtra(TelephonyIntents.EXTRA_PCO_ID_KEY, pcoData.pcoId);
+                intent.putExtra(TelephonyIntents.EXTRA_PCO_VALUE_KEY, pcoData.contents);
+                mPhone.getCarrierSignalAgent().notifyCarrierSignalReceivers(intent);
+            }
+        }
+    }
+
+    /**
+     * Data-Stall
+     */
+    // Recovery action taken in case of data stall
+    private static class RecoveryAction {
+        public static final int GET_DATA_CALL_LIST      = 0;
+        public static final int CLEANUP                 = 1;
+        public static final int REREGISTER              = 2;
+        public static final int RADIO_RESTART           = 3;
+        public static final int RADIO_RESTART_WITH_PROP = 4;
+
+        private static boolean isAggressiveRecovery(int value) {
+            return ((value == RecoveryAction.CLEANUP) ||
+                    (value == RecoveryAction.REREGISTER) ||
+                    (value == RecoveryAction.RADIO_RESTART) ||
+                    (value == RecoveryAction.RADIO_RESTART_WITH_PROP));
+        }
+    }
+
+    private int getRecoveryAction() {
+        int action = Settings.System.getInt(mResolver,
+                "radio.data.stall.recovery.action", RecoveryAction.GET_DATA_CALL_LIST);
+        if (VDBG_STALL) log("getRecoveryAction: " + action);
+        return action;
+    }
+
+    private void putRecoveryAction(int action) {
+        Settings.System.putInt(mResolver, "radio.data.stall.recovery.action", action);
+        if (VDBG_STALL) log("putRecoveryAction: " + action);
+    }
+
+    private void doRecovery() {
+        if (getOverallState() == DctConstants.State.CONNECTED) {
+            // Go through a series of recovery steps, each action transitions to the next action
+            final int recoveryAction = getRecoveryAction();
+            TelephonyMetrics.getInstance().writeDataStallEvent(mPhone.getPhoneId(), recoveryAction);
+            switch (recoveryAction) {
+            case RecoveryAction.GET_DATA_CALL_LIST:
+                EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_GET_DATA_CALL_LIST,
+                        mSentSinceLastRecv);
+                if (DBG) log("doRecovery() get data call list");
+                mPhone.mCi.getDataCallList(obtainMessage(DctConstants.EVENT_DATA_STATE_CHANGED));
+                putRecoveryAction(RecoveryAction.CLEANUP);
+                break;
+            case RecoveryAction.CLEANUP:
+                EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_CLEANUP, mSentSinceLastRecv);
+                if (DBG) log("doRecovery() cleanup all connections");
+                cleanUpAllConnections(Phone.REASON_PDP_RESET);
+                putRecoveryAction(RecoveryAction.REREGISTER);
+                break;
+            case RecoveryAction.REREGISTER:
+                EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_REREGISTER,
+                        mSentSinceLastRecv);
+                if (DBG) log("doRecovery() re-register");
+                mPhone.getServiceStateTracker().reRegisterNetwork(null);
+                putRecoveryAction(RecoveryAction.RADIO_RESTART);
+                break;
+            case RecoveryAction.RADIO_RESTART:
+                EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_RADIO_RESTART,
+                        mSentSinceLastRecv);
+                if (DBG) log("restarting radio");
+                putRecoveryAction(RecoveryAction.RADIO_RESTART_WITH_PROP);
+                restartRadio();
+                break;
+            case RecoveryAction.RADIO_RESTART_WITH_PROP:
+                // This is in case radio restart has not recovered the data.
+                // It will set an additional "gsm.radioreset" property to tell
+                // RIL or system to take further action.
+                // The implementation of hard reset recovery action is up to OEM product.
+                // Once RADIO_RESET property is consumed, it is expected to set back
+                // to false by RIL.
+                EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_RADIO_RESTART_WITH_PROP, -1);
+                if (DBG) log("restarting radio with gsm.radioreset to true");
+                SystemProperties.set(RADIO_RESET_PROPERTY, "true");
+                // give 1 sec so property change can be notified.
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException e) {}
+                restartRadio();
+                putRecoveryAction(RecoveryAction.GET_DATA_CALL_LIST);
+                break;
+            default:
+                throw new RuntimeException("doRecovery: Invalid recoveryAction=" +
+                    recoveryAction);
+            }
+            mSentSinceLastRecv = 0;
+        }
+    }
+
+    private void updateDataStallInfo() {
+        long sent, received;
+
+        TxRxSum preTxRxSum = new TxRxSum(mDataStallTxRxSum);
+        mDataStallTxRxSum.updateTxRxSum();
+
+        if (VDBG_STALL) {
+            log("updateDataStallInfo: mDataStallTxRxSum=" + mDataStallTxRxSum +
+                    " preTxRxSum=" + preTxRxSum);
+        }
+
+        sent = mDataStallTxRxSum.txPkts - preTxRxSum.txPkts;
+        received = mDataStallTxRxSum.rxPkts - preTxRxSum.rxPkts;
+
+        if (RADIO_TESTS) {
+            if (SystemProperties.getBoolean("radio.test.data.stall", false)) {
+                log("updateDataStallInfo: radio.test.data.stall true received = 0;");
+                received = 0;
+            }
+        }
+        if ( sent > 0 && received > 0 ) {
+            if (VDBG_STALL) log("updateDataStallInfo: IN/OUT");
+            mSentSinceLastRecv = 0;
+            putRecoveryAction(RecoveryAction.GET_DATA_CALL_LIST);
+        } else if (sent > 0 && received == 0) {
+            if (isPhoneStateIdle()) {
+                mSentSinceLastRecv += sent;
+            } else {
+                mSentSinceLastRecv = 0;
+            }
+            if (DBG) {
+                log("updateDataStallInfo: OUT sent=" + sent +
+                        " mSentSinceLastRecv=" + mSentSinceLastRecv);
+            }
+        } else if (sent == 0 && received > 0) {
+            if (VDBG_STALL) log("updateDataStallInfo: IN");
+            mSentSinceLastRecv = 0;
+            putRecoveryAction(RecoveryAction.GET_DATA_CALL_LIST);
+        } else {
+            if (VDBG_STALL) log("updateDataStallInfo: NONE");
+        }
+    }
+
+    private boolean isPhoneStateIdle() {
+        for (int i = 0; i < TelephonyManager.getDefault().getPhoneCount(); i++) {
+            Phone phone = PhoneFactory.getPhone(i);
+            if (phone != null && phone.getState() != PhoneConstants.State.IDLE) {
+                log("isPhoneStateIdle false: Voice call active on phone " + i);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void onDataStallAlarm(int tag) {
+        if (mDataStallAlarmTag != tag) {
+            if (DBG) {
+                log("onDataStallAlarm: ignore, tag=" + tag + " expecting " + mDataStallAlarmTag);
+            }
+            return;
+        }
+        updateDataStallInfo();
+
+        int hangWatchdogTrigger = Settings.Global.getInt(mResolver,
+                Settings.Global.PDP_WATCHDOG_TRIGGER_PACKET_COUNT,
+                NUMBER_SENT_PACKETS_OF_HANG);
+
+        boolean suspectedStall = DATA_STALL_NOT_SUSPECTED;
+        if (mSentSinceLastRecv >= hangWatchdogTrigger) {
+            if (DBG) {
+                log("onDataStallAlarm: tag=" + tag + " do recovery action=" + getRecoveryAction());
+            }
+            suspectedStall = DATA_STALL_SUSPECTED;
+            sendMessage(obtainMessage(DctConstants.EVENT_DO_RECOVERY));
+        } else {
+            if (VDBG_STALL) {
+                log("onDataStallAlarm: tag=" + tag + " Sent " + String.valueOf(mSentSinceLastRecv) +
+                    " pkts since last received, < watchdogTrigger=" + hangWatchdogTrigger);
+            }
+        }
+        startDataStallAlarm(suspectedStall);
+    }
+
+    private void startDataStallAlarm(boolean suspectedStall) {
+        int nextAction = getRecoveryAction();
+        int delayInMs;
+
+        if (mDataStallDetectionEnabled && getOverallState() == DctConstants.State.CONNECTED) {
+            // If screen is on or data stall is currently suspected, set the alarm
+            // with an aggressive timeout.
+            if (mIsScreenOn || suspectedStall || RecoveryAction.isAggressiveRecovery(nextAction)) {
+                delayInMs = Settings.Global.getInt(mResolver,
+                        Settings.Global.DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS,
+                        DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS_DEFAULT);
+            } else {
+                delayInMs = Settings.Global.getInt(mResolver,
+                        Settings.Global.DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS,
+                        DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS_DEFAULT);
+            }
+
+            mDataStallAlarmTag += 1;
+            if (VDBG_STALL) {
+                log("startDataStallAlarm: tag=" + mDataStallAlarmTag +
+                        " delay=" + (delayInMs / 1000) + "s");
+            }
+            Intent intent = new Intent(INTENT_DATA_STALL_ALARM);
+            intent.putExtra(DATA_STALL_ALARM_TAG_EXTRA, mDataStallAlarmTag);
+            mDataStallAlarmIntent = PendingIntent.getBroadcast(mPhone.getContext(), 0, intent,
+                    PendingIntent.FLAG_UPDATE_CURRENT);
+            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME,
+                    SystemClock.elapsedRealtime() + delayInMs, mDataStallAlarmIntent);
+        } else {
+            if (VDBG_STALL) {
+                log("startDataStallAlarm: NOT started, no connection tag=" + mDataStallAlarmTag);
+            }
+        }
+    }
+
+    private void stopDataStallAlarm() {
+        if (VDBG_STALL) {
+            log("stopDataStallAlarm: current tag=" + mDataStallAlarmTag +
+                    " mDataStallAlarmIntent=" + mDataStallAlarmIntent);
+        }
+        mDataStallAlarmTag += 1;
+        if (mDataStallAlarmIntent != null) {
+            mAlarmManager.cancel(mDataStallAlarmIntent);
+            mDataStallAlarmIntent = null;
+        }
+    }
+
+    private void restartDataStallAlarm() {
+        if (isConnected() == false) return;
+        // To be called on screen status change.
+        // Do not cancel the alarm if it is set with aggressive timeout.
+        int nextAction = getRecoveryAction();
+
+        if (RecoveryAction.isAggressiveRecovery(nextAction)) {
+            if (DBG) log("restartDataStallAlarm: action is pending. not resetting the alarm.");
+            return;
+        }
+        if (VDBG_STALL) log("restartDataStallAlarm: stop then start.");
+        stopDataStallAlarm();
+        startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
+    }
+
+    /**
+     * Provisioning APN
+     */
+    private void onActionIntentProvisioningApnAlarm(Intent intent) {
+        if (DBG) log("onActionIntentProvisioningApnAlarm: action=" + intent.getAction());
+        Message msg = obtainMessage(DctConstants.EVENT_PROVISIONING_APN_ALARM,
+                intent.getAction());
+        msg.arg1 = intent.getIntExtra(PROVISIONING_APN_ALARM_TAG_EXTRA, 0);
+        sendMessage(msg);
+    }
+
+    private void startProvisioningApnAlarm() {
+        int delayInMs = Settings.Global.getInt(mResolver,
+                                Settings.Global.PROVISIONING_APN_ALARM_DELAY_IN_MS,
+                                PROVISIONING_APN_ALARM_DELAY_IN_MS_DEFAULT);
+        if (Build.IS_DEBUGGABLE) {
+            // Allow debug code to use a system property to provide another value
+            String delayInMsStrg = Integer.toString(delayInMs);
+            delayInMsStrg = System.getProperty(DEBUG_PROV_APN_ALARM, delayInMsStrg);
+            try {
+                delayInMs = Integer.parseInt(delayInMsStrg);
+            } catch (NumberFormatException e) {
+                loge("startProvisioningApnAlarm: e=" + e);
+            }
+        }
+        mProvisioningApnAlarmTag += 1;
+        if (DBG) {
+            log("startProvisioningApnAlarm: tag=" + mProvisioningApnAlarmTag +
+                    " delay=" + (delayInMs / 1000) + "s");
+        }
+        Intent intent = new Intent(INTENT_PROVISIONING_APN_ALARM);
+        intent.putExtra(PROVISIONING_APN_ALARM_TAG_EXTRA, mProvisioningApnAlarmTag);
+        mProvisioningApnAlarmIntent = PendingIntent.getBroadcast(mPhone.getContext(), 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                SystemClock.elapsedRealtime() + delayInMs, mProvisioningApnAlarmIntent);
+    }
+
+    private void stopProvisioningApnAlarm() {
+        if (DBG) {
+            log("stopProvisioningApnAlarm: current tag=" + mProvisioningApnAlarmTag +
+                    " mProvsioningApnAlarmIntent=" + mProvisioningApnAlarmIntent);
+        }
+        mProvisioningApnAlarmTag += 1;
+        if (mProvisioningApnAlarmIntent != null) {
+            mAlarmManager.cancel(mProvisioningApnAlarmIntent);
+            mProvisioningApnAlarmIntent = null;
+        }
+    }
+
+}
diff --git a/com/android/internal/telephony/dataconnection/TelephonyNetworkFactory.java b/com/android/internal/telephony/dataconnection/TelephonyNetworkFactory.java
new file mode 100644
index 0000000..114a4b4
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/TelephonyNetworkFactory.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.dataconnection;
+
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkFactory;
+import android.net.NetworkRequest;
+import android.net.StringNetworkSpecifier;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.util.LocalLog;
+
+import com.android.internal.telephony.PhoneSwitcher;
+import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.SubscriptionMonitor;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashMap;
+
+public class TelephonyNetworkFactory extends NetworkFactory {
+    public final String LOG_TAG;
+    protected static final boolean DBG = true;
+
+    private final PhoneSwitcher mPhoneSwitcher;
+    private final SubscriptionController mSubscriptionController;
+    private final SubscriptionMonitor mSubscriptionMonitor;
+    private final DcTracker mDcTracker;
+
+    private final HashMap<NetworkRequest, LocalLog> mDefaultRequests =
+            new HashMap<NetworkRequest, LocalLog>();
+    private final HashMap<NetworkRequest, LocalLog> mSpecificRequests =
+            new HashMap<NetworkRequest, LocalLog>();
+
+    private int mPhoneId;
+    private boolean mIsActive;
+    private boolean mIsDefault;
+    private int mSubscriptionId;
+
+    private final static int TELEPHONY_NETWORK_SCORE = 50;
+
+    private final Handler mInternalHandler;
+    private static final int EVENT_ACTIVE_PHONE_SWITCH          = 1;
+    private static final int EVENT_SUBSCRIPTION_CHANGED         = 2;
+    private static final int EVENT_DEFAULT_SUBSCRIPTION_CHANGED = 3;
+    private static final int EVENT_NETWORK_REQUEST              = 4;
+    private static final int EVENT_NETWORK_RELEASE              = 5;
+
+    public TelephonyNetworkFactory(PhoneSwitcher phoneSwitcher,
+            SubscriptionController subscriptionController, SubscriptionMonitor subscriptionMonitor,
+            Looper looper, Context context, int phoneId, DcTracker dcTracker) {
+        super(looper, context, "TelephonyNetworkFactory[" + phoneId + "]", null);
+        mInternalHandler = new InternalHandler(looper);
+
+        setCapabilityFilter(makeNetworkFilter(subscriptionController, phoneId));
+        setScoreFilter(TELEPHONY_NETWORK_SCORE);
+
+        mPhoneSwitcher = phoneSwitcher;
+        mSubscriptionController = subscriptionController;
+        mSubscriptionMonitor = subscriptionMonitor;
+        mPhoneId = phoneId;
+        LOG_TAG = "TelephonyNetworkFactory[" + phoneId + "]";
+        mDcTracker = dcTracker;
+
+        mIsActive = false;
+        mPhoneSwitcher.registerForActivePhoneSwitch(mPhoneId, mInternalHandler,
+                EVENT_ACTIVE_PHONE_SWITCH, null);
+
+        mSubscriptionId = INVALID_SUBSCRIPTION_ID;
+        mSubscriptionMonitor.registerForSubscriptionChanged(mPhoneId, mInternalHandler,
+                EVENT_SUBSCRIPTION_CHANGED, null);
+
+        mIsDefault = false;
+        mSubscriptionMonitor.registerForDefaultDataSubscriptionChanged(mPhoneId, mInternalHandler,
+                EVENT_DEFAULT_SUBSCRIPTION_CHANGED, null);
+
+        register();
+    }
+
+    private NetworkCapabilities makeNetworkFilter(SubscriptionController subscriptionController,
+            int phoneId) {
+        final int subscriptionId = subscriptionController.getSubIdUsingPhoneId(phoneId);
+        return makeNetworkFilter(subscriptionId);
+    }
+
+    private NetworkCapabilities makeNetworkFilter(int subscriptionId) {
+        NetworkCapabilities nc = new NetworkCapabilities();
+        nc.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_SUPL);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_FOTA);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_CBS);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_IA);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_RCS);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_XCAP);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_EIMS);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        nc.setNetworkSpecifier(new StringNetworkSpecifier(String.valueOf(subscriptionId)));
+        return nc;
+    }
+
+    private class InternalHandler extends Handler {
+        public InternalHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_ACTIVE_PHONE_SWITCH: {
+                    onActivePhoneSwitch();
+                    break;
+                }
+                case EVENT_SUBSCRIPTION_CHANGED: {
+                    onSubIdChange();
+                    break;
+                }
+                case EVENT_DEFAULT_SUBSCRIPTION_CHANGED: {
+                    onDefaultChange();
+                    break;
+                }
+                case EVENT_NETWORK_REQUEST: {
+                    onNeedNetworkFor(msg);
+                    break;
+                }
+                case EVENT_NETWORK_RELEASE: {
+                    onReleaseNetworkFor(msg);
+                    break;
+                }
+            }
+        }
+    }
+
+    private static final int REQUEST_LOG_SIZE = 40;
+    private static final boolean REQUEST = true;
+    private static final boolean RELEASE = false;
+
+    private void applyRequests(HashMap<NetworkRequest, LocalLog> requestMap, boolean action,
+            String logStr) {
+        for (NetworkRequest networkRequest : requestMap.keySet()) {
+            LocalLog localLog = requestMap.get(networkRequest);
+            localLog.log(logStr);
+            if (action == REQUEST) {
+                mDcTracker.requestNetwork(networkRequest, localLog);
+            } else {
+                mDcTracker.releaseNetwork(networkRequest, localLog);
+            }
+        }
+    }
+
+    // apply or revoke requests if our active-ness changes
+    private void onActivePhoneSwitch() {
+        final boolean newIsActive = mPhoneSwitcher.isPhoneActive(mPhoneId);
+        if (mIsActive != newIsActive) {
+            mIsActive = newIsActive;
+            String logString = "onActivePhoneSwitch(" + mIsActive + ", " + mIsDefault + ")";
+            if (DBG) log(logString);
+            if (mIsDefault) {
+                applyRequests(mDefaultRequests, (mIsActive ? REQUEST : RELEASE), logString);
+            }
+            applyRequests(mSpecificRequests, (mIsActive ? REQUEST : RELEASE), logString);
+        }
+    }
+
+    // watch for phone->subId changes, reapply new filter and let
+    // that flow through to apply/revoke of requests
+    private void onSubIdChange() {
+        final int newSubscriptionId = mSubscriptionController.getSubIdUsingPhoneId(mPhoneId);
+        if (mSubscriptionId != newSubscriptionId) {
+            if (DBG) log("onSubIdChange " + mSubscriptionId + "->" + newSubscriptionId);
+            mSubscriptionId = newSubscriptionId;
+            setCapabilityFilter(makeNetworkFilter(mSubscriptionId));
+        }
+    }
+
+    // watch for default-data changes (could be side effect of
+    // phoneId->subId map change or direct change of default subId)
+    // and apply/revoke default-only requests.
+    private void onDefaultChange() {
+        final int newDefaultSubscriptionId = mSubscriptionController.getDefaultDataSubId();
+        final boolean newIsDefault = (newDefaultSubscriptionId == mSubscriptionId);
+        if (newIsDefault != mIsDefault) {
+            mIsDefault = newIsDefault;
+            String logString = "onDefaultChange(" + mIsActive + "," + mIsDefault + ")";
+            if (DBG) log(logString);
+            if (mIsActive == false) return;
+            applyRequests(mDefaultRequests, (mIsDefault ? REQUEST : RELEASE), logString);
+        }
+    }
+
+    @Override
+    public void needNetworkFor(NetworkRequest networkRequest, int score) {
+        Message msg = mInternalHandler.obtainMessage(EVENT_NETWORK_REQUEST);
+        msg.obj = networkRequest;
+        msg.sendToTarget();
+    }
+
+    private void onNeedNetworkFor(Message msg) {
+        NetworkRequest networkRequest = (NetworkRequest)msg.obj;
+        boolean isApplicable = false;
+        LocalLog localLog = null;
+        if (networkRequest.networkCapabilities.getNetworkSpecifier() == null) {
+            // request only for the default network
+            localLog = mDefaultRequests.get(networkRequest);
+            if (localLog == null) {
+                localLog = new LocalLog(REQUEST_LOG_SIZE);
+                localLog.log("created for " + networkRequest);
+                mDefaultRequests.put(networkRequest, localLog);
+                isApplicable = mIsDefault;
+            }
+        } else {
+            localLog = mSpecificRequests.get(networkRequest);
+            if (localLog == null) {
+                localLog = new LocalLog(REQUEST_LOG_SIZE);
+                mSpecificRequests.put(networkRequest, localLog);
+                isApplicable = true;
+            }
+        }
+        if (mIsActive && isApplicable) {
+            String s = "onNeedNetworkFor";
+            localLog.log(s);
+            log(s + " " + networkRequest);
+            mDcTracker.requestNetwork(networkRequest, localLog);
+        } else {
+            String s = "not acting - isApp=" + isApplicable + ", isAct=" + mIsActive;
+            localLog.log(s);
+            log(s + " " + networkRequest);
+        }
+    }
+
+    @Override
+    public void releaseNetworkFor(NetworkRequest networkRequest) {
+        Message msg = mInternalHandler.obtainMessage(EVENT_NETWORK_RELEASE);
+        msg.obj = networkRequest;
+        msg.sendToTarget();
+    }
+
+    private void onReleaseNetworkFor(Message msg) {
+        NetworkRequest networkRequest = (NetworkRequest)msg.obj;
+        LocalLog localLog = null;
+        boolean isApplicable = false;
+        if (networkRequest.networkCapabilities.getNetworkSpecifier() == null) {
+            // request only for the default network
+            localLog = mDefaultRequests.remove(networkRequest);
+            isApplicable = (localLog != null) && mIsDefault;
+        } else {
+            localLog = mSpecificRequests.remove(networkRequest);
+            isApplicable = (localLog != null);
+        }
+        if (mIsActive && isApplicable) {
+            String s = "onReleaseNetworkFor";
+            localLog.log(s);
+            log(s + " " + networkRequest);
+            mDcTracker.releaseNetwork(networkRequest, localLog);
+        } else {
+            String s = "not releasing - isApp=" + isApplicable + ", isAct=" + mIsActive;
+            localLog.log(s);
+            log(s + " " + networkRequest);
+        }
+    }
+
+    protected void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        pw.println(LOG_TAG + " mSubId=" + mSubscriptionId + " mIsActive=" +
+                mIsActive + " mIsDefault=" + mIsDefault);
+        pw.println("Default Requests:");
+        pw.increaseIndent();
+        for (NetworkRequest nr : mDefaultRequests.keySet()) {
+            pw.println(nr);
+            pw.increaseIndent();
+            mDefaultRequests.get(nr).dump(fd, pw, args);
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/com/android/internal/telephony/euicc/EuiccConnector.java b/com/android/internal/telephony/euicc/EuiccConnector.java
new file mode 100644
index 0000000..5e07eed
--- /dev/null
+++ b/com/android/internal/telephony/euicc/EuiccConnector.java
@@ -0,0 +1,1035 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.euicc;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.service.euicc.EuiccService;
+import android.service.euicc.GetDefaultDownloadableSubscriptionListResult;
+import android.service.euicc.GetDownloadableSubscriptionMetadataResult;
+import android.service.euicc.GetEuiccProfileInfoListResult;
+import android.service.euicc.IDeleteSubscriptionCallback;
+import android.service.euicc.IDownloadSubscriptionCallback;
+import android.service.euicc.IEraseSubscriptionsCallback;
+import android.service.euicc.IEuiccService;
+import android.service.euicc.IGetDefaultDownloadableSubscriptionListCallback;
+import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback;
+import android.service.euicc.IGetEidCallback;
+import android.service.euicc.IGetEuiccInfoCallback;
+import android.service.euicc.IGetEuiccProfileInfoListCallback;
+import android.service.euicc.IRetainSubscriptionsForFactoryResetCallback;
+import android.service.euicc.ISwitchToSubscriptionCallback;
+import android.service.euicc.IUpdateSubscriptionNicknameCallback;
+import android.telephony.SubscriptionManager;
+import android.telephony.euicc.DownloadableSubscription;
+import android.telephony.euicc.EuiccInfo;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.util.IState;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * State machine which maintains the binding to the EuiccService implementation and issues commands.
+ *
+ * <p>Keeps track of the highest-priority EuiccService implementation to use. When a command comes
+ * in, brings up a binding to that service, issues the command, and lingers the binding as long as
+ * more commands are coming in. The binding is dropped after an idle timeout.
+ */
+public class EuiccConnector extends StateMachine implements ServiceConnection {
+    private static final String TAG = "EuiccConnector";
+
+    /**
+     * Maximum amount of time to wait for a connection to be established after bindService returns
+     * true or onServiceDisconnected is called (and no package change has occurred which should
+     * force us to reestablish the binding).
+     */
+    private static final int BIND_TIMEOUT_MILLIS = 30000;
+
+    /**
+     * Maximum amount of idle time to hold the binding while in {@link ConnectedState}. After this,
+     * the binding is dropped to free up memory as the EuiccService is not expected to be used
+     * frequently as part of ongoing device operation.
+     */
+    @VisibleForTesting
+    static final int LINGER_TIMEOUT_MILLIS = 60000;
+
+    /**
+     * Command indicating that a package change has occurred.
+     *
+     * <p>{@link Message#obj} is an optional package name. If set, this package has changed in a
+     * way that will permanently sever any open bindings, and if we're bound to it, the binding must
+     * be forcefully reestablished.
+     */
+    private static final int CMD_PACKAGE_CHANGE = 1;
+    /** Command indicating that {@link #BIND_TIMEOUT_MILLIS} has been reached. */
+    private static final int CMD_CONNECT_TIMEOUT = 2;
+    /** Command indicating that {@link #LINGER_TIMEOUT_MILLIS} has been reached. */
+    private static final int CMD_LINGER_TIMEOUT = 3;
+    /**
+     * Command indicating that the service has connected.
+     *
+     * <p>{@link Message#obj} is the connected {@link IEuiccService} implementation.
+     */
+    private static final int CMD_SERVICE_CONNECTED = 4;
+    /** Command indicating that the service has disconnected. */
+    private static final int CMD_SERVICE_DISCONNECTED = 5;
+    /**
+     * Command indicating that a command has completed and the callback should be executed.
+     *
+     * <p>{@link Message#obj} is a {@link Runnable} which will trigger the callback.
+     */
+    private static final int CMD_COMMAND_COMPLETE = 6;
+
+    // Commands corresponding with EuiccService APIs. Keep isEuiccCommand in sync with any changes.
+    private static final int CMD_GET_EID = 100;
+    private static final int CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA = 101;
+    private static final int CMD_DOWNLOAD_SUBSCRIPTION = 102;
+    private static final int CMD_GET_EUICC_PROFILE_INFO_LIST = 103;
+    private static final int CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST = 104;
+    private static final int CMD_GET_EUICC_INFO = 105;
+    private static final int CMD_DELETE_SUBSCRIPTION = 106;
+    private static final int CMD_SWITCH_TO_SUBSCRIPTION = 107;
+    private static final int CMD_UPDATE_SUBSCRIPTION_NICKNAME = 108;
+    private static final int CMD_ERASE_SUBSCRIPTIONS = 109;
+    private static final int CMD_RETAIN_SUBSCRIPTIONS = 110;
+
+    private static boolean isEuiccCommand(int what) {
+        return what >= CMD_GET_EID;
+    }
+
+    /** Flags to use when querying PackageManager for Euicc component implementations. */
+    private static final int EUICC_QUERY_FLAGS =
+            PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DEBUG_TRIAGED_MISSING
+                    | PackageManager.GET_RESOLVED_FILTER;
+
+    /**
+     * Return the activity info of the activity to start for the given intent, or null if none
+     * was found.
+     */
+    public static ActivityInfo findBestActivity(PackageManager packageManager, Intent intent) {
+        List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(intent,
+                EUICC_QUERY_FLAGS);
+        ActivityInfo bestComponent =
+                (ActivityInfo) findBestComponent(packageManager, resolveInfoList);
+        if (bestComponent == null) {
+            Log.w(TAG, "No valid component found for intent: " + intent);
+        }
+        return bestComponent;
+    }
+
+    /**
+     * Return the component info of the EuiccService to bind to, or null if none were found.
+     */
+    public static ComponentInfo findBestComponent(PackageManager packageManager) {
+        Intent intent = new Intent(EuiccService.EUICC_SERVICE_INTERFACE);
+        List<ResolveInfo> resolveInfoList =
+                packageManager.queryIntentServices(intent, EUICC_QUERY_FLAGS);
+        ComponentInfo bestComponent = findBestComponent(packageManager, resolveInfoList);
+        if (bestComponent == null) {
+            Log.w(TAG, "No valid EuiccService implementation found");
+        }
+        return bestComponent;
+    }
+
+    /** Base class for all command callbacks. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface BaseEuiccCommandCallback {
+        /** Called when a command fails because the service is or became unavailable. */
+        void onEuiccServiceUnavailable();
+    }
+
+    /** Callback class for {@link #getEid}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface GetEidCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the EID lookup has completed. */
+        void onGetEidComplete(String eid);
+    }
+
+    static class GetMetadataRequest {
+        DownloadableSubscription mSubscription;
+        boolean mForceDeactivateSim;
+        GetMetadataCommandCallback mCallback;
+    }
+
+    /** Callback class for {@link #getDownloadableSubscriptionMetadata}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface GetMetadataCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the metadata lookup has completed (though it may have failed). */
+        void onGetMetadataComplete(GetDownloadableSubscriptionMetadataResult result);
+    }
+
+    static class DownloadRequest {
+        DownloadableSubscription mSubscription;
+        boolean mSwitchAfterDownload;
+        boolean mForceDeactivateSim;
+        DownloadCommandCallback mCallback;
+    }
+
+    /** Callback class for {@link #downloadSubscription}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface DownloadCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the download has completed (though it may have failed). */
+        void onDownloadComplete(int result);
+    }
+
+    interface GetEuiccProfileInfoListCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the list has completed (though it may have failed). */
+        void onListComplete(GetEuiccProfileInfoListResult result);
+    }
+
+    static class GetDefaultListRequest {
+        boolean mForceDeactivateSim;
+        GetDefaultListCommandCallback mCallback;
+    }
+
+    /** Callback class for {@link #getDefaultDownloadableSubscriptionList}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface GetDefaultListCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the list has completed (though it may have failed). */
+        void onGetDefaultListComplete(GetDefaultDownloadableSubscriptionListResult result);
+    }
+
+    /** Callback class for {@link #getEuiccInfo}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface GetEuiccInfoCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the EuiccInfo lookup has completed. */
+        void onGetEuiccInfoComplete(EuiccInfo euiccInfo);
+    }
+
+    static class DeleteRequest {
+        String mIccid;
+        DeleteCommandCallback mCallback;
+    }
+
+    /** Callback class for {@link #deleteSubscription}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface DeleteCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the delete has completed (though it may have failed). */
+        void onDeleteComplete(int result);
+    }
+
+    static class SwitchRequest {
+        @Nullable String mIccid;
+        boolean mForceDeactivateSim;
+        SwitchCommandCallback mCallback;
+    }
+
+    /** Callback class for {@link #switchToSubscription}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface SwitchCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the switch has completed (though it may have failed). */
+        void onSwitchComplete(int result);
+    }
+
+    static class UpdateNicknameRequest {
+        String mIccid;
+        String mNickname;
+        UpdateNicknameCommandCallback mCallback;
+    }
+
+    /** Callback class for {@link #updateSubscriptionNickname}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface UpdateNicknameCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the update has completed (though it may have failed). */
+        void onUpdateNicknameComplete(int result);
+    }
+
+    /** Callback class for {@link #eraseSubscriptions}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface EraseCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the erase has completed (though it may have failed). */
+        void onEraseComplete(int result);
+    }
+
+    /** Callback class for {@link #retainSubscriptions}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public interface RetainSubscriptionsCommandCallback extends BaseEuiccCommandCallback {
+        /** Called when the retain command has completed (though it may have failed). */
+        void onRetainSubscriptionsComplete(int result);
+    }
+
+    private Context mContext;
+    private PackageManager mPm;
+
+    private final PackageMonitor mPackageMonitor = new EuiccPackageMonitor();
+    private final BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+                // On user unlock, new components might become available, so rebind if needed. This
+                // can never make a component unavailable so there's never a need to force a
+                // rebind.
+                sendMessage(CMD_PACKAGE_CHANGE);
+            }
+        }
+    };
+
+    /** Set to the current component we should bind to except in {@link UnavailableState}. */
+    private @Nullable ServiceInfo mSelectedComponent;
+
+    /** Set to the currently connected EuiccService implementation in {@link ConnectedState}. */
+    private @Nullable IEuiccService mEuiccService;
+
+    /** The callbacks for all (asynchronous) commands which are currently in flight. */
+    private Set<BaseEuiccCommandCallback> mActiveCommandCallbacks = new ArraySet<>();
+
+    @VisibleForTesting(visibility = PACKAGE) public UnavailableState mUnavailableState;
+    @VisibleForTesting(visibility = PACKAGE) public AvailableState mAvailableState;
+    @VisibleForTesting(visibility = PACKAGE) public BindingState mBindingState;
+    @VisibleForTesting(visibility = PACKAGE) public DisconnectedState mDisconnectedState;
+    @VisibleForTesting(visibility = PACKAGE) public ConnectedState mConnectedState;
+
+    EuiccConnector(Context context) {
+        super(TAG);
+        init(context);
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    public EuiccConnector(Context context, Looper looper) {
+        super(TAG, looper);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mContext = context;
+        mPm = context.getPackageManager();
+
+        // Unavailable/Available both monitor for package changes and update mSelectedComponent but
+        // do not need to adjust the binding.
+        mUnavailableState = new UnavailableState();
+        addState(mUnavailableState);
+        mAvailableState = new AvailableState();
+        addState(mAvailableState, mUnavailableState);
+
+        mBindingState = new BindingState();
+        addState(mBindingState);
+
+        // Disconnected/Connected both monitor for package changes and reestablish the active
+        // binding if necessary.
+        mDisconnectedState = new DisconnectedState();
+        addState(mDisconnectedState);
+        mConnectedState = new ConnectedState();
+        addState(mConnectedState, mDisconnectedState);
+
+        mSelectedComponent = findBestComponent();
+        setInitialState(mSelectedComponent != null ? mAvailableState : mUnavailableState);
+
+        mPackageMonitor.register(mContext, null /* thread */, false /* externalStorage */);
+        mContext.registerReceiver(
+                mUserUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
+
+        start();
+    }
+
+    @Override
+    public void onHalting() {
+        mPackageMonitor.unregister();
+        mContext.unregisterReceiver(mUserUnlockedReceiver);
+    }
+
+    /** Asynchronously fetch the EID. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void getEid(GetEidCommandCallback callback) {
+        sendMessage(CMD_GET_EID, callback);
+    }
+
+    /** Asynchronously fetch metadata for the given downloadable subscription. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
+            boolean forceDeactivateSim, GetMetadataCommandCallback callback) {
+        GetMetadataRequest request =
+                new GetMetadataRequest();
+        request.mSubscription = subscription;
+        request.mForceDeactivateSim = forceDeactivateSim;
+        request.mCallback = callback;
+        sendMessage(CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA, request);
+    }
+
+    /** Asynchronously download the given subscription. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void downloadSubscription(DownloadableSubscription subscription,
+            boolean switchAfterDownload, boolean forceDeactivateSim,
+            DownloadCommandCallback callback) {
+        DownloadRequest request = new DownloadRequest();
+        request.mSubscription = subscription;
+        request.mSwitchAfterDownload = switchAfterDownload;
+        request.mForceDeactivateSim = forceDeactivateSim;
+        request.mCallback = callback;
+        sendMessage(CMD_DOWNLOAD_SUBSCRIPTION, request);
+    }
+
+    void getEuiccProfileInfoList(GetEuiccProfileInfoListCommandCallback callback) {
+        sendMessage(CMD_GET_EUICC_PROFILE_INFO_LIST, callback);
+    }
+
+    /** Asynchronously fetch the default downloadable subscription list. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void getDefaultDownloadableSubscriptionList(
+            boolean forceDeactivateSim, GetDefaultListCommandCallback callback) {
+        GetDefaultListRequest request = new GetDefaultListRequest();
+        request.mForceDeactivateSim = forceDeactivateSim;
+        request.mCallback = callback;
+        sendMessage(CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST, request);
+    }
+
+    /** Asynchronously fetch the {@link EuiccInfo}. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void getEuiccInfo(GetEuiccInfoCommandCallback callback) {
+        sendMessage(CMD_GET_EUICC_INFO, callback);
+    }
+
+    /** Asynchronously delete the given subscription. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void deleteSubscription(String iccid, DeleteCommandCallback callback) {
+        DeleteRequest request = new DeleteRequest();
+        request.mIccid = iccid;
+        request.mCallback = callback;
+        sendMessage(CMD_DELETE_SUBSCRIPTION, request);
+    }
+
+    /** Asynchronously switch to the given subscription. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void switchToSubscription(@Nullable String iccid, boolean forceDeactivateSim,
+            SwitchCommandCallback callback) {
+        SwitchRequest request = new SwitchRequest();
+        request.mIccid = iccid;
+        request.mForceDeactivateSim = forceDeactivateSim;
+        request.mCallback = callback;
+        sendMessage(CMD_SWITCH_TO_SUBSCRIPTION, request);
+    }
+
+    /** Asynchronously update the nickname of the given subscription. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void updateSubscriptionNickname(
+            String iccid, String nickname, UpdateNicknameCommandCallback callback) {
+        UpdateNicknameRequest request = new UpdateNicknameRequest();
+        request.mIccid = iccid;
+        request.mNickname = nickname;
+        request.mCallback = callback;
+        sendMessage(CMD_UPDATE_SUBSCRIPTION_NICKNAME, request);
+    }
+
+    /** Asynchronously erase all profiles on the eUICC. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void eraseSubscriptions(EraseCommandCallback callback) {
+        sendMessage(CMD_ERASE_SUBSCRIPTIONS, callback);
+    }
+
+    /** Asynchronously ensure that all profiles will be retained on the next factory reset. */
+    @VisibleForTesting(visibility = PACKAGE)
+    public void retainSubscriptions(RetainSubscriptionsCommandCallback callback) {
+        sendMessage(CMD_RETAIN_SUBSCRIPTIONS, callback);
+    }
+
+    /**
+     * State in which no EuiccService is available.
+     *
+     * <p>All incoming commands will be rejected through
+     * {@link BaseEuiccCommandCallback#onEuiccServiceUnavailable()}.
+     *
+     * <p>Package state changes will lead to transitions between {@link UnavailableState} and
+     * {@link AvailableState} depending on whether an EuiccService becomes unavailable or
+     * available.
+     */
+    private class UnavailableState extends State {
+        @Override
+        public boolean processMessage(Message message) {
+            if (message.what == CMD_PACKAGE_CHANGE) {
+                mSelectedComponent = findBestComponent();
+                if (mSelectedComponent != null) {
+                    transitionTo(mAvailableState);
+                } else if (getCurrentState() != mUnavailableState) {
+                    transitionTo(mUnavailableState);
+                }
+                return HANDLED;
+            } else if (isEuiccCommand(message.what)) {
+                BaseEuiccCommandCallback callback = getCallback(message);
+                callback.onEuiccServiceUnavailable();
+                return HANDLED;
+            }
+
+            return NOT_HANDLED;
+        }
+    }
+
+    /**
+     * State in which a EuiccService is available, but no binding is established or in the process
+     * of being established.
+     *
+     * <p>If a command is received, this state will defer the message and enter {@link BindingState}
+     * to bring up the binding.
+     */
+    private class AvailableState extends State {
+        @Override
+        public boolean processMessage(Message message) {
+            if (isEuiccCommand(message.what)) {
+                deferMessage(message);
+                transitionTo(mBindingState);
+                return HANDLED;
+            }
+
+            return NOT_HANDLED;
+        }
+    }
+
+    /**
+     * State in which we are binding to the current EuiccService.
+     *
+     * <p>This is a transient state. If bindService returns true, we enter {@link DisconnectedState}
+     * while waiting for the binding to be established. If it returns false, we move back to
+     * {@link AvailableState}.
+     *
+     * <p>Any received messages will be deferred.
+     */
+    private class BindingState extends State {
+        @Override
+        public void enter() {
+            if (createBinding()) {
+                transitionTo(mDisconnectedState);
+            } else {
+                // createBinding() should generally not return false since we've already performed
+                // Intent resolution, but it's always possible that the package state changes
+                // asynchronously. Transition to available for now, and if the package state has
+                // changed, we'll process that event and move to mUnavailableState as needed.
+                transitionTo(mAvailableState);
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            deferMessage(message);
+            return HANDLED;
+        }
+    }
+
+    /**
+     * State in which a binding is established, but not currently connected.
+     *
+     * <p>We wait up to {@link #BIND_TIMEOUT_MILLIS} for the binding to establish. If it doesn't,
+     * we go back to {@link AvailableState} to try again.
+     *
+     * <p>Package state changes will cause us to unbind and move to {@link BindingState} to
+     * reestablish the binding if the selected component has changed or if a forced rebind is
+     * necessary.
+     *
+     * <p>Any received commands will be deferred.
+     */
+    private class DisconnectedState extends State {
+        @Override
+        public void enter() {
+            sendMessageDelayed(CMD_CONNECT_TIMEOUT, BIND_TIMEOUT_MILLIS);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (message.what == CMD_SERVICE_CONNECTED) {
+                mEuiccService = (IEuiccService) message.obj;
+                transitionTo(mConnectedState);
+                return HANDLED;
+            } else if (message.what == CMD_PACKAGE_CHANGE) {
+                ServiceInfo bestComponent = findBestComponent();
+                String affectedPackage = (String) message.obj;
+                boolean isSameComponent;
+                if (bestComponent == null) {
+                    isSameComponent = mSelectedComponent != null;
+                } else {
+                    isSameComponent = mSelectedComponent == null
+                            || Objects.equals(
+                                    bestComponent.getComponentName(),
+                                    mSelectedComponent.getComponentName());
+                }
+                boolean forceRebind = bestComponent != null
+                        && Objects.equals(bestComponent.packageName, affectedPackage);
+                if (!isSameComponent || forceRebind) {
+                    unbind();
+                    mSelectedComponent = bestComponent;
+                    if (mSelectedComponent == null) {
+                        transitionTo(mUnavailableState);
+                    } else {
+                        transitionTo(mBindingState);
+                    }
+                }
+                return HANDLED;
+            } else if (message.what == CMD_CONNECT_TIMEOUT) {
+                transitionTo(mAvailableState);
+                return HANDLED;
+            } else if (isEuiccCommand(message.what)) {
+                deferMessage(message);
+                return HANDLED;
+            }
+
+            return NOT_HANDLED;
+        }
+    }
+
+    /**
+     * State in which the binding is connected.
+     *
+     * <p>Commands will be processed as long as we're in this state. We wait up to
+     * {@link #LINGER_TIMEOUT_MILLIS} between commands; if this timeout is reached, we will drop the
+     * binding until the next command is received.
+     */
+    private class ConnectedState extends State {
+        @Override
+        public void enter() {
+            removeMessages(CMD_CONNECT_TIMEOUT);
+            sendMessageDelayed(CMD_LINGER_TIMEOUT, LINGER_TIMEOUT_MILLIS);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (message.what == CMD_SERVICE_DISCONNECTED) {
+                mEuiccService = null;
+                transitionTo(mDisconnectedState);
+                return HANDLED;
+            } else if (message.what == CMD_LINGER_TIMEOUT) {
+                unbind();
+                transitionTo(mAvailableState);
+                return HANDLED;
+            } else if (message.what == CMD_COMMAND_COMPLETE) {
+                Runnable runnable = (Runnable) message.obj;
+                runnable.run();
+                return HANDLED;
+            } else if (isEuiccCommand(message.what)) {
+                final BaseEuiccCommandCallback callback = getCallback(message);
+                onCommandStart(callback);
+                // TODO(b/36260308): Plumb through an actual SIM slot ID.
+                int slotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+                try {
+                    switch (message.what) {
+                        case CMD_GET_EID: {
+                            mEuiccService.getEid(slotId,
+                                    new IGetEidCallback.Stub() {
+                                        @Override
+                                        public void onSuccess(String eid) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((GetEidCommandCallback) callback)
+                                                        .onGetEidComplete(eid);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA: {
+                            GetMetadataRequest request = (GetMetadataRequest) message.obj;
+                            mEuiccService.getDownloadableSubscriptionMetadata(slotId,
+                                    request.mSubscription,
+                                    request.mForceDeactivateSim,
+                                    new IGetDownloadableSubscriptionMetadataCallback.Stub() {
+                                        @Override
+                                        public void onComplete(
+                                                GetDownloadableSubscriptionMetadataResult result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((GetMetadataCommandCallback) callback)
+                                                        .onGetMetadataComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_DOWNLOAD_SUBSCRIPTION: {
+                            DownloadRequest request = (DownloadRequest) message.obj;
+                            mEuiccService.downloadSubscription(slotId,
+                                    request.mSubscription,
+                                    request.mSwitchAfterDownload,
+                                    request.mForceDeactivateSim,
+                                    new IDownloadSubscriptionCallback.Stub() {
+                                        @Override
+                                        public void onComplete(int result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((DownloadCommandCallback) callback)
+                                                        .onDownloadComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_GET_EUICC_PROFILE_INFO_LIST: {
+                            mEuiccService.getEuiccProfileInfoList(slotId,
+                                    new IGetEuiccProfileInfoListCallback.Stub() {
+                                        @Override
+                                        public void onComplete(
+                                                GetEuiccProfileInfoListResult result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((GetEuiccProfileInfoListCommandCallback) callback)
+                                                        .onListComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST: {
+                            GetDefaultListRequest request = (GetDefaultListRequest) message.obj;
+                            mEuiccService.getDefaultDownloadableSubscriptionList(slotId,
+                                    request.mForceDeactivateSim,
+                                    new IGetDefaultDownloadableSubscriptionListCallback.Stub() {
+                                        @Override
+                                        public void onComplete(
+                                                GetDefaultDownloadableSubscriptionListResult result
+                                        ) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((GetDefaultListCommandCallback) callback)
+                                                        .onGetDefaultListComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_GET_EUICC_INFO: {
+                            mEuiccService.getEuiccInfo(slotId,
+                                    new IGetEuiccInfoCallback.Stub() {
+                                        @Override
+                                        public void onSuccess(EuiccInfo euiccInfo) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((GetEuiccInfoCommandCallback) callback)
+                                                        .onGetEuiccInfoComplete(euiccInfo);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_DELETE_SUBSCRIPTION: {
+                            DeleteRequest request = (DeleteRequest) message.obj;
+                            mEuiccService.deleteSubscription(slotId, request.mIccid,
+                                    new IDeleteSubscriptionCallback.Stub() {
+                                        @Override
+                                        public void onComplete(int result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((DeleteCommandCallback) callback)
+                                                        .onDeleteComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_SWITCH_TO_SUBSCRIPTION: {
+                            SwitchRequest request = (SwitchRequest) message.obj;
+                            mEuiccService.switchToSubscription(slotId, request.mIccid,
+                                    request.mForceDeactivateSim,
+                                    new ISwitchToSubscriptionCallback.Stub() {
+                                        @Override
+                                        public void onComplete(int result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((SwitchCommandCallback) callback)
+                                                        .onSwitchComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_UPDATE_SUBSCRIPTION_NICKNAME: {
+                            UpdateNicknameRequest request = (UpdateNicknameRequest) message.obj;
+                            mEuiccService.updateSubscriptionNickname(slotId, request.mIccid,
+                                    request.mNickname,
+                                    new IUpdateSubscriptionNicknameCallback.Stub() {
+                                        @Override
+                                        public void onComplete(int result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((UpdateNicknameCommandCallback) callback)
+                                                        .onUpdateNicknameComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_ERASE_SUBSCRIPTIONS: {
+                            mEuiccService.eraseSubscriptions(slotId,
+                                    new IEraseSubscriptionsCallback.Stub() {
+                                        @Override
+                                        public void onComplete(int result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((EraseCommandCallback) callback)
+                                                        .onEraseComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        case CMD_RETAIN_SUBSCRIPTIONS: {
+                            mEuiccService.retainSubscriptionsForFactoryReset(slotId,
+                                    new IRetainSubscriptionsForFactoryResetCallback.Stub() {
+                                        @Override
+                                        public void onComplete(int result) {
+                                            sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+                                                ((RetainSubscriptionsCommandCallback) callback)
+                                                        .onRetainSubscriptionsComplete(result);
+                                                onCommandEnd(callback);
+                                            });
+                                        }
+                                    });
+                            break;
+                        }
+                        default: {
+                            Log.wtf(TAG, "Unimplemented eUICC command: " + message.what);
+                            callback.onEuiccServiceUnavailable();
+                            onCommandEnd(callback);
+                            return HANDLED;
+                        }
+                    }
+                } catch (Exception e) {
+                    // If this is a RemoteException, we expect to be disconnected soon. For other
+                    // exceptions, this is a bug in the EuiccService implementation, but we must
+                    // not let it crash the phone process.
+                    Log.w(TAG, "Exception making binder call to EuiccService", e);
+                    callback.onEuiccServiceUnavailable();
+                    onCommandEnd(callback);
+                }
+
+                return HANDLED;
+            }
+
+            return NOT_HANDLED;
+        }
+
+        @Override
+        public void exit() {
+            removeMessages(CMD_LINGER_TIMEOUT);
+            // Dispatch callbacks for all in-flight commands; they will no longer succeed. (The
+            // remote process cannot possibly trigger a callback at this stage because the
+            // connection has dropped).
+            for (BaseEuiccCommandCallback callback : mActiveCommandCallbacks) {
+                callback.onEuiccServiceUnavailable();
+            }
+            mActiveCommandCallbacks.clear();
+        }
+    }
+
+    private static BaseEuiccCommandCallback getCallback(Message message) {
+        switch (message.what) {
+            case CMD_GET_EID:
+            case CMD_GET_EUICC_PROFILE_INFO_LIST:
+            case CMD_GET_EUICC_INFO:
+            case CMD_ERASE_SUBSCRIPTIONS:
+            case CMD_RETAIN_SUBSCRIPTIONS:
+                return (BaseEuiccCommandCallback) message.obj;
+            case CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA:
+                return ((GetMetadataRequest) message.obj).mCallback;
+            case CMD_DOWNLOAD_SUBSCRIPTION:
+                return ((DownloadRequest) message.obj).mCallback;
+            case CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST:
+                return ((GetDefaultListRequest) message.obj).mCallback;
+            case CMD_DELETE_SUBSCRIPTION:
+                return ((DeleteRequest) message.obj).mCallback;
+            case CMD_SWITCH_TO_SUBSCRIPTION:
+                return ((SwitchRequest) message.obj).mCallback;
+            case CMD_UPDATE_SUBSCRIPTION_NICKNAME:
+                return ((UpdateNicknameRequest) message.obj).mCallback;
+            default:
+                throw new IllegalArgumentException("Unsupported message: " + message.what);
+        }
+    }
+
+    /** Call this at the beginning of the execution of any command. */
+    private void onCommandStart(BaseEuiccCommandCallback callback) {
+        mActiveCommandCallbacks.add(callback);
+        removeMessages(CMD_LINGER_TIMEOUT);
+    }
+
+    /** Call this at the end of execution of any command (whether or not it succeeded). */
+    private void onCommandEnd(BaseEuiccCommandCallback callback) {
+        if (!mActiveCommandCallbacks.remove(callback)) {
+            Log.wtf(TAG, "Callback already removed from mActiveCommandCallbacks");
+        }
+        if (mActiveCommandCallbacks.isEmpty()) {
+            sendMessageDelayed(CMD_LINGER_TIMEOUT, LINGER_TIMEOUT_MILLIS);
+        }
+    }
+
+    /** Return the service info of the EuiccService to bind to, or null if none were found. */
+    @Nullable
+    private ServiceInfo findBestComponent() {
+        return (ServiceInfo) findBestComponent(mPm);
+    }
+
+    /**
+     * Bring up a binding to the currently-selected component.
+     *
+     * <p>Returns true if we've successfully bound to the service.
+     */
+    private boolean createBinding() {
+        if (mSelectedComponent == null) {
+            Log.wtf(TAG, "Attempting to create binding but no component is selected");
+            return false;
+        }
+        Intent intent = new Intent(EuiccService.EUICC_SERVICE_INTERFACE);
+        intent.setComponent(mSelectedComponent.getComponentName());
+        // We bind this as a foreground service because it is operating directly on the SIM, and we
+        // do not want it subjected to power-savings restrictions while doing so.
+        return mContext.bindService(intent, this,
+                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE);
+    }
+
+    private void unbind() {
+        mEuiccService = null;
+        mContext.unbindService(this);
+    }
+
+    private static ComponentInfo findBestComponent(
+            PackageManager packageManager, List<ResolveInfo> resolveInfoList) {
+        int bestPriority = Integer.MIN_VALUE;
+        ComponentInfo bestComponent = null;
+        if (resolveInfoList != null) {
+            for (ResolveInfo resolveInfo : resolveInfoList) {
+                if (!isValidEuiccComponent(packageManager, resolveInfo)) {
+                    continue;
+                }
+
+                if (resolveInfo.filter.getPriority() > bestPriority) {
+                    bestPriority = resolveInfo.filter.getPriority();
+                    bestComponent = resolveInfo.getComponentInfo();
+                }
+            }
+        }
+
+        return bestComponent;
+    }
+
+    private static boolean isValidEuiccComponent(
+            PackageManager packageManager, ResolveInfo resolveInfo) {
+        ComponentInfo componentInfo = resolveInfo.getComponentInfo();
+        String packageName = componentInfo.getComponentName().getPackageName();
+
+        // Verify that the app is privileged (via granting of a privileged permission).
+        if (packageManager.checkPermission(
+                Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS, packageName)
+                        != PackageManager.PERMISSION_GRANTED) {
+            Log.wtf(TAG, "Package " + packageName
+                    + " does not declare WRITE_EMBEDDED_SUBSCRIPTIONS");
+            return false;
+        }
+
+        // Verify that only the system can access the component.
+        final String permission;
+        if (componentInfo instanceof ServiceInfo) {
+            permission = ((ServiceInfo) componentInfo).permission;
+        } else if (componentInfo instanceof ActivityInfo) {
+            permission = ((ActivityInfo) componentInfo).permission;
+        } else {
+            throw new IllegalArgumentException("Can only verify services/activities");
+        }
+        if (!TextUtils.equals(permission, Manifest.permission.BIND_EUICC_SERVICE)) {
+            Log.wtf(TAG, "Package " + packageName
+                    + " does not require the BIND_EUICC_SERVICE permission");
+            return false;
+        }
+
+        // Verify that the component declares a priority.
+        if (resolveInfo.filter == null || resolveInfo.filter.getPriority() == 0) {
+            Log.wtf(TAG, "Package " + packageName + " does not specify a priority");
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        IEuiccService euiccService = IEuiccService.Stub.asInterface(service);
+        sendMessage(CMD_SERVICE_CONNECTED, euiccService);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        sendMessage(CMD_SERVICE_DISCONNECTED);
+    }
+
+    private class EuiccPackageMonitor extends PackageMonitor {
+        @Override
+        public void onPackageAdded(String packageName, int reason) {
+            sendPackageChange(packageName, true /* forceUnbindForThisPackage */);
+        }
+
+        @Override
+        public void onPackageRemoved(String packageName, int reason) {
+            sendPackageChange(packageName, true /* forceUnbindForThisPackage */);
+        }
+
+        @Override
+        public void onPackageUpdateFinished(String packageName, int uid) {
+            sendPackageChange(packageName, true /* forceUnbindForThisPackage */);
+        }
+
+        @Override
+        public void onPackageModified(String packageName) {
+            sendPackageChange(packageName, false /* forceUnbindForThisPackage */);
+        }
+
+        @Override
+        public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
+            if (doit) {
+                for (String packageName : packages) {
+                    sendPackageChange(packageName, true /* forceUnbindForThisPackage */);
+                }
+            }
+            return super.onHandleForceStop(intent, packages, uid, doit);
+        }
+
+        private void sendPackageChange(String packageName, boolean forceUnbindForThisPackage) {
+            sendMessage(CMD_PACKAGE_CHANGE, forceUnbindForThisPackage ? packageName : null);
+        }
+    }
+
+    @Override
+    protected void unhandledMessage(Message msg) {
+        IState state = getCurrentState();
+        Log.wtf(TAG, "Unhandled message " + msg.what + " in state "
+                + (state == null ? "null" : state.getName()));
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        super.dump(fd, pw, args);
+        pw.println("mSelectedComponent=" + mSelectedComponent);
+        pw.println("mEuiccService=" + mEuiccService);
+        pw.println("mActiveCommandCount=" + mActiveCommandCallbacks.size());
+    }
+}
diff --git a/com/android/internal/telephony/euicc/EuiccController.java b/com/android/internal/telephony/euicc/EuiccController.java
new file mode 100644
index 0000000..0d58d80
--- /dev/null
+++ b/com/android/internal/telephony/euicc/EuiccController.java
@@ -0,0 +1,995 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.euicc;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.ServiceManager;
+import android.provider.Settings;
+import android.service.euicc.EuiccService;
+import android.service.euicc.GetDefaultDownloadableSubscriptionListResult;
+import android.service.euicc.GetDownloadableSubscriptionMetadataResult;
+import android.service.euicc.GetEuiccProfileInfoListResult;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccAccessRule;
+import android.telephony.euicc.DownloadableSubscription;
+import android.telephony.euicc.EuiccInfo;
+import android.telephony.euicc.EuiccManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.SubscriptionController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Backing implementation of {@link android.telephony.euicc.EuiccManager}. */
+public class EuiccController extends IEuiccController.Stub {
+    private static final String TAG = "EuiccController";
+
+    /** Extra set on resolution intents containing the {@link EuiccOperation}. */
+    @VisibleForTesting
+    static final String EXTRA_OPERATION = "operation";
+
+    // Aliases so line lengths stay short.
+    private static final int OK = EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK;
+    private static final int RESOLVABLE_ERROR =
+            EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR;
+    private static final int ERROR =
+            EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_ERROR;
+
+    private static EuiccController sInstance;
+
+    private final Context mContext;
+    private final EuiccConnector mConnector;
+    private final SubscriptionManager mSubscriptionManager;
+    private final AppOpsManager mAppOpsManager;
+    private final PackageManager mPackageManager;
+
+    /** Initialize the instance. Should only be called once. */
+    public static EuiccController init(Context context) {
+        synchronized (EuiccController.class) {
+            if (sInstance == null) {
+                sInstance = new EuiccController(context);
+            } else {
+                Log.wtf(TAG, "init() called multiple times! sInstance = " + sInstance);
+            }
+        }
+        return sInstance;
+    }
+
+    /** Get an instance. Assumes one has already been initialized with {@link #init}. */
+    public static EuiccController get() {
+        if (sInstance == null) {
+            synchronized (EuiccController.class) {
+                if (sInstance == null) {
+                    throw new IllegalStateException("get() called before init()");
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    private EuiccController(Context context) {
+        this(context, new EuiccConnector(context));
+        ServiceManager.addService("econtroller", this);
+    }
+
+    @VisibleForTesting
+    public EuiccController(Context context, EuiccConnector connector) {
+        mContext = context;
+        mConnector = connector;
+        mSubscriptionManager = (SubscriptionManager)
+                context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+        mPackageManager = context.getPackageManager();
+    }
+
+    /**
+     * Continue an operation which failed with a user-resolvable error.
+     *
+     * <p>The implementation here makes a key assumption that the resolutionIntent has not been
+     * tampered with. This is guaranteed because:
+     * <UL>
+     * <LI>The intent is wrapped in a PendingIntent created by the phone process which is created
+     * with {@link #EXTRA_OPERATION} already present. This means that the operation cannot be
+     * overridden on the PendingIntent - a caller can only add new extras.
+     * <LI>The resolution activity is restricted by a privileged permission; unprivileged apps
+     * cannot start it directly. So the PendingIntent is the only way to start it.
+     * </UL>
+     */
+    @Override
+    public void continueOperation(Intent resolutionIntent, Bundle resolutionExtras) {
+        if (!callerCanWriteEmbeddedSubscriptions()) {
+            throw new SecurityException(
+                    "Must have WRITE_EMBEDDED_SUBSCRIPTIONS to continue operation");
+        }
+        long token = Binder.clearCallingIdentity();
+        try {
+            EuiccOperation op = resolutionIntent.getParcelableExtra(EXTRA_OPERATION);
+            if (op == null) {
+                throw new IllegalArgumentException("Invalid resolution intent");
+            }
+
+            PendingIntent callbackIntent =
+                    resolutionIntent.getParcelableExtra(
+                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_CALLBACK_INTENT);
+            op.continueOperation(resolutionExtras, callbackIntent);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Return the EID.
+     *
+     * <p>For API simplicity, this call blocks until completion; while it requires an IPC to load,
+     * that IPC should generally be fast, and the EID shouldn't be needed in the normal course of
+     * operation.
+     */
+    @Override
+    public String getEid() {
+        if (!callerCanReadPhoneStatePrivileged()
+                && !callerHasCarrierPrivilegesForActiveSubscription()) {
+            throw new SecurityException(
+                    "Must have carrier privileges on active subscription to read EID");
+        }
+        long token = Binder.clearCallingIdentity();
+        try {
+            return blockingGetEidFromEuiccService();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
+            String callingPackage, PendingIntent callbackIntent) {
+        getDownloadableSubscriptionMetadata(
+                subscription, false /* forceDeactivateSim */, callingPackage, callbackIntent);
+    }
+
+    void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
+            boolean forceDeactivateSim, String callingPackage, PendingIntent callbackIntent) {
+        if (!callerCanWriteEmbeddedSubscriptions()) {
+            throw new SecurityException("Must have WRITE_EMBEDDED_SUBSCRIPTIONS to get metadata");
+        }
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
+        long token = Binder.clearCallingIdentity();
+        try {
+            mConnector.getDownloadableSubscriptionMetadata(
+                    subscription, forceDeactivateSim,
+                    new GetMetadataCommandCallback(
+                            token, subscription, callingPackage, callbackIntent));
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    class GetMetadataCommandCallback implements EuiccConnector.GetMetadataCommandCallback {
+        protected final long mCallingToken;
+        protected final DownloadableSubscription mSubscription;
+        protected final String mCallingPackage;
+        protected final PendingIntent mCallbackIntent;
+
+        GetMetadataCommandCallback(
+                long callingToken,
+                DownloadableSubscription subscription,
+                String callingPackage,
+                PendingIntent callbackIntent) {
+            mCallingToken = callingToken;
+            mSubscription = subscription;
+            mCallingPackage = callingPackage;
+            mCallbackIntent = callbackIntent;
+        }
+
+        @Override
+        public void onGetMetadataComplete(
+                GetDownloadableSubscriptionMetadataResult result) {
+            Intent extrasIntent = new Intent();
+            final int resultCode;
+            switch (result.result) {
+                case EuiccService.RESULT_OK:
+                    resultCode = OK;
+                    extrasIntent.putExtra(
+                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION,
+                            result.subscription);
+                    break;
+                case EuiccService.RESULT_MUST_DEACTIVATE_SIM:
+                    resultCode = RESOLVABLE_ERROR;
+                    addResolutionIntent(extrasIntent,
+                            EuiccService.ACTION_RESOLVE_DEACTIVATE_SIM,
+                            mCallingPackage,
+                            getOperationForDeactivateSim());
+                    break;
+                default:
+                    resultCode = ERROR;
+                    extrasIntent.putExtra(
+                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                            result.result);
+                    break;
+            }
+
+            sendResult(mCallbackIntent, resultCode, extrasIntent);
+        }
+
+        @Override
+        public void onEuiccServiceUnavailable() {
+            sendResult(mCallbackIntent, ERROR, null /* extrasIntent */);
+        }
+
+        protected EuiccOperation getOperationForDeactivateSim() {
+            return EuiccOperation.forGetMetadataDeactivateSim(
+                    mCallingToken, mSubscription, mCallingPackage);
+        }
+    }
+
+    @Override
+    public void downloadSubscription(DownloadableSubscription subscription,
+            boolean switchAfterDownload, String callingPackage, PendingIntent callbackIntent) {
+        downloadSubscription(subscription, switchAfterDownload, callingPackage,
+                false /* forceDeactivateSim */, callbackIntent);
+    }
+
+    void downloadSubscription(DownloadableSubscription subscription,
+            boolean switchAfterDownload, String callingPackage, boolean forceDeactivateSim,
+            PendingIntent callbackIntent) {
+        boolean callerCanWriteEmbeddedSubscriptions = callerCanWriteEmbeddedSubscriptions();
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
+
+        long token = Binder.clearCallingIdentity();
+        try {
+            if (callerCanWriteEmbeddedSubscriptions) {
+                // With WRITE_EMBEDDED_SUBSCRIPTIONS, we can skip profile-specific permission checks
+                // and move straight to the profile download.
+                downloadSubscriptionPrivileged(token, subscription, switchAfterDownload,
+                        forceDeactivateSim, callingPackage, callbackIntent);
+                return;
+            }
+            // Without WRITE_EMBEDDED_SUBSCRIPTIONS, the caller *must* be whitelisted per the
+            // metadata of the profile to be downloaded, so check the metadata first.
+            mConnector.getDownloadableSubscriptionMetadata(subscription,
+                    forceDeactivateSim,
+                    new DownloadSubscriptionGetMetadataCommandCallback(token, subscription,
+                            switchAfterDownload, callingPackage, forceDeactivateSim,
+                            callbackIntent));
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    class DownloadSubscriptionGetMetadataCommandCallback extends GetMetadataCommandCallback {
+        private final boolean mSwitchAfterDownload;
+        private final boolean mForceDeactivateSim;
+
+        DownloadSubscriptionGetMetadataCommandCallback(long callingToken,
+                DownloadableSubscription subscription, boolean switchAfterDownload,
+                String callingPackage, boolean forceDeactivateSim,
+                PendingIntent callbackIntent) {
+            super(callingToken, subscription, callingPackage, callbackIntent);
+            mSwitchAfterDownload = switchAfterDownload;
+            mForceDeactivateSim = forceDeactivateSim;
+        }
+
+        @Override
+        public void onGetMetadataComplete(
+                GetDownloadableSubscriptionMetadataResult result) {
+            if (result.result == EuiccService.RESULT_MUST_DEACTIVATE_SIM) {
+                // If we need to deactivate the current SIM to even check permissions, go ahead and
+                // require that the user resolve the stronger permission dialog.
+                Intent extrasIntent = new Intent();
+                addResolutionIntent(extrasIntent, EuiccService.ACTION_RESOLVE_NO_PRIVILEGES,
+                        mCallingPackage,
+                        EuiccOperation.forDownloadNoPrivileges(
+                                mCallingToken, mSubscription, mSwitchAfterDownload,
+                                mCallingPackage));
+                sendResult(mCallbackIntent, RESOLVABLE_ERROR, extrasIntent);
+                return;
+            }
+
+            if (result.result != EuiccService.RESULT_OK) {
+                // Just propagate the error as normal.
+                super.onGetMetadataComplete(result);
+                return;
+            }
+
+            DownloadableSubscription subscription = result.subscription;
+            UiccAccessRule[] rules = subscription.getAccessRules();
+            if (rules == null) {
+                Log.e(TAG, "No access rules but caller is unprivileged");
+                sendResult(mCallbackIntent, ERROR, null /* extrasIntent */);
+                return;
+            }
+
+            final PackageInfo info;
+            try {
+                info = mPackageManager.getPackageInfo(
+                        mCallingPackage, PackageManager.GET_SIGNATURES);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "Calling package valid but gone");
+                sendResult(mCallbackIntent, ERROR, null /* extrasIntent */);
+                return;
+            }
+
+            for (int i = 0; i < rules.length; i++) {
+                if (rules[i].getCarrierPrivilegeStatus(info)
+                        == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+                    // Caller can download this profile. Now, determine whether the caller can also
+                    // manage the current profile; if so, we can perform the download silently; if
+                    // not, the user must provide consent.
+                    if (canManageActiveSubscription(mCallingPackage)) {
+                        downloadSubscriptionPrivileged(
+                                mCallingToken, subscription, mSwitchAfterDownload,
+                                mForceDeactivateSim, mCallingPackage, mCallbackIntent);
+                        return;
+                    }
+
+                    // Switch might still be permitted, but the user must consent first.
+                    Intent extrasIntent = new Intent();
+                    addResolutionIntent(extrasIntent, EuiccService.ACTION_RESOLVE_NO_PRIVILEGES,
+                            mCallingPackage,
+                            EuiccOperation.forDownloadNoPrivileges(
+                                    mCallingToken, subscription, mSwitchAfterDownload,
+                                    mCallingPackage));
+                    sendResult(mCallbackIntent, RESOLVABLE_ERROR, extrasIntent);
+                    return;
+                }
+            }
+            Log.e(TAG, "Caller is not permitted to download this profile");
+            sendResult(mCallbackIntent, ERROR, null /* extrasIntent */);
+        }
+
+        @Override
+        protected EuiccOperation getOperationForDeactivateSim() {
+            return EuiccOperation.forDownloadDeactivateSim(
+                    mCallingToken, mSubscription, mSwitchAfterDownload, mCallingPackage);
+        }
+    }
+
+    void downloadSubscriptionPrivileged(final long callingToken,
+            DownloadableSubscription subscription, boolean switchAfterDownload,
+            boolean forceDeactivateSim, final String callingPackage,
+            final PendingIntent callbackIntent) {
+        mConnector.downloadSubscription(
+                subscription,
+                switchAfterDownload,
+                forceDeactivateSim,
+                new EuiccConnector.DownloadCommandCallback() {
+                    @Override
+                    public void onDownloadComplete(int result) {
+                        Intent extrasIntent = new Intent();
+                        final int resultCode;
+                        switch (result) {
+                            case EuiccService.RESULT_OK:
+                                resultCode = OK;
+                                // Now that a profile has been successfully downloaded, mark the
+                                // eUICC as provisioned so it appears in settings UI as appropriate.
+                                Settings.Global.putInt(
+                                        mContext.getContentResolver(),
+                                        Settings.Global.EUICC_PROVISIONED,
+                                        1);
+                                if (!switchAfterDownload) {
+                                    // Since we're not switching, nothing will trigger a
+                                    // subscription list refresh on its own, so request one here.
+                                    refreshSubscriptionsAndSendResult(
+                                            callbackIntent, resultCode, extrasIntent);
+                                    return;
+                                }
+                                break;
+                            case EuiccService.RESULT_MUST_DEACTIVATE_SIM:
+                                resultCode = RESOLVABLE_ERROR;
+                                addResolutionIntent(extrasIntent,
+                                        EuiccService.ACTION_RESOLVE_DEACTIVATE_SIM,
+                                        callingPackage,
+                                        EuiccOperation.forDownloadDeactivateSim(
+                                                callingToken, subscription, switchAfterDownload,
+                                                callingPackage));
+                                break;
+                            default:
+                                resultCode = ERROR;
+                                extrasIntent.putExtra(
+                                        EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                                        result);
+                                break;
+                        }
+
+                        sendResult(callbackIntent, resultCode, extrasIntent);
+                    }
+
+                    @Override
+                    public void onEuiccServiceUnavailable() {
+                        sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                    }
+                });
+    }
+
+    /**
+     * Blocking call to {@link EuiccService#onGetEuiccProfileInfoList}.
+     *
+     * <p>Does not perform permission checks as this is not an exposed API and is only used within
+     * the phone process.
+     */
+    public GetEuiccProfileInfoListResult blockingGetEuiccProfileInfoList() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicReference<GetEuiccProfileInfoListResult> resultRef = new AtomicReference<>();
+        mConnector.getEuiccProfileInfoList(
+                new EuiccConnector.GetEuiccProfileInfoListCommandCallback() {
+                    @Override
+                    public void onListComplete(GetEuiccProfileInfoListResult result) {
+                        resultRef.set(result);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onEuiccServiceUnavailable() {
+                        latch.countDown();
+                    }
+                });
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        return resultRef.get();
+    }
+
+    @Override
+    public void getDefaultDownloadableSubscriptionList(
+            String callingPackage, PendingIntent callbackIntent) {
+        getDefaultDownloadableSubscriptionList(
+                false /* forceDeactivateSim */, callingPackage, callbackIntent);
+    }
+
+    void getDefaultDownloadableSubscriptionList(
+            boolean forceDeactivateSim, String callingPackage, PendingIntent callbackIntent) {
+        if (!callerCanWriteEmbeddedSubscriptions()) {
+            throw new SecurityException(
+                    "Must have WRITE_EMBEDDED_SUBSCRIPTIONS to get default list");
+        }
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
+        long token = Binder.clearCallingIdentity();
+        try {
+            mConnector.getDefaultDownloadableSubscriptionList(
+                    forceDeactivateSim, new GetDefaultListCommandCallback(
+                            token, callingPackage, callbackIntent));
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    class GetDefaultListCommandCallback implements EuiccConnector.GetDefaultListCommandCallback {
+        final long mCallingToken;
+        final String mCallingPackage;
+        final PendingIntent mCallbackIntent;
+
+        GetDefaultListCommandCallback(long callingToken, String callingPackage,
+                PendingIntent callbackIntent) {
+            mCallingToken = callingToken;
+            mCallingPackage = callingPackage;
+            mCallbackIntent = callbackIntent;
+        }
+
+        @Override
+        public void onGetDefaultListComplete(GetDefaultDownloadableSubscriptionListResult result) {
+            Intent extrasIntent = new Intent();
+            final int resultCode;
+            switch (result.result) {
+                case EuiccService.RESULT_OK:
+                    resultCode = OK;
+                    extrasIntent.putExtra(
+                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTIONS,
+                            result.subscriptions);
+                    break;
+                case EuiccService.RESULT_MUST_DEACTIVATE_SIM:
+                    resultCode = RESOLVABLE_ERROR;
+                    addResolutionIntent(extrasIntent,
+                            EuiccService.ACTION_RESOLVE_DEACTIVATE_SIM,
+                            mCallingPackage,
+                            EuiccOperation.forGetDefaultListDeactivateSim(
+                                    mCallingToken, mCallingPackage));
+                    break;
+                default:
+                    resultCode = ERROR;
+                    extrasIntent.putExtra(
+                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                            result.result);
+                    break;
+            }
+
+            sendResult(mCallbackIntent, resultCode, extrasIntent);
+        }
+
+        @Override
+        public void onEuiccServiceUnavailable() {
+            sendResult(mCallbackIntent, ERROR, null /* extrasIntent */);
+        }
+    }
+
+    /**
+     * Return the {@link EuiccInfo}.
+     *
+     * <p>For API simplicity, this call blocks until completion; while it requires an IPC to load,
+     * that IPC should generally be fast, and this info shouldn't be needed in the normal course of
+     * operation.
+     */
+    @Override
+    public EuiccInfo getEuiccInfo() {
+        // No permissions required as EuiccInfo is not sensitive.
+        long token = Binder.clearCallingIdentity();
+        try {
+            return blockingGetEuiccInfoFromEuiccService();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void deleteSubscription(int subscriptionId, String callingPackage,
+            PendingIntent callbackIntent) {
+        boolean callerCanWriteEmbeddedSubscriptions = callerCanWriteEmbeddedSubscriptions();
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
+
+        long token = Binder.clearCallingIdentity();
+        try {
+            SubscriptionInfo sub = getSubscriptionForSubscriptionId(subscriptionId);
+            if (sub == null) {
+                Log.e(TAG, "Cannot delete nonexistent subscription: " + subscriptionId);
+                sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                return;
+            }
+
+            if (!callerCanWriteEmbeddedSubscriptions
+                    && !sub.canManageSubscription(mContext, callingPackage)) {
+                Log.e(TAG, "No permissions: " + subscriptionId);
+                sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                return;
+            }
+
+            deleteSubscriptionPrivileged(sub.getIccId(), callbackIntent);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    void deleteSubscriptionPrivileged(String iccid, final PendingIntent callbackIntent) {
+        mConnector.deleteSubscription(
+                iccid,
+                new EuiccConnector.DeleteCommandCallback() {
+                    @Override
+                    public void onDeleteComplete(int result) {
+                        Intent extrasIntent = new Intent();
+                        final int resultCode;
+                        switch (result) {
+                            case EuiccService.RESULT_OK:
+                                resultCode = OK;
+                                refreshSubscriptionsAndSendResult(
+                                        callbackIntent, resultCode, extrasIntent);
+                                return;
+                            default:
+                                resultCode = ERROR;
+                                extrasIntent.putExtra(
+                                        EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                                        result);
+                                break;
+                        }
+
+                        sendResult(callbackIntent, resultCode, extrasIntent);
+                    }
+
+                    @Override
+                    public void onEuiccServiceUnavailable() {
+                        sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                    }
+                });
+    }
+
+    @Override
+    public void switchToSubscription(int subscriptionId, String callingPackage,
+            PendingIntent callbackIntent) {
+        switchToSubscription(
+                subscriptionId, false /* forceDeactivateSim */, callingPackage, callbackIntent);
+    }
+
+    void switchToSubscription(int subscriptionId, boolean forceDeactivateSim, String callingPackage,
+            PendingIntent callbackIntent) {
+        boolean callerCanWriteEmbeddedSubscriptions = callerCanWriteEmbeddedSubscriptions();
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
+
+        long token = Binder.clearCallingIdentity();
+        try {
+            if (callerCanWriteEmbeddedSubscriptions) {
+                // Assume that if a privileged caller is calling us, we don't need to prompt the
+                // user about changing carriers, because the caller would only be acting in response
+                // to user action.
+                forceDeactivateSim = true;
+            }
+
+            final String iccid;
+            if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                // Switch to "no" subscription. Only the system can do this.
+                if (!callerCanWriteEmbeddedSubscriptions) {
+                    Log.e(TAG, "Not permitted to switch to empty subscription");
+                    sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                    return;
+                }
+                iccid = null;
+            } else {
+                SubscriptionInfo sub = getSubscriptionForSubscriptionId(subscriptionId);
+                if (sub == null) {
+                    Log.e(TAG, "Cannot switch to nonexistent subscription: " + subscriptionId);
+                    sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                    return;
+                }
+                if (!callerCanWriteEmbeddedSubscriptions
+                        && !sub.canManageSubscription(mContext, callingPackage)) {
+                    Log.e(TAG, "Not permitted to switch to subscription: " + subscriptionId);
+                    sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                    return;
+                }
+                iccid = sub.getIccId();
+            }
+
+            if (!callerCanWriteEmbeddedSubscriptions
+                    && !canManageActiveSubscription(callingPackage)) {
+                // Switch needs consent.
+                Intent extrasIntent = new Intent();
+                addResolutionIntent(extrasIntent,
+                        EuiccService.ACTION_RESOLVE_NO_PRIVILEGES,
+                        callingPackage,
+                        EuiccOperation.forSwitchNoPrivileges(
+                                token, subscriptionId, callingPackage));
+                sendResult(callbackIntent, RESOLVABLE_ERROR, extrasIntent);
+                return;
+            }
+
+            switchToSubscriptionPrivileged(token, subscriptionId, iccid, forceDeactivateSim,
+                    callingPackage, callbackIntent);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    void switchToSubscriptionPrivileged(final long callingToken, int subscriptionId,
+            boolean forceDeactivateSim, final String callingPackage,
+            final PendingIntent callbackIntent) {
+        String iccid = null;
+        SubscriptionInfo sub = getSubscriptionForSubscriptionId(subscriptionId);
+        if (sub != null) {
+            iccid = sub.getIccId();
+        }
+        switchToSubscriptionPrivileged(callingToken, subscriptionId, iccid, forceDeactivateSim,
+                callingPackage, callbackIntent);
+    }
+
+    void switchToSubscriptionPrivileged(final long callingToken, int subscriptionId,
+            @Nullable String iccid, boolean forceDeactivateSim, final String callingPackage,
+            final PendingIntent callbackIntent) {
+        mConnector.switchToSubscription(
+                iccid,
+                forceDeactivateSim,
+                new EuiccConnector.SwitchCommandCallback() {
+                    @Override
+                    public void onSwitchComplete(int result) {
+                        Intent extrasIntent = new Intent();
+                        final int resultCode;
+                        switch (result) {
+                            case EuiccService.RESULT_OK:
+                                resultCode = OK;
+                                break;
+                            case EuiccService.RESULT_MUST_DEACTIVATE_SIM:
+                                resultCode = RESOLVABLE_ERROR;
+                                addResolutionIntent(extrasIntent,
+                                        EuiccService.ACTION_RESOLVE_DEACTIVATE_SIM,
+                                        callingPackage,
+                                        EuiccOperation.forSwitchDeactivateSim(
+                                                callingToken, subscriptionId, callingPackage));
+                                break;
+                            default:
+                                resultCode = ERROR;
+                                extrasIntent.putExtra(
+                                        EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                                        result);
+                                break;
+                        }
+
+                        sendResult(callbackIntent, resultCode, extrasIntent);
+                    }
+
+                    @Override
+                    public void onEuiccServiceUnavailable() {
+                        sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                    }
+                });
+    }
+
+    @Override
+    public void updateSubscriptionNickname(int subscriptionId, String nickname,
+            PendingIntent callbackIntent) {
+        if (!callerCanWriteEmbeddedSubscriptions()) {
+            throw new SecurityException(
+                    "Must have WRITE_EMBEDDED_SUBSCRIPTIONS to update nickname");
+        }
+        long token = Binder.clearCallingIdentity();
+        try {
+            SubscriptionInfo sub = getSubscriptionForSubscriptionId(subscriptionId);
+            if (sub == null) {
+                Log.e(TAG, "Cannot update nickname to nonexistent subscription: " + subscriptionId);
+                sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                return;
+            }
+            mConnector.updateSubscriptionNickname(
+                    sub.getIccId(), nickname,
+                    new EuiccConnector.UpdateNicknameCommandCallback() {
+                        @Override
+                        public void onUpdateNicknameComplete(int result) {
+                            Intent extrasIntent = new Intent();
+                            final int resultCode;
+                            switch (result) {
+                                case EuiccService.RESULT_OK:
+                                    resultCode = OK;
+                                    break;
+                                default:
+                                    resultCode = ERROR;
+                                    extrasIntent.putExtra(
+                                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                                            result);
+                                    break;
+                            }
+
+                            sendResult(callbackIntent, resultCode, extrasIntent);
+                        }
+
+                        @Override
+                        public void onEuiccServiceUnavailable() {
+                            sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                        }
+                    });
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void eraseSubscriptions(PendingIntent callbackIntent) {
+        if (!callerCanWriteEmbeddedSubscriptions()) {
+            throw new SecurityException(
+                    "Must have WRITE_EMBEDDED_SUBSCRIPTIONS to erase subscriptions");
+        }
+        long token = Binder.clearCallingIdentity();
+        try {
+            mConnector.eraseSubscriptions(new EuiccConnector.EraseCommandCallback() {
+                @Override
+                public void onEraseComplete(int result) {
+                    Intent extrasIntent = new Intent();
+                    final int resultCode;
+                    switch (result) {
+                        case EuiccService.RESULT_OK:
+                            resultCode = OK;
+                            refreshSubscriptionsAndSendResult(
+                                    callbackIntent, resultCode, extrasIntent);
+                            return;
+                        default:
+                            resultCode = ERROR;
+                            extrasIntent.putExtra(
+                                    EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                                    result);
+                            break;
+                    }
+
+                    sendResult(callbackIntent, resultCode, extrasIntent);
+                }
+
+                @Override
+                public void onEuiccServiceUnavailable() {
+                    sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                }
+            });
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void retainSubscriptionsForFactoryReset(PendingIntent callbackIntent) {
+        mContext.enforceCallingPermission(Manifest.permission.MASTER_CLEAR,
+                "Must have MASTER_CLEAR to retain subscriptions for factory reset");
+        long token = Binder.clearCallingIdentity();
+        try {
+            mConnector.retainSubscriptions(
+                    new EuiccConnector.RetainSubscriptionsCommandCallback() {
+                        @Override
+                        public void onRetainSubscriptionsComplete(int result) {
+                            Intent extrasIntent = new Intent();
+                            final int resultCode;
+                            switch (result) {
+                                case EuiccService.RESULT_OK:
+                                    resultCode = OK;
+                                    break;
+                                default:
+                                    resultCode = ERROR;
+                                    extrasIntent.putExtra(
+                                            EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE,
+                                            result);
+                                    break;
+                            }
+
+                            sendResult(callbackIntent, resultCode, extrasIntent);
+                        }
+
+                        @Override
+                        public void onEuiccServiceUnavailable() {
+                            sendResult(callbackIntent, ERROR, null /* extrasIntent */);
+                        }
+                    });
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /** Refresh the embedded subscription list and dispatch the given result upon completion. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public void refreshSubscriptionsAndSendResult(
+            PendingIntent callbackIntent, int resultCode, Intent extrasIntent) {
+        SubscriptionController.getInstance()
+                .requestEmbeddedSubscriptionInfoListRefresh(
+                        () -> sendResult(callbackIntent, resultCode, extrasIntent));
+    }
+
+    /** Dispatch the given callback intent with the given result code and data. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public void sendResult(PendingIntent callbackIntent, int resultCode, Intent extrasIntent) {
+        try {
+            callbackIntent.send(mContext, resultCode, extrasIntent);
+        } catch (PendingIntent.CanceledException e) {
+            // Caller canceled the callback; do nothing.
+        }
+    }
+
+    /** Add a resolution intent to the given extras intent. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public void addResolutionIntent(Intent extrasIntent, String resolutionAction,
+            String callingPackage, EuiccOperation op) {
+        Intent intent = new Intent(EuiccManager.ACTION_RESOLVE_ERROR);
+        intent.putExtra(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_ACTION,
+                resolutionAction);
+        intent.putExtra(EuiccService.EXTRA_RESOLUTION_CALLING_PACKAGE, callingPackage);
+        intent.putExtra(EXTRA_OPERATION, op);
+        PendingIntent resolutionIntent = PendingIntent.getActivity(
+                mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_ONE_SHOT);
+        extrasIntent.putExtra(
+                EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT, resolutionIntent);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, "Requires DUMP");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mConnector.dump(fd, pw, args);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Nullable
+    private SubscriptionInfo getSubscriptionForSubscriptionId(int subscriptionId) {
+        List<SubscriptionInfo> subs = mSubscriptionManager.getAvailableSubscriptionInfoList();
+        int subCount = subs.size();
+        for (int i = 0; i < subCount; i++) {
+            SubscriptionInfo sub = subs.get(i);
+            if (subscriptionId == sub.getSubscriptionId()) {
+                return sub;
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    private String blockingGetEidFromEuiccService() {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<String> eidRef = new AtomicReference<>();
+        mConnector.getEid(new EuiccConnector.GetEidCommandCallback() {
+            @Override
+            public void onGetEidComplete(String eid) {
+                eidRef.set(eid);
+                latch.countDown();
+            }
+
+            @Override
+            public void onEuiccServiceUnavailable() {
+                latch.countDown();
+            }
+        });
+        return awaitResult(latch, eidRef);
+    }
+
+    @Nullable
+    private EuiccInfo blockingGetEuiccInfoFromEuiccService() {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<EuiccInfo> euiccInfoRef = new AtomicReference<>();
+        mConnector.getEuiccInfo(new EuiccConnector.GetEuiccInfoCommandCallback() {
+            @Override
+            public void onGetEuiccInfoComplete(EuiccInfo euiccInfo) {
+                euiccInfoRef.set(euiccInfo);
+                latch.countDown();
+            }
+
+            @Override
+            public void onEuiccServiceUnavailable() {
+                latch.countDown();
+            }
+        });
+        return awaitResult(latch, euiccInfoRef);
+    }
+
+    private static <T> T awaitResult(CountDownLatch latch, AtomicReference<T> resultRef) {
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+        return resultRef.get();
+    }
+
+    private boolean canManageActiveSubscription(String callingPackage) {
+        // TODO(b/36260308): We should plumb a slot ID through here for multi-SIM devices.
+        List<SubscriptionInfo> subInfoList = mSubscriptionManager.getActiveSubscriptionInfoList();
+        if (subInfoList == null) {
+            return false;
+        }
+        int size = subInfoList.size();
+        for (int subIndex = 0; subIndex < size; subIndex++) {
+            SubscriptionInfo subInfo = subInfoList.get(subIndex);
+            if (subInfo.isEmbedded() && subInfo.canManageSubscription(mContext, callingPackage)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean callerCanReadPhoneStatePrivileged() {
+        return mContext.checkCallingPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    private boolean callerCanWriteEmbeddedSubscriptions() {
+        return mContext.checkCallingPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns whether the caller has carrier privileges for the active mSubscription on this eUICC.
+     */
+    private boolean callerHasCarrierPrivilegesForActiveSubscription() {
+        // TODO(b/36260308): We should plumb a slot ID through here for multi-SIM devices.
+        TelephonyManager tm =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        return tm.hasCarrierPrivileges();
+    }
+}
diff --git a/com/android/internal/telephony/euicc/EuiccOperation.java b/com/android/internal/telephony/euicc/EuiccOperation.java
new file mode 100644
index 0000000..3b0dbc5
--- /dev/null
+++ b/com/android/internal/telephony/euicc/EuiccOperation.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.euicc;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.euicc.EuiccService;
+import android.telephony.euicc.DownloadableSubscription;
+import android.telephony.euicc.EuiccManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Representation of an {@link EuiccController} operation which failed with a resolvable error.
+ *
+ * <p>This class tracks the operation which failed and the reason for failure. Once the error is
+ * resolved, the operation can be resumed with {@link #continueOperation}.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class EuiccOperation implements Parcelable {
+    private static final String TAG = "EuiccOperation";
+
+    public static final Creator<EuiccOperation> CREATOR = new Creator<EuiccOperation>() {
+        @Override
+        public EuiccOperation createFromParcel(Parcel in) {
+            return new EuiccOperation(in);
+        }
+
+        @Override
+        public EuiccOperation[] newArray(int size) {
+            return new EuiccOperation[size];
+        }
+    };
+
+    @VisibleForTesting
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            ACTION_GET_METADATA_DEACTIVATE_SIM,
+            ACTION_DOWNLOAD_DEACTIVATE_SIM,
+            ACTION_DOWNLOAD_NO_PRIVILEGES,
+    })
+    @interface Action {}
+
+    @VisibleForTesting
+    static final int ACTION_GET_METADATA_DEACTIVATE_SIM = 1;
+    @VisibleForTesting
+    static final int ACTION_DOWNLOAD_DEACTIVATE_SIM = 2;
+    @VisibleForTesting
+    static final int ACTION_DOWNLOAD_NO_PRIVILEGES = 3;
+    @VisibleForTesting
+    static final int ACTION_GET_DEFAULT_LIST_DEACTIVATE_SIM = 4;
+    @VisibleForTesting
+    static final int ACTION_SWITCH_DEACTIVATE_SIM = 5;
+    @VisibleForTesting
+    static final int ACTION_SWITCH_NO_PRIVILEGES = 6;
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public final @Action int mAction;
+
+    private final long mCallingToken;
+
+    @Nullable
+    private final DownloadableSubscription mDownloadableSubscription;
+    private final int mSubscriptionId;
+    private final boolean mSwitchAfterDownload;
+    @Nullable
+    private final String mCallingPackage;
+
+    /**
+     * {@link EuiccManager#getDownloadableSubscriptionMetadata} failed with
+     * {@link EuiccService#RESULT_MUST_DEACTIVATE_SIM}.
+     */
+    public static EuiccOperation forGetMetadataDeactivateSim(long callingToken,
+            DownloadableSubscription subscription, String callingPackage) {
+        return new EuiccOperation(ACTION_GET_METADATA_DEACTIVATE_SIM, callingToken,
+                subscription, 0 /* subscriptionId */, false /* switchAfterDownload */,
+                callingPackage);
+    }
+
+    /**
+     * {@link EuiccManager#downloadSubscription} failed with a mustDeactivateSim error. Should only
+     * be used for privileged callers; for unprivileged callers, use
+     * {@link #forDownloadNoPrivileges} to avoid a double prompt.
+     */
+    public static EuiccOperation forDownloadDeactivateSim(long callingToken,
+            DownloadableSubscription subscription, boolean switchAfterDownload,
+            String callingPackage) {
+        return new EuiccOperation(ACTION_DOWNLOAD_DEACTIVATE_SIM, callingToken,
+                subscription,  0 /* subscriptionId */, switchAfterDownload, callingPackage);
+    }
+
+    /**
+     * {@link EuiccManager#downloadSubscription} failed because the calling app does not have
+     * permission to manage the current active subscription, or because we cannot determine the
+     * privileges without deactivating the current SIM first.
+     */
+    public static EuiccOperation forDownloadNoPrivileges(long callingToken,
+            DownloadableSubscription subscription, boolean switchAfterDownload,
+            String callingPackage) {
+        return new EuiccOperation(ACTION_DOWNLOAD_NO_PRIVILEGES, callingToken,
+                subscription,  0 /* subscriptionId */, switchAfterDownload, callingPackage);
+    }
+
+    static EuiccOperation forGetDefaultListDeactivateSim(long callingToken, String callingPackage) {
+        return new EuiccOperation(ACTION_GET_DEFAULT_LIST_DEACTIVATE_SIM, callingToken,
+                null /* downloadableSubscription */, 0 /* subscriptionId */,
+                false /* switchAfterDownload */, callingPackage);
+    }
+
+    static EuiccOperation forSwitchDeactivateSim(long callingToken, int subscriptionId,
+            String callingPackage) {
+        return new EuiccOperation(ACTION_SWITCH_DEACTIVATE_SIM, callingToken,
+                null /* downloadableSubscription */, subscriptionId,
+                false /* switchAfterDownload */, callingPackage);
+    }
+
+    static EuiccOperation forSwitchNoPrivileges(long callingToken, int subscriptionId,
+            String callingPackage) {
+        return new EuiccOperation(ACTION_SWITCH_NO_PRIVILEGES, callingToken,
+                null /* downloadableSubscription */, subscriptionId,
+                false /* switchAfterDownload */, callingPackage);
+    }
+
+    EuiccOperation(@Action int action,
+            long callingToken,
+            @Nullable DownloadableSubscription downloadableSubscription,
+            int subscriptionId,
+            boolean switchAfterDownload,
+            String callingPackage) {
+        mAction = action;
+        mCallingToken = callingToken;
+        mDownloadableSubscription = downloadableSubscription;
+        mSubscriptionId = subscriptionId;
+        mSwitchAfterDownload = switchAfterDownload;
+        mCallingPackage = callingPackage;
+    }
+
+    EuiccOperation(Parcel in) {
+        mAction = in.readInt();
+        mCallingToken = in.readLong();
+        mDownloadableSubscription = in.readTypedObject(DownloadableSubscription.CREATOR);
+        mSubscriptionId = in.readInt();
+        mSwitchAfterDownload = in.readBoolean();
+        mCallingPackage = in.readString();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mAction);
+        dest.writeLong(mCallingToken);
+        dest.writeTypedObject(mDownloadableSubscription, flags);
+        dest.writeInt(mSubscriptionId);
+        dest.writeBoolean(mSwitchAfterDownload);
+        dest.writeString(mCallingPackage);
+    }
+
+    /**
+     * Resume this operation based on the results of the resolution activity.
+     *
+     * @param resolutionExtras The resolution extras as provided to
+     *     {@link EuiccManager#continueOperation}.
+     * @param callbackIntent The callback intent to trigger after the operation completes.
+     */
+    public void continueOperation(Bundle resolutionExtras, PendingIntent callbackIntent) {
+        // Restore the identity of the caller. We should err on the side of caution and redo any
+        // permission checks before continuing with the operation in case the caller state has
+        // changed. Resolution flows can re-clear the identity if required.
+        Binder.restoreCallingIdentity(mCallingToken);
+
+        switch (mAction) {
+            case ACTION_GET_METADATA_DEACTIVATE_SIM:
+                resolvedGetMetadataDeactivateSim(
+                        resolutionExtras.getBoolean(EuiccService.RESOLUTION_EXTRA_CONSENT),
+                        callbackIntent);
+                break;
+            case ACTION_DOWNLOAD_DEACTIVATE_SIM:
+                resolvedDownloadDeactivateSim(
+                        resolutionExtras.getBoolean(EuiccService.RESOLUTION_EXTRA_CONSENT),
+                        callbackIntent);
+                break;
+            case ACTION_DOWNLOAD_NO_PRIVILEGES:
+                resolvedDownloadNoPrivileges(
+                        resolutionExtras.getBoolean(EuiccService.RESOLUTION_EXTRA_CONSENT),
+                        callbackIntent);
+                break;
+            case ACTION_GET_DEFAULT_LIST_DEACTIVATE_SIM:
+                resolvedGetDefaultListDeactivateSim(
+                        resolutionExtras.getBoolean(EuiccService.RESOLUTION_EXTRA_CONSENT),
+                        callbackIntent);
+                break;
+            case ACTION_SWITCH_DEACTIVATE_SIM:
+                resolvedSwitchDeactivateSim(
+                        resolutionExtras.getBoolean(EuiccService.RESOLUTION_EXTRA_CONSENT),
+                        callbackIntent);
+                break;
+            case ACTION_SWITCH_NO_PRIVILEGES:
+                resolvedSwitchNoPrivileges(
+                        resolutionExtras.getBoolean(EuiccService.RESOLUTION_EXTRA_CONSENT),
+                        callbackIntent);
+                break;
+            default:
+                Log.wtf(TAG, "Unknown action: " + mAction);
+                break;
+        }
+    }
+
+    private void resolvedGetMetadataDeactivateSim(
+            boolean consent, PendingIntent callbackIntent) {
+        if (consent) {
+            // User has consented; perform the lookup, but this time, tell the LPA to deactivate any
+            // required active SIMs.
+            EuiccController.get().getDownloadableSubscriptionMetadata(
+                    mDownloadableSubscription,
+                    true /* forceDeactivateSim */,
+                    mCallingPackage,
+                    callbackIntent);
+        } else {
+            // User has not consented; fail the operation.
+            fail(callbackIntent);
+        }
+    }
+
+    private void resolvedDownloadDeactivateSim(
+            boolean consent, PendingIntent callbackIntent) {
+        if (consent) {
+            // User has consented; perform the download, but this time, tell the LPA to deactivate
+            // any required active SIMs.
+            EuiccController.get().downloadSubscription(
+                    mDownloadableSubscription,
+                    mSwitchAfterDownload,
+                    mCallingPackage,
+                    true /* forceDeactivateSim */,
+                    callbackIntent);
+        } else {
+            // User has not consented; fail the operation.
+            fail(callbackIntent);
+        }
+    }
+
+    private void resolvedDownloadNoPrivileges(boolean consent, PendingIntent callbackIntent) {
+        if (consent) {
+            // User has consented; perform the download with full privileges.
+            long token = Binder.clearCallingIdentity();
+            try {
+                // Note: We turn on "forceDeactivateSim" here under the assumption that the
+                // privilege prompt should also cover permission to deactivate an active SIM, as
+                // the privilege prompt makes it clear that we're switching from the current
+                // carrier.
+                EuiccController.get().downloadSubscriptionPrivileged(
+                        token,
+                        mDownloadableSubscription,
+                        mSwitchAfterDownload,
+                        true /* forceDeactivateSim */,
+                        mCallingPackage,
+                        callbackIntent);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } else {
+            // User has not consented; fail the operation.
+            fail(callbackIntent);
+        }
+    }
+
+    private void resolvedGetDefaultListDeactivateSim(
+            boolean consent, PendingIntent callbackIntent) {
+        if (consent) {
+            // User has consented; perform the lookup, but this time, tell the LPA to deactivate any
+            // required active SIMs.
+            EuiccController.get().getDefaultDownloadableSubscriptionList(
+                    true /* forceDeactivateSim */, mCallingPackage, callbackIntent);
+        } else {
+            // User has not consented; fail the operation.
+            fail(callbackIntent);
+        }
+    }
+
+    private void resolvedSwitchDeactivateSim(
+            boolean consent, PendingIntent callbackIntent) {
+        if (consent) {
+            // User has consented; perform the switch, but this time, tell the LPA to deactivate any
+            // required active SIMs.
+            EuiccController.get().switchToSubscription(
+                    mSubscriptionId,
+                    true /* forceDeactivateSim */,
+                    mCallingPackage,
+                    callbackIntent);
+        } else {
+            // User has not consented; fail the operation.
+            fail(callbackIntent);
+        }
+    }
+
+    private void resolvedSwitchNoPrivileges(boolean consent, PendingIntent callbackIntent) {
+        if (consent) {
+            // User has consented; perform the switch with full privileges.
+            long token = Binder.clearCallingIdentity();
+            try {
+                // Note: We turn on "forceDeactivateSim" here under the assumption that the
+                // privilege prompt should also cover permission to deactivate an active SIM, as
+                // the privilege prompt makes it clear that we're switching from the current
+                // carrier. Also note that in practice, we'd need to deactivate the active SIM to
+                // even reach this point, because we cannot fetch the metadata needed to check the
+                // privileges without doing so.
+                EuiccController.get().switchToSubscriptionPrivileged(
+                        token,
+                        mSubscriptionId,
+                        true /* forceDeactivateSim */,
+                        mCallingPackage,
+                        callbackIntent);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } else {
+            // User has not consented; fail the operation.
+            fail(callbackIntent);
+        }
+    }
+
+    private static void fail(PendingIntent callbackIntent) {
+        EuiccController.get().sendResult(
+                callbackIntent,
+                EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_ERROR,
+                null /* extrasIntent */);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java b/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
new file mode 100644
index 0000000..dec9805
--- /dev/null
+++ b/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
@@ -0,0 +1,243 @@
+/*
+ * 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.internal.telephony.gsm;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.CellLocation;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.telephony.TelephonyManager;
+import android.telephony.gsm.GsmCellLocation;
+
+import com.android.internal.telephony.CellBroadcastHandler;
+import com.android.internal.telephony.Phone;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
+ */
+public class GsmCellBroadcastHandler extends CellBroadcastHandler {
+    private static final boolean VDBG = false;  // log CB PDU data
+
+    /** This map holds incomplete concatenated messages waiting for assembly. */
+    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
+            new HashMap<SmsCbConcatInfo, byte[][]>(4);
+
+    protected GsmCellBroadcastHandler(Context context, Phone phone) {
+        super("GsmCellBroadcastHandler", context, phone);
+        phone.mCi.setOnNewGsmBroadcastSms(getHandler(), EVENT_NEW_SMS_MESSAGE, null);
+    }
+
+    @Override
+    protected void onQuitting() {
+        mPhone.mCi.unSetOnNewGsmBroadcastSms(getHandler());
+        super.onQuitting();     // release wakelock
+    }
+
+    /**
+     * Create a new CellBroadcastHandler.
+     * @param context the context to use for dispatching Intents
+     * @return the new handler
+     */
+    public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context,
+            Phone phone) {
+        GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, phone);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Handle 3GPP-format Cell Broadcast messages sent from radio.
+     *
+     * @param message the message to process
+     * @return true if an ordered broadcast was sent; false on failure
+     */
+    @Override
+    protected boolean handleSmsMessage(Message message) {
+        if (message.obj instanceof AsyncResult) {
+            SmsCbMessage cbMessage = handleGsmBroadcastSms((AsyncResult) message.obj);
+            if (cbMessage != null) {
+                handleBroadcastSms(cbMessage);
+                return true;
+            }
+        }
+        return super.handleSmsMessage(message);
+    }
+
+    /**
+     * Handle 3GPP format SMS-CB message.
+     * @param ar the AsyncResult containing the received PDUs
+     */
+    private SmsCbMessage handleGsmBroadcastSms(AsyncResult ar) {
+        try {
+            byte[] receivedPdu = (byte[]) ar.result;
+
+            if (VDBG) {
+                int pduLength = receivedPdu.length;
+                for (int i = 0; i < pduLength; i += 8) {
+                    StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
+                    for (int j = i; j < i + 8 && j < pduLength; j++) {
+                        int b = receivedPdu[j] & 0xff;
+                        if (b < 0x10) {
+                            sb.append('0');
+                        }
+                        sb.append(Integer.toHexString(b)).append(' ');
+                    }
+                    log(sb.toString());
+                }
+            }
+
+            SmsCbHeader header = new SmsCbHeader(receivedPdu);
+            String plmn = TelephonyManager.from(mContext).getNetworkOperatorForPhone(
+                    mPhone.getPhoneId());
+            int lac = -1;
+            int cid = -1;
+            CellLocation cl = mPhone.getCellLocation();
+            // Check if cell location is GsmCellLocation.  This is required to support
+            // dual-mode devices such as CDMA/LTE devices that require support for
+            // both 3GPP and 3GPP2 format messages
+            if (cl instanceof GsmCellLocation) {
+                GsmCellLocation cellLocation = (GsmCellLocation)cl;
+                lac = cellLocation.getLac();
+                cid = cellLocation.getCid();
+            }
+
+            SmsCbLocation location;
+            switch (header.getGeographicalScope()) {
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
+                    location = new SmsCbLocation(plmn, lac, -1);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
+                    location = new SmsCbLocation(plmn, lac, cid);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
+                default:
+                    location = new SmsCbLocation(plmn);
+                    break;
+            }
+
+            byte[][] pdus;
+            int pageCount = header.getNumberOfPages();
+            if (pageCount > 1) {
+                // Multi-page message
+                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
+
+                // Try to find other pages of the same message
+                pdus = mSmsCbPageMap.get(concatInfo);
+
+                if (pdus == null) {
+                    // This is the first page of this message, make room for all
+                    // pages and keep until complete
+                    pdus = new byte[pageCount][];
+
+                    mSmsCbPageMap.put(concatInfo, pdus);
+                }
+
+                // Page parameter is one-based
+                pdus[header.getPageIndex() - 1] = receivedPdu;
+
+                for (byte[] pdu : pdus) {
+                    if (pdu == null) {
+                        // Still missing pages, exit
+                        return null;
+                    }
+                }
+
+                // Message complete, remove and dispatch
+                mSmsCbPageMap.remove(concatInfo);
+            } else {
+                // Single page message
+                pdus = new byte[1][];
+                pdus[0] = receivedPdu;
+            }
+
+            // Remove messages that are out of scope to prevent the map from
+            // growing indefinitely, containing incomplete messages that were
+            // never assembled
+            Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
+
+            while (iter.hasNext()) {
+                SmsCbConcatInfo info = iter.next();
+
+                if (!info.matchesLocation(plmn, lac, cid)) {
+                    iter.remove();
+                }
+            }
+
+            return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus);
+
+        } catch (RuntimeException e) {
+            loge("Error in decoding SMS CB pdu", e);
+            return null;
+        }
+    }
+
+    /**
+     * Holds all info about a message page needed to assemble a complete concatenated message.
+     */
+    private static final class SmsCbConcatInfo {
+
+        private final SmsCbHeader mHeader;
+        private final SmsCbLocation mLocation;
+
+        SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
+            mHeader = header;
+            mLocation = location;
+        }
+
+        @Override
+        public int hashCode() {
+            return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof SmsCbConcatInfo) {
+                SmsCbConcatInfo other = (SmsCbConcatInfo)obj;
+
+                // Two pages match if they have the same serial number (which includes the
+                // geographical scope and update number), and both pages belong to the same
+                // location (PLMN, plus LAC and CID if these are part of the geographical scope).
+                return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
+                        && mLocation.equals(other.mLocation);
+            }
+
+            return false;
+        }
+
+        /**
+         * Compare the location code for this message to the current location code. The match is
+         * relative to the geographical scope of the message, which determines whether the LAC
+         * and Cell ID are saved in mLocation or set to -1 to match all values.
+         *
+         * @param plmn the current PLMN
+         * @param lac the current Location Area (GSM) or Service Area (UMTS)
+         * @param cid the current Cell ID
+         * @return true if this message is valid for the current location; false otherwise
+         */
+        public boolean matchesLocation(String plmn, int lac, int cid) {
+            return mLocation.isInLocationArea(plmn, lac, cid);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java b/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
new file mode 100644
index 0000000..3fe2a33
--- /dev/null
+++ b/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
@@ -0,0 +1,213 @@
+/*
+ * 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.internal.telephony.gsm;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Message;
+import android.provider.Telephony.Sms.Intents;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.InboundSmsHandler;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.SmsStorageMonitor;
+import com.android.internal.telephony.VisualVoicemailSmsFilter;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.uicc.UsimServiceTable;
+
+/**
+ * This class broadcasts incoming SMS messages to interested apps after storing them in
+ * the SmsProvider "raw" table and ACKing them to the SMSC. After each message has been
+ */
+public class GsmInboundSmsHandler extends InboundSmsHandler {
+
+    /** Handler for SMS-PP data download messages to UICC. */
+    private final UsimDataDownloadHandler mDataDownloadHandler;
+
+    /**
+     * Create a new GSM inbound SMS handler.
+     */
+    private GsmInboundSmsHandler(Context context, SmsStorageMonitor storageMonitor,
+            Phone phone) {
+        super("GsmInboundSmsHandler", context, storageMonitor, phone,
+                GsmCellBroadcastHandler.makeGsmCellBroadcastHandler(context, phone));
+        phone.mCi.setOnNewGsmSms(getHandler(), EVENT_NEW_SMS, null);
+        mDataDownloadHandler = new UsimDataDownloadHandler(phone.mCi);
+    }
+
+    /**
+     * Unregister for GSM SMS.
+     */
+    @Override
+    protected void onQuitting() {
+        mPhone.mCi.unSetOnNewGsmSms(getHandler());
+        mCellBroadcastHandler.dispose();
+
+        if (DBG) log("unregistered for 3GPP SMS");
+        super.onQuitting();     // release wakelock
+    }
+
+    /**
+     * Wait for state machine to enter startup state. We can't send any messages until then.
+     */
+    public static GsmInboundSmsHandler makeInboundSmsHandler(Context context,
+            SmsStorageMonitor storageMonitor, Phone phone) {
+        GsmInboundSmsHandler handler = new GsmInboundSmsHandler(context, storageMonitor, phone);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Return true if this handler is for 3GPP2 messages; false for 3GPP format.
+     * @return false (3GPP)
+     */
+    @Override
+    protected boolean is3gpp2() {
+        return false;
+    }
+
+    /**
+     * Handle type zero, SMS-PP data download, and 3GPP/CPHS MWI type SMS. Normal SMS messages
+     * are handled by {@link #dispatchNormalMessage} in parent class.
+     *
+     * @param smsb the SmsMessageBase object from the RIL
+     * @return a result code from {@link android.provider.Telephony.Sms.Intents},
+     *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
+     */
+    @Override
+    protected int dispatchMessageRadioSpecific(SmsMessageBase smsb) {
+        SmsMessage sms = (SmsMessage) smsb;
+
+        if (sms.isTypeZero()) {
+            // Some carriers will send visual voicemail SMS as type zero.
+            int destPort = -1;
+            SmsHeader smsHeader = sms.getUserDataHeader();
+            if (smsHeader != null && smsHeader.portAddrs != null) {
+                // The message was sent to a port.
+                destPort = smsHeader.portAddrs.destPort;
+            }
+            VisualVoicemailSmsFilter
+                    .filter(mContext, new byte[][]{sms.getPdu()}, SmsConstants.FORMAT_3GPP,
+                            destPort, mPhone.getSubId());
+            // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be
+            // Displayed/Stored/Notified. They should only be acknowledged.
+            log("Received short message type 0, Don't display or store it. Send Ack");
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        // Send SMS-PP data download messages to UICC. See 3GPP TS 31.111 section 7.1.1.
+        if (sms.isUsimDataDownload()) {
+            UsimServiceTable ust = mPhone.getUsimServiceTable();
+            return mDataDownloadHandler.handleUsimDataDownload(ust, sms);
+        }
+
+        boolean handled = false;
+        if (sms.isMWISetMessage()) {
+            updateMessageWaitingIndicator(sms.getNumOfVoicemails());
+            handled = sms.isMwiDontStore();
+            if (DBG) log("Received voice mail indicator set SMS shouldStore=" + !handled);
+        } else if (sms.isMWIClearMessage()) {
+            updateMessageWaitingIndicator(0);
+            handled = sms.isMwiDontStore();
+            if (DBG) log("Received voice mail indicator clear SMS shouldStore=" + !handled);
+        }
+        if (handled) {
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        if (!mStorageMonitor.isStorageAvailable() &&
+                sms.getMessageClass() != SmsConstants.MessageClass.CLASS_0) {
+            // It's a storable message and there's no storage available.  Bail.
+            // (See TS 23.038 for a description of class 0 messages.)
+            return Intents.RESULT_SMS_OUT_OF_MEMORY;
+        }
+
+        return dispatchNormalMessage(smsb);
+    }
+
+    private void updateMessageWaitingIndicator(int voicemailCount) {
+        // range check
+        if (voicemailCount < 0) {
+            voicemailCount = -1;
+        } else if (voicemailCount > 0xff) {
+            // TS 23.040 9.2.3.24.2
+            // "The value 255 shall be taken to mean 255 or greater"
+            voicemailCount = 0xff;
+        }
+        // update voice mail count in Phone
+        mPhone.setVoiceMessageCount(voicemailCount);
+        // store voice mail count in SIM & shared preferences
+        IccRecords records = UiccController.getInstance().getIccRecords(
+                mPhone.getPhoneId(), UiccController.APP_FAM_3GPP);
+        if (records != null) {
+            log("updateMessageWaitingIndicator: updating SIM Records");
+            records.setVoiceMessageWaiting(1, voicemailCount);
+        } else {
+            log("updateMessageWaitingIndicator: SIM Records not found");
+        }
+    }
+
+    /**
+     * Send an acknowledge message.
+     * @param success indicates that last message was successfully received.
+     * @param result result code indicating any error
+     * @param response callback message sent when operation completes.
+     */
+    @Override
+    protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) {
+        mPhone.mCi.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response);
+    }
+
+    /**
+     * Called when the phone changes the default method updates mPhone
+     * mStorageMonitor and mCellBroadcastHandler.updatePhoneObject.
+     * Override if different or other behavior is desired.
+     *
+     * @param phone
+     */
+    @Override
+    protected void onUpdatePhoneObject(Phone phone) {
+        super.onUpdatePhoneObject(phone);
+        log("onUpdatePhoneObject: dispose of old CellBroadcastHandler and make a new one");
+        mCellBroadcastHandler.dispose();
+        mCellBroadcastHandler = GsmCellBroadcastHandler
+                .makeGsmCellBroadcastHandler(mContext, phone);
+    }
+
+    /**
+     * Convert Android result code to 3GPP SMS failure cause.
+     * @param rc the Android SMS intent result value
+     * @return 0 for success, or a 3GPP SMS failure cause value
+     */
+    private static int resultToCause(int rc) {
+        switch (rc) {
+            case Activity.RESULT_OK:
+            case Intents.RESULT_SMS_HANDLED:
+                // Cause code is ignored on success.
+                return 0;
+            case Intents.RESULT_SMS_OUT_OF_MEMORY:
+                return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED;
+            case Intents.RESULT_SMS_GENERIC_ERROR:
+            default:
+                return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/gsm/GsmMmiCode.java b/com/android/internal/telephony/gsm/GsmMmiCode.java
new file mode 100644
index 0000000..54c4d29
--- /dev/null
+++ b/com/android/internal/telephony/gsm/GsmMmiCode.java
@@ -0,0 +1,1694 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.gsm;
+
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_DATA;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_DATA_ASYNC;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_DATA_SYNC;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_FAX;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_MAX;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_NONE;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_PACKET;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_PAD;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_SMS;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_VOICE;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.text.BidiFormatter;
+import android.text.SpannableStringBuilder;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The motto for this file is:
+ *
+ * "NOTE:    By using the # as a separator, most cases are expected to be unambiguous."
+ *   -- TS 22.030 6.5.2
+ *
+ * {@hide}
+ *
+ */
+public final class GsmMmiCode extends Handler implements MmiCode {
+    static final String LOG_TAG = "GsmMmiCode";
+
+    //***** Constants
+
+    // Max Size of the Short Code (aka Short String from TS 22.030 6.5.2)
+    static final int MAX_LENGTH_SHORT_CODE = 2;
+
+    // TS 22.030 6.5.2 Every Short String USSD command will end with #-key
+    // (known as #-String)
+    static final char END_OF_USSD_COMMAND = '#';
+
+    // From TS 22.030 6.5.2
+    static final String ACTION_ACTIVATE = "*";
+    static final String ACTION_DEACTIVATE = "#";
+    static final String ACTION_INTERROGATE = "*#";
+    static final String ACTION_REGISTER = "**";
+    static final String ACTION_ERASURE = "##";
+
+    // Supp Service codes from TS 22.030 Annex B
+
+    //Called line presentation
+    static final String SC_CLIP    = "30";
+    static final String SC_CLIR    = "31";
+
+    // Call Forwarding
+    static final String SC_CFU     = "21";
+    static final String SC_CFB     = "67";
+    static final String SC_CFNRy   = "61";
+    static final String SC_CFNR    = "62";
+
+    static final String SC_CF_All = "002";
+    static final String SC_CF_All_Conditional = "004";
+
+    // Call Waiting
+    static final String SC_WAIT     = "43";
+
+    // Call Barring
+    static final String SC_BAOC         = "33";
+    static final String SC_BAOIC        = "331";
+    static final String SC_BAOICxH      = "332";
+    static final String SC_BAIC         = "35";
+    static final String SC_BAICr        = "351";
+
+    static final String SC_BA_ALL       = "330";
+    static final String SC_BA_MO        = "333";
+    static final String SC_BA_MT        = "353";
+
+    // Supp Service Password registration
+    static final String SC_PWD          = "03";
+
+    // PIN/PIN2/PUK/PUK2
+    static final String SC_PIN          = "04";
+    static final String SC_PIN2         = "042";
+    static final String SC_PUK          = "05";
+    static final String SC_PUK2         = "052";
+
+    //***** Event Constants
+
+    static final int EVENT_SET_COMPLETE         = 1;
+    static final int EVENT_GET_CLIR_COMPLETE    = 2;
+    static final int EVENT_QUERY_CF_COMPLETE    = 3;
+    static final int EVENT_USSD_COMPLETE        = 4;
+    static final int EVENT_QUERY_COMPLETE       = 5;
+    static final int EVENT_SET_CFF_COMPLETE     = 6;
+    static final int EVENT_USSD_CANCEL_COMPLETE = 7;
+
+    //***** Instance Variables
+
+    GsmCdmaPhone mPhone;
+    Context mContext;
+    UiccCardApplication mUiccApplication;
+    IccRecords mIccRecords;
+
+    String mAction;              // One of ACTION_*
+    String mSc;                  // Service Code
+    String mSia, mSib, mSic;       // Service Info a,b,c
+    String mPoundString;         // Entire MMI string up to and including #
+    public String mDialingNumber;
+    String mPwd;                 // For password registration
+
+    /** Set to true in processCode, not at newFromDialString time */
+    private boolean mIsPendingUSSD;
+
+    private boolean mIsUssdRequest;
+
+    private boolean mIsCallFwdReg;
+    State mState = State.PENDING;
+    CharSequence mMessage;
+    private boolean mIsSsInfo = false;
+    private ResultReceiver mCallbackReceiver;
+
+
+    //***** Class Variables
+
+
+    // See TS 22.030 6.5.2 "Structure of the MMI"
+
+    static Pattern sPatternSuppService = Pattern.compile(
+        "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)");
+/*       1  2                    3          4  5       6   7         8    9     10  11             12
+
+         1 = Full string up to and including #
+         2 = action (activation/interrogation/registration/erasure)
+         3 = service code
+         5 = SIA
+         7 = SIB
+         9 = SIC
+         10 = dialing number
+*/
+
+    static final int MATCH_GROUP_POUND_STRING = 1;
+
+    static final int MATCH_GROUP_ACTION = 2;
+                        //(activation/interrogation/registration/erasure)
+
+    static final int MATCH_GROUP_SERVICE_CODE = 3;
+    static final int MATCH_GROUP_SIA = 5;
+    static final int MATCH_GROUP_SIB = 7;
+    static final int MATCH_GROUP_SIC = 9;
+    static final int MATCH_GROUP_PWD_CONFIRM = 11;
+    static final int MATCH_GROUP_DIALING_NUMBER = 12;
+    static private String[] sTwoDigitNumberPattern;
+
+    //***** Public Class methods
+
+    /**
+     * Some dial strings in GSM are defined to do non-call setup
+     * things, such as modify or query supplementary service settings (eg, call
+     * forwarding). These are generally referred to as "MMI codes".
+     * We look to see if the dial string contains a valid MMI code (potentially
+     * with a dial string at the end as well) and return info here.
+     *
+     * If the dial string contains no MMI code, we return an instance with
+     * only "dialingNumber" set
+     *
+     * Please see flow chart in TS 22.030 6.5.3.2
+     */
+    public static GsmMmiCode newFromDialString(String dialString, GsmCdmaPhone phone,
+            UiccCardApplication app) {
+        return newFromDialString(dialString, phone, app, null);
+    }
+
+    public static GsmMmiCode newFromDialString(String dialString, GsmCdmaPhone phone,
+            UiccCardApplication app, ResultReceiver wrappedCallback) {
+        Matcher m;
+        GsmMmiCode ret = null;
+
+        if (phone.getServiceState().getVoiceRoaming()
+                && phone.supportsConversionOfCdmaCallerIdMmiCodesWhileRoaming()) {
+            /* The CDMA MMI coded dialString will be converted to a 3GPP MMI Coded dialString
+               so that it can be processed by the matcher and code below
+             */
+            dialString = convertCdmaMmiCodesTo3gppMmiCodes(dialString);
+        }
+
+        m = sPatternSuppService.matcher(dialString);
+
+        // Is this formatted like a standard supplementary service code?
+        if (m.matches()) {
+            ret = new GsmMmiCode(phone, app);
+            ret.mPoundString = makeEmptyNull(m.group(MATCH_GROUP_POUND_STRING));
+            ret.mAction = makeEmptyNull(m.group(MATCH_GROUP_ACTION));
+            ret.mSc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE));
+            ret.mSia = makeEmptyNull(m.group(MATCH_GROUP_SIA));
+            ret.mSib = makeEmptyNull(m.group(MATCH_GROUP_SIB));
+            ret.mSic = makeEmptyNull(m.group(MATCH_GROUP_SIC));
+            ret.mPwd = makeEmptyNull(m.group(MATCH_GROUP_PWD_CONFIRM));
+            ret.mDialingNumber = makeEmptyNull(m.group(MATCH_GROUP_DIALING_NUMBER));
+            ret.mCallbackReceiver = wrappedCallback;
+            // According to TS 22.030 6.5.2 "Structure of the MMI",
+            // the dialing number should not ending with #.
+            // The dialing number ending # is treated as unique USSD,
+            // eg, *400#16 digit number# to recharge the prepaid card
+            // in India operator(Mumbai MTNL)
+            if(ret.mDialingNumber != null &&
+                    ret.mDialingNumber.endsWith("#") &&
+                    dialString.endsWith("#")){
+                ret = new GsmMmiCode(phone, app);
+                ret.mPoundString = dialString;
+            }
+        } else if (dialString.endsWith("#")) {
+            // TS 22.030 sec 6.5.3.2
+            // "Entry of any characters defined in the 3GPP TS 23.038 [8] Default Alphabet
+            // (up to the maximum defined in 3GPP TS 24.080 [10]), followed by #SEND".
+
+            ret = new GsmMmiCode(phone, app);
+            ret.mPoundString = dialString;
+        } else if (isTwoDigitShortCode(phone.getContext(), dialString)) {
+            //Is a country-specific exception to short codes as defined in TS 22.030, 6.5.3.2
+            ret = null;
+        } else if (isShortCode(dialString, phone)) {
+            // this may be a short code, as defined in TS 22.030, 6.5.3.2
+            ret = new GsmMmiCode(phone, app);
+            ret.mDialingNumber = dialString;
+        }
+
+        return ret;
+    }
+
+    private static String convertCdmaMmiCodesTo3gppMmiCodes(String dialString) {
+        Matcher m;
+        m = sPatternCdmaMmiCodeWhileRoaming.matcher(dialString);
+        if (m.matches()) {
+            String serviceCode = makeEmptyNull(m.group(MATCH_GROUP_CDMA_MMI_CODE_SERVICE_CODE));
+            String prefix = m.group(MATCH_GROUP_CDMA_MMI_CODE_NUMBER_PREFIX);
+            String number = makeEmptyNull(m.group(MATCH_GROUP_CDMA_MMI_CODE_NUMBER));
+
+            if (serviceCode.equals("67") && number != null) {
+                // "#31#number" to invoke CLIR
+                dialString = ACTION_DEACTIVATE + SC_CLIR + ACTION_DEACTIVATE + prefix + number;
+            } else if (serviceCode.equals("82") && number != null) {
+                // "*31#number" to suppress CLIR
+                dialString = ACTION_ACTIVATE + SC_CLIR + ACTION_DEACTIVATE + prefix + number;
+            }
+        }
+        return dialString;
+    }
+
+    public static GsmMmiCode
+    newNetworkInitiatedUssd(String ussdMessage,
+                            boolean isUssdRequest, GsmCdmaPhone phone, UiccCardApplication app) {
+        GsmMmiCode ret;
+
+        ret = new GsmMmiCode(phone, app);
+
+        ret.mMessage = ussdMessage;
+        ret.mIsUssdRequest = isUssdRequest;
+
+        // If it's a request, set to PENDING so that it's cancelable.
+        if (isUssdRequest) {
+            ret.mIsPendingUSSD = true;
+            ret.mState = State.PENDING;
+        } else {
+            ret.mState = State.COMPLETE;
+        }
+
+        return ret;
+    }
+
+    public static GsmMmiCode newFromUssdUserInput(String ussdMessge,
+                                                  GsmCdmaPhone phone,
+                                                  UiccCardApplication app) {
+        GsmMmiCode ret = new GsmMmiCode(phone, app);
+
+        ret.mMessage = ussdMessge;
+        ret.mState = State.PENDING;
+        ret.mIsPendingUSSD = true;
+
+        return ret;
+    }
+
+    /** Process SS Data */
+    public void
+    processSsData(AsyncResult data) {
+        Rlog.d(LOG_TAG, "In processSsData");
+
+        mIsSsInfo = true;
+        try {
+            SsData ssData = (SsData)data.result;
+            parseSsData(ssData);
+        } catch (ClassCastException ex) {
+            Rlog.e(LOG_TAG, "Class Cast Exception in parsing SS Data : " + ex);
+        } catch (NullPointerException ex) {
+            Rlog.e(LOG_TAG, "Null Pointer Exception in parsing SS Data : " + ex);
+        }
+    }
+
+    void parseSsData(SsData ssData) {
+        CommandException ex;
+
+        ex = CommandException.fromRilErrno(ssData.result);
+        mSc = getScStringFromScType(ssData.serviceType);
+        mAction = getActionStringFromReqType(ssData.requestType);
+        Rlog.d(LOG_TAG, "parseSsData msc = " + mSc + ", action = " + mAction + ", ex = " + ex);
+
+        switch (ssData.requestType) {
+            case SS_ACTIVATION:
+            case SS_DEACTIVATION:
+            case SS_REGISTRATION:
+            case SS_ERASURE:
+                if ((ssData.result == RILConstants.SUCCESS) &&
+                      ssData.serviceType.isTypeUnConditional()) {
+                    /*
+                     * When ServiceType is SS_CFU/SS_CF_ALL and RequestType is activate/register
+                     * and ServiceClass is Voice/None, set IccRecords.setVoiceCallForwardingFlag.
+                     * Only CF status can be set here since number is not available.
+                     */
+                    boolean cffEnabled = ((ssData.requestType == SsData.RequestType.SS_ACTIVATION ||
+                            ssData.requestType == SsData.RequestType.SS_REGISTRATION) &&
+                            isServiceClassVoiceorNone(ssData.serviceClass));
+
+                    Rlog.d(LOG_TAG, "setVoiceCallForwardingFlag cffEnabled: " + cffEnabled);
+                    if (mIccRecords != null) {
+                        mPhone.setVoiceCallForwardingFlag(1, cffEnabled, null);
+                        Rlog.d(LOG_TAG, "setVoiceCallForwardingFlag done from SS Info.");
+                    } else {
+                        Rlog.e(LOG_TAG, "setVoiceCallForwardingFlag aborted. sim records is null.");
+                    }
+                }
+                onSetComplete(null, new AsyncResult(null, ssData.cfInfo, ex));
+                break;
+            case SS_INTERROGATION:
+                if (ssData.serviceType.isTypeClir()) {
+                    Rlog.d(LOG_TAG, "CLIR INTERROGATION");
+                    onGetClirComplete(new AsyncResult(null, ssData.ssInfo, ex));
+                } else if (ssData.serviceType.isTypeCF()) {
+                    Rlog.d(LOG_TAG, "CALL FORWARD INTERROGATION");
+                    onQueryCfComplete(new AsyncResult(null, ssData.cfInfo, ex));
+                } else {
+                    onQueryComplete(new AsyncResult(null, ssData.ssInfo, ex));
+                }
+                break;
+            default:
+                Rlog.e(LOG_TAG, "Invaid requestType in SSData : " + ssData.requestType);
+                break;
+        }
+    }
+
+    private String getScStringFromScType(SsData.ServiceType sType) {
+        switch (sType) {
+            case SS_CFU:
+                return SC_CFU;
+            case SS_CF_BUSY:
+                return SC_CFB;
+            case SS_CF_NO_REPLY:
+                return SC_CFNRy;
+            case SS_CF_NOT_REACHABLE:
+                return SC_CFNR;
+            case SS_CF_ALL:
+                return SC_CF_All;
+            case SS_CF_ALL_CONDITIONAL:
+                return SC_CF_All_Conditional;
+            case SS_CLIP:
+                return SC_CLIP;
+            case SS_CLIR:
+                return SC_CLIR;
+            case SS_WAIT:
+                return SC_WAIT;
+            case SS_BAOC:
+                return SC_BAOC;
+            case SS_BAOIC:
+                return SC_BAOIC;
+            case SS_BAOIC_EXC_HOME:
+                return SC_BAOICxH;
+            case SS_BAIC:
+                return SC_BAIC;
+            case SS_BAIC_ROAMING:
+                return SC_BAICr;
+            case SS_ALL_BARRING:
+                return SC_BA_ALL;
+            case SS_OUTGOING_BARRING:
+                return SC_BA_MO;
+            case SS_INCOMING_BARRING:
+                return SC_BA_MT;
+        }
+
+        return "";
+    }
+
+    private String getActionStringFromReqType(SsData.RequestType rType) {
+        switch (rType) {
+            case SS_ACTIVATION:
+                return ACTION_ACTIVATE;
+            case SS_DEACTIVATION:
+                return ACTION_DEACTIVATE;
+            case SS_INTERROGATION:
+                return ACTION_INTERROGATE;
+            case SS_REGISTRATION:
+                return ACTION_REGISTER;
+            case SS_ERASURE:
+                return ACTION_ERASURE;
+        }
+
+        return "";
+    }
+
+    private boolean isServiceClassVoiceorNone(int serviceClass) {
+        return (((serviceClass & CommandsInterface.SERVICE_CLASS_VOICE) != 0) ||
+                (serviceClass == CommandsInterface.SERVICE_CLASS_NONE));
+    }
+
+    //***** Private Class methods
+
+    /** make empty strings be null.
+     *  Regexp returns empty strings for empty groups
+     */
+    private static String
+    makeEmptyNull (String s) {
+        if (s != null && s.length() == 0) return null;
+
+        return s;
+    }
+
+    /** returns true of the string is empty or null */
+    private static boolean
+    isEmptyOrNull(CharSequence s) {
+        return s == null || (s.length() == 0);
+    }
+
+
+    private static int
+    scToCallForwardReason(String sc) {
+        if (sc == null) {
+            throw new RuntimeException ("invalid call forward sc");
+        }
+
+        if (sc.equals(SC_CF_All)) {
+           return CommandsInterface.CF_REASON_ALL;
+        } else if (sc.equals(SC_CFU)) {
+            return CommandsInterface.CF_REASON_UNCONDITIONAL;
+        } else if (sc.equals(SC_CFB)) {
+            return CommandsInterface.CF_REASON_BUSY;
+        } else if (sc.equals(SC_CFNR)) {
+            return CommandsInterface.CF_REASON_NOT_REACHABLE;
+        } else if (sc.equals(SC_CFNRy)) {
+            return CommandsInterface.CF_REASON_NO_REPLY;
+        } else if (sc.equals(SC_CF_All_Conditional)) {
+           return CommandsInterface.CF_REASON_ALL_CONDITIONAL;
+        } else {
+            throw new RuntimeException ("invalid call forward sc");
+        }
+    }
+
+    private static int
+    siToServiceClass(String si) {
+        if (si == null || si.length() == 0) {
+                return  SERVICE_CLASS_NONE;
+        } else {
+            // NumberFormatException should cause MMI fail
+            int serviceCode = Integer.parseInt(si, 10);
+
+            switch (serviceCode) {
+                case 10: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX  + SERVICE_CLASS_VOICE;
+                case 11: return SERVICE_CLASS_VOICE;
+                case 12: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX;
+                case 13: return SERVICE_CLASS_FAX;
+
+                case 16: return SERVICE_CLASS_SMS;
+
+                case 19: return SERVICE_CLASS_FAX + SERVICE_CLASS_VOICE;
+/*
+    Note for code 20:
+     From TS 22.030 Annex C:
+                "All GPRS bearer services" are not included in "All tele and bearer services"
+                    and "All bearer services"."
+....so SERVICE_CLASS_DATA, which (according to 27.007) includes GPRS
+*/
+                case 20: return SERVICE_CLASS_DATA_ASYNC + SERVICE_CLASS_DATA_SYNC;
+
+                case 21: return SERVICE_CLASS_PAD + SERVICE_CLASS_DATA_ASYNC;
+                case 22: return SERVICE_CLASS_PACKET + SERVICE_CLASS_DATA_SYNC;
+                case 24: return SERVICE_CLASS_DATA_SYNC;
+                case 25: return SERVICE_CLASS_DATA_ASYNC;
+                case 26: return SERVICE_CLASS_DATA_SYNC + SERVICE_CLASS_VOICE;
+                case 99: return SERVICE_CLASS_PACKET;
+
+                default:
+                    throw new RuntimeException("unsupported MMI service code " + si);
+            }
+        }
+    }
+
+    private static int
+    siToTime (String si) {
+        if (si == null || si.length() == 0) {
+            return 0;
+        } else {
+            // NumberFormatException should cause MMI fail
+            return Integer.parseInt(si, 10);
+        }
+    }
+
+    static boolean
+    isServiceCodeCallForwarding(String sc) {
+        return sc != null &&
+                (sc.equals(SC_CFU)
+                || sc.equals(SC_CFB) || sc.equals(SC_CFNRy)
+                || sc.equals(SC_CFNR) || sc.equals(SC_CF_All)
+                || sc.equals(SC_CF_All_Conditional));
+    }
+
+    static boolean
+    isServiceCodeCallBarring(String sc) {
+        Resources resource = Resources.getSystem();
+        if (sc != null) {
+            String[] barringMMI = resource.getStringArray(
+                com.android.internal.R.array.config_callBarringMMI);
+            if (barringMMI != null) {
+                for (String match : barringMMI) {
+                    if (sc.equals(match)) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    static String
+    scToBarringFacility(String sc) {
+        if (sc == null) {
+            throw new RuntimeException ("invalid call barring sc");
+        }
+
+        if (sc.equals(SC_BAOC)) {
+            return CommandsInterface.CB_FACILITY_BAOC;
+        } else if (sc.equals(SC_BAOIC)) {
+            return CommandsInterface.CB_FACILITY_BAOIC;
+        } else if (sc.equals(SC_BAOICxH)) {
+            return CommandsInterface.CB_FACILITY_BAOICxH;
+        } else if (sc.equals(SC_BAIC)) {
+            return CommandsInterface.CB_FACILITY_BAIC;
+        } else if (sc.equals(SC_BAICr)) {
+            return CommandsInterface.CB_FACILITY_BAICr;
+        } else if (sc.equals(SC_BA_ALL)) {
+            return CommandsInterface.CB_FACILITY_BA_ALL;
+        } else if (sc.equals(SC_BA_MO)) {
+            return CommandsInterface.CB_FACILITY_BA_MO;
+        } else if (sc.equals(SC_BA_MT)) {
+            return CommandsInterface.CB_FACILITY_BA_MT;
+        } else {
+            throw new RuntimeException ("invalid call barring sc");
+        }
+    }
+
+    //***** Constructor
+
+    public GsmMmiCode(GsmCdmaPhone phone, UiccCardApplication app) {
+        // The telephony unit-test cases may create GsmMmiCode's
+        // in secondary threads
+        super(phone.getHandler().getLooper());
+        mPhone = phone;
+        mContext = phone.getContext();
+        mUiccApplication = app;
+        if (app != null) {
+            mIccRecords = app.getIccRecords();
+        }
+    }
+
+    //***** MmiCode implementation
+
+    @Override
+    public State
+    getState() {
+        return mState;
+    }
+
+    @Override
+    public CharSequence
+    getMessage() {
+        return mMessage;
+    }
+
+    public Phone
+    getPhone() {
+        return ((Phone) mPhone);
+    }
+
+    // inherited javadoc suffices
+    @Override
+    public void
+    cancel() {
+        // Complete or failed cannot be cancelled
+        if (mState == State.COMPLETE || mState == State.FAILED) {
+            return;
+        }
+
+        mState = State.CANCELLED;
+
+        if (mIsPendingUSSD) {
+            /*
+             * There can only be one pending USSD session, so tell the radio to
+             * cancel it.
+             */
+            mPhone.mCi.cancelPendingUssd(obtainMessage(EVENT_USSD_CANCEL_COMPLETE, this));
+
+            /*
+             * Don't call phone.onMMIDone here; wait for CANCEL_COMPLETE notice
+             * from RIL.
+             */
+        } else {
+            // TODO in cases other than USSD, it would be nice to cancel
+            // the pending radio operation. This requires RIL cancellation
+            // support, which does not presently exist.
+
+            mPhone.onMMIDone (this);
+        }
+
+    }
+
+    @Override
+    public boolean isCancelable() {
+        /* Can only cancel pending USSD sessions. */
+        return mIsPendingUSSD;
+    }
+
+    //***** Instance Methods
+
+    /** Does this dial string contain a structured or unstructured MMI code? */
+    boolean
+    isMMI() {
+        return mPoundString != null;
+    }
+
+    /* Is this a 1 or 2 digit "short code" as defined in TS 22.030 sec 6.5.3.2? */
+    boolean
+    isShortCode() {
+        return mPoundString == null
+                    && mDialingNumber != null && mDialingNumber.length() <= 2;
+
+    }
+
+    @Override
+    public String getDialString() {
+        return mPoundString;
+    }
+
+    static private boolean
+    isTwoDigitShortCode(Context context, String dialString) {
+        Rlog.d(LOG_TAG, "isTwoDigitShortCode");
+
+        if (dialString == null || dialString.length() > 2) return false;
+
+        if (sTwoDigitNumberPattern == null) {
+            sTwoDigitNumberPattern = context.getResources().getStringArray(
+                    com.android.internal.R.array.config_twoDigitNumberPattern);
+        }
+
+        for (String dialnumber : sTwoDigitNumberPattern) {
+            Rlog.d(LOG_TAG, "Two Digit Number Pattern " + dialnumber);
+            if (dialString.equals(dialnumber)) {
+                Rlog.d(LOG_TAG, "Two Digit Number Pattern -true");
+                return true;
+            }
+        }
+        Rlog.d(LOG_TAG, "Two Digit Number Pattern -false");
+        return false;
+    }
+
+    /**
+     * Helper function for newFromDialString. Returns true if dialString appears
+     * to be a short code AND conditions are correct for it to be treated as
+     * such.
+     */
+    static private boolean isShortCode(String dialString, GsmCdmaPhone phone) {
+        // Refer to TS 22.030 Figure 3.5.3.2:
+        if (dialString == null) {
+            return false;
+        }
+
+        // Illegal dial string characters will give a ZERO length.
+        // At this point we do not want to crash as any application with
+        // call privileges may send a non dial string.
+        // It return false as when the dialString is equal to NULL.
+        if (dialString.length() == 0) {
+            return false;
+        }
+
+        if (PhoneNumberUtils.isLocalEmergencyNumber(phone.getContext(), dialString)) {
+            return false;
+        } else {
+            return isShortCodeUSSD(dialString, phone);
+        }
+    }
+
+    /**
+     * Helper function for isShortCode. Returns true if dialString appears to be
+     * a short code and it is a USSD structure
+     *
+     * According to the 3PGG TS 22.030 specification Figure 3.5.3.2: A 1 or 2
+     * digit "short code" is treated as USSD if it is entered while on a call or
+     * does not satisfy the condition (exactly 2 digits && starts with '1'), there
+     * are however exceptions to this rule (see below)
+     *
+     * Exception (1) to Call initiation is: If the user of the device is already in a call
+     * and enters a Short String without any #-key at the end and the length of the Short String is
+     * equal or less then the MAX_LENGTH_SHORT_CODE [constant that is equal to 2]
+     *
+     * The phone shall initiate a USSD/SS commands.
+     */
+    static private boolean isShortCodeUSSD(String dialString, GsmCdmaPhone phone) {
+        if (dialString != null && dialString.length() <= MAX_LENGTH_SHORT_CODE) {
+            if (phone.isInCall()) {
+                return true;
+            }
+
+            if (dialString.length() != MAX_LENGTH_SHORT_CODE ||
+                    dialString.charAt(0) != '1') {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related
+     */
+    public boolean isPinPukCommand() {
+        return mSc != null && (mSc.equals(SC_PIN) || mSc.equals(SC_PIN2)
+                              || mSc.equals(SC_PUK) || mSc.equals(SC_PUK2));
+     }
+
+    /**
+     * See TS 22.030 Annex B.
+     * In temporary mode, to suppress CLIR for a single call, enter:
+     *      " * 31 # [called number] SEND "
+     *  In temporary mode, to invoke CLIR for a single call enter:
+     *       " # 31 # [called number] SEND "
+     */
+    public boolean
+    isTemporaryModeCLIR() {
+        return mSc != null && mSc.equals(SC_CLIR) && mDialingNumber != null
+                && (isActivate() || isDeactivate());
+    }
+
+    /**
+     * returns CommandsInterface.CLIR_*
+     * See also isTemporaryModeCLIR()
+     */
+    public int
+    getCLIRMode() {
+        if (mSc != null && mSc.equals(SC_CLIR)) {
+            if (isActivate()) {
+                return CommandsInterface.CLIR_SUPPRESSION;
+            } else if (isDeactivate()) {
+                return CommandsInterface.CLIR_INVOCATION;
+            }
+        }
+
+        return CommandsInterface.CLIR_DEFAULT;
+    }
+
+    boolean isActivate() {
+        return mAction != null && mAction.equals(ACTION_ACTIVATE);
+    }
+
+    boolean isDeactivate() {
+        return mAction != null && mAction.equals(ACTION_DEACTIVATE);
+    }
+
+    boolean isInterrogate() {
+        return mAction != null && mAction.equals(ACTION_INTERROGATE);
+    }
+
+    boolean isRegister() {
+        return mAction != null && mAction.equals(ACTION_REGISTER);
+    }
+
+    boolean isErasure() {
+        return mAction != null && mAction.equals(ACTION_ERASURE);
+    }
+
+    /**
+     * Returns true if this is a USSD code that's been submitted to the
+     * network...eg, after processCode() is called
+     */
+    public boolean isPendingUSSD() {
+        return mIsPendingUSSD;
+    }
+
+    @Override
+    public boolean isUssdRequest() {
+        return mIsUssdRequest;
+    }
+
+    public boolean isSsInfo() {
+        return mIsSsInfo;
+    }
+
+    /** Process a MMI code or short code...anything that isn't a dialing number */
+    public void
+    processCode() throws CallStateException {
+        try {
+            if (isShortCode()) {
+                Rlog.d(LOG_TAG, "processCode: isShortCode");
+                // These just get treated as USSD.
+                sendUssd(mDialingNumber);
+            } else if (mDialingNumber != null) {
+                // We should have no dialing numbers here
+                throw new RuntimeException ("Invalid or Unsupported MMI Code");
+            } else if (mSc != null && mSc.equals(SC_CLIP)) {
+                Rlog.d(LOG_TAG, "processCode: is CLIP");
+                if (isInterrogate()) {
+                    mPhone.mCi.queryCLIP(
+                            obtainMessage(EVENT_QUERY_COMPLETE, this));
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mSc != null && mSc.equals(SC_CLIR)) {
+                Rlog.d(LOG_TAG, "processCode: is CLIR");
+                if (isActivate()) {
+                    mPhone.mCi.setCLIR(CommandsInterface.CLIR_INVOCATION,
+                        obtainMessage(EVENT_SET_COMPLETE, this));
+                } else if (isDeactivate()) {
+                    mPhone.mCi.setCLIR(CommandsInterface.CLIR_SUPPRESSION,
+                        obtainMessage(EVENT_SET_COMPLETE, this));
+                } else if (isInterrogate()) {
+                    mPhone.mCi.getCLIR(
+                        obtainMessage(EVENT_GET_CLIR_COMPLETE, this));
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (isServiceCodeCallForwarding(mSc)) {
+                Rlog.d(LOG_TAG, "processCode: is CF");
+
+                String dialingNumber = mSia;
+                int serviceClass = siToServiceClass(mSib);
+                int reason = scToCallForwardReason(mSc);
+                int time = siToTime(mSic);
+
+                if (isInterrogate()) {
+                    mPhone.mCi.queryCallForwardStatus(
+                            reason, serviceClass,  dialingNumber,
+                                obtainMessage(EVENT_QUERY_CF_COMPLETE, this));
+                } else {
+                    int cfAction;
+
+                    if (isActivate()) {
+                        // 3GPP TS 22.030 6.5.2
+                        // a call forwarding request with a single * would be
+                        // interpreted as registration if containing a forwarded-to
+                        // number, or an activation if not
+                        if (isEmptyOrNull(dialingNumber)) {
+                            cfAction = CommandsInterface.CF_ACTION_ENABLE;
+                            mIsCallFwdReg = false;
+                        } else {
+                            cfAction = CommandsInterface.CF_ACTION_REGISTRATION;
+                            mIsCallFwdReg = true;
+                        }
+                    } else if (isDeactivate()) {
+                        cfAction = CommandsInterface.CF_ACTION_DISABLE;
+                    } else if (isRegister()) {
+                        cfAction = CommandsInterface.CF_ACTION_REGISTRATION;
+                    } else if (isErasure()) {
+                        cfAction = CommandsInterface.CF_ACTION_ERASURE;
+                    } else {
+                        throw new RuntimeException ("invalid action");
+                    }
+
+                    int isSettingUnconditionalVoice =
+                        (((reason == CommandsInterface.CF_REASON_UNCONDITIONAL) ||
+                                (reason == CommandsInterface.CF_REASON_ALL)) &&
+                                (((serviceClass & CommandsInterface.SERVICE_CLASS_VOICE) != 0) ||
+                                 (serviceClass == CommandsInterface.SERVICE_CLASS_NONE))) ? 1 : 0;
+
+                    int isEnableDesired =
+                        ((cfAction == CommandsInterface.CF_ACTION_ENABLE) ||
+                                (cfAction == CommandsInterface.CF_ACTION_REGISTRATION)) ? 1 : 0;
+
+                    Rlog.d(LOG_TAG, "processCode: is CF setCallForward");
+                    mPhone.mCi.setCallForward(cfAction, reason, serviceClass,
+                            dialingNumber, time, obtainMessage(
+                                    EVENT_SET_CFF_COMPLETE,
+                                    isSettingUnconditionalVoice,
+                                    isEnableDesired, this));
+                }
+            } else if (isServiceCodeCallBarring(mSc)) {
+                // sia = password
+                // sib = basic service group
+
+                String password = mSia;
+                int serviceClass = siToServiceClass(mSib);
+                String facility = scToBarringFacility(mSc);
+
+                if (isInterrogate()) {
+                    mPhone.mCi.queryFacilityLock(facility, password,
+                            serviceClass, obtainMessage(EVENT_QUERY_COMPLETE, this));
+                } else if (isActivate() || isDeactivate()) {
+                    mPhone.mCi.setFacilityLock(facility, isActivate(), password,
+                            serviceClass, obtainMessage(EVENT_SET_COMPLETE, this));
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+
+            } else if (mSc != null && mSc.equals(SC_PWD)) {
+                // sia = fac
+                // sib = old pwd
+                // sic = new pwd
+                // pwd = new pwd
+                String facility;
+                String oldPwd = mSib;
+                String newPwd = mSic;
+                if (isActivate() || isRegister()) {
+                    // Even though ACTIVATE is acceptable, this is really termed a REGISTER
+                    mAction = ACTION_REGISTER;
+
+                    if (mSia == null) {
+                        // If sc was not specified, treat it as BA_ALL.
+                        facility = CommandsInterface.CB_FACILITY_BA_ALL;
+                    } else {
+                        facility = scToBarringFacility(mSia);
+                    }
+                    if (newPwd.equals(mPwd)) {
+                        mPhone.mCi.changeBarringPassword(facility, oldPwd,
+                                newPwd, obtainMessage(EVENT_SET_COMPLETE, this));
+                    } else {
+                        // password mismatch; return error
+                        handlePasswordError(com.android.internal.R.string.passwordIncorrect);
+                    }
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+
+            } else if (mSc != null && mSc.equals(SC_WAIT)) {
+                // sia = basic service group
+                int serviceClass = siToServiceClass(mSia);
+
+                if (isActivate() || isDeactivate()) {
+                    mPhone.mCi.setCallWaiting(isActivate(), serviceClass,
+                            obtainMessage(EVENT_SET_COMPLETE, this));
+                } else if (isInterrogate()) {
+                    mPhone.mCi.queryCallWaiting(serviceClass,
+                            obtainMessage(EVENT_QUERY_COMPLETE, this));
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (isPinPukCommand()) {
+                // TODO: This is the same as the code in CmdaMmiCode.java,
+                // MmiCode should be an abstract or base class and this and
+                // other common variables and code should be promoted.
+
+                // sia = old PIN or PUK
+                // sib = new PIN
+                // sic = new PIN
+                String oldPinOrPuk = mSia;
+                String newPinOrPuk = mSib;
+                int pinLen = newPinOrPuk.length();
+                if (isRegister()) {
+                    if (!newPinOrPuk.equals(mSic)) {
+                        // password mismatch; return error
+                        handlePasswordError(com.android.internal.R.string.mismatchPin);
+                    } else if (pinLen < 4 || pinLen > 8 ) {
+                        // invalid length
+                        handlePasswordError(com.android.internal.R.string.invalidPin);
+                    } else if (mSc.equals(SC_PIN)
+                            && mUiccApplication != null
+                            && mUiccApplication.getState() == AppState.APPSTATE_PUK) {
+                        // Sim is puk-locked
+                        handlePasswordError(com.android.internal.R.string.needPuk);
+                    } else if (mUiccApplication != null) {
+                        Rlog.d(LOG_TAG,
+                                "processCode: process mmi service code using UiccApp sc=" + mSc);
+
+                        // We have an app and the pre-checks are OK
+                        if (mSc.equals(SC_PIN)) {
+                            mUiccApplication.changeIccLockPassword(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else if (mSc.equals(SC_PIN2)) {
+                            mUiccApplication.changeIccFdnPassword(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else if (mSc.equals(SC_PUK)) {
+                            mUiccApplication.supplyPuk(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else if (mSc.equals(SC_PUK2)) {
+                            mUiccApplication.supplyPuk2(oldPinOrPuk, newPinOrPuk,
+                                    obtainMessage(EVENT_SET_COMPLETE, this));
+                        } else {
+                            throw new RuntimeException("uicc unsupported service code=" + mSc);
+                        }
+                    } else {
+                        throw new RuntimeException("No application mUiccApplicaiton is null");
+                    }
+                } else {
+                    throw new RuntimeException ("Ivalid register/action=" + mAction);
+                }
+            } else if (mPoundString != null) {
+                sendUssd(mPoundString);
+            } else {
+                Rlog.d(LOG_TAG, "processCode: Invalid or Unsupported MMI Code");
+                throw new RuntimeException ("Invalid or Unsupported MMI Code");
+            }
+        } catch (RuntimeException exc) {
+            mState = State.FAILED;
+            mMessage = mContext.getText(com.android.internal.R.string.mmiError);
+            Rlog.d(LOG_TAG, "processCode: RuntimeException=" + exc);
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    private void handlePasswordError(int res) {
+        mState = State.FAILED;
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+        sb.append(mContext.getText(res));
+        mMessage = sb;
+        mPhone.onMMIDone(this);
+    }
+
+    /**
+     * Called from GsmCdmaPhone
+     *
+     * An unsolicited USSD NOTIFY or REQUEST has come in matching
+     * up with this pending USSD request
+     *
+     * Note: If REQUEST, this exchange is complete, but the session remains
+     *       active (ie, the network expects user input).
+     */
+    public void
+    onUssdFinished(String ussdMessage, boolean isUssdRequest) {
+        if (mState == State.PENDING) {
+            if (TextUtils.isEmpty(ussdMessage)) {
+                Rlog.d(LOG_TAG, "onUssdFinished: no network provided message; using default.");
+                mMessage = mContext.getText(com.android.internal.R.string.mmiComplete);
+            } else {
+                mMessage = ussdMessage;
+            }
+            mIsUssdRequest = isUssdRequest;
+            // If it's a request, leave it PENDING so that it's cancelable.
+            if (!isUssdRequest) {
+                mState = State.COMPLETE;
+            }
+            Rlog.d(LOG_TAG, "onUssdFinished: ussdMessage=" + ussdMessage);
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    /**
+     * Called from GsmCdmaPhone
+     *
+     * The radio has reset, and this is still pending
+     */
+
+    public void
+    onUssdFinishedError() {
+        if (mState == State.PENDING) {
+            mState = State.FAILED;
+            mMessage = mContext.getText(com.android.internal.R.string.mmiError);
+            Rlog.d(LOG_TAG, "onUssdFinishedError");
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    /**
+     * Called from GsmCdmaPhone
+     *
+     * An unsolicited USSD NOTIFY or REQUEST has come in matching
+     * up with this pending USSD request
+     *
+     * Note: If REQUEST, this exchange is complete, but the session remains
+     *       active (ie, the network expects user input).
+     */
+    public void
+    onUssdRelease() {
+        if (mState == State.PENDING) {
+            mState = State.COMPLETE;
+            mMessage = null;
+            Rlog.d(LOG_TAG, "onUssdRelease");
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    public void sendUssd(String ussdMessage) {
+        // Treat this as a USSD string
+        mIsPendingUSSD = true;
+
+        // Note that unlike most everything else, the USSD complete
+        // response does not complete this MMI code...we wait for
+        // an unsolicited USSD "Notify" or "Request".
+        // The matching up of this is done in GsmCdmaPhone.
+        mPhone.mCi.sendUSSD(ussdMessage,
+            obtainMessage(EVENT_USSD_COMPLETE, this));
+    }
+
+    /** Called from GsmCdmaPhone.handleMessage; not a Handler subclass */
+    @Override
+    public void
+    handleMessage (Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+            case EVENT_SET_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+
+                onSetComplete(msg, ar);
+                break;
+
+            case EVENT_SET_CFF_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+
+                /*
+                * msg.arg1 = 1 means to set unconditional voice call forwarding
+                * msg.arg2 = 1 means to enable voice call forwarding
+                */
+                if ((ar.exception == null) && (msg.arg1 == 1)) {
+                    boolean cffEnabled = (msg.arg2 == 1);
+                    if (mIccRecords != null) {
+                        mPhone.setVoiceCallForwardingFlag(1, cffEnabled, mDialingNumber);
+                    }
+                }
+
+                onSetComplete(msg, ar);
+                break;
+
+            case EVENT_GET_CLIR_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onGetClirComplete(ar);
+            break;
+
+            case EVENT_QUERY_CF_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onQueryCfComplete(ar);
+            break;
+
+            case EVENT_QUERY_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onQueryComplete(ar);
+            break;
+
+            case EVENT_USSD_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+
+                if (ar.exception != null) {
+                    mState = State.FAILED;
+                    mMessage = getErrorMessage(ar);
+
+                    mPhone.onMMIDone(this);
+                }
+
+                // Note that unlike most everything else, the USSD complete
+                // response does not complete this MMI code...we wait for
+                // an unsolicited USSD "Notify" or "Request".
+                // The matching up of this is done in GsmCdmaPhone.
+
+            break;
+
+            case EVENT_USSD_CANCEL_COMPLETE:
+                mPhone.onMMIDone(this);
+            break;
+        }
+    }
+    //***** Private instance methods
+
+    private CharSequence getErrorMessage(AsyncResult ar) {
+
+        if (ar.exception instanceof CommandException) {
+            CommandException.Error err = ((CommandException)(ar.exception)).getCommandError();
+            if (err == CommandException.Error.FDN_CHECK_FAILURE) {
+                Rlog.i(LOG_TAG, "FDN_CHECK_FAILURE");
+                return mContext.getText(com.android.internal.R.string.mmiFdnError);
+            } else if (err == CommandException.Error.USSD_MODIFIED_TO_DIAL) {
+                Rlog.i(LOG_TAG, "USSD_MODIFIED_TO_DIAL");
+                return mContext.getText(com.android.internal.R.string.stk_cc_ussd_to_dial);
+            } else if (err == CommandException.Error.USSD_MODIFIED_TO_SS) {
+                Rlog.i(LOG_TAG, "USSD_MODIFIED_TO_SS");
+                return mContext.getText(com.android.internal.R.string.stk_cc_ussd_to_ss);
+            } else if (err == CommandException.Error.USSD_MODIFIED_TO_USSD) {
+                Rlog.i(LOG_TAG, "USSD_MODIFIED_TO_USSD");
+                return mContext.getText(com.android.internal.R.string.stk_cc_ussd_to_ussd);
+            } else if (err == CommandException.Error.SS_MODIFIED_TO_DIAL) {
+                Rlog.i(LOG_TAG, "SS_MODIFIED_TO_DIAL");
+                return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_dial);
+            } else if (err == CommandException.Error.SS_MODIFIED_TO_USSD) {
+                Rlog.i(LOG_TAG, "SS_MODIFIED_TO_USSD");
+                return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ussd);
+            } else if (err == CommandException.Error.SS_MODIFIED_TO_SS) {
+                Rlog.i(LOG_TAG, "SS_MODIFIED_TO_SS");
+                return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ss);
+            }
+        }
+
+        return mContext.getText(com.android.internal.R.string.mmiError);
+    }
+
+    private CharSequence getScString() {
+        if (mSc != null) {
+            if (isServiceCodeCallBarring(mSc)) {
+                return mContext.getText(com.android.internal.R.string.BaMmi);
+            } else if (isServiceCodeCallForwarding(mSc)) {
+                return mContext.getText(com.android.internal.R.string.CfMmi);
+            } else if (mSc.equals(SC_CLIP)) {
+                return mContext.getText(com.android.internal.R.string.ClipMmi);
+            } else if (mSc.equals(SC_CLIR)) {
+                return mContext.getText(com.android.internal.R.string.ClirMmi);
+            } else if (mSc.equals(SC_PWD)) {
+                return mContext.getText(com.android.internal.R.string.PwdMmi);
+            } else if (mSc.equals(SC_WAIT)) {
+                return mContext.getText(com.android.internal.R.string.CwMmi);
+            } else if (isPinPukCommand()) {
+                return mContext.getText(com.android.internal.R.string.PinMmi);
+            }
+        }
+
+        return "";
+    }
+
+    private void
+    onSetComplete(Message msg, AsyncResult ar){
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+            if (ar.exception instanceof CommandException) {
+                CommandException.Error err = ((CommandException)(ar.exception)).getCommandError();
+                if (err == CommandException.Error.PASSWORD_INCORRECT) {
+                    if (isPinPukCommand()) {
+                        // look specifically for the PUK commands and adjust
+                        // the message accordingly.
+                        if (mSc.equals(SC_PUK) || mSc.equals(SC_PUK2)) {
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.badPuk));
+                        } else {
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.badPin));
+                        }
+                        // Get the No. of retries remaining to unlock PUK/PUK2
+                        int attemptsRemaining = msg.arg1;
+                        if (attemptsRemaining <= 0) {
+                            Rlog.d(LOG_TAG, "onSetComplete: PUK locked,"
+                                    + " cancel as lock screen will handle this");
+                            mState = State.CANCELLED;
+                        } else if (attemptsRemaining > 0) {
+                            Rlog.d(LOG_TAG, "onSetComplete: attemptsRemaining="+attemptsRemaining);
+                            sb.append(mContext.getResources().getQuantityString(
+                                    com.android.internal.R.plurals.pinpuk_attempts,
+                                    attemptsRemaining, attemptsRemaining));
+                        }
+                    } else {
+                        sb.append(mContext.getText(
+                                com.android.internal.R.string.passwordIncorrect));
+                    }
+                } else if (err == CommandException.Error.SIM_PUK2) {
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.badPin));
+                    sb.append("\n");
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.needPuk2));
+                } else if (err == CommandException.Error.REQUEST_NOT_SUPPORTED) {
+                    if (mSc.equals(SC_PIN)) {
+                        sb.append(mContext.getText(com.android.internal.R.string.enablePin));
+                    }
+                } else if (err == CommandException.Error.FDN_CHECK_FAILURE) {
+                    Rlog.i(LOG_TAG, "FDN_CHECK_FAILURE");
+                    sb.append(mContext.getText(com.android.internal.R.string.mmiFdnError));
+                } else if (err == CommandException.Error.MODEM_ERR) {
+                    // Some carriers do not allow changing call forwarding settings while roaming
+                    // and will return an error from the modem.
+                    if (isServiceCodeCallForwarding(mSc)
+                            && mPhone.getServiceState().getVoiceRoaming()
+                            && !mPhone.supports3gppCallForwardingWhileRoaming()) {
+                        sb.append(mContext.getText(
+                                com.android.internal.R.string.mmiErrorWhileRoaming));
+                    } else {
+                        sb.append(getErrorMessage(ar));
+                    }
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            } else {
+                sb.append(mContext.getText(
+                        com.android.internal.R.string.mmiError));
+            }
+        } else if (isActivate()) {
+            mState = State.COMPLETE;
+            if (mIsCallFwdReg) {
+                sb.append(mContext.getText(
+                        com.android.internal.R.string.serviceRegistered));
+            } else {
+                sb.append(mContext.getText(
+                        com.android.internal.R.string.serviceEnabled));
+            }
+            // Record CLIR setting
+            if (mSc.equals(SC_CLIR)) {
+                mPhone.saveClirSetting(CommandsInterface.CLIR_INVOCATION);
+            }
+        } else if (isDeactivate()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceDisabled));
+            // Record CLIR setting
+            if (mSc.equals(SC_CLIR)) {
+                mPhone.saveClirSetting(CommandsInterface.CLIR_SUPPRESSION);
+            }
+        } else if (isRegister()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceRegistered));
+        } else if (isErasure()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceErased));
+        } else {
+            mState = State.FAILED;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.mmiError));
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onSetComplete mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    private void
+    onGetClirComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+            sb.append(getErrorMessage(ar));
+        } else {
+            int clirArgs[];
+
+            clirArgs = (int[])ar.result;
+
+            // the 'm' parameter from TS 27.007 7.7
+            switch (clirArgs[1]) {
+                case 0: // CLIR not provisioned
+                    sb.append(mContext.getText(
+                                com.android.internal.R.string.serviceNotProvisioned));
+                    mState = State.COMPLETE;
+                break;
+
+                case 1: // CLIR provisioned in permanent mode
+                    sb.append(mContext.getText(
+                                com.android.internal.R.string.CLIRPermanent));
+                    mState = State.COMPLETE;
+                break;
+
+                case 2: // unknown (e.g. no network, etc.)
+                    sb.append(mContext.getText(
+                                com.android.internal.R.string.mmiError));
+                    mState = State.FAILED;
+                break;
+
+                case 3: // CLIR temporary mode presentation restricted
+
+                    // the 'n' parameter from TS 27.007 7.7
+                    switch (clirArgs[0]) {
+                        default:
+                        case 0: // Default
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOnNextCallOn));
+                        break;
+                        case 1: // CLIR invocation
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOnNextCallOn));
+                        break;
+                        case 2: // CLIR suppression
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOnNextCallOff));
+                        break;
+                    }
+                    mState = State.COMPLETE;
+                break;
+
+                case 4: // CLIR temporary mode presentation allowed
+                    // the 'n' parameter from TS 27.007 7.7
+                    switch (clirArgs[0]) {
+                        default:
+                        case 0: // Default
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOffNextCallOff));
+                        break;
+                        case 1: // CLIR invocation
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOffNextCallOn));
+                        break;
+                        case 2: // CLIR suppression
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOffNextCallOff));
+                        break;
+                    }
+
+                    mState = State.COMPLETE;
+                break;
+            }
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onGetClirComplete: mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    /**
+     * @param serviceClass 1 bit of the service class bit vectory
+     * @return String to be used for call forward query MMI response text.
+     *        Returns null if unrecognized
+     */
+
+    private CharSequence
+    serviceClassToCFString (int serviceClass) {
+        switch (serviceClass) {
+            case SERVICE_CLASS_VOICE:
+                return mContext.getText(com.android.internal.R.string.serviceClassVoice);
+            case SERVICE_CLASS_DATA:
+                return mContext.getText(com.android.internal.R.string.serviceClassData);
+            case SERVICE_CLASS_FAX:
+                return mContext.getText(com.android.internal.R.string.serviceClassFAX);
+            case SERVICE_CLASS_SMS:
+                return mContext.getText(com.android.internal.R.string.serviceClassSMS);
+            case SERVICE_CLASS_DATA_SYNC:
+                return mContext.getText(com.android.internal.R.string.serviceClassDataSync);
+            case SERVICE_CLASS_DATA_ASYNC:
+                return mContext.getText(com.android.internal.R.string.serviceClassDataAsync);
+            case SERVICE_CLASS_PACKET:
+                return mContext.getText(com.android.internal.R.string.serviceClassPacket);
+            case SERVICE_CLASS_PAD:
+                return mContext.getText(com.android.internal.R.string.serviceClassPAD);
+            default:
+                return null;
+        }
+    }
+
+
+    /** one CallForwardInfo + serviceClassMask -> one line of text */
+    private CharSequence
+    makeCFQueryResultMessage(CallForwardInfo info, int serviceClassMask) {
+        CharSequence template;
+        String sources[] = {"{0}", "{1}", "{2}"};
+        CharSequence destinations[] = new CharSequence[3];
+        boolean needTimeTemplate;
+
+        // CF_REASON_NO_REPLY also has a time value associated with
+        // it. All others don't.
+
+        needTimeTemplate =
+            (info.reason == CommandsInterface.CF_REASON_NO_REPLY);
+
+        if (info.status == 1) {
+            if (needTimeTemplate) {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateForwardedTime);
+            } else {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateForwarded);
+            }
+        } else if (info.status == 0 && isEmptyOrNull(info.number)) {
+            template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateNotForwarded);
+        } else { /* (info.status == 0) && !isEmptyOrNull(info.number) */
+            // A call forward record that is not active but contains
+            // a phone number is considered "registered"
+
+            if (needTimeTemplate) {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateRegisteredTime);
+            } else {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateRegistered);
+            }
+        }
+
+        // In the template (from strings.xmls)
+        //         {0} is one of "bearerServiceCode*"
+        //        {1} is dialing number
+        //      {2} is time in seconds
+
+        destinations[0] = serviceClassToCFString(info.serviceClass & serviceClassMask);
+        destinations[1] = formatLtr(
+                PhoneNumberUtils.stringFromStringAndTOA(info.number, info.toa));
+        destinations[2] = Integer.toString(info.timeSeconds);
+
+        if (info.reason == CommandsInterface.CF_REASON_UNCONDITIONAL &&
+                (info.serviceClass & serviceClassMask)
+                        == CommandsInterface.SERVICE_CLASS_VOICE) {
+            boolean cffEnabled = (info.status == 1);
+            if (mIccRecords != null) {
+                mPhone.setVoiceCallForwardingFlag(1, cffEnabled, info.number);
+            }
+        }
+
+        return TextUtils.replace(template, sources, destinations);
+    }
+
+    /**
+     * Used to format a string that should be displayed as LTR even in RTL locales
+     */
+    private String formatLtr(String str) {
+        BidiFormatter fmt = BidiFormatter.getInstance();
+        return str == null ? str : fmt.unicodeWrap(str, TextDirectionHeuristics.LTR, true);
+    }
+
+    private void
+    onQueryCfComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+            sb.append(getErrorMessage(ar));
+        } else {
+            CallForwardInfo infos[];
+
+            infos = (CallForwardInfo[]) ar.result;
+
+            if (infos.length == 0) {
+                // Assume the default is not active
+                sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+
+                // Set unconditional CFF in SIM to false
+                if (mIccRecords != null) {
+                    mPhone.setVoiceCallForwardingFlag(1, false, null);
+                }
+            } else {
+
+                SpannableStringBuilder tb = new SpannableStringBuilder();
+
+                // Each bit in the service class gets its own result line
+                // The service classes may be split up over multiple
+                // CallForwardInfos. So, for each service class, find out
+                // which CallForwardInfo represents it and then build
+                // the response text based on that
+
+                for (int serviceClassMask = 1
+                            ; serviceClassMask <= SERVICE_CLASS_MAX
+                            ; serviceClassMask <<= 1
+                ) {
+                    for (int i = 0, s = infos.length; i < s ; i++) {
+                        if ((serviceClassMask & infos[i].serviceClass) != 0) {
+                            tb.append(makeCFQueryResultMessage(infos[i],
+                                            serviceClassMask));
+                            tb.append("\n");
+                        }
+                    }
+                }
+                sb.append(tb);
+            }
+
+            mState = State.COMPLETE;
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onQueryCfComplete: mmi=" + this);
+        mPhone.onMMIDone(this);
+
+    }
+
+    private void
+    onQueryComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+            sb.append(getErrorMessage(ar));
+        } else {
+            int[] ints = (int[])ar.result;
+
+            if (ints.length != 0) {
+                if (ints[0] == 0) {
+                    sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+                } else if (mSc.equals(SC_WAIT)) {
+                    // Call Waiting includes additional data in the response.
+                    sb.append(createQueryCallWaitingResultMessage(ints[1]));
+                } else if (isServiceCodeCallBarring(mSc)) {
+                    // ints[0] for Call Barring is a bit vector of services
+                    sb.append(createQueryCallBarringResultMessage(ints[0]));
+                } else if (ints[0] == 1) {
+                    // for all other services, treat it as a boolean
+                    sb.append(mContext.getText(com.android.internal.R.string.serviceEnabled));
+                } else {
+                    sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+                }
+            } else {
+                sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+            }
+            mState = State.COMPLETE;
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onQueryComplete: mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    private CharSequence
+    createQueryCallWaitingResultMessage(int serviceClass) {
+        StringBuilder sb =
+                new StringBuilder(mContext.getText(com.android.internal.R.string.serviceEnabledFor));
+
+        for (int classMask = 1
+                    ; classMask <= SERVICE_CLASS_MAX
+                    ; classMask <<= 1
+        ) {
+            if ((classMask & serviceClass) != 0) {
+                sb.append("\n");
+                sb.append(serviceClassToCFString(classMask & serviceClass));
+            }
+        }
+        return sb;
+    }
+    private CharSequence
+    createQueryCallBarringResultMessage(int serviceClass)
+    {
+        StringBuilder sb = new StringBuilder(mContext.getText(com.android.internal.R.string.serviceEnabledFor));
+
+        for (int classMask = 1
+                    ; classMask <= SERVICE_CLASS_MAX
+                    ; classMask <<= 1
+        ) {
+            if ((classMask & serviceClass) != 0) {
+                sb.append("\n");
+                sb.append(serviceClassToCFString(classMask & serviceClass));
+            }
+        }
+        return sb;
+    }
+
+    public ResultReceiver getUssdCallbackReceiver() {
+        return this.mCallbackReceiver;
+    }
+
+    /***
+     * TODO: It would be nice to have a method here that can take in a dialstring and
+     * figure out if there is an MMI code embedded within it.  This code would replace
+     * some of the string parsing functionality in the Phone App's
+     * SpecialCharSequenceMgr class.
+     */
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("GsmMmiCode {");
+
+        sb.append("State=" + getState());
+        if (mAction != null) sb.append(" action=" + mAction);
+        if (mSc != null) sb.append(" sc=" + mSc);
+        if (mSia != null) sb.append(" sia=" + Rlog.pii(LOG_TAG, mSia));
+        if (mSib != null) sb.append(" sib=" + Rlog.pii(LOG_TAG, mSib));
+        if (mSic != null) sb.append(" sic=" + Rlog.pii(LOG_TAG, mSic));
+        if (mPoundString != null) sb.append(" poundString=" + Rlog.pii(LOG_TAG, mPoundString));
+        if (mDialingNumber != null) {
+            sb.append(" dialingNumber=" + Rlog.pii(LOG_TAG, mDialingNumber));
+        }
+        if (mPwd != null) sb.append(" pwd=" + Rlog.pii(LOG_TAG, mPwd));
+        if (mCallbackReceiver != null) sb.append(" hasReceiver");
+        sb.append("}");
+        return sb.toString();
+    }
+}
diff --git a/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
new file mode 100644
index 0000000..8f18c61
--- /dev/null
+++ b/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.gsm;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.ImsSMSDispatcher;
+import com.android.internal.telephony.InboundSmsHandler;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.SMSDispatcher;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+import com.android.internal.telephony.uicc.UiccController;
+
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class GsmSMSDispatcher extends SMSDispatcher {
+    private static final String TAG = "GsmSMSDispatcher";
+    private static final boolean VDBG = false;
+    protected UiccController mUiccController = null;
+    private AtomicReference<IccRecords> mIccRecords = new AtomicReference<IccRecords>();
+    private AtomicReference<UiccCardApplication> mUiccApplication =
+            new AtomicReference<UiccCardApplication>();
+    private GsmInboundSmsHandler mGsmInboundSmsHandler;
+
+    /** Status report received */
+    private static final int EVENT_NEW_SMS_STATUS_REPORT = 100;
+
+    public GsmSMSDispatcher(Phone phone, SmsUsageMonitor usageMonitor,
+            ImsSMSDispatcher imsSMSDispatcher,
+            GsmInboundSmsHandler gsmInboundSmsHandler) {
+        super(phone, usageMonitor, imsSMSDispatcher);
+        mCi.setOnSmsStatus(this, EVENT_NEW_SMS_STATUS_REPORT, null);
+        mGsmInboundSmsHandler = gsmInboundSmsHandler;
+        mUiccController = UiccController.getInstance();
+        mUiccController.registerForIccChanged(this, EVENT_ICC_CHANGED, null);
+        Rlog.d(TAG, "GsmSMSDispatcher created");
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        mCi.unSetOnSmsStatus(this);
+        mUiccController.unregisterForIccChanged(this);
+    }
+
+    @Override
+    protected String getFormat() {
+        return SmsConstants.FORMAT_3GPP;
+    }
+
+    /**
+     * Handles 3GPP format-specific events coming from the phone stack.
+     * Other events are handled by {@link SMSDispatcher#handleMessage}.
+     *
+     * @param msg the message to handle
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+        case EVENT_NEW_SMS_STATUS_REPORT:
+            handleStatusReport((AsyncResult) msg.obj);
+            break;
+
+        case EVENT_NEW_ICC_SMS:
+        // pass to InboundSmsHandler to process
+        mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_NEW_SMS, msg.obj);
+        break;
+
+        case EVENT_ICC_CHANGED:
+            onUpdateIccAvailability();
+            break;
+
+        default:
+            super.handleMessage(msg);
+        }
+    }
+
+    /**
+     * Called when a status report is received.  This should correspond to
+     * a previously successful SEND.
+     *
+     * @param ar AsyncResult passed into the message handler.  ar.result should
+     *           be a String representing the status report PDU, as ASCII hex.
+     */
+    private void handleStatusReport(AsyncResult ar) {
+        byte[] pdu = (byte[]) ar.result;
+        SmsMessage sms = SmsMessage.newFromCDS(pdu);
+
+        if (sms != null) {
+            int tpStatus = sms.getStatus();
+            int messageRef = sms.mMessageRef;
+            for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
+                SmsTracker tracker = deliveryPendingList.get(i);
+                if (tracker.mMessageRef == messageRef) {
+                    // Found it.  Remove from list and broadcast.
+                    if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) {
+                       deliveryPendingList.remove(i);
+                       // Update the message status (COMPLETE or FAILED)
+                       tracker.updateSentMessageStatus(mContext, tpStatus);
+                    }
+                    PendingIntent intent = tracker.mDeliveryIntent;
+                    Intent fillIn = new Intent();
+                    fillIn.putExtra("pdu", pdu);
+                    fillIn.putExtra("format", getFormat());
+                    try {
+                        intent.send(mContext, Activity.RESULT_OK, fillIn);
+                    } catch (CanceledException ex) {}
+
+                    // Only expect to see one tracker matching this messageref
+                    break;
+                }
+            }
+        }
+        mCi.acknowledgeLastIncomingGsmSms(true, Intents.RESULT_SMS_HANDLED, null);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void sendData(String destAddr, String scAddr, int destPort,
+            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
+                scAddr, destAddr, destPort, data, (deliveryIntent != null));
+        if (pdu != null) {
+            HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
+            SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
+                    null /*messageUri*/, false /*isExpectMore*/, null /*fullMessageText*/,
+                    false /*isText*/, true /*persistMessage*/);
+
+            String carrierPackage = getCarrierAppPackageName();
+            if (carrierPackage != null) {
+                Rlog.d(TAG, "Found carrier package.");
+                DataSmsSender smsSender = new DataSmsSender(tracker);
+                smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
+            } else {
+                Rlog.v(TAG, "No carrier package.");
+                sendRawPdu(tracker);
+            }
+        } else {
+            Rlog.e(TAG, "GsmSMSDispatcher.sendData(): getSubmitPdu() returned null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @VisibleForTesting
+    @Override
+    public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
+            PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
+            boolean persistMessage) {
+        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
+                scAddr, destAddr, text, (deliveryIntent != null));
+        if (pdu != null) {
+            HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
+            SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
+                    messageUri, false /*isExpectMore*/, text /*fullMessageText*/, true /*isText*/,
+                    persistMessage);
+
+            String carrierPackage = getCarrierAppPackageName();
+            if (carrierPackage != null) {
+                Rlog.d(TAG, "Found carrier package.");
+                TextSmsSender smsSender = new TextSmsSender(tracker);
+                smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
+            } else {
+                Rlog.v(TAG, "No carrier package.");
+                sendRawPdu(tracker);
+            }
+        } else {
+            Rlog.e(TAG, "GsmSMSDispatcher.sendText(): getSubmitPdu() returned null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
+        throw new IllegalStateException("This method must be called only on ImsSMSDispatcher");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody,
+            boolean use7bitOnly) {
+        return SmsMessage.calculateLength(messageBody, use7bitOnly);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
+            String message, SmsHeader smsHeader, int encoding,
+            PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart,
+            AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
+            String fullMessageText) {
+        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddress, destinationAddress,
+                message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader),
+                encoding, smsHeader.languageTable, smsHeader.languageShiftTable);
+        if (pdu != null) {
+            HashMap map =  getSmsTrackerMap(destinationAddress, scAddress,
+                    message, pdu);
+            return getSmsTracker(map, sentIntent,
+                    deliveryIntent, getFormat(), unsentPartCount, anyPartFailed, messageUri,
+                    smsHeader, !lastPart, fullMessageText, true /*isText*/,
+                    false /*persistMessage*/);
+        } else {
+            Rlog.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null");
+            return null;
+        }
+    }
+
+    @Override
+    protected void sendSubmitPdu(SmsTracker tracker) {
+        sendRawPdu(tracker);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void sendSms(SmsTracker tracker) {
+        HashMap<String, Object> map = tracker.getData();
+
+        byte pdu[] = (byte[]) map.get("pdu");
+
+        if (tracker.mRetryCount > 0) {
+            Rlog.d(TAG, "sendSms: "
+                    + " mRetryCount=" + tracker.mRetryCount
+                    + " mMessageRef=" + tracker.mMessageRef
+                    + " SS=" + mPhone.getServiceState().getState());
+
+            // per TS 23.040 Section 9.2.3.6:  If TP-MTI SMS-SUBMIT (0x01) type
+            //   TP-RD (bit 2) is 1 for retry
+            //   and TP-MR is set to previously failed sms TP-MR
+            if (((0x01 & pdu[0]) == 0x01)) {
+                pdu[0] |= 0x04; // TP-RD
+                pdu[1] = (byte) tracker.mMessageRef; // TP-MR
+            }
+        }
+        Rlog.d(TAG, "sendSms: "
+                + " isIms()=" + isIms()
+                + " mRetryCount=" + tracker.mRetryCount
+                + " mImsRetry=" + tracker.mImsRetry
+                + " mMessageRef=" + tracker.mMessageRef
+                + " SS=" + mPhone.getServiceState().getState());
+
+        sendSmsByPstn(tracker);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void sendSmsByPstn(SmsTracker tracker) {
+        int ss = mPhone.getServiceState().getState();
+        // if sms over IMS is not supported on data and voice is not available...
+        if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
+            tracker.onFailed(mContext, getNotInServiceError(ss), 0/*errorCode*/);
+            return;
+        }
+
+        HashMap<String, Object> map = tracker.getData();
+
+        byte smsc[] = (byte[]) map.get("smsc");
+        byte[] pdu = (byte[]) map.get("pdu");
+        Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);
+
+        // sms over gsm is used:
+        //   if sms over IMS is not supported AND
+        //   this is not a retry case after sms over IMS failed
+        //     indicated by mImsRetry > 0
+        if (0 == tracker.mImsRetry && !isIms()) {
+            if (tracker.mRetryCount > 0) {
+                // per TS 23.040 Section 9.2.3.6:  If TP-MTI SMS-SUBMIT (0x01) type
+                //   TP-RD (bit 2) is 1 for retry
+                //   and TP-MR is set to previously failed sms TP-MR
+                if (((0x01 & pdu[0]) == 0x01)) {
+                    pdu[0] |= 0x04; // TP-RD
+                    pdu[1] = (byte) tracker.mMessageRef; // TP-MR
+                }
+            }
+            if (tracker.mRetryCount == 0 && tracker.mExpectMore) {
+                mCi.sendSMSExpectMore(IccUtils.bytesToHexString(smsc),
+                        IccUtils.bytesToHexString(pdu), reply);
+            } else {
+                mCi.sendSMS(IccUtils.bytesToHexString(smsc),
+                        IccUtils.bytesToHexString(pdu), reply);
+            }
+        } else {
+            mCi.sendImsGsmSms(IccUtils.bytesToHexString(smsc),
+                    IccUtils.bytesToHexString(pdu), tracker.mImsRetry,
+                    tracker.mMessageRef, reply);
+            // increment it here, so in case of SMS_FAIL_RETRY over IMS
+            // next retry will be sent using IMS request again.
+            tracker.mImsRetry++;
+        }
+    }
+
+    protected UiccCardApplication getUiccCardApplication() {
+            Rlog.d(TAG, "GsmSMSDispatcher: subId = " + mPhone.getSubId()
+                    + " slotId = " + mPhone.getPhoneId());
+                return mUiccController.getUiccCardApplication(mPhone.getPhoneId(),
+                        UiccController.APP_FAM_3GPP);
+    }
+
+    private void onUpdateIccAvailability() {
+        if (mUiccController == null ) {
+            return;
+        }
+
+        UiccCardApplication newUiccApplication = getUiccCardApplication();
+
+        UiccCardApplication app = mUiccApplication.get();
+        if (app != newUiccApplication) {
+            if (app != null) {
+                Rlog.d(TAG, "Removing stale icc objects.");
+                if (mIccRecords.get() != null) {
+                    mIccRecords.get().unregisterForNewSms(this);
+                }
+                mIccRecords.set(null);
+                mUiccApplication.set(null);
+            }
+            if (newUiccApplication != null) {
+                Rlog.d(TAG, "New Uicc application found");
+                mUiccApplication.set(newUiccApplication);
+                mIccRecords.set(newUiccApplication.getIccRecords());
+                if (mIccRecords.get() != null) {
+                    mIccRecords.get().registerForNewSms(this, EVENT_NEW_ICC_SMS, null);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/gsm/GsmSmsAddress.java b/com/android/internal/telephony/gsm/GsmSmsAddress.java
new file mode 100644
index 0000000..2fbf7ed
--- /dev/null
+++ b/com/android/internal/telephony/gsm/GsmSmsAddress.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.gsm;
+
+import android.telephony.PhoneNumberUtils;
+import java.text.ParseException;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.SmsAddress;
+
+public class GsmSmsAddress extends SmsAddress {
+
+    static final int OFFSET_ADDRESS_LENGTH = 0;
+
+    static final int OFFSET_TOA = 1;
+
+    static final int OFFSET_ADDRESS_VALUE = 2;
+
+    /**
+     * New GsmSmsAddress from TS 23.040 9.1.2.5 Address Field
+     *
+     * @param offset the offset of the Address-Length byte
+     * @param length the length in bytes rounded up, e.g. "2 +
+     *        (addressLength + 1) / 2"
+     * @throws ParseException
+     */
+
+    public GsmSmsAddress(byte[] data, int offset, int length) throws ParseException {
+        origBytes = new byte[length];
+        System.arraycopy(data, offset, origBytes, 0, length);
+
+        // addressLength is the count of semi-octets, not bytes
+        int addressLength = origBytes[OFFSET_ADDRESS_LENGTH] & 0xff;
+
+        int toa = origBytes[OFFSET_TOA] & 0xff;
+        ton = 0x7 & (toa >> 4);
+
+        // TOA must have its high bit set
+        if ((toa & 0x80) != 0x80) {
+            throw new ParseException("Invalid TOA - high bit must be set. toa = " + toa,
+                    offset + OFFSET_TOA);
+        }
+
+        if (isAlphanumeric()) {
+            // An alphanumeric address
+            int countSeptets = addressLength * 4 / 7;
+
+            address = GsmAlphabet.gsm7BitPackedToString(origBytes,
+                    OFFSET_ADDRESS_VALUE, countSeptets);
+        } else {
+            // TS 23.040 9.1.2.5 says
+            // that "the MS shall interpret reserved values as 'Unknown'
+            // but shall store them exactly as received"
+
+            byte lastByte = origBytes[length - 1];
+
+            if ((addressLength & 1) == 1) {
+                // Make sure the final unused BCD digit is 0xf
+                origBytes[length - 1] |= 0xf0;
+            }
+            address = PhoneNumberUtils.calledPartyBCDToString(origBytes,
+                    OFFSET_TOA, length - OFFSET_TOA);
+
+            // And restore origBytes
+            origBytes[length - 1] = lastByte;
+        }
+    }
+
+    @Override
+    public String getAddressString() {
+        return address;
+    }
+
+    /**
+     * Returns true if this is an alphanumeric address
+     */
+    @Override
+    public boolean isAlphanumeric() {
+        return ton == TON_ALPHANUMERIC;
+    }
+
+    @Override
+    public boolean isNetworkSpecific() {
+        return ton == TON_NETWORK;
+    }
+
+    /**
+     * Returns true of this is a valid CPHS voice message waiting indicator
+     * address
+     */
+    public boolean isCphsVoiceMessageIndicatorAddress() {
+        // CPHS-style MWI message
+        // See CPHS 4.7 B.4.2.1
+        //
+        // Basically:
+        //
+        // - Originating address should be 4 bytes long and alphanumeric
+        // - Decode will result with two chars:
+        // - Char 1
+        // 76543210
+        // ^ set/clear indicator (0 = clear)
+        // ^^^ type of indicator (000 = voice)
+        // ^^^^ must be equal to 0001
+        // - Char 2:
+        // 76543210
+        // ^ line number (0 = line 1)
+        // ^^^^^^^ set to 0
+        //
+        // Remember, since the alpha address is stored in 7-bit compact form,
+        // the "line number" is really the top bit of the first address value
+        // byte
+
+        return (origBytes[OFFSET_ADDRESS_LENGTH] & 0xff) == 4
+                && isAlphanumeric() && (origBytes[OFFSET_TOA] & 0x0f) == 0;
+    }
+
+    /**
+     * Returns true if this is a valid CPHS voice message waiting indicator
+     * address indicating a "set" of "indicator 1" of type "voice message
+     * waiting"
+     */
+    public boolean isCphsVoiceMessageSet() {
+        // 0x11 means "set" "voice message waiting" "indicator 1"
+        return isCphsVoiceMessageIndicatorAddress()
+                && (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x11;
+
+    }
+
+    /**
+     * Returns true if this is a valid CPHS voice message waiting indicator
+     * address indicating a "clear" of "indicator 1" of type "voice message
+     * waiting"
+     */
+    public boolean isCphsVoiceMessageClear() {
+        // 0x10 means "clear" "voice message waiting" "indicator 1"
+        return isCphsVoiceMessageIndicatorAddress()
+                && (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x10;
+
+    }
+}
diff --git a/com/android/internal/telephony/gsm/GsmSmsCbMessage.java b/com/android/internal/telephony/gsm/GsmSmsCbMessage.java
new file mode 100644
index 0000000..6bf22a0
--- /dev/null
+++ b/com/android/internal/telephony/gsm/GsmSmsCbMessage.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony.gsm;
+
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.util.Pair;
+
+import com.android.internal.R;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.SmsConstants;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is
+ * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases.
+ */
+public class GsmSmsCbMessage {
+
+    /**
+     * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     */
+    private static final String[] LANGUAGE_CODES_GROUP_0 = {
+            "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
+            "pl", null
+    };
+
+    /**
+     * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     */
+    private static final String[] LANGUAGE_CODES_GROUP_2 = {
+            "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null,
+            null, null
+    };
+
+    private static final char CARRIAGE_RETURN = 0x0d;
+
+    private static final int PDU_BODY_PAGE_LENGTH = 82;
+
+    /** Utility class with only static methods. */
+    private GsmSmsCbMessage() { }
+
+    /**
+     * Get built-in ETWS primary messages by category. ETWS primary message does not contain text,
+     * so we have to show the pre-built messages to the user.
+     *
+     * @param context Device context
+     * @param category ETWS message category defined in SmsCbConstants
+     * @return ETWS text message in string. Return an empty string if no match.
+     */
+    private static String getEtwsPrimaryMessage(Context context, int category) {
+        final Resources r = context.getResources();
+        switch (category) {
+            case ETWS_WARNING_TYPE_EARTHQUAKE:
+                return r.getString(R.string.etws_primary_default_message_earthquake);
+            case ETWS_WARNING_TYPE_TSUNAMI:
+                return r.getString(R.string.etws_primary_default_message_tsunami);
+            case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
+                return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami);
+            case ETWS_WARNING_TYPE_TEST_MESSAGE:
+                return r.getString(R.string.etws_primary_default_message_test);
+            case ETWS_WARNING_TYPE_OTHER_EMERGENCY:
+                return r.getString(R.string.etws_primary_default_message_others);
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * Create a new SmsCbMessage object from a header object plus one or more received PDUs.
+     *
+     * @param pdus PDU bytes
+     */
+    public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
+                                                  SmsCbLocation location, byte[][] pdus)
+            throws IllegalArgumentException {
+        if (header.isEtwsPrimaryNotification()) {
+            // ETSI TS 23.041 ETWS Primary Notification message
+            // ETWS primary message only contains 4 fields including serial number,
+            // message identifier, warning type, and warning security information.
+            // There is no field for the content/text so we get the text from the resources.
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(),
+                    header.getSerialNumber(), location, header.getServiceCategory(), null,
+                    getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
+                    SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
+                    header.getCmasInfo());
+        } else {
+            String language = null;
+            StringBuilder sb = new StringBuilder();
+            for (byte[] pdu : pdus) {
+                Pair<String, String> p = parseBody(header, pdu);
+                language = p.first;
+                sb.append(p.second);
+            }
+            int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
+                    : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
+
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                    header.getGeographicalScope(), header.getSerialNumber(), location,
+                    header.getServiceCategory(), language, sb.toString(), priority,
+                    header.getEtwsInfo(), header.getCmasInfo());
+        }
+    }
+
+    /**
+     * Parse and unpack the body text according to the encoding in the DCS.
+     * After completing successfully this method will have assigned the body
+     * text into mBody, and optionally the language code into mLanguage
+     *
+     * @param header the message header to use
+     * @param pdu the PDU to decode
+     * @return a Pair of Strings containing the language and body of the message
+     */
+    private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) {
+        int encoding;
+        String language = null;
+        boolean hasLanguageIndicator = false;
+        int dataCodingScheme = header.getDataCodingScheme();
+
+        // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
+        // section 5.
+        switch ((dataCodingScheme & 0xf0) >> 4) {
+            case 0x00:
+                encoding = SmsConstants.ENCODING_7BIT;
+                language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f];
+                break;
+
+            case 0x01:
+                hasLanguageIndicator = true;
+                if ((dataCodingScheme & 0x0f) == 0x01) {
+                    encoding = SmsConstants.ENCODING_16BIT;
+                } else {
+                    encoding = SmsConstants.ENCODING_7BIT;
+                }
+                break;
+
+            case 0x02:
+                encoding = SmsConstants.ENCODING_7BIT;
+                language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f];
+                break;
+
+            case 0x03:
+                encoding = SmsConstants.ENCODING_7BIT;
+                break;
+
+            case 0x04:
+            case 0x05:
+                switch ((dataCodingScheme & 0x0c) >> 2) {
+                    case 0x01:
+                        encoding = SmsConstants.ENCODING_8BIT;
+                        break;
+
+                    case 0x02:
+                        encoding = SmsConstants.ENCODING_16BIT;
+                        break;
+
+                    case 0x00:
+                    default:
+                        encoding = SmsConstants.ENCODING_7BIT;
+                        break;
+                }
+                break;
+
+            case 0x06:
+            case 0x07:
+                // Compression not supported
+            case 0x09:
+                // UDH structure not supported
+            case 0x0e:
+                // Defined by the WAP forum not supported
+                throw new IllegalArgumentException("Unsupported GSM dataCodingScheme "
+                        + dataCodingScheme);
+
+            case 0x0f:
+                if (((dataCodingScheme & 0x04) >> 2) == 0x01) {
+                    encoding = SmsConstants.ENCODING_8BIT;
+                } else {
+                    encoding = SmsConstants.ENCODING_7BIT;
+                }
+                break;
+
+            default:
+                // Reserved values are to be treated as 7-bit
+                encoding = SmsConstants.ENCODING_7BIT;
+                break;
+        }
+
+        if (header.isUmtsFormat()) {
+            // Payload may contain multiple pages
+            int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
+
+            if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
+                    * nrPages) {
+                throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
+                        + nrPages + " pages");
+            }
+
+            StringBuilder sb = new StringBuilder();
+
+            for (int i = 0; i < nrPages; i++) {
+                // Each page is 82 bytes followed by a length octet indicating
+                // the number of useful octets within those 82
+                int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
+                int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
+
+                if (length > PDU_BODY_PAGE_LENGTH) {
+                    throw new IllegalArgumentException("Page length " + length
+                            + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
+                }
+
+                Pair<String, String> p = unpackBody(pdu, encoding, offset, length,
+                        hasLanguageIndicator, language);
+                language = p.first;
+                sb.append(p.second);
+            }
+            return new Pair<String, String>(language, sb.toString());
+        } else {
+            // Payload is one single page
+            int offset = SmsCbHeader.PDU_HEADER_LENGTH;
+            int length = pdu.length - offset;
+
+            return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language);
+        }
+    }
+
+    /**
+     * Unpack body text from the pdu using the given encoding, position and
+     * length within the pdu
+     *
+     * @param pdu The pdu
+     * @param encoding The encoding, as derived from the DCS
+     * @param offset Position of the first byte to unpack
+     * @param length Number of bytes to unpack
+     * @param hasLanguageIndicator true if the body text is preceded by a
+     *            language indicator. If so, this method will as a side-effect
+     *            assign the extracted language code into mLanguage
+     * @param language the language to return if hasLanguageIndicator is false
+     * @return a Pair of Strings containing the language and body of the message
+     */
+    private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length,
+            boolean hasLanguageIndicator, String language) {
+        String body = null;
+
+        switch (encoding) {
+            case SmsConstants.ENCODING_7BIT:
+                body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
+
+                if (hasLanguageIndicator && body != null && body.length() > 2) {
+                    // Language is two GSM characters followed by a CR.
+                    // The actual body text is offset by 3 characters.
+                    language = body.substring(0, 2);
+                    body = body.substring(3);
+                }
+                break;
+
+            case SmsConstants.ENCODING_16BIT:
+                if (hasLanguageIndicator && pdu.length >= offset + 2) {
+                    // Language is two GSM characters.
+                    // The actual body text is offset by 2 bytes.
+                    language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
+                    offset += 2;
+                    length -= 2;
+                }
+
+                try {
+                    body = new String(pdu, offset, (length & 0xfffe), "utf-16");
+                } catch (UnsupportedEncodingException e) {
+                    // Apparently it wasn't valid UTF-16.
+                    throw new IllegalArgumentException("Error decoding UTF-16 message", e);
+                }
+                break;
+
+            default:
+                break;
+        }
+
+        if (body != null) {
+            // Remove trailing carriage return
+            for (int i = body.length() - 1; i >= 0; i--) {
+                if (body.charAt(i) != CARRIAGE_RETURN) {
+                    body = body.substring(0, i + 1);
+                    break;
+                }
+            }
+        } else {
+            body = "";
+        }
+
+        return new Pair<String, String>(language, body);
+    }
+}
diff --git a/com/android/internal/telephony/gsm/SimTlv.java b/com/android/internal/telephony/gsm/SimTlv.java
new file mode 100644
index 0000000..c98b9a1
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SimTlv.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.gsm;
+
+/**
+ * SIM Tag-Length-Value record
+ * TS 102 223 Annex C
+ *
+ * {@hide}
+ *
+ */
+public class SimTlv
+{
+    //***** Private Instance Variables
+
+    byte mRecord[];
+    int mTlvOffset;
+    int mTlvLength;
+    int mCurOffset;
+    int mCurDataOffset;
+    int mCurDataLength;
+    boolean mHasValidTlvObject;
+
+    public SimTlv(byte[] record, int offset, int length) {
+        mRecord = record;
+
+        mTlvOffset = offset;
+        mTlvLength = length;
+        mCurOffset = offset;
+
+        mHasValidTlvObject = parseCurrentTlvObject();
+    }
+
+    public boolean nextObject() {
+        if (!mHasValidTlvObject) return false;
+        mCurOffset = mCurDataOffset + mCurDataLength;
+        mHasValidTlvObject = parseCurrentTlvObject();
+        return mHasValidTlvObject;
+    }
+
+    public boolean isValidObject() {
+        return mHasValidTlvObject;
+    }
+
+    /**
+     * Returns the tag for the current TLV object
+     * Return 0 if !isValidObject()
+     * 0 and 0xff are invalid tag values
+     * valid tags range from 1 - 0xfe
+     */
+    public int getTag() {
+        if (!mHasValidTlvObject) return 0;
+        return mRecord[mCurOffset] & 0xff;
+    }
+
+    /**
+     * Returns data associated with current TLV object
+     * returns null if !isValidObject()
+     */
+
+    public byte[] getData() {
+        if (!mHasValidTlvObject) return null;
+
+        byte[] ret = new byte[mCurDataLength];
+        System.arraycopy(mRecord, mCurDataOffset, ret, 0, mCurDataLength);
+        return ret;
+    }
+
+    /**
+     * Updates curDataLength and curDataOffset
+     * @return false on invalid record, true on valid record
+     */
+
+    private boolean parseCurrentTlvObject() {
+        // 0x00 and 0xff are invalid tag values
+
+        try {
+            if (mRecord[mCurOffset] == 0 || (mRecord[mCurOffset] & 0xff) == 0xff) {
+                return false;
+            }
+
+            if ((mRecord[mCurOffset + 1] & 0xff) < 0x80) {
+                // one byte length 0 - 0x7f
+                mCurDataLength = mRecord[mCurOffset + 1] & 0xff;
+                mCurDataOffset = mCurOffset + 2;
+            } else if ((mRecord[mCurOffset + 1] & 0xff) == 0x81) {
+                // two byte length 0x80 - 0xff
+                mCurDataLength = mRecord[mCurOffset + 2] & 0xff;
+                mCurDataOffset = mCurOffset + 3;
+            } else {
+                return false;
+            }
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            return false;
+        }
+
+        if (mCurDataLength + mCurDataOffset > mTlvOffset + mTlvLength) {
+            return false;
+        }
+
+        return true;
+    }
+
+}
diff --git a/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java b/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java
new file mode 100644
index 0000000..f4f4036
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java
@@ -0,0 +1,133 @@
+/*
+ * 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.internal.telephony.gsm;
+
+/**
+ * SmsBroadcastConfigInfo defines one configuration of Cell Broadcast
+ * Message (CBM) to be received by the ME
+ *
+ * fromServiceId - toServiceId defines a range of CBM message identifiers
+ * whose value is 0x0000 - 0xFFFF as defined in TS 23.041 9.4.1.2.2 for GMS
+ * and 9.4.4.2.2 for UMTS. All other values can be treated as empty
+ * CBM message ID.
+ *
+ * fromCodeScheme - toCodeScheme defines a range of CBM data coding schemes
+ * whose value is 0x00 - 0xFF as defined in TS 23.041 9.4.1.2.3 for GMS
+ * and 9.4.4.2.3 for UMTS.
+ * All other values can be treated as empty CBM data coding scheme.
+ *
+ * selected false means message types specified in {@code <fromServiceId, toServiceId>}
+ * and {@code <fromCodeScheme, toCodeScheme>} are not accepted, while true means accepted.
+ *
+ */
+public final class SmsBroadcastConfigInfo {
+    private int mFromServiceId;
+    private int mToServiceId;
+    private int mFromCodeScheme;
+    private int mToCodeScheme;
+    private boolean mSelected;
+
+    /**
+     * Initialize the object from rssi and cid.
+     */
+    public SmsBroadcastConfigInfo(int fromId, int toId, int fromScheme,
+            int toScheme, boolean selected) {
+        mFromServiceId = fromId;
+        mToServiceId = toId;
+        mFromCodeScheme = fromScheme;
+        mToCodeScheme = toScheme;
+        mSelected = selected;
+    }
+
+    /**
+     * @param fromServiceId the fromServiceId to set
+     */
+    public void setFromServiceId(int fromServiceId) {
+        mFromServiceId = fromServiceId;
+    }
+
+    /**
+     * @return the fromServiceId
+     */
+    public int getFromServiceId() {
+        return mFromServiceId;
+    }
+
+    /**
+     * @param toServiceId the toServiceId to set
+     */
+    public void setToServiceId(int toServiceId) {
+        mToServiceId = toServiceId;
+    }
+
+    /**
+     * @return the toServiceId
+     */
+    public int getToServiceId() {
+        return mToServiceId;
+    }
+
+    /**
+     * @param fromCodeScheme the fromCodeScheme to set
+     */
+    public void setFromCodeScheme(int fromCodeScheme) {
+        mFromCodeScheme = fromCodeScheme;
+    }
+
+    /**
+     * @return the fromCodeScheme
+     */
+    public int getFromCodeScheme() {
+        return mFromCodeScheme;
+    }
+
+    /**
+     * @param toCodeScheme the toCodeScheme to set
+     */
+    public void setToCodeScheme(int toCodeScheme) {
+        mToCodeScheme = toCodeScheme;
+    }
+
+    /**
+     * @return the toCodeScheme
+     */
+    public int getToCodeScheme() {
+        return mToCodeScheme;
+    }
+
+    /**
+     * @param selected the selected to set
+     */
+    public void setSelected(boolean selected) {
+        mSelected = selected;
+    }
+
+    /**
+     * @return the selected
+     */
+    public boolean isSelected() {
+        return mSelected;
+    }
+
+    @Override
+    public String toString() {
+        return "SmsBroadcastConfigInfo: Id [" +
+                mFromServiceId + ',' + mToServiceId + "] Code [" +
+                mFromCodeScheme + ',' + mToCodeScheme + "] " +
+            (mSelected ? "ENABLED" : "DISABLED");
+    }
+}
diff --git a/com/android/internal/telephony/gsm/SmsCbConstants.java b/com/android/internal/telephony/gsm/SmsCbConstants.java
new file mode 100644
index 0000000..0fabc2f
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SmsCbConstants.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony.gsm;
+
+/**
+ * Constants used in SMS Cell Broadcast messages (see 3GPP TS 23.041). This class is used by the
+ * boot-time broadcast channel enable and database upgrade code in CellBroadcastReceiver, so it
+ * is public, but should be avoided in favor of the radio technology independent constants in
+ * {@link android.telephony.SmsCbMessage}, {@link android.telephony.SmsCbEtwsInfo}, and
+ * {@link android.telephony.SmsCbCmasInfo} classes.
+ *
+ * {@hide}
+ */
+public class SmsCbConstants {
+
+    /** Private constructor for utility class. */
+    private SmsCbConstants() { }
+
+    /** Start of PWS Message Identifier range (includes ETWS and CMAS). */
+    public static final int MESSAGE_ID_PWS_FIRST_IDENTIFIER
+            = 0x1100; // 4352
+
+    /** Bitmask for messages of ETWS type (including future extensions). */
+    public static final int MESSAGE_ID_ETWS_TYPE_MASK
+            = 0xFFF8;
+
+    /** Value for messages of ETWS type after applying {@link #MESSAGE_ID_ETWS_TYPE_MASK}. */
+    public static final int MESSAGE_ID_ETWS_TYPE
+            = 0x1100; // 4352
+
+    /** ETWS Message Identifier for earthquake warning message. */
+    public static final int MESSAGE_ID_ETWS_EARTHQUAKE_WARNING
+            = 0x1100; // 4352
+
+    /** ETWS Message Identifier for tsunami warning message. */
+    public static final int MESSAGE_ID_ETWS_TSUNAMI_WARNING
+            = 0x1101; // 4353
+
+    /** ETWS Message Identifier for earthquake and tsunami combined warning message. */
+    public static final int MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING
+            = 0x1102; // 4354
+
+    /** ETWS Message Identifier for test message. */
+    public static final int MESSAGE_ID_ETWS_TEST_MESSAGE
+            = 0x1103; // 4355
+
+    /** ETWS Message Identifier for messages related to other emergency types. */
+    public static final int MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE
+            = 0x1104; // 4356
+
+    /** Start of CMAS Message Identifier range. */
+    public static final int MESSAGE_ID_CMAS_FIRST_IDENTIFIER
+            = 0x1112; // 4370
+
+    /** CMAS Message Identifier for Presidential Level alerts. */
+    public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL
+            = 0x1112; // 4370
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED
+            = 0x1113; // 4371
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY
+            = 0x1114; // 4372
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED
+            = 0x1115; // 4373
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY
+            = 0x1116; // 4374
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED
+            = 0x1117; // 4375
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY
+            = 0x1118; // 4376
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED
+            = 0x1119; // 4377
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY
+            = 0x111A; // 4378
+
+    /** CMAS Message Identifier for Child Abduction Emergency (Amber Alert). */
+    public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY
+            = 0x111B; // 4379
+
+    /** CMAS Message Identifier for the Required Monthly Test. */
+    public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST
+            = 0x111C; // 4380
+
+    /** CMAS Message Identifier for CMAS Exercise. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE
+            = 0x111D; // 4381
+
+    /** CMAS Message Identifier for operator defined use. */
+    public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE
+            = 0x111E; // 4382
+
+    /** CMAS Message Identifier for Presidential Level alerts for additional languages
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE
+            = 0x111F; // 4383
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE
+            = 0x1120; // 4384
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE
+            = 0x1121; // 4385
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE
+            = 0x1122; // 4386
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE
+            = 0x1123; // 4387
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE
+            = 0x1124; // 4388
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely
+     *  for additional languages.*/
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE
+            = 0x1125; // 4389
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE
+            = 0x1126; // 4390
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely
+     *  for additional languages.*/
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE
+            = 0x1127; // 4391
+
+    /** CMAS Message Identifier for Child Abduction Emergency (Amber Alert)
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE
+            = 0x1128; // 4392
+
+    /** CMAS Message Identifier for the Required Monthly Test
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE
+            = 0x1129; // 4393
+
+    /** CMAS Message Identifier for CMAS Exercise
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE
+            = 0x112A; // 4394
+
+    /** CMAS Message Identifier for operator defined use
+     *  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE
+            = 0x112B; // 4395
+
+    /** End of CMAS Message Identifier range (including future extensions). */
+    public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER
+            = 0x112F; // 4399
+
+    /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */
+    public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER
+            = 0x18FF; // 6399
+
+    /** ETWS serial number flag to activate the popup display. */
+    public static final int SERIAL_NUMBER_ETWS_ACTIVATE_POPUP
+            = 0x1000; // 4096
+
+    /** ETWS serial number flag to activate the emergency user alert. */
+    public static final int SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT
+            = 0x2000; // 8192
+}
diff --git a/com/android/internal/telephony/gsm/SmsCbHeader.java b/com/android/internal/telephony/gsm/SmsCbHeader.java
new file mode 100644
index 0000000..d267ad2
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SmsCbHeader.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.gsm;
+
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbEtwsInfo;
+
+import java.util.Arrays;
+
+/**
+ * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by
+ * CellBroadcastReceiver test cases, but should not be used by applications.
+ *
+ * All relevant header information is now sent as a Parcelable
+ * {@link android.telephony.SmsCbMessage} object in the "message" extra of the
+ * {@link android.provider.Telephony.Sms.Intents#SMS_CB_RECEIVED_ACTION} or
+ * {@link android.provider.Telephony.Sms.Intents#SMS_EMERGENCY_CB_RECEIVED_ACTION} intent.
+ * The raw PDU is no longer sent to SMS CB applications.
+ */
+public class SmsCbHeader {
+
+    /**
+     * Length of SMS-CB header
+     */
+    static final int PDU_HEADER_LENGTH = 6;
+
+    /**
+     * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1
+     */
+    static final int FORMAT_GSM = 1;
+
+    /**
+     * UMTS pdu format, as defined in 3gpp TS 23.041, section 9.4.2
+     */
+    static final int FORMAT_UMTS = 2;
+
+    /**
+     * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1.3
+     */
+    static final int FORMAT_ETWS_PRIMARY = 3;
+
+    /**
+     * Message type value as defined in 3gpp TS 25.324, section 11.1.
+     */
+    private static final int MESSAGE_TYPE_CBS_MESSAGE = 1;
+
+    /**
+     * Length of GSM pdus
+     */
+    private static final int PDU_LENGTH_GSM = 88;
+
+    /**
+     * Maximum length of ETWS primary message GSM pdus
+     */
+    private static final int PDU_LENGTH_ETWS = 56;
+
+    private final int mGeographicalScope;
+
+    /** The serial number combines geographical scope, message code, and update number. */
+    private final int mSerialNumber;
+
+    /** The Message Identifier in 3GPP is the same as the Service Category in CDMA. */
+    private final int mMessageIdentifier;
+
+    private final int mDataCodingScheme;
+
+    private final int mPageIndex;
+
+    private final int mNrOfPages;
+
+    private final int mFormat;
+
+    /** ETWS warning notification info. */
+    private final SmsCbEtwsInfo mEtwsInfo;
+
+    /** CMAS warning notification info. */
+    private final SmsCbCmasInfo mCmasInfo;
+
+    public SmsCbHeader(byte[] pdu) throws IllegalArgumentException {
+        if (pdu == null || pdu.length < PDU_HEADER_LENGTH) {
+            throw new IllegalArgumentException("Illegal PDU");
+        }
+
+        if (pdu.length <= PDU_LENGTH_GSM) {
+            // can be ETWS or GSM format.
+            // Per TS23.041 9.4.1.2 and 9.4.1.3.2, GSM and ETWS format both
+            // contain serial number which contains GS, Message Code, and Update Number
+            // per 9.4.1.2.1, and message identifier in same octets
+            mGeographicalScope = (pdu[0] & 0xc0) >>> 6;
+            mSerialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff);
+            mMessageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff);
+            if (isEtwsMessage() && pdu.length <= PDU_LENGTH_ETWS) {
+                mFormat = FORMAT_ETWS_PRIMARY;
+                mDataCodingScheme = -1;
+                mPageIndex = -1;
+                mNrOfPages = -1;
+                boolean emergencyUserAlert = (pdu[4] & 0x1) != 0;
+                boolean activatePopup = (pdu[5] & 0x80) != 0;
+                int warningType = (pdu[4] & 0xfe) >>> 1;
+                byte[] warningSecurityInfo;
+                // copy the Warning-Security-Information, if present
+                if (pdu.length > PDU_HEADER_LENGTH) {
+                    warningSecurityInfo = Arrays.copyOfRange(pdu, 6, pdu.length);
+                } else {
+                    warningSecurityInfo = null;
+                }
+                mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup,
+                        true, warningSecurityInfo);
+                mCmasInfo = null;
+                return;     // skip the ETWS/CMAS initialization code for regular notifications
+            } else {
+                // GSM pdus are no more than 88 bytes
+                mFormat = FORMAT_GSM;
+                mDataCodingScheme = pdu[4] & 0xff;
+
+                // Check for invalid page parameter
+                int pageIndex = (pdu[5] & 0xf0) >>> 4;
+                int nrOfPages = pdu[5] & 0x0f;
+
+                if (pageIndex == 0 || nrOfPages == 0 || pageIndex > nrOfPages) {
+                    pageIndex = 1;
+                    nrOfPages = 1;
+                }
+
+                mPageIndex = pageIndex;
+                mNrOfPages = nrOfPages;
+            }
+        } else {
+            // UMTS pdus are always at least 90 bytes since the payload includes
+            // a number-of-pages octet and also one length octet per page
+            mFormat = FORMAT_UMTS;
+
+            int messageType = pdu[0];
+
+            if (messageType != MESSAGE_TYPE_CBS_MESSAGE) {
+                throw new IllegalArgumentException("Unsupported message type " + messageType);
+            }
+
+            mMessageIdentifier = ((pdu[1] & 0xff) << 8) | pdu[2] & 0xff;
+            mGeographicalScope = (pdu[3] & 0xc0) >>> 6;
+            mSerialNumber = ((pdu[3] & 0xff) << 8) | (pdu[4] & 0xff);
+            mDataCodingScheme = pdu[5] & 0xff;
+
+            // We will always consider a UMTS message as having one single page
+            // since there's only one instance of the header, even though the
+            // actual payload may contain several pages.
+            mPageIndex = 1;
+            mNrOfPages = 1;
+        }
+
+        if (isEtwsMessage()) {
+            boolean emergencyUserAlert = isEtwsEmergencyUserAlert();
+            boolean activatePopup = isEtwsPopupAlert();
+            int warningType = getEtwsWarningType();
+            mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup,
+                    false, null);
+            mCmasInfo = null;
+        } else if (isCmasMessage()) {
+            int messageClass = getCmasMessageClass();
+            int severity = getCmasSeverity();
+            int urgency = getCmasUrgency();
+            int certainty = getCmasCertainty();
+            mEtwsInfo = null;
+            mCmasInfo = new SmsCbCmasInfo(messageClass, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN,
+                    SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, severity, urgency, certainty);
+        } else {
+            mEtwsInfo = null;
+            mCmasInfo = null;
+        }
+    }
+
+    int getGeographicalScope() {
+        return mGeographicalScope;
+    }
+
+    int getSerialNumber() {
+        return mSerialNumber;
+    }
+
+    int getServiceCategory() {
+        return mMessageIdentifier;
+    }
+
+    int getDataCodingScheme() {
+        return mDataCodingScheme;
+    }
+
+    int getPageIndex() {
+        return mPageIndex;
+    }
+
+    int getNumberOfPages() {
+        return mNrOfPages;
+    }
+
+    SmsCbEtwsInfo getEtwsInfo() {
+        return mEtwsInfo;
+    }
+
+    SmsCbCmasInfo getCmasInfo() {
+        return mCmasInfo;
+    }
+
+    /**
+     * Return whether this broadcast is an emergency (PWS) message type.
+     * @return true if this message is emergency type; false otherwise
+     */
+    boolean isEmergencyMessage() {
+        return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_PWS_FIRST_IDENTIFIER
+                && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_PWS_LAST_IDENTIFIER;
+    }
+
+    /**
+     * Return whether this broadcast is an ETWS emergency message type.
+     * @return true if this message is ETWS emergency type; false otherwise
+     */
+    private boolean isEtwsMessage() {
+        return (mMessageIdentifier & SmsCbConstants.MESSAGE_ID_ETWS_TYPE_MASK)
+                == SmsCbConstants.MESSAGE_ID_ETWS_TYPE;
+    }
+
+    /**
+     * Return whether this broadcast is an ETWS primary notification.
+     * @return true if this message is an ETWS primary notification; false otherwise
+     */
+    boolean isEtwsPrimaryNotification() {
+        return mFormat == FORMAT_ETWS_PRIMARY;
+    }
+
+    /**
+     * Return whether this broadcast is in UMTS format.
+     * @return true if this message is in UMTS format; false otherwise
+     */
+    boolean isUmtsFormat() {
+        return mFormat == FORMAT_UMTS;
+    }
+
+    /**
+     * Return whether this message is a CMAS emergency message type.
+     * @return true if this message is CMAS emergency type; false otherwise
+     */
+    private boolean isCmasMessage() {
+        return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_CMAS_FIRST_IDENTIFIER
+                && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_CMAS_LAST_IDENTIFIER;
+    }
+
+    /**
+     * Return whether the popup alert flag is set for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return true if the message code indicates a popup alert should be displayed
+     */
+    private boolean isEtwsPopupAlert() {
+        return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_ACTIVATE_POPUP) != 0;
+    }
+
+    /**
+     * Return whether the emergency user alert flag is set for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return true if the message code indicates an emergency user alert
+     */
+    private boolean isEtwsEmergencyUserAlert() {
+        return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT) != 0;
+    }
+
+    /**
+     * Returns the warning type for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return the ETWS warning type defined in 3GPP TS 23.041 section 9.3.24
+     */
+    private int getEtwsWarningType() {
+        return mMessageIdentifier - SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING;
+    }
+
+    /**
+     * Returns the message class for a CMAS warning notification.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS message class as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasMessageClass() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the severity for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS severity as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasSeverity() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_SEVERITY_EXTREME;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_SEVERITY_SEVERE;
+
+            default:
+                return SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the urgency for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS urgency as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasUrgency() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_URGENCY_EXPECTED;
+
+            default:
+                return SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the certainty for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS certainty as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasCertainty() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SmsCbHeader{GS=" + mGeographicalScope + ", serialNumber=0x" +
+                Integer.toHexString(mSerialNumber) +
+                ", messageIdentifier=0x" + Integer.toHexString(mMessageIdentifier) +
+                ", DCS=0x" + Integer.toHexString(mDataCodingScheme) +
+                ", page " + mPageIndex + " of " + mNrOfPages + '}';
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/gsm/SmsMessage.java b/com/android/internal/telephony/gsm/SmsMessage.java
new file mode 100644
index 0000000..d4098d9
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SmsMessage.java
@@ -0,0 +1,1379 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.gsm;
+
+import android.telephony.PhoneNumberUtils;
+import android.text.format.Time;
+import android.telephony.Rlog;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.EncodeException;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.Sms7BitEncodingTranslator;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.ParseException;
+
+import static com.android.internal.telephony.SmsConstants.MessageClass;
+import static com.android.internal.telephony.SmsConstants.ENCODING_UNKNOWN;
+import static com.android.internal.telephony.SmsConstants.ENCODING_7BIT;
+import static com.android.internal.telephony.SmsConstants.ENCODING_8BIT;
+import static com.android.internal.telephony.SmsConstants.ENCODING_16BIT;
+import static com.android.internal.telephony.SmsConstants.ENCODING_KSC5601;
+import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_SEPTETS;
+import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_BYTES;
+import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER;
+
+/**
+ * A Short Message Service message.
+ *
+ */
+public class SmsMessage extends SmsMessageBase {
+    static final String LOG_TAG = "SmsMessage";
+    private static final boolean VDBG = false;
+
+    private MessageClass messageClass;
+
+    /**
+     * TP-Message-Type-Indicator
+     * 9.2.3
+     */
+    private int mMti;
+
+    /** TP-Protocol-Identifier (TP-PID) */
+    private int mProtocolIdentifier;
+
+    // TP-Data-Coding-Scheme
+    // see TS 23.038
+    private int mDataCodingScheme;
+
+    // TP-Reply-Path
+    // e.g. 23.040 9.2.2.1
+    private boolean mReplyPathPresent = false;
+
+    /** The address of the receiver. */
+    private GsmSmsAddress mRecipientAddress;
+
+    /**
+     *  TP-Status - status of a previously submitted SMS.
+     *  This field applies to SMS-STATUS-REPORT messages.  0 indicates success;
+     *  see TS 23.040, 9.2.3.15 for description of other possible values.
+     */
+    private int mStatus;
+
+    /**
+     *  TP-Status - status of a previously submitted SMS.
+     *  This field is true iff the message is a SMS-STATUS-REPORT message.
+     */
+    private boolean mIsStatusReportMessage = false;
+
+    private int mVoiceMailCount = 0;
+
+    public static class SubmitPdu extends SubmitPduBase {
+    }
+
+    /**
+     * Create an SmsMessage from a raw PDU.
+     */
+    public static SmsMessage createFromPdu(byte[] pdu) {
+        try {
+            SmsMessage msg = new SmsMessage();
+            msg.parsePdu(pdu);
+            return msg;
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
+            return null;
+        } catch (OutOfMemoryError e) {
+            Rlog.e(LOG_TAG, "SMS PDU parsing failed with out of memory: ", e);
+            return null;
+        }
+    }
+
+    /**
+     * 3GPP TS 23.040 9.2.3.9 specifies that Type Zero messages are indicated
+     * by TP_PID field set to value 0x40
+     */
+    public boolean isTypeZero() {
+        return (mProtocolIdentifier == 0x40);
+    }
+
+    /**
+     * TS 27.005 3.4.1 lines[0] and lines[1] are the two lines read from the
+     * +CMT unsolicited response (PDU mode, of course)
+     *  +CMT: [&lt;alpha>],<length><CR><LF><pdu>
+     *
+     * Only public for debugging
+     *
+     * {@hide}
+     */
+    public static SmsMessage newFromCMT(byte[] pdu) {
+        try {
+            SmsMessage msg = new SmsMessage();
+            msg.parsePdu(pdu);
+            return msg;
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
+            return null;
+        }
+    }
+
+    /** @hide */
+    public static SmsMessage newFromCDS(byte[] pdu) {
+        try {
+            SmsMessage msg = new SmsMessage();
+            msg.parsePdu(pdu);
+            return msg;
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "CDS SMS PDU parsing failed: ", ex);
+            return null;
+        }
+    }
+
+    /**
+     * Create an SmsMessage from an SMS EF record.
+     *
+     * @param index Index of SMS record. This should be index in ArrayList
+     *              returned by SmsManager.getAllMessagesFromSim + 1.
+     * @param data Record data.
+     * @return An SmsMessage representing the record.
+     *
+     * @hide
+     */
+    public static SmsMessage createFromEfRecord(int index, byte[] data) {
+        try {
+            SmsMessage msg = new SmsMessage();
+
+            msg.mIndexOnIcc = index;
+
+            // First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT,
+            // or STORED_UNSENT
+            // See TS 51.011 10.5.3
+            if ((data[0] & 1) == 0) {
+                Rlog.w(LOG_TAG,
+                        "SMS parsing failed: Trying to parse a free record");
+                return null;
+            } else {
+                msg.mStatusOnIcc = data[0] & 0x07;
+            }
+
+            int size = data.length - 1;
+
+            // Note: Data may include trailing FF's.  That's OK; message
+            // should still parse correctly.
+            byte[] pdu = new byte[size];
+            System.arraycopy(data, 1, pdu, 0, size);
+            msg.parsePdu(pdu);
+            return msg;
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
+            return null;
+        }
+    }
+
+    /**
+     * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the
+     * length in bytes (not hex chars) less the SMSC header
+     */
+    public static int getTPLayerLengthForPDU(String pdu) {
+        int len = pdu.length() / 2;
+        int smscLen = Integer.parseInt(pdu.substring(0, 2), 16);
+
+        return len - smscLen - 1;
+    }
+
+    /**
+     * Get an SMS-SUBMIT PDU for a destination address and a message
+     *
+     * @param scAddress Service Centre address.  Null means use default.
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     * @hide
+     */
+    public static SubmitPdu getSubmitPdu(String scAddress,
+            String destinationAddress, String message,
+            boolean statusReportRequested, byte[] header) {
+        return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, header,
+                ENCODING_UNKNOWN, 0, 0);
+    }
+
+
+    /**
+     * Get an SMS-SUBMIT PDU for a destination address and a message using the
+     * specified encoding.
+     *
+     * @param scAddress Service Centre address.  Null means use default.
+     * @param encoding Encoding defined by constants in
+     *        com.android.internal.telephony.SmsConstants.ENCODING_*
+     * @param languageTable
+     * @param languageShiftTable
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     * @hide
+     */
+    public static SubmitPdu getSubmitPdu(String scAddress,
+            String destinationAddress, String message,
+            boolean statusReportRequested, byte[] header, int encoding,
+            int languageTable, int languageShiftTable) {
+
+        // Perform null parameter checks.
+        if (message == null || destinationAddress == null) {
+            return null;
+        }
+
+        if (encoding == ENCODING_UNKNOWN) {
+            // Find the best encoding to use
+            TextEncodingDetails ted = calculateLength(message, false);
+            encoding = ted.codeUnitSize;
+            languageTable = ted.languageTable;
+            languageShiftTable = ted.languageShiftTable;
+
+            if (encoding == ENCODING_7BIT &&
+                    (languageTable != 0 || languageShiftTable != 0)) {
+                if (header != null) {
+                    SmsHeader smsHeader = SmsHeader.fromByteArray(header);
+                    if (smsHeader.languageTable != languageTable
+                            || smsHeader.languageShiftTable != languageShiftTable) {
+                        Rlog.w(LOG_TAG, "Updating language table in SMS header: "
+                                + smsHeader.languageTable + " -> " + languageTable + ", "
+                                + smsHeader.languageShiftTable + " -> " + languageShiftTable);
+                        smsHeader.languageTable = languageTable;
+                        smsHeader.languageShiftTable = languageShiftTable;
+                        header = SmsHeader.toByteArray(smsHeader);
+                    }
+                } else {
+                    SmsHeader smsHeader = new SmsHeader();
+                    smsHeader.languageTable = languageTable;
+                    smsHeader.languageShiftTable = languageShiftTable;
+                    header = SmsHeader.toByteArray(smsHeader);
+                }
+            }
+        }
+
+        SubmitPdu ret = new SubmitPdu();
+        // MTI = SMS-SUBMIT, UDHI = header != null
+        byte mtiByte = (byte)(0x01 | (header != null ? 0x40 : 0x00));
+        ByteArrayOutputStream bo = getSubmitPduHead(
+                scAddress, destinationAddress, mtiByte,
+                statusReportRequested, ret);
+
+        // Skip encoding pdu if error occurs when create pdu head and the error will be handled
+        // properly later on encodedMessage sanity check.
+        if (bo == null) return ret;
+
+        // User Data (and length)
+        byte[] userData;
+        try {
+            if (encoding == ENCODING_7BIT) {
+                userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, header,
+                        languageTable, languageShiftTable);
+            } else { //assume UCS-2
+                try {
+                    userData = encodeUCS2(message, header);
+                } catch(UnsupportedEncodingException uex) {
+                    Rlog.e(LOG_TAG,
+                            "Implausible UnsupportedEncodingException ",
+                            uex);
+                    return null;
+                }
+            }
+        } catch (EncodeException ex) {
+            // Encoding to the 7-bit alphabet failed. Let's see if we can
+            // send it as a UCS-2 encoded message
+            try {
+                userData = encodeUCS2(message, header);
+                encoding = ENCODING_16BIT;
+            } catch(UnsupportedEncodingException uex) {
+                Rlog.e(LOG_TAG,
+                        "Implausible UnsupportedEncodingException ",
+                        uex);
+                return null;
+            }
+        }
+
+        if (encoding == ENCODING_7BIT) {
+            if ((0xff & userData[0]) > MAX_USER_DATA_SEPTETS) {
+                // Message too long
+                Rlog.e(LOG_TAG, "Message too long (" + (0xff & userData[0]) + " septets)");
+                return null;
+            }
+            // TP-Data-Coding-Scheme
+            // Default encoding, uncompressed
+            // To test writing messages to the SIM card, change this value 0x00
+            // to 0x12, which means "bits 1 and 0 contain message class, and the
+            // class is 2". Note that this takes effect for the sender. In other
+            // words, messages sent by the phone with this change will end up on
+            // the receiver's SIM card. You can then send messages to yourself
+            // (on a phone with this change) and they'll end up on the SIM card.
+            bo.write(0x00);
+        } else { // assume UCS-2
+            if ((0xff & userData[0]) > MAX_USER_DATA_BYTES) {
+                // Message too long
+                Rlog.e(LOG_TAG, "Message too long (" + (0xff & userData[0]) + " bytes)");
+                return null;
+            }
+            // TP-Data-Coding-Scheme
+            // UCS-2 encoding, uncompressed
+            bo.write(0x08);
+        }
+
+        // (no TP-Validity-Period)
+        bo.write(userData, 0, userData.length);
+        ret.encodedMessage = bo.toByteArray();
+        return ret;
+    }
+
+    /**
+     * Packs header and UCS-2 encoded message. Includes TP-UDL & TP-UDHL if necessary
+     *
+     * @return encoded message as UCS2
+     * @throws UnsupportedEncodingException
+     */
+    private static byte[] encodeUCS2(String message, byte[] header)
+        throws UnsupportedEncodingException {
+        byte[] userData, textPart;
+        textPart = message.getBytes("utf-16be");
+
+        if (header != null) {
+            // Need 1 byte for UDHL
+            userData = new byte[header.length + textPart.length + 1];
+
+            userData[0] = (byte)header.length;
+            System.arraycopy(header, 0, userData, 1, header.length);
+            System.arraycopy(textPart, 0, userData, header.length + 1, textPart.length);
+        }
+        else {
+            userData = textPart;
+        }
+        byte[] ret = new byte[userData.length+1];
+        ret[0] = (byte) (userData.length & 0xff );
+        System.arraycopy(userData, 0, ret, 1, userData.length);
+        return ret;
+    }
+
+    /**
+     * Get an SMS-SUBMIT PDU for a destination address and a message
+     *
+     * @param scAddress Service Centre address.  Null means use default.
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     */
+    public static SubmitPdu getSubmitPdu(String scAddress,
+            String destinationAddress, String message,
+            boolean statusReportRequested) {
+
+        return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, null);
+    }
+
+    /**
+     * Get an SMS-SUBMIT PDU for a data message to a destination address &amp; port
+     *
+     * @param scAddress Service Centre address. null == use default
+     * @param destinationAddress the address of the destination for the message
+     * @param destinationPort the port to deliver the message to at the
+     *        destination
+     * @param data the data for the message
+     * @return a <code>SubmitPdu</code> containing the encoded SC
+     *         address, if applicable, and the encoded message.
+     *         Returns null on encode error.
+     */
+    public static SubmitPdu getSubmitPdu(String scAddress,
+            String destinationAddress, int destinationPort, byte[] data,
+            boolean statusReportRequested) {
+
+        SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs();
+        portAddrs.destPort = destinationPort;
+        portAddrs.origPort = 0;
+        portAddrs.areEightBits = false;
+
+        SmsHeader smsHeader = new SmsHeader();
+        smsHeader.portAddrs = portAddrs;
+
+        byte[] smsHeaderData = SmsHeader.toByteArray(smsHeader);
+
+        if ((data.length + smsHeaderData.length + 1) > MAX_USER_DATA_BYTES) {
+            Rlog.e(LOG_TAG, "SMS data message may only contain "
+                    + (MAX_USER_DATA_BYTES - smsHeaderData.length - 1) + " bytes");
+            return null;
+        }
+
+        SubmitPdu ret = new SubmitPdu();
+        ByteArrayOutputStream bo = getSubmitPduHead(
+                scAddress, destinationAddress, (byte) 0x41, // MTI = SMS-SUBMIT,
+                                                            // TP-UDHI = true
+                statusReportRequested, ret);
+        // Skip encoding pdu if error occurs when create pdu head and the error will be handled
+        // properly later on encodedMessage sanity check.
+        if (bo == null) return ret;
+
+        // TP-Data-Coding-Scheme
+        // No class, 8 bit data
+        bo.write(0x04);
+
+        // (no TP-Validity-Period)
+
+        // Total size
+        bo.write(data.length + smsHeaderData.length + 1);
+
+        // User data header
+        bo.write(smsHeaderData.length);
+        bo.write(smsHeaderData, 0, smsHeaderData.length);
+
+        // User data
+        bo.write(data, 0, data.length);
+
+        ret.encodedMessage = bo.toByteArray();
+        return ret;
+    }
+
+    /**
+     * Create the beginning of a SUBMIT PDU.  This is the part of the
+     * SUBMIT PDU that is common to the two versions of {@link #getSubmitPdu},
+     * one of which takes a byte array and the other of which takes a
+     * <code>String</code>.
+     *
+     * @param scAddress Service Centre address. null == use default
+     * @param destinationAddress the address of the destination for the message
+     * @param mtiByte
+     * @param ret <code>SubmitPdu</code> containing the encoded SC
+     *        address, if applicable, and the encoded message. Returns null on encode error.
+     */
+    private static ByteArrayOutputStream getSubmitPduHead(
+            String scAddress, String destinationAddress, byte mtiByte,
+            boolean statusReportRequested, SubmitPdu ret) {
+        ByteArrayOutputStream bo = new ByteArrayOutputStream(
+                MAX_USER_DATA_BYTES + 40);
+
+        // SMSC address with length octet, or 0
+        if (scAddress == null) {
+            ret.encodedScAddress = null;
+        } else {
+            ret.encodedScAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(
+                    scAddress);
+        }
+
+        // TP-Message-Type-Indicator (and friends)
+        if (statusReportRequested) {
+            // Set TP-Status-Report-Request bit.
+            mtiByte |= 0x20;
+            if (VDBG) Rlog.d(LOG_TAG, "SMS status report requested");
+        }
+        bo.write(mtiByte);
+
+        // space for TP-Message-Reference
+        bo.write(0);
+
+        byte[] daBytes;
+
+        daBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(destinationAddress);
+
+        // return empty pduHead for invalid destination address
+        if (daBytes == null) return null;
+
+        // destination address length in BCD digits, ignoring TON byte and pad
+        // TODO Should be better.
+        bo.write((daBytes.length - 1) * 2
+                - ((daBytes[daBytes.length - 1] & 0xf0) == 0xf0 ? 1 : 0));
+
+        // destination address
+        bo.write(daBytes, 0, daBytes.length);
+
+        // TP-Protocol-Identifier
+        bo.write(0);
+        return bo;
+    }
+
+    private static class PduParser {
+        byte mPdu[];
+        int mCur;
+        SmsHeader mUserDataHeader;
+        byte[] mUserData;
+        int mUserDataSeptetPadding;
+
+        PduParser(byte[] pdu) {
+            mPdu = pdu;
+            mCur = 0;
+            mUserDataSeptetPadding = 0;
+        }
+
+        /**
+         * Parse and return the SC address prepended to SMS messages coming via
+         * the TS 27.005 / AT interface.  Returns null on invalid address
+         */
+        String getSCAddress() {
+            int len;
+            String ret;
+
+            // length of SC Address
+            len = getByte();
+
+            if (len == 0) {
+                // no SC address
+                ret = null;
+            } else {
+                // SC address
+                try {
+                    ret = PhoneNumberUtils
+                            .calledPartyBCDToString(mPdu, mCur, len);
+                } catch (RuntimeException tr) {
+                    Rlog.d(LOG_TAG, "invalid SC address: ", tr);
+                    ret = null;
+                }
+            }
+
+            mCur += len;
+
+            return ret;
+        }
+
+        /**
+         * returns non-sign-extended byte value
+         */
+        int getByte() {
+            return mPdu[mCur++] & 0xff;
+        }
+
+        /**
+         * Any address except the SC address (eg, originating address) See TS
+         * 23.040 9.1.2.5
+         */
+        GsmSmsAddress getAddress() {
+            GsmSmsAddress ret;
+
+            // "The Address-Length field is an integer representation of
+            // the number field, i.e. excludes any semi-octet containing only
+            // fill bits."
+            // The TOA field is not included as part of this
+            int addressLength = mPdu[mCur] & 0xff;
+            int lengthBytes = 2 + (addressLength + 1) / 2;
+
+            try {
+                ret = new GsmSmsAddress(mPdu, mCur, lengthBytes);
+            } catch (ParseException e) {
+                ret = null;
+                //This is caught by createFromPdu(byte[] pdu)
+                throw new RuntimeException(e.getMessage());
+            }
+
+            mCur += lengthBytes;
+
+            return ret;
+        }
+
+        /**
+         * Parses an SC timestamp and returns a currentTimeMillis()-style
+         * timestamp
+         */
+
+        long getSCTimestampMillis() {
+            // TP-Service-Centre-Time-Stamp
+            int year = IccUtils.gsmBcdByteToInt(mPdu[mCur++]);
+            int month = IccUtils.gsmBcdByteToInt(mPdu[mCur++]);
+            int day = IccUtils.gsmBcdByteToInt(mPdu[mCur++]);
+            int hour = IccUtils.gsmBcdByteToInt(mPdu[mCur++]);
+            int minute = IccUtils.gsmBcdByteToInt(mPdu[mCur++]);
+            int second = IccUtils.gsmBcdByteToInt(mPdu[mCur++]);
+
+            // For the timezone, the most significant bit of the
+            // least significant nibble is the sign byte
+            // (meaning the max range of this field is 79 quarter-hours,
+            // which is more than enough)
+
+            byte tzByte = mPdu[mCur++];
+
+            // Mask out sign bit.
+            int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08)));
+
+            timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset;
+
+            Time time = new Time(Time.TIMEZONE_UTC);
+
+            // It's 2006.  Should I really support years < 2000?
+            time.year = year >= 90 ? year + 1900 : year + 2000;
+            time.month = month - 1;
+            time.monthDay = day;
+            time.hour = hour;
+            time.minute = minute;
+            time.second = second;
+
+            // Timezone offset is in quarter hours.
+            return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000);
+        }
+
+        /**
+         * Pulls the user data out of the PDU, and separates the payload from
+         * the header if there is one.
+         *
+         * @param hasUserDataHeader true if there is a user data header
+         * @param dataInSeptets true if the data payload is in septets instead
+         *  of octets
+         * @return the number of septets or octets in the user data payload
+         */
+        int constructUserData(boolean hasUserDataHeader, boolean dataInSeptets) {
+            int offset = mCur;
+            int userDataLength = mPdu[offset++] & 0xff;
+            int headerSeptets = 0;
+            int userDataHeaderLength = 0;
+
+            if (hasUserDataHeader) {
+                userDataHeaderLength = mPdu[offset++] & 0xff;
+
+                byte[] udh = new byte[userDataHeaderLength];
+                System.arraycopy(mPdu, offset, udh, 0, userDataHeaderLength);
+                mUserDataHeader = SmsHeader.fromByteArray(udh);
+                offset += userDataHeaderLength;
+
+                int headerBits = (userDataHeaderLength + 1) * 8;
+                headerSeptets = headerBits / 7;
+                headerSeptets += (headerBits % 7) > 0 ? 1 : 0;
+                mUserDataSeptetPadding = (headerSeptets * 7) - headerBits;
+            }
+
+            int bufferLen;
+            if (dataInSeptets) {
+                /*
+                 * Here we just create the user data length to be the remainder of
+                 * the pdu minus the user data header, since userDataLength means
+                 * the number of uncompressed septets.
+                 */
+                bufferLen = mPdu.length - offset;
+            } else {
+                /*
+                 * userDataLength is the count of octets, so just subtract the
+                 * user data header.
+                 */
+                bufferLen = userDataLength - (hasUserDataHeader ? (userDataHeaderLength + 1) : 0);
+                if (bufferLen < 0) {
+                    bufferLen = 0;
+                }
+            }
+
+            mUserData = new byte[bufferLen];
+            System.arraycopy(mPdu, offset, mUserData, 0, mUserData.length);
+            mCur = offset;
+
+            if (dataInSeptets) {
+                // Return the number of septets
+                int count = userDataLength - headerSeptets;
+                // If count < 0, return 0 (means UDL was probably incorrect)
+                return count < 0 ? 0 : count;
+            } else {
+                // Return the number of octets
+                return mUserData.length;
+            }
+        }
+
+        /**
+         * Returns the user data payload, not including the headers
+         *
+         * @return the user data payload, not including the headers
+         */
+        byte[] getUserData() {
+            return mUserData;
+        }
+
+        /**
+         * Returns an object representing the user data headers
+         *
+         * {@hide}
+         */
+        SmsHeader getUserDataHeader() {
+            return mUserDataHeader;
+        }
+
+        /**
+         * Interprets the user data payload as packed GSM 7bit characters, and
+         * decodes them into a String.
+         *
+         * @param septetCount the number of septets in the user data payload
+         * @return a String with the decoded characters
+         */
+        String getUserDataGSM7Bit(int septetCount, int languageTable,
+                int languageShiftTable) {
+            String ret;
+
+            ret = GsmAlphabet.gsm7BitPackedToString(mPdu, mCur, septetCount,
+                    mUserDataSeptetPadding, languageTable, languageShiftTable);
+
+            mCur += (septetCount * 7) / 8;
+
+            return ret;
+        }
+
+        /**
+         * Interprets the user data payload as pack GSM 8-bit (a GSM alphabet string that's
+         * stored in 8-bit unpacked format) characters, and decodes them into a String.
+         *
+         * @param byteCount the number of byest in the user data payload
+         * @return a String with the decoded characters
+         */
+        String getUserDataGSM8bit(int byteCount) {
+            String ret;
+
+            ret = GsmAlphabet.gsm8BitUnpackedToString(mPdu, mCur, byteCount);
+
+            mCur += byteCount;
+
+            return ret;
+        }
+
+        /**
+         * Interprets the user data payload as UCS2 characters, and
+         * decodes them into a String.
+         *
+         * @param byteCount the number of bytes in the user data payload
+         * @return a String with the decoded characters
+         */
+        String getUserDataUCS2(int byteCount) {
+            String ret;
+
+            try {
+                ret = new String(mPdu, mCur, byteCount, "utf-16");
+            } catch (UnsupportedEncodingException ex) {
+                ret = "";
+                Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", ex);
+            }
+
+            mCur += byteCount;
+            return ret;
+        }
+
+        /**
+         * Interprets the user data payload as KSC-5601 characters, and
+         * decodes them into a String.
+         *
+         * @param byteCount the number of bytes in the user data payload
+         * @return a String with the decoded characters
+         */
+        String getUserDataKSC5601(int byteCount) {
+            String ret;
+
+            try {
+                ret = new String(mPdu, mCur, byteCount, "KSC5601");
+            } catch (UnsupportedEncodingException ex) {
+                ret = "";
+                Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", ex);
+            }
+
+            mCur += byteCount;
+            return ret;
+        }
+
+        boolean moreDataPresent() {
+            return (mPdu.length > mCur);
+        }
+    }
+
+    /**
+     * Calculates the number of SMS's required to encode the message body and
+     * the number of characters remaining until the next message.
+     *
+     * @param msgBody the message to encode
+     * @param use7bitOnly ignore (but still count) illegal characters if true
+     * @return TextEncodingDetails
+     */
+    public static TextEncodingDetails calculateLength(CharSequence msgBody,
+            boolean use7bitOnly) {
+        CharSequence newMsgBody = null;
+        Resources r = Resources.getSystem();
+        if (r.getBoolean(com.android.internal.R.bool.config_sms_force_7bit_encoding)) {
+            newMsgBody  = Sms7BitEncodingTranslator.translate(msgBody);
+        }
+        if (TextUtils.isEmpty(newMsgBody)) {
+            newMsgBody = msgBody;
+        }
+        TextEncodingDetails ted = GsmAlphabet.countGsmSeptets(newMsgBody, use7bitOnly);
+        if (ted == null) {
+            return SmsMessageBase.calcUnicodeEncodingDetails(newMsgBody);
+        }
+        return ted;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getProtocolIdentifier() {
+        return mProtocolIdentifier;
+    }
+
+    /**
+     * Returns the TP-Data-Coding-Scheme byte, for acknowledgement of SMS-PP download messages.
+     * @return the TP-DCS field of the SMS header
+     */
+    int getDataCodingScheme() {
+        return mDataCodingScheme;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isReplace() {
+        return (mProtocolIdentifier & 0xc0) == 0x40
+                && (mProtocolIdentifier & 0x3f) > 0
+                && (mProtocolIdentifier & 0x3f) < 8;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isCphsMwiMessage() {
+        return ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageClear()
+                || ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageSet();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isMWIClearMessage() {
+        if (mIsMwi && !mMwiSense) {
+            return true;
+        }
+
+        return mOriginatingAddress != null
+                && ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageClear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isMWISetMessage() {
+        if (mIsMwi && mMwiSense) {
+            return true;
+        }
+
+        return mOriginatingAddress != null
+                && ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageSet();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isMwiDontStore() {
+        if (mIsMwi && mMwiDontStore) {
+            return true;
+        }
+
+        if (isCphsMwiMessage()) {
+            // See CPHS 4.2 Section B.4.2.1
+            // If the user data is a single space char, do not store
+            // the message. Otherwise, store and display as usual
+            if (" ".equals(getMessageBody())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isStatusReportMessage() {
+        return mIsStatusReportMessage;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isReplyPathPresent() {
+        return mReplyPathPresent;
+    }
+
+    /**
+     * TS 27.005 3.1, &lt;pdu&gt; definition "In the case of SMS: 3GPP TS 24.011 [6]
+     * SC address followed by 3GPP TS 23.040 [3] TPDU in hexadecimal format:
+     * ME/TA converts each octet of TP data unit into two IRA character long
+     * hex number (e.g. octet with integer value 42 is presented to TE as two
+     * characters 2A (IRA 50 and 65))" ...in the case of cell broadcast,
+     * something else...
+     */
+    private void parsePdu(byte[] pdu) {
+        mPdu = pdu;
+        // Rlog.d(LOG_TAG, "raw sms message:");
+        // Rlog.d(LOG_TAG, s);
+
+        PduParser p = new PduParser(pdu);
+
+        mScAddress = p.getSCAddress();
+
+        if (mScAddress != null) {
+            if (VDBG) Rlog.d(LOG_TAG, "SMS SC address: " + mScAddress);
+        }
+
+        // TODO(mkf) support reply path, user data header indicator
+
+        // TP-Message-Type-Indicator
+        // 9.2.3
+        int firstByte = p.getByte();
+
+        mMti = firstByte & 0x3;
+        switch (mMti) {
+        // TP-Message-Type-Indicator
+        // 9.2.3
+        case 0:
+        case 3: //GSM 03.40 9.2.3.1: MTI == 3 is Reserved.
+                //This should be processed in the same way as MTI == 0 (Deliver)
+            parseSmsDeliver(p, firstByte);
+            break;
+        case 1:
+            parseSmsSubmit(p, firstByte);
+            break;
+        case 2:
+            parseSmsStatusReport(p, firstByte);
+            break;
+        default:
+            // TODO(mkf) the rest of these
+            throw new RuntimeException("Unsupported message type");
+        }
+    }
+
+    /**
+     * Parses a SMS-STATUS-REPORT message.
+     *
+     * @param p A PduParser, cued past the first byte.
+     * @param firstByte The first byte of the PDU, which contains MTI, etc.
+     */
+    private void parseSmsStatusReport(PduParser p, int firstByte) {
+        mIsStatusReportMessage = true;
+
+        // TP-Message-Reference
+        mMessageRef = p.getByte();
+        // TP-Recipient-Address
+        mRecipientAddress = p.getAddress();
+        // TP-Service-Centre-Time-Stamp
+        mScTimeMillis = p.getSCTimestampMillis();
+        p.getSCTimestampMillis();
+        // TP-Status
+        mStatus = p.getByte();
+
+        // The following are optional fields that may or may not be present.
+        if (p.moreDataPresent()) {
+            // TP-Parameter-Indicator
+            int extraParams = p.getByte();
+            int moreExtraParams = extraParams;
+            while ((moreExtraParams & 0x80) != 0) {
+                // We only know how to parse a few extra parameters, all
+                // indicated in the first TP-PI octet, so skip over any
+                // additional TP-PI octets.
+                moreExtraParams = p.getByte();
+            }
+            // As per 3GPP 23.040 section 9.2.3.27 TP-Parameter-Indicator,
+            // only process the byte if the reserved bits (bits3 to 6) are zero.
+            if ((extraParams & 0x78) == 0) {
+                // TP-Protocol-Identifier
+                if ((extraParams & 0x01) != 0) {
+                    mProtocolIdentifier = p.getByte();
+                }
+                // TP-Data-Coding-Scheme
+                if ((extraParams & 0x02) != 0) {
+                    mDataCodingScheme = p.getByte();
+                }
+                // TP-User-Data-Length (implies existence of TP-User-Data)
+                if ((extraParams & 0x04) != 0) {
+                    boolean hasUserDataHeader = (firstByte & 0x40) == 0x40;
+                    parseUserData(p, hasUserDataHeader);
+                }
+            }
+        }
+    }
+
+    private void parseSmsDeliver(PduParser p, int firstByte) {
+        mReplyPathPresent = (firstByte & 0x80) == 0x80;
+
+        mOriginatingAddress = p.getAddress();
+
+        if (mOriginatingAddress != null) {
+            if (VDBG) Rlog.v(LOG_TAG, "SMS originating address: "
+                    + mOriginatingAddress.address);
+        }
+
+        // TP-Protocol-Identifier (TP-PID)
+        // TS 23.040 9.2.3.9
+        mProtocolIdentifier = p.getByte();
+
+        // TP-Data-Coding-Scheme
+        // see TS 23.038
+        mDataCodingScheme = p.getByte();
+
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "SMS TP-PID:" + mProtocolIdentifier
+                    + " data coding scheme: " + mDataCodingScheme);
+        }
+
+        mScTimeMillis = p.getSCTimestampMillis();
+
+        if (VDBG) Rlog.d(LOG_TAG, "SMS SC timestamp: " + mScTimeMillis);
+
+        boolean hasUserDataHeader = (firstByte & 0x40) == 0x40;
+
+        parseUserData(p, hasUserDataHeader);
+    }
+
+    /**
+     * Parses a SMS-SUBMIT message.
+     *
+     * @param p A PduParser, cued past the first byte.
+     * @param firstByte The first byte of the PDU, which contains MTI, etc.
+     */
+    private void parseSmsSubmit(PduParser p, int firstByte) {
+        mReplyPathPresent = (firstByte & 0x80) == 0x80;
+
+        // TP-MR (TP-Message Reference)
+        mMessageRef = p.getByte();
+
+        mRecipientAddress = p.getAddress();
+
+        if (mRecipientAddress != null) {
+            if (VDBG) Rlog.v(LOG_TAG, "SMS recipient address: " + mRecipientAddress.address);
+        }
+
+        // TP-Protocol-Identifier (TP-PID)
+        // TS 23.040 9.2.3.9
+        mProtocolIdentifier = p.getByte();
+
+        // TP-Data-Coding-Scheme
+        // see TS 23.038
+        mDataCodingScheme = p.getByte();
+
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "SMS TP-PID:" + mProtocolIdentifier
+                    + " data coding scheme: " + mDataCodingScheme);
+        }
+
+        // TP-Validity-Period-Format
+        int validityPeriodLength = 0;
+        int validityPeriodFormat = ((firstByte>>3) & 0x3);
+        if (0x0 == validityPeriodFormat) /* 00, TP-VP field not present*/
+        {
+            validityPeriodLength = 0;
+        }
+        else if (0x2 == validityPeriodFormat) /* 10, TP-VP: relative format*/
+        {
+            validityPeriodLength = 1;
+        }
+        else /* other case, 11 or 01, TP-VP: absolute or enhanced format*/
+        {
+            validityPeriodLength = 7;
+        }
+
+        // TP-Validity-Period is not used on phone, so just ignore it for now.
+        while (validityPeriodLength-- > 0)
+        {
+            p.getByte();
+        }
+
+        boolean hasUserDataHeader = (firstByte & 0x40) == 0x40;
+
+        parseUserData(p, hasUserDataHeader);
+    }
+
+    /**
+     * Parses the User Data of an SMS.
+     *
+     * @param p The current PduParser.
+     * @param hasUserDataHeader Indicates whether a header is present in the
+     *                          User Data.
+     */
+    private void parseUserData(PduParser p, boolean hasUserDataHeader) {
+        boolean hasMessageClass = false;
+        boolean userDataCompressed = false;
+
+        int encodingType = ENCODING_UNKNOWN;
+
+        // Look up the data encoding scheme
+        if ((mDataCodingScheme & 0x80) == 0) {
+            userDataCompressed = (0 != (mDataCodingScheme & 0x20));
+            hasMessageClass = (0 != (mDataCodingScheme & 0x10));
+
+            if (userDataCompressed) {
+                Rlog.w(LOG_TAG, "4 - Unsupported SMS data coding scheme "
+                        + "(compression) " + (mDataCodingScheme & 0xff));
+            } else {
+                switch ((mDataCodingScheme >> 2) & 0x3) {
+                case 0: // GSM 7 bit default alphabet
+                    encodingType = ENCODING_7BIT;
+                    break;
+
+                case 2: // UCS 2 (16bit)
+                    encodingType = ENCODING_16BIT;
+                    break;
+
+                case 1: // 8 bit data
+                    //Support decoding the user data payload as pack GSM 8-bit (a GSM alphabet string
+                    //that's stored in 8-bit unpacked format) characters.
+                    Resources r = Resources.getSystem();
+                    if (r.getBoolean(com.android.internal.
+                            R.bool.config_sms_decode_gsm_8bit_data)) {
+                        encodingType = ENCODING_8BIT;
+                        break;
+                    }
+
+                case 3: // reserved
+                    Rlog.w(LOG_TAG, "1 - Unsupported SMS data coding scheme "
+                            + (mDataCodingScheme & 0xff));
+                    encodingType = ENCODING_8BIT;
+                    break;
+                }
+            }
+        } else if ((mDataCodingScheme & 0xf0) == 0xf0) {
+            hasMessageClass = true;
+            userDataCompressed = false;
+
+            if (0 == (mDataCodingScheme & 0x04)) {
+                // GSM 7 bit default alphabet
+                encodingType = ENCODING_7BIT;
+            } else {
+                // 8 bit data
+                encodingType = ENCODING_8BIT;
+            }
+        } else if ((mDataCodingScheme & 0xF0) == 0xC0
+                || (mDataCodingScheme & 0xF0) == 0xD0
+                || (mDataCodingScheme & 0xF0) == 0xE0) {
+            // 3GPP TS 23.038 V7.0.0 (2006-03) section 4
+
+            // 0xC0 == 7 bit, don't store
+            // 0xD0 == 7 bit, store
+            // 0xE0 == UCS-2, store
+
+            if ((mDataCodingScheme & 0xF0) == 0xE0) {
+                encodingType = ENCODING_16BIT;
+            } else {
+                encodingType = ENCODING_7BIT;
+            }
+
+            userDataCompressed = false;
+            boolean active = ((mDataCodingScheme & 0x08) == 0x08);
+            // bit 0x04 reserved
+
+            // VM - If TP-UDH is present, these values will be overwritten
+            if ((mDataCodingScheme & 0x03) == 0x00) {
+                mIsMwi = true; /* Indicates vmail */
+                mMwiSense = active;/* Indicates vmail notification set/clear */
+                mMwiDontStore = ((mDataCodingScheme & 0xF0) == 0xC0);
+
+                /* Set voice mail count based on notification bit */
+                if (active == true) {
+                    mVoiceMailCount = -1; // unknown number of messages waiting
+                } else {
+                    mVoiceMailCount = 0; // no unread messages
+                }
+
+                Rlog.w(LOG_TAG, "MWI in DCS for Vmail. DCS = "
+                        + (mDataCodingScheme & 0xff) + " Dont store = "
+                        + mMwiDontStore + " vmail count = " + mVoiceMailCount);
+
+            } else {
+                mIsMwi = false;
+                Rlog.w(LOG_TAG, "MWI in DCS for fax/email/other: "
+                        + (mDataCodingScheme & 0xff));
+            }
+        } else if ((mDataCodingScheme & 0xC0) == 0x80) {
+            // 3GPP TS 23.038 V7.0.0 (2006-03) section 4
+            // 0x80..0xBF == Reserved coding groups
+            if (mDataCodingScheme == 0x84) {
+                // This value used for KSC5601 by carriers in Korea.
+                encodingType = ENCODING_KSC5601;
+            } else {
+                Rlog.w(LOG_TAG, "5 - Unsupported SMS data coding scheme "
+                        + (mDataCodingScheme & 0xff));
+            }
+        } else {
+            Rlog.w(LOG_TAG, "3 - Unsupported SMS data coding scheme "
+                    + (mDataCodingScheme & 0xff));
+        }
+
+        // set both the user data and the user data header.
+        int count = p.constructUserData(hasUserDataHeader,
+                encodingType == ENCODING_7BIT);
+        this.mUserData = p.getUserData();
+        this.mUserDataHeader = p.getUserDataHeader();
+
+        /*
+         * Look for voice mail indication in TP_UDH TS23.040 9.2.3.24
+         * ieid = 1 (0x1) (SPECIAL_SMS_MSG_IND)
+         * ieidl =2 octets
+         * ieda msg_ind_type = 0x00 (voice mail; discard sms )or
+         *                   = 0x80 (voice mail; store sms)
+         * msg_count = 0x00 ..0xFF
+         */
+        if (hasUserDataHeader && (mUserDataHeader.specialSmsMsgList.size() != 0)) {
+            for (SmsHeader.SpecialSmsMsg msg : mUserDataHeader.specialSmsMsgList) {
+                int msgInd = msg.msgIndType & 0xff;
+                /*
+                 * TS 23.040 V6.8.1 Sec 9.2.3.24.2
+                 * bits 1 0 : basic message indication type
+                 * bits 4 3 2 : extended message indication type
+                 * bits 6 5 : Profile id bit 7 storage type
+                 */
+                if ((msgInd == 0) || (msgInd == 0x80)) {
+                    mIsMwi = true;
+                    if (msgInd == 0x80) {
+                        /* Store message because TP_UDH indicates so*/
+                        mMwiDontStore = false;
+                    } else if (mMwiDontStore == false) {
+                        /* Storage bit is not set by TP_UDH
+                         * Check for conflict
+                         * between message storage bit in TP_UDH
+                         * & DCS. The message shall be stored if either of
+                         * the one indicates so.
+                         * TS 23.040 V6.8.1 Sec 9.2.3.24.2
+                         */
+                        if (!((((mDataCodingScheme & 0xF0) == 0xD0)
+                               || ((mDataCodingScheme & 0xF0) == 0xE0))
+                               && ((mDataCodingScheme & 0x03) == 0x00))) {
+                            /* Even DCS did not have voice mail with Storage bit
+                             * 3GPP TS 23.038 V7.0.0 section 4
+                             * So clear this flag*/
+                            mMwiDontStore = true;
+                        }
+                    }
+
+                    mVoiceMailCount = msg.msgCount & 0xff;
+
+                    /*
+                     * In the event of a conflict between message count setting
+                     * and DCS then the Message Count in the TP-UDH shall
+                     * override the indication in the TP-DCS. Set voice mail
+                     * notification based on count in TP-UDH
+                     */
+                    if (mVoiceMailCount > 0)
+                        mMwiSense = true;
+                    else
+                        mMwiSense = false;
+
+                    Rlog.w(LOG_TAG, "MWI in TP-UDH for Vmail. Msg Ind = " + msgInd
+                            + " Dont store = " + mMwiDontStore + " Vmail count = "
+                            + mVoiceMailCount);
+
+                    /*
+                     * There can be only one IE for each type of message
+                     * indication in TP_UDH. In the event they are duplicated
+                     * last occurence will be used. Hence the for loop
+                     */
+                } else {
+                    Rlog.w(LOG_TAG, "TP_UDH fax/email/"
+                            + "extended msg/multisubscriber profile. Msg Ind = " + msgInd);
+                }
+            } // end of for
+        } // end of if UDH
+
+        switch (encodingType) {
+        case ENCODING_UNKNOWN:
+            mMessageBody = null;
+            break;
+
+        case ENCODING_8BIT:
+            //Support decoding the user data payload as pack GSM 8-bit (a GSM alphabet string
+            //that's stored in 8-bit unpacked format) characters.
+            Resources r = Resources.getSystem();
+            if (r.getBoolean(com.android.internal.
+                    R.bool.config_sms_decode_gsm_8bit_data)) {
+                mMessageBody = p.getUserDataGSM8bit(count);
+            } else {
+                mMessageBody = null;
+            }
+            break;
+
+        case ENCODING_7BIT:
+            mMessageBody = p.getUserDataGSM7Bit(count,
+                    hasUserDataHeader ? mUserDataHeader.languageTable : 0,
+                    hasUserDataHeader ? mUserDataHeader.languageShiftTable : 0);
+            break;
+
+        case ENCODING_16BIT:
+            mMessageBody = p.getUserDataUCS2(count);
+            break;
+
+        case ENCODING_KSC5601:
+            mMessageBody = p.getUserDataKSC5601(count);
+            break;
+        }
+
+        if (VDBG) Rlog.v(LOG_TAG, "SMS message body (raw): '" + mMessageBody + "'");
+
+        if (mMessageBody != null) {
+            parseMessageBody();
+        }
+
+        if (!hasMessageClass) {
+            messageClass = MessageClass.UNKNOWN;
+        } else {
+            switch (mDataCodingScheme & 0x3) {
+            case 0:
+                messageClass = MessageClass.CLASS_0;
+                break;
+            case 1:
+                messageClass = MessageClass.CLASS_1;
+                break;
+            case 2:
+                messageClass = MessageClass.CLASS_2;
+                break;
+            case 3:
+                messageClass = MessageClass.CLASS_3;
+                break;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public MessageClass getMessageClass() {
+        return messageClass;
+    }
+
+    /**
+     * Returns true if this is a (U)SIM data download type SM.
+     * See 3GPP TS 31.111 section 9.1 and TS 23.040 section 9.2.3.9.
+     *
+     * @return true if this is a USIM data download message; false otherwise
+     */
+    boolean isUsimDataDownload() {
+        return messageClass == MessageClass.CLASS_2 &&
+                (mProtocolIdentifier == 0x7f || mProtocolIdentifier == 0x7c);
+    }
+
+    public int getNumOfVoicemails() {
+        /*
+         * Order of priority if multiple indications are present is 1.UDH,
+         *      2.DCS, 3.CPHS.
+         * Voice mail count if voice mail present indication is
+         * received
+         *  1. UDH (or both UDH & DCS): mVoiceMailCount = 0 to 0xff. Ref[TS 23. 040]
+         *  2. DCS only: count is unknown mVoiceMailCount= -1
+         *  3. CPHS only: count is unknown mVoiceMailCount = 0xff. Ref[GSM-BTR-1-4700]
+         * Voice mail clear, mVoiceMailCount = 0.
+         */
+        if ((!mIsMwi) && isCphsMwiMessage()) {
+            if (mOriginatingAddress != null
+                    && ((GsmSmsAddress) mOriginatingAddress).isCphsVoiceMessageSet()) {
+                mVoiceMailCount = 0xff;
+            } else {
+                mVoiceMailCount = 0;
+            }
+            Rlog.v(LOG_TAG, "CPHS voice mail message");
+        }
+        return mVoiceMailCount;
+    }
+}
diff --git a/com/android/internal/telephony/gsm/SsData.java b/com/android/internal/telephony/gsm/SsData.java
new file mode 100644
index 0000000..a5f67d8
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SsData.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2012-2013, The Linux Foundation. All rights reserved.
+ * Not a Contribution.
+ *
+ * Copyright (C) 2006 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.internal.telephony.gsm;
+
+import android.telephony.Rlog;
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.GsmCdmaPhone;
+
+import java.util.ArrayList;
+
+/**
+ * See also RIL_StkCcUnsolSsResponse in include/telephony/ril.h
+ *
+ * {@hide}
+ */
+public class SsData {
+    public enum ServiceType {
+        SS_CFU,
+        SS_CF_BUSY,
+        SS_CF_NO_REPLY,
+        SS_CF_NOT_REACHABLE,
+        SS_CF_ALL,
+        SS_CF_ALL_CONDITIONAL,
+        SS_CLIP,
+        SS_CLIR,
+        SS_COLP,
+        SS_COLR,
+        SS_WAIT,
+        SS_BAOC,
+        SS_BAOIC,
+        SS_BAOIC_EXC_HOME,
+        SS_BAIC,
+        SS_BAIC_ROAMING,
+        SS_ALL_BARRING,
+        SS_OUTGOING_BARRING,
+        SS_INCOMING_BARRING;
+
+        public boolean isTypeCF() {
+            return (this == SS_CFU || this == SS_CF_BUSY || this == SS_CF_NO_REPLY ||
+                  this == SS_CF_NOT_REACHABLE || this == SS_CF_ALL ||
+                  this == SS_CF_ALL_CONDITIONAL);
+        }
+
+        public boolean isTypeUnConditional() {
+            return (this == SS_CFU || this == SS_CF_ALL);
+        }
+
+        public boolean isTypeCW() {
+            return (this == SS_WAIT);
+        }
+
+        public boolean isTypeClip() {
+            return (this == SS_CLIP);
+        }
+
+        public boolean isTypeClir() {
+            return (this == SS_CLIR);
+        }
+
+        public boolean isTypeBarring() {
+            return (this == SS_BAOC || this == SS_BAOIC || this == SS_BAOIC_EXC_HOME ||
+                  this == SS_BAIC || this == SS_BAIC_ROAMING || this == SS_ALL_BARRING ||
+                  this == SS_OUTGOING_BARRING || this == SS_INCOMING_BARRING);
+        }
+    };
+
+    public enum RequestType {
+        SS_ACTIVATION,
+        SS_DEACTIVATION,
+        SS_INTERROGATION,
+        SS_REGISTRATION,
+        SS_ERASURE;
+
+        public boolean isTypeInterrogation() {
+            return (this == SS_INTERROGATION);
+        }
+    };
+
+    public enum TeleserviceType {
+        SS_ALL_TELE_AND_BEARER_SERVICES,
+        SS_ALL_TELESEVICES,
+        SS_TELEPHONY,
+        SS_ALL_DATA_TELESERVICES,
+        SS_SMS_SERVICES,
+        SS_ALL_TELESERVICES_EXCEPT_SMS;
+    };
+
+    public ServiceType serviceType;
+    public RequestType requestType;
+    public TeleserviceType teleserviceType;
+    public int serviceClass;
+    public int result;
+
+    public int[] ssInfo; /* This is the response data for most of the SS GET/SET
+                            RIL requests. E.g. RIL_REQUSET_GET_CLIR returns
+                            two ints, so first two values of ssInfo[] will be
+                            used for respone if serviceType is SS_CLIR and
+                            requestType is SS_INTERROGATION */
+
+    public CallForwardInfo[] cfInfo; /* This is the response data for SS request
+                                        to query call forward status. see
+                                        RIL_REQUEST_QUERY_CALL_FORWARD_STATUS */
+
+    public ServiceType ServiceTypeFromRILInt(int type) {
+        try {
+            return ServiceType.values()[type];
+        } catch (IndexOutOfBoundsException e) {
+            Rlog.e(GsmCdmaPhone.LOG_TAG, "Invalid Service type");
+            return null;
+        }
+    }
+
+    public RequestType RequestTypeFromRILInt(int type) {
+        try {
+            return RequestType.values()[type];
+        } catch (IndexOutOfBoundsException e) {
+            Rlog.e(GsmCdmaPhone.LOG_TAG, "Invalid Request type");
+            return null;
+        }
+    }
+
+    public TeleserviceType TeleserviceTypeFromRILInt(int type) {
+        try {
+            return TeleserviceType.values()[type];
+        } catch (IndexOutOfBoundsException e) {
+            Rlog.e(GsmCdmaPhone.LOG_TAG, "Invalid Teleservice type");
+            return null;
+        }
+    }
+
+    public String toString() {
+        return "[SsData] " + "ServiceType: " + serviceType
+            + " RequestType: " + requestType
+            + " TeleserviceType: " + teleserviceType
+            + " ServiceClass: " + serviceClass
+            + " Result: " + result
+            + " Is Service Type CF: " + serviceType.isTypeCF();
+    }
+}
diff --git a/com/android/internal/telephony/gsm/SuppServiceNotification.java b/com/android/internal/telephony/gsm/SuppServiceNotification.java
new file mode 100644
index 0000000..8b64ade
--- /dev/null
+++ b/com/android/internal/telephony/gsm/SuppServiceNotification.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2007 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.internal.telephony.gsm;
+
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * Represents a Supplementary Service Notification received from the network.
+ *
+ * {@hide}
+ */
+public class SuppServiceNotification {
+    /** Type of notification: 0 = MO; 1 = MT */
+    public int notificationType;
+    /** TS 27.007 7.17 "code1" or "code2" */
+    public int code;
+    /** TS 27.007 7.17 "index" */
+    public int index;
+    /** TS 27.007 7.17 "type" (MT only) */
+    public int type;
+    /** TS 27.007 7.17 "number" (MT only) */
+    public String number;
+
+    /** List of forwarded numbers, if any */
+    public String[] history;
+
+    static public final int MO_CODE_UNCONDITIONAL_CF_ACTIVE     = 0;
+    static public final int MO_CODE_SOME_CF_ACTIVE              = 1;
+    static public final int MO_CODE_CALL_FORWARDED              = 2;
+    static public final int MO_CODE_CALL_IS_WAITING             = 3;
+    static public final int MO_CODE_CUG_CALL                    = 4;
+    static public final int MO_CODE_OUTGOING_CALLS_BARRED       = 5;
+    static public final int MO_CODE_INCOMING_CALLS_BARRED       = 6;
+    static public final int MO_CODE_CLIR_SUPPRESSION_REJECTED   = 7;
+    static public final int MO_CODE_CALL_DEFLECTED              = 8;
+
+    static public final int MT_CODE_FORWARDED_CALL              = 0;
+    static public final int MT_CODE_CUG_CALL                    = 1;
+    static public final int MT_CODE_CALL_ON_HOLD                = 2;
+    static public final int MT_CODE_CALL_RETRIEVED              = 3;
+    static public final int MT_CODE_MULTI_PARTY_CALL            = 4;
+    static public final int MT_CODE_ON_HOLD_CALL_RELEASED       = 5;
+    static public final int MT_CODE_FORWARD_CHECK_RECEIVED      = 6;
+    static public final int MT_CODE_CALL_CONNECTING_ECT         = 7;
+    static public final int MT_CODE_CALL_CONNECTED_ECT          = 8;
+    static public final int MT_CODE_DEFLECTED_CALL              = 9;
+    static public final int MT_CODE_ADDITIONAL_CALL_FORWARDED   = 10;
+
+    @Override
+    public String toString()
+    {
+        return super.toString() + " mobile"
+            + (notificationType == 0 ? " originated " : " terminated ")
+            + " code: " + code
+            + " index: " + index
+            + " history: " + history
+            + " \""
+            + PhoneNumberUtils.stringFromStringAndTOA(number, type) + "\" ";
+    }
+
+}
diff --git a/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java b/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java
new file mode 100644
index 0000000..3536755
--- /dev/null
+++ b/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.gsm;
+
+import android.app.Activity;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.cat.ComprehensionTlvTag;
+import com.android.internal.telephony.uicc.IccIoResult;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UsimServiceTable;
+
+/**
+ * Handler for SMS-PP data download messages.
+ * See 3GPP TS 31.111 section 7.1.1
+ */
+public class UsimDataDownloadHandler extends Handler {
+    private static final String TAG = "UsimDataDownloadHandler";
+
+    /** BER-TLV tag for SMS-PP download. TS 31.111 section 9.1. */
+    private static final int BER_SMS_PP_DOWNLOAD_TAG      = 0xd1;
+
+    /** Device identity value for UICC (destination). */
+    private static final int DEV_ID_UICC        = 0x81;
+
+    /** Device identity value for network (source). */
+    private static final int DEV_ID_NETWORK     = 0x83;
+
+    /** Message containing new SMS-PP message to process. */
+    private static final int EVENT_START_DATA_DOWNLOAD = 1;
+
+    /** Response to SMS-PP download envelope command. */
+    private static final int EVENT_SEND_ENVELOPE_RESPONSE = 2;
+
+    /** Result of writing SM to UICC (when SMS-PP service is not available). */
+    private static final int EVENT_WRITE_SMS_COMPLETE = 3;
+
+    private final CommandsInterface mCi;
+
+    public UsimDataDownloadHandler(CommandsInterface commandsInterface) {
+        mCi = commandsInterface;
+    }
+
+    /**
+     * Handle SMS-PP data download messages. Normally these are automatically handled by the
+     * radio, but we may have to deal with this type of SM arriving via the IMS stack. If the
+     * data download service is not enabled, try to write to the USIM as an SMS, and send the
+     * UICC response as the acknowledgment to the SMSC.
+     *
+     * @param ust the UsimServiceTable, to check if data download is enabled
+     * @param smsMessage the SMS message to process
+     * @return {@code Activity.RESULT_OK} on success; {@code RESULT_SMS_GENERIC_ERROR} on failure
+     */
+    int handleUsimDataDownload(UsimServiceTable ust, SmsMessage smsMessage) {
+        // If we receive an SMS-PP message before the UsimServiceTable has been loaded,
+        // assume that the data download service is not present. This is very unlikely to
+        // happen because the IMS connection will not be established until after the ISIM
+        // records have been loaded, after the USIM service table has been loaded.
+        if (ust != null && ust.isAvailable(
+                UsimServiceTable.UsimService.DATA_DL_VIA_SMS_PP)) {
+            Rlog.d(TAG, "Received SMS-PP data download, sending to UICC.");
+            return startDataDownload(smsMessage);
+        } else {
+            Rlog.d(TAG, "DATA_DL_VIA_SMS_PP service not available, storing message to UICC.");
+            String smsc = IccUtils.bytesToHexString(
+                    PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(
+                            smsMessage.getServiceCenterAddress()));
+            mCi.writeSmsToSim(SmsManager.STATUS_ON_ICC_UNREAD, smsc,
+                    IccUtils.bytesToHexString(smsMessage.getPdu()),
+                    obtainMessage(EVENT_WRITE_SMS_COMPLETE));
+            return Activity.RESULT_OK;  // acknowledge after response from write to USIM
+        }
+
+    }
+
+    /**
+     * Start an SMS-PP data download for the specified message. Can be called from a different
+     * thread than this Handler is running on.
+     *
+     * @param smsMessage the message to process
+     * @return {@code Activity.RESULT_OK} on success; {@code RESULT_SMS_GENERIC_ERROR} on failure
+     */
+    public int startDataDownload(SmsMessage smsMessage) {
+        if (sendMessage(obtainMessage(EVENT_START_DATA_DOWNLOAD, smsMessage))) {
+            return Activity.RESULT_OK;  // we will send SMS ACK/ERROR based on UICC response
+        } else {
+            Rlog.e(TAG, "startDataDownload failed to send message to start data download.");
+            return Intents.RESULT_SMS_GENERIC_ERROR;
+        }
+    }
+
+    private void handleDataDownload(SmsMessage smsMessage) {
+        int dcs = smsMessage.getDataCodingScheme();
+        int pid = smsMessage.getProtocolIdentifier();
+        byte[] pdu = smsMessage.getPdu();           // includes SC address
+
+        int scAddressLength = pdu[0] & 0xff;
+        int tpduIndex = scAddressLength + 1;        // start of TPDU
+        int tpduLength = pdu.length - tpduIndex;
+
+        int bodyLength = getEnvelopeBodyLength(scAddressLength, tpduLength);
+
+        // Add 1 byte for SMS-PP download tag and 1-2 bytes for BER-TLV length.
+        // See ETSI TS 102 223 Annex C for encoding of length and tags.
+        int totalLength = bodyLength + 1 + (bodyLength > 127 ? 2 : 1);
+
+        byte[] envelope = new byte[totalLength];
+        int index = 0;
+
+        // SMS-PP download tag and length (assumed to be < 256 bytes).
+        envelope[index++] = (byte) BER_SMS_PP_DOWNLOAD_TAG;
+        if (bodyLength > 127) {
+            envelope[index++] = (byte) 0x81;    // length 128-255 encoded as 0x81 + length
+        }
+        envelope[index++] = (byte) bodyLength;
+
+        // Device identities TLV
+        envelope[index++] = (byte) (0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value());
+        envelope[index++] = (byte) 2;
+        envelope[index++] = (byte) DEV_ID_NETWORK;
+        envelope[index++] = (byte) DEV_ID_UICC;
+
+        // Address TLV (if present). Encoded length is assumed to be < 127 bytes.
+        if (scAddressLength != 0) {
+            envelope[index++] = (byte) ComprehensionTlvTag.ADDRESS.value();
+            envelope[index++] = (byte) scAddressLength;
+            System.arraycopy(pdu, 1, envelope, index, scAddressLength);
+            index += scAddressLength;
+        }
+
+        // SMS TPDU TLV. Length is assumed to be < 256 bytes.
+        envelope[index++] = (byte) (0x80 | ComprehensionTlvTag.SMS_TPDU.value());
+        if (tpduLength > 127) {
+            envelope[index++] = (byte) 0x81;    // length 128-255 encoded as 0x81 + length
+        }
+        envelope[index++] = (byte) tpduLength;
+        System.arraycopy(pdu, tpduIndex, envelope, index, tpduLength);
+        index += tpduLength;
+
+        // Verify that we calculated the payload size correctly.
+        if (index != envelope.length) {
+            Rlog.e(TAG, "startDataDownload() calculated incorrect envelope length, aborting.");
+            acknowledgeSmsWithError(CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR);
+            return;
+        }
+
+        String encodedEnvelope = IccUtils.bytesToHexString(envelope);
+        mCi.sendEnvelopeWithStatus(encodedEnvelope, obtainMessage(
+                EVENT_SEND_ENVELOPE_RESPONSE, new int[]{ dcs, pid }));
+    }
+
+    /**
+     * Return the size in bytes of the envelope to send to the UICC, excluding the
+     * SMS-PP download tag byte and length byte(s). If the size returned is <= 127,
+     * the BER-TLV length will be encoded in 1 byte, otherwise 2 bytes are required.
+     *
+     * @param scAddressLength the length of the SMSC address, or zero if not present
+     * @param tpduLength the length of the TPDU from the SMS-PP message
+     * @return the number of bytes to allocate for the envelope command
+     */
+    private static int getEnvelopeBodyLength(int scAddressLength, int tpduLength) {
+        // Add 4 bytes for device identities TLV + 1 byte for SMS TPDU tag byte
+        int length = tpduLength + 5;
+        // Add 1 byte for TPDU length, or 2 bytes if length > 127
+        length += (tpduLength > 127 ? 2 : 1);
+        // Add length of address tag, if present (+ 2 bytes for tag and length)
+        if (scAddressLength != 0) {
+            length = length + 2 + scAddressLength;
+        }
+        return length;
+    }
+
+    /**
+     * Handle the response to the ENVELOPE command.
+     * @param response UICC response encoded as hexadecimal digits. First two bytes are the
+     *  UICC SW1 and SW2 status bytes.
+     */
+    private void sendSmsAckForEnvelopeResponse(IccIoResult response, int dcs, int pid) {
+        int sw1 = response.sw1;
+        int sw2 = response.sw2;
+
+        boolean success;
+        if ((sw1 == 0x90 && sw2 == 0x00) || sw1 == 0x91) {
+            Rlog.d(TAG, "USIM data download succeeded: " + response.toString());
+            success = true;
+        } else if (sw1 == 0x93 && sw2 == 0x00) {
+            Rlog.e(TAG, "USIM data download failed: Toolkit busy");
+            acknowledgeSmsWithError(CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_APP_TOOLKIT_BUSY);
+            return;
+        } else if (sw1 == 0x62 || sw1 == 0x63) {
+            Rlog.e(TAG, "USIM data download failed: " + response.toString());
+            success = false;
+        } else {
+            Rlog.e(TAG, "Unexpected SW1/SW2 response from UICC: " + response.toString());
+            success = false;
+        }
+
+        byte[] responseBytes = response.payload;
+        if (responseBytes == null || responseBytes.length == 0) {
+            if (success) {
+                mCi.acknowledgeLastIncomingGsmSms(true, 0, null);
+            } else {
+                acknowledgeSmsWithError(
+                        CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR);
+            }
+            return;
+        }
+
+        byte[] smsAckPdu;
+        int index = 0;
+        if (success) {
+            smsAckPdu = new byte[responseBytes.length + 5];
+            smsAckPdu[index++] = 0x00;      // TP-MTI, TP-UDHI
+            smsAckPdu[index++] = 0x07;      // TP-PI: TP-PID, TP-DCS, TP-UDL present
+        } else {
+            smsAckPdu = new byte[responseBytes.length + 6];
+            smsAckPdu[index++] = 0x00;      // TP-MTI, TP-UDHI
+            smsAckPdu[index++] = (byte)
+                    CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR;  // TP-FCS
+            smsAckPdu[index++] = 0x07;      // TP-PI: TP-PID, TP-DCS, TP-UDL present
+        }
+
+        smsAckPdu[index++] = (byte) pid;
+        smsAckPdu[index++] = (byte) dcs;
+
+        if (is7bitDcs(dcs)) {
+            int septetCount = responseBytes.length * 8 / 7;
+            smsAckPdu[index++] = (byte) septetCount;
+        } else {
+            smsAckPdu[index++] = (byte) responseBytes.length;
+        }
+
+        System.arraycopy(responseBytes, 0, smsAckPdu, index, responseBytes.length);
+
+        mCi.acknowledgeIncomingGsmSmsWithPdu(success,
+                IccUtils.bytesToHexString(smsAckPdu), null);
+    }
+
+    private void acknowledgeSmsWithError(int cause) {
+        mCi.acknowledgeLastIncomingGsmSms(false, cause, null);
+    }
+
+    /**
+     * Returns whether the DCS is 7 bit. If so, set TP-UDL to the septet count of TP-UD;
+     * otherwise, set TP-UDL to the octet count of TP-UD.
+     * @param dcs the TP-Data-Coding-Scheme field from the original download SMS
+     * @return true if the DCS specifies 7 bit encoding; false otherwise
+     */
+    private static boolean is7bitDcs(int dcs) {
+        // See 3GPP TS 23.038 section 4
+        return ((dcs & 0x8C) == 0x00) || ((dcs & 0xF4) == 0xF0);
+    }
+
+    /**
+     * Handle UICC envelope response and send SMS acknowledgement.
+     *
+     * @param msg the message to handle
+     */
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+            case EVENT_START_DATA_DOWNLOAD:
+                handleDataDownload((SmsMessage) msg.obj);
+                break;
+
+            case EVENT_SEND_ENVELOPE_RESPONSE:
+                ar = (AsyncResult) msg.obj;
+
+                if (ar.exception != null) {
+                    Rlog.e(TAG, "UICC Send Envelope failure, exception: " + ar.exception);
+                    acknowledgeSmsWithError(
+                            CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR);
+                    return;
+                }
+
+                int[] dcsPid = (int[]) ar.userObj;
+                sendSmsAckForEnvelopeResponse((IccIoResult) ar.result, dcsPid[0], dcsPid[1]);
+                break;
+
+            case EVENT_WRITE_SMS_COMPLETE:
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null) {
+                    Rlog.d(TAG, "Successfully wrote SMS-PP message to UICC");
+                    mCi.acknowledgeLastIncomingGsmSms(true, 0, null);
+                } else {
+                    Rlog.d(TAG, "Failed to write SMS-PP message to UICC", ar.exception);
+                    mCi.acknowledgeLastIncomingGsmSms(false,
+                            CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR, null);
+                }
+                break;
+
+            default:
+                Rlog.e(TAG, "Ignoring unexpected message, what=" + msg.what);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/gsm/UsimPhoneBookManager.java b/com/android/internal/telephony/gsm/UsimPhoneBookManager.java
new file mode 100644
index 0000000..6489014
--- /dev/null
+++ b/com/android/internal/telephony/gsm/UsimPhoneBookManager.java
@@ -0,0 +1,666 @@
+/*
+ * 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.internal.telephony.gsm;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.telephony.uicc.AdnRecord;
+import com.android.internal.telephony.uicc.AdnRecordCache;
+import com.android.internal.telephony.uicc.IccConstants;
+import com.android.internal.telephony.uicc.IccFileHandler;
+import com.android.internal.telephony.uicc.IccUtils;
+import java.util.ArrayList;
+
+/**
+ * This class implements reading and parsing USIM records.
+ * Refer to Spec 3GPP TS 31.102 for more details.
+ *
+ * {@hide}
+ */
+public class UsimPhoneBookManager extends Handler implements IccConstants {
+    private static final String LOG_TAG = "UsimPhoneBookManager";
+    private static final boolean DBG = true;
+    private ArrayList<PbrRecord> mPbrRecords;
+    private Boolean mIsPbrPresent;
+    private IccFileHandler mFh;
+    private AdnRecordCache mAdnCache;
+    private Object mLock = new Object();
+    private ArrayList<AdnRecord> mPhoneBookRecords;
+    private ArrayList<byte[]> mIapFileRecord;
+    private ArrayList<byte[]> mEmailFileRecord;
+
+    // email list for each ADN record. The key would be
+    // ADN's efid << 8 + record #
+    private SparseArray<ArrayList<String>> mEmailsForAdnRec;
+
+    // SFI to ADN Efid mapping table
+    private SparseIntArray mSfiEfidTable;
+
+    private boolean mRefreshCache = false;
+
+
+    private static final int EVENT_PBR_LOAD_DONE = 1;
+    private static final int EVENT_USIM_ADN_LOAD_DONE = 2;
+    private static final int EVENT_IAP_LOAD_DONE = 3;
+    private static final int EVENT_EMAIL_LOAD_DONE = 4;
+
+    private static final int USIM_TYPE1_TAG   = 0xA8;
+    private static final int USIM_TYPE2_TAG   = 0xA9;
+    private static final int USIM_TYPE3_TAG   = 0xAA;
+    private static final int USIM_EFADN_TAG   = 0xC0;
+    private static final int USIM_EFIAP_TAG   = 0xC1;
+    private static final int USIM_EFEXT1_TAG  = 0xC2;
+    private static final int USIM_EFSNE_TAG   = 0xC3;
+    private static final int USIM_EFANR_TAG   = 0xC4;
+    private static final int USIM_EFPBC_TAG   = 0xC5;
+    private static final int USIM_EFGRP_TAG   = 0xC6;
+    private static final int USIM_EFAAS_TAG   = 0xC7;
+    private static final int USIM_EFGSD_TAG   = 0xC8;
+    private static final int USIM_EFUID_TAG   = 0xC9;
+    private static final int USIM_EFEMAIL_TAG = 0xCA;
+    private static final int USIM_EFCCP1_TAG  = 0xCB;
+
+    private static final int INVALID_SFI = -1;
+    private static final byte INVALID_BYTE = -1;
+
+    // class File represent a PBR record TLV object which points to the rest of the phonebook EFs
+    private class File {
+        // Phonebook reference file constructed tag defined in 3GPP TS 31.102
+        // section 4.4.2.1 table 4.1
+        private final int mParentTag;
+        // EFID of the file
+        private final int mEfid;
+        // SFI (Short File Identification) of the file. 0xFF indicates invalid SFI.
+        private final int mSfi;
+        // The order of this tag showing in the PBR record.
+        private final int mIndex;
+
+        File(int parentTag, int efid, int sfi, int index) {
+            mParentTag = parentTag;
+            mEfid = efid;
+            mSfi = sfi;
+            mIndex = index;
+        }
+
+        public int getParentTag() { return mParentTag; }
+        public int getEfid() { return mEfid; }
+        public int getSfi() { return mSfi; }
+        public int getIndex() { return mIndex; }
+    }
+
+    public UsimPhoneBookManager(IccFileHandler fh, AdnRecordCache cache) {
+        mFh = fh;
+        mPhoneBookRecords = new ArrayList<AdnRecord>();
+        mPbrRecords = null;
+        // We assume its present, after the first read this is updated.
+        // So we don't have to read from UICC if its not present on subsequent reads.
+        mIsPbrPresent = true;
+        mAdnCache = cache;
+        mEmailsForAdnRec = new SparseArray<ArrayList<String>>();
+        mSfiEfidTable = new SparseIntArray();
+    }
+
+    public void reset() {
+        mPhoneBookRecords.clear();
+        mIapFileRecord = null;
+        mEmailFileRecord = null;
+        mPbrRecords = null;
+        mIsPbrPresent = true;
+        mRefreshCache = false;
+        mEmailsForAdnRec.clear();
+        mSfiEfidTable.clear();
+    }
+
+    // Load all phonebook related EFs from the SIM.
+    public ArrayList<AdnRecord> loadEfFilesFromUsim() {
+        synchronized (mLock) {
+            if (!mPhoneBookRecords.isEmpty()) {
+                if (mRefreshCache) {
+                    mRefreshCache = false;
+                    refreshCache();
+                }
+                return mPhoneBookRecords;
+            }
+
+            if (!mIsPbrPresent) return null;
+
+            // Check if the PBR file is present in the cache, if not read it
+            // from the USIM.
+            if (mPbrRecords == null) {
+                readPbrFileAndWait();
+            }
+
+            if (mPbrRecords == null)
+                return null;
+
+            int numRecs = mPbrRecords.size();
+
+            log("loadEfFilesFromUsim: Loading adn and emails");
+            for (int i = 0; i < numRecs; i++) {
+                readAdnFileAndWait(i);
+                readEmailFileAndWait(i);
+            }
+
+            updatePhoneAdnRecord();
+            // All EF files are loaded, return all the records
+        }
+        return mPhoneBookRecords;
+    }
+
+    // Refresh the phonebook cache.
+    private void refreshCache() {
+        if (mPbrRecords == null) return;
+        mPhoneBookRecords.clear();
+
+        int numRecs = mPbrRecords.size();
+        for (int i = 0; i < numRecs; i++) {
+            readAdnFileAndWait(i);
+        }
+    }
+
+    // Invalidate the phonebook cache.
+    public void invalidateCache() {
+        mRefreshCache = true;
+    }
+
+    // Read the phonebook reference file EF_PBR.
+    private void readPbrFileAndWait() {
+        mFh.loadEFLinearFixedAll(EF_PBR, obtainMessage(EVENT_PBR_LOAD_DONE));
+        try {
+            mLock.wait();
+        } catch (InterruptedException e) {
+            Rlog.e(LOG_TAG, "Interrupted Exception in readAdnFileAndWait");
+        }
+    }
+
+    // Read EF_EMAIL which contains the email records.
+    private void readEmailFileAndWait(int recId) {
+        SparseArray<File> files;
+        files = mPbrRecords.get(recId).mFileIds;
+        if (files == null) return;
+
+        File email = files.get(USIM_EFEMAIL_TAG);
+        if (email != null) {
+
+            /**
+             * Check if the EF_EMAIL is a Type 1 file or a type 2 file.
+             * If mEmailPresentInIap is true, its a type 2 file.
+             * So we read the IAP file and then read the email records.
+             * instead of reading directly.
+             */
+            if (email.getParentTag() == USIM_TYPE2_TAG) {
+                if (files.get(USIM_EFIAP_TAG) == null) {
+                    Rlog.e(LOG_TAG, "Can't locate EF_IAP in EF_PBR.");
+                    return;
+                }
+
+                log("EF_IAP exists. Loading EF_IAP to retrieve the index.");
+                readIapFileAndWait(files.get(USIM_EFIAP_TAG).getEfid());
+                if (mIapFileRecord == null) {
+                    Rlog.e(LOG_TAG, "Error: IAP file is empty");
+                    return;
+                }
+
+                log("EF_EMAIL order in PBR record: " + email.getIndex());
+            }
+
+            int emailEfid = email.getEfid();
+            log("EF_EMAIL exists in PBR. efid = 0x" +
+                    Integer.toHexString(emailEfid).toUpperCase());
+
+            /**
+             * Make sure this EF_EMAIL was never read earlier. Sometimes two PBR record points
+             */
+            // to the same EF_EMAIL
+            for (int i = 0; i < recId; i++) {
+                if (mPbrRecords.get(i) != null) {
+                    SparseArray<File> previousFileIds = mPbrRecords.get(i).mFileIds;
+                    if (previousFileIds != null) {
+                        File id = previousFileIds.get(USIM_EFEMAIL_TAG);
+                        if (id != null && id.getEfid() == emailEfid) {
+                            log("Skipped this EF_EMAIL which was loaded earlier");
+                            return;
+                        }
+                    }
+                }
+            }
+
+            // Read the EFEmail file.
+            mFh.loadEFLinearFixedAll(emailEfid,
+                    obtainMessage(EVENT_EMAIL_LOAD_DONE));
+            try {
+                mLock.wait();
+            } catch (InterruptedException e) {
+                Rlog.e(LOG_TAG, "Interrupted Exception in readEmailFileAndWait");
+            }
+
+            if (mEmailFileRecord == null) {
+                Rlog.e(LOG_TAG, "Error: Email file is empty");
+                return;
+            }
+
+            // Build email list
+            if (email.getParentTag() == USIM_TYPE2_TAG && mIapFileRecord != null) {
+                // If the tag is type 2 and EF_IAP exists, we need to build tpe 2 email list
+                buildType2EmailList(recId);
+            }
+            else {
+                // If one the followings is true, we build type 1 email list
+                // 1. EF_IAP does not exist or it is failed to load
+                // 2. ICC cards can be made such that they have an IAP file but all
+                //    records are empty. In that case buildType2EmailList will fail and
+                //    we need to build type 1 email list.
+
+                // Build type 1 email list
+                buildType1EmailList(recId);
+            }
+        }
+    }
+
+    // Build type 1 email list
+    private void buildType1EmailList(int recId) {
+        /**
+         * If this is type 1, the number of records in EF_EMAIL would be same as the record number
+         * in the master/reference file.
+         */
+        if (mPbrRecords.get(recId) == null)
+            return;
+
+        int numRecs = mPbrRecords.get(recId).mMasterFileRecordNum;
+        log("Building type 1 email list. recId = "
+                + recId + ", numRecs = " + numRecs);
+
+        byte[] emailRec;
+        for (int i = 0; i < numRecs; i++) {
+            try {
+                emailRec = mEmailFileRecord.get(i);
+            } catch (IndexOutOfBoundsException e) {
+                Rlog.e(LOG_TAG, "Error: Improper ICC card: No email record for ADN, continuing");
+                break;
+            }
+
+            /**
+             *  3GPP TS 31.102 4.4.2.13 EF_EMAIL (e-mail address)
+             *
+             *  The fields below are mandatory if and only if the file
+             *  is not type 1 (as specified in EF_PBR)
+             *
+             *  Byte [X + 1]: ADN file SFI (Short File Identification)
+             *  Byte [X + 2]: ADN file Record Identifier
+             */
+            int sfi = emailRec[emailRec.length - 2];
+            int adnRecId = emailRec[emailRec.length - 1];
+
+            String email = readEmailRecord(i);
+
+            if (email == null || email.equals("")) {
+                continue;
+            }
+
+            // Get the associated ADN's efid first.
+            int adnEfid = 0;
+            if (sfi == INVALID_SFI || mSfiEfidTable.get(sfi) == 0) {
+
+                // If SFI is invalid or cannot be mapped to any ADN, use the ADN's efid
+                // in the same PBR files.
+                File file = mPbrRecords.get(recId).mFileIds.get(USIM_EFADN_TAG);
+                if (file == null)
+                    continue;
+                adnEfid = file.getEfid();
+            }
+            else {
+                adnEfid = mSfiEfidTable.get(sfi);
+            }
+            /**
+             * SIM record numbers are 1 based.
+             * The key is constructed by efid and record index.
+             */
+            int index = (((adnEfid & 0xFFFF) << 8) | ((adnRecId - 1) & 0xFF));
+            ArrayList<String> emailList = mEmailsForAdnRec.get(index);
+            if (emailList == null) {
+                emailList = new ArrayList<String>();
+            }
+            log("Adding email #" + i + " list to index 0x" +
+                    Integer.toHexString(index).toUpperCase());
+            emailList.add(email);
+            mEmailsForAdnRec.put(index, emailList);
+        }
+    }
+
+    // Build type 2 email list
+    private boolean buildType2EmailList(int recId) {
+
+        if (mPbrRecords.get(recId) == null)
+            return false;
+
+        int numRecs = mPbrRecords.get(recId).mMasterFileRecordNum;
+        log("Building type 2 email list. recId = "
+                + recId + ", numRecs = " + numRecs);
+
+        /**
+         * 3GPP TS 31.102 4.4.2.1 EF_PBR (Phone Book Reference file) table 4.1
+
+         * The number of records in the IAP file is same as the number of records in the master
+         * file (e.g EF_ADN). The order of the pointers in an EF_IAP shall be the same as the
+         * order of file IDs that appear in the TLV object indicated by Tag 'A9' in the
+         * reference file record (e.g value of mEmailTagNumberInIap)
+         */
+
+        File adnFile = mPbrRecords.get(recId).mFileIds.get(USIM_EFADN_TAG);
+        if (adnFile == null) {
+            Rlog.e(LOG_TAG, "Error: Improper ICC card: EF_ADN does not exist in PBR files");
+            return false;
+        }
+        int adnEfid = adnFile.getEfid();
+
+        for (int i = 0; i < numRecs; i++) {
+            byte[] record;
+            int emailRecId;
+            try {
+                record = mIapFileRecord.get(i);
+                emailRecId =
+                        record[mPbrRecords.get(recId).mFileIds.get(USIM_EFEMAIL_TAG).getIndex()];
+            } catch (IndexOutOfBoundsException e) {
+                Rlog.e(LOG_TAG, "Error: Improper ICC card: Corrupted EF_IAP");
+                continue;
+            }
+
+            String email = readEmailRecord(emailRecId - 1);
+            if (email != null && !email.equals("")) {
+                // The key is constructed by efid and record index.
+                int index = (((adnEfid & 0xFFFF) << 8) | (i & 0xFF));
+                ArrayList<String> emailList = mEmailsForAdnRec.get(index);
+                if (emailList == null) {
+                    emailList = new ArrayList<String>();
+                }
+                emailList.add(email);
+                log("Adding email list to index 0x" +
+                        Integer.toHexString(index).toUpperCase());
+                mEmailsForAdnRec.put(index, emailList);
+            }
+        }
+        return true;
+    }
+
+    // Read Phonebook Index Admistration EF_IAP file
+    private void readIapFileAndWait(int efid) {
+        mFh.loadEFLinearFixedAll(efid, obtainMessage(EVENT_IAP_LOAD_DONE));
+        try {
+            mLock.wait();
+        } catch (InterruptedException e) {
+            Rlog.e(LOG_TAG, "Interrupted Exception in readIapFileAndWait");
+        }
+    }
+
+    private void updatePhoneAdnRecord() {
+
+        int numAdnRecs = mPhoneBookRecords.size();
+
+        for (int i = 0; i < numAdnRecs; i++) {
+
+            AdnRecord rec = mPhoneBookRecords.get(i);
+
+            int adnEfid = rec.getEfid();
+            int adnRecId = rec.getRecId();
+
+            int index = (((adnEfid & 0xFFFF) << 8) | ((adnRecId - 1) & 0xFF));
+
+            ArrayList<String> emailList;
+            try {
+                emailList = mEmailsForAdnRec.get(index);
+            } catch (IndexOutOfBoundsException e) {
+                continue;
+            }
+
+            if (emailList == null)
+                continue;
+
+            String[] emails = new String[emailList.size()];
+            System.arraycopy(emailList.toArray(), 0, emails, 0, emailList.size());
+            rec.setEmails(emails);
+            log("Adding email list to ADN (0x" +
+                    Integer.toHexString(mPhoneBookRecords.get(i).getEfid()).toUpperCase() +
+                    ") record #" + mPhoneBookRecords.get(i).getRecId());
+            mPhoneBookRecords.set(i, rec);
+        }
+    }
+
+    // Read email from the record of EF_EMAIL
+    private String readEmailRecord(int recId) {
+        byte[] emailRec;
+        try {
+            emailRec = mEmailFileRecord.get(recId);
+        } catch (IndexOutOfBoundsException e) {
+            return null;
+        }
+
+        // The length of the record is X+2 byte, where X bytes is the email address
+        return IccUtils.adnStringFieldToString(emailRec, 0, emailRec.length - 2);
+    }
+
+    // Read EF_ADN file
+    private void readAdnFileAndWait(int recId) {
+        SparseArray<File> files;
+        files = mPbrRecords.get(recId).mFileIds;
+        if (files == null || files.size() == 0) return;
+
+        int extEf = 0;
+        // Only call fileIds.get while EF_EXT1_TAG is available
+        if (files.get(USIM_EFEXT1_TAG) != null) {
+            extEf = files.get(USIM_EFEXT1_TAG).getEfid();
+        }
+
+        if (files.get(USIM_EFADN_TAG) == null)
+            return;
+
+        int previousSize = mPhoneBookRecords.size();
+        mAdnCache.requestLoadAllAdnLike(files.get(USIM_EFADN_TAG).getEfid(),
+            extEf, obtainMessage(EVENT_USIM_ADN_LOAD_DONE));
+        try {
+            mLock.wait();
+        } catch (InterruptedException e) {
+            Rlog.e(LOG_TAG, "Interrupted Exception in readAdnFileAndWait");
+        }
+
+        /**
+         * The recent added ADN record # would be the reference record size
+         * for the rest of EFs associated within this PBR.
+         */
+        mPbrRecords.get(recId).mMasterFileRecordNum = mPhoneBookRecords.size() - previousSize;
+    }
+
+    // Create the phonebook reference file based on EF_PBR
+    private void createPbrFile(ArrayList<byte[]> records) {
+        if (records == null) {
+            mPbrRecords = null;
+            mIsPbrPresent = false;
+            return;
+        }
+
+        mPbrRecords = new ArrayList<PbrRecord>();
+        for (int i = 0; i < records.size(); i++) {
+            // Some cards have two records but the 2nd record is filled with all invalid char 0xff.
+            // So we need to check if the record is valid or not before adding into the PBR records.
+            if (records.get(i)[0] != INVALID_BYTE) {
+                mPbrRecords.add(new PbrRecord(records.get(i)));
+            }
+        }
+
+        for (PbrRecord record : mPbrRecords) {
+            File file = record.mFileIds.get(USIM_EFADN_TAG);
+            // If the file does not contain EF_ADN, we'll just skip it.
+            if (file != null) {
+                int sfi = file.getSfi();
+                if (sfi != INVALID_SFI) {
+                    mSfiEfidTable.put(sfi, record.mFileIds.get(USIM_EFADN_TAG).getEfid());
+                }
+            }
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch(msg.what) {
+        case EVENT_PBR_LOAD_DONE:
+            log("Loading PBR records done");
+            ar = (AsyncResult) msg.obj;
+            if (ar.exception == null) {
+                createPbrFile((ArrayList<byte[]>)ar.result);
+            }
+            synchronized (mLock) {
+                mLock.notify();
+            }
+            break;
+        case EVENT_USIM_ADN_LOAD_DONE:
+            log("Loading USIM ADN records done");
+            ar = (AsyncResult) msg.obj;
+            if (ar.exception == null) {
+                mPhoneBookRecords.addAll((ArrayList<AdnRecord>)ar.result);
+            }
+            synchronized (mLock) {
+                mLock.notify();
+            }
+            break;
+        case EVENT_IAP_LOAD_DONE:
+            log("Loading USIM IAP records done");
+            ar = (AsyncResult) msg.obj;
+            if (ar.exception == null) {
+                mIapFileRecord = ((ArrayList<byte[]>)ar.result);
+            }
+            synchronized (mLock) {
+                mLock.notify();
+            }
+            break;
+        case EVENT_EMAIL_LOAD_DONE:
+            log("Loading USIM Email records done");
+            ar = (AsyncResult) msg.obj;
+            if (ar.exception == null) {
+                mEmailFileRecord = ((ArrayList<byte[]>)ar.result);
+            }
+
+            synchronized (mLock) {
+                mLock.notify();
+            }
+            break;
+        }
+    }
+
+    // PbrRecord represents a record in EF_PBR
+    private class PbrRecord {
+        // TLV tags
+        private SparseArray<File> mFileIds;
+
+        /**
+         * 3GPP TS 31.102 4.4.2.1 EF_PBR (Phone Book Reference file)
+         * If this is type 1 files, files that contain as many records as the
+         * reference/master file (EF_ADN, EF_ADN1) and are linked on record number
+         * bases (Rec1 -> Rec1). The master file record number is the reference.
+         */
+        private int mMasterFileRecordNum;
+
+        PbrRecord(byte[] record) {
+            mFileIds = new SparseArray<File>();
+            SimTlv recTlv;
+            log("PBR rec: " + IccUtils.bytesToHexString(record));
+            recTlv = new SimTlv(record, 0, record.length);
+            parseTag(recTlv);
+        }
+
+        void parseTag(SimTlv tlv) {
+            SimTlv tlvEfSfi;
+            int tag;
+            byte[] data;
+
+            do {
+                tag = tlv.getTag();
+                switch(tag) {
+                case USIM_TYPE1_TAG: // A8
+                case USIM_TYPE3_TAG: // AA
+                case USIM_TYPE2_TAG: // A9
+                    data = tlv.getData();
+                    tlvEfSfi = new SimTlv(data, 0, data.length);
+                    parseEfAndSFI(tlvEfSfi, tag);
+                    break;
+                }
+            } while (tlv.nextObject());
+        }
+
+        void parseEfAndSFI(SimTlv tlv, int parentTag) {
+            int tag;
+            byte[] data;
+            int tagNumberWithinParentTag = 0;
+            do {
+                tag = tlv.getTag();
+                switch(tag) {
+                    case USIM_EFEMAIL_TAG:
+                    case USIM_EFADN_TAG:
+                    case USIM_EFEXT1_TAG:
+                    case USIM_EFANR_TAG:
+                    case USIM_EFPBC_TAG:
+                    case USIM_EFGRP_TAG:
+                    case USIM_EFAAS_TAG:
+                    case USIM_EFGSD_TAG:
+                    case USIM_EFUID_TAG:
+                    case USIM_EFCCP1_TAG:
+                    case USIM_EFIAP_TAG:
+                    case USIM_EFSNE_TAG:
+                        /** 3GPP TS 31.102, 4.4.2.1 EF_PBR (Phone Book Reference file)
+                         *
+                         * The SFI value assigned to an EF which is indicated in EF_PBR shall
+                         * correspond to the SFI indicated in the TLV object in EF_PBR.
+
+                         * The primitive tag identifies clearly the type of data, its value
+                         * field indicates the file identifier and, if applicable, the SFI
+                         * value of the specified EF. That is, the length value of a primitive
+                         * tag indicates if an SFI value is available for the EF or not:
+                         * - Length = '02' Value: 'EFID (2 bytes)'
+                         * - Length = '03' Value: 'EFID (2 bytes)', 'SFI (1 byte)'
+                         */
+
+                        int sfi = INVALID_SFI;
+                        data = tlv.getData();
+
+                        if (data.length < 2 || data.length > 3) {
+                            log("Invalid TLV length: " + data.length);
+                            break;
+                        }
+
+                        if (data.length == 3) {
+                            sfi = data[2] & 0xFF;
+                        }
+
+                        int efid = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
+
+                        mFileIds.put(tag, new File(parentTag, efid, sfi, tagNumberWithinParentTag));
+                        break;
+                }
+                tagNumberWithinParentTag++;
+            } while(tlv.nextObject());
+        }
+    }
+
+    private void log(String msg) {
+        if(DBG) Rlog.d(LOG_TAG, msg);
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/ims/ImsResolver.java b/com/android/internal/telephony/ims/ImsResolver.java
new file mode 100644
index 0000000..2f790b7
--- /dev/null
+++ b/com/android/internal/telephony/ims/ImsResolver.java
@@ -0,0 +1,696 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.ims;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.feature.ImsFeature;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.ims.internal.IImsServiceController;
+import com.android.ims.internal.IImsServiceFeatureListener;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.PhoneConstants;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Creates a list of ImsServices that are available to bind to based on the Device configuration
+ * overlay value "config_ims_package" and Carrier Configuration value
+ * "config_ims_package_override_string".
+ * These ImsServices are then bound to in the following order:
+ *
+ * 1. Carrier Config defined override value per SIM.
+ * 2. Device overlay default value (including no SIM case).
+ *
+ * ImsManager can then retrieve the binding to the correct ImsService using
+ * {@link #getImsServiceControllerAndListen} on a per-slot and per feature basis.
+ */
+
+public class ImsResolver implements ImsServiceController.ImsServiceControllerCallbacks {
+
+    private static final String TAG = "ImsResolver";
+
+    public static final String SERVICE_INTERFACE = "android.telephony.ims.ImsService";
+    public static final String METADATA_EMERGENCY_MMTEL_FEATURE =
+            "android.telephony.ims.EMERGENCY_MMTEL_FEATURE";
+    public static final String METADATA_MMTEL_FEATURE = "android.telephony.ims.MMTEL_FEATURE";
+    public static final String METADATA_RCS_FEATURE = "android.telephony.ims.RCS_FEATURE";
+
+    // Based on updates from PackageManager
+    private static final int HANDLER_ADD_PACKAGE = 0;
+    // Based on updates from PackageManager
+    private static final int HANDLER_REMOVE_PACKAGE = 1;
+    // Based on updates from CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
+    private static final int HANDLER_CONFIG_CHANGED = 2;
+
+    /**
+     * Stores information about an ImsService, including the package name, class name, and features
+     * that the service supports.
+     */
+    @VisibleForTesting
+    public static class ImsServiceInfo {
+        public ComponentName name;
+        public Set<Integer> supportedFeatures;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            ImsServiceInfo that = (ImsServiceInfo) o;
+
+            if (name != null ? !name.equals(that.name) : that.name != null) return false;
+            return supportedFeatures != null ? supportedFeatures.equals(that.supportedFeatures)
+                    : that.supportedFeatures == null;
+
+        }
+
+        @Override
+        public int hashCode() {
+            int result = name != null ? name.hashCode() : 0;
+            result = 31 * result + (supportedFeatures != null ? supportedFeatures.hashCode() : 0);
+            return result;
+        }
+    }
+
+    // Receives broadcasts from the system involving changes to the installed applications. If
+    // an ImsService that we are configured to use is installed, we must bind to it.
+    private BroadcastReceiver mAppChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            final String packageName = intent.getData().getSchemeSpecificPart();
+            switch (action) {
+                case Intent.ACTION_PACKAGE_ADDED:
+                    // intentional fall-through
+                case Intent.ACTION_PACKAGE_CHANGED:
+                    mHandler.obtainMessage(HANDLER_ADD_PACKAGE, packageName).sendToTarget();
+                    break;
+                case Intent.ACTION_PACKAGE_REMOVED:
+                    mHandler.obtainMessage(HANDLER_REMOVE_PACKAGE, packageName).sendToTarget();
+                    break;
+                default:
+                    return;
+            }
+        }
+    };
+
+    // Receives the broadcast that a new Carrier Config has been loaded in order to possibly
+    // unbind from one service and bind to another.
+    private BroadcastReceiver mConfigChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+
+            int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+                    SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+            if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                Log.i(TAG, "Received SIM change for invalid sub id.");
+                return;
+            }
+
+            Log.i(TAG, "Received Carrier Config Changed for SubId: " + subId);
+
+            mHandler.obtainMessage(HANDLER_CONFIG_CHANGED, subId).sendToTarget();
+        }
+    };
+
+    /**
+     * Testing interface used to mock SubscriptionManager in testing
+     */
+    @VisibleForTesting
+    public interface SubscriptionManagerProxy {
+        /**
+         * Mock-able interface for {@link SubscriptionManager#getSubId(int)} used for testing.
+         */
+        int getSubId(int slotId);
+        /**
+         * Mock-able interface for {@link SubscriptionManager#getSlotIndex(int)} used for testing.
+         */
+        int getSlotIndex(int subId);
+    }
+
+    private SubscriptionManagerProxy mSubscriptionManagerProxy = new SubscriptionManagerProxy() {
+        @Override
+        public int getSubId(int slotId) {
+            int[] subIds = SubscriptionManager.getSubId(slotId);
+            if (subIds != null) {
+                // This is done in all other places getSubId is used.
+                return subIds[0];
+            }
+            return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        }
+
+        @Override
+        public int getSlotIndex(int subId) {
+            return SubscriptionManager.getSlotIndex(subId);
+        }
+    };
+
+    /**
+     * Testing interface for injecting mock ImsServiceControllers.
+     */
+    @VisibleForTesting
+    public interface ImsServiceControllerFactory {
+        /**
+         * Returns the ImsServiceController created usiing the context and componentName supplied.
+         * Used for DI when testing.
+         */
+        ImsServiceController get(Context context, ComponentName componentName);
+    }
+
+    private ImsServiceControllerFactory mImsServiceControllerFactory = (context, componentName) ->
+            new ImsServiceController(context, componentName, this);
+
+    private final CarrierConfigManager mCarrierConfigManager;
+    private final Context mContext;
+    // Locks mBoundImsServicesByFeature only. Be careful to avoid deadlocks from
+    // ImsServiceController callbacks.
+    private final Object mBoundServicesLock = new Object();
+    private final int mNumSlots;
+
+    // Synchronize all messages on a handler to ensure that the cache includes the most recent
+    // version of the installed ImsServices.
+    private Handler mHandler = new Handler(Looper.getMainLooper(), (msg) -> {
+        switch (msg.what) {
+            case HANDLER_ADD_PACKAGE: {
+                String packageName = (String) msg.obj;
+                maybeAddedImsService(packageName);
+                break;
+            }
+            case HANDLER_REMOVE_PACKAGE: {
+                String packageName = (String) msg.obj;
+                maybeRemovedImsService(packageName);
+                break;
+            }
+            case HANDLER_CONFIG_CHANGED: {
+                int subId = (Integer) msg.obj;
+                maybeRebindService(subId);
+                break;
+            }
+            default:
+                return false;
+        }
+        return true;
+    });
+
+    // Package name of the default device service.
+    private String mDeviceService;
+    // Array index corresponds to slot Id associated with the service package name.
+    private String[] mCarrierServices;
+    // List index corresponds to Slot Id, Maps ImsFeature.FEATURE->bound ImsServiceController
+    // Locked on mBoundServicesLock
+    private List<SparseArray<ImsServiceController>> mBoundImsServicesByFeature;
+    // not locked, only accessed on a handler thread.
+    private Set<ImsServiceInfo> mInstalledServicesCache = new ArraySet<>();
+    // not locked, only accessed on a handler thread.
+    private Set<ImsServiceController> mActiveControllers = new ArraySet<>();
+
+    public ImsResolver(Context context, String defaultImsPackageName, int numSlots) {
+        mContext = context;
+        mDeviceService = defaultImsPackageName;
+        mNumSlots = numSlots;
+        mCarrierConfigManager = (CarrierConfigManager) mContext.getSystemService(
+                Context.CARRIER_CONFIG_SERVICE);
+        mCarrierServices = new String[numSlots];
+        mBoundImsServicesByFeature = Stream.generate(SparseArray<ImsServiceController>::new)
+                .limit(mNumSlots).collect(Collectors.toList());
+
+        IntentFilter appChangedFilter = new IntentFilter();
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        appChangedFilter.addDataScheme("package");
+        context.registerReceiverAsUser(mAppChangedReceiver, UserHandle.ALL, appChangedFilter, null,
+                null);
+
+        context.registerReceiver(mConfigChangedReceiver, new IntentFilter(
+                CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+    }
+
+    @VisibleForTesting
+    public void setSubscriptionManagerProxy(SubscriptionManagerProxy proxy) {
+        mSubscriptionManagerProxy = proxy;
+    }
+
+    @VisibleForTesting
+    public void setImsServiceControllerFactory(ImsServiceControllerFactory factory) {
+        mImsServiceControllerFactory = factory;
+    }
+
+    @VisibleForTesting
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * Needs to be called after the constructor to first populate the cache and possibly bind to
+     * ImsServices.
+     */
+    public void populateCacheAndStartBind() {
+        Log.i(TAG, "Initializing cache and binding.");
+        // Populates the CarrierConfig override package names for each slot
+        mHandler.obtainMessage(HANDLER_CONFIG_CHANGED, -1).sendToTarget();
+        // Starts first bind to the system.
+        mHandler.obtainMessage(HANDLER_ADD_PACKAGE, null).sendToTarget();
+    }
+
+    /**
+     * Returns the {@link IImsServiceController} that corresponds to the given slot Id and IMS
+     * feature or {@link null} if the service is not available. If an ImsServiceController is
+     * available, the {@link IImsServiceFeatureListener} callback is registered as a listener for
+     * feature updates.
+     * @param slotId The SIM slot that we are requesting the {@link IImsServiceController} for.
+     * @param feature The IMS Feature we are requesting.
+     * @param callback Listener that will send updates to ImsManager when there are updates to
+     * ImsServiceController.
+     * @return {@link IImsServiceController} interface for the feature specified or {@link null} if
+     * it is unavailable.
+     */
+    public IImsServiceController getImsServiceControllerAndListen(int slotId, int feature,
+            IImsServiceFeatureListener callback) {
+        if (slotId < 0 || slotId >= mNumSlots || feature <= ImsFeature.INVALID
+                || feature >= ImsFeature.MAX) {
+            return null;
+        }
+        ImsServiceController controller;
+        synchronized (mBoundServicesLock) {
+            SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId);
+            if (services == null) {
+                return null;
+            }
+            controller = services.get(feature);
+        }
+        if (controller != null) {
+            controller.addImsServiceFeatureListener(callback);
+            return controller.getImsServiceController();
+        }
+        return null;
+    }
+
+    private void putImsController(int slotId, int feature, ImsServiceController controller) {
+        if (slotId < 0 || slotId >= mNumSlots || feature <= ImsFeature.INVALID
+                || feature >= ImsFeature.MAX) {
+            Log.w(TAG, "putImsController received invalid parameters - slot: " + slotId
+                    + ", feature: " + feature);
+            return;
+        }
+        synchronized (mBoundServicesLock) {
+            SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId);
+            if (services == null) {
+                services = new SparseArray<>();
+                mBoundImsServicesByFeature.add(slotId, services);
+            }
+            Log.i(TAG, "ImsServiceController added on slot: " + slotId + " with feature: "
+                    + feature + " using package: " + controller.getComponentName());
+            services.put(feature, controller);
+        }
+    }
+
+    private ImsServiceController removeImsController(int slotId, int feature) {
+        if (slotId < 0 || slotId >= mNumSlots || feature <= ImsFeature.INVALID
+                || feature >= ImsFeature.MAX) {
+            Log.w(TAG, "removeImsController received invalid parameters - slot: " + slotId
+                    + ", feature: " + feature);
+            return null;
+        }
+        synchronized (mBoundServicesLock) {
+            SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId);
+            if (services == null) {
+                return null;
+            }
+            ImsServiceController c = services.get(feature, null);
+            if (c != null) {
+                Log.i(TAG, "ImsServiceController removed on slot: " + slotId + " with feature: "
+                        + feature + " using package: " + c.getComponentName());
+                services.remove(feature);
+            }
+            return c;
+        }
+    }
+
+
+    // Update the current cache with the new ImsService(s) if it has been added or update the
+    // supported IMS features if they have changed.
+    // Called from the handler ONLY
+    private void maybeAddedImsService(String packageName) {
+        Log.d(TAG, "maybeAddedImsService, packageName: " + packageName);
+        List<ImsServiceInfo> infos = getImsServiceInfo(packageName);
+        List<ImsServiceInfo> newlyAddedInfos = new ArrayList<>();
+        for (ImsServiceInfo info : infos) {
+            // Checking to see if the ComponentName is the same, so we can update the supported
+            // features. Will only be one (if it exists), since it is a set.
+            Optional<ImsServiceInfo> match = getInfoByComponentName(mInstalledServicesCache,
+                    info.name);
+            if (match.isPresent()) {
+                // update features in the cache
+                Log.i(TAG, "Updating features in cached ImsService: " + info.name);
+                Log.d(TAG, "Updating features - Old features: " + match.get().supportedFeatures
+                        + " new features: " + info.supportedFeatures);
+                match.get().supportedFeatures = info.supportedFeatures;
+                updateImsServiceFeatures(info);
+            } else {
+                Log.i(TAG, "Adding newly added ImsService to cache: " + info.name);
+                mInstalledServicesCache.add(info);
+                newlyAddedInfos.add(info);
+            }
+        }
+        // Loop through the newly created ServiceInfos in a separate loop to make sure the cache
+        // is fully updated.
+        for (ImsServiceInfo info : newlyAddedInfos) {
+            if (isActiveCarrierService(info)) {
+                // New ImsService is registered to active carrier services and must be newly
+                // bound.
+                bindNewImsService(info);
+                // Update existing device service features
+                updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
+            } else if (isDeviceService(info)) {
+                // New ImsService is registered as device default and must be newly bound.
+                bindNewImsService(info);
+            }
+        }
+    }
+
+    // Remove the ImsService from the cache. At this point, the ImsService will have already been
+    // killed.
+    // Called from the handler ONLY
+    private boolean maybeRemovedImsService(String packageName) {
+        Optional<ImsServiceInfo> match = getInfoByPackageName(mInstalledServicesCache, packageName);
+        if (match.isPresent()) {
+            mInstalledServicesCache.remove(match.get());
+            Log.i(TAG, "Removing ImsService: " + match.get().name);
+            unbindImsService(match.get());
+            updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
+            return true;
+        }
+        return false;
+    }
+
+    // Returns true if the CarrierConfig that has been loaded includes this ImsServiceInfo
+    // package name.
+    // Called from Handler ONLY
+    private boolean isActiveCarrierService(ImsServiceInfo info) {
+        for (int i = 0; i < mNumSlots; i++) {
+            if (TextUtils.equals(mCarrierServices[i], info.name.getPackageName())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isDeviceService(ImsServiceInfo info) {
+        return TextUtils.equals(mDeviceService, info.name.getPackageName());
+    }
+
+    private int getSlotForActiveCarrierService(ImsServiceInfo info) {
+        for (int i = 0; i < mNumSlots; i++) {
+            if (TextUtils.equals(mCarrierServices[i], info.name.getPackageName())) {
+                return i;
+            }
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
+    private Optional<ImsServiceController> getControllerByServiceInfo(
+            Set<ImsServiceController> searchSet, ImsServiceInfo matchValue) {
+        return searchSet.stream()
+                .filter(c -> Objects.equals(c.getComponentName(), matchValue.name)).findFirst();
+    }
+
+    private Optional<ImsServiceInfo> getInfoByPackageName(Set<ImsServiceInfo> searchSet,
+            String matchValue) {
+        return searchSet.stream()
+                .filter((i) -> Objects.equals(i.name.getPackageName(), matchValue)).findFirst();
+    }
+
+    private Optional<ImsServiceInfo> getInfoByComponentName(Set<ImsServiceInfo> searchSet,
+            ComponentName matchValue) {
+        return searchSet.stream()
+                .filter((i) -> Objects.equals(i.name, matchValue)).findFirst();
+    }
+
+    // Creates new features in active ImsServices and removes obsolete cached features. If
+    // cachedInfo == null, then newInfo is assumed to be a new ImsService and will have all features
+    // created.
+    private void updateImsServiceFeatures(ImsServiceInfo newInfo) {
+        if (newInfo == null) {
+            return;
+        }
+        Optional<ImsServiceController> o = getControllerByServiceInfo(mActiveControllers,
+                newInfo);
+        if (o.isPresent()) {
+            Log.i(TAG, "Updating features for ImsService: " + o.get().getComponentName());
+            HashSet<Pair<Integer, Integer>> features = calculateFeaturesToCreate(newInfo);
+            try {
+                if (features.size() > 0) {
+                    Log.d(TAG, "Updating Features - New Features: " + features);
+                    o.get().changeImsServiceFeatures(features);
+
+                    // If the carrier service features have changed, the device features will also
+                    // need to be recalculated.
+                    if (isActiveCarrierService(newInfo)
+                            // Prevent infinite recursion from bad behavior
+                            && !TextUtils.equals(newInfo.name.getPackageName(), mDeviceService)) {
+                        Log.i(TAG, "Updating device default");
+                        updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
+                    }
+                } else {
+                    Log.i(TAG, "Unbinding: features = 0 for ImsService: "
+                            + o.get().getComponentName());
+                    o.get().unbind();
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "updateImsServiceFeatures: Remote Exception: " + e.getMessage());
+            }
+        }
+    }
+
+    // Bind to a new ImsService and wait for the service to be connected to create ImsFeatures.
+    private void bindNewImsService(ImsServiceInfo info) {
+        if (info == null) {
+            return;
+        }
+        ImsServiceController controller = mImsServiceControllerFactory.get(mContext, info.name);
+        HashSet<Pair<Integer, Integer>> features = calculateFeaturesToCreate(info);
+        // Only bind if there are features that will be created by the service.
+        if (features.size() > 0) {
+            Log.i(TAG, "Binding ImsService: " + controller.getComponentName() + " with features: "
+                    + features);
+            controller.bind(features);
+            mActiveControllers.add(controller);
+        }
+    }
+
+    // Clean up and unbind from an ImsService
+    private void unbindImsService(ImsServiceInfo info) {
+        if (info == null) {
+            return;
+        }
+        Optional<ImsServiceController> o = getControllerByServiceInfo(mActiveControllers, info);
+        if (o.isPresent()) {
+            // Calls imsServiceFeatureRemoved on all features in the controller
+            try {
+                Log.i(TAG, "Unbinding ImsService: " + o.get().getComponentName());
+                o.get().unbind();
+            } catch (RemoteException e) {
+                Log.e(TAG, "unbindImsService: Remote Exception: " + e.getMessage());
+            }
+            mActiveControllers.remove(o.get());
+        }
+    }
+
+    // Calculate which features an ImsServiceController will need. If it is the carrier specific
+    // ImsServiceController, it will be granted all of the features it requests on the associated
+    // slot. If it is the device ImsService, it will get all of the features not covered by the
+    // carrier implementation.
+    private HashSet<Pair<Integer, Integer>> calculateFeaturesToCreate(ImsServiceInfo info) {
+        HashSet<Pair<Integer, Integer>> imsFeaturesBySlot = new HashSet<>();
+        // Check if the info is a carrier service
+        int slotId = getSlotForActiveCarrierService(info);
+        if (slotId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            imsFeaturesBySlot.addAll(info.supportedFeatures.stream().map(
+                    feature -> new Pair<>(slotId, feature)).collect(Collectors.toList()));
+        } else if (isDeviceService(info)) {
+            // For all slots that are not currently using a carrier ImsService, enable all features
+            // for the device default.
+            for (int i = 0; i < mNumSlots; i++) {
+                final int currSlotId = i;
+                ImsServiceInfo carrierImsInfo = getImsServiceInfoFromCache(mCarrierServices[i]);
+                if (carrierImsInfo == null) {
+                    // No Carrier override, add all features
+                    imsFeaturesBySlot.addAll(info.supportedFeatures.stream().map(
+                            feature -> new Pair<>(currSlotId, feature)).collect(
+                            Collectors.toList()));
+                } else {
+                    // Add all features to the device service that are not currently covered by
+                    // the carrier ImsService.
+                    Set<Integer> deviceFeatures = new HashSet<>(info.supportedFeatures);
+                    deviceFeatures.removeAll(carrierImsInfo.supportedFeatures);
+                    imsFeaturesBySlot.addAll(deviceFeatures.stream().map(
+                            feature -> new Pair<>(currSlotId, feature)).collect(
+                            Collectors.toList()));
+                }
+            }
+        }
+        return imsFeaturesBySlot;
+    }
+
+    /**
+     * Implementation of
+     * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeatureCreated}, which
+     * removes the ImsServiceController from the mBoundImsServicesByFeature structure.
+     */
+    public void imsServiceFeatureCreated(int slotId, int feature, ImsServiceController controller) {
+        putImsController(slotId, feature, controller);
+    }
+
+    /**
+     * Implementation of
+     * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeatureRemoved}, which
+     * removes the ImsServiceController from the mBoundImsServicesByFeature structure.
+     */
+    public void imsServiceFeatureRemoved(int slotId, int feature, ImsServiceController controller) {
+        removeImsController(slotId, feature);
+    }
+
+    // Possibly rebind to another ImsService if currently installed ImsServices were changed or if
+    // the SIM card has changed.
+    // Called from the handler ONLY
+    private void maybeRebindService(int subId) {
+        if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            // not specified, replace package on all slots.
+            for (int i = 0; i < mNumSlots; i++) {
+                // get Sub id from Slot Id
+                subId = mSubscriptionManagerProxy.getSubId(i);
+                updateBoundCarrierServices(subId);
+            }
+        } else {
+            updateBoundCarrierServices(subId);
+        }
+
+    }
+
+    private void updateBoundCarrierServices(int subId) {
+        int slotId = mSubscriptionManagerProxy.getSlotIndex(subId);
+        String newPackageName = mCarrierConfigManager.getConfigForSubId(subId).getString(
+                CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null);
+        if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mNumSlots) {
+            String oldPackageName = mCarrierServices[slotId];
+            mCarrierServices[slotId] = newPackageName;
+            if (!TextUtils.equals(newPackageName, oldPackageName)) {
+                Log.i(TAG, "Carrier Config updated, binding new ImsService");
+                // Unbind old ImsService, not needed anymore
+                // ImsService is retrieved from the cache. If the cache hasn't been populated yet,
+                // the calls to unbind/bind will fail (intended during initial start up).
+                unbindImsService(getImsServiceInfoFromCache(oldPackageName));
+                bindNewImsService(getImsServiceInfoFromCache(newPackageName));
+                // Recalculate the device ImsService features to reflect changes.
+                updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
+            }
+        }
+    }
+
+    /**
+     * Returns the ImsServiceInfo that matches the provided packageName. Visible for testing
+     * the ImsService caching functionality.
+     */
+    @VisibleForTesting
+    public ImsServiceInfo getImsServiceInfoFromCache(String packageName) {
+        if (TextUtils.isEmpty(packageName)) {
+            return null;
+        }
+        Optional<ImsServiceInfo> infoFilter = getInfoByPackageName(mInstalledServicesCache,
+                packageName);
+        if (infoFilter.isPresent()) {
+            return infoFilter.get();
+        } else {
+            return null;
+        }
+    }
+
+    // Return the ImsServiceInfo specified for the package name. If the package name is null,
+    // get all packages that support ImsServices.
+    private List<ImsServiceInfo> getImsServiceInfo(String packageName) {
+        List<ImsServiceInfo> infos = new ArrayList<>();
+
+        Intent serviceIntent = new Intent(SERVICE_INTERFACE);
+        serviceIntent.setPackage(packageName);
+
+        PackageManager packageManager = mContext.getPackageManager();
+        for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
+                serviceIntent,
+                PackageManager.GET_META_DATA,
+                mContext.getUserId())) {
+            ServiceInfo serviceInfo = entry.serviceInfo;
+
+            if (serviceInfo != null) {
+                ImsServiceInfo info = new ImsServiceInfo();
+                info.name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
+                info.supportedFeatures = new HashSet<>(ImsFeature.MAX);
+                // Add all supported features
+                if (serviceInfo.metaData != null) {
+                    if (serviceInfo.metaData.getBoolean(METADATA_EMERGENCY_MMTEL_FEATURE, false)) {
+                        info.supportedFeatures.add(ImsFeature.EMERGENCY_MMTEL);
+                    }
+                    if (serviceInfo.metaData.getBoolean(METADATA_MMTEL_FEATURE, false)) {
+                        info.supportedFeatures.add(ImsFeature.MMTEL);
+                    }
+                    if (serviceInfo.metaData.getBoolean(METADATA_RCS_FEATURE, false)) {
+                        info.supportedFeatures.add(ImsFeature.RCS);
+                    }
+                }
+                // Check manifest permission to be sure that the service declares the correct
+                // permissions.
+                if (TextUtils.equals(serviceInfo.permission,
+                        Manifest.permission.BIND_IMS_SERVICE)) {
+                    Log.d(TAG, "ImsService added to cache: " + info.name + " with features: "
+                            + info.supportedFeatures);
+                    infos.add(info);
+                } else {
+                    Log.w(TAG, "ImsService does not have BIND_IMS_SERVICE permission: "
+                            + info.name);
+                }
+            }
+        }
+        return infos;
+    }
+}
diff --git a/com/android/internal/telephony/ims/ImsServiceController.java b/com/android/internal/telephony/ims/ImsServiceController.java
new file mode 100644
index 0000000..6fcefbd
--- /dev/null
+++ b/com/android/internal/telephony/ims/ImsServiceController.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.ims;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.IPackageManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.ims.internal.IImsFeatureStatusCallback;
+import com.android.ims.internal.IImsServiceController;
+import com.android.ims.internal.IImsServiceFeatureListener;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.ExponentialBackoff;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Manages the Binding lifecycle of one ImsService as well as the relevant ImsFeatures that the
+ * ImsService will support.
+ *
+ * When the ImsService is first bound, {@link IImsServiceController#createImsFeature} will be
+ * called
+ * on each feature that the service supports. For each ImsFeature that is created,
+ * {@link ImsServiceControllerCallbacks#imsServiceFeatureCreated} will be called to notify the
+ * listener that the ImsService now supports that feature.
+ *
+ * When {@link #changeImsServiceFeatures} is called with a set of features that is different from
+ * the original set, {@link IImsServiceController#createImsFeature} and
+ * {@link IImsServiceController#removeImsFeature} will be called for each feature that is
+ * created/removed.
+ */
+public class ImsServiceController {
+
+    class ImsDeathRecipient implements IBinder.DeathRecipient {
+
+        private ComponentName mComponentName;
+
+        ImsDeathRecipient(ComponentName name) {
+            mComponentName = name;
+        }
+
+        @Override
+        public void binderDied() {
+            Log.e(LOG_TAG, "ImsService(" + mComponentName + ") died. Restarting...");
+            notifyAllFeaturesRemoved();
+            cleanUpService();
+            startDelayedRebindToService();
+        }
+    }
+
+    class ImsServiceConnection implements ServiceConnection {
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mBackoff.stop();
+            synchronized (mLock) {
+                mIsBound = true;
+                mIsBinding = false;
+                grantPermissionsToService();
+                Log.d(LOG_TAG, "ImsService(" + name + "): onServiceConnected with binder: "
+                        + service);
+                if (service != null) {
+                    mImsDeathRecipient = new ImsDeathRecipient(name);
+                    try {
+                        service.linkToDeath(mImsDeathRecipient, 0);
+                        mImsServiceControllerBinder = service;
+                        mIImsServiceController = IImsServiceController.Stub.asInterface(service);
+                        // create all associated features in the ImsService
+                        for (Pair<Integer, Integer> i : mImsFeatures) {
+                            addImsServiceFeature(i);
+                        }
+                    } catch (RemoteException e) {
+                        mIsBound = false;
+                        mIsBinding = false;
+                        // Remote exception means that the binder already died.
+                        if (mImsDeathRecipient != null) {
+                            mImsDeathRecipient.binderDied();
+                        }
+                        Log.e(LOG_TAG, "ImsService(" + name + ") RemoteException:"
+                                + e.getMessage());
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            synchronized (mLock) {
+                mIsBinding = false;
+            }
+            if (mIImsServiceController != null) {
+                mImsServiceControllerBinder.unlinkToDeath(mImsDeathRecipient, 0);
+            }
+            notifyAllFeaturesRemoved();
+            cleanUpService();
+            Log.w(LOG_TAG, "ImsService(" + name + "): onServiceDisconnected. Rebinding...");
+            startDelayedRebindToService();
+        }
+    }
+
+    /**
+     * Defines callbacks that are used by the ImsServiceController to notify when an ImsService
+     * has created or removed a new feature as well as the associated ImsServiceController.
+     */
+    public interface ImsServiceControllerCallbacks {
+        /**
+         * Called by ImsServiceController when a new feature has been created.
+         */
+        void imsServiceFeatureCreated(int slotId, int feature, ImsServiceController controller);
+        /**
+         * Called by ImsServiceController when a new feature has been removed.
+         */
+        void imsServiceFeatureRemoved(int slotId, int feature, ImsServiceController controller);
+    }
+
+    /**
+     * Returns the currently defined rebind retry timeout. Used for testing.
+     */
+    @VisibleForTesting
+    public interface RebindRetry {
+        /**
+         * Returns a long in ms indicating how long the ImsServiceController should wait before
+         * rebinding for the first time.
+         */
+        long getStartDelay();
+
+        /**
+         * Returns a long in ms indicating the maximum time the ImsServiceController should wait
+         * before rebinding.
+         */
+        long getMaximumDelay();
+    }
+
+    private static final String LOG_TAG = "ImsServiceController";
+    private static final int REBIND_START_DELAY_MS = 2 * 1000; // 2 seconds
+    private static final int REBIND_MAXIMUM_DELAY_MS = 60 * 1000; // 1 minute
+    private final Context mContext;
+    private final ComponentName mComponentName;
+    private final Object mLock = new Object();
+    private final HandlerThread mHandlerThread = new HandlerThread("ImsServiceControllerHandler");
+    private final IPackageManager mPackageManager;
+    private ImsServiceControllerCallbacks mCallbacks;
+    private ExponentialBackoff mBackoff;
+
+    private boolean mIsBound = false;
+    private boolean mIsBinding = false;
+    // Set of a pair of slotId->feature
+    private HashSet<Pair<Integer, Integer>> mImsFeatures;
+    private IImsServiceController mIImsServiceController;
+    // Easier for testing.
+    private IBinder mImsServiceControllerBinder;
+    private ImsServiceConnection mImsServiceConnection;
+    private ImsDeathRecipient mImsDeathRecipient;
+    private Set<IImsServiceFeatureListener> mImsStatusCallbacks = new HashSet<>();
+    // Only added or removed, never accessed on purpose.
+    private Set<ImsFeatureStatusCallback> mFeatureStatusCallbacks = new HashSet<>();
+
+    /**
+     * Container class for the IImsFeatureStatusCallback callback implementation. This class is
+     * never used directly, but we need to keep track of the IImsFeatureStatusCallback
+     * implementations explicitly.
+     */
+    private class ImsFeatureStatusCallback {
+        private int mSlotId;
+        private int mFeatureType;
+
+        private final IImsFeatureStatusCallback mCallback = new IImsFeatureStatusCallback.Stub() {
+
+            @Override
+            public void notifyImsFeatureStatus(int featureStatus) throws RemoteException {
+                Log.i(LOG_TAG, "notifyImsFeatureStatus: slot=" + mSlotId + ", feature="
+                        + mFeatureType + ", status=" + featureStatus);
+                sendImsFeatureStatusChanged(mSlotId, mFeatureType, featureStatus);
+            }
+        };
+
+        ImsFeatureStatusCallback(int slotId, int featureType) {
+            mSlotId = slotId;
+            mFeatureType = featureType;
+        }
+
+        public IImsFeatureStatusCallback getCallback() {
+            return mCallback;
+        }
+    }
+
+    // Retry the bind to the ImsService that has died after mRebindRetry timeout.
+    private Runnable mRestartImsServiceRunnable = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                if (mIsBound) {
+                    return;
+                }
+                bind(mImsFeatures);
+            }
+        }
+    };
+
+    private RebindRetry mRebindRetry = new RebindRetry() {
+        @Override
+        public long getStartDelay() {
+            return REBIND_START_DELAY_MS;
+        }
+
+        @Override
+        public long getMaximumDelay() {
+            return REBIND_MAXIMUM_DELAY_MS;
+        }
+    };
+
+    public ImsServiceController(Context context, ComponentName componentName,
+            ImsServiceControllerCallbacks callbacks) {
+        mContext = context;
+        mComponentName = componentName;
+        mCallbacks = callbacks;
+        mHandlerThread.start();
+        mBackoff = new ExponentialBackoff(
+                mRebindRetry.getStartDelay(),
+                mRebindRetry.getMaximumDelay(),
+                2, /* multiplier */
+                mHandlerThread.getLooper(),
+                mRestartImsServiceRunnable);
+        mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+    }
+
+    @VisibleForTesting
+    // Creating a new HandlerThread and background handler for each test causes a segfault, so for
+    // testing, use a handler supplied by the testing system.
+    public ImsServiceController(Context context, ComponentName componentName,
+            ImsServiceControllerCallbacks callbacks, Handler handler, RebindRetry rebindRetry) {
+        mContext = context;
+        mComponentName = componentName;
+        mCallbacks = callbacks;
+        mBackoff = new ExponentialBackoff(
+                rebindRetry.getStartDelay(),
+                rebindRetry.getMaximumDelay(),
+                2, /* multiplier */
+                handler,
+                mRestartImsServiceRunnable);
+        mPackageManager = null;
+    }
+
+    /**
+     * Sends request to bind to ImsService designated by the {@ComponentName} with the feature set
+     * imsFeatureSet
+     *
+     * @param imsFeatureSet a Set of Pairs that designate the slotId->featureId that need to be
+     *                      created once the service is bound.
+     * @return {@link true} if the service is in the process of being bound, {@link false} if it
+     * has failed.
+     */
+    public boolean bind(HashSet<Pair<Integer, Integer>> imsFeatureSet) {
+        synchronized (mLock) {
+            if (!mIsBound && !mIsBinding) {
+                mIsBinding = true;
+                mImsFeatures = imsFeatureSet;
+                Intent imsServiceIntent = new Intent(ImsResolver.SERVICE_INTERFACE).setComponent(
+                        mComponentName);
+                mImsServiceConnection = new ImsServiceConnection();
+                int serviceFlags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
+                        | Context.BIND_IMPORTANT;
+                Log.i(LOG_TAG, "Binding ImsService:" + mComponentName);
+                try {
+                    return mContext.bindService(imsServiceIntent, mImsServiceConnection,
+                            serviceFlags);
+                } catch (Exception e) {
+                    mBackoff.notifyFailed();
+                    Log.e(LOG_TAG, "Error binding (" + mComponentName + ") with exception: "
+                            + e.getMessage() + ", rebinding in " + mBackoff.getCurrentDelay()
+                            + " ms");
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Calls {@link IImsServiceController#removeImsFeature} on all features that the
+     * ImsService supports and then unbinds the service.
+     */
+    public void unbind() throws RemoteException {
+        synchronized (mLock) {
+            mBackoff.stop();
+            if (mImsServiceConnection == null || mImsDeathRecipient == null) {
+                return;
+            }
+            // Clean up all features
+            changeImsServiceFeatures(new HashSet<>());
+            removeImsServiceFeatureListener();
+            mImsServiceControllerBinder.unlinkToDeath(mImsDeathRecipient, 0);
+            Log.i(LOG_TAG, "Unbinding ImsService: " + mComponentName);
+            mContext.unbindService(mImsServiceConnection);
+            cleanUpService();
+        }
+    }
+
+    /**
+     * Finds the difference between the set of features that the ImsService has active and the new
+     * set defined in newImsFeatures. For every feature that is added,
+     * {@link IImsServiceController#createImsFeature} is called on the service. For every ImsFeature
+     * that is removed, {@link IImsServiceController#removeImsFeature} is called.
+     */
+    public void changeImsServiceFeatures(HashSet<Pair<Integer, Integer>> newImsFeatures)
+            throws RemoteException {
+        synchronized (mLock) {
+            if (mIsBound) {
+                // add features to service.
+                HashSet<Pair<Integer, Integer>> newFeatures = new HashSet<>(newImsFeatures);
+                newFeatures.removeAll(mImsFeatures);
+                for (Pair<Integer, Integer> i : newFeatures) {
+                    addImsServiceFeature(i);
+                }
+                // remove old features
+                HashSet<Pair<Integer, Integer>> oldFeatures = new HashSet<>(mImsFeatures);
+                oldFeatures.removeAll(newImsFeatures);
+                for (Pair<Integer, Integer> i : oldFeatures) {
+                    removeImsServiceFeature(i);
+                }
+            }
+            Log.i(LOG_TAG, "Features changed (" + mImsFeatures + "->" + newImsFeatures + ") for "
+                    + "ImsService: " + mComponentName);
+            mImsFeatures = newImsFeatures;
+        }
+    }
+
+    @VisibleForTesting
+    public IImsServiceController getImsServiceController() {
+        return mIImsServiceController;
+    }
+
+    @VisibleForTesting
+    public IBinder getImsServiceControllerBinder() {
+        return mImsServiceControllerBinder;
+    }
+
+    @VisibleForTesting
+    public long getRebindDelay() {
+        return mBackoff.getCurrentDelay();
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    /**
+     * Add a callback to ImsManager that signals a new feature that the ImsServiceProxy can handle.
+     */
+    public void addImsServiceFeatureListener(IImsServiceFeatureListener callback) {
+        synchronized (mLock) {
+            mImsStatusCallbacks.add(callback);
+        }
+    }
+
+    private void removeImsServiceFeatureListener() {
+        synchronized (mLock) {
+            mImsStatusCallbacks.clear();
+        }
+    }
+
+    // Only add a new rebind if there are no pending rebinds waiting.
+    private void startDelayedRebindToService() {
+        mBackoff.start();
+    }
+
+    // Grant runtime permissions to ImsService. PackageManager ensures that the ImsService is
+    // system/signed before granting permissions.
+    private void grantPermissionsToService() {
+        Log.i(LOG_TAG, "Granting Runtime permissions to:" + getComponentName());
+        String[] pkgToGrant = {mComponentName.getPackageName()};
+        try {
+            if (mPackageManager != null) {
+                mPackageManager.grantDefaultPermissionsToEnabledImsServices(pkgToGrant,
+                        mContext.getUserId());
+            }
+        } catch (RemoteException e) {
+            Log.w(LOG_TAG, "Unable to grant permissions, binder died.");
+        }
+    }
+
+    private void sendImsFeatureCreatedCallback(int slot, int feature) {
+        synchronized (mLock) {
+            for (Iterator<IImsServiceFeatureListener> i = mImsStatusCallbacks.iterator();
+                    i.hasNext(); ) {
+                IImsServiceFeatureListener callbacks = i.next();
+                try {
+                    callbacks.imsFeatureCreated(slot, feature);
+                } catch (RemoteException e) {
+                    // binder died, remove callback.
+                    Log.w(LOG_TAG, "sendImsFeatureCreatedCallback: Binder died, removing "
+                            + "callback. Exception:" + e.getMessage());
+                    i.remove();
+                }
+            }
+        }
+    }
+
+    private void sendImsFeatureRemovedCallback(int slot, int feature) {
+        synchronized (mLock) {
+            for (Iterator<IImsServiceFeatureListener> i = mImsStatusCallbacks.iterator();
+                    i.hasNext(); ) {
+                IImsServiceFeatureListener callbacks = i.next();
+                try {
+                    callbacks.imsFeatureRemoved(slot, feature);
+                } catch (RemoteException e) {
+                    // binder died, remove callback.
+                    Log.w(LOG_TAG, "sendImsFeatureRemovedCallback: Binder died, removing "
+                            + "callback. Exception:" + e.getMessage());
+                    i.remove();
+                }
+            }
+        }
+    }
+
+    private void sendImsFeatureStatusChanged(int slot, int feature, int status) {
+        synchronized (mLock) {
+            for (Iterator<IImsServiceFeatureListener> i = mImsStatusCallbacks.iterator();
+                    i.hasNext(); ) {
+                IImsServiceFeatureListener callbacks = i.next();
+                try {
+                    callbacks.imsStatusChanged(slot, feature, status);
+                } catch (RemoteException e) {
+                    // binder died, remove callback.
+                    Log.w(LOG_TAG, "sendImsFeatureStatusChanged: Binder died, removing "
+                            + "callback. Exception:" + e.getMessage());
+                    i.remove();
+                }
+            }
+        }
+    }
+
+    // This method should only be called when synchronized on mLock
+    private void addImsServiceFeature(Pair<Integer, Integer> featurePair) throws RemoteException {
+        if (mIImsServiceController == null || mCallbacks == null) {
+            Log.w(LOG_TAG, "addImsServiceFeature called with null values.");
+            return;
+        }
+        ImsFeatureStatusCallback c = new ImsFeatureStatusCallback(featurePair.first,
+                featurePair.second);
+        mFeatureStatusCallbacks.add(c);
+        mIImsServiceController.createImsFeature(featurePair.first, featurePair.second,
+                c.getCallback());
+        // Signal ImsResolver to change supported ImsFeatures for this ImsServiceController
+        mCallbacks.imsServiceFeatureCreated(featurePair.first, featurePair.second, this);
+        // Send callback to ImsServiceProxy to change supported ImsFeatures
+        sendImsFeatureCreatedCallback(featurePair.first, featurePair.second);
+    }
+
+    // This method should only be called when synchronized on mLock
+    private void removeImsServiceFeature(Pair<Integer, Integer> featurePair)
+            throws RemoteException {
+        if (mIImsServiceController == null || mCallbacks == null) {
+            Log.w(LOG_TAG, "removeImsServiceFeature called with null values.");
+            return;
+        }
+        ImsFeatureStatusCallback callbackToRemove = mFeatureStatusCallbacks.stream().filter(c ->
+                c.mSlotId == featurePair.first && c.mFeatureType == featurePair.second)
+                .findFirst().orElse(null);
+        // Remove status callbacks from list.
+        if (callbackToRemove != null) {
+            mFeatureStatusCallbacks.remove(callbackToRemove);
+        }
+        mIImsServiceController.removeImsFeature(featurePair.first, featurePair.second,
+                (callbackToRemove != null ? callbackToRemove.getCallback() : null));
+        // Signal ImsResolver to change supported ImsFeatures for this ImsServiceController
+        mCallbacks.imsServiceFeatureRemoved(featurePair.first, featurePair.second, this);
+        // Send callback to ImsServiceProxy to change supported ImsFeatures
+        // Ensure that ImsServiceProxy callback occurs after ImsResolver callback. If an
+        // ImsManager requests the ImsService while it is being removed in ImsResolver, this
+        // callback will clean it up after.
+        sendImsFeatureRemovedCallback(featurePair.first, featurePair.second);
+    }
+
+    private void notifyAllFeaturesRemoved() {
+        if (mCallbacks == null) {
+            Log.w(LOG_TAG, "notifyAllFeaturesRemoved called with invalid callbacks.");
+            return;
+        }
+        synchronized (mLock) {
+            for (Pair<Integer, Integer> feature : mImsFeatures) {
+                mCallbacks.imsServiceFeatureRemoved(feature.first, feature.second, this);
+                sendImsFeatureRemovedCallback(feature.first, feature.second);
+            }
+        }
+    }
+
+    private void cleanUpService() {
+        synchronized (mLock) {
+            mImsDeathRecipient = null;
+            mImsServiceConnection = null;
+            mImsServiceControllerBinder = null;
+            mIImsServiceController = null;
+            mIsBound = false;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsExternalCall.java b/com/android/internal/telephony/imsphone/ImsExternalCall.java
new file mode 100644
index 0000000..b833533
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsExternalCall.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.imsphone;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+
+import java.util.List;
+
+/**
+ * Companion class for {@link ImsExternalConnection}; represents an external call which was
+ * received via {@link com.android.ims.ImsExternalCallState} info.
+ */
+public class ImsExternalCall extends Call {
+
+    private Phone mPhone;
+
+    public ImsExternalCall(Phone phone, ImsExternalConnection connection) {
+        mPhone = phone;
+        mConnections.add(connection);
+    }
+
+    @Override
+    public List<Connection> getConnections() {
+        return mConnections;
+    }
+
+    @Override
+    public Phone getPhone() {
+        return mPhone;
+    }
+
+    @Override
+    public boolean isMultiparty() {
+        return false;
+    }
+
+    @Override
+    public void hangup() throws CallStateException {
+
+    }
+
+    /**
+     * Sets the call state to active.
+     */
+    public void setActive() {
+        setState(State.ACTIVE);
+    }
+
+    /**
+     * Sets the call state to terminated.
+     */
+    public void setTerminated() {
+        setState(State.DISCONNECTED);
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsExternalCallTracker.java b/com/android/internal/telephony/imsphone/ImsExternalCallTracker.java
new file mode 100644
index 0000000..4bea73d
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsExternalCallTracker.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.imsphone;
+
+import com.android.ims.ImsCallProfile;
+import com.android.ims.ImsExternalCallState;
+import com.android.ims.ImsExternalCallStateListener;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Responsible for tracking external calls known to the system.
+ */
+public class ImsExternalCallTracker implements ImsPhoneCallTracker.PhoneStateListener {
+
+    /**
+     * Interface implemented by modules which are capable of notifying interested parties of new
+     * unknown connections, and changes to call state.
+     * This is used to break the dependency between {@link ImsExternalCallTracker} and
+     * {@link ImsPhone}.
+     *
+     * @hide
+     */
+    public static interface ImsCallNotify {
+        /**
+         * Notifies that an unknown connection has been added.
+         * @param c The new unknown connection.
+         */
+        void notifyUnknownConnection(Connection c);
+
+        /**
+         * Notifies of a change to call state.
+         */
+        void notifyPreciseCallStateChanged();
+    }
+
+
+    /**
+     * Implements the {@link ImsExternalCallStateListener}, which is responsible for receiving
+     * external call state updates from the IMS framework.
+     */
+    public class ExternalCallStateListener extends ImsExternalCallStateListener {
+        @Override
+        public void onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState) {
+            refreshExternalCallState(externalCallState);
+        }
+    }
+
+    /**
+     * Receives callbacks from {@link ImsExternalConnection}s when a call pull has been initiated.
+     */
+    public class ExternalConnectionListener implements ImsExternalConnection.Listener {
+        @Override
+        public void onPullExternalCall(ImsExternalConnection connection) {
+            Log.d(TAG, "onPullExternalCall: connection = " + connection);
+            if (mCallPuller == null) {
+                Log.e(TAG, "onPullExternalCall : No call puller defined");
+                return;
+            }
+            mCallPuller.pullExternalCall(connection.getAddress(), connection.getVideoState(),
+                    connection.getCallId());
+        }
+    }
+
+    public final static String TAG = "ImsExternalCallTracker";
+
+    private static final int EVENT_VIDEO_CAPABILITIES_CHANGED = 1;
+
+    /**
+     * Extra key used when informing telecom of a new external call using the
+     * {@link android.telecom.TelecomManager#addNewUnknownCall(PhoneAccountHandle, Bundle)} API.
+     * Used to ensure that when Telecom requests the {@link android.telecom.ConnectionService} to
+     * create the connection for the unknown call that we can determine which
+     * {@link ImsExternalConnection} in {@link #mExternalConnections} is the one being requested.
+     */
+    public final static String EXTRA_IMS_EXTERNAL_CALL_ID =
+            "android.telephony.ImsExternalCallTracker.extra.EXTERNAL_CALL_ID";
+
+    /**
+     * Contains a list of the external connections known by the ImsExternalCallTracker.  These are
+     * connections which originated from a dialog event package and reside on another device.
+     * Used in multi-endpoint (VoLTE for internet connected endpoints) scenarios.
+     */
+    private Map<Integer, ImsExternalConnection> mExternalConnections =
+            new ArrayMap<>();
+
+    /**
+     * Tracks whether each external connection tracked in
+     * {@link #mExternalConnections} can be pulled, as reported by the latest dialog event package
+     * received from the network.  We need to know this because the pull state of a call can be
+     * overridden based on the following factors:
+     * 1) An external video call cannot be pulled if the current device does not have video
+     *    capability.
+     * 2) If the device has any active or held calls locally, no external calls may be pulled to
+     *    the local device.
+     */
+    private Map<Integer, Boolean> mExternalCallPullableState = new ArrayMap<>();
+    private final ImsPhone mPhone;
+    private final ImsCallNotify mCallStateNotifier;
+    private final ExternalCallStateListener mExternalCallStateListener;
+    private final ExternalConnectionListener mExternalConnectionListener =
+            new ExternalConnectionListener();
+    private ImsPullCall mCallPuller;
+    private boolean mIsVideoCapable;
+    private boolean mHasActiveCalls;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_VIDEO_CAPABILITIES_CHANGED:
+                    handleVideoCapabilitiesChanged((AsyncResult) msg.obj);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    @VisibleForTesting
+    public ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller,
+            ImsCallNotify callNotifier) {
+
+        mPhone = phone;
+        mCallStateNotifier = callNotifier;
+        mExternalCallStateListener = new ExternalCallStateListener();
+        mCallPuller = callPuller;
+    }
+
+    public ImsExternalCallTracker(ImsPhone phone) {
+        mPhone = phone;
+        mCallStateNotifier = new ImsCallNotify() {
+            @Override
+            public void notifyUnknownConnection(Connection c) {
+                mPhone.notifyUnknownConnection(c);
+            }
+
+            @Override
+            public void notifyPreciseCallStateChanged() {
+                mPhone.notifyPreciseCallStateChanged();
+            }
+        };
+        mExternalCallStateListener = new ExternalCallStateListener();
+        registerForNotifications();
+    }
+
+    /**
+     * Performs any cleanup required before the ImsExternalCallTracker is destroyed.
+     */
+    public void tearDown() {
+        unregisterForNotifications();
+    }
+
+    /**
+     * Sets the implementation of {@link ImsPullCall} which is responsible for pulling calls.
+     *
+     * @param callPuller The pull call implementation.
+     */
+    public void setCallPuller(ImsPullCall callPuller) {
+       mCallPuller = callPuller;
+    }
+
+    public ExternalCallStateListener getExternalCallStateListener() {
+        return mExternalCallStateListener;
+    }
+
+    /**
+     * Handles changes to the phone state as notified by the {@link ImsPhoneCallTracker}.
+     *
+     * @param oldState The previous phone state.
+     * @param newState The new phone state.
+     */
+    @Override
+    public void onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState) {
+        mHasActiveCalls = newState != PhoneConstants.State.IDLE;
+        Log.i(TAG, "onPhoneStateChanged : hasActiveCalls = " + mHasActiveCalls);
+
+        refreshCallPullState();
+    }
+
+    /**
+     * Registers for video capability changes.
+     */
+    private void registerForNotifications() {
+        if (mPhone != null) {
+            Log.d(TAG, "Registering: " + mPhone);
+            mPhone.getDefaultPhone().registerForVideoCapabilityChanged(mHandler,
+                    EVENT_VIDEO_CAPABILITIES_CHANGED, null);
+        }
+    }
+
+    /**
+     * Unregisters for video capability changes.
+     */
+    private void unregisterForNotifications() {
+        if (mPhone != null) {
+            Log.d(TAG, "Unregistering: " + mPhone);
+            mPhone.unregisterForVideoCapabilityChanged(mHandler);
+        }
+    }
+
+
+    /**
+     * Called when the IMS stack receives a new dialog event package.  Triggers the creation and
+     * update of {@link ImsExternalConnection}s to represent the dialogs in the dialog event
+     * package data.
+     *
+     * @param externalCallStates the {@link ImsExternalCallState} information for the dialog event
+     *                           package.
+     */
+    public void refreshExternalCallState(List<ImsExternalCallState> externalCallStates) {
+        Log.d(TAG, "refreshExternalCallState");
+
+        // Check to see if any call Ids are no longer present in the external call state.  If they
+        // are, the calls are terminated and should be removed.
+        Iterator<Map.Entry<Integer, ImsExternalConnection>> connectionIterator =
+                mExternalConnections.entrySet().iterator();
+        boolean wasCallRemoved = false;
+        while (connectionIterator.hasNext()) {
+            Map.Entry<Integer, ImsExternalConnection> entry = connectionIterator.next();
+            int callId = entry.getKey().intValue();
+
+            if (!containsCallId(externalCallStates, callId)) {
+                ImsExternalConnection externalConnection = entry.getValue();
+                externalConnection.setTerminated();
+                externalConnection.removeListener(mExternalConnectionListener);
+                connectionIterator.remove();
+                wasCallRemoved = true;
+            }
+        }
+        // If one or more calls were removed, trigger a notification that will cause the
+        // TelephonyConnection instancse to refresh their state with Telecom.
+        if (wasCallRemoved) {
+            mCallStateNotifier.notifyPreciseCallStateChanged();
+        }
+
+        // Check for new calls, and updates to existing ones.
+        if (externalCallStates != null && !externalCallStates.isEmpty()) {
+            for (ImsExternalCallState callState : externalCallStates) {
+                if (!mExternalConnections.containsKey(callState.getCallId())) {
+                    Log.d(TAG, "refreshExternalCallState: got = " + callState);
+                    // If there is a new entry and it is already terminated, don't bother adding it to
+                    // telecom.
+                    if (callState.getCallState() != ImsExternalCallState.CALL_STATE_CONFIRMED) {
+                        continue;
+                    }
+                    createExternalConnection(callState);
+                } else {
+                    updateExistingConnection(mExternalConnections.get(callState.getCallId()),
+                            callState);
+                }
+            }
+        }
+    }
+
+    /**
+     * Finds an external connection given a call Id.
+     *
+     * @param callId The call Id.
+     * @return The {@link Connection}, or {@code null} if no match found.
+     */
+    public Connection getConnectionById(int callId) {
+        return mExternalConnections.get(callId);
+    }
+
+    /**
+     * Given an {@link ImsExternalCallState} instance obtained from a dialog event package,
+     * creates a new instance of {@link ImsExternalConnection} to represent the connection, and
+     * initiates the addition of the new call to Telecom as an unknown call.
+     *
+     * @param state External call state from a dialog event package.
+     */
+    private void createExternalConnection(ImsExternalCallState state) {
+        Log.i(TAG, "createExternalConnection : state = " + state);
+
+        int videoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
+
+        boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), videoState);
+        ImsExternalConnection connection = new ImsExternalConnection(mPhone,
+                state.getCallId(), /* Dialog event package call id */
+                state.getAddress() /* phone number */,
+                isCallPullPermitted);
+        connection.setVideoState(videoState);
+        connection.addListener(mExternalConnectionListener);
+
+        Log.d(TAG,
+                "createExternalConnection - pullable state : externalCallId = "
+                        + connection.getCallId()
+                        + " ; isPullable = " + isCallPullPermitted
+                        + " ; networkPullable = " + state.isCallPullable()
+                        + " ; isVideo = " + VideoProfile.isVideo(videoState)
+                        + " ; videoEnabled = " + mIsVideoCapable
+                        + " ; hasActiveCalls = " + mHasActiveCalls);
+
+        // Add to list of tracked connections.
+        mExternalConnections.put(connection.getCallId(), connection);
+        mExternalCallPullableState.put(connection.getCallId(), state.isCallPullable());
+
+        // Note: The notification of unknown connection is ultimately handled by
+        // PstnIncomingCallNotifier#addNewUnknownCall.  That method will ensure that an extra is set
+        // containing the ImsExternalConnection#mCallId so that we have a means of reconciling which
+        // unknown call was added.
+        mCallStateNotifier.notifyUnknownConnection(connection);
+    }
+
+    /**
+     * Given an existing {@link ImsExternalConnection}, applies any changes found found in a
+     * {@link ImsExternalCallState} instance received from a dialog event package to the connection.
+     *
+     * @param connection The connection to apply changes to.
+     * @param state The new dialog state for the connection.
+     */
+    private void updateExistingConnection(ImsExternalConnection connection,
+            ImsExternalCallState state) {
+
+        Log.i(TAG, "updateExistingConnection : state = " + state);
+        Call.State existingState = connection.getState();
+        Call.State newState = state.getCallState() == ImsExternalCallState.CALL_STATE_CONFIRMED ?
+                Call.State.ACTIVE : Call.State.DISCONNECTED;
+
+        if (existingState != newState) {
+            if (newState == Call.State.ACTIVE) {
+                connection.setActive();
+            } else {
+                connection.setTerminated();
+                connection.removeListener(mExternalConnectionListener);
+                mExternalConnections.remove(connection.getCallId());
+                mExternalCallPullableState.remove(connection.getCallId());
+                mCallStateNotifier.notifyPreciseCallStateChanged();
+            }
+        }
+
+        int newVideoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
+        if (newVideoState != connection.getVideoState()) {
+            connection.setVideoState(newVideoState);
+        }
+
+        mExternalCallPullableState.put(state.getCallId(), state.isCallPullable());
+        boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), newVideoState);
+        Log.d(TAG,
+                "updateExistingConnection - pullable state : externalCallId = " + connection
+                        .getCallId()
+                        + " ; isPullable = " + isCallPullPermitted
+                        + " ; networkPullable = " + state.isCallPullable()
+                        + " ; isVideo = "
+                        + VideoProfile.isVideo(connection.getVideoState())
+                        + " ; videoEnabled = " + mIsVideoCapable
+                        + " ; hasActiveCalls = " + mHasActiveCalls);
+
+        connection.setIsPullable(isCallPullPermitted);
+    }
+
+    /**
+     * Update whether the external calls known can be pulled.  Combines the last known network
+     * pullable state with local device conditions to determine if each call can be pulled.
+     */
+    private void refreshCallPullState() {
+        Log.d(TAG, "refreshCallPullState");
+
+        for (ImsExternalConnection imsExternalConnection : mExternalConnections.values()) {
+            boolean isNetworkPullable =
+                    mExternalCallPullableState.get(imsExternalConnection.getCallId())
+                            .booleanValue();
+            boolean isCallPullPermitted =
+                    isCallPullPermitted(isNetworkPullable, imsExternalConnection.getVideoState());
+            Log.d(TAG,
+                    "refreshCallPullState : externalCallId = " + imsExternalConnection.getCallId()
+                            + " ; isPullable = " + isCallPullPermitted
+                            + " ; networkPullable = " + isNetworkPullable
+                            + " ; isVideo = "
+                            + VideoProfile.isVideo(imsExternalConnection.getVideoState())
+                            + " ; videoEnabled = " + mIsVideoCapable
+                            + " ; hasActiveCalls = " + mHasActiveCalls);
+            imsExternalConnection.setIsPullable(isCallPullPermitted);
+        }
+    }
+
+    /**
+     * Determines if a list of call states obtained from a dialog event package contacts an existing
+     * call Id.
+     *
+     * @param externalCallStates The dialog event package state information.
+     * @param callId The call Id.
+     * @return {@code true} if the state information contains the call Id, {@code false} otherwise.
+     */
+    private boolean containsCallId(List<ImsExternalCallState> externalCallStates, int callId) {
+        if (externalCallStates == null) {
+            return false;
+        }
+
+        for (ImsExternalCallState state : externalCallStates) {
+            if (state.getCallId() == callId) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Handles a change to the video capabilities reported by
+     * {@link Phone#notifyForVideoCapabilityChanged(boolean)}.
+     *
+     * @param ar The AsyncResult containing the new video capability of the device.
+     */
+    private void handleVideoCapabilitiesChanged(AsyncResult ar) {
+        mIsVideoCapable = (Boolean) ar.result;
+        Log.i(TAG, "handleVideoCapabilitiesChanged : isVideoCapable = " + mIsVideoCapable);
+
+        // Refresh pullable state if video capability changed.
+        refreshCallPullState();
+    }
+
+    /**
+     * Determines whether an external call can be pulled based on the pullability state enforced
+     * by the network, as well as local device rules.
+     *
+     * @param isNetworkPullable {@code true} if the network indicates the call can be pulled,
+     *      {@code false} otherwise.
+     * @param videoState the VideoState of the external call.
+     * @return {@code true} if the external call can be pulled, {@code false} otherwise.
+     */
+    private boolean isCallPullPermitted(boolean isNetworkPullable, int videoState) {
+        if (VideoProfile.isVideo(videoState) && !mIsVideoCapable) {
+            // If the external call is a video call and the local device does not have video
+            // capability at this time, it cannot be pulled.
+            return false;
+        }
+
+        if (mHasActiveCalls) {
+            // If there are active calls on the local device, the call cannot be pulled.
+            return false;
+        }
+
+        return isNetworkPullable;
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsExternalConnection.java b/com/android/internal/telephony/imsphone/ImsExternalConnection.java
new file mode 100644
index 0000000..071aebb
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsExternalConnection.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.imsphone;
+
+import com.android.internal.R;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.UUSInfo;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Represents an IMS call external to the device.  This class is used to represent a call which
+ * takes places on a secondary device associated with this one.  Originates from a Dialog Event
+ * Package.
+ *
+ * Dialog event package information is received from the IMS framework via
+ * {@link com.android.ims.ImsExternalCallState} instances.
+ *
+ * @hide
+ */
+public class ImsExternalConnection extends Connection {
+
+    private static final String CONFERENCE_PREFIX = "conf";
+    private final Context mContext;
+
+    public interface Listener {
+        void onPullExternalCall(ImsExternalConnection connection);
+    }
+
+    /**
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+    /**
+     * The unqiue dialog event package specified ID associated with this external connection.
+     */
+    private int mCallId;
+
+    /**
+     * A backing call associated with this external connection.
+     */
+    private ImsExternalCall mCall;
+
+    /**
+     * The original address as contained in the dialog event package.
+     */
+    private Uri mOriginalAddress;
+
+    /**
+     * Determines if the call is pullable.
+     */
+    private boolean mIsPullable;
+
+    protected ImsExternalConnection(Phone phone, int callId, Uri address, boolean isPullable) {
+        super(phone.getPhoneType());
+        mContext = phone.getContext();
+        mCall = new ImsExternalCall(phone, this);
+        mCallId = callId;
+        setExternalConnectionAddress(address);
+        mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
+        mIsPullable = isPullable;
+
+        rebuildCapabilities();
+        setActive();
+    }
+
+    /**
+     * @return the unique ID of this connection from the dialog event package data.
+     */
+    public int getCallId() {
+        return mCallId;
+    }
+
+    @Override
+    public Call getCall() {
+        return mCall;
+    }
+
+    @Override
+    public long getDisconnectTime() {
+        return 0;
+    }
+
+    @Override
+    public long getHoldDurationMillis() {
+        return 0;
+    }
+
+    @Override
+    public String getVendorDisconnectCause() {
+        return null;
+    }
+
+    @Override
+    public void hangup() throws CallStateException {
+        // No-op - Hangup is not supported for external calls.
+    }
+
+    @Override
+    public void separate() throws CallStateException {
+        // No-op - Separate is not supported for external calls.
+    }
+
+    @Override
+    public void proceedAfterWaitChar() {
+        // No-op - not supported for external calls.
+    }
+
+    @Override
+    public void proceedAfterWildChar(String str) {
+        // No-op - not supported for external calls.
+    }
+
+    @Override
+    public void cancelPostDial() {
+        // No-op - not supported for external calls.
+    }
+
+    @Override
+    public int getNumberPresentation() {
+        return mNumberPresentation;
+    }
+
+    @Override
+    public UUSInfo getUUSInfo() {
+        return null;
+    }
+
+    @Override
+    public int getPreciseDisconnectCause() {
+        return 0;
+    }
+
+    @Override
+    public boolean isMultiparty() {
+        return false;
+    }
+
+    /**
+     * Called by a {@link android.telecom.Connection} to indicate that this call should be pulled
+     * to the local device.
+     *
+     * Informs all listeners, in this case {@link ImsExternalCallTracker}, of the request to pull
+     * the call.
+     */
+    @Override
+    public void pullExternalCall() {
+        for (Listener listener : mListeners) {
+            listener.onPullExternalCall(this);
+        }
+    }
+
+    /**
+     * Sets this external call as active.
+     */
+    public void setActive() {
+        if (mCall == null) {
+            return;
+        }
+        mCall.setActive();
+    }
+
+    /**
+     * Sets this external call as terminated.
+     */
+    public void setTerminated() {
+        if (mCall == null) {
+            return;
+        }
+
+        mCall.setTerminated();
+    }
+
+    /**
+     * Changes whether the call can be pulled or not.
+     *
+     * @param isPullable {@code true} if the call can be pulled, {@code false} otherwise.
+     */
+    public void setIsPullable(boolean isPullable) {
+        mIsPullable = isPullable;
+        rebuildCapabilities();
+    }
+
+    /**
+     * Sets the address of this external connection.  Ensures that dialog event package SIP
+     * {@link Uri}s are converted to a regular telephone number.
+     *
+     * @param address The address from the dialog event package.
+     */
+    public void setExternalConnectionAddress(Uri address) {
+        mOriginalAddress = address;
+
+        if (PhoneAccount.SCHEME_SIP.equals(address.getScheme())) {
+            if (address.getSchemeSpecificPart().startsWith(CONFERENCE_PREFIX)) {
+                mCnapName = mContext.getString(com.android.internal.R.string.conference_call);
+                mCnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED;
+                mAddress = "";
+                mNumberPresentation = PhoneConstants.PRESENTATION_RESTRICTED;
+                return;
+            }
+        }
+        Uri telUri = PhoneNumberUtils.convertSipUriToTelUri(address);
+        mAddress = telUri.getSchemeSpecificPart();
+    }
+
+    public void addListener(Listener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeListener(Listener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Build a human representation of a connection instance, suitable for debugging.
+     * Don't log personal stuff unless in debug mode.
+     * @return a string representing the internal state of this connection.
+     */
+    public String toString() {
+        StringBuilder str = new StringBuilder(128);
+        str.append("[ImsExternalConnection dialogCallId:");
+        str.append(mCallId);
+        str.append(" state:");
+        if (mCall.getState() == Call.State.ACTIVE) {
+            str.append("Active");
+        } else if (mCall.getState() == Call.State.DISCONNECTED) {
+            str.append("Disconnected");
+        }
+        str.append("]");
+        return str.toString();
+    }
+
+    /**
+     * Rebuilds the connection capabilities.
+     */
+    private void rebuildCapabilities() {
+        int capabilities = Capability.IS_EXTERNAL_CONNECTION;
+        if (mIsPullable) {
+            capabilities |= Capability.IS_PULLABLE;
+        }
+
+        setConnectionCapabilities(capabilities);
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhone.java b/com/android/internal/telephony/imsphone/ImsPhone.java
new file mode 100644
index 0000000..45dc0b2
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhone.java
@@ -0,0 +1,1762 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BAIC;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BAICr;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BAOC;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BAOIC;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BAOICxH;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BA_ALL;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BA_MO;
+import static com.android.internal.telephony.CommandsInterface.CB_FACILITY_BA_MT;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_DISABLE;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_ENABLE;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_ERASURE;
+import static com.android.internal.telephony.CommandsInterface.CF_ACTION_REGISTRATION;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_ALL;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_ALL_CONDITIONAL;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_BUSY;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_NOT_REACHABLE;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_NO_REPLY;
+import static com.android.internal.telephony.CommandsInterface.CF_REASON_UNCONDITIONAL;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_NONE;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_VOICE;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkStats;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.ResultReceiver;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.telecom.VideoProfile;
+import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UssdResponse;
+import android.text.TextUtils;
+
+import com.android.ims.ImsCallForwardInfo;
+import com.android.ims.ImsCallProfile;
+import com.android.ims.ImsEcbm;
+import com.android.ims.ImsEcbmStateListener;
+import com.android.ims.ImsException;
+import com.android.ims.ImsManager;
+import com.android.ims.ImsReasonInfo;
+import com.android.ims.ImsSsInfo;
+import com.android.ims.ImsUtInterface;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.CallTracker;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneNotifier;
+import com.android.internal.telephony.TelephonyComponentFactory;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.util.NotificationChannelController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public class ImsPhone extends ImsPhoneBase {
+    private static final String LOG_TAG = "ImsPhone";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    private static final int EVENT_SET_CALL_BARRING_DONE             = EVENT_LAST + 1;
+    private static final int EVENT_GET_CALL_BARRING_DONE             = EVENT_LAST + 2;
+    private static final int EVENT_SET_CALL_WAITING_DONE             = EVENT_LAST + 3;
+    private static final int EVENT_GET_CALL_WAITING_DONE             = EVENT_LAST + 4;
+    private static final int EVENT_SET_CLIR_DONE                     = EVENT_LAST + 5;
+    private static final int EVENT_GET_CLIR_DONE                     = EVENT_LAST + 6;
+    private static final int EVENT_DEFAULT_PHONE_DATA_STATE_CHANGED  = EVENT_LAST + 7;
+    private static final int EVENT_SERVICE_STATE_CHANGED             = EVENT_LAST + 8;
+    private static final int EVENT_VOICE_CALL_ENDED                  = EVENT_LAST + 9;
+
+    static final int RESTART_ECM_TIMER = 0; // restart Ecm timer
+    static final int CANCEL_ECM_TIMER  = 1; // cancel Ecm timer
+
+    // Default Emergency Callback Mode exit timer
+    private static final int DEFAULT_ECM_EXIT_TIMER_VALUE = 300000;
+
+    // Instance Variables
+    Phone mDefaultPhone;
+    ImsPhoneCallTracker mCT;
+    ImsExternalCallTracker mExternalCallTracker;
+    private ArrayList <ImsPhoneMmiCode> mPendingMMIs = new ArrayList<ImsPhoneMmiCode>();
+    private ServiceState mSS = new ServiceState();
+
+    // To redial silently through GSM or CDMA when dialing through IMS fails
+    private String mLastDialString;
+
+    private WakeLock mWakeLock;
+
+    // mEcmExitRespRegistrant is informed after the phone has been exited the emergency
+    // callback mode keep track of if phone is in emergency callback mode
+    private Registrant mEcmExitRespRegistrant;
+
+    private final RegistrantList mSilentRedialRegistrants = new RegistrantList();
+
+    private boolean mImsRegistered = false;
+
+    private boolean mRoaming = false;
+
+    // List of Registrants to send supplementary service notifications to.
+    private RegistrantList mSsnRegistrants = new RegistrantList();
+
+    // A runnable which is used to automatically exit from Ecm after a period of time.
+    private Runnable mExitEcmRunnable = new Runnable() {
+        @Override
+        public void run() {
+            exitEmergencyCallbackMode();
+        }
+    };
+
+    private Uri[] mCurrentSubscriberUris;
+
+    protected void setCurrentSubscriberUris(Uri[] currentSubscriberUris) {
+        this.mCurrentSubscriberUris = currentSubscriberUris;
+    }
+
+    @Override
+    public Uri[] getCurrentSubscriberUris() {
+        return mCurrentSubscriberUris;
+    }
+
+    // Create Cf (Call forward) so that dialling number &
+    // mIsCfu (true if reason is call forward unconditional)
+    // mOnComplete (Message object passed by client) can be packed &
+    // given as a single Cf object as user data to UtInterface.
+    private static class Cf {
+        final String mSetCfNumber;
+        final Message mOnComplete;
+        final boolean mIsCfu;
+
+        Cf(String cfNumber, boolean isCfu, Message onComplete) {
+            mSetCfNumber = cfNumber;
+            mIsCfu = isCfu;
+            mOnComplete = onComplete;
+        }
+    }
+
+    // Constructors
+    public ImsPhone(Context context, PhoneNotifier notifier, Phone defaultPhone) {
+        this(context, notifier, defaultPhone, false);
+    }
+
+    @VisibleForTesting
+    public ImsPhone(Context context, PhoneNotifier notifier, Phone defaultPhone,
+                    boolean unitTestMode) {
+        super("ImsPhone", context, notifier, unitTestMode);
+
+        mDefaultPhone = defaultPhone;
+        // The ImsExternalCallTracker needs to be defined before the ImsPhoneCallTracker, as the
+        // ImsPhoneCallTracker uses a thread to spool up the ImsManager.  Part of this involves
+        // setting the multiendpoint listener on the external call tracker.  So we need to ensure
+        // the external call tracker is available first to avoid potential timing issues.
+        mExternalCallTracker =
+                TelephonyComponentFactory.getInstance().makeImsExternalCallTracker(this);
+        mCT = TelephonyComponentFactory.getInstance().makeImsPhoneCallTracker(this);
+        mCT.registerPhoneStateListener(mExternalCallTracker);
+        mExternalCallTracker.setCallPuller(mCT);
+
+        mSS.setStateOff();
+
+        mPhoneId = mDefaultPhone.getPhoneId();
+
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+        mWakeLock.setReferenceCounted(false);
+
+        if (mDefaultPhone.getServiceStateTracker() != null) {
+            mDefaultPhone.getServiceStateTracker()
+                    .registerForDataRegStateOrRatChanged(this,
+                            EVENT_DEFAULT_PHONE_DATA_STATE_CHANGED, null);
+        }
+        // Sets the Voice reg state to STATE_OUT_OF_SERVICE and also queries the data service
+        // state. We don't ever need the voice reg state to be anything other than in or out of
+        // service.
+        setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
+
+        mDefaultPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
+        // Force initial roaming state update later, on EVENT_CARRIER_CONFIG_CHANGED.
+        // Settings provider or CarrierConfig may not be loaded now.
+    }
+
+    //todo: get rid of this function. It is not needed since parentPhone obj never changes
+    @Override
+    public void dispose() {
+        Rlog.d(LOG_TAG, "dispose");
+        // Nothing to dispose in Phone
+        //super.dispose();
+        mPendingMMIs.clear();
+        mExternalCallTracker.tearDown();
+        mCT.unregisterPhoneStateListener(mExternalCallTracker);
+        mCT.unregisterForVoiceCallEnded(this);
+        mCT.dispose();
+
+        //Force all referenced classes to unregister their former registered events
+        if (mDefaultPhone != null && mDefaultPhone.getServiceStateTracker() != null) {
+            mDefaultPhone.getServiceStateTracker().
+                    unregisterForDataRegStateOrRatChanged(this);
+            mDefaultPhone.unregisterForServiceStateChanged(this);
+        }
+    }
+
+    @Override
+    public ServiceState getServiceState() {
+        return mSS;
+    }
+
+    @VisibleForTesting
+    public void setServiceState(int state) {
+        boolean isVoiceRegStateChanged = false;
+        synchronized (this) {
+            isVoiceRegStateChanged = mSS.getVoiceRegState() != state;
+            mSS.setVoiceRegState(state);
+        }
+        updateDataServiceState();
+
+        // Notifies the service state to the listeners. The service state combined from ImsPhone
+        // and GsmCdmaPhone, it may be changed when the service state in ImsPhone is changed.
+        if (isVoiceRegStateChanged) {
+            mNotifier.notifyServiceState(mDefaultPhone);
+        }
+    }
+
+    @Override
+    public CallTracker getCallTracker() {
+        return mCT;
+    }
+
+    public ImsExternalCallTracker getExternalCallTracker() {
+        return mExternalCallTracker;
+    }
+
+    @Override
+    public List<? extends ImsPhoneMmiCode>
+    getPendingMmiCodes() {
+        return mPendingMMIs;
+    }
+
+    @Override
+    public void
+    acceptCall(int videoState) throws CallStateException {
+        mCT.acceptCall(videoState);
+    }
+
+    @Override
+    public void
+    rejectCall() throws CallStateException {
+        mCT.rejectCall();
+    }
+
+    @Override
+    public void
+    switchHoldingAndActive() throws CallStateException {
+        mCT.switchWaitingOrHoldingAndActive();
+    }
+
+    @Override
+    public boolean canConference() {
+        return mCT.canConference();
+    }
+
+    public boolean canDial() {
+        return mCT.canDial();
+    }
+
+    @Override
+    public void conference() {
+        mCT.conference();
+    }
+
+    @Override
+    public void clearDisconnected() {
+        mCT.clearDisconnected();
+    }
+
+    @Override
+    public boolean canTransfer() {
+        return mCT.canTransfer();
+    }
+
+    @Override
+    public void explicitCallTransfer() {
+        mCT.explicitCallTransfer();
+    }
+
+    @Override
+    public ImsPhoneCall
+    getForegroundCall() {
+        return mCT.mForegroundCall;
+    }
+
+    @Override
+    public ImsPhoneCall
+    getBackgroundCall() {
+        return mCT.mBackgroundCall;
+    }
+
+    @Override
+    public ImsPhoneCall
+    getRingingCall() {
+        return mCT.mRingingCall;
+    }
+
+    @Override
+    public boolean isImsAvailable() {
+        return mCT.isImsServiceReady();
+    }
+
+    private boolean handleCallDeflectionIncallSupplementaryService(
+            String dialString) {
+        if (dialString.length() > 1) {
+            return false;
+        }
+
+        if (getRingingCall().getState() != ImsPhoneCall.State.IDLE) {
+            if (DBG) Rlog.d(LOG_TAG, "MmiCode 0: rejectCall");
+            try {
+                mCT.rejectCall();
+            } catch (CallStateException e) {
+                if (DBG) Rlog.d(LOG_TAG, "reject failed", e);
+                notifySuppServiceFailed(Phone.SuppService.REJECT);
+            }
+        } else if (getBackgroundCall().getState() != ImsPhoneCall.State.IDLE) {
+            if (DBG) Rlog.d(LOG_TAG, "MmiCode 0: hangupWaitingOrBackground");
+            try {
+                mCT.hangup(getBackgroundCall());
+            } catch (CallStateException e) {
+                if (DBG) Rlog.d(LOG_TAG, "hangup failed", e);
+            }
+        }
+
+        return true;
+    }
+
+    private void sendUssdResponse(String ussdRequest, CharSequence message, int returnCode,
+                                   ResultReceiver wrappedCallback) {
+        UssdResponse response = new UssdResponse(ussdRequest, message);
+        Bundle returnData = new Bundle();
+        returnData.putParcelable(TelephonyManager.USSD_RESPONSE, response);
+        wrappedCallback.send(returnCode, returnData);
+
+    }
+
+    @Override
+    public boolean handleUssdRequest(String ussdRequest, ResultReceiver wrappedCallback)
+            throws CallStateException {
+        if (mPendingMMIs.size() > 0) {
+            // There are MMI codes in progress; fail attempt now.
+            Rlog.i(LOG_TAG, "handleUssdRequest: queue full: " + Rlog.pii(LOG_TAG, ussdRequest));
+            sendUssdResponse(ussdRequest, null, TelephonyManager.USSD_RETURN_FAILURE,
+                    wrappedCallback );
+            return true;
+        }
+        try {
+            dialInternal(ussdRequest, VideoProfile.STATE_AUDIO_ONLY, null, wrappedCallback);
+        } catch (CallStateException cse) {
+            if (CS_FALLBACK.equals(cse.getMessage())) {
+                throw cse;
+            } else {
+                Rlog.w(LOG_TAG, "Could not execute USSD " + cse);
+                sendUssdResponse(ussdRequest, null, TelephonyManager.USSD_RETURN_FAILURE,
+                        wrappedCallback);
+            }
+        } catch (Exception e) {
+            Rlog.w(LOG_TAG, "Could not execute USSD " + e);
+            sendUssdResponse(ussdRequest, null, TelephonyManager.USSD_RETURN_FAILURE,
+                    wrappedCallback);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean handleCallWaitingIncallSupplementaryService(
+            String dialString) {
+        int len = dialString.length();
+
+        if (len > 2) {
+            return false;
+        }
+
+        ImsPhoneCall call = getForegroundCall();
+
+        try {
+            if (len > 1) {
+                if (DBG) Rlog.d(LOG_TAG, "not support 1X SEND");
+                notifySuppServiceFailed(Phone.SuppService.HANGUP);
+            } else {
+                if (call.getState() != ImsPhoneCall.State.IDLE) {
+                    if (DBG) Rlog.d(LOG_TAG, "MmiCode 1: hangup foreground");
+                    mCT.hangup(call);
+                } else {
+                    if (DBG) Rlog.d(LOG_TAG, "MmiCode 1: switchWaitingOrHoldingAndActive");
+                    mCT.switchWaitingOrHoldingAndActive();
+                }
+            }
+        } catch (CallStateException e) {
+            if (DBG) Rlog.d(LOG_TAG, "hangup failed", e);
+            notifySuppServiceFailed(Phone.SuppService.HANGUP);
+        }
+
+        return true;
+    }
+
+    private boolean handleCallHoldIncallSupplementaryService(String dialString) {
+        int len = dialString.length();
+
+        if (len > 2) {
+            return false;
+        }
+
+        if (len > 1) {
+            if (DBG) Rlog.d(LOG_TAG, "separate not supported");
+            notifySuppServiceFailed(Phone.SuppService.SEPARATE);
+        } else {
+            try {
+                if (getRingingCall().getState() != ImsPhoneCall.State.IDLE) {
+                    if (DBG) Rlog.d(LOG_TAG, "MmiCode 2: accept ringing call");
+                    mCT.acceptCall(ImsCallProfile.CALL_TYPE_VOICE);
+                } else {
+                    if (DBG) Rlog.d(LOG_TAG, "MmiCode 2: switchWaitingOrHoldingAndActive");
+                    mCT.switchWaitingOrHoldingAndActive();
+                }
+            } catch (CallStateException e) {
+                if (DBG) Rlog.d(LOG_TAG, "switch failed", e);
+                notifySuppServiceFailed(Phone.SuppService.SWITCH);
+            }
+        }
+
+        return true;
+    }
+
+    private boolean handleMultipartyIncallSupplementaryService(
+            String dialString) {
+        if (dialString.length() > 1) {
+            return false;
+        }
+
+        if (DBG) Rlog.d(LOG_TAG, "MmiCode 3: merge calls");
+        conference();
+        return true;
+    }
+
+    private boolean handleEctIncallSupplementaryService(String dialString) {
+
+        int len = dialString.length();
+
+        if (len != 1) {
+            return false;
+        }
+
+        if (DBG) Rlog.d(LOG_TAG, "MmiCode 4: not support explicit call transfer");
+        notifySuppServiceFailed(Phone.SuppService.TRANSFER);
+        return true;
+    }
+
+    private boolean handleCcbsIncallSupplementaryService(String dialString) {
+        if (dialString.length() > 1) {
+            return false;
+        }
+
+        Rlog.i(LOG_TAG, "MmiCode 5: CCBS not supported!");
+        // Treat it as an "unknown" service.
+        notifySuppServiceFailed(Phone.SuppService.UNKNOWN);
+        return true;
+    }
+
+    public void notifySuppSvcNotification(SuppServiceNotification suppSvc) {
+        Rlog.d(LOG_TAG, "notifySuppSvcNotification: suppSvc = " + suppSvc);
+
+        AsyncResult ar = new AsyncResult(null, suppSvc, null);
+        mSsnRegistrants.notifyRegistrants(ar);
+    }
+
+    @Override
+    public boolean handleInCallMmiCommands(String dialString) {
+        if (!isInCall()) {
+            return false;
+        }
+
+        if (TextUtils.isEmpty(dialString)) {
+            return false;
+        }
+
+        boolean result = false;
+        char ch = dialString.charAt(0);
+        switch (ch) {
+            case '0':
+                result = handleCallDeflectionIncallSupplementaryService(
+                        dialString);
+                break;
+            case '1':
+                result = handleCallWaitingIncallSupplementaryService(
+                        dialString);
+                break;
+            case '2':
+                result = handleCallHoldIncallSupplementaryService(dialString);
+                break;
+            case '3':
+                result = handleMultipartyIncallSupplementaryService(dialString);
+                break;
+            case '4':
+                result = handleEctIncallSupplementaryService(dialString);
+                break;
+            case '5':
+                result = handleCcbsIncallSupplementaryService(dialString);
+                break;
+            default:
+                break;
+        }
+
+        return result;
+    }
+
+    boolean isInCall() {
+        ImsPhoneCall.State foregroundCallState = getForegroundCall().getState();
+        ImsPhoneCall.State backgroundCallState = getBackgroundCall().getState();
+        ImsPhoneCall.State ringingCallState = getRingingCall().getState();
+
+       return (foregroundCallState.isAlive() ||
+               backgroundCallState.isAlive() ||
+               ringingCallState.isAlive());
+    }
+
+    @Override
+    public boolean isInEcm() {
+        return mDefaultPhone.isInEcm();
+    }
+
+    @Override
+    public void setIsInEcm(boolean isInEcm){
+        mDefaultPhone.setIsInEcm(isInEcm);
+    }
+
+    public void notifyNewRingingConnection(Connection c) {
+        mDefaultPhone.notifyNewRingingConnectionP(c);
+    }
+
+    void notifyUnknownConnection(Connection c) {
+        mDefaultPhone.notifyUnknownConnectionP(c);
+    }
+
+    @Override
+    public void notifyForVideoCapabilityChanged(boolean isVideoCapable) {
+        mIsVideoCapable = isVideoCapable;
+        mDefaultPhone.notifyForVideoCapabilityChanged(isVideoCapable);
+    }
+
+    @Override
+    public Connection
+    dial(String dialString, int videoState) throws CallStateException {
+        return dialInternal(dialString, videoState, null, null);
+    }
+
+    @Override
+    public Connection
+    dial(String dialString, UUSInfo uusInfo, int videoState, Bundle intentExtras)
+            throws CallStateException {
+        // ignore UUSInfo
+        return dialInternal (dialString, videoState, intentExtras, null);
+    }
+
+    protected Connection dialInternal(String dialString, int videoState, Bundle intentExtras)
+            throws CallStateException {
+        return dialInternal(dialString, videoState, intentExtras, null);
+    }
+
+    private Connection dialInternal(String dialString, int videoState,
+                                    Bundle intentExtras, ResultReceiver wrappedCallback)
+            throws CallStateException {
+        // Need to make sure dialString gets parsed properly
+        String newDialString = PhoneNumberUtils.stripSeparators(dialString);
+
+        // handle in-call MMI first if applicable
+        if (handleInCallMmiCommands(newDialString)) {
+            return null;
+        }
+
+        if (mDefaultPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+            return mCT.dial(dialString, videoState, intentExtras);
+        }
+
+        // Only look at the Network portion for mmi
+        String networkPortion = PhoneNumberUtils.extractNetworkPortionAlt(newDialString);
+        ImsPhoneMmiCode mmi =
+                ImsPhoneMmiCode.newFromDialString(networkPortion, this, wrappedCallback);
+        if (DBG) Rlog.d(LOG_TAG,
+                "dialInternal: dialing w/ mmi '" + mmi + "'...");
+
+        if (mmi == null) {
+            return mCT.dial(dialString, videoState, intentExtras);
+        } else if (mmi.isTemporaryModeCLIR()) {
+            return mCT.dial(mmi.getDialingNumber(), mmi.getCLIRMode(), videoState, intentExtras);
+        } else if (!mmi.isSupportedOverImsPhone()) {
+            // If the mmi is not supported by IMS service,
+            // try to initiate dialing with default phone
+            // Note: This code is never reached; there is a bug in isSupportedOverImsPhone which
+            // causes it to return true even though the "processCode" method ultimately throws the
+            // exception.
+            Rlog.i(LOG_TAG, "dialInternal: USSD not supported by IMS; fallback to CS.");
+            throw new CallStateException(CS_FALLBACK);
+        } else {
+            mPendingMMIs.add(mmi);
+            mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
+
+            try {
+                mmi.processCode();
+            } catch (CallStateException cse) {
+                if (CS_FALLBACK.equals(cse.getMessage())) {
+                    Rlog.i(LOG_TAG, "dialInternal: fallback to GSM required.");
+                    // Make sure we remove from the list of pending MMIs since it will handover to
+                    // GSM.
+                    mPendingMMIs.remove(mmi);
+                    throw cse;
+                }
+            }
+
+            return null;
+        }
+    }
+
+    @Override
+    public void
+    sendDtmf(char c) {
+        if (!PhoneNumberUtils.is12Key(c)) {
+            Rlog.e(LOG_TAG,
+                    "sendDtmf called with invalid character '" + c + "'");
+        } else {
+            if (mCT.getState() ==  PhoneConstants.State.OFFHOOK) {
+                mCT.sendDtmf(c, null);
+            }
+        }
+    }
+
+    @Override
+    public void
+    startDtmf(char c) {
+        if (!(PhoneNumberUtils.is12Key(c) || (c >= 'A' && c <= 'D'))) {
+            Rlog.e(LOG_TAG,
+                    "startDtmf called with invalid character '" + c + "'");
+        } else {
+            mCT.startDtmf(c);
+        }
+    }
+
+    @Override
+    public void
+    stopDtmf() {
+        mCT.stopDtmf();
+    }
+
+    public void notifyIncomingRing() {
+        if (DBG) Rlog.d(LOG_TAG, "notifyIncomingRing");
+        AsyncResult ar = new AsyncResult(null, null, null);
+        sendMessage(obtainMessage(EVENT_CALL_RING, ar));
+    }
+
+    @Override
+    public void setMute(boolean muted) {
+        mCT.setMute(muted);
+    }
+
+    @Override
+    public void setTTYMode(int ttyMode, Message onComplete) {
+        mCT.setTtyMode(ttyMode);
+    }
+
+    @Override
+    public void setUiTTYMode(int uiTtyMode, Message onComplete) {
+        mCT.setUiTTYMode(uiTtyMode, onComplete);
+    }
+
+    @Override
+    public boolean getMute() {
+        return mCT.getMute();
+    }
+
+    @Override
+    public PhoneConstants.State getState() {
+        return mCT.getState();
+    }
+
+    private boolean isValidCommandInterfaceCFReason (int commandInterfaceCFReason) {
+        switch (commandInterfaceCFReason) {
+        case CF_REASON_UNCONDITIONAL:
+        case CF_REASON_BUSY:
+        case CF_REASON_NO_REPLY:
+        case CF_REASON_NOT_REACHABLE:
+        case CF_REASON_ALL:
+        case CF_REASON_ALL_CONDITIONAL:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    private boolean isValidCommandInterfaceCFAction (int commandInterfaceCFAction) {
+        switch (commandInterfaceCFAction) {
+        case CF_ACTION_DISABLE:
+        case CF_ACTION_ENABLE:
+        case CF_ACTION_REGISTRATION:
+        case CF_ACTION_ERASURE:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    private  boolean isCfEnable(int action) {
+        return (action == CF_ACTION_ENABLE) || (action == CF_ACTION_REGISTRATION);
+    }
+
+    private int getConditionFromCFReason(int reason) {
+        switch(reason) {
+            case CF_REASON_UNCONDITIONAL: return ImsUtInterface.CDIV_CF_UNCONDITIONAL;
+            case CF_REASON_BUSY: return ImsUtInterface.CDIV_CF_BUSY;
+            case CF_REASON_NO_REPLY: return ImsUtInterface.CDIV_CF_NO_REPLY;
+            case CF_REASON_NOT_REACHABLE: return ImsUtInterface.CDIV_CF_NOT_REACHABLE;
+            case CF_REASON_ALL: return ImsUtInterface.CDIV_CF_ALL;
+            case CF_REASON_ALL_CONDITIONAL: return ImsUtInterface.CDIV_CF_ALL_CONDITIONAL;
+            default:
+                break;
+        }
+
+        return ImsUtInterface.INVALID;
+    }
+
+    private int getCFReasonFromCondition(int condition) {
+        switch(condition) {
+            case ImsUtInterface.CDIV_CF_UNCONDITIONAL: return CF_REASON_UNCONDITIONAL;
+            case ImsUtInterface.CDIV_CF_BUSY: return CF_REASON_BUSY;
+            case ImsUtInterface.CDIV_CF_NO_REPLY: return CF_REASON_NO_REPLY;
+            case ImsUtInterface.CDIV_CF_NOT_REACHABLE: return CF_REASON_NOT_REACHABLE;
+            case ImsUtInterface.CDIV_CF_ALL: return CF_REASON_ALL;
+            case ImsUtInterface.CDIV_CF_ALL_CONDITIONAL: return CF_REASON_ALL_CONDITIONAL;
+            default:
+                break;
+        }
+
+        return CF_REASON_NOT_REACHABLE;
+    }
+
+    private int getActionFromCFAction(int action) {
+        switch(action) {
+            case CF_ACTION_DISABLE: return ImsUtInterface.ACTION_DEACTIVATION;
+            case CF_ACTION_ENABLE: return ImsUtInterface.ACTION_ACTIVATION;
+            case CF_ACTION_ERASURE: return ImsUtInterface.ACTION_ERASURE;
+            case CF_ACTION_REGISTRATION: return ImsUtInterface.ACTION_REGISTRATION;
+            default:
+                break;
+        }
+
+        return ImsUtInterface.INVALID;
+    }
+
+    @Override
+    public void getOutgoingCallerIdDisplay(Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "getCLIR");
+        Message resp;
+        resp = obtainMessage(EVENT_GET_CLIR_DONE, onComplete);
+
+        try {
+            ImsUtInterface ut = mCT.getUtInterface();
+            ut.queryCLIR(resp);
+        } catch (ImsException e) {
+            sendErrorResponse(onComplete, e);
+        }
+    }
+
+    @Override
+    public void setOutgoingCallerIdDisplay(int clirMode, Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "setCLIR action= " + clirMode);
+        Message resp;
+        // Packing CLIR value in the message. This will be required for
+        // SharedPreference caching, if the message comes back as part of
+        // a success response.
+        resp = obtainMessage(EVENT_SET_CLIR_DONE, clirMode, 0, onComplete);
+        try {
+            ImsUtInterface ut = mCT.getUtInterface();
+            ut.updateCLIR(clirMode, resp);
+        } catch (ImsException e) {
+            sendErrorResponse(onComplete, e);
+        }
+    }
+
+    @Override
+    public void getCallForwardingOption(int commandInterfaceCFReason,
+            Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "getCallForwardingOption reason=" + commandInterfaceCFReason);
+        if (isValidCommandInterfaceCFReason(commandInterfaceCFReason)) {
+            if (DBG) Rlog.d(LOG_TAG, "requesting call forwarding query.");
+            Message resp;
+            resp = obtainMessage(EVENT_GET_CALL_FORWARD_DONE, onComplete);
+
+            try {
+                ImsUtInterface ut = mCT.getUtInterface();
+                ut.queryCallForward(getConditionFromCFReason(commandInterfaceCFReason), null, resp);
+            } catch (ImsException e) {
+                sendErrorResponse(onComplete, e);
+            }
+        } else if (onComplete != null) {
+            sendErrorResponse(onComplete);
+        }
+    }
+
+    @Override
+    public void setCallForwardingOption(int commandInterfaceCFAction,
+            int commandInterfaceCFReason,
+            String dialingNumber,
+            int timerSeconds,
+            Message onComplete) {
+        setCallForwardingOption(commandInterfaceCFAction, commandInterfaceCFReason, dialingNumber,
+                CommandsInterface.SERVICE_CLASS_VOICE, timerSeconds, onComplete);
+    }
+
+    public void setCallForwardingOption(int commandInterfaceCFAction,
+            int commandInterfaceCFReason,
+            String dialingNumber,
+            int serviceClass,
+            int timerSeconds,
+            Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "setCallForwardingOption action=" + commandInterfaceCFAction
+                + ", reason=" + commandInterfaceCFReason + " serviceClass=" + serviceClass);
+        if ((isValidCommandInterfaceCFAction(commandInterfaceCFAction)) &&
+                (isValidCommandInterfaceCFReason(commandInterfaceCFReason))) {
+            Message resp;
+            Cf cf = new Cf(dialingNumber,
+                    (commandInterfaceCFReason == CF_REASON_UNCONDITIONAL ? true : false),
+                    onComplete);
+            resp = obtainMessage(EVENT_SET_CALL_FORWARD_DONE,
+                    isCfEnable(commandInterfaceCFAction) ? 1 : 0, 0, cf);
+
+            try {
+                ImsUtInterface ut = mCT.getUtInterface();
+                ut.updateCallForward(getActionFromCFAction(commandInterfaceCFAction),
+                        getConditionFromCFReason(commandInterfaceCFReason),
+                        dialingNumber,
+                        serviceClass,
+                        timerSeconds,
+                        resp);
+            } catch (ImsException e) {
+                sendErrorResponse(onComplete, e);
+            }
+        } else if (onComplete != null) {
+            sendErrorResponse(onComplete);
+        }
+    }
+
+    @Override
+    public void getCallWaiting(Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "getCallWaiting");
+        Message resp;
+        resp = obtainMessage(EVENT_GET_CALL_WAITING_DONE, onComplete);
+
+        try {
+            ImsUtInterface ut = mCT.getUtInterface();
+            ut.queryCallWaiting(resp);
+        } catch (ImsException e) {
+            sendErrorResponse(onComplete, e);
+        }
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, Message onComplete) {
+        setCallWaiting(enable, CommandsInterface.SERVICE_CLASS_VOICE, onComplete);
+    }
+
+    public void setCallWaiting(boolean enable, int serviceClass, Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "setCallWaiting enable=" + enable);
+        Message resp;
+        resp = obtainMessage(EVENT_SET_CALL_WAITING_DONE, onComplete);
+
+        try {
+            ImsUtInterface ut = mCT.getUtInterface();
+            ut.updateCallWaiting(enable, serviceClass, resp);
+        } catch (ImsException e) {
+            sendErrorResponse(onComplete, e);
+        }
+    }
+
+    private int getCBTypeFromFacility(String facility) {
+        if (CB_FACILITY_BAOC.equals(facility)) {
+            return ImsUtInterface.CB_BAOC;
+        } else if (CB_FACILITY_BAOIC.equals(facility)) {
+            return ImsUtInterface.CB_BOIC;
+        } else if (CB_FACILITY_BAOICxH.equals(facility)) {
+            return ImsUtInterface.CB_BOIC_EXHC;
+        } else if (CB_FACILITY_BAIC.equals(facility)) {
+            return ImsUtInterface.CB_BAIC;
+        } else if (CB_FACILITY_BAICr.equals(facility)) {
+            return ImsUtInterface.CB_BIC_WR;
+        } else if (CB_FACILITY_BA_ALL.equals(facility)) {
+            return ImsUtInterface.CB_BA_ALL;
+        } else if (CB_FACILITY_BA_MO.equals(facility)) {
+            return ImsUtInterface.CB_BA_MO;
+        } else if (CB_FACILITY_BA_MT.equals(facility)) {
+            return ImsUtInterface.CB_BA_MT;
+        }
+
+        return 0;
+    }
+
+    public void getCallBarring(String facility, Message onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "getCallBarring facility=" + facility);
+        Message resp;
+        resp = obtainMessage(EVENT_GET_CALL_BARRING_DONE, onComplete);
+
+        try {
+            ImsUtInterface ut = mCT.getUtInterface();
+            ut.queryCallBarring(getCBTypeFromFacility(facility), resp);
+        } catch (ImsException e) {
+            sendErrorResponse(onComplete, e);
+        }
+    }
+
+    public void setCallBarring(String facility, boolean lockState, String password, Message
+            onComplete) {
+        if (DBG) Rlog.d(LOG_TAG, "setCallBarring facility=" + facility
+                + ", lockState=" + lockState);
+        Message resp;
+        resp = obtainMessage(EVENT_SET_CALL_BARRING_DONE, onComplete);
+
+        int action;
+        if (lockState) {
+            action = CommandsInterface.CF_ACTION_ENABLE;
+        }
+        else {
+            action = CommandsInterface.CF_ACTION_DISABLE;
+        }
+
+        try {
+            ImsUtInterface ut = mCT.getUtInterface();
+            // password is not required with Ut interface
+            ut.updateCallBarring(getCBTypeFromFacility(facility), action, resp, null);
+        } catch (ImsException e) {
+            sendErrorResponse(onComplete, e);
+        }
+    }
+
+    @Override
+    public void sendUssdResponse(String ussdMessge) {
+        Rlog.d(LOG_TAG, "sendUssdResponse");
+        ImsPhoneMmiCode mmi = ImsPhoneMmiCode.newFromUssdUserInput(ussdMessge, this);
+        mPendingMMIs.add(mmi);
+        mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null));
+        mmi.sendUssd(ussdMessge);
+    }
+
+    public void sendUSSD(String ussdString, Message response) {
+        mCT.sendUSSD(ussdString, response);
+    }
+
+    @Override
+    public void cancelUSSD() {
+        mCT.cancelUSSD();
+    }
+
+    private void sendErrorResponse(Message onComplete) {
+        Rlog.d(LOG_TAG, "sendErrorResponse");
+        if (onComplete != null) {
+            AsyncResult.forMessage(onComplete, null,
+                    new CommandException(CommandException.Error.GENERIC_FAILURE));
+            onComplete.sendToTarget();
+        }
+    }
+
+    @VisibleForTesting
+    public void sendErrorResponse(Message onComplete, Throwable e) {
+        Rlog.d(LOG_TAG, "sendErrorResponse");
+        if (onComplete != null) {
+            AsyncResult.forMessage(onComplete, null, getCommandException(e));
+            onComplete.sendToTarget();
+        }
+    }
+
+    private CommandException getCommandException(int code, String errorString) {
+        Rlog.d(LOG_TAG, "getCommandException code= " + code
+                + ", errorString= " + errorString);
+        CommandException.Error error = CommandException.Error.GENERIC_FAILURE;
+
+        switch(code) {
+            case ImsReasonInfo.CODE_UT_NOT_SUPPORTED:
+                error = CommandException.Error.REQUEST_NOT_SUPPORTED;
+                break;
+            case ImsReasonInfo.CODE_UT_CB_PASSWORD_MISMATCH:
+                error = CommandException.Error.PASSWORD_INCORRECT;
+                break;
+            case ImsReasonInfo.CODE_UT_SERVICE_UNAVAILABLE:
+                error = CommandException.Error.RADIO_NOT_AVAILABLE;
+            default:
+                break;
+        }
+
+        return new CommandException(error, errorString);
+    }
+
+    private CommandException getCommandException(Throwable e) {
+        CommandException ex = null;
+
+        if (e instanceof ImsException) {
+            ex = getCommandException(((ImsException)e).getCode(), e.getMessage());
+        } else {
+            Rlog.d(LOG_TAG, "getCommandException generic failure");
+            ex = new CommandException(CommandException.Error.GENERIC_FAILURE);
+        }
+        return ex;
+    }
+
+    private void
+    onNetworkInitiatedUssd(ImsPhoneMmiCode mmi) {
+        Rlog.d(LOG_TAG, "onNetworkInitiatedUssd");
+        mMmiCompleteRegistrants.notifyRegistrants(
+            new AsyncResult(null, mmi, null));
+    }
+
+    /* package */
+    void onIncomingUSSD(int ussdMode, String ussdMessage) {
+        if (DBG) Rlog.d(LOG_TAG, "onIncomingUSSD ussdMode=" + ussdMode);
+
+        boolean isUssdError;
+        boolean isUssdRequest;
+
+        isUssdRequest
+            = (ussdMode == CommandsInterface.USSD_MODE_REQUEST);
+
+        isUssdError
+            = (ussdMode != CommandsInterface.USSD_MODE_NOTIFY
+                && ussdMode != CommandsInterface.USSD_MODE_REQUEST);
+
+        ImsPhoneMmiCode found = null;
+        for (int i = 0, s = mPendingMMIs.size() ; i < s; i++) {
+            if(mPendingMMIs.get(i).isPendingUSSD()) {
+                found = mPendingMMIs.get(i);
+                break;
+            }
+        }
+
+        if (found != null) {
+            // Complete pending USSD
+            if (isUssdError) {
+                found.onUssdFinishedError();
+            } else {
+                found.onUssdFinished(ussdMessage, isUssdRequest);
+            }
+        } else if (!isUssdError && ussdMessage != null) {
+                // pending USSD not found
+                // The network may initiate its own USSD request
+
+                // ignore everything that isnt a Notify or a Request
+                // also, discard if there is no message to present
+                ImsPhoneMmiCode mmi;
+                mmi = ImsPhoneMmiCode.newNetworkInitiatedUssd(ussdMessage,
+                        isUssdRequest,
+                        this);
+                onNetworkInitiatedUssd(mmi);
+        }
+    }
+
+    /**
+     * Removes the given MMI from the pending list and notifies
+     * registrants that it is complete.
+     * @param mmi MMI that is done
+     */
+    public void onMMIDone(ImsPhoneMmiCode mmi) {
+        /* Only notify complete if it's on the pending list.
+         * Otherwise, it's already been handled (eg, previously canceled).
+         * The exception is cancellation of an incoming USSD-REQUEST, which is
+         * not on the list.
+         */
+        Rlog.d(LOG_TAG, "onMMIDone: mmi=" + mmi);
+        if (mPendingMMIs.remove(mmi) || mmi.isUssdRequest()) {
+            ResultReceiver receiverCallback = mmi.getUssdCallbackReceiver();
+            if (receiverCallback != null) {
+                int returnCode = (mmi.getState() ==  MmiCode.State.COMPLETE) ?
+                        TelephonyManager.USSD_RETURN_SUCCESS : TelephonyManager.USSD_RETURN_FAILURE;
+                sendUssdResponse(mmi.getDialString(), mmi.getMessage(), returnCode,
+                        receiverCallback );
+            } else {
+                Rlog.v(LOG_TAG, "onMMIDone: notifyRegistrants");
+                mMmiCompleteRegistrants.notifyRegistrants(
+                    new AsyncResult(null, mmi, null));
+            }
+        }
+    }
+
+    @Override
+    public ArrayList<Connection> getHandoverConnection() {
+        ArrayList<Connection> connList = new ArrayList<Connection>();
+        // Add all foreground call connections
+        connList.addAll(getForegroundCall().mConnections);
+        // Add all background call connections
+        connList.addAll(getBackgroundCall().mConnections);
+        // Add all background call connections
+        connList.addAll(getRingingCall().mConnections);
+        if (connList.size() > 0) {
+            return connList;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void notifySrvccState(Call.SrvccState state) {
+        mCT.notifySrvccState(state);
+    }
+
+    /* package */ void
+    initiateSilentRedial() {
+        String result = mLastDialString;
+        AsyncResult ar = new AsyncResult(null, result, null);
+        if (ar != null) {
+            mSilentRedialRegistrants.notifyRegistrants(ar);
+        }
+    }
+
+    @Override
+    public void registerForSilentRedial(Handler h, int what, Object obj) {
+        mSilentRedialRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForSilentRedial(Handler h) {
+        mSilentRedialRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForSuppServiceNotification(Handler h, int what, Object obj) {
+        mSsnRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForSuppServiceNotification(Handler h) {
+        mSsnRegistrants.remove(h);
+    }
+
+    @Override
+    public int getSubId() {
+        return mDefaultPhone.getSubId();
+    }
+
+    @Override
+    public int getPhoneId() {
+        return mDefaultPhone.getPhoneId();
+    }
+
+    private CallForwardInfo getCallForwardInfo(ImsCallForwardInfo info) {
+        CallForwardInfo cfInfo = new CallForwardInfo();
+        cfInfo.status = info.mStatus;
+        cfInfo.reason = getCFReasonFromCondition(info.mCondition);
+        cfInfo.serviceClass = SERVICE_CLASS_VOICE;
+        cfInfo.toa = info.mToA;
+        cfInfo.number = info.mNumber;
+        cfInfo.timeSeconds = info.mTimeSeconds;
+        return cfInfo;
+    }
+
+    private CallForwardInfo[] handleCfQueryResult(ImsCallForwardInfo[] infos) {
+        CallForwardInfo[] cfInfos = null;
+
+        if (infos != null && infos.length != 0) {
+            cfInfos = new CallForwardInfo[infos.length];
+        }
+
+        IccRecords r = mDefaultPhone.getIccRecords();
+        if (infos == null || infos.length == 0) {
+            if (r != null) {
+                // Assume the default is not active
+                // Set unconditional CFF in SIM to false
+                setVoiceCallForwardingFlag(r, 1, false, null);
+            }
+        } else {
+            for (int i = 0, s = infos.length; i < s; i++) {
+                if (infos[i].mCondition == ImsUtInterface.CDIV_CF_UNCONDITIONAL) {
+                    if (r != null) {
+                        setVoiceCallForwardingFlag(r, 1, (infos[i].mStatus == 1),
+                            infos[i].mNumber);
+                    }
+                }
+                cfInfos[i] = getCallForwardInfo(infos[i]);
+            }
+        }
+
+        return cfInfos;
+    }
+
+    private int[] handleCbQueryResult(ImsSsInfo[] infos) {
+        int[] cbInfos = new int[1];
+        cbInfos[0] = SERVICE_CLASS_NONE;
+
+        if (infos[0].mStatus == 1) {
+            cbInfos[0] = SERVICE_CLASS_VOICE;
+        }
+
+        return cbInfos;
+    }
+
+    private int[] handleCwQueryResult(ImsSsInfo[] infos) {
+        int[] cwInfos = new int[2];
+        cwInfos[0] = 0;
+
+        if (infos[0].mStatus == 1) {
+            cwInfos[0] = 1;
+            cwInfos[1] = SERVICE_CLASS_VOICE;
+        }
+
+        return cwInfos;
+    }
+
+    private void
+    sendResponse(Message onComplete, Object result, Throwable e) {
+        if (onComplete != null) {
+            CommandException ex = null;
+            if (e != null) {
+                ex = getCommandException(e);
+            }
+            AsyncResult.forMessage(onComplete, result, ex);
+            onComplete.sendToTarget();
+        }
+    }
+
+    private void updateDataServiceState() {
+        if (mSS != null && mDefaultPhone.getServiceStateTracker() != null
+                && mDefaultPhone.getServiceStateTracker().mSS != null) {
+            ServiceState ss = mDefaultPhone.getServiceStateTracker().mSS;
+            mSS.setDataRegState(ss.getDataRegState());
+            mSS.setRilDataRadioTechnology(ss.getRilDataRadioTechnology());
+            Rlog.d(LOG_TAG, "updateDataServiceState: defSs = " + ss + " imsSs = " + mSS);
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar = (AsyncResult) msg.obj;
+
+        if (DBG) Rlog.d(LOG_TAG, "handleMessage what=" + msg.what);
+        switch (msg.what) {
+            case EVENT_SET_CALL_FORWARD_DONE:
+                IccRecords r = mDefaultPhone.getIccRecords();
+                Cf cf = (Cf) ar.userObj;
+                if (cf.mIsCfu && ar.exception == null && r != null) {
+                    setVoiceCallForwardingFlag(r, 1, msg.arg1 == 1, cf.mSetCfNumber);
+                }
+                sendResponse(cf.mOnComplete, null, ar.exception);
+                break;
+
+            case EVENT_GET_CALL_FORWARD_DONE:
+                CallForwardInfo[] cfInfos = null;
+                if (ar.exception == null) {
+                    cfInfos = handleCfQueryResult((ImsCallForwardInfo[])ar.result);
+                }
+                sendResponse((Message) ar.userObj, cfInfos, ar.exception);
+                break;
+
+            case EVENT_GET_CALL_BARRING_DONE:
+            case EVENT_GET_CALL_WAITING_DONE:
+                int[] ssInfos = null;
+                if (ar.exception == null) {
+                    if (msg.what == EVENT_GET_CALL_BARRING_DONE) {
+                        ssInfos = handleCbQueryResult((ImsSsInfo[])ar.result);
+                    } else if (msg.what == EVENT_GET_CALL_WAITING_DONE) {
+                        ssInfos = handleCwQueryResult((ImsSsInfo[])ar.result);
+                    }
+                }
+                sendResponse((Message) ar.userObj, ssInfos, ar.exception);
+                break;
+
+            case EVENT_GET_CLIR_DONE:
+                Bundle ssInfo = (Bundle) ar.result;
+                int[] clirInfo = null;
+                if (ssInfo != null) {
+                    clirInfo = ssInfo.getIntArray(ImsPhoneMmiCode.UT_BUNDLE_KEY_CLIR);
+                }
+                sendResponse((Message) ar.userObj, clirInfo, ar.exception);
+                break;
+
+            case EVENT_SET_CLIR_DONE:
+                if (ar.exception == null) {
+                    saveClirSetting(msg.arg1);
+                }
+                 // (Intentional fallthrough)
+            case EVENT_SET_CALL_BARRING_DONE:
+            case EVENT_SET_CALL_WAITING_DONE:
+                sendResponse((Message) ar.userObj, null, ar.exception);
+                break;
+
+            case EVENT_DEFAULT_PHONE_DATA_STATE_CHANGED:
+                if (DBG) Rlog.d(LOG_TAG, "EVENT_DEFAULT_PHONE_DATA_STATE_CHANGED");
+                updateDataServiceState();
+                break;
+
+            case EVENT_SERVICE_STATE_CHANGED:
+                if (VDBG) Rlog.d(LOG_TAG, "EVENT_SERVICE_STATE_CHANGED");
+                ar = (AsyncResult) msg.obj;
+                ServiceState newServiceState = (ServiceState) ar.result;
+                // only update if roaming status changed
+                if (mRoaming != newServiceState.getRoaming()) {
+                    if (DBG) Rlog.d(LOG_TAG, "Roaming state changed");
+                    updateRoamingState(newServiceState.getRoaming());
+                }
+                break;
+            case EVENT_VOICE_CALL_ENDED:
+                if (DBG) Rlog.d(LOG_TAG, "Voice call ended. Handle pending updateRoamingState.");
+                mCT.unregisterForVoiceCallEnded(this);
+                // only update if roaming status changed
+                boolean newRoaming = getCurrentRoaming();
+                if (mRoaming != newRoaming) {
+                    updateRoamingState(newRoaming);
+                }
+                break;
+
+            default:
+                super.handleMessage(msg);
+                break;
+        }
+    }
+
+    /**
+     * Listen to the IMS ECBM state change
+     */
+    private ImsEcbmStateListener mImsEcbmStateListener =
+            new ImsEcbmStateListener() {
+                @Override
+                public void onECBMEntered() {
+                    if (DBG) Rlog.d(LOG_TAG, "onECBMEntered");
+                    handleEnterEmergencyCallbackMode();
+                }
+
+                @Override
+                public void onECBMExited() {
+                    if (DBG) Rlog.d(LOG_TAG, "onECBMExited");
+                    handleExitEmergencyCallbackMode();
+                }
+            };
+
+    @VisibleForTesting
+    public ImsEcbmStateListener getImsEcbmStateListener() {
+        return mImsEcbmStateListener;
+    }
+
+    @Override
+    public boolean isInEmergencyCall() {
+        return mCT.isInEmergencyCall();
+    }
+
+    private void sendEmergencyCallbackModeChange() {
+        // Send an Intent
+        Intent intent = new Intent(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
+        intent.putExtra(PhoneConstants.PHONE_IN_ECM_STATE, isInEcm());
+        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, getPhoneId());
+        ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+        if (DBG) Rlog.d(LOG_TAG, "sendEmergencyCallbackModeChange: isInEcm=" + isInEcm());
+    }
+
+    @Override
+    public void exitEmergencyCallbackMode() {
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+        if (DBG) Rlog.d(LOG_TAG, "exitEmergencyCallbackMode()");
+
+        // Send a message which will invoke handleExitEmergencyCallbackMode
+        ImsEcbm ecbm;
+        try {
+            ecbm = mCT.getEcbmInterface();
+            ecbm.exitEmergencyCallbackMode();
+        } catch (ImsException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void handleEnterEmergencyCallbackMode() {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleEnterEmergencyCallbackMode,mIsPhoneInEcmState= "
+                    + isInEcm());
+        }
+        // if phone is not in Ecm mode, and it's changed to Ecm mode
+        if (!isInEcm()) {
+            setIsInEcm(true);
+            // notify change
+            sendEmergencyCallbackModeChange();
+
+            // Post this runnable so we will automatically exit
+            // if no one invokes exitEmergencyCallbackMode() directly.
+            long delayInMillis = SystemProperties.getLong(
+                    TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE);
+            postDelayed(mExitEcmRunnable, delayInMillis);
+            // We don't want to go to sleep while in Ecm
+            mWakeLock.acquire();
+        }
+    }
+
+    @Override
+    protected void handleExitEmergencyCallbackMode() {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleExitEmergencyCallbackMode: mIsPhoneInEcmState = "
+                    + isInEcm());
+        }
+
+        if (isInEcm()) {
+            setIsInEcm(false);
+        }
+
+        // Remove pending exit Ecm runnable, if any
+        removeCallbacks(mExitEcmRunnable);
+
+        if (mEcmExitRespRegistrant != null) {
+            mEcmExitRespRegistrant.notifyResult(Boolean.TRUE);
+        }
+
+        // release wakeLock
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+
+        // send an Intent
+        sendEmergencyCallbackModeChange();
+    }
+
+    /**
+     * Handle to cancel or restart Ecm timer in emergency call back mode if action is
+     * CANCEL_ECM_TIMER, cancel Ecm timer and notify apps the timer is canceled; otherwise, restart
+     * Ecm timer and notify apps the timer is restarted.
+     */
+    void handleTimerInEmergencyCallbackMode(int action) {
+        switch (action) {
+            case CANCEL_ECM_TIMER:
+                removeCallbacks(mExitEcmRunnable);
+                ((GsmCdmaPhone) mDefaultPhone).notifyEcbmTimerReset(Boolean.TRUE);
+                break;
+            case RESTART_ECM_TIMER:
+                long delayInMillis = SystemProperties.getLong(
+                        TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE);
+                postDelayed(mExitEcmRunnable, delayInMillis);
+                ((GsmCdmaPhone) mDefaultPhone).notifyEcbmTimerReset(Boolean.FALSE);
+                break;
+            default:
+                Rlog.e(LOG_TAG, "handleTimerInEmergencyCallbackMode, unsupported action " + action);
+        }
+    }
+
+    @Override
+    public void setOnEcbModeExitResponse(Handler h, int what, Object obj) {
+        mEcmExitRespRegistrant = new Registrant(h, what, obj);
+    }
+
+    @Override
+    public void unsetOnEcbModeExitResponse(Handler h) {
+        mEcmExitRespRegistrant.clear();
+    }
+
+    public void onFeatureCapabilityChanged() {
+        mDefaultPhone.getServiceStateTracker().onImsCapabilityChanged();
+    }
+
+    @Override
+    public boolean isVolteEnabled() {
+        return mCT.isVolteEnabled();
+    }
+
+    @Override
+    public boolean isWifiCallingEnabled() {
+        return mCT.isVowifiEnabled();
+    }
+
+    @Override
+    public boolean isVideoEnabled() {
+        return mCT.isVideoCallEnabled();
+    }
+
+    @Override
+    public Phone getDefaultPhone() {
+        return mDefaultPhone;
+    }
+
+    @Override
+    public boolean isImsRegistered() {
+        return mImsRegistered;
+    }
+
+    public void setImsRegistered(boolean value) {
+        mImsRegistered = value;
+    }
+
+    @Override
+    public void callEndCleanupHandOverCallIfAny() {
+        mCT.callEndCleanupHandOverCallIfAny();
+    }
+
+    private BroadcastReceiver mResultReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // Add notification only if alert was not shown by WfcSettings
+            if (getResultCode() == Activity.RESULT_OK) {
+                // Default result code (as passed to sendOrderedBroadcast)
+                // means that intent was not received by WfcSettings.
+
+                CharSequence title = intent.getCharSequenceExtra(EXTRA_KEY_ALERT_TITLE);
+                CharSequence messageAlert = intent.getCharSequenceExtra(EXTRA_KEY_ALERT_MESSAGE);
+                CharSequence messageNotification = intent.getCharSequenceExtra(EXTRA_KEY_NOTIFICATION_MESSAGE);
+
+                Intent resultIntent = new Intent(Intent.ACTION_MAIN);
+                resultIntent.setClassName("com.android.settings",
+                        "com.android.settings.Settings$WifiCallingSettingsActivity");
+                resultIntent.putExtra(EXTRA_KEY_ALERT_SHOW, true);
+                resultIntent.putExtra(EXTRA_KEY_ALERT_TITLE, title);
+                resultIntent.putExtra(EXTRA_KEY_ALERT_MESSAGE, messageAlert);
+                PendingIntent resultPendingIntent =
+                        PendingIntent.getActivity(
+                                mContext,
+                                0,
+                                resultIntent,
+                                PendingIntent.FLAG_UPDATE_CURRENT
+                        );
+
+                final Notification notification = new Notification.Builder(mContext)
+                                .setSmallIcon(android.R.drawable.stat_sys_warning)
+                                .setContentTitle(title)
+                                .setContentText(messageNotification)
+                                .setAutoCancel(true)
+                                .setContentIntent(resultPendingIntent)
+                                .setStyle(new Notification.BigTextStyle()
+                                .bigText(messageNotification))
+                                .setChannelId(NotificationChannelController.CHANNEL_ID_WFC)
+                                .build();
+                final String notificationTag = "wifi_calling";
+                final int notificationId = 1;
+
+                NotificationManager notificationManager =
+                        (NotificationManager) mContext.getSystemService(
+                                Context.NOTIFICATION_SERVICE);
+                notificationManager.notify(notificationTag, notificationId,
+                        notification);
+            }
+        }
+    };
+
+    /**
+     * Show notification in case of some error codes.
+     */
+    public void processDisconnectReason(ImsReasonInfo imsReasonInfo) {
+        if (imsReasonInfo.mCode == imsReasonInfo.CODE_REGISTRATION_ERROR
+                && imsReasonInfo.mExtraMessage != null) {
+            // Suppress WFC Registration notifications if WFC is not enabled by the user.
+            if (ImsManager.isWfcEnabledByUser(mContext)) {
+                processWfcDisconnectForNotification(imsReasonInfo);
+            }
+        }
+    }
+
+    // Processes an IMS disconnect cause for possible WFC registration errors and optionally
+    // disable WFC.
+    private void processWfcDisconnectForNotification(ImsReasonInfo imsReasonInfo) {
+        CarrierConfigManager configManager =
+                (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) {
+            Rlog.e(LOG_TAG, "processDisconnectReason: CarrierConfigManager is not ready");
+            return;
+        }
+        PersistableBundle pb = configManager.getConfigForSubId(getSubId());
+        if (pb == null) {
+            Rlog.e(LOG_TAG, "processDisconnectReason: no config for subId " + getSubId());
+            return;
+        }
+        final String[] wfcOperatorErrorCodes =
+                pb.getStringArray(
+                        CarrierConfigManager.KEY_WFC_OPERATOR_ERROR_CODES_STRING_ARRAY);
+        if (wfcOperatorErrorCodes == null) {
+            // no operator-specific error codes
+            return;
+        }
+
+        final String[] wfcOperatorErrorAlertMessages =
+                mContext.getResources().getStringArray(
+                        com.android.internal.R.array.wfcOperatorErrorAlertMessages);
+        final String[] wfcOperatorErrorNotificationMessages =
+                mContext.getResources().getStringArray(
+                        com.android.internal.R.array.wfcOperatorErrorNotificationMessages);
+
+        for (int i = 0; i < wfcOperatorErrorCodes.length; i++) {
+            String[] codes = wfcOperatorErrorCodes[i].split("\\|");
+            if (codes.length != 2) {
+                Rlog.e(LOG_TAG, "Invalid carrier config: " + wfcOperatorErrorCodes[i]);
+                continue;
+            }
+
+            // Match error code.
+            if (!imsReasonInfo.mExtraMessage.startsWith(
+                    codes[0])) {
+                continue;
+            }
+            // If there is no delimiter at the end of error code string
+            // then we need to verify that we are not matching partial code.
+            // EXAMPLE: "REG9" must not match "REG99".
+            // NOTE: Error code must not be empty.
+            int codeStringLength = codes[0].length();
+            char lastChar = codes[0].charAt(codeStringLength - 1);
+            if (Character.isLetterOrDigit(lastChar)) {
+                if (imsReasonInfo.mExtraMessage.length() > codeStringLength) {
+                    char nextChar = imsReasonInfo.mExtraMessage.charAt(codeStringLength);
+                    if (Character.isLetterOrDigit(nextChar)) {
+                        continue;
+                    }
+                }
+            }
+
+            final CharSequence title = mContext.getText(
+                    com.android.internal.R.string.wfcRegErrorTitle);
+
+            int idx = Integer.parseInt(codes[1]);
+            if (idx < 0
+                    || idx >= wfcOperatorErrorAlertMessages.length
+                    || idx >= wfcOperatorErrorNotificationMessages.length) {
+                Rlog.e(LOG_TAG, "Invalid index: " + wfcOperatorErrorCodes[i]);
+                continue;
+            }
+            String messageAlert = imsReasonInfo.mExtraMessage;
+            String messageNotification = imsReasonInfo.mExtraMessage;
+            if (!wfcOperatorErrorAlertMessages[idx].isEmpty()) {
+                messageAlert = String.format(
+                        wfcOperatorErrorAlertMessages[idx],
+                        imsReasonInfo.mExtraMessage); // Fill IMS error code into alert message
+            }
+            if (!wfcOperatorErrorNotificationMessages[idx].isEmpty()) {
+                messageNotification = String.format(
+                        wfcOperatorErrorNotificationMessages[idx],
+                        imsReasonInfo.mExtraMessage); // Fill IMS error code into notification
+            }
+
+            // UX requirement is to disable WFC in case of "permanent" registration failures.
+            ImsManager.setWfcSetting(mContext, false);
+
+            // If WfcSettings are active then alert will be shown
+            // otherwise notification will be added.
+            Intent intent = new Intent(ImsManager.ACTION_IMS_REGISTRATION_ERROR);
+            intent.putExtra(EXTRA_KEY_ALERT_TITLE, title);
+            intent.putExtra(EXTRA_KEY_ALERT_MESSAGE, messageAlert);
+            intent.putExtra(EXTRA_KEY_NOTIFICATION_MESSAGE, messageNotification);
+            mContext.sendOrderedBroadcast(intent, null, mResultReceiver,
+                    null, Activity.RESULT_OK, null, null);
+
+            // We can only match a single error code
+            // so should break the loop after a successful match.
+            break;
+        }
+    }
+
+    @Override
+    public boolean isUtEnabled() {
+        return mCT.isUtEnabled();
+    }
+
+    @Override
+    public void sendEmergencyCallStateChange(boolean callActive) {
+        mDefaultPhone.sendEmergencyCallStateChange(callActive);
+    }
+
+    @Override
+    public void setBroadcastEmergencyCallStateChanges(boolean broadcast) {
+        mDefaultPhone.setBroadcastEmergencyCallStateChanges(broadcast);
+    }
+
+    @VisibleForTesting
+    public PowerManager.WakeLock getWakeLock() {
+        return mWakeLock;
+    }
+
+    @Override
+    public NetworkStats getVtDataUsage(boolean perUidStats) {
+        return mCT.getVtDataUsage(perUidStats);
+    }
+
+    private void updateRoamingState(boolean newRoaming) {
+        if (mCT.getState() == PhoneConstants.State.IDLE) {
+            if (DBG) Rlog.d(LOG_TAG, "updateRoamingState now: " + newRoaming);
+            mRoaming = newRoaming;
+            ImsManager.setWfcMode(mContext,
+                    ImsManager.getWfcMode(mContext, newRoaming), newRoaming);
+        } else {
+            if (DBG) Rlog.d(LOG_TAG, "updateRoamingState postponed: " + newRoaming);
+            mCT.registerForVoiceCallEnded(this,
+                    EVENT_VOICE_CALL_ENDED, null);
+        }
+    }
+
+    private boolean getCurrentRoaming() {
+        TelephonyManager tm = (TelephonyManager) mContext
+                .getSystemService(Context.TELEPHONY_SERVICE);
+        return tm.isNetworkRoaming();
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("ImsPhone extends:");
+        super.dump(fd, pw, args);
+        pw.flush();
+
+        pw.println("ImsPhone:");
+        pw.println("  mDefaultPhone = " + mDefaultPhone);
+        pw.println("  mPendingMMIs = " + mPendingMMIs);
+        pw.println("  mPostDialHandler = " + mPostDialHandler);
+        pw.println("  mSS = " + mSS);
+        pw.println("  mWakeLock = " + mWakeLock);
+        pw.println("  mIsPhoneInEcmState = " + isInEcm());
+        pw.println("  mEcmExitRespRegistrant = " + mEcmExitRespRegistrant);
+        pw.println("  mSilentRedialRegistrants = " + mSilentRedialRegistrants);
+        pw.println("  mImsRegistered = " + mImsRegistered);
+        pw.println("  mRoaming = " + mRoaming);
+        pw.println("  mSsnRegistrants = " + mSsnRegistrants);
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneBase.java b/com/android/internal/telephony/imsphone/ImsPhoneBase.java
new file mode 100644
index 0000000..87b96d8
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneBase.java
@@ -0,0 +1,562 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RegistrantList;
+import android.os.SystemProperties;
+import android.os.WorkSource;
+import android.telephony.CellInfo;
+import android.telephony.CellLocation;
+import android.telephony.NetworkScanRequest;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.util.Pair;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.IccPhoneBookInterfaceManager;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.OperatorInfo;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneNotifier;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.dataconnection.DataConnection;
+import com.android.internal.telephony.uicc.IccFileHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class ImsPhoneBase extends Phone {
+    private static final String LOG_TAG = "ImsPhoneBase";
+
+    private RegistrantList mRingbackRegistrants = new RegistrantList();
+    private RegistrantList mOnHoldRegistrants = new RegistrantList();
+    private RegistrantList mTtyModeReceivedRegistrants = new RegistrantList();
+    private PhoneConstants.State mState = PhoneConstants.State.IDLE;
+
+    public ImsPhoneBase(String name, Context context, PhoneNotifier notifier,
+                        boolean unitTestMode) {
+        super(name, notifier, context, new ImsPhoneCommandInterface(context), unitTestMode);
+    }
+
+    @Override
+    public void migrateFrom(Phone from) {
+        super.migrateFrom(from);
+        migrate(mRingbackRegistrants, ((ImsPhoneBase)from).mRingbackRegistrants);
+    }
+
+    @Override
+    public void registerForRingbackTone(Handler h, int what, Object obj) {
+        mRingbackRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForRingbackTone(Handler h) {
+        mRingbackRegistrants.remove(h);
+    }
+
+    @Override
+    public void startRingbackTone() {
+        AsyncResult result = new AsyncResult(null, Boolean.TRUE, null);
+        mRingbackRegistrants.notifyRegistrants(result);
+    }
+
+    @Override
+    public void stopRingbackTone() {
+        AsyncResult result = new AsyncResult(null, Boolean.FALSE, null);
+        mRingbackRegistrants.notifyRegistrants(result);
+    }
+
+    @Override
+    public void registerForOnHoldTone(Handler h, int what, Object obj) {
+        mOnHoldRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForOnHoldTone(Handler h) {
+        mOnHoldRegistrants.remove(h);
+    }
+
+    /**
+     * Signals all registrants that the remote hold tone should be started for a connection.
+     *
+     * @param cn The connection.
+     */
+    protected void startOnHoldTone(Connection cn) {
+        Pair<Connection, Boolean> result = new Pair<Connection, Boolean>(cn, Boolean.TRUE);
+        mOnHoldRegistrants.notifyRegistrants(new AsyncResult(null, result, null));
+    }
+
+    /**
+     * Signals all registrants that the remote hold tone should be stopped for a connection.
+     *
+     * @param cn The connection.
+     */
+    protected void stopOnHoldTone(Connection cn) {
+        Pair<Connection, Boolean> result = new Pair<Connection, Boolean>(cn, Boolean.FALSE);
+        mOnHoldRegistrants.notifyRegistrants(new AsyncResult(null, result, null));
+    }
+
+    @Override
+    public void registerForTtyModeReceived(Handler h, int what, Object obj){
+        mTtyModeReceivedRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForTtyModeReceived(Handler h) {
+        mTtyModeReceivedRegistrants.remove(h);
+    }
+
+    public void onTtyModeReceived(int mode) {
+        AsyncResult result = new AsyncResult(null, Integer.valueOf(mode), null);
+        mTtyModeReceivedRegistrants.notifyRegistrants(result);
+    }
+
+    @Override
+    public ServiceState getServiceState() {
+        // FIXME: we may need to provide this when data connectivity is lost
+        // or when server is down
+        ServiceState s = new ServiceState();
+        s.setVoiceRegState(ServiceState.STATE_IN_SERVICE);
+        return s;
+    }
+
+    /**
+     * @return all available cell information or null if none.
+     */
+    @Override
+    public List<CellInfo> getAllCellInfo(WorkSource workSource) {
+        return getServiceStateTracker().getAllCellInfo(workSource);
+    }
+
+    @Override
+    public CellLocation getCellLocation(WorkSource workSource) {
+        return null;
+    }
+
+    @Override
+    public PhoneConstants.State getState() {
+        return mState;
+    }
+
+    @Override
+    public int getPhoneType() {
+        return PhoneConstants.PHONE_TYPE_IMS;
+    }
+
+    @Override
+    public SignalStrength getSignalStrength() {
+        return new SignalStrength();
+    }
+
+    @Override
+    public boolean getMessageWaitingIndicator() {
+        return false;
+    }
+
+    @Override
+    public boolean getCallForwardingIndicator() {
+        return false;
+    }
+
+    @Override
+    public List<? extends MmiCode> getPendingMmiCodes() {
+        return new ArrayList<MmiCode>(0);
+    }
+
+    @Override
+    public PhoneConstants.DataState getDataConnectionState() {
+        return PhoneConstants.DataState.DISCONNECTED;
+    }
+
+    @Override
+    public PhoneConstants.DataState getDataConnectionState(String apnType) {
+        return PhoneConstants.DataState.DISCONNECTED;
+    }
+
+    @Override
+    public DataActivityState getDataActivityState() {
+        return DataActivityState.NONE;
+    }
+
+    /**
+     * Notify any interested party of a Phone state change
+     * {@link com.android.internal.telephony.PhoneConstants.State}
+     */
+    public void notifyPhoneStateChanged() {
+        mNotifier.notifyPhoneState(this);
+    }
+
+    /**
+     * Notify registrants of a change in the call state. This notifies changes in
+     * {@link com.android.internal.telephony.Call.State}. Use this when changes
+     * in the precise call state are needed, else use notifyPhoneStateChanged.
+     */
+    public void notifyPreciseCallStateChanged() {
+        /* we'd love it if this was package-scoped*/
+        super.notifyPreciseCallStateChangedP();
+    }
+
+    public void notifyDisconnect(Connection cn) {
+        mDisconnectRegistrants.notifyResult(cn);
+
+        mNotifier.notifyDisconnectCause(cn.getDisconnectCause(), cn.getPreciseDisconnectCause());
+    }
+
+    void notifyUnknownConnection() {
+        mUnknownConnectionRegistrants.notifyResult(this);
+    }
+
+    public void notifySuppServiceFailed(SuppService code) {
+        mSuppServiceFailedRegistrants.notifyResult(code);
+    }
+
+    void notifyServiceStateChanged(ServiceState ss) {
+        super.notifyServiceStateChangedP(ss);
+    }
+
+    @Override
+    public void notifyCallForwardingIndicator() {
+        mNotifier.notifyCallForwardingChanged(this);
+    }
+
+    public boolean canDial() {
+        int serviceState = getServiceState().getState();
+        Rlog.v(LOG_TAG, "canDial(): serviceState = " + serviceState);
+        if (serviceState == ServiceState.STATE_POWER_OFF) return false;
+
+        String disableCall = SystemProperties.get(
+                TelephonyProperties.PROPERTY_DISABLE_CALL, "false");
+        Rlog.v(LOG_TAG, "canDial(): disableCall = " + disableCall);
+        if (disableCall.equals("true")) return false;
+
+        Rlog.v(LOG_TAG, "canDial(): ringingCall: " + getRingingCall().getState());
+        Rlog.v(LOG_TAG, "canDial(): foregndCall: " + getForegroundCall().getState());
+        Rlog.v(LOG_TAG, "canDial(): backgndCall: " + getBackgroundCall().getState());
+        return !getRingingCall().isRinging()
+                && (!getForegroundCall().getState().isAlive()
+                    || !getBackgroundCall().getState().isAlive());
+    }
+
+    @Override
+    public boolean handleInCallMmiCommands(String dialString) {
+        return false;
+    }
+
+    boolean isInCall() {
+        Call.State foregroundCallState = getForegroundCall().getState();
+        Call.State backgroundCallState = getBackgroundCall().getState();
+        Call.State ringingCallState = getRingingCall().getState();
+
+       return (foregroundCallState.isAlive() || backgroundCallState.isAlive()
+               || ringingCallState.isAlive());
+    }
+
+    @Override
+    public boolean handlePinMmi(String dialString) {
+        return false;
+    }
+
+    @Override
+    public void sendUssdResponse(String ussdMessge) {
+    }
+
+    @Override
+    public void registerForSuppServiceNotification(
+            Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void unregisterForSuppServiceNotification(Handler h) {
+    }
+
+    @Override
+    public void setRadioPower(boolean power) {
+    }
+
+    @Override
+    public String getVoiceMailNumber() {
+        return null;
+    }
+
+    @Override
+    public String getVoiceMailAlphaTag() {
+        return null;
+    }
+
+    @Override
+    public String getDeviceId() {
+        return null;
+    }
+
+    @Override
+    public String getDeviceSvn() {
+        return null;
+    }
+
+    @Override
+    public String getImei() {
+        return null;
+    }
+
+    @Override
+    public String getEsn() {
+        Rlog.e(LOG_TAG, "[VoltePhone] getEsn() is a CDMA method");
+        return "0";
+    }
+
+    @Override
+    public String getMeid() {
+        Rlog.e(LOG_TAG, "[VoltePhone] getMeid() is a CDMA method");
+        return "0";
+    }
+
+    @Override
+    public String getSubscriberId() {
+        return null;
+    }
+
+    @Override
+    public String getGroupIdLevel1() {
+        return null;
+    }
+
+    @Override
+    public String getGroupIdLevel2() {
+        return null;
+    }
+
+    @Override
+    public String getIccSerialNumber() {
+        return null;
+    }
+
+    @Override
+    public String getLine1Number() {
+        return null;
+    }
+
+    @Override
+    public String getLine1AlphaTag() {
+        return null;
+    }
+
+    @Override
+    public boolean setLine1Number(String alphaTag, String number, Message onComplete) {
+        // FIXME: what to reply for Volte?
+        return false;
+    }
+
+    @Override
+    public void setVoiceMailNumber(String alphaTag, String voiceMailNumber,
+            Message onComplete) {
+        // FIXME: what to reply for Volte?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void getCallForwardingOption(int commandInterfaceCFReason, Message onComplete) {
+    }
+
+    @Override
+    public void setCallForwardingOption(int commandInterfaceCFAction,
+            int commandInterfaceCFReason, String dialingNumber,
+            int timerSeconds, Message onComplete) {
+    }
+
+    @Override
+    public void getOutgoingCallerIdDisplay(Message onComplete) {
+        // FIXME: what to reply?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode,
+            Message onComplete) {
+        // FIXME: what's this for Volte?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void getCallWaiting(Message onComplete) {
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, Message onComplete) {
+        Rlog.e(LOG_TAG, "call waiting not supported");
+    }
+
+    @Override
+    public boolean getIccRecordsLoaded() {
+        return false;
+    }
+
+    @Override
+    public IccCard getIccCard() {
+        return null;
+    }
+
+    @Override
+    public void getAvailableNetworks(Message response) {
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message response) {
+    }
+
+    @Override
+    public void stopNetworkScan(Message response) {
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message response) {
+    }
+
+    @Override
+    public void selectNetworkManually(OperatorInfo network, boolean persistSelection,
+            Message response) {
+    }
+
+    @Override
+    public void getDataCallList(Message response) {
+    }
+
+    public List<DataConnection> getCurrentDataConnectionList () {
+        return null;
+    }
+
+    @Override
+    public void updateServiceLocation() {
+    }
+
+    @Override
+    public void enableLocationUpdates() {
+    }
+
+    @Override
+    public void disableLocationUpdates() {
+    }
+
+    @Override
+    public boolean getDataRoamingEnabled() {
+        return false;
+    }
+
+    @Override
+    public void setDataRoamingEnabled(boolean enable) {
+    }
+
+    @Override
+    public boolean getDataEnabled() {
+        return false;
+    }
+
+    @Override
+    public void setDataEnabled(boolean enable) {
+    }
+
+
+    public boolean enableDataConnectivity() {
+        return false;
+    }
+
+    public boolean disableDataConnectivity() {
+        return false;
+    }
+
+    @Override
+    public boolean isDataAllowed() {
+        return false;
+    }
+
+    public void saveClirSetting(int commandInterfaceCLIRMode) {
+    }
+
+    @Override
+    public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(){
+        return null;
+    }
+
+    @Override
+    public IccFileHandler getIccFileHandler(){
+        return null;
+    }
+
+    @Override
+    public void activateCellBroadcastSms(int activate, Message response) {
+        Rlog.e(LOG_TAG, "Error! This functionality is not implemented for Volte.");
+    }
+
+    @Override
+    public void getCellBroadcastSmsConfig(Message response) {
+        Rlog.e(LOG_TAG, "Error! This functionality is not implemented for Volte.");
+    }
+
+    @Override
+    public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response){
+        Rlog.e(LOG_TAG, "Error! This functionality is not implemented for Volte.");
+    }
+
+    //@Override
+    @Override
+    public boolean needsOtaServiceProvisioning() {
+        // FIXME: what's this for Volte?
+        return false;
+    }
+
+    //@Override
+    @Override
+    public LinkProperties getLinkProperties(String apnType) {
+        // FIXME: what's this for Volte?
+        return null;
+    }
+
+    @Override
+    protected void onUpdateIccAvailability() {
+    }
+
+    void updatePhoneState() {
+        PhoneConstants.State oldState = mState;
+
+        if (getRingingCall().isRinging()) {
+            mState = PhoneConstants.State.RINGING;
+        } else if (getForegroundCall().isIdle()
+                && getBackgroundCall().isIdle()) {
+            mState = PhoneConstants.State.IDLE;
+        } else {
+            mState = PhoneConstants.State.OFFHOOK;
+        }
+
+        if (mState != oldState) {
+            Rlog.d(LOG_TAG, " ^^^ new phone state: " + mState);
+            notifyPhoneStateChanged();
+        }
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCall.java b/com/android/internal/telephony/imsphone/ImsPhoneCall.java
new file mode 100644
index 0000000..52636f5
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCall.java
@@ -0,0 +1,370 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import android.telecom.ConferenceParticipant;
+import android.telephony.Rlog;
+import android.telephony.DisconnectCause;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.ims.ImsCall;
+import com.android.ims.ImsException;
+import com.android.ims.ImsStreamMediaProfile;
+
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public class ImsPhoneCall extends Call {
+    private static final String LOG_TAG = "ImsPhoneCall";
+
+    // This flag is meant to be used as a debugging tool to quickly see all logs
+    // regardless of the actual log level set on this component.
+    private static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */
+    private static final boolean DBG = FORCE_DEBUG || Rlog.isLoggable(LOG_TAG, Log.DEBUG);
+    private static final boolean VDBG = FORCE_DEBUG || Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
+
+    /*************************** Instance Variables **************************/
+    public static final String CONTEXT_UNKNOWN = "UK";
+    public static final String CONTEXT_RINGING = "RG";
+    public static final String CONTEXT_FOREGROUND = "FG";
+    public static final String CONTEXT_BACKGROUND = "BG";
+    public static final String CONTEXT_HANDOVER = "HO";
+
+    /*package*/ ImsPhoneCallTracker mOwner;
+
+    private boolean mRingbackTonePlayed = false;
+
+    // Determines what type of ImsPhoneCall this is.  ImsPhoneCallTracker uses instances of
+    // ImsPhoneCall to for fg, bg, etc calls.  This is used as a convenience for logging so that it
+    // can be made clear whether a call being logged is the foreground, background, etc.
+    private final String mCallContext;
+
+    /****************************** Constructors *****************************/
+    /*package*/
+    ImsPhoneCall() {
+        mCallContext = CONTEXT_UNKNOWN;
+    }
+
+    public ImsPhoneCall(ImsPhoneCallTracker owner, String context) {
+        mOwner = owner;
+        mCallContext = context;
+    }
+
+    public void dispose() {
+        try {
+            mOwner.hangup(this);
+        } catch (CallStateException ex) {
+            //Rlog.e(LOG_TAG, "dispose: unexpected error on hangup", ex);
+            //while disposing, ignore the exception and clean the connections
+        } finally {
+            for(int i = 0, s = mConnections.size(); i < s; i++) {
+                ImsPhoneConnection c = (ImsPhoneConnection) mConnections.get(i);
+                c.onDisconnect(DisconnectCause.LOST_SIGNAL);
+            }
+        }
+    }
+
+    /************************** Overridden from Call *************************/
+
+    @Override
+    public List<Connection>
+    getConnections() {
+        return mConnections;
+    }
+
+    @Override
+    public Phone
+    getPhone() {
+        return mOwner.mPhone;
+    }
+
+    @Override
+    public boolean
+    isMultiparty() {
+        ImsCall imsCall = getImsCall();
+        if (imsCall == null) {
+            return false;
+        }
+
+        return imsCall.isMultiparty();
+    }
+
+    /** Please note: if this is the foreground call and a
+     *  background call exists, the background call will be resumed.
+     */
+    @Override
+    public void
+    hangup() throws CallStateException {
+        mOwner.hangup(this);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[ImsPhoneCall ");
+        sb.append(mCallContext);
+        sb.append(" state: ");
+        sb.append(mState.toString());
+        sb.append(" ");
+        if (mConnections.size() > 1) {
+            sb.append(" ERROR_MULTIPLE ");
+        }
+        for (Connection conn : mConnections) {
+            sb.append(conn);
+            sb.append(" ");
+        }
+
+        sb.append("]");
+        return sb.toString();
+    }
+
+    @Override
+    public List<ConferenceParticipant> getConferenceParticipants() {
+         ImsCall call = getImsCall();
+         if (call == null) {
+             return null;
+         }
+         return call.getConferenceParticipants();
+    }
+
+    //***** Called from ImsPhoneConnection
+
+    public void attach(Connection conn) {
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "attach : " + mCallContext + " conn = " + conn);
+        }
+        clearDisconnected();
+        mConnections.add(conn);
+
+        mOwner.logState();
+    }
+
+    public void attach(Connection conn, State state) {
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "attach : " + mCallContext + " state = " +
+                    state.toString());
+        }
+        this.attach(conn);
+        mState = state;
+    }
+
+    public void attachFake(Connection conn, State state) {
+        attach(conn, state);
+    }
+
+    /**
+     * Called by ImsPhoneConnection when it has disconnected
+     */
+    public boolean connectionDisconnected(ImsPhoneConnection conn) {
+        if (mState != State.DISCONNECTED) {
+            /* If only disconnected connections remain, we are disconnected*/
+
+            boolean hasOnlyDisconnectedConnections = true;
+
+            for (int i = 0, s = mConnections.size()  ; i < s; i ++) {
+                if (mConnections.get(i).getState() != State.DISCONNECTED) {
+                    hasOnlyDisconnectedConnections = false;
+                    break;
+                }
+            }
+
+            if (hasOnlyDisconnectedConnections) {
+                mState = State.DISCONNECTED;
+                if (VDBG) {
+                    Rlog.v(LOG_TAG, "connectionDisconnected : " + mCallContext + " state = " +
+                            mState);
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public void detach(ImsPhoneConnection conn) {
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "detach : " + mCallContext + " conn = " + conn);
+        }
+        mConnections.remove(conn);
+        clearDisconnected();
+
+        mOwner.logState();
+    }
+
+    /**
+     * @return true if there's no space in this call for additional
+     * connections to be added via "conference"
+     */
+    /*package*/ boolean
+    isFull() {
+        return mConnections.size() == ImsPhoneCallTracker.MAX_CONNECTIONS_PER_CALL;
+    }
+
+    //***** Called from ImsPhoneCallTracker
+    /**
+     * Called when this Call is being hung up locally (eg, user pressed "end")
+     */
+    void
+    onHangupLocal() {
+        for (int i = 0, s = mConnections.size(); i < s; i++) {
+            ImsPhoneConnection cn = (ImsPhoneConnection)mConnections.get(i);
+            cn.onHangupLocal();
+        }
+        mState = State.DISCONNECTING;
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "onHangupLocal : " + mCallContext + " state = " + mState);
+        }
+    }
+
+    /*package*/ ImsPhoneConnection
+    getFirstConnection() {
+        if (mConnections.size() == 0) return null;
+
+        return (ImsPhoneConnection) mConnections.get(0);
+    }
+
+    /*package*/ void
+    setMute(boolean mute) {
+        ImsCall imsCall = getFirstConnection() == null ?
+                null : getFirstConnection().getImsCall();
+        if (imsCall != null) {
+            try {
+                imsCall.setMute(mute);
+            } catch (ImsException e) {
+                Rlog.e(LOG_TAG, "setMute failed : " + e.getMessage());
+            }
+        }
+    }
+
+    /* package */ void
+    merge(ImsPhoneCall that, State state) {
+        // This call is the conference host and the "that" call is the one being merged in.
+        // Set the connect time for the conference; this will have been determined when the
+        // conference was initially created.
+        ImsPhoneConnection imsPhoneConnection = getFirstConnection();
+        if (imsPhoneConnection != null) {
+            long conferenceConnectTime = imsPhoneConnection.getConferenceConnectTime();
+            if (conferenceConnectTime > 0) {
+                imsPhoneConnection.setConnectTime(conferenceConnectTime);
+                imsPhoneConnection.setConnectTimeReal(imsPhoneConnection.getConnectTimeReal());
+            } else {
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "merge: conference connect time is 0");
+                }
+            }
+        }
+        if (DBG) {
+            Rlog.d(LOG_TAG, "merge(" + mCallContext + "): " + that + "state = "
+                    + state);
+        }
+    }
+
+    /**
+     * Retrieves the {@link ImsCall} for the current {@link ImsPhoneCall}.
+     * <p>
+     * Marked as {@code VisibleForTesting} so that the
+     * {@link com.android.internal.telephony.TelephonyTester} class can inject a test conference
+     * event package into a regular ongoing IMS call.
+     *
+     * @return The {@link ImsCall}.
+     */
+    @VisibleForTesting
+    public ImsCall
+    getImsCall() {
+        return (getFirstConnection() == null) ? null : getFirstConnection().getImsCall();
+    }
+
+    /*package*/ static boolean isLocalTone(ImsCall imsCall) {
+        if ((imsCall == null) || (imsCall.getCallProfile() == null)
+                || (imsCall.getCallProfile().mMediaProfile == null)) {
+            return false;
+        }
+
+        ImsStreamMediaProfile mediaProfile = imsCall.getCallProfile().mMediaProfile;
+
+        return (mediaProfile.mAudioDirection == ImsStreamMediaProfile.DIRECTION_INACTIVE)
+                ? true : false;
+    }
+
+    public boolean update (ImsPhoneConnection conn, ImsCall imsCall, State state) {
+        boolean changed = false;
+        State oldState = mState;
+
+        //ImsCall.Listener.onCallProgressing can be invoked several times
+        //and ringback tone mode can be changed during the call setup procedure
+        if (state == State.ALERTING) {
+            if (mRingbackTonePlayed && !isLocalTone(imsCall)) {
+                mOwner.mPhone.stopRingbackTone();
+                mRingbackTonePlayed = false;
+            } else if (!mRingbackTonePlayed && isLocalTone(imsCall)) {
+                mOwner.mPhone.startRingbackTone();
+                mRingbackTonePlayed = true;
+            }
+        } else {
+            if (mRingbackTonePlayed) {
+                mOwner.mPhone.stopRingbackTone();
+                mRingbackTonePlayed = false;
+            }
+        }
+
+        if ((state != mState) && (state != State.DISCONNECTED)) {
+            mState = state;
+            changed = true;
+        } else if (state == State.DISCONNECTED) {
+            changed = true;
+        }
+
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "update : " + mCallContext + " state: " + oldState + " --> " + mState);
+        }
+
+        return changed;
+    }
+
+    /* package */ ImsPhoneConnection
+    getHandoverConnection() {
+        return (ImsPhoneConnection) getEarliestConnection();
+    }
+
+    public void switchWith(ImsPhoneCall that) {
+        if (VDBG) {
+            Rlog.v(LOG_TAG, "switchWith : switchCall = " + this + " withCall = " + that);
+        }
+        synchronized (ImsPhoneCall.class) {
+            ImsPhoneCall tmp = new ImsPhoneCall();
+            tmp.takeOver(this);
+            this.takeOver(that);
+            that.takeOver(tmp);
+        }
+        mOwner.logState();
+    }
+
+    private void takeOver(ImsPhoneCall that) {
+        mConnections = that.mConnections;
+        mState = that.mState;
+        for (Connection c : mConnections) {
+            ((ImsPhoneConnection) c).changeParent(this);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
new file mode 100644
index 0000000..abaf061
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -0,0 +1,3628 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkStats;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.telecom.ConferenceParticipant;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.PreciseDisconnectCause;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsServiceProxy;
+import android.telephony.ims.feature.ImsFeature;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseIntArray;
+
+import com.android.ims.ImsCall;
+import com.android.ims.ImsCallProfile;
+import com.android.ims.ImsConfig;
+import com.android.ims.ImsConfigListener;
+import com.android.ims.ImsConnectionStateListener;
+import com.android.ims.ImsEcbm;
+import com.android.ims.ImsException;
+import com.android.ims.ImsManager;
+import com.android.ims.ImsMultiEndpoint;
+import com.android.ims.ImsReasonInfo;
+import com.android.ims.ImsServiceClass;
+import com.android.ims.ImsSuppServiceNotification;
+import com.android.ims.ImsUtInterface;
+import com.android.ims.internal.IImsVideoCallProvider;
+import com.android.ims.internal.ImsVideoCallProviderWrapper;
+import com.android.ims.internal.VideoPauseTracker;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.CallTracker;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.dataconnection.DataEnabledSettings;
+import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.ImsCommand;
+import com.android.server.net.NetworkStatsService;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ */
+public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
+    static final String LOG_TAG = "ImsPhoneCallTracker";
+    static final String VERBOSE_STATE_TAG = "IPCTState";
+
+    public interface PhoneStateListener {
+        void onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState);
+    }
+
+    public interface SharedPreferenceProxy {
+        SharedPreferences getDefaultSharedPreferences(Context context);
+    }
+
+    public interface PhoneNumberUtilsProxy {
+        boolean isEmergencyNumber(String number);
+    }
+
+    private static final boolean DBG = true;
+
+    // When true, dumps the state of ImsPhoneCallTracker after changes to foreground and background
+    // calls.  This is helpful for debugging.  It is also possible to enable this at runtime by
+    // setting the IPCTState log tag to VERBOSE.
+    private static final boolean FORCE_VERBOSE_STATE_LOGGING = false; /* stopship if true */
+    private static final boolean VERBOSE_STATE_LOGGING = FORCE_VERBOSE_STATE_LOGGING ||
+            Rlog.isLoggable(VERBOSE_STATE_TAG, Log.VERBOSE);
+
+    //Indices map to ImsConfig.FeatureConstants
+    private boolean[] mImsFeatureEnabled = {false, false, false, false, false, false};
+    private final String[] mImsFeatureStrings = {"VoLTE", "ViLTE", "VoWiFi", "ViWiFi",
+            "UTLTE", "UTWiFi"};
+
+    private TelephonyMetrics mMetrics;
+
+    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(ImsManager.ACTION_IMS_INCOMING_CALL)) {
+                if (DBG) log("onReceive : incoming call intent");
+
+                if (mImsManager == null) return;
+
+                if (mServiceId < 0) return;
+
+                try {
+                    // Network initiated USSD will be treated by mImsUssdListener
+                    boolean isUssd = intent.getBooleanExtra(ImsManager.EXTRA_USSD, false);
+                    if (isUssd) {
+                        if (DBG) log("onReceive : USSD");
+                        mUssdSession = mImsManager.takeCall(mServiceId, intent, mImsUssdListener);
+                        if (mUssdSession != null) {
+                            mUssdSession.accept(ImsCallProfile.CALL_TYPE_VOICE);
+                        }
+                        return;
+                    }
+
+                    boolean isUnknown = intent.getBooleanExtra(ImsManager.EXTRA_IS_UNKNOWN_CALL,
+                            false);
+                    if (DBG) {
+                        log("onReceive : isUnknown = " + isUnknown +
+                                " fg = " + mForegroundCall.getState() +
+                                " bg = " + mBackgroundCall.getState());
+                    }
+
+                    // Normal MT/Unknown call
+                    ImsCall imsCall = mImsManager.takeCall(mServiceId, intent, mImsCallListener);
+                    ImsPhoneConnection conn = new ImsPhoneConnection(mPhone, imsCall,
+                            ImsPhoneCallTracker.this,
+                            (isUnknown? mForegroundCall: mRingingCall), isUnknown);
+
+                    // If there is an active call.
+                    if (mForegroundCall.hasConnections()) {
+                        ImsCall activeCall = mForegroundCall.getFirstConnection().getImsCall();
+                        if (activeCall != null && imsCall != null) {
+                            // activeCall could be null if the foreground call is in a disconnected
+                            // state.  If either of the calls is null there is no need to check if
+                            // one will be disconnected on answer.
+                            boolean answeringWillDisconnect =
+                                    shouldDisconnectActiveCallOnAnswer(activeCall, imsCall);
+                            conn.setActiveCallDisconnectedOnAnswer(answeringWillDisconnect);
+                        }
+                    }
+                    conn.setAllowAddCallDuringVideoCall(mAllowAddCallDuringVideoCall);
+                    addConnection(conn);
+
+                    setVideoCallProvider(conn, imsCall);
+
+                    TelephonyMetrics.getInstance().writeOnImsCallReceive(mPhone.getPhoneId(),
+                            imsCall.getSession());
+
+                    if (isUnknown) {
+                        mPhone.notifyUnknownConnection(conn);
+                    } else {
+                        if ((mForegroundCall.getState() != ImsPhoneCall.State.IDLE) ||
+                                (mBackgroundCall.getState() != ImsPhoneCall.State.IDLE)) {
+                            conn.update(imsCall, ImsPhoneCall.State.WAITING);
+                        }
+
+                        mPhone.notifyNewRingingConnection(conn);
+                        mPhone.notifyIncomingRing();
+                    }
+
+                    updatePhoneState();
+                    mPhone.notifyPreciseCallStateChanged();
+                } catch (ImsException e) {
+                    loge("onReceive : exception " + e);
+                } catch (RemoteException e) {
+                }
+            } else if (intent.getAction().equals(
+                    CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+                if (subId == mPhone.getSubId()) {
+                    cacheCarrierConfiguration(subId);
+                    log("onReceive : Updating mAllowEmergencyVideoCalls = " +
+                            mAllowEmergencyVideoCalls);
+                }
+            } else if (TelecomManager.ACTION_CHANGE_DEFAULT_DIALER.equals(intent.getAction())) {
+                mDefaultDialerUid.set(getPackageUid(context, intent.getStringExtra(
+                        TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME)));
+            }
+        }
+    };
+
+    //***** Constants
+
+    static final int MAX_CONNECTIONS = 7;
+    static final int MAX_CONNECTIONS_PER_CALL = 5;
+
+    private static final int EVENT_HANGUP_PENDINGMO = 18;
+    private static final int EVENT_RESUME_BACKGROUND = 19;
+    private static final int EVENT_DIAL_PENDINGMO = 20;
+    private static final int EVENT_EXIT_ECBM_BEFORE_PENDINGMO = 21;
+    private static final int EVENT_VT_DATA_USAGE_UPDATE = 22;
+    private static final int EVENT_DATA_ENABLED_CHANGED = 23;
+    private static final int EVENT_GET_IMS_SERVICE = 24;
+    private static final int EVENT_CHECK_FOR_WIFI_HANDOVER = 25;
+    private static final int EVENT_ON_FEATURE_CAPABILITY_CHANGED = 26;
+
+    private static final int TIMEOUT_HANGUP_PENDINGMO = 500;
+
+    // Initial condition for ims connection retry.
+    private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms
+    // Ceiling bitshift amount for service query timeout, calculated as:
+    // 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
+    // mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
+    private static final int CEILING_SERVICE_RETRY_COUNT = 6;
+
+    private static final int HANDOVER_TO_WIFI_TIMEOUT_MS = 60000; // ms
+
+    //***** Instance Variables
+    private ArrayList<ImsPhoneConnection> mConnections = new ArrayList<ImsPhoneConnection>();
+    private RegistrantList mVoiceCallEndedRegistrants = new RegistrantList();
+    private RegistrantList mVoiceCallStartedRegistrants = new RegistrantList();
+
+    public ImsPhoneCall mRingingCall = new ImsPhoneCall(this, ImsPhoneCall.CONTEXT_RINGING);
+    public ImsPhoneCall mForegroundCall = new ImsPhoneCall(this,
+            ImsPhoneCall.CONTEXT_FOREGROUND);
+    public ImsPhoneCall mBackgroundCall = new ImsPhoneCall(this,
+            ImsPhoneCall.CONTEXT_BACKGROUND);
+    public ImsPhoneCall mHandoverCall = new ImsPhoneCall(this, ImsPhoneCall.CONTEXT_HANDOVER);
+
+    // Hold aggregated video call data usage for each video call since boot.
+    // The ImsCall's call id is the key of the map.
+    private final HashMap<Integer, Long> mVtDataUsageMap = new HashMap<>();
+
+    private volatile NetworkStats mVtDataUsageSnapshot = null;
+    private volatile NetworkStats mVtDataUsageUidSnapshot = null;
+
+    private final AtomicInteger mDefaultDialerUid = new AtomicInteger(NetworkStats.UID_ALL);
+
+    private ImsPhoneConnection mPendingMO;
+    private int mClirMode = CommandsInterface.CLIR_DEFAULT;
+    private Object mSyncHold = new Object();
+
+    private ImsCall mUssdSession = null;
+    private Message mPendingUssd = null;
+
+    ImsPhone mPhone;
+
+    private boolean mDesiredMute = false;    // false = mute off
+    private boolean mOnHoldToneStarted = false;
+    private int mOnHoldToneId = -1;
+
+    private PhoneConstants.State mState = PhoneConstants.State.IDLE;
+
+    private int mImsServiceRetryCount;
+    private ImsManager mImsManager;
+    private int mServiceId = -1;
+
+    private Call.SrvccState mSrvccState = Call.SrvccState.NONE;
+
+    private boolean mIsInEmergencyCall = false;
+    private boolean mIsDataEnabled = false;
+
+    private int pendingCallClirMode;
+    private int mPendingCallVideoState;
+    private Bundle mPendingIntentExtras;
+    private boolean pendingCallInEcm = false;
+    private boolean mSwitchingFgAndBgCalls = false;
+    private ImsCall mCallExpectedToResume = null;
+    private boolean mAllowEmergencyVideoCalls = false;
+    private boolean mIgnoreDataEnabledChangedForVideoCalls = false;
+    private boolean mIsViLteDataMetered = false;
+
+    /**
+     * Listeners to changes in the phone state.  Intended for use by other interested IMS components
+     * without the need to register a full blown {@link android.telephony.PhoneStateListener}.
+     */
+    private List<PhoneStateListener> mPhoneStateListeners = new ArrayList<>();
+
+    /**
+     * Carrier configuration option which determines if video calls which have been downgraded to an
+     * audio call should be treated as if they are still video calls.
+     */
+    private boolean mTreatDowngradedVideoCallsAsVideoCalls = false;
+
+    /**
+     * Carrier configuration option which determines if an ongoing video call over wifi should be
+     * dropped when an audio call is answered.
+     */
+    private boolean mDropVideoCallWhenAnsweringAudioCall = false;
+
+    /**
+     * Carrier configuration option which determines whether adding a call during a video call
+     * should be allowed.
+     */
+    private boolean mAllowAddCallDuringVideoCall = true;
+
+    /**
+     * Carrier configuration option which determines whether to notify the connection if a handover
+     * to wifi fails.
+     */
+    private boolean mNotifyVtHandoverToWifiFail = false;
+
+    /**
+     * Carrier configuration option which determines whether the carrier supports downgrading a
+     * TX/RX/TX-RX video call directly to an audio-only call.
+     */
+    private boolean mSupportDowngradeVtToAudio = false;
+
+    /**
+     * Stores the mapping of {@code ImsReasonInfo#CODE_*} to {@code PreciseDisconnectCause#*}
+     */
+    private static final SparseIntArray PRECISE_CAUSE_MAP = new SparseIntArray();
+    static {
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_ILLEGAL_ARGUMENT,
+                PreciseDisconnectCause.LOCAL_ILLEGAL_ARGUMENT);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE,
+                PreciseDisconnectCause.LOCAL_ILLEGAL_STATE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_INTERNAL_ERROR,
+                PreciseDisconnectCause.LOCAL_INTERNAL_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN,
+                PreciseDisconnectCause.LOCAL_IMS_SERVICE_DOWN);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_NO_PENDING_CALL,
+                PreciseDisconnectCause.LOCAL_NO_PENDING_CALL);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_ENDED_BY_CONFERENCE_MERGE,
+                PreciseDisconnectCause.NORMAL);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_POWER_OFF,
+                PreciseDisconnectCause.LOCAL_POWER_OFF);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_LOW_BATTERY,
+                PreciseDisconnectCause.LOCAL_LOW_BATTERY);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_NETWORK_NO_SERVICE,
+                PreciseDisconnectCause.LOCAL_NETWORK_NO_SERVICE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_NETWORK_NO_LTE_COVERAGE,
+                PreciseDisconnectCause.LOCAL_NETWORK_NO_LTE_COVERAGE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_NETWORK_ROAMING,
+                PreciseDisconnectCause.LOCAL_NETWORK_ROAMING);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_NETWORK_IP_CHANGED,
+                PreciseDisconnectCause.LOCAL_NETWORK_IP_CHANGED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE,
+                PreciseDisconnectCause.LOCAL_SERVICE_UNAVAILABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED,
+                PreciseDisconnectCause.LOCAL_NOT_REGISTERED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_EXCEEDED,
+                PreciseDisconnectCause.LOCAL_MAX_CALL_EXCEEDED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_DECLINE,
+                PreciseDisconnectCause.LOCAL_CALL_DECLINE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_VCC_ON_PROGRESSING,
+                PreciseDisconnectCause.LOCAL_CALL_VCC_ON_PROGRESSING);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_RESOURCE_RESERVATION_FAILED,
+                PreciseDisconnectCause.LOCAL_CALL_RESOURCE_RESERVATION_FAILED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED,
+                PreciseDisconnectCause.LOCAL_CALL_CS_RETRY_REQUIRED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_VOLTE_RETRY_REQUIRED,
+                PreciseDisconnectCause.LOCAL_CALL_VOLTE_RETRY_REQUIRED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED,
+                PreciseDisconnectCause.LOCAL_CALL_TERMINATED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOCAL_HO_NOT_FEASIBLE,
+                PreciseDisconnectCause.LOCAL_HO_NOT_FEASIBLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_TIMEOUT_1XX_WAITING,
+                PreciseDisconnectCause.TIMEOUT_1XX_WAITING);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER,
+                PreciseDisconnectCause.TIMEOUT_NO_ANSWER);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER_CALL_UPDATE,
+                PreciseDisconnectCause.TIMEOUT_NO_ANSWER_CALL_UPDATE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_FDN_BLOCKED,
+                PreciseDisconnectCause.FDN_BLOCKED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_REDIRECTED,
+                PreciseDisconnectCause.SIP_REDIRECTED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_BAD_REQUEST,
+                PreciseDisconnectCause.SIP_BAD_REQUEST);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_FORBIDDEN,
+                PreciseDisconnectCause.SIP_FORBIDDEN);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_NOT_FOUND,
+                PreciseDisconnectCause.SIP_NOT_FOUND);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_NOT_SUPPORTED,
+                PreciseDisconnectCause.SIP_NOT_SUPPORTED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_REQUEST_TIMEOUT,
+                PreciseDisconnectCause.SIP_REQUEST_TIMEOUT);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_TEMPRARILY_UNAVAILABLE,
+                PreciseDisconnectCause.SIP_TEMPRARILY_UNAVAILABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_BAD_ADDRESS,
+                PreciseDisconnectCause.SIP_BAD_ADDRESS);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_BUSY,
+                PreciseDisconnectCause.SIP_BUSY);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_REQUEST_CANCELLED,
+                PreciseDisconnectCause.SIP_REQUEST_CANCELLED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_NOT_ACCEPTABLE,
+                PreciseDisconnectCause.SIP_NOT_ACCEPTABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_NOT_REACHABLE,
+                PreciseDisconnectCause.SIP_NOT_REACHABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_CLIENT_ERROR,
+                PreciseDisconnectCause.SIP_CLIENT_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_SERVER_INTERNAL_ERROR,
+                PreciseDisconnectCause.SIP_SERVER_INTERNAL_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_SERVICE_UNAVAILABLE,
+                PreciseDisconnectCause.SIP_SERVICE_UNAVAILABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_SERVER_TIMEOUT,
+                PreciseDisconnectCause.SIP_SERVER_TIMEOUT);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_SERVER_ERROR,
+                PreciseDisconnectCause.SIP_SERVER_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_USER_REJECTED,
+                PreciseDisconnectCause.SIP_USER_REJECTED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SIP_GLOBAL_ERROR,
+                PreciseDisconnectCause.SIP_GLOBAL_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_EMERGENCY_TEMP_FAILURE,
+                PreciseDisconnectCause.EMERGENCY_TEMP_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_EMERGENCY_PERM_FAILURE,
+                PreciseDisconnectCause.EMERGENCY_PERM_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_MEDIA_INIT_FAILED,
+                PreciseDisconnectCause.MEDIA_INIT_FAILED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_MEDIA_NO_DATA,
+                PreciseDisconnectCause.MEDIA_NO_DATA);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_MEDIA_NOT_ACCEPTABLE,
+                PreciseDisconnectCause.MEDIA_NOT_ACCEPTABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_MEDIA_UNSPECIFIED,
+                PreciseDisconnectCause.MEDIA_UNSPECIFIED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_USER_TERMINATED,
+                PreciseDisconnectCause.USER_TERMINATED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_USER_NOANSWER,
+                PreciseDisconnectCause.USER_NOANSWER);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_USER_IGNORE,
+                PreciseDisconnectCause.USER_IGNORE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_USER_DECLINE,
+                PreciseDisconnectCause.USER_DECLINE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_LOW_BATTERY,
+                PreciseDisconnectCause.LOW_BATTERY);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_BLACKLISTED_CALL_ID,
+                PreciseDisconnectCause.BLACKLISTED_CALL_ID);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE,
+                PreciseDisconnectCause.USER_TERMINATED_BY_REMOTE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_UT_NOT_SUPPORTED,
+                PreciseDisconnectCause.UT_NOT_SUPPORTED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_UT_SERVICE_UNAVAILABLE,
+                PreciseDisconnectCause.UT_SERVICE_UNAVAILABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_UT_OPERATION_NOT_ALLOWED,
+                PreciseDisconnectCause.UT_OPERATION_NOT_ALLOWED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_UT_NETWORK_ERROR,
+                PreciseDisconnectCause.UT_NETWORK_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_UT_CB_PASSWORD_MISMATCH,
+                PreciseDisconnectCause.UT_CB_PASSWORD_MISMATCH);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_ECBM_NOT_SUPPORTED,
+                PreciseDisconnectCause.ECBM_NOT_SUPPORTED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_MULTIENDPOINT_NOT_SUPPORTED,
+                PreciseDisconnectCause.MULTIENDPOINT_NOT_SUPPORTED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_CALL_DROP_IWLAN_TO_LTE_UNAVAILABLE,
+                PreciseDisconnectCause.CALL_DROP_IWLAN_TO_LTE_UNAVAILABLE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_ANSWERED_ELSEWHERE,
+                PreciseDisconnectCause.ANSWERED_ELSEWHERE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_CALL_PULL_OUT_OF_SYNC,
+                PreciseDisconnectCause.CALL_PULL_OUT_OF_SYNC);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_CALL_END_CAUSE_CALL_PULL,
+                PreciseDisconnectCause.CALL_PULLED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SUPP_SVC_FAILED,
+                PreciseDisconnectCause.SUPP_SVC_FAILED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SUPP_SVC_CANCELLED,
+                PreciseDisconnectCause.SUPP_SVC_CANCELLED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_SUPP_SVC_REINVITE_COLLISION,
+                PreciseDisconnectCause.SUPP_SVC_REINVITE_COLLISION);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_IWLAN_DPD_FAILURE,
+                PreciseDisconnectCause.IWLAN_DPD_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_EPDG_TUNNEL_ESTABLISH_FAILURE,
+                PreciseDisconnectCause.EPDG_TUNNEL_ESTABLISH_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_EPDG_TUNNEL_REKEY_FAILURE,
+                PreciseDisconnectCause.EPDG_TUNNEL_REKEY_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_EPDG_TUNNEL_LOST_CONNECTION,
+                PreciseDisconnectCause.EPDG_TUNNEL_LOST_CONNECTION);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_MAXIMUM_NUMBER_OF_CALLS_REACHED,
+                PreciseDisconnectCause.MAXIMUM_NUMBER_OF_CALLS_REACHED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_REMOTE_CALL_DECLINE,
+                PreciseDisconnectCause.REMOTE_CALL_DECLINE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_DATA_LIMIT_REACHED,
+                PreciseDisconnectCause.DATA_LIMIT_REACHED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_DATA_DISABLED,
+                PreciseDisconnectCause.DATA_DISABLED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_WIFI_LOST,
+                PreciseDisconnectCause.WIFI_LOST);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_OFF,
+                PreciseDisconnectCause.RADIO_OFF);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_NO_VALID_SIM,
+                PreciseDisconnectCause.NO_VALID_SIM);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_INTERNAL_ERROR,
+                PreciseDisconnectCause.RADIO_INTERNAL_ERROR);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_NETWORK_RESP_TIMEOUT,
+                PreciseDisconnectCause.NETWORK_RESP_TIMEOUT);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_NETWORK_REJECT,
+                PreciseDisconnectCause.NETWORK_REJECT);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_ACCESS_FAILURE,
+                PreciseDisconnectCause.RADIO_ACCESS_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_LINK_FAILURE,
+                PreciseDisconnectCause.RADIO_LINK_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_LINK_LOST,
+                PreciseDisconnectCause.RADIO_LINK_LOST);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_UPLINK_FAILURE,
+                PreciseDisconnectCause.RADIO_UPLINK_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_SETUP_FAILURE,
+                PreciseDisconnectCause.RADIO_SETUP_FAILURE);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_RELEASE_NORMAL,
+                PreciseDisconnectCause.RADIO_RELEASE_NORMAL);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_RADIO_RELEASE_ABNORMAL,
+                PreciseDisconnectCause.RADIO_RELEASE_ABNORMAL);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_ACCESS_CLASS_BLOCKED,
+                PreciseDisconnectCause.ACCESS_CLASS_BLOCKED);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_NETWORK_DETACH,
+                PreciseDisconnectCause.NETWORK_DETACH);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_1,
+                PreciseDisconnectCause.OEM_CAUSE_1);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_2,
+                PreciseDisconnectCause.OEM_CAUSE_2);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_3,
+                PreciseDisconnectCause.OEM_CAUSE_3);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_4,
+                PreciseDisconnectCause.OEM_CAUSE_4);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_5,
+                PreciseDisconnectCause.OEM_CAUSE_5);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_6,
+                PreciseDisconnectCause.OEM_CAUSE_6);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_7,
+                PreciseDisconnectCause.OEM_CAUSE_7);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_8,
+                PreciseDisconnectCause.OEM_CAUSE_8);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_9,
+                PreciseDisconnectCause.OEM_CAUSE_9);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_10,
+                PreciseDisconnectCause.OEM_CAUSE_10);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_11,
+                PreciseDisconnectCause.OEM_CAUSE_11);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_12,
+                PreciseDisconnectCause.OEM_CAUSE_12);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_13,
+                PreciseDisconnectCause.OEM_CAUSE_13);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_14,
+                PreciseDisconnectCause.OEM_CAUSE_14);
+        PRECISE_CAUSE_MAP.append(ImsReasonInfo.CODE_OEM_CAUSE_15,
+                PreciseDisconnectCause.OEM_CAUSE_15);
+    }
+
+    /**
+     * Carrier configuration option which determines whether the carrier wants to inform the user
+     * when a video call is handed over from WIFI to LTE.
+     * See {@link CarrierConfigManager#KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL} for more
+     * information.
+     */
+    private boolean mNotifyHandoverVideoFromWifiToLTE = false;
+
+    /**
+     * Carrier configuration option which determines whether the carrier supports the
+     * {@link VideoProfile#STATE_PAUSED} signalling.
+     * See {@link CarrierConfigManager#KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL} for more information.
+     */
+    private boolean mSupportPauseVideo = false;
+
+    /**
+     * Carrier configuration option which defines a mapping from pairs of
+     * {@link ImsReasonInfo#getCode()} and {@link ImsReasonInfo#getExtraMessage()} values to a new
+     * {@code ImsReasonInfo#CODE_*} value.
+     *
+     * See {@link CarrierConfigManager#KEY_IMS_REASONINFO_MAPPING_STRING_ARRAY}.
+     */
+    private Map<Pair<Integer, String>, Integer> mImsReasonCodeMap = new ArrayMap<>();
+
+
+    /**
+     * TODO: Remove this code; it is a workaround.
+     * When {@code true}, forces {@link ImsManager#updateImsServiceConfig(Context, int, boolean)} to
+     * be called when an ongoing video call is disconnected.  In some cases, where video pause is
+     * supported by the carrier, when {@link #onDataEnabledChanged(boolean, int)} reports that data
+     * has been disabled we will pause the video rather than disconnecting the call.  When this
+     * happens we need to prevent the IMS service config from being updated, as this will cause VT
+     * to be disabled mid-call, resulting in an inability to un-pause the video.
+     */
+    private boolean mShouldUpdateImsConfigOnDisconnect = false;
+
+    /**
+     * Default implementation for retrieving shared preferences; uses the actual PreferencesManager.
+     */
+    private SharedPreferenceProxy mSharedPreferenceProxy = (Context context) -> {
+        return PreferenceManager.getDefaultSharedPreferences(context);
+    };
+
+    /**
+     * Default implementation for determining if a number is an emergency number.  Uses the real
+     * PhoneNumberUtils.
+     */
+    private PhoneNumberUtilsProxy mPhoneNumberUtilsProxy = (String string) -> {
+        return PhoneNumberUtils.isEmergencyNumber(string);
+    };
+
+    // Callback fires when ImsManager MMTel Feature changes state
+    private ImsServiceProxy.INotifyStatusChanged mNotifyStatusChangedCallback = () -> {
+        try {
+            int status = mImsManager.getImsServiceStatus();
+            log("Status Changed: " + status);
+            switch(status) {
+                case ImsFeature.STATE_READY: {
+                    startListeningForCalls();
+                    break;
+                }
+                case ImsFeature.STATE_INITIALIZING:
+                    // fall through
+                case ImsFeature.STATE_NOT_AVAILABLE: {
+                    stopListeningForCalls();
+                    break;
+                }
+                default: {
+                    Log.w(LOG_TAG, "Unexpected State!");
+                }
+            }
+        } catch (ImsException e) {
+            // Could not get the ImsService, retry!
+            retryGetImsService();
+        }
+    };
+
+    @VisibleForTesting
+    public interface IRetryTimeout {
+        int get();
+    }
+
+    /**
+     * Default implementation of interface that calculates the ImsService retry timeout.
+     * Override-able for testing.
+     */
+    @VisibleForTesting
+    public IRetryTimeout mRetryTimeout = () -> {
+        int timeout = (1 << mImsServiceRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
+        if (mImsServiceRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
+            mImsServiceRetryCount++;
+        }
+        return timeout;
+    };
+
+    //***** Events
+
+
+    //***** Constructors
+
+    public ImsPhoneCallTracker(ImsPhone phone) {
+        this.mPhone = phone;
+
+        mMetrics = TelephonyMetrics.getInstance();
+
+        IntentFilter intentfilter = new IntentFilter();
+        intentfilter.addAction(ImsManager.ACTION_IMS_INCOMING_CALL);
+        intentfilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        intentfilter.addAction(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER);
+        mPhone.getContext().registerReceiver(mReceiver, intentfilter);
+        cacheCarrierConfiguration(mPhone.getSubId());
+
+        mPhone.getDefaultPhone().registerForDataEnabledChanged(
+                this, EVENT_DATA_ENABLED_CHANGED, null);
+
+        mImsServiceRetryCount = 0;
+
+        final TelecomManager telecomManager =
+                (TelecomManager) mPhone.getContext().getSystemService(Context.TELECOM_SERVICE);
+        mDefaultDialerUid.set(
+                getPackageUid(mPhone.getContext(), telecomManager.getDefaultDialerPackage()));
+
+        long currentTime = SystemClock.elapsedRealtime();
+        mVtDataUsageSnapshot = new NetworkStats(currentTime, 1);
+        mVtDataUsageUidSnapshot = new NetworkStats(currentTime, 1);
+
+        // Send a message to connect to the Ims Service and open a connection through
+        // getImsService().
+        sendEmptyMessage(EVENT_GET_IMS_SERVICE);
+    }
+
+    /**
+     * Test-only method used to mock out access to the shared preferences through the
+     * {@link PreferenceManager}.
+     * @param sharedPreferenceProxy
+     */
+    @VisibleForTesting
+    public void setSharedPreferenceProxy(SharedPreferenceProxy sharedPreferenceProxy) {
+        mSharedPreferenceProxy = sharedPreferenceProxy;
+    }
+
+    /**
+     * Test-only method used to mock out access to the phone number utils class.
+     * @param phoneNumberUtilsProxy
+     */
+    @VisibleForTesting
+    public void setPhoneNumberUtilsProxy(PhoneNumberUtilsProxy phoneNumberUtilsProxy) {
+        mPhoneNumberUtilsProxy = phoneNumberUtilsProxy;
+    }
+
+    private int getPackageUid(Context context, String pkg) {
+        if (pkg == null) {
+            return NetworkStats.UID_ALL;
+        }
+
+        int uid = NetworkStats.UID_ALL;
+        try {
+            uid = context.getPackageManager().getPackageUid(pkg, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            loge("Cannot find package uid. pkg = " + pkg);
+        }
+        return uid;
+    }
+
+    private PendingIntent createIncomingCallPendingIntent() {
+        Intent intent = new Intent(ImsManager.ACTION_IMS_INCOMING_CALL);
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        return PendingIntent.getBroadcast(mPhone.getContext(), 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    private void getImsService() throws ImsException {
+        if (DBG) log("getImsService");
+        mImsManager = ImsManager.getInstance(mPhone.getContext(), mPhone.getPhoneId());
+        // Adding to set, will be safe adding multiple times. If the ImsService is not active yet,
+        // this method will throw an ImsException.
+        mImsManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
+        // Wait for ImsService.STATE_READY to start listening for calls.
+        // Call the callback right away for compatibility with older devices that do not use states.
+        mNotifyStatusChangedCallback.notifyStatusChanged();
+    }
+
+    private void startListeningForCalls() throws ImsException {
+        mImsServiceRetryCount = 0;
+        mServiceId = mImsManager.open(ImsServiceClass.MMTEL,
+                createIncomingCallPendingIntent(),
+                mImsConnectionStateListener);
+
+        mImsManager.setImsConfigListener(mImsConfigListener);
+
+        // Get the ECBM interface and set IMSPhone's listener object for notifications
+        getEcbmInterface().setEcbmStateListener(mPhone.getImsEcbmStateListener());
+        if (mPhone.isInEcm()) {
+            // Call exit ECBM which will invoke onECBMExited
+            mPhone.exitEmergencyCallbackMode();
+        }
+        int mPreferredTtyMode = Settings.Secure.getInt(
+                mPhone.getContext().getContentResolver(),
+                Settings.Secure.PREFERRED_TTY_MODE,
+                Phone.TTY_MODE_OFF);
+        mImsManager.setUiTTYMode(mPhone.getContext(), mPreferredTtyMode, null);
+
+        ImsMultiEndpoint multiEndpoint = getMultiEndpointInterface();
+        if (multiEndpoint != null) {
+            multiEndpoint.setExternalCallStateListener(
+                    mPhone.getExternalCallTracker().getExternalCallStateListener());
+        }
+    }
+
+    private void stopListeningForCalls() {
+        try {
+            resetImsCapabilities();
+            // Only close on valid session.
+            if (mImsManager != null && mServiceId > 0) {
+                mImsManager.close(mServiceId);
+                mServiceId = -1;
+            }
+        } catch (ImsException e) {
+            // If the binder is unavailable, then the ImsService doesn't need to close.
+        }
+    }
+
+    public void dispose() {
+        if (DBG) log("dispose");
+        mRingingCall.dispose();
+        mBackgroundCall.dispose();
+        mForegroundCall.dispose();
+        mHandoverCall.dispose();
+
+        clearDisconnected();
+        mPhone.getContext().unregisterReceiver(mReceiver);
+        mPhone.getDefaultPhone().unregisterForDataEnabledChanged(this);
+        removeMessages(EVENT_GET_IMS_SERVICE);
+    }
+
+    @Override
+    protected void finalize() {
+        log("ImsPhoneCallTracker finalized");
+    }
+
+    //***** Instance Methods
+
+    //***** Public Methods
+    @Override
+    public void registerForVoiceCallStarted(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mVoiceCallStartedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForVoiceCallStarted(Handler h) {
+        mVoiceCallStartedRegistrants.remove(h);
+    }
+
+    @Override
+    public void registerForVoiceCallEnded(Handler h, int what, Object obj) {
+        Registrant r = new Registrant(h, what, obj);
+        mVoiceCallEndedRegistrants.add(r);
+    }
+
+    @Override
+    public void unregisterForVoiceCallEnded(Handler h) {
+        mVoiceCallEndedRegistrants.remove(h);
+    }
+
+    public Connection dial(String dialString, int videoState, Bundle intentExtras) throws
+            CallStateException {
+        int oirMode;
+        if (mSharedPreferenceProxy != null && mPhone.getDefaultPhone() != null) {
+            SharedPreferences sp = mSharedPreferenceProxy.getDefaultSharedPreferences(
+                    mPhone.getContext());
+            oirMode = sp.getInt(Phone.CLIR_KEY + mPhone.getDefaultPhone().getPhoneId(),
+                    CommandsInterface.CLIR_DEFAULT);
+        } else {
+            loge("dial; could not get default CLIR mode.");
+            oirMode = CommandsInterface.CLIR_DEFAULT;
+        }
+        return dial(dialString, oirMode, videoState, intentExtras);
+    }
+
+    /**
+     * oirMode is one of the CLIR_ constants
+     */
+    synchronized Connection
+    dial(String dialString, int clirMode, int videoState, Bundle intentExtras)
+            throws CallStateException {
+        boolean isPhoneInEcmMode = isPhoneInEcbMode();
+        boolean isEmergencyNumber = mPhoneNumberUtilsProxy.isEmergencyNumber(dialString);
+
+        if (DBG) log("dial clirMode=" + clirMode);
+        if (isEmergencyNumber) {
+            clirMode = CommandsInterface.CLIR_SUPPRESSION;
+            if (DBG) log("dial emergency call, set clirModIe=" + clirMode);
+        }
+
+        // note that this triggers call state changed notif
+        clearDisconnected();
+
+        if (mImsManager == null) {
+            throw new CallStateException("service not available");
+        }
+
+        if (!canDial()) {
+            throw new CallStateException("cannot dial in current state");
+        }
+
+        if (isPhoneInEcmMode && isEmergencyNumber) {
+            handleEcmTimer(ImsPhone.CANCEL_ECM_TIMER);
+        }
+
+        // If the call is to an emergency number and the carrier does not support video emergency
+        // calls, dial as an audio-only call.
+        if (isEmergencyNumber && VideoProfile.isVideo(videoState) &&
+                !mAllowEmergencyVideoCalls) {
+            loge("dial: carrier does not support video emergency calls; downgrade to audio-only");
+            videoState = VideoProfile.STATE_AUDIO_ONLY;
+        }
+
+        boolean holdBeforeDial = false;
+
+        // The new call must be assigned to the foreground call.
+        // That call must be idle, so place anything that's
+        // there on hold
+        if (mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE) {
+            if (mBackgroundCall.getState() != ImsPhoneCall.State.IDLE) {
+                //we should have failed in !canDial() above before we get here
+                throw new CallStateException("cannot dial in current state");
+            }
+            // foreground call is empty for the newly dialed connection
+            holdBeforeDial = true;
+            // Cache the video state for pending MO call.
+            mPendingCallVideoState = videoState;
+            mPendingIntentExtras = intentExtras;
+            switchWaitingOrHoldingAndActive();
+        }
+
+        ImsPhoneCall.State fgState = ImsPhoneCall.State.IDLE;
+        ImsPhoneCall.State bgState = ImsPhoneCall.State.IDLE;
+
+        mClirMode = clirMode;
+
+        synchronized (mSyncHold) {
+            if (holdBeforeDial) {
+                fgState = mForegroundCall.getState();
+                bgState = mBackgroundCall.getState();
+
+                //holding foreground call failed
+                if (fgState == ImsPhoneCall.State.ACTIVE) {
+                    throw new CallStateException("cannot dial in current state");
+                }
+
+                //holding foreground call succeeded
+                if (bgState == ImsPhoneCall.State.HOLDING) {
+                    holdBeforeDial = false;
+                }
+            }
+
+            mPendingMO = new ImsPhoneConnection(mPhone,
+                    checkForTestEmergencyNumber(dialString), this, mForegroundCall,
+                    isEmergencyNumber);
+            mPendingMO.setVideoState(videoState);
+        }
+        addConnection(mPendingMO);
+
+        if (!holdBeforeDial) {
+            if ((!isPhoneInEcmMode) || (isPhoneInEcmMode && isEmergencyNumber)) {
+                dialInternal(mPendingMO, clirMode, videoState, intentExtras);
+            } else {
+                try {
+                    getEcbmInterface().exitEmergencyCallbackMode();
+                } catch (ImsException e) {
+                    e.printStackTrace();
+                    throw new CallStateException("service not available");
+                }
+                mPhone.setOnEcbModeExitResponse(this, EVENT_EXIT_ECM_RESPONSE_CDMA, null);
+                pendingCallClirMode = clirMode;
+                mPendingCallVideoState = videoState;
+                pendingCallInEcm = true;
+            }
+        }
+
+        updatePhoneState();
+        mPhone.notifyPreciseCallStateChanged();
+
+        return mPendingMO;
+    }
+
+    boolean isImsServiceReady() {
+        if (mImsManager == null) {
+            return false;
+        }
+
+        return mImsManager.isServiceAvailable();
+    }
+
+    /**
+     * Caches frequently used carrier configuration items locally.
+     *
+     * @param subId The sub id.
+     */
+    private void cacheCarrierConfiguration(int subId) {
+        CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
+                mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (carrierConfigManager == null) {
+            loge("cacheCarrierConfiguration: No carrier config service found.");
+            return;
+        }
+
+        PersistableBundle carrierConfig = carrierConfigManager.getConfigForSubId(subId);
+        if (carrierConfig == null) {
+            loge("cacheCarrierConfiguration: Empty carrier config.");
+            return;
+        }
+
+        mAllowEmergencyVideoCalls =
+                carrierConfig.getBoolean(CarrierConfigManager.KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL);
+        mTreatDowngradedVideoCallsAsVideoCalls =
+                carrierConfig.getBoolean(
+                        CarrierConfigManager.KEY_TREAT_DOWNGRADED_VIDEO_CALLS_AS_VIDEO_CALLS_BOOL);
+        mDropVideoCallWhenAnsweringAudioCall =
+                carrierConfig.getBoolean(
+                        CarrierConfigManager.KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL);
+        mAllowAddCallDuringVideoCall =
+                carrierConfig.getBoolean(
+                        CarrierConfigManager.KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL);
+        mNotifyVtHandoverToWifiFail = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_NOTIFY_VT_HANDOVER_TO_WIFI_FAILURE_BOOL);
+        mSupportDowngradeVtToAudio = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_SUPPORT_DOWNGRADE_VT_TO_AUDIO_BOOL);
+        mNotifyHandoverVideoFromWifiToLTE = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL);
+        mIgnoreDataEnabledChangedForVideoCalls = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
+        mIsViLteDataMetered = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_VILTE_DATA_IS_METERED_BOOL);
+        mSupportPauseVideo = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL);
+
+        String[] mappings = carrierConfig
+                .getStringArray(CarrierConfigManager.KEY_IMS_REASONINFO_MAPPING_STRING_ARRAY);
+        if (mappings != null && mappings.length > 0) {
+            for (String mapping : mappings) {
+                String[] values = mapping.split(Pattern.quote("|"));
+                if (values.length != 3) {
+                    continue;
+                }
+
+                try {
+                    Integer fromCode;
+                    if (values[0].equals("*")) {
+                        fromCode = null;
+                    } else {
+                        fromCode = Integer.parseInt(values[0]);
+                    }
+                    String message = values[1];
+                    int toCode = Integer.parseInt(values[2]);
+
+                    addReasonCodeRemapping(fromCode, message, toCode);
+                    log("Loaded ImsReasonInfo mapping : fromCode = " +
+                            fromCode == null ? "any" : fromCode + " ; message = " +
+                            message + " ; toCode = " + toCode);
+                } catch (NumberFormatException nfe) {
+                    loge("Invalid ImsReasonInfo mapping found: " + mapping);
+                }
+            }
+        } else {
+            log("No carrier ImsReasonInfo mappings defined.");
+        }
+    }
+
+    private void handleEcmTimer(int action) {
+        mPhone.handleTimerInEmergencyCallbackMode(action);
+        switch (action) {
+            case ImsPhone.CANCEL_ECM_TIMER:
+                break;
+            case ImsPhone.RESTART_ECM_TIMER:
+                break;
+            default:
+                log("handleEcmTimer, unsupported action " + action);
+        }
+    }
+
+    private void dialInternal(ImsPhoneConnection conn, int clirMode, int videoState,
+            Bundle intentExtras) {
+
+        if (conn == null) {
+            return;
+        }
+
+        if (conn.getAddress()== null || conn.getAddress().length() == 0
+                || conn.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0) {
+            // Phone number is invalid
+            conn.setDisconnectCause(DisconnectCause.INVALID_NUMBER);
+            sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
+            return;
+        }
+
+        // Always unmute when initiating a new call
+        setMute(false);
+        int serviceType = mPhoneNumberUtilsProxy.isEmergencyNumber(conn.getAddress()) ?
+                ImsCallProfile.SERVICE_TYPE_EMERGENCY : ImsCallProfile.SERVICE_TYPE_NORMAL;
+        int callType = ImsCallProfile.getCallTypeFromVideoState(videoState);
+        //TODO(vt): Is this sufficient?  At what point do we know the video state of the call?
+        conn.setVideoState(videoState);
+
+        try {
+            String[] callees = new String[] { conn.getAddress() };
+            ImsCallProfile profile = mImsManager.createCallProfile(mServiceId,
+                    serviceType, callType);
+            profile.setCallExtraInt(ImsCallProfile.EXTRA_OIR, clirMode);
+
+            // Translate call subject intent-extra from Telecom-specific extra key to the
+            // ImsCallProfile key.
+            if (intentExtras != null) {
+                if (intentExtras.containsKey(android.telecom.TelecomManager.EXTRA_CALL_SUBJECT)) {
+                    intentExtras.putString(ImsCallProfile.EXTRA_DISPLAY_TEXT,
+                            cleanseInstantLetteringMessage(intentExtras.getString(
+                                    android.telecom.TelecomManager.EXTRA_CALL_SUBJECT))
+                    );
+                }
+
+                if (intentExtras.containsKey(ImsCallProfile.EXTRA_IS_CALL_PULL)) {
+                    profile.mCallExtras.putBoolean(ImsCallProfile.EXTRA_IS_CALL_PULL,
+                            intentExtras.getBoolean(ImsCallProfile.EXTRA_IS_CALL_PULL));
+                    int dialogId = intentExtras.getInt(
+                            ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID);
+                    conn.setIsPulledCall(true);
+                    conn.setPulledDialogId(dialogId);
+                }
+
+                // Pack the OEM-specific call extras.
+                profile.mCallExtras.putBundle(ImsCallProfile.EXTRA_OEM_EXTRAS, intentExtras);
+
+                // NOTE: Extras to be sent over the network are packed into the
+                // intentExtras individually, with uniquely defined keys.
+                // These key-value pairs are processed by IMS Service before
+                // being sent to the lower layers/to the network.
+            }
+
+            ImsCall imsCall = mImsManager.makeCall(mServiceId, profile,
+                    callees, mImsCallListener);
+            conn.setImsCall(imsCall);
+
+            mMetrics.writeOnImsCallStart(mPhone.getPhoneId(),
+                    imsCall.getSession());
+
+            setVideoCallProvider(conn, imsCall);
+            conn.setAllowAddCallDuringVideoCall(mAllowAddCallDuringVideoCall);
+        } catch (ImsException e) {
+            loge("dialInternal : " + e);
+            conn.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED);
+            sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
+            retryGetImsService();
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Accepts a call with the specified video state.  The video state is the video state that the
+     * user has agreed upon in the InCall UI.
+     *
+     * @param videoState The video State
+     * @throws CallStateException
+     */
+    public void acceptCall (int videoState) throws CallStateException {
+        if (DBG) log("acceptCall");
+
+        if (mForegroundCall.getState().isAlive()
+                && mBackgroundCall.getState().isAlive()) {
+            throw new CallStateException("cannot accept call");
+        }
+
+        if ((mRingingCall.getState() == ImsPhoneCall.State.WAITING)
+                && mForegroundCall.getState().isAlive()) {
+            setMute(false);
+
+            boolean answeringWillDisconnect = false;
+            ImsCall activeCall = mForegroundCall.getImsCall();
+            ImsCall ringingCall = mRingingCall.getImsCall();
+            if (mForegroundCall.hasConnections() && mRingingCall.hasConnections()) {
+                answeringWillDisconnect =
+                        shouldDisconnectActiveCallOnAnswer(activeCall, ringingCall);
+            }
+
+            // Cache video state for pending MT call.
+            mPendingCallVideoState = videoState;
+
+            if (answeringWillDisconnect) {
+                // We need to disconnect the foreground call before answering the background call.
+                mForegroundCall.hangup();
+                try {
+                    ringingCall.accept(ImsCallProfile.getCallTypeFromVideoState(videoState));
+                } catch (ImsException e) {
+                    throw new CallStateException("cannot accept call");
+                }
+            } else {
+                switchWaitingOrHoldingAndActive();
+            }
+        } else if (mRingingCall.getState().isRinging()) {
+            if (DBG) log("acceptCall: incoming...");
+            // Always unmute when answering a new call
+            setMute(false);
+            try {
+                ImsCall imsCall = mRingingCall.getImsCall();
+                if (imsCall != null) {
+                    imsCall.accept(ImsCallProfile.getCallTypeFromVideoState(videoState));
+                    mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                            ImsCommand.IMS_CMD_ACCEPT);
+                } else {
+                    throw new CallStateException("no valid ims call");
+                }
+            } catch (ImsException e) {
+                throw new CallStateException("cannot accept call");
+            }
+        } else {
+            throw new CallStateException("phone not ringing");
+        }
+    }
+
+    public void rejectCall () throws CallStateException {
+        if (DBG) log("rejectCall");
+
+        if (mRingingCall.getState().isRinging()) {
+            hangup(mRingingCall);
+        } else {
+            throw new CallStateException("phone not ringing");
+        }
+    }
+
+
+    private void switchAfterConferenceSuccess() {
+        if (DBG) log("switchAfterConferenceSuccess fg =" + mForegroundCall.getState() +
+                ", bg = " + mBackgroundCall.getState());
+
+        if (mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING) {
+            log("switchAfterConferenceSuccess");
+            mForegroundCall.switchWith(mBackgroundCall);
+        }
+    }
+
+    public void switchWaitingOrHoldingAndActive() throws CallStateException {
+        if (DBG) log("switchWaitingOrHoldingAndActive");
+
+        if (mRingingCall.getState() == ImsPhoneCall.State.INCOMING) {
+            throw new CallStateException("cannot be in the incoming state");
+        }
+
+        if (mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE) {
+            ImsCall imsCall = mForegroundCall.getImsCall();
+            if (imsCall == null) {
+                throw new CallStateException("no ims call");
+            }
+
+            // Swap the ImsCalls pointed to by the foreground and background ImsPhoneCalls.
+            // If hold or resume later fails, we will swap them back.
+            boolean switchingWithWaitingCall = !mBackgroundCall.getState().isAlive() &&
+                    mRingingCall != null &&
+                    mRingingCall.getState() == ImsPhoneCall.State.WAITING;
+
+            mSwitchingFgAndBgCalls = true;
+            if (switchingWithWaitingCall) {
+                mCallExpectedToResume = mRingingCall.getImsCall();
+            } else {
+                mCallExpectedToResume = mBackgroundCall.getImsCall();
+            }
+            mForegroundCall.switchWith(mBackgroundCall);
+
+            // Hold the foreground call; once the foreground call is held, the background call will
+            // be resumed.
+            try {
+                imsCall.hold();
+                mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                        ImsCommand.IMS_CMD_HOLD);
+
+                // If there is no background call to resume, then don't expect there to be a switch.
+                if (mCallExpectedToResume == null) {
+                    log("mCallExpectedToResume is null");
+                    mSwitchingFgAndBgCalls = false;
+                }
+            } catch (ImsException e) {
+                mForegroundCall.switchWith(mBackgroundCall);
+                throw new CallStateException(e.getMessage());
+            }
+        } else if (mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING) {
+            resumeWaitingOrHolding();
+        }
+    }
+
+    public void
+    conference() {
+        ImsCall fgImsCall = mForegroundCall.getImsCall();
+        if (fgImsCall == null) {
+            log("conference no foreground ims call");
+            return;
+        }
+
+        ImsCall bgImsCall = mBackgroundCall.getImsCall();
+        if (bgImsCall == null) {
+            log("conference no background ims call");
+            return;
+        }
+
+        if (fgImsCall.isCallSessionMergePending()) {
+            log("conference: skip; foreground call already in process of merging.");
+            return;
+        }
+
+        if (bgImsCall.isCallSessionMergePending()) {
+            log("conference: skip; background call already in process of merging.");
+            return;
+        }
+
+        // Keep track of the connect time of the earliest call so that it can be set on the
+        // {@code ImsConference} when it is created.
+        long foregroundConnectTime = mForegroundCall.getEarliestConnectTime();
+        long backgroundConnectTime = mBackgroundCall.getEarliestConnectTime();
+        long conferenceConnectTime;
+        if (foregroundConnectTime > 0 && backgroundConnectTime > 0) {
+            conferenceConnectTime = Math.min(mForegroundCall.getEarliestConnectTime(),
+                    mBackgroundCall.getEarliestConnectTime());
+            log("conference - using connect time = " + conferenceConnectTime);
+        } else if (foregroundConnectTime > 0) {
+            log("conference - bg call connect time is 0; using fg = " + foregroundConnectTime);
+            conferenceConnectTime = foregroundConnectTime;
+        } else {
+            log("conference - fg call connect time is 0; using bg = " + backgroundConnectTime);
+            conferenceConnectTime = backgroundConnectTime;
+        }
+
+        String foregroundId = "";
+        ImsPhoneConnection foregroundConnection = mForegroundCall.getFirstConnection();
+        if (foregroundConnection != null) {
+            foregroundConnection.setConferenceConnectTime(conferenceConnectTime);
+            foregroundConnection.handleMergeStart();
+            foregroundId = foregroundConnection.getTelecomCallId();
+        }
+        String backgroundId = "";
+        ImsPhoneConnection backgroundConnection = findConnection(bgImsCall);
+        if (backgroundConnection != null) {
+            backgroundConnection.handleMergeStart();
+            backgroundId = backgroundConnection.getTelecomCallId();
+        }
+        log("conference: fgCallId=" + foregroundId + ", bgCallId=" + backgroundId);
+
+        try {
+            fgImsCall.merge(bgImsCall);
+        } catch (ImsException e) {
+            log("conference " + e.getMessage());
+        }
+    }
+
+    public void
+    explicitCallTransfer() {
+        //TODO : implement
+    }
+
+    public void
+    clearDisconnected() {
+        if (DBG) log("clearDisconnected");
+
+        internalClearDisconnected();
+
+        updatePhoneState();
+        mPhone.notifyPreciseCallStateChanged();
+    }
+
+    public boolean
+    canConference() {
+        return mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE
+            && mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING
+            && !mBackgroundCall.isFull()
+            && !mForegroundCall.isFull();
+    }
+
+    public boolean canDial() {
+        boolean ret;
+        String disableCall = SystemProperties.get(
+                TelephonyProperties.PROPERTY_DISABLE_CALL, "false");
+
+        ret = mPendingMO == null
+                && !mRingingCall.isRinging()
+                && !disableCall.equals("true")
+                && (!mForegroundCall.getState().isAlive()
+                        || !mBackgroundCall.getState().isAlive());
+
+        return ret;
+    }
+
+    public boolean
+    canTransfer() {
+        return mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE
+            && mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING;
+    }
+
+    //***** Private Instance Methods
+
+    private void
+    internalClearDisconnected() {
+        mRingingCall.clearDisconnected();
+        mForegroundCall.clearDisconnected();
+        mBackgroundCall.clearDisconnected();
+        mHandoverCall.clearDisconnected();
+    }
+
+    private void
+    updatePhoneState() {
+        PhoneConstants.State oldState = mState;
+
+        boolean isPendingMOIdle = mPendingMO == null || !mPendingMO.getState().isAlive();
+
+        if (mRingingCall.isRinging()) {
+            mState = PhoneConstants.State.RINGING;
+        } else if (!isPendingMOIdle || !mForegroundCall.isIdle() || !mBackgroundCall.isIdle()) {
+            // There is a non-idle call, so we're off the hook.
+            mState = PhoneConstants.State.OFFHOOK;
+        } else {
+            mState = PhoneConstants.State.IDLE;
+        }
+
+        if (mState == PhoneConstants.State.IDLE && oldState != mState) {
+            mVoiceCallEndedRegistrants.notifyRegistrants(
+                    new AsyncResult(null, null, null));
+        } else if (oldState == PhoneConstants.State.IDLE && oldState != mState) {
+            mVoiceCallStartedRegistrants.notifyRegistrants (
+                    new AsyncResult(null, null, null));
+        }
+
+        if (DBG) {
+            log("updatePhoneState pendingMo = " + (mPendingMO == null ? "null"
+                    : mPendingMO.getState()) + ", fg= " + mForegroundCall.getState() + "("
+                    + mForegroundCall.getConnections().size() + "), bg= " + mBackgroundCall
+                    .getState() + "(" + mBackgroundCall.getConnections().size() + ")");
+            log("updatePhoneState oldState=" + oldState + ", newState=" + mState);
+        }
+
+        if (mState != oldState) {
+            mPhone.notifyPhoneStateChanged();
+            mMetrics.writePhoneState(mPhone.getPhoneId(), mState);
+            notifyPhoneStateChanged(oldState, mState);
+        }
+    }
+
+    private void
+    handleRadioNotAvailable() {
+        // handlePollCalls will clear out its
+        // call list when it gets the CommandException
+        // error result from this
+        pollCallsWhenSafe();
+    }
+
+    private void
+    dumpState() {
+        List l;
+
+        log("Phone State:" + mState);
+
+        log("Ringing call: " + mRingingCall.toString());
+
+        l = mRingingCall.getConnections();
+        for (int i = 0, s = l.size(); i < s; i++) {
+            log(l.get(i).toString());
+        }
+
+        log("Foreground call: " + mForegroundCall.toString());
+
+        l = mForegroundCall.getConnections();
+        for (int i = 0, s = l.size(); i < s; i++) {
+            log(l.get(i).toString());
+        }
+
+        log("Background call: " + mBackgroundCall.toString());
+
+        l = mBackgroundCall.getConnections();
+        for (int i = 0, s = l.size(); i < s; i++) {
+            log(l.get(i).toString());
+        }
+
+    }
+
+    //***** Called from ImsPhone
+    /**
+     * Set the TTY mode. This is the actual tty mode (varies depending on peripheral status)
+     */
+    public void setTtyMode(int ttyMode) {
+        if (mImsManager == null) {
+            Log.w(LOG_TAG, "ImsManager is null when setting TTY mode");
+            return;
+        }
+
+        try {
+            mImsManager.setTtyMode(ttyMode);
+        } catch (ImsException e) {
+            loge("setTtyMode : " + e);
+            retryGetImsService();
+        }
+    }
+
+    /**
+     * Sets the UI TTY mode. This is the preferred TTY mode that the user sets in the call
+     * settings screen.
+     */
+    public void setUiTTYMode(int uiTtyMode, Message onComplete) {
+        if (mImsManager == null) {
+            mPhone.sendErrorResponse(onComplete, getImsManagerIsNullException());
+            return;
+        }
+
+        try {
+            mImsManager.setUiTTYMode(mPhone.getContext(), uiTtyMode, onComplete);
+        } catch (ImsException e) {
+            loge("setUITTYMode : " + e);
+            mPhone.sendErrorResponse(onComplete, e);
+            retryGetImsService();
+        }
+    }
+
+    public void setMute(boolean mute) {
+        mDesiredMute = mute;
+        mForegroundCall.setMute(mute);
+    }
+
+    public boolean getMute() {
+        return mDesiredMute;
+    }
+
+    public void sendDtmf(char c, Message result) {
+        if (DBG) log("sendDtmf");
+
+        ImsCall imscall = mForegroundCall.getImsCall();
+        if (imscall != null) {
+            imscall.sendDtmf(c, result);
+        }
+    }
+
+    public void
+    startDtmf(char c) {
+        if (DBG) log("startDtmf");
+
+        ImsCall imscall = mForegroundCall.getImsCall();
+        if (imscall != null) {
+            imscall.startDtmf(c);
+        } else {
+            loge("startDtmf : no foreground call");
+        }
+    }
+
+    public void
+    stopDtmf() {
+        if (DBG) log("stopDtmf");
+
+        ImsCall imscall = mForegroundCall.getImsCall();
+        if (imscall != null) {
+            imscall.stopDtmf();
+        } else {
+            loge("stopDtmf : no foreground call");
+        }
+    }
+
+    //***** Called from ImsPhoneConnection
+
+    public void hangup (ImsPhoneConnection conn) throws CallStateException {
+        if (DBG) log("hangup connection");
+
+        if (conn.getOwner() != this) {
+            throw new CallStateException ("ImsPhoneConnection " + conn
+                    + "does not belong to ImsPhoneCallTracker " + this);
+        }
+
+        hangup(conn.getCall());
+    }
+
+    //***** Called from ImsPhoneCall
+
+    public void hangup (ImsPhoneCall call) throws CallStateException {
+        if (DBG) log("hangup call");
+
+        if (call.getConnections().size() == 0) {
+            throw new CallStateException("no connections");
+        }
+
+        ImsCall imsCall = call.getImsCall();
+        boolean rejectCall = false;
+
+        if (call == mRingingCall) {
+            if (Phone.DEBUG_PHONE) log("(ringing) hangup incoming");
+            rejectCall = true;
+        } else if (call == mForegroundCall) {
+            if (call.isDialingOrAlerting()) {
+                if (Phone.DEBUG_PHONE) {
+                    log("(foregnd) hangup dialing or alerting...");
+                }
+            } else {
+                if (Phone.DEBUG_PHONE) {
+                    log("(foregnd) hangup foreground");
+                }
+                //held call will be resumed by onCallTerminated
+            }
+        } else if (call == mBackgroundCall) {
+            if (Phone.DEBUG_PHONE) {
+                log("(backgnd) hangup waiting or background");
+            }
+        } else {
+            throw new CallStateException ("ImsPhoneCall " + call +
+                    "does not belong to ImsPhoneCallTracker " + this);
+        }
+
+        call.onHangupLocal();
+
+        try {
+            if (imsCall != null) {
+                if (rejectCall) {
+                    imsCall.reject(ImsReasonInfo.CODE_USER_DECLINE);
+                    mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                            ImsCommand.IMS_CMD_REJECT);
+                } else {
+                    imsCall.terminate(ImsReasonInfo.CODE_USER_TERMINATED);
+                    mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                            ImsCommand.IMS_CMD_TERMINATE);
+                }
+            } else if (mPendingMO != null && call == mForegroundCall) {
+                // is holding a foreground call
+                mPendingMO.update(null, ImsPhoneCall.State.DISCONNECTED);
+                mPendingMO.onDisconnect();
+                removeConnection(mPendingMO);
+                mPendingMO = null;
+                updatePhoneState();
+                removeMessages(EVENT_DIAL_PENDINGMO);
+            }
+        } catch (ImsException e) {
+            throw new CallStateException(e.getMessage());
+        }
+
+        mPhone.notifyPreciseCallStateChanged();
+    }
+
+    void callEndCleanupHandOverCallIfAny() {
+        if (mHandoverCall.mConnections.size() > 0) {
+            if (DBG) log("callEndCleanupHandOverCallIfAny, mHandoverCall.mConnections="
+                    + mHandoverCall.mConnections);
+            mHandoverCall.mConnections.clear();
+            mConnections.clear();
+            mState = PhoneConstants.State.IDLE;
+        }
+    }
+
+    /* package */
+    void resumeWaitingOrHolding() throws CallStateException {
+        if (DBG) log("resumeWaitingOrHolding");
+
+        try {
+            if (mForegroundCall.getState().isAlive()) {
+                //resume foreground call after holding background call
+                //they were switched before holding
+                ImsCall imsCall = mForegroundCall.getImsCall();
+                if (imsCall != null) {
+                    imsCall.resume();
+                    mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                            ImsCommand.IMS_CMD_RESUME);
+                }
+            } else if (mRingingCall.getState() == ImsPhoneCall.State.WAITING) {
+                //accept waiting call after holding background call
+                ImsCall imsCall = mRingingCall.getImsCall();
+                if (imsCall != null) {
+                    imsCall.accept(
+                        ImsCallProfile.getCallTypeFromVideoState(mPendingCallVideoState));
+                    mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                            ImsCommand.IMS_CMD_ACCEPT);
+                }
+            } else {
+                //Just resume background call.
+                //To distinguish resuming call with swapping calls
+                //we do not switch calls.here
+                //ImsPhoneConnection.update will chnage the parent when completed
+                ImsCall imsCall = mBackgroundCall.getImsCall();
+                if (imsCall != null) {
+                    imsCall.resume();
+                    mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
+                            ImsCommand.IMS_CMD_RESUME);
+                }
+            }
+        } catch (ImsException e) {
+            throw new CallStateException(e.getMessage());
+        }
+    }
+
+    public void sendUSSD (String ussdString, Message response) {
+        if (DBG) log("sendUSSD");
+
+        try {
+            if (mUssdSession != null) {
+                mUssdSession.sendUssd(ussdString);
+                AsyncResult.forMessage(response, null, null);
+                response.sendToTarget();
+                return;
+            }
+
+            if (mImsManager == null) {
+                mPhone.sendErrorResponse(response, getImsManagerIsNullException());
+                return;
+            }
+
+            String[] callees = new String[] { ussdString };
+            ImsCallProfile profile = mImsManager.createCallProfile(mServiceId,
+                    ImsCallProfile.SERVICE_TYPE_NORMAL, ImsCallProfile.CALL_TYPE_VOICE);
+            profile.setCallExtraInt(ImsCallProfile.EXTRA_DIALSTRING,
+                    ImsCallProfile.DIALSTRING_USSD);
+
+            mUssdSession = mImsManager.makeCall(mServiceId, profile,
+                    callees, mImsUssdListener);
+        } catch (ImsException e) {
+            loge("sendUSSD : " + e);
+            mPhone.sendErrorResponse(response, e);
+            retryGetImsService();
+        }
+    }
+
+    public void cancelUSSD() {
+        if (mUssdSession == null) return;
+
+        try {
+            mUssdSession.terminate(ImsReasonInfo.CODE_USER_TERMINATED);
+        } catch (ImsException e) {
+        }
+
+    }
+
+    private synchronized ImsPhoneConnection findConnection(final ImsCall imsCall) {
+        for (ImsPhoneConnection conn : mConnections) {
+            if (conn.getImsCall() == imsCall) {
+                return conn;
+            }
+        }
+        return null;
+    }
+
+    private synchronized void removeConnection(ImsPhoneConnection conn) {
+        mConnections.remove(conn);
+        // If not emergency call is remaining, notify emergency call registrants
+        if (mIsInEmergencyCall) {
+            boolean isEmergencyCallInList = false;
+            // if no emergency calls pending, set this to false
+            for (ImsPhoneConnection imsPhoneConnection : mConnections) {
+                if (imsPhoneConnection != null && imsPhoneConnection.isEmergency() == true) {
+                    isEmergencyCallInList = true;
+                    break;
+                }
+            }
+
+            if (!isEmergencyCallInList) {
+                mIsInEmergencyCall = false;
+                mPhone.sendEmergencyCallStateChange(false);
+            }
+        }
+    }
+
+    private synchronized void addConnection(ImsPhoneConnection conn) {
+        mConnections.add(conn);
+        if (conn.isEmergency()) {
+            mIsInEmergencyCall = true;
+            mPhone.sendEmergencyCallStateChange(true);
+        }
+    }
+
+    private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause) {
+        if (DBG) log("processCallStateChange " + imsCall + " state=" + state + " cause=" + cause);
+        // This method is called on onCallUpdate() where there is not necessarily a call state
+        // change. In these situations, we'll ignore the state related updates and only process
+        // the change in media capabilities (as expected).  The default is to not ignore state
+        // changes so we do not change existing behavior.
+        processCallStateChange(imsCall, state, cause, false /* do not ignore state update */);
+    }
+
+    private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause,
+            boolean ignoreState) {
+        if (DBG) {
+            log("processCallStateChange state=" + state + " cause=" + cause
+                    + " ignoreState=" + ignoreState);
+        }
+
+        if (imsCall == null) return;
+
+        boolean changed = false;
+        ImsPhoneConnection conn = findConnection(imsCall);
+
+        if (conn == null) {
+            // TODO : what should be done?
+            return;
+        }
+
+        // processCallStateChange is triggered for onCallUpdated as well.
+        // onCallUpdated should not modify the state of the call
+        // It should modify only other capabilities of call through updateMediaCapabilities
+        // State updates will be triggered through individual callbacks
+        // i.e. onCallHeld, onCallResume, etc and conn.update will be responsible for the update
+        conn.updateMediaCapabilities(imsCall);
+        if (ignoreState) {
+            conn.updateAddressDisplay(imsCall);
+            conn.updateExtras(imsCall);
+
+            maybeSetVideoCallProvider(conn, imsCall);
+            return;
+        }
+
+        changed = conn.update(imsCall, state);
+        if (state == ImsPhoneCall.State.DISCONNECTED) {
+            changed = conn.onDisconnect(cause) || changed;
+            //detach the disconnected connections
+            conn.getCall().detach(conn);
+            removeConnection(conn);
+        }
+
+        if (changed) {
+            if (conn.getCall() == mHandoverCall) return;
+            updatePhoneState();
+            mPhone.notifyPreciseCallStateChanged();
+        }
+    }
+
+    private void maybeSetVideoCallProvider(ImsPhoneConnection conn, ImsCall imsCall) {
+        android.telecom.Connection.VideoProvider connVideoProvider = conn.getVideoProvider();
+        if (connVideoProvider != null || imsCall.getCallSession().getVideoCallProvider() == null) {
+            return;
+        }
+
+        try {
+            setVideoCallProvider(conn, imsCall);
+        } catch (RemoteException e) {
+            loge("maybeSetVideoCallProvider: exception " + e);
+        }
+    }
+
+    /**
+     * Adds a reason code remapping, for test purposes.
+     *
+     * @param fromCode The from code, or {@code null} if all.
+     * @param message The message to map.
+     * @param toCode The code to remap to.
+     */
+    @VisibleForTesting
+    public void addReasonCodeRemapping(Integer fromCode, String message, Integer toCode) {
+        mImsReasonCodeMap.put(new Pair<>(fromCode, message), toCode);
+    }
+
+    /**
+     * Returns the {@link ImsReasonInfo#getCode()}, potentially remapping to a new value based on
+     * the {@link ImsReasonInfo#getCode()} and {@link ImsReasonInfo#getExtraMessage()}.
+     *
+     * See {@link #mImsReasonCodeMap}.
+     *
+     * @param reasonInfo The {@link ImsReasonInfo}.
+     * @return The remapped code.
+     */
+    @VisibleForTesting
+    public int maybeRemapReasonCode(ImsReasonInfo reasonInfo) {
+        int code = reasonInfo.getCode();
+
+        Pair<Integer, String> toCheck = new Pair<>(code, reasonInfo.getExtraMessage());
+        Pair<Integer, String> wildcardToCheck = new Pair<>(null, reasonInfo.getExtraMessage());
+        if (mImsReasonCodeMap.containsKey(toCheck)) {
+            int toCode = mImsReasonCodeMap.get(toCheck);
+
+            log("maybeRemapReasonCode : fromCode = " + reasonInfo.getCode() + " ; message = "
+                    + reasonInfo.getExtraMessage() + " ; toCode = " + toCode);
+            return toCode;
+        } else if (mImsReasonCodeMap.containsKey(wildcardToCheck)) {
+            // Handle the case where a wildcard is specified for the fromCode; in this case we will
+            // match without caring about the fromCode.
+            int toCode = mImsReasonCodeMap.get(wildcardToCheck);
+
+            log("maybeRemapReasonCode : fromCode(wildcard) = " + reasonInfo.getCode() +
+                    " ; message = " + reasonInfo.getExtraMessage() + " ; toCode = " + toCode);
+            return toCode;
+        }
+        return code;
+    }
+
+    /**
+     * Maps an {@link ImsReasonInfo} reason code to a {@link DisconnectCause} cause code.
+     * The {@link Call.State} provided is the state of the call prior to disconnection.
+     * @param reasonInfo the {@link ImsReasonInfo} for the disconnection.
+     * @param callState The {@link Call.State} prior to disconnection.
+     * @return The {@link DisconnectCause} code.
+     */
+    @VisibleForTesting
+    public int getDisconnectCauseFromReasonInfo(ImsReasonInfo reasonInfo, Call.State callState) {
+        int cause = DisconnectCause.ERROR_UNSPECIFIED;
+
+        int code = maybeRemapReasonCode(reasonInfo);
+        switch (code) {
+            case ImsReasonInfo.CODE_SIP_BAD_ADDRESS:
+            case ImsReasonInfo.CODE_SIP_NOT_REACHABLE:
+                return DisconnectCause.NUMBER_UNREACHABLE;
+
+            case ImsReasonInfo.CODE_SIP_BUSY:
+                return DisconnectCause.BUSY;
+
+            case ImsReasonInfo.CODE_USER_TERMINATED:
+                return DisconnectCause.LOCAL;
+
+            case ImsReasonInfo.CODE_LOCAL_ENDED_BY_CONFERENCE_MERGE:
+                return DisconnectCause.IMS_MERGED_SUCCESSFULLY;
+
+            case ImsReasonInfo.CODE_LOCAL_CALL_DECLINE:
+            case ImsReasonInfo.CODE_REMOTE_CALL_DECLINE:
+                // If the call has been declined locally (on this device), or on remotely (on
+                // another device using multiendpoint functionality), mark it as rejected.
+                return DisconnectCause.INCOMING_REJECTED;
+
+            case ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE:
+                return DisconnectCause.NORMAL;
+
+            case ImsReasonInfo.CODE_SIP_FORBIDDEN:
+                return DisconnectCause.SERVER_ERROR;
+
+            case ImsReasonInfo.CODE_SIP_REDIRECTED:
+            case ImsReasonInfo.CODE_SIP_BAD_REQUEST:
+            case ImsReasonInfo.CODE_SIP_NOT_ACCEPTABLE:
+            case ImsReasonInfo.CODE_SIP_USER_REJECTED:
+            case ImsReasonInfo.CODE_SIP_GLOBAL_ERROR:
+                return DisconnectCause.SERVER_ERROR;
+
+            case ImsReasonInfo.CODE_SIP_SERVICE_UNAVAILABLE:
+            case ImsReasonInfo.CODE_SIP_NOT_FOUND:
+            case ImsReasonInfo.CODE_SIP_SERVER_ERROR:
+                return DisconnectCause.SERVER_UNREACHABLE;
+
+            case ImsReasonInfo.CODE_LOCAL_NETWORK_ROAMING:
+            case ImsReasonInfo.CODE_LOCAL_NETWORK_IP_CHANGED:
+            case ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN:
+            case ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE:
+            case ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED:
+            case ImsReasonInfo.CODE_LOCAL_NETWORK_NO_LTE_COVERAGE:
+            case ImsReasonInfo.CODE_LOCAL_NETWORK_NO_SERVICE:
+            case ImsReasonInfo.CODE_LOCAL_CALL_VCC_ON_PROGRESSING:
+                return DisconnectCause.OUT_OF_SERVICE;
+
+            case ImsReasonInfo.CODE_SIP_REQUEST_TIMEOUT:
+            case ImsReasonInfo.CODE_TIMEOUT_1XX_WAITING:
+            case ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER:
+            case ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER_CALL_UPDATE:
+                return DisconnectCause.TIMED_OUT;
+
+            case ImsReasonInfo.CODE_LOCAL_POWER_OFF:
+                return DisconnectCause.POWER_OFF;
+
+            case ImsReasonInfo.CODE_LOCAL_LOW_BATTERY:
+            case ImsReasonInfo.CODE_LOW_BATTERY: {
+                if (callState == Call.State.DIALING) {
+                    return DisconnectCause.DIAL_LOW_BATTERY;
+                } else {
+                    return DisconnectCause.LOW_BATTERY;
+                }
+            }
+
+            case ImsReasonInfo.CODE_FDN_BLOCKED:
+                return DisconnectCause.FDN_BLOCKED;
+
+            case ImsReasonInfo.CODE_IMEI_NOT_ACCEPTED:
+                return DisconnectCause.IMEI_NOT_ACCEPTED;
+
+            case ImsReasonInfo.CODE_ANSWERED_ELSEWHERE:
+                return DisconnectCause.ANSWERED_ELSEWHERE;
+
+            case ImsReasonInfo.CODE_CALL_END_CAUSE_CALL_PULL:
+                return DisconnectCause.CALL_PULLED;
+
+            case ImsReasonInfo.CODE_MAXIMUM_NUMBER_OF_CALLS_REACHED:
+                return DisconnectCause.MAXIMUM_NUMBER_OF_CALLS_REACHED;
+
+            case ImsReasonInfo.CODE_DATA_DISABLED:
+                return DisconnectCause.DATA_DISABLED;
+
+            case ImsReasonInfo.CODE_DATA_LIMIT_REACHED:
+                return DisconnectCause.DATA_LIMIT_REACHED;
+
+            case ImsReasonInfo.CODE_WIFI_LOST:
+                return DisconnectCause.WIFI_LOST;
+
+            case ImsReasonInfo.CODE_ACCESS_CLASS_BLOCKED:
+                return DisconnectCause.IMS_ACCESS_BLOCKED;
+
+            case ImsReasonInfo.CODE_EMERGENCY_TEMP_FAILURE:
+                return DisconnectCause.EMERGENCY_TEMP_FAILURE;
+
+            case ImsReasonInfo.CODE_EMERGENCY_PERM_FAILURE:
+                return DisconnectCause.EMERGENCY_PERM_FAILURE;
+
+            default:
+        }
+
+        return cause;
+    }
+
+    private int getPreciseDisconnectCauseFromReasonInfo(ImsReasonInfo reasonInfo) {
+        return PRECISE_CAUSE_MAP.get(maybeRemapReasonCode(reasonInfo),
+                PreciseDisconnectCause.ERROR_UNSPECIFIED);
+    }
+
+    /**
+     * @return true if the phone is in Emergency Callback mode, otherwise false
+     */
+    private boolean isPhoneInEcbMode() {
+        return mPhone.isInEcm();
+    }
+
+    /**
+     * Before dialing pending MO request, check for the Emergency Callback mode.
+     * If device is in Emergency callback mode, then exit the mode before dialing pending MO.
+     */
+    private void dialPendingMO() {
+        boolean isPhoneInEcmMode = isPhoneInEcbMode();
+        boolean isEmergencyNumber = mPendingMO.isEmergency();
+        if ((!isPhoneInEcmMode) || (isPhoneInEcmMode && isEmergencyNumber)) {
+            sendEmptyMessage(EVENT_DIAL_PENDINGMO);
+        } else {
+            sendEmptyMessage(EVENT_EXIT_ECBM_BEFORE_PENDINGMO);
+        }
+    }
+
+    /**
+     * Listen to the IMS call state change
+     */
+    private ImsCall.Listener mImsCallListener = new ImsCall.Listener() {
+        @Override
+        public void onCallProgressing(ImsCall imsCall) {
+            if (DBG) log("onCallProgressing");
+
+            mPendingMO = null;
+            processCallStateChange(imsCall, ImsPhoneCall.State.ALERTING,
+                    DisconnectCause.NOT_DISCONNECTED);
+            mMetrics.writeOnImsCallProgressing(mPhone.getPhoneId(), imsCall.getCallSession());
+        }
+
+        @Override
+        public void onCallStarted(ImsCall imsCall) {
+            if (DBG) log("onCallStarted");
+
+            if (mSwitchingFgAndBgCalls) {
+                // If we put a call on hold to answer an incoming call, we should reset the
+                // variables that keep track of the switch here.
+                if (mCallExpectedToResume != null && mCallExpectedToResume == imsCall) {
+                    if (DBG) log("onCallStarted: starting a call as a result of a switch.");
+                    mSwitchingFgAndBgCalls = false;
+                    mCallExpectedToResume = null;
+                }
+            }
+
+            mPendingMO = null;
+            processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE,
+                    DisconnectCause.NOT_DISCONNECTED);
+
+            if (mNotifyVtHandoverToWifiFail &&
+                    !imsCall.isWifiCall() && imsCall.isVideoCall() && isWifiConnected()) {
+                // Schedule check to see if handover succeeded.
+                sendMessageDelayed(obtainMessage(EVENT_CHECK_FOR_WIFI_HANDOVER, imsCall),
+                        HANDOVER_TO_WIFI_TIMEOUT_MS);
+            }
+
+            mMetrics.writeOnImsCallStarted(mPhone.getPhoneId(), imsCall.getCallSession());
+        }
+
+        @Override
+        public void onCallUpdated(ImsCall imsCall) {
+            if (DBG) log("onCallUpdated");
+            if (imsCall == null) {
+                return;
+            }
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                processCallStateChange(imsCall, conn.getCall().mState,
+                        DisconnectCause.NOT_DISCONNECTED, true /*ignore state update*/);
+                mMetrics.writeImsCallState(mPhone.getPhoneId(),
+                        imsCall.getCallSession(), conn.getCall().mState);
+            }
+        }
+
+        /**
+         * onCallStartFailed will be invoked when:
+         * case 1) Dialing fails
+         * case 2) Ringing call is disconnected by local or remote user
+         */
+        @Override
+        public void onCallStartFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
+            if (DBG) log("onCallStartFailed reasonCode=" + reasonInfo.getCode());
+
+            if (mSwitchingFgAndBgCalls) {
+                // If we put a call on hold to answer an incoming call, we should reset the
+                // variables that keep track of the switch here.
+                if (mCallExpectedToResume != null && mCallExpectedToResume == imsCall) {
+                    if (DBG) log("onCallStarted: starting a call as a result of a switch.");
+                    mSwitchingFgAndBgCalls = false;
+                    mCallExpectedToResume = null;
+                }
+            }
+
+            if (mPendingMO != null) {
+                // To initiate dialing circuit-switched call
+                if (reasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED
+                        && mBackgroundCall.getState() == ImsPhoneCall.State.IDLE
+                        && mRingingCall.getState() == ImsPhoneCall.State.IDLE) {
+                    mForegroundCall.detach(mPendingMO);
+                    removeConnection(mPendingMO);
+                    mPendingMO.finalize();
+                    mPendingMO = null;
+                    mPhone.initiateSilentRedial();
+                    return;
+                } else {
+                    mPendingMO = null;
+                    ImsPhoneConnection conn = findConnection(imsCall);
+                    Call.State callState;
+                    if (conn != null) {
+                        callState = conn.getState();
+                    } else {
+                        // Need to fall back in case connection is null; it shouldn't be, but a sane
+                        // fallback is to assume we're dialing.  This state is only used to
+                        // determine which disconnect string to show in the case of a low battery
+                        // disconnect.
+                        callState = Call.State.DIALING;
+                    }
+                    int cause = getDisconnectCauseFromReasonInfo(reasonInfo, callState);
+
+                    if(conn != null) {
+                        conn.setPreciseDisconnectCause(
+                                getPreciseDisconnectCauseFromReasonInfo(reasonInfo));
+                    }
+
+                    processCallStateChange(imsCall, ImsPhoneCall.State.DISCONNECTED, cause);
+                }
+                mMetrics.writeOnImsCallStartFailed(mPhone.getPhoneId(), imsCall.getCallSession(),
+                        reasonInfo);
+            }
+        }
+
+        @Override
+        public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) {
+            if (DBG) log("onCallTerminated reasonCode=" + reasonInfo.getCode());
+
+            ImsPhoneConnection conn = findConnection(imsCall);
+            Call.State callState;
+            if (conn != null) {
+                callState = conn.getState();
+            } else {
+                // Connection shouldn't be null, but if it is, we can assume the call was active.
+                // This call state is only used for determining which disconnect message to show in
+                // the case of the device's battery being low resulting in a call drop.
+                callState = Call.State.ACTIVE;
+            }
+            int cause = getDisconnectCauseFromReasonInfo(reasonInfo, callState);
+
+            if (DBG) log("cause = " + cause + " conn = " + conn);
+
+            if (conn != null) {
+                android.telecom.Connection.VideoProvider videoProvider = conn.getVideoProvider();
+                if (videoProvider instanceof ImsVideoCallProviderWrapper) {
+                    ImsVideoCallProviderWrapper wrapper = (ImsVideoCallProviderWrapper)
+                            videoProvider;
+
+                    wrapper.removeImsVideoProviderCallback(conn);
+                }
+            }
+            if (mOnHoldToneId == System.identityHashCode(conn)) {
+                if (conn != null && mOnHoldToneStarted) {
+                    mPhone.stopOnHoldTone(conn);
+                }
+                mOnHoldToneStarted = false;
+                mOnHoldToneId = -1;
+            }
+            if (conn != null) {
+                if (conn.isPulledCall() && (
+                        reasonInfo.getCode() == ImsReasonInfo.CODE_CALL_PULL_OUT_OF_SYNC ||
+                        reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_TEMPRARILY_UNAVAILABLE ||
+                        reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_FORBIDDEN) &&
+                        mPhone != null && mPhone.getExternalCallTracker() != null) {
+
+                    log("Call pull failed.");
+                    // Call was being pulled, but the call pull has failed -- inform the associated
+                    // TelephonyConnection that the pull failed, and provide it with the original
+                    // external connection which was pulled so that it can be swapped back.
+                    conn.onCallPullFailed(mPhone.getExternalCallTracker()
+                            .getConnectionById(conn.getPulledDialogId()));
+                    // Do not mark as disconnected; the call will just change from being a regular
+                    // call to being an external call again.
+                    cause = DisconnectCause.NOT_DISCONNECTED;
+
+                } else if (conn.isIncoming() && conn.getConnectTime() == 0
+                        && cause != DisconnectCause.ANSWERED_ELSEWHERE) {
+                    // Missed
+                    if (cause == DisconnectCause.NORMAL) {
+                        cause = DisconnectCause.INCOMING_MISSED;
+                    } else {
+                        cause = DisconnectCause.INCOMING_REJECTED;
+                    }
+                    if (DBG) log("Incoming connection of 0 connect time detected - translated " +
+                            "cause = " + cause);
+                }
+            }
+
+            if (cause == DisconnectCause.NORMAL && conn != null && conn.getImsCall().isMerged()) {
+                // Call was terminated while it is merged instead of a remote disconnect.
+                cause = DisconnectCause.IMS_MERGED_SUCCESSFULLY;
+            }
+
+            mMetrics.writeOnImsCallTerminated(mPhone.getPhoneId(), imsCall.getCallSession(),
+                    reasonInfo);
+
+            if(conn != null) {
+                conn.setPreciseDisconnectCause(getPreciseDisconnectCauseFromReasonInfo(reasonInfo));
+            }
+
+            processCallStateChange(imsCall, ImsPhoneCall.State.DISCONNECTED, cause);
+            if (mForegroundCall.getState() != ImsPhoneCall.State.ACTIVE) {
+                if (mRingingCall.getState().isRinging()) {
+                    // Drop pending MO. We should address incoming call first
+                    mPendingMO = null;
+                } else if (mPendingMO != null) {
+                    sendEmptyMessage(EVENT_DIAL_PENDINGMO);
+                }
+            }
+
+            if (mSwitchingFgAndBgCalls) {
+                if (DBG) {
+                    log("onCallTerminated: Call terminated in the midst of Switching " +
+                            "Fg and Bg calls.");
+                }
+                // If we are the in midst of swapping FG and BG calls and the call that was
+                // terminated was the one that we expected to resume, we need to swap the FG and
+                // BG calls back.
+                if (imsCall == mCallExpectedToResume) {
+                    if (DBG) {
+                        log("onCallTerminated: switching " + mForegroundCall + " with "
+                                + mBackgroundCall);
+                    }
+                    mForegroundCall.switchWith(mBackgroundCall);
+                }
+                // This call terminated in the midst of a switch after the other call was held, so
+                // resume it back to ACTIVE state since the switch failed.
+                log("onCallTerminated: foreground call in state " + mForegroundCall.getState() +
+                        " and ringing call in state " + (mRingingCall == null ? "null" :
+                        mRingingCall.getState().toString()));
+
+                if (mForegroundCall.getState() == ImsPhoneCall.State.HOLDING ||
+                        mRingingCall.getState() == ImsPhoneCall.State.WAITING) {
+                    sendEmptyMessage(EVENT_RESUME_BACKGROUND);
+                    mSwitchingFgAndBgCalls = false;
+                    mCallExpectedToResume = null;
+                }
+            }
+
+            if (mShouldUpdateImsConfigOnDisconnect) {
+                // Ensure we update the IMS config when the call is disconnected; we delayed this
+                // because a video call was paused.
+                ImsManager.updateImsServiceConfig(mPhone.getContext(), mPhone.getPhoneId(), true);
+                mShouldUpdateImsConfigOnDisconnect = false;
+            }
+        }
+
+        @Override
+        public void onCallHeld(ImsCall imsCall) {
+            if (DBG) {
+                if (mForegroundCall.getImsCall() == imsCall) {
+                    log("onCallHeld (fg) " + imsCall);
+                } else if (mBackgroundCall.getImsCall() == imsCall) {
+                    log("onCallHeld (bg) " + imsCall);
+                }
+            }
+
+            synchronized (mSyncHold) {
+                ImsPhoneCall.State oldState = mBackgroundCall.getState();
+                processCallStateChange(imsCall, ImsPhoneCall.State.HOLDING,
+                        DisconnectCause.NOT_DISCONNECTED);
+
+                // Note: If we're performing a switchWaitingOrHoldingAndActive, the call to
+                // processCallStateChange above may have caused the mBackgroundCall and
+                // mForegroundCall references below to change meaning.  Watch out for this if you
+                // are reading through this code.
+                if (oldState == ImsPhoneCall.State.ACTIVE) {
+                    // Note: This case comes up when we have just held a call in response to a
+                    // switchWaitingOrHoldingAndActive.  We now need to resume the background call.
+                    // The EVENT_RESUME_BACKGROUND causes resumeWaitingOrHolding to be called.
+                    if ((mForegroundCall.getState() == ImsPhoneCall.State.HOLDING)
+                            || (mRingingCall.getState() == ImsPhoneCall.State.WAITING)) {
+                            sendEmptyMessage(EVENT_RESUME_BACKGROUND);
+                    } else {
+                        //when multiple connections belong to background call,
+                        //only the first callback reaches here
+                        //otherwise the oldState is already HOLDING
+                        if (mPendingMO != null) {
+                            dialPendingMO();
+                        }
+
+                        // In this case there will be no call resumed, so we can assume that we
+                        // are done switching fg and bg calls now.
+                        // This may happen if there is no BG call and we are holding a call so that
+                        // we can dial another one.
+                        mSwitchingFgAndBgCalls = false;
+                    }
+                } else if (oldState == ImsPhoneCall.State.IDLE && mSwitchingFgAndBgCalls) {
+                    // The other call terminated in the midst of a switch before this call was held,
+                    // so resume the foreground call back to ACTIVE state since the switch failed.
+                    if (mForegroundCall.getState() == ImsPhoneCall.State.HOLDING) {
+                        sendEmptyMessage(EVENT_RESUME_BACKGROUND);
+                        mSwitchingFgAndBgCalls = false;
+                        mCallExpectedToResume = null;
+                    }
+                }
+            }
+            mMetrics.writeOnImsCallHeld(mPhone.getPhoneId(), imsCall.getCallSession());
+        }
+
+        @Override
+        public void onCallHoldFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
+            if (DBG) log("onCallHoldFailed reasonCode=" + reasonInfo.getCode());
+
+            synchronized (mSyncHold) {
+                ImsPhoneCall.State bgState = mBackgroundCall.getState();
+                if (reasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED) {
+                    // disconnected while processing hold
+                    if (mPendingMO != null) {
+                        dialPendingMO();
+                    }
+                } else if (bgState == ImsPhoneCall.State.ACTIVE) {
+                    mForegroundCall.switchWith(mBackgroundCall);
+
+                    if (mPendingMO != null) {
+                        mPendingMO.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED);
+                        sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
+                    }
+                }
+                mPhone.notifySuppServiceFailed(Phone.SuppService.HOLD);
+            }
+            mMetrics.writeOnImsCallHoldFailed(mPhone.getPhoneId(), imsCall.getCallSession(),
+                    reasonInfo);
+        }
+
+        @Override
+        public void onCallResumed(ImsCall imsCall) {
+            if (DBG) log("onCallResumed");
+
+            // If we are the in midst of swapping FG and BG calls and the call we end up resuming
+            // is not the one we expected, we likely had a resume failure and we need to swap the
+            // FG and BG calls back.
+            if (mSwitchingFgAndBgCalls) {
+                if (imsCall != mCallExpectedToResume) {
+                    // If the call which resumed isn't as expected, we need to swap back to the
+                    // previous configuration; the swap has failed.
+                    if (DBG) {
+                        log("onCallResumed : switching " + mForegroundCall + " with "
+                                + mBackgroundCall);
+                    }
+                    mForegroundCall.switchWith(mBackgroundCall);
+                } else {
+                    // The call which resumed is the one we expected to resume, so we can clear out
+                    // the mSwitchingFgAndBgCalls flag.
+                    if (DBG) {
+                        log("onCallResumed : expected call resumed.");
+                    }
+                }
+                mSwitchingFgAndBgCalls = false;
+                mCallExpectedToResume = null;
+            }
+            processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE,
+                    DisconnectCause.NOT_DISCONNECTED);
+            mMetrics.writeOnImsCallResumed(mPhone.getPhoneId(), imsCall.getCallSession());
+        }
+
+        @Override
+        public void onCallResumeFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
+            if (mSwitchingFgAndBgCalls) {
+                // If we are in the midst of swapping the FG and BG calls and
+                // we got a resume fail, we need to swap back the FG and BG calls.
+                // Since the FG call was held, will also try to resume the same.
+                if (imsCall == mCallExpectedToResume) {
+                    if (DBG) {
+                        log("onCallResumeFailed : switching " + mForegroundCall + " with "
+                                + mBackgroundCall);
+                    }
+                    mForegroundCall.switchWith(mBackgroundCall);
+                    if (mForegroundCall.getState() == ImsPhoneCall.State.HOLDING) {
+                            sendEmptyMessage(EVENT_RESUME_BACKGROUND);
+                    }
+                }
+
+                //Call swap is done, reset the relevant variables
+                mCallExpectedToResume = null;
+                mSwitchingFgAndBgCalls = false;
+            }
+            mPhone.notifySuppServiceFailed(Phone.SuppService.RESUME);
+            mMetrics.writeOnImsCallResumeFailed(mPhone.getPhoneId(), imsCall.getCallSession(),
+                    reasonInfo);
+        }
+
+        @Override
+        public void onCallResumeReceived(ImsCall imsCall) {
+            if (DBG) log("onCallResumeReceived");
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                if (mOnHoldToneStarted) {
+                    mPhone.stopOnHoldTone(conn);
+                    mOnHoldToneStarted = false;
+                }
+                conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_REMOTELY_UNHELD, null);
+            }
+
+            boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.config_useVideoPauseWorkaround);
+            if (useVideoPauseWorkaround && mSupportPauseVideo &&
+                    VideoProfile.isVideo(conn.getVideoState())) {
+                // If we are using the video pause workaround, the vendor IMS code has issues
+                // with video pause signalling.  In this case, when a call is remotely
+                // held, the modem does not reliably change the video state of the call to be
+                // paused.
+                // As a workaround, we will turn on that bit now.
+                conn.changeToUnPausedState();
+            }
+
+            SuppServiceNotification supp = new SuppServiceNotification();
+            // Type of notification: 0 = MO; 1 = MT
+            // Refer SuppServiceNotification class documentation.
+            supp.notificationType = 1;
+            supp.code = SuppServiceNotification.MT_CODE_CALL_RETRIEVED;
+            mPhone.notifySuppSvcNotification(supp);
+            mMetrics.writeOnImsCallResumeReceived(mPhone.getPhoneId(), imsCall.getCallSession());
+        }
+
+        @Override
+        public void onCallHoldReceived(ImsCall imsCall) {
+            if (DBG) log("onCallHoldReceived");
+
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                if (!mOnHoldToneStarted && ImsPhoneCall.isLocalTone(imsCall) &&
+                        conn.getState() == ImsPhoneCall.State.ACTIVE) {
+                    mPhone.startOnHoldTone(conn);
+                    mOnHoldToneStarted = true;
+                    mOnHoldToneId = System.identityHashCode(conn);
+                }
+                conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_REMOTELY_HELD, null);
+
+                boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
+                        com.android.internal.R.bool.config_useVideoPauseWorkaround);
+                if (useVideoPauseWorkaround && mSupportPauseVideo &&
+                        VideoProfile.isVideo(conn.getVideoState())) {
+                    // If we are using the video pause workaround, the vendor IMS code has issues
+                    // with video pause signalling.  In this case, when a call is remotely
+                    // held, the modem does not reliably change the video state of the call to be
+                    // paused.
+                    // As a workaround, we will turn on that bit now.
+                    conn.changeToPausedState();
+                }
+            }
+
+            SuppServiceNotification supp = new SuppServiceNotification();
+            // Type of notification: 0 = MO; 1 = MT
+            // Refer SuppServiceNotification class documentation.
+            supp.notificationType = 1;
+            supp.code = SuppServiceNotification.MT_CODE_CALL_ON_HOLD;
+            mPhone.notifySuppSvcNotification(supp);
+            mMetrics.writeOnImsCallHoldReceived(mPhone.getPhoneId(), imsCall.getCallSession());
+        }
+
+        @Override
+        public void onCallSuppServiceReceived(ImsCall call,
+                ImsSuppServiceNotification suppServiceInfo) {
+            if (DBG) log("onCallSuppServiceReceived: suppServiceInfo=" + suppServiceInfo);
+
+            SuppServiceNotification supp = new SuppServiceNotification();
+            supp.notificationType = suppServiceInfo.notificationType;
+            supp.code = suppServiceInfo.code;
+            supp.index = suppServiceInfo.index;
+            supp.number = suppServiceInfo.number;
+            supp.history = suppServiceInfo.history;
+
+            mPhone.notifySuppSvcNotification(supp);
+        }
+
+        @Override
+        public void onCallMerged(final ImsCall call, final ImsCall peerCall, boolean swapCalls) {
+            if (DBG) log("onCallMerged");
+
+            ImsPhoneCall foregroundImsPhoneCall = findConnection(call).getCall();
+            ImsPhoneConnection peerConnection = findConnection(peerCall);
+            ImsPhoneCall peerImsPhoneCall = peerConnection == null ? null
+                    : peerConnection.getCall();
+
+            if (swapCalls) {
+                switchAfterConferenceSuccess();
+            }
+            foregroundImsPhoneCall.merge(peerImsPhoneCall, ImsPhoneCall.State.ACTIVE);
+
+            try {
+                final ImsPhoneConnection conn = findConnection(call);
+                log("onCallMerged: ImsPhoneConnection=" + conn);
+                log("onCallMerged: CurrentVideoProvider=" + conn.getVideoProvider());
+                setVideoCallProvider(conn, call);
+                log("onCallMerged: CurrentVideoProvider=" + conn.getVideoProvider());
+            } catch (Exception e) {
+                loge("onCallMerged: exception " + e);
+            }
+
+            // After merge complete, update foreground as Active
+            // and background call as Held, if background call exists
+            processCallStateChange(mForegroundCall.getImsCall(), ImsPhoneCall.State.ACTIVE,
+                    DisconnectCause.NOT_DISCONNECTED);
+            if (peerConnection != null) {
+                processCallStateChange(mBackgroundCall.getImsCall(), ImsPhoneCall.State.HOLDING,
+                    DisconnectCause.NOT_DISCONNECTED);
+            }
+
+            // Check if the merge was requested by an existing conference call. In that
+            // case, no further action is required.
+            if (!call.isMergeRequestedByConf()) {
+                log("onCallMerged :: calling onMultipartyStateChanged()");
+                onMultipartyStateChanged(call, true);
+            } else {
+                log("onCallMerged :: Merge requested by existing conference.");
+                // Reset the flag.
+                call.resetIsMergeRequestedByConf(false);
+            }
+            logState();
+        }
+
+        @Override
+        public void onCallMergeFailed(ImsCall call, ImsReasonInfo reasonInfo) {
+            if (DBG) log("onCallMergeFailed reasonInfo=" + reasonInfo);
+
+            // TODO: the call to notifySuppServiceFailed throws up the "merge failed" dialog
+            // We should move this into the InCallService so that it is handled appropriately
+            // based on the user facing UI.
+            mPhone.notifySuppServiceFailed(Phone.SuppService.CONFERENCE);
+
+            // Start plumbing this even through Telecom so other components can take
+            // appropriate action.
+            ImsPhoneConnection conn = findConnection(call);
+            if (conn != null) {
+                conn.onConferenceMergeFailed();
+                conn.handleMergeComplete();
+            }
+        }
+
+        /**
+         * Called when the state of IMS conference participant(s) has changed.
+         *
+         * @param call the call object that carries out the IMS call.
+         * @param participants the participant(s) and their new state information.
+         */
+        @Override
+        public void onConferenceParticipantsStateChanged(ImsCall call,
+                List<ConferenceParticipant> participants) {
+            if (DBG) log("onConferenceParticipantsStateChanged");
+
+            ImsPhoneConnection conn = findConnection(call);
+            if (conn != null) {
+                conn.updateConferenceParticipants(participants);
+            }
+        }
+
+        @Override
+        public void onCallSessionTtyModeReceived(ImsCall call, int mode) {
+            mPhone.onTtyModeReceived(mode);
+        }
+
+        @Override
+        public void onCallHandover(ImsCall imsCall, int srcAccessTech, int targetAccessTech,
+            ImsReasonInfo reasonInfo) {
+            if (DBG) {
+                log("onCallHandover ::  srcAccessTech=" + srcAccessTech + ", targetAccessTech=" +
+                        targetAccessTech + ", reasonInfo=" + reasonInfo);
+            }
+
+            // Only consider it a valid handover to WIFI if the source radio tech is known.
+            boolean isHandoverToWifi = srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
+                    && srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                    && targetAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
+            if (isHandoverToWifi) {
+                // If we handed over to wifi successfully, don't check for failure in the future.
+                removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
+            }
+
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                // Only consider it a handover from WIFI if the source and target radio tech is known.
+                boolean isHandoverFromWifi =
+                        srcAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+                                && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
+                                && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
+                if (isHandoverFromWifi && imsCall.isVideoCall()) {
+                    if (mNotifyHandoverVideoFromWifiToLTE) {
+                        log("onCallHandover :: notifying of WIFI to LTE handover.");
+                        conn.onConnectionEvent(
+                                TelephonyManager.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE, null);
+                    }
+
+                    if (!mIsDataEnabled && mIsViLteDataMetered) {
+                        // Call was downgraded from WIFI to LTE and data is metered; downgrade the
+                        // call now.
+                        downgradeVideoCall(ImsReasonInfo.CODE_DATA_DISABLED, conn);
+                    }
+                }
+            } else {
+                loge("onCallHandover :: connection null.");
+            }
+
+            mMetrics.writeOnImsCallHandoverEvent(mPhone.getPhoneId(),
+                    TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER, imsCall.getCallSession(),
+                    srcAccessTech, targetAccessTech, reasonInfo);
+        }
+
+        @Override
+        public void onCallHandoverFailed(ImsCall imsCall, int srcAccessTech, int targetAccessTech,
+            ImsReasonInfo reasonInfo) {
+            if (DBG) {
+                log("onCallHandoverFailed :: srcAccessTech=" + srcAccessTech +
+                    ", targetAccessTech=" + targetAccessTech + ", reasonInfo=" + reasonInfo);
+            }
+            mMetrics.writeOnImsCallHandoverEvent(mPhone.getPhoneId(),
+                    TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER_FAILED,
+                    imsCall.getCallSession(), srcAccessTech, targetAccessTech, reasonInfo);
+
+            boolean isHandoverToWifi = srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN &&
+                    targetAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null && isHandoverToWifi) {
+                log("onCallHandoverFailed - handover to WIFI Failed");
+
+                // If we know we failed to handover, don't check for failure in the future.
+                removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
+
+                if (mNotifyVtHandoverToWifiFail) {
+                    // Only notify others if carrier config indicates to do so.
+                    conn.onHandoverToWifiFailed();
+                }
+            }
+        }
+
+        @Override
+        public void onRttModifyRequestReceived(ImsCall imsCall) {
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                conn.onRttModifyRequestReceived();
+            }
+        }
+
+        @Override
+        public void onRttModifyResponseReceived(ImsCall imsCall, int status) {
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                conn.onRttModifyResponseReceived(status);
+                if (status ==
+                        android.telecom.Connection.RttModifyStatus.SESSION_MODIFY_REQUEST_SUCCESS) {
+                    conn.startRttTextProcessing();
+                }
+            }
+        }
+
+        @Override
+        public void onRttMessageReceived(ImsCall imsCall, String message) {
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                conn.onRttMessageReceived(message);
+            }
+        }
+
+        /**
+         * Handles a change to the multiparty state for an {@code ImsCall}.  Notifies the associated
+         * {@link ImsPhoneConnection} of the change.
+         *
+         * @param imsCall The IMS call.
+         * @param isMultiParty {@code true} if the call became multiparty, {@code false}
+         *      otherwise.
+         */
+        @Override
+        public void onMultipartyStateChanged(ImsCall imsCall, boolean isMultiParty) {
+            if (DBG) log("onMultipartyStateChanged to " + (isMultiParty ? "Y" : "N"));
+
+            ImsPhoneConnection conn = findConnection(imsCall);
+            if (conn != null) {
+                conn.updateMultipartyState(isMultiParty);
+            }
+        }
+    };
+
+    /**
+     * Listen to the IMS call state change
+     */
+    private ImsCall.Listener mImsUssdListener = new ImsCall.Listener() {
+        @Override
+        public void onCallStarted(ImsCall imsCall) {
+            if (DBG) log("mImsUssdListener onCallStarted");
+
+            if (imsCall == mUssdSession) {
+                if (mPendingUssd != null) {
+                    AsyncResult.forMessage(mPendingUssd);
+                    mPendingUssd.sendToTarget();
+                    mPendingUssd = null;
+                }
+            }
+        }
+
+        @Override
+        public void onCallStartFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
+            if (DBG) log("mImsUssdListener onCallStartFailed reasonCode=" + reasonInfo.getCode());
+
+            onCallTerminated(imsCall, reasonInfo);
+        }
+
+        @Override
+        public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) {
+            if (DBG) log("mImsUssdListener onCallTerminated reasonCode=" + reasonInfo.getCode());
+            removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
+
+            if (imsCall == mUssdSession) {
+                mUssdSession = null;
+                if (mPendingUssd != null) {
+                    CommandException ex =
+                            new CommandException(CommandException.Error.GENERIC_FAILURE);
+                    AsyncResult.forMessage(mPendingUssd, null, ex);
+                    mPendingUssd.sendToTarget();
+                    mPendingUssd = null;
+                }
+            }
+            imsCall.close();
+        }
+
+        @Override
+        public void onCallUssdMessageReceived(ImsCall call,
+                int mode, String ussdMessage) {
+            if (DBG) log("mImsUssdListener onCallUssdMessageReceived mode=" + mode);
+
+            int ussdMode = -1;
+
+            switch(mode) {
+                case ImsCall.USSD_MODE_REQUEST:
+                    ussdMode = CommandsInterface.USSD_MODE_REQUEST;
+                    break;
+
+                case ImsCall.USSD_MODE_NOTIFY:
+                    ussdMode = CommandsInterface.USSD_MODE_NOTIFY;
+                    break;
+            }
+
+            mPhone.onIncomingUSSD(ussdMode, ussdMessage);
+        }
+    };
+
+    /**
+     * Listen to the IMS service state change
+     *
+     */
+    private ImsConnectionStateListener mImsConnectionStateListener =
+        new ImsConnectionStateListener() {
+        @Override
+        public void onImsConnected(int imsRadioTech) {
+            if (DBG) log("onImsConnected imsRadioTech=" + imsRadioTech);
+            mPhone.setServiceState(ServiceState.STATE_IN_SERVICE);
+            mPhone.setImsRegistered(true);
+            mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
+                    ImsConnectionState.State.CONNECTED, null);
+        }
+
+        @Override
+        public void onImsDisconnected(ImsReasonInfo imsReasonInfo) {
+            if (DBG) log("onImsDisconnected imsReasonInfo=" + imsReasonInfo);
+            resetImsCapabilities();
+            mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
+            mPhone.setImsRegistered(false);
+            mPhone.processDisconnectReason(imsReasonInfo);
+            mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
+                    ImsConnectionState.State.DISCONNECTED, imsReasonInfo);
+        }
+
+        @Override
+        public void onImsProgressing(int imsRadioTech) {
+            if (DBG) log("onImsProgressing imsRadioTech=" + imsRadioTech);
+            mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
+            mPhone.setImsRegistered(false);
+            mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
+                    ImsConnectionState.State.PROGRESSING, null);
+        }
+
+        @Override
+        public void onImsResumed() {
+            if (DBG) log("onImsResumed");
+            mPhone.setServiceState(ServiceState.STATE_IN_SERVICE);
+            mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
+                    ImsConnectionState.State.RESUMED, null);
+        }
+
+        @Override
+        public void onImsSuspended() {
+            if (DBG) log("onImsSuspended");
+            mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
+            mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
+                    ImsConnectionState.State.SUSPENDED, null);
+
+        }
+
+        @Override
+        public void onFeatureCapabilityChanged(int serviceClass,
+                int[] enabledFeatures, int[] disabledFeatures) {
+            if (DBG) log("onFeatureCapabilityChanged");
+            SomeArgs args = SomeArgs.obtain();
+            args.argi1 = serviceClass;
+            args.arg1 = enabledFeatures;
+            args.arg2 = disabledFeatures;
+            // Remove any pending updates; they're already stale, so no need to process them.
+            removeMessages(EVENT_ON_FEATURE_CAPABILITY_CHANGED);
+            obtainMessage(EVENT_ON_FEATURE_CAPABILITY_CHANGED, args).sendToTarget();
+        }
+
+        @Override
+        public void onVoiceMessageCountChanged(int count) {
+            if (DBG) log("onVoiceMessageCountChanged :: count=" + count);
+            mPhone.mDefaultPhone.setVoiceMessageCount(count);
+        }
+
+        @Override
+        public void registrationAssociatedUriChanged(Uri[] uris) {
+            if (DBG) log("registrationAssociatedUriChanged");
+            mPhone.setCurrentSubscriberUris(uris);
+        }
+    };
+
+    private ImsConfigListener.Stub mImsConfigListener = new ImsConfigListener.Stub() {
+        @Override
+        public void onGetFeatureResponse(int feature, int network, int value, int status) {}
+
+        @Override
+        public void onSetFeatureResponse(int feature, int network, int value, int status) {
+            mMetrics.writeImsSetFeatureValue(
+                    mPhone.getPhoneId(), feature, network, value, status);
+        }
+
+        @Override
+        public void onGetVideoQuality(int status, int quality) {}
+
+        @Override
+        public void onSetVideoQuality(int status) {}
+
+    };
+
+    public ImsUtInterface getUtInterface() throws ImsException {
+        if (mImsManager == null) {
+            throw getImsManagerIsNullException();
+        }
+
+        ImsUtInterface ut = mImsManager.getSupplementaryServiceConfiguration();
+        return ut;
+    }
+
+    private void transferHandoverConnections(ImsPhoneCall call) {
+        if (call.mConnections != null) {
+            for (Connection c : call.mConnections) {
+                c.mPreHandoverState = call.mState;
+                log ("Connection state before handover is " + c.getStateBeforeHandover());
+            }
+        }
+        if (mHandoverCall.mConnections == null ) {
+            mHandoverCall.mConnections = call.mConnections;
+        } else { // Multi-call SRVCC
+            mHandoverCall.mConnections.addAll(call.mConnections);
+        }
+        if (mHandoverCall.mConnections != null) {
+            if (call.getImsCall() != null) {
+                call.getImsCall().close();
+            }
+            for (Connection c : mHandoverCall.mConnections) {
+                ((ImsPhoneConnection)c).changeParent(mHandoverCall);
+                ((ImsPhoneConnection)c).releaseWakeLock();
+            }
+        }
+        if (call.getState().isAlive()) {
+            log ("Call is alive and state is " + call.mState);
+            mHandoverCall.mState = call.mState;
+        }
+        call.mConnections.clear();
+        call.mState = ImsPhoneCall.State.IDLE;
+    }
+
+    /* package */
+    void notifySrvccState(Call.SrvccState state) {
+        if (DBG) log("notifySrvccState state=" + state);
+
+        mSrvccState = state;
+
+        if (mSrvccState == Call.SrvccState.COMPLETED) {
+            transferHandoverConnections(mForegroundCall);
+            transferHandoverConnections(mBackgroundCall);
+            transferHandoverConnections(mRingingCall);
+        }
+    }
+
+    //****** Overridden from Handler
+
+    @Override
+    public void
+    handleMessage (Message msg) {
+        AsyncResult ar;
+        if (DBG) log("handleMessage what=" + msg.what);
+
+        switch (msg.what) {
+            case EVENT_HANGUP_PENDINGMO:
+                if (mPendingMO != null) {
+                    mPendingMO.onDisconnect();
+                    removeConnection(mPendingMO);
+                    mPendingMO = null;
+                }
+                mPendingIntentExtras = null;
+                updatePhoneState();
+                mPhone.notifyPreciseCallStateChanged();
+                break;
+            case EVENT_RESUME_BACKGROUND:
+                try {
+                    resumeWaitingOrHolding();
+                } catch (CallStateException e) {
+                    if (Phone.DEBUG_PHONE) {
+                        loge("handleMessage EVENT_RESUME_BACKGROUND exception=" + e);
+                    }
+                }
+                break;
+            case EVENT_DIAL_PENDINGMO:
+                dialInternal(mPendingMO, mClirMode, mPendingCallVideoState, mPendingIntentExtras);
+                mPendingIntentExtras = null;
+                break;
+
+            case EVENT_EXIT_ECBM_BEFORE_PENDINGMO:
+                if (mPendingMO != null) {
+                    //Send ECBM exit request
+                    try {
+                        getEcbmInterface().exitEmergencyCallbackMode();
+                        mPhone.setOnEcbModeExitResponse(this, EVENT_EXIT_ECM_RESPONSE_CDMA, null);
+                        pendingCallClirMode = mClirMode;
+                        pendingCallInEcm = true;
+                    } catch (ImsException e) {
+                        e.printStackTrace();
+                        mPendingMO.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED);
+                        sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
+                    }
+                }
+                break;
+
+            case EVENT_EXIT_ECM_RESPONSE_CDMA:
+                // no matter the result, we still do the same here
+                if (pendingCallInEcm) {
+                    dialInternal(mPendingMO, pendingCallClirMode,
+                            mPendingCallVideoState, mPendingIntentExtras);
+                    mPendingIntentExtras = null;
+                    pendingCallInEcm = false;
+                }
+                mPhone.unsetOnEcbModeExitResponse(this);
+                break;
+            case EVENT_VT_DATA_USAGE_UPDATE:
+                ar = (AsyncResult) msg.obj;
+                ImsCall call = (ImsCall) ar.userObj;
+                Long usage = (long) ar.result;
+                log("VT data usage update. usage = " + usage + ", imsCall = " + call);
+                if (usage > 0) {
+                    updateVtDataUsage(call, usage);
+                }
+                break;
+            case EVENT_DATA_ENABLED_CHANGED:
+                ar = (AsyncResult) msg.obj;
+                if (ar.result instanceof Pair) {
+                    Pair<Boolean, Integer> p = (Pair<Boolean, Integer>) ar.result;
+                    onDataEnabledChanged(p.first, p.second);
+                }
+                break;
+            case EVENT_GET_IMS_SERVICE:
+                try {
+                    getImsService();
+                } catch (ImsException e) {
+                    loge("getImsService: " + e);
+                    retryGetImsService();
+                }
+                break;
+            case EVENT_CHECK_FOR_WIFI_HANDOVER:
+                if (msg.obj instanceof ImsCall) {
+                    ImsCall imsCall = (ImsCall) msg.obj;
+                    if (!imsCall.isWifiCall()) {
+                        // Call did not handover to wifi, notify of handover failure.
+                        ImsPhoneConnection conn = findConnection(imsCall);
+                        if (conn != null) {
+                            conn.onHandoverToWifiFailed();
+                        }
+                    }
+                }
+                break;
+            case EVENT_ON_FEATURE_CAPABILITY_CHANGED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                try {
+                    int serviceClass = args.argi1;
+                    int[] enabledFeatures = (int[]) args.arg1;
+                    int[] disabledFeatures = (int[]) args.arg2;
+                    handleFeatureCapabilityChanged(serviceClass, enabledFeatures, disabledFeatures);
+                } finally {
+                    args.recycle();
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Update video call data usage
+     *
+     * @param call The IMS call
+     * @param dataUsage The aggregated data usage for the call
+     */
+    private void updateVtDataUsage(ImsCall call, long dataUsage) {
+        long oldUsage = 0L;
+        if (mVtDataUsageMap.containsKey(call.uniqueId)) {
+            oldUsage = mVtDataUsageMap.get(call.uniqueId);
+        }
+
+        long delta = dataUsage - oldUsage;
+        mVtDataUsageMap.put(call.uniqueId, dataUsage);
+
+        log("updateVtDataUsage: call=" + call + ", delta=" + delta);
+
+        long currentTime = SystemClock.elapsedRealtime();
+        int isRoaming = mPhone.getServiceState().getDataRoaming() ? 1 : 0;
+
+        // Create the snapshot of total video call data usage.
+        NetworkStats vtDataUsageSnapshot = new NetworkStats(currentTime, 1);
+        vtDataUsageSnapshot.combineAllValues(mVtDataUsageSnapshot);
+        // Since the modem only reports the total vt data usage rather than rx/tx separately,
+        // the only thing we can do here is splitting the usage into half rx and half tx.
+        // Uid -1 indicates this is for the overall device data usage.
+        vtDataUsageSnapshot.combineValues(new NetworkStats.Entry(
+                NetworkStatsService.VT_INTERFACE, -1, NetworkStats.SET_FOREGROUND,
+                NetworkStats.TAG_NONE, 1, isRoaming, delta / 2, 0, delta / 2, 0, 0));
+        mVtDataUsageSnapshot = vtDataUsageSnapshot;
+
+        // Create the snapshot of video call data usage per dialer. combineValues will create
+        // a separate entry if uid is different from the previous snapshot.
+        NetworkStats vtDataUsageUidSnapshot = new NetworkStats(currentTime, 1);
+        vtDataUsageUidSnapshot.combineAllValues(mVtDataUsageUidSnapshot);
+        // Since the modem only reports the total vt data usage rather than rx/tx separately,
+        // the only thing we can do here is splitting the usage into half rx and half tx.
+        vtDataUsageUidSnapshot.combineValues(new NetworkStats.Entry(
+                NetworkStatsService.VT_INTERFACE, mDefaultDialerUid.get(),
+                NetworkStats.SET_FOREGROUND, NetworkStats.TAG_NONE, 1, isRoaming, delta / 2,
+                0, delta / 2, 0, 0));
+        mVtDataUsageUidSnapshot = vtDataUsageUidSnapshot;
+    }
+
+    @Override
+    protected void log(String msg) {
+        Rlog.d(LOG_TAG, "[ImsPhoneCallTracker] " + msg);
+    }
+
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, "[ImsPhoneCallTracker] " + msg);
+    }
+
+    /**
+     * Logs the current state of the ImsPhoneCallTracker.  Useful for debugging issues with
+     * call tracking.
+     */
+    /* package */
+    void logState() {
+        if (!VERBOSE_STATE_LOGGING) {
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("Current IMS PhoneCall State:\n");
+        sb.append(" Foreground: ");
+        sb.append(mForegroundCall);
+        sb.append("\n");
+        sb.append(" Background: ");
+        sb.append(mBackgroundCall);
+        sb.append("\n");
+        sb.append(" Ringing: ");
+        sb.append(mRingingCall);
+        sb.append("\n");
+        sb.append(" Handover: ");
+        sb.append(mHandoverCall);
+        sb.append("\n");
+        Rlog.v(LOG_TAG, sb.toString());
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("ImsPhoneCallTracker extends:");
+        super.dump(fd, pw, args);
+        pw.println(" mVoiceCallEndedRegistrants=" + mVoiceCallEndedRegistrants);
+        pw.println(" mVoiceCallStartedRegistrants=" + mVoiceCallStartedRegistrants);
+        pw.println(" mRingingCall=" + mRingingCall);
+        pw.println(" mForegroundCall=" + mForegroundCall);
+        pw.println(" mBackgroundCall=" + mBackgroundCall);
+        pw.println(" mHandoverCall=" + mHandoverCall);
+        pw.println(" mPendingMO=" + mPendingMO);
+        //pw.println(" mHangupPendingMO=" + mHangupPendingMO);
+        pw.println(" mPhone=" + mPhone);
+        pw.println(" mDesiredMute=" + mDesiredMute);
+        pw.println(" mState=" + mState);
+        for (int i = 0; i < mImsFeatureEnabled.length; i++) {
+            pw.println(" " + mImsFeatureStrings[i] + ": "
+                    + ((mImsFeatureEnabled[i]) ? "enabled" : "disabled"));
+        }
+        pw.println(" mDefaultDialerUid=" + mDefaultDialerUid.get());
+        pw.println(" mVtDataUsageSnapshot=" + mVtDataUsageSnapshot);
+        pw.println(" mVtDataUsageUidSnapshot=" + mVtDataUsageUidSnapshot);
+
+        pw.flush();
+        pw.println("++++++++++++++++++++++++++++++++");
+
+        try {
+            if (mImsManager != null) {
+                mImsManager.dump(fd, pw, args);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        if (mConnections != null && mConnections.size() > 0) {
+            pw.println("mConnections:");
+            for (int i = 0; i < mConnections.size(); i++) {
+                pw.println("  [" + i + "]: " + mConnections.get(i));
+            }
+        }
+    }
+
+    @Override
+    protected void handlePollCalls(AsyncResult ar) {
+    }
+
+    /* package */
+    ImsEcbm getEcbmInterface() throws ImsException {
+        if (mImsManager == null) {
+            throw getImsManagerIsNullException();
+        }
+
+        ImsEcbm ecbm = mImsManager.getEcbmInterface(mServiceId);
+        return ecbm;
+    }
+
+    /* package */
+    ImsMultiEndpoint getMultiEndpointInterface() throws ImsException {
+        if (mImsManager == null) {
+            throw getImsManagerIsNullException();
+        }
+
+        try {
+            return mImsManager.getMultiEndpointInterface(mServiceId);
+        } catch (ImsException e) {
+            if (e.getCode() == ImsReasonInfo.CODE_MULTIENDPOINT_NOT_SUPPORTED) {
+                return null;
+            } else {
+                throw e;
+            }
+
+        }
+    }
+
+    public boolean isInEmergencyCall() {
+        return mIsInEmergencyCall;
+    }
+
+    public boolean isVolteEnabled() {
+        return mImsFeatureEnabled[ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE];
+    }
+
+    public boolean isVowifiEnabled() {
+        return mImsFeatureEnabled[ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_WIFI];
+    }
+
+    public boolean isVideoCallEnabled() {
+        return (mImsFeatureEnabled[ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_LTE]
+                || mImsFeatureEnabled[ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_WIFI]);
+    }
+
+    @Override
+    public PhoneConstants.State getState() {
+        return mState;
+    }
+
+    private void retryGetImsService() {
+        // The binder connection is already up. Do not try to get it again.
+        if (mImsManager.isServiceAvailable()) {
+            return;
+        }
+        //Leave mImsManager as null, then CallStateException will be thrown when dialing
+        mImsManager = null;
+        // Exponential backoff during retry, limited to 32 seconds.
+        loge("getImsService: Retrying getting ImsService...");
+        removeMessages(EVENT_GET_IMS_SERVICE);
+        sendEmptyMessageDelayed(EVENT_GET_IMS_SERVICE, mRetryTimeout.get());
+    }
+
+    private void setVideoCallProvider(ImsPhoneConnection conn, ImsCall imsCall)
+            throws RemoteException {
+        IImsVideoCallProvider imsVideoCallProvider =
+                imsCall.getCallSession().getVideoCallProvider();
+        if (imsVideoCallProvider != null) {
+            // TODO: Remove this when we can better formalize the format of session modify requests.
+            boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.config_useVideoPauseWorkaround);
+
+            ImsVideoCallProviderWrapper imsVideoCallProviderWrapper =
+                    new ImsVideoCallProviderWrapper(imsVideoCallProvider);
+            if (useVideoPauseWorkaround) {
+                imsVideoCallProviderWrapper.setUseVideoPauseWorkaround(useVideoPauseWorkaround);
+            }
+            conn.setVideoProvider(imsVideoCallProviderWrapper);
+            imsVideoCallProviderWrapper.registerForDataUsageUpdate
+                    (this, EVENT_VT_DATA_USAGE_UPDATE, imsCall);
+            imsVideoCallProviderWrapper.addImsVideoProviderCallback(conn);
+        }
+    }
+
+    public boolean isUtEnabled() {
+        return (mImsFeatureEnabled[ImsConfig.FeatureConstants.FEATURE_TYPE_UT_OVER_LTE]
+            || mImsFeatureEnabled[ImsConfig.FeatureConstants.FEATURE_TYPE_UT_OVER_WIFI]);
+    }
+
+    /**
+     * Given a call subject, removes any characters considered by the current carrier to be
+     * invalid, as well as escaping (using \) any characters which the carrier requires to be
+     * escaped.
+     *
+     * @param callSubject The call subject.
+     * @return The call subject with invalid characters removed and escaping applied as required.
+     */
+    private String cleanseInstantLetteringMessage(String callSubject) {
+        if (TextUtils.isEmpty(callSubject)) {
+            return callSubject;
+        }
+
+        // Get the carrier config for the current sub.
+        CarrierConfigManager configMgr = (CarrierConfigManager)
+                mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        // Bail if we can't find the carrier config service.
+        if (configMgr == null) {
+            return callSubject;
+        }
+
+        PersistableBundle carrierConfig = configMgr.getConfigForSubId(mPhone.getSubId());
+        // Bail if no carrier config found.
+        if (carrierConfig == null) {
+            return callSubject;
+        }
+
+        // Try to replace invalid characters
+        String invalidCharacters = carrierConfig.getString(
+                CarrierConfigManager.KEY_CARRIER_INSTANT_LETTERING_INVALID_CHARS_STRING);
+        if (!TextUtils.isEmpty(invalidCharacters)) {
+            callSubject = callSubject.replaceAll(invalidCharacters, "");
+        }
+
+        // Try to escape characters which need to be escaped.
+        String escapedCharacters = carrierConfig.getString(
+                CarrierConfigManager.KEY_CARRIER_INSTANT_LETTERING_ESCAPED_CHARS_STRING);
+        if (!TextUtils.isEmpty(escapedCharacters)) {
+            callSubject = escapeChars(escapedCharacters, callSubject);
+        }
+        return callSubject;
+    }
+
+    /**
+     * Given a source string, return a string where a set of characters are escaped using the
+     * backslash character.
+     *
+     * @param toEscape The characters to escape with a backslash.
+     * @param source The source string.
+     * @return The source string with characters escaped.
+     */
+    private String escapeChars(String toEscape, String source) {
+        StringBuilder escaped = new StringBuilder();
+        for (char c : source.toCharArray()) {
+            if (toEscape.contains(Character.toString(c))) {
+                escaped.append("\\");
+            }
+            escaped.append(c);
+        }
+
+        return escaped.toString();
+    }
+
+    /**
+     * Initiates a pull of an external call.
+     *
+     * Initiates a pull by making a dial request with the {@link ImsCallProfile#EXTRA_IS_CALL_PULL}
+     * extra specified.  We call {@link ImsPhone#notifyUnknownConnection(Connection)} which notifies
+     * Telecom of the new dialed connection.  The
+     * {@code PstnIncomingCallNotifier#maybeSwapWithUnknownConnection} logic ensures that the new
+     * {@link ImsPhoneConnection} resulting from the dial gets swapped with the
+     * {@link ImsExternalConnection}, which effectively makes the external call become a regular
+     * call.  Magic!
+     *
+     * @param number The phone number of the call to be pulled.
+     * @param videoState The desired video state of the pulled call.
+     * @param dialogId The {@link ImsExternalConnection#getCallId()} dialog id associated with the
+     *                 call which is being pulled.
+     */
+    @Override
+    public void pullExternalCall(String number, int videoState, int dialogId) {
+        Bundle extras = new Bundle();
+        extras.putBoolean(ImsCallProfile.EXTRA_IS_CALL_PULL, true);
+        extras.putInt(ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID, dialogId);
+        try {
+            Connection connection = dial(number, videoState, extras);
+            mPhone.notifyUnknownConnection(connection);
+        } catch (CallStateException e) {
+            loge("pullExternalCall failed - " + e);
+        }
+    }
+
+    private ImsException getImsManagerIsNullException() {
+        return new ImsException("no ims manager", ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
+    }
+
+    /**
+     * Determines if answering an incoming call will cause the active call to be disconnected.
+     * <p>
+     * This will be the case if
+     * {@link CarrierConfigManager#KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL} is
+     * {@code true} for the carrier, the active call is a video call over WIFI, and the incoming
+     * call is an audio call.
+     *
+     * @param activeCall The active call.
+     * @param incomingCall The incoming call.
+     * @return {@code true} if answering the incoming call will cause the active call to be
+     *      disconnected, {@code false} otherwise.
+     */
+    private boolean shouldDisconnectActiveCallOnAnswer(ImsCall activeCall,
+            ImsCall incomingCall) {
+
+        if (activeCall == null || incomingCall == null) {
+            return false;
+        }
+
+        if (!mDropVideoCallWhenAnsweringAudioCall) {
+            return false;
+        }
+
+        boolean isActiveCallVideo = activeCall.isVideoCall() ||
+                (mTreatDowngradedVideoCallsAsVideoCalls && activeCall.wasVideoCall());
+        boolean isActiveCallOnWifi = activeCall.isWifiCall();
+        boolean isVoWifiEnabled = mImsManager.isWfcEnabledByPlatform(mPhone.getContext()) &&
+                mImsManager.isWfcEnabledByUser(mPhone.getContext());
+        boolean isIncomingCallAudio = !incomingCall.isVideoCall();
+        log("shouldDisconnectActiveCallOnAnswer : isActiveCallVideo=" + isActiveCallVideo +
+                " isActiveCallOnWifi=" + isActiveCallOnWifi + " isIncomingCallAudio=" +
+                isIncomingCallAudio + " isVowifiEnabled=" + isVoWifiEnabled);
+
+        return isActiveCallVideo && isActiveCallOnWifi && isIncomingCallAudio && !isVoWifiEnabled;
+    }
+
+    /**
+     * Get aggregated video call data usage since boot.
+     *
+     * @param perUidStats True if requesting data usage per uid, otherwise overall usage.
+     * @return Snapshot of video call data usage
+     */
+    public NetworkStats getVtDataUsage(boolean perUidStats) {
+
+        // If there is an ongoing VT call, request the latest VT usage from the modem. The latest
+        // usage will return asynchronously so it won't be counted in this round, but it will be
+        // eventually counted when next getVtDataUsage is called.
+        if (mState != PhoneConstants.State.IDLE) {
+            for (ImsPhoneConnection conn : mConnections) {
+                android.telecom.Connection.VideoProvider videoProvider = conn.getVideoProvider();
+                if (videoProvider != null) {
+                    videoProvider.onRequestConnectionDataUsage();
+                }
+            }
+        }
+
+        return perUidStats ? mVtDataUsageUidSnapshot : mVtDataUsageSnapshot;
+    }
+
+    public void registerPhoneStateListener(PhoneStateListener listener) {
+        mPhoneStateListeners.add(listener);
+    }
+
+    public void unregisterPhoneStateListener(PhoneStateListener listener) {
+        mPhoneStateListeners.remove(listener);
+    }
+
+    /**
+     * Notifies local telephony listeners of changes to the IMS phone state.
+     *
+     * @param oldState The old state.
+     * @param newState The new state.
+     */
+    private void notifyPhoneStateChanged(PhoneConstants.State oldState,
+            PhoneConstants.State newState) {
+
+        for (PhoneStateListener listener : mPhoneStateListeners) {
+            listener.onPhoneStateChanged(oldState, newState);
+        }
+    }
+
+    /** Modify video call to a new video state.
+     *
+     * @param imsCall IMS call to be modified
+     * @param newVideoState New video state. (Refer to VideoProfile)
+     */
+    private void modifyVideoCall(ImsCall imsCall, int newVideoState) {
+        ImsPhoneConnection conn = findConnection(imsCall);
+        if (conn != null) {
+            int oldVideoState = conn.getVideoState();
+            if (conn.getVideoProvider() != null) {
+                conn.getVideoProvider().onSendSessionModifyRequest(
+                        new VideoProfile(oldVideoState), new VideoProfile(newVideoState));
+            }
+        }
+    }
+
+    /**
+     * Handler of data enabled changed event
+     * @param enabled True if data is enabled, otherwise disabled.
+     * @param reason Reason for data enabled/disabled (see {@code REASON_*} in
+     *      {@link DataEnabledSettings}.
+     */
+    private void onDataEnabledChanged(boolean enabled, int reason) {
+
+        log("onDataEnabledChanged: enabled=" + enabled + ", reason=" + reason);
+
+        ImsManager.getInstance(mPhone.getContext(), mPhone.getPhoneId()).setDataEnabled(enabled);
+        mIsDataEnabled = enabled;
+
+        if (!mIsViLteDataMetered) {
+            log("Ignore data " + ((enabled) ? "enabled" : "disabled") + " - carrier policy "
+                    + "indicates that data is not metered for ViLTE calls.");
+            return;
+        }
+
+        // Inform connections that data has been disabled to ensure we turn off video capability
+        // if this is an LTE call.
+        for (ImsPhoneConnection conn : mConnections) {
+            conn.handleDataEnabledChange(enabled);
+        }
+
+        int reasonCode;
+        if (reason == DataEnabledSettings.REASON_POLICY_DATA_ENABLED) {
+            reasonCode = ImsReasonInfo.CODE_DATA_LIMIT_REACHED;
+        } else if (reason == DataEnabledSettings.REASON_USER_DATA_ENABLED) {
+            reasonCode = ImsReasonInfo.CODE_DATA_DISABLED;
+        } else {
+            // Unexpected code, default to data disabled.
+            reasonCode = ImsReasonInfo.CODE_DATA_DISABLED;
+        }
+
+        // Potentially send connection events so the InCall UI knows that video calls are being
+        // downgraded due to data being enabled/disabled.
+        maybeNotifyDataDisabled(enabled, reasonCode);
+        // Handle video state changes required as a result of data being enabled/disabled.
+        handleDataEnabledChange(enabled, reasonCode);
+
+        // We do not want to update the ImsConfig for REASON_REGISTERED, since it can happen before
+        // the carrier config has loaded and will deregister IMS.
+        if (!mShouldUpdateImsConfigOnDisconnect
+                && reason != DataEnabledSettings.REASON_REGISTERED) {
+            // This will call into updateVideoCallFeatureValue and eventually all clients will be
+            // asynchronously notified that the availability of VT over LTE has changed.
+            ImsManager.updateImsServiceConfig(mPhone.getContext(), mPhone.getPhoneId(), true);
+        }
+    }
+
+    private void maybeNotifyDataDisabled(boolean enabled, int reasonCode) {
+        if (!enabled) {
+            // If data is disabled while there are ongoing VT calls which are not taking place over
+            // wifi, then they should be disconnected to prevent the user from incurring further
+            // data charges.
+            for (ImsPhoneConnection conn : mConnections) {
+                ImsCall imsCall = conn.getImsCall();
+                if (imsCall != null && imsCall.isVideoCall() && !imsCall.isWifiCall()) {
+                    if (conn.hasCapabilities(
+                            Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL |
+                                    Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE)) {
+
+                        // If the carrier supports downgrading to voice, then we can simply issue a
+                        // downgrade to voice instead of terminating the call.
+                        if (reasonCode == ImsReasonInfo.CODE_DATA_DISABLED) {
+                            conn.onConnectionEvent(TelephonyManager.EVENT_DOWNGRADE_DATA_DISABLED,
+                                    null);
+                        } else if (reasonCode == ImsReasonInfo.CODE_DATA_LIMIT_REACHED) {
+                            conn.onConnectionEvent(
+                                    TelephonyManager.EVENT_DOWNGRADE_DATA_LIMIT_REACHED, null);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Handles changes to the enabled state of mobile data.
+     * When data is disabled, handles auto-downgrade of video calls over LTE.
+     * When data is enabled, handled resuming of video calls paused when data was disabled.
+     * @param enabled {@code true} if mobile data is enabled, {@code false} if mobile data is
+     *                            disabled.
+     * @param reasonCode The {@link ImsReasonInfo} code for the data enabled state change.
+     */
+    private void handleDataEnabledChange(boolean enabled, int reasonCode) {
+        if (!enabled) {
+            // If data is disabled while there are ongoing VT calls which are not taking place over
+            // wifi, then they should be disconnected to prevent the user from incurring further
+            // data charges.
+            for (ImsPhoneConnection conn : mConnections) {
+                ImsCall imsCall = conn.getImsCall();
+                if (imsCall != null && imsCall.isVideoCall() && !imsCall.isWifiCall()) {
+                    log("handleDataEnabledChange - downgrading " + conn);
+                    downgradeVideoCall(reasonCode, conn);
+                }
+            }
+        } else if (mSupportPauseVideo) {
+            // Data was re-enabled, so un-pause previously paused video calls.
+            for (ImsPhoneConnection conn : mConnections) {
+                // If video is paused, check to see if there are any pending pauses due to enabled
+                // state of data changing.
+                log("handleDataEnabledChange - resuming " + conn);
+                if (VideoProfile.isPaused(conn.getVideoState()) &&
+                        conn.wasVideoPausedFromSource(VideoPauseTracker.SOURCE_DATA_ENABLED)) {
+                    // The data enabled state was a cause of a pending pause, so potentially
+                    // resume the video now.
+                    conn.resumeVideo(VideoPauseTracker.SOURCE_DATA_ENABLED);
+                }
+            }
+            mShouldUpdateImsConfigOnDisconnect = false;
+        }
+    }
+
+    /**
+     * Handles downgrading a video call.  The behavior depends on carrier capabilities; we will
+     * attempt to take one of the following actions (in order of precedence):
+     * 1. If supported by the carrier, the call will be downgraded to an audio-only call.
+     * 2. If the carrier supports video pause signalling, the video will be paused.
+     * 3. The call will be disconnected.
+     * @param reasonCode The {@link ImsReasonInfo} reason code for the downgrade.
+     * @param conn The {@link ImsPhoneConnection} to downgrade.
+     */
+    private void downgradeVideoCall(int reasonCode, ImsPhoneConnection conn) {
+        ImsCall imsCall = conn.getImsCall();
+        if (imsCall != null) {
+            if (conn.hasCapabilities(
+                    Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL |
+                            Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE)) {
+
+                // If the carrier supports downgrading to voice, then we can simply issue a
+                // downgrade to voice instead of terminating the call.
+                modifyVideoCall(imsCall, VideoProfile.STATE_AUDIO_ONLY);
+            } else if (mSupportPauseVideo) {
+                // The carrier supports video pause signalling, so pause the video.
+                mShouldUpdateImsConfigOnDisconnect = true;
+                conn.pauseVideo(VideoPauseTracker.SOURCE_DATA_ENABLED);
+            } else {
+                // At this point the only choice we have is to terminate the call.
+                try {
+                    imsCall.terminate(ImsReasonInfo.CODE_USER_TERMINATED, reasonCode);
+                } catch (ImsException ie) {
+                    loge("Couldn't terminate call " + imsCall);
+                }
+            }
+        }
+    }
+
+    private void resetImsCapabilities() {
+        log("Resetting Capabilities...");
+        for (int i = 0; i < mImsFeatureEnabled.length; i++) {
+            mImsFeatureEnabled[i] = false;
+        }
+    }
+
+    /**
+     * @return {@code true} if the device is connected to a WIFI network, {@code false} otherwise.
+     */
+    private boolean isWifiConnected() {
+        ConnectivityManager cm = (ConnectivityManager) mPhone.getContext()
+                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (cm != null) {
+            NetworkInfo ni = cm.getActiveNetworkInfo();
+            if (ni != null && ni.isConnected()) {
+                return ni.getType() == ConnectivityManager.TYPE_WIFI;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return {@code true} if downgrading of a video call to audio is supported.
+     */
+    public boolean isCarrierDowngradeOfVtCallSupported() {
+        return mSupportDowngradeVtToAudio;
+    }
+
+    private void handleFeatureCapabilityChanged(int serviceClass,
+            int[] enabledFeatures, int[] disabledFeatures) {
+        if (serviceClass == ImsServiceClass.MMTEL) {
+            boolean tmpIsVideoCallEnabled = isVideoCallEnabled();
+            // Check enabledFeatures to determine capabilities. We ignore disabledFeatures.
+            StringBuilder sb;
+            if (DBG) {
+                sb = new StringBuilder(120);
+                sb.append("handleFeatureCapabilityChanged: ");
+            }
+            for (int  i = ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE;
+                    i <= ImsConfig.FeatureConstants.FEATURE_TYPE_UT_OVER_WIFI &&
+                            i < enabledFeatures.length; i++) {
+                if (enabledFeatures[i] == i) {
+                    // If the feature is set to its own integer value it is enabled.
+                    if (DBG) {
+                        sb.append(mImsFeatureStrings[i]);
+                        sb.append(":true ");
+                    }
+
+                    mImsFeatureEnabled[i] = true;
+                } else if (enabledFeatures[i]
+                        == ImsConfig.FeatureConstants.FEATURE_TYPE_UNKNOWN) {
+                    // FEATURE_TYPE_UNKNOWN indicates that a feature is disabled.
+                    if (DBG) {
+                        sb.append(mImsFeatureStrings[i]);
+                        sb.append(":false ");
+                    }
+
+                    mImsFeatureEnabled[i] = false;
+                } else {
+                    // Feature has unknown state; it is not its own value or -1.
+                    if (DBG) {
+                        loge("handleFeatureCapabilityChanged(" + i + ", " + mImsFeatureStrings[i]
+                                + "): unexpectedValue=" + enabledFeatures[i]);
+                    }
+                }
+            }
+            boolean isVideoEnabled = isVideoCallEnabled();
+            boolean isVideoEnabledStatechanged = tmpIsVideoCallEnabled != isVideoEnabled;
+            if (DBG) {
+                sb.append(" isVideoEnabledStateChanged=");
+                sb.append(isVideoEnabledStatechanged);
+            }
+
+            if (isVideoEnabledStatechanged) {
+                log("handleFeatureCapabilityChanged - notifyForVideoCapabilityChanged=" +
+                        isVideoEnabled);
+                mPhone.notifyForVideoCapabilityChanged(isVideoEnabled);
+            }
+
+            if (DBG) {
+                log(sb.toString());
+            }
+
+            if (DBG) log("handleFeatureCapabilityChanged: isVolteEnabled=" + isVolteEnabled()
+                    + ", isVideoCallEnabled=" + isVideoCallEnabled()
+                    + ", isVowifiEnabled=" + isVowifiEnabled()
+                    + ", isUtEnabled=" + isUtEnabled());
+
+            mPhone.onFeatureCapabilityChanged();
+
+            mMetrics.writeOnImsCapabilities(
+                    mPhone.getPhoneId(), mImsFeatureEnabled);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java b/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
new file mode 100644
index 0000000..60a50b9
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
@@ -0,0 +1,651 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+
+import com.android.internal.telephony.BaseCommands;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.RadioCapability;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.dataconnection.DataProfile;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+
+import java.util.List;
+
+/**
+ * Volte doesn't need CommandsInterface. The class does nothing but made to work
+ * with Phone's constructor.
+ */
+class ImsPhoneCommandInterface extends BaseCommands implements CommandsInterface {
+    ImsPhoneCommandInterface(Context context) {
+        super(context);
+    }
+
+    @Override public void setOnNITZTime(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void getIccCardStatus(Message result) {
+    }
+
+    @Override
+    public void supplyIccPin(String pin, Message result) {
+    }
+
+    @Override
+    public void supplyIccPuk(String puk, String newPin, Message result) {
+    }
+
+    @Override
+    public void supplyIccPin2(String pin, Message result) {
+    }
+
+    @Override
+    public void supplyIccPuk2(String puk, String newPin2, Message result) {
+    }
+
+    @Override
+    public void changeIccPin(String oldPin, String newPin, Message result) {
+    }
+
+    @Override
+    public void changeIccPin2(String oldPin2, String newPin2, Message result) {
+    }
+
+    @Override
+    public void changeBarringPassword(String facility, String oldPwd,
+            String newPwd, Message result) {
+    }
+
+    @Override
+    public void supplyNetworkDepersonalization(String netpin, Message result) {
+    }
+
+    @Override
+    public void getCurrentCalls(Message result) {
+    }
+
+    @Override
+    @Deprecated public void getPDPContextList(Message result) {
+    }
+
+    @Override
+    public void getDataCallList(Message result) {
+    }
+
+    @Override
+    public void dial(String address, int clirMode, Message result) {
+    }
+
+    @Override
+    public void dial(String address, int clirMode, UUSInfo uusInfo,
+            Message result) {
+    }
+
+    @Override
+    public void getIMSI(Message result) {
+    }
+
+    @Override
+    public void getIMSIForApp(String aid, Message result) {
+    }
+
+    @Override
+    public void getIMEI(Message result) {
+    }
+
+    @Override
+    public void getIMEISV(Message result) {
+    }
+
+    @Override
+    public void hangupConnection (int gsmIndex, Message result) {
+    }
+
+    @Override
+    public void hangupWaitingOrBackground (Message result) {
+    }
+
+    @Override
+    public void hangupForegroundResumeBackground (Message result) {
+    }
+
+    @Override
+    public void switchWaitingOrHoldingAndActive (Message result) {
+    }
+
+    @Override
+    public void conference (Message result) {
+    }
+
+    @Override
+    public void setPreferredVoicePrivacy(boolean enable, Message result) {
+    }
+
+    @Override
+    public void getPreferredVoicePrivacy(Message result) {
+    }
+
+    @Override
+    public void separateConnection (int gsmIndex, Message result) {
+    }
+
+    @Override
+    public void acceptCall (Message result) {
+    }
+
+    @Override
+    public void rejectCall (Message result) {
+    }
+
+    @Override
+    public void explicitCallTransfer (Message result) {
+    }
+
+    @Override
+    public void getLastCallFailCause (Message result) {
+    }
+
+    @Deprecated
+    @Override
+    public void getLastPdpFailCause (Message result) {
+    }
+
+    @Override
+    public void getLastDataCallFailCause (Message result) {
+    }
+
+    @Override
+    public void setMute (boolean enableMute, Message response) {
+    }
+
+    @Override
+    public void getMute (Message response) {
+    }
+
+    @Override
+    public void getSignalStrength (Message result) {
+    }
+
+    @Override
+    public void getVoiceRegistrationState (Message result) {
+    }
+
+    @Override
+    public void getDataRegistrationState (Message result) {
+    }
+
+    @Override
+    public void getOperator(Message result) {
+    }
+
+    @Override
+    public void sendDtmf(char c, Message result) {
+    }
+
+    @Override
+    public void startDtmf(char c, Message result) {
+    }
+
+    @Override
+    public void stopDtmf(Message result) {
+    }
+
+    @Override
+    public void sendBurstDtmf(String dtmfString, int on, int off,
+            Message result) {
+    }
+
+    @Override
+    public void sendSMS (String smscPDU, String pdu, Message result) {
+    }
+
+    @Override
+    public void sendSMSExpectMore (String smscPDU, String pdu, Message result) {
+    }
+
+    @Override
+    public void sendCdmaSms(byte[] pdu, Message result) {
+    }
+
+    @Override
+    public void sendImsGsmSms (String smscPDU, String pdu,
+            int retry, int messageRef, Message response) {
+    }
+
+    @Override
+    public void sendImsCdmaSms(byte[] pdu, int retry, int messageRef,
+            Message response) {
+    }
+
+    @Override
+    public void getImsRegistrationState (Message result) {
+    }
+
+    @Override
+    public void deleteSmsOnSim(int index, Message response) {
+    }
+
+    @Override
+    public void deleteSmsOnRuim(int index, Message response) {
+    }
+
+    @Override
+    public void writeSmsToSim(int status, String smsc, String pdu, Message response) {
+    }
+
+    @Override
+    public void writeSmsToRuim(int status, String pdu, Message response) {
+    }
+
+    @Override
+    public void setupDataCall(int radioTechnology, DataProfile dataProfile, boolean isRoaming,
+                              boolean allowRoaming, Message result) {
+    }
+
+    @Override
+    public void deactivateDataCall(int cid, int reason, Message result) {
+    }
+
+    @Override
+    public void setRadioPower(boolean on, Message result) {
+    }
+
+    @Override
+    public void setSuppServiceNotifications(boolean enable, Message result) {
+    }
+
+    @Override
+    public void acknowledgeLastIncomingGsmSms(boolean success, int cause,
+            Message result) {
+    }
+
+    @Override
+    public void acknowledgeLastIncomingCdmaSms(boolean success, int cause,
+            Message result) {
+    }
+
+    @Override
+    public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu,
+            Message result) {
+    }
+
+    @Override
+    public void iccIO (int command, int fileid, String path, int p1, int p2,
+            int p3, String data, String pin2, Message result) {
+    }
+    @Override
+    public void iccIOForApp (int command, int fileid, String path, int p1, int p2,
+            int p3, String data, String pin2, String aid, Message result) {
+    }
+
+    @Override
+    public void getCLIR(Message result) {
+    }
+
+    @Override
+    public void setCLIR(int clirMode, Message result) {
+    }
+
+    @Override
+    public void queryCallWaiting(int serviceClass, Message response) {
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, int serviceClass,
+            Message response) {
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message response) {
+    }
+
+    @Override
+    public void setNetworkSelectionModeManual(
+            String operatorNumeric, Message response) {
+    }
+
+    @Override
+    public void getNetworkSelectionMode(Message response) {
+    }
+
+    @Override
+    public void getAvailableNetworks(Message response) {
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message response) {
+    }
+
+    @Override
+    public void stopNetworkScan(Message response) {
+    }
+
+    @Override
+    public void setCallForward(int action, int cfReason, int serviceClass,
+                String number, int timeSeconds, Message response) {
+    }
+
+    @Override
+    public void queryCallForwardStatus(int cfReason, int serviceClass,
+            String number, Message response) {
+    }
+
+    @Override
+    public void queryCLIP(Message response) {
+    }
+
+    @Override
+    public void getBasebandVersion (Message response) {
+    }
+
+    @Override
+    public void queryFacilityLock(String facility, String password,
+            int serviceClass, Message response) {
+    }
+
+    @Override
+    public void queryFacilityLockForApp(String facility, String password,
+            int serviceClass, String appId, Message response) {
+    }
+
+    @Override
+    public void setFacilityLock(String facility, boolean lockState,
+            String password, int serviceClass, Message response) {
+    }
+
+    @Override
+    public void setFacilityLockForApp(String facility, boolean lockState,
+            String password, int serviceClass, String appId, Message response) {
+    }
+
+    @Override
+    public void sendUSSD (String ussdString, Message response) {
+    }
+
+    @Override
+    public void cancelPendingUssd (Message response) {
+    }
+
+    @Override
+    public void resetRadio(Message result) {
+    }
+
+    @Override
+    public void invokeOemRilRequestRaw(byte[] data, Message response) {
+    }
+
+    @Override
+    public void invokeOemRilRequestStrings(String[] strings, Message response) {
+    }
+
+    @Override
+    public void setBandMode (int bandMode, Message response) {
+    }
+
+    @Override
+    public void queryAvailableBandMode (Message response) {
+    }
+
+    @Override
+    public void sendTerminalResponse(String contents, Message response) {
+    }
+
+    @Override
+    public void sendEnvelope(String contents, Message response) {
+    }
+
+    @Override
+    public void sendEnvelopeWithStatus(String contents, Message response) {
+    }
+
+    @Override
+    public void handleCallSetupRequestFromSim(
+            boolean accept, Message response) {
+    }
+
+    @Override
+    public void setPreferredNetworkType(int networkType , Message response) {
+    }
+
+    @Override
+    public void getPreferredNetworkType(Message response) {
+    }
+
+    @Override
+    public void setLocationUpdates(boolean enable, Message response) {
+    }
+
+    @Override
+    public void getSmscAddress(Message result) {
+    }
+
+    @Override
+    public void setSmscAddress(String address, Message result) {
+    }
+
+    @Override
+    public void reportSmsMemoryStatus(boolean available, Message result) {
+    }
+
+    @Override
+    public void reportStkServiceIsRunning(Message result) {
+    }
+
+    @Override
+    public void getCdmaSubscriptionSource(Message response) {
+    }
+
+    @Override
+    public void getGsmBroadcastConfig(Message response) {
+    }
+
+    @Override
+    public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) {
+    }
+
+    @Override
+    public void setGsmBroadcastActivation(boolean activate, Message response) {
+    }
+
+    // ***** Methods for CDMA support
+    @Override
+    public void getDeviceIdentity(Message response) {
+    }
+
+    @Override
+    public void getCDMASubscription(Message response) {
+    }
+
+    @Override
+    public void setPhoneType(int phoneType) { //Set by CDMAPhone and GSMPhone constructor
+    }
+
+    @Override
+    public void queryCdmaRoamingPreference(Message response) {
+    }
+
+    @Override
+    public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) {
+    }
+
+    @Override
+    public void setCdmaSubscriptionSource(int cdmaSubscription , Message response) {
+    }
+
+    @Override
+    public void queryTTYMode(Message response) {
+    }
+
+    @Override
+    public void setTTYMode(int ttyMode, Message response) {
+    }
+
+    @Override
+    public void sendCDMAFeatureCode(String FeatureCode, Message response) {
+    }
+
+    @Override
+    public void getCdmaBroadcastConfig(Message response) {
+    }
+
+    @Override
+    public void setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs, Message response) {
+    }
+
+    @Override
+    public void setCdmaBroadcastActivation(boolean activate, Message response) {
+    }
+
+    @Override
+    public void exitEmergencyCallbackMode(Message response) {
+    }
+
+    @Override
+    public void supplyIccPinForApp(String pin, String aid, Message response) {
+    }
+
+    @Override
+    public void supplyIccPukForApp(String puk, String newPin, String aid, Message response) {
+    }
+
+    @Override
+    public void supplyIccPin2ForApp(String pin2, String aid, Message response) {
+    }
+
+    @Override
+    public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message response) {
+    }
+
+    @Override
+    public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message response) {
+    }
+
+    @Override
+    public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr,
+            Message response) {
+    }
+
+    @Override
+    public void requestIsimAuthentication(String nonce, Message response) {
+    }
+
+    @Override
+    public void requestIccSimAuthentication(int authContext, String data, String aid, Message response) {
+    }
+
+    @Override
+    public void getVoiceRadioTechnology(Message result) {
+    }
+
+    @Override
+    public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming, Message result) {
+    }
+
+    @Override
+    public void setDataProfile(DataProfile[] dps, boolean isRoaming, Message result) {
+    }
+
+    @Override
+    public void iccOpenLogicalChannel(String AID, int p2, Message response) {}
+
+    @Override
+    public void iccCloseLogicalChannel(int channel, Message response) {}
+
+    @Override
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction,
+                                              int p1, int p2, int p3, String data,
+                                              Message response) {}
+    @Override
+    public void iccTransmitApduBasicChannel(int cla, int instruction, int p1, int p2,
+                                            int p3, String data, Message response) {}
+
+    @Override
+    public void nvReadItem(int itemID, Message response) {}
+
+    @Override
+    public void nvWriteItem(int itemID, String itemValue, Message response) {}
+
+    @Override
+    public void nvWriteCdmaPrl(byte[] preferredRoamingList, Message response) {}
+
+    @Override
+    public void nvResetConfig(int resetType, Message response) {}
+
+    @Override
+    public void getHardwareConfig(Message result) {}
+
+    @Override
+    public void requestShutdown(Message result) {
+    }
+
+    @Override
+    public void setRadioCapability(RadioCapability rc, Message response) {
+    }
+
+    @Override
+    public void getRadioCapability(Message response) {
+    }
+
+    @Override
+    public void startLceService(int reportIntervalMs, boolean pullMode, Message result) {
+    }
+
+    @Override
+    public void stopLceService(Message result) {
+    }
+
+    @Override
+    public void pullLceData(Message result) {
+    }
+
+    @Override
+    public void getModemActivityInfo(Message result) {
+    }
+
+    @Override
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                Message result) {
+    }
+
+    @Override
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message result) {
+    }
+
+    @Override
+    public void getAllowedCarriers(Message result) {
+    }
+
+    @Override
+    public void sendDeviceState(int stateType, boolean state, Message result) {
+    }
+
+    @Override
+    public void setUnsolResponseFilter(int filter, Message result){
+    }
+
+    @Override
+    public void setSimCardPower(int state, Message result) {
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneConnection.java b/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
new file mode 100644
index 0000000..9800a44
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
@@ -0,0 +1,1255 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.Registrant;
+import android.os.SystemClock;
+import android.telecom.VideoProfile;
+import android.telephony.CarrierConfigManager;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+
+import com.android.ims.ImsCall;
+import com.android.ims.ImsCallProfile;
+import com.android.ims.ImsException;
+import com.android.ims.ImsStreamMediaProfile;
+import com.android.ims.internal.ImsVideoCallProviderWrapper;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.UUSInfo;
+
+import java.util.Objects;
+
+/**
+ * {@hide}
+ */
+public class ImsPhoneConnection extends Connection implements
+        ImsVideoCallProviderWrapper.ImsVideoProviderWrapperCallback {
+
+    private static final String LOG_TAG = "ImsPhoneConnection";
+    private static final boolean DBG = true;
+
+    //***** Instance Variables
+
+    private ImsPhoneCallTracker mOwner;
+    private ImsPhoneCall mParent;
+    private ImsCall mImsCall;
+    private Bundle mExtras = new Bundle();
+
+    private boolean mDisconnected;
+
+    /*
+    int mIndex;          // index in ImsPhoneCallTracker.connections[], -1 if unassigned
+                        // The GSM index is 1 + this
+    */
+
+    /*
+     * These time/timespan values are based on System.currentTimeMillis(),
+     * i.e., "wall clock" time.
+     */
+    private long mDisconnectTime;
+
+    private UUSInfo mUusInfo;
+    private Handler mHandler;
+
+    private PowerManager.WakeLock mPartialWakeLock;
+
+    // The cached connect time of the connection when it turns into a conference.
+    private long mConferenceConnectTime = 0;
+
+    // The cached delay to be used between DTMF tones fetched from carrier config.
+    private int mDtmfToneDelay = 0;
+
+    private boolean mIsEmergency = false;
+
+    /**
+     * Used to indicate that video state changes detected by
+     * {@link #updateMediaCapabilities(ImsCall)} should be ignored.  When a video state change from
+     * unpaused to paused occurs, we set this flag and then update the existing video state when
+     * new {@link #onReceiveSessionModifyResponse(int, VideoProfile, VideoProfile)} callbacks come
+     * in.  When the video un-pauses we continue receiving the video state updates.
+     */
+    private boolean mShouldIgnoreVideoStateChanges = false;
+
+    private ImsVideoCallProviderWrapper mImsVideoCallProviderWrapper;
+
+    private int mPreciseDisconnectCause = 0;
+
+    private ImsRttTextHandler mRttTextHandler;
+    private android.telecom.Connection.RttTextStream mRttTextStream;
+
+    /**
+     * Used to indicate that this call is in the midst of being merged into a conference.
+     */
+    private boolean mIsMergeInProcess = false;
+
+    /**
+     * Used as an override to determine whether video is locally available for this call.
+     * This allows video availability to be overridden in the case that the modem says video is
+     * currently available, but mobile data is off and the carrier is metering data for video
+     * calls.
+     */
+    private boolean mIsVideoEnabled = true;
+
+    //***** Event Constants
+    private static final int EVENT_DTMF_DONE = 1;
+    private static final int EVENT_PAUSE_DONE = 2;
+    private static final int EVENT_NEXT_POST_DIAL = 3;
+    private static final int EVENT_WAKE_LOCK_TIMEOUT = 4;
+    private static final int EVENT_DTMF_DELAY_DONE = 5;
+
+    //***** Constants
+    private static final int PAUSE_DELAY_MILLIS = 3 * 1000;
+    private static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000;
+
+    //***** Inner Classes
+
+    class MyHandler extends Handler {
+        MyHandler(Looper l) {super(l);}
+
+        @Override
+        public void
+        handleMessage(Message msg) {
+
+            switch (msg.what) {
+                case EVENT_NEXT_POST_DIAL:
+                case EVENT_DTMF_DELAY_DONE:
+                case EVENT_PAUSE_DONE:
+                    processNextPostDialChar();
+                    break;
+                case EVENT_WAKE_LOCK_TIMEOUT:
+                    releaseWakeLock();
+                    break;
+                case EVENT_DTMF_DONE:
+                    // We may need to add a delay specified by carrier between DTMF tones that are
+                    // sent out.
+                    mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_DTMF_DELAY_DONE),
+                            mDtmfToneDelay);
+                    break;
+            }
+        }
+    }
+
+    //***** Constructors
+
+    /** This is probably an MT call */
+    public ImsPhoneConnection(Phone phone, ImsCall imsCall, ImsPhoneCallTracker ct,
+           ImsPhoneCall parent, boolean isUnknown) {
+        super(PhoneConstants.PHONE_TYPE_IMS);
+        createWakeLock(phone.getContext());
+        acquireWakeLock();
+
+        mOwner = ct;
+        mHandler = new MyHandler(mOwner.getLooper());
+        mImsCall = imsCall;
+
+        if ((imsCall != null) && (imsCall.getCallProfile() != null)) {
+            mAddress = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_OI);
+            mCnapName = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_CNA);
+            mNumberPresentation = ImsCallProfile.OIRToPresentation(
+                    imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_OIR));
+            mCnapNamePresentation = ImsCallProfile.OIRToPresentation(
+                    imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_CNAP));
+            updateMediaCapabilities(imsCall);
+        } else {
+            mNumberPresentation = PhoneConstants.PRESENTATION_UNKNOWN;
+            mCnapNamePresentation = PhoneConstants.PRESENTATION_UNKNOWN;
+        }
+
+        mIsIncoming = !isUnknown;
+        mCreateTime = System.currentTimeMillis();
+        mUusInfo = null;
+
+        // Ensure any extras set on the ImsCallProfile at the start of the call are cached locally
+        // in the ImsPhoneConnection.  This isn't going to inform any listeners (since the original
+        // connection is not likely to be associated with a TelephonyConnection yet).
+        updateExtras(imsCall);
+
+        mParent = parent;
+        mParent.attach(this,
+                (mIsIncoming? ImsPhoneCall.State.INCOMING: ImsPhoneCall.State.DIALING));
+
+        fetchDtmfToneDelay(phone);
+
+        if (phone.getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_use_voip_mode_for_ims)) {
+            setAudioModeIsVoip(true);
+        }
+    }
+
+    /** This is an MO call, created when dialing */
+    public ImsPhoneConnection(Phone phone, String dialString, ImsPhoneCallTracker ct,
+            ImsPhoneCall parent, boolean isEmergency) {
+        super(PhoneConstants.PHONE_TYPE_IMS);
+        createWakeLock(phone.getContext());
+        acquireWakeLock();
+
+        mOwner = ct;
+        mHandler = new MyHandler(mOwner.getLooper());
+
+        mDialString = dialString;
+
+        mAddress = PhoneNumberUtils.extractNetworkPortionAlt(dialString);
+        mPostDialString = PhoneNumberUtils.extractPostDialPortion(dialString);
+
+        //mIndex = -1;
+
+        mIsIncoming = false;
+        mCnapName = null;
+        mCnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED;
+        mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
+        mCreateTime = System.currentTimeMillis();
+
+        mParent = parent;
+        parent.attachFake(this, ImsPhoneCall.State.DIALING);
+
+        mIsEmergency = isEmergency;
+
+        fetchDtmfToneDelay(phone);
+
+        if (phone.getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_use_voip_mode_for_ims)) {
+            setAudioModeIsVoip(true);
+        }
+    }
+
+    public void dispose() {
+    }
+
+    static boolean
+    equalsHandlesNulls (Object a, Object b) {
+        return (a == null) ? (b == null) : a.equals (b);
+    }
+
+    static boolean
+    equalsBaseDialString (String a, String b) {
+        return (a == null) ? (b == null) : (b != null && a.startsWith (b));
+    }
+
+    private int applyLocalCallCapabilities(ImsCallProfile localProfile, int capabilities) {
+        Rlog.i(LOG_TAG, "applyLocalCallCapabilities - localProfile = " + localProfile);
+        capabilities = removeCapability(capabilities,
+                Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
+
+        if (!mIsVideoEnabled) {
+            Rlog.i(LOG_TAG, "applyLocalCallCapabilities - disabling video (overidden)");
+            return capabilities;
+        }
+        switch (localProfile.mCallType) {
+            case ImsCallProfile.CALL_TYPE_VT:
+                // Fall-through
+            case ImsCallProfile.CALL_TYPE_VIDEO_N_VOICE:
+                capabilities = addCapability(capabilities,
+                        Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
+                break;
+        }
+        return capabilities;
+    }
+
+    private static int applyRemoteCallCapabilities(ImsCallProfile remoteProfile, int capabilities) {
+        Rlog.w(LOG_TAG, "applyRemoteCallCapabilities - remoteProfile = "+remoteProfile);
+        capabilities = removeCapability(capabilities,
+                Connection.Capability.SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
+
+        switch (remoteProfile.mCallType) {
+            case ImsCallProfile.CALL_TYPE_VT:
+                // fall-through
+            case ImsCallProfile.CALL_TYPE_VIDEO_N_VOICE:
+                capabilities = addCapability(capabilities,
+                        Connection.Capability.SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
+                break;
+        }
+        return capabilities;
+    }
+
+    @Override
+    public String getOrigDialString(){
+        return mDialString;
+    }
+
+    @Override
+    public ImsPhoneCall getCall() {
+        return mParent;
+    }
+
+    @Override
+    public long getDisconnectTime() {
+        return mDisconnectTime;
+    }
+
+    @Override
+    public long getHoldingStartTime() {
+        return mHoldingStartTime;
+    }
+
+    @Override
+    public long getHoldDurationMillis() {
+        if (getState() != ImsPhoneCall.State.HOLDING) {
+            // If not holding, return 0
+            return 0;
+        } else {
+            return SystemClock.elapsedRealtime() - mHoldingStartTime;
+        }
+    }
+
+    public void setDisconnectCause(int cause) {
+        mCause = cause;
+    }
+
+    @Override
+    public String getVendorDisconnectCause() {
+      return null;
+    }
+
+    public ImsPhoneCallTracker getOwner () {
+        return mOwner;
+    }
+
+    @Override
+    public ImsPhoneCall.State getState() {
+        if (mDisconnected) {
+            return ImsPhoneCall.State.DISCONNECTED;
+        } else {
+            return super.getState();
+        }
+    }
+
+    @Override
+    public void hangup() throws CallStateException {
+        if (!mDisconnected) {
+            mOwner.hangup(this);
+        } else {
+            throw new CallStateException ("disconnected");
+        }
+    }
+
+    @Override
+    public void separate() throws CallStateException {
+        throw new CallStateException ("not supported");
+    }
+
+    @Override
+    public void proceedAfterWaitChar() {
+        if (mPostDialState != PostDialState.WAIT) {
+            Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected "
+                    + "getPostDialState() to be WAIT but was " + mPostDialState);
+            return;
+        }
+
+        setPostDialState(PostDialState.STARTED);
+
+        processNextPostDialChar();
+    }
+
+    @Override
+    public void proceedAfterWildChar(String str) {
+        if (mPostDialState != PostDialState.WILD) {
+            Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected "
+                    + "getPostDialState() to be WILD but was " + mPostDialState);
+            return;
+        }
+
+        setPostDialState(PostDialState.STARTED);
+
+        // make a new postDialString, with the wild char replacement string
+        // at the beginning, followed by the remaining postDialString.
+
+        StringBuilder buf = new StringBuilder(str);
+        buf.append(mPostDialString.substring(mNextPostDialChar));
+        mPostDialString = buf.toString();
+        mNextPostDialChar = 0;
+        if (Phone.DEBUG_PHONE) {
+            Rlog.d(LOG_TAG, "proceedAfterWildChar: new postDialString is " +
+                    mPostDialString);
+        }
+
+        processNextPostDialChar();
+    }
+
+    @Override
+    public void cancelPostDial() {
+        setPostDialState(PostDialState.CANCELLED);
+    }
+
+    /**
+     * Called when this Connection is being hung up locally (eg, user pressed "end")
+     */
+    void
+    onHangupLocal() {
+        mCause = DisconnectCause.LOCAL;
+    }
+
+    /** Called when the connection has been disconnected */
+    @Override
+    public boolean onDisconnect(int cause) {
+        Rlog.d(LOG_TAG, "onDisconnect: cause=" + cause);
+        if (mCause != DisconnectCause.LOCAL || cause == DisconnectCause.INCOMING_REJECTED) {
+            mCause = cause;
+        }
+        return onDisconnect();
+    }
+
+    public boolean onDisconnect() {
+        boolean changed = false;
+
+        if (!mDisconnected) {
+            //mIndex = -1;
+
+            mDisconnectTime = System.currentTimeMillis();
+            mDuration = SystemClock.elapsedRealtime() - mConnectTimeReal;
+            mDisconnected = true;
+
+            mOwner.mPhone.notifyDisconnect(this);
+
+            if (mParent != null) {
+                changed = mParent.connectionDisconnected(this);
+            } else {
+                Rlog.d(LOG_TAG, "onDisconnect: no parent");
+            }
+            synchronized (this) {
+                if (mImsCall != null) mImsCall.close();
+                mImsCall = null;
+            }
+        }
+        releaseWakeLock();
+        return changed;
+    }
+
+    /**
+     * An incoming or outgoing call has connected
+     */
+    void
+    onConnectedInOrOut() {
+        mConnectTime = System.currentTimeMillis();
+        mConnectTimeReal = SystemClock.elapsedRealtime();
+        mDuration = 0;
+
+        if (Phone.DEBUG_PHONE) {
+            Rlog.d(LOG_TAG, "onConnectedInOrOut: connectTime=" + mConnectTime);
+        }
+
+        if (!mIsIncoming) {
+            // outgoing calls only
+            processNextPostDialChar();
+        }
+        releaseWakeLock();
+    }
+
+    /*package*/ void
+    onStartedHolding() {
+        mHoldingStartTime = SystemClock.elapsedRealtime();
+    }
+    /**
+     * Performs the appropriate action for a post-dial char, but does not
+     * notify application. returns false if the character is invalid and
+     * should be ignored
+     */
+    private boolean
+    processPostDialChar(char c) {
+        if (PhoneNumberUtils.is12Key(c)) {
+            mOwner.sendDtmf(c, mHandler.obtainMessage(EVENT_DTMF_DONE));
+        } else if (c == PhoneNumberUtils.PAUSE) {
+            // From TS 22.101:
+            // It continues...
+            // Upon the called party answering the UE shall send the DTMF digits
+            // automatically to the network after a delay of 3 seconds( 20 ).
+            // The digits shall be sent according to the procedures and timing
+            // specified in 3GPP TS 24.008 [13]. The first occurrence of the
+            // "DTMF Control Digits Separator" shall be used by the ME to
+            // distinguish between the addressing digits (i.e. the phone number)
+            // and the DTMF digits. Upon subsequent occurrences of the
+            // separator,
+            // the UE shall pause again for 3 seconds ( 20 ) before sending
+            // any further DTMF digits.
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_PAUSE_DONE),
+                    PAUSE_DELAY_MILLIS);
+        } else if (c == PhoneNumberUtils.WAIT) {
+            setPostDialState(PostDialState.WAIT);
+        } else if (c == PhoneNumberUtils.WILD) {
+            setPostDialState(PostDialState.WILD);
+        } else {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    protected void finalize() {
+        releaseWakeLock();
+    }
+
+    private void
+    processNextPostDialChar() {
+        char c = 0;
+        Registrant postDialHandler;
+
+        if (mPostDialState == PostDialState.CANCELLED) {
+            //Rlog.d(LOG_TAG, "##### processNextPostDialChar: postDialState == CANCELLED, bail");
+            return;
+        }
+
+        if (mPostDialString == null || mPostDialString.length() <= mNextPostDialChar) {
+            setPostDialState(PostDialState.COMPLETE);
+
+            // notifyMessage.arg1 is 0 on complete
+            c = 0;
+        } else {
+            boolean isValid;
+
+            setPostDialState(PostDialState.STARTED);
+
+            c = mPostDialString.charAt(mNextPostDialChar++);
+
+            isValid = processPostDialChar(c);
+
+            if (!isValid) {
+                // Will call processNextPostDialChar
+                mHandler.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget();
+                // Don't notify application
+                Rlog.e(LOG_TAG, "processNextPostDialChar: c=" + c + " isn't valid!");
+                return;
+            }
+        }
+
+        notifyPostDialListenersNextChar(c);
+
+        // TODO: remove the following code since the handler no longer executes anything.
+        postDialHandler = mOwner.mPhone.getPostDialHandler();
+
+        Message notifyMessage;
+
+        if (postDialHandler != null
+                && (notifyMessage = postDialHandler.messageForRegistrant()) != null) {
+            // The AsyncResult.result is the Connection object
+            PostDialState state = mPostDialState;
+            AsyncResult ar = AsyncResult.forMessage(notifyMessage);
+            ar.result = this;
+            ar.userObj = state;
+
+            // arg1 is the character that was/is being processed
+            notifyMessage.arg1 = c;
+
+            //Rlog.v(LOG_TAG,
+            //      "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c);
+            notifyMessage.sendToTarget();
+        }
+    }
+
+    /**
+     * Set post dial state and acquire wake lock while switching to "started"
+     * state, the wake lock will be released if state switches out of "started"
+     * state or after WAKE_LOCK_TIMEOUT_MILLIS.
+     * @param s new PostDialState
+     */
+    private void setPostDialState(PostDialState s) {
+        if (mPostDialState != PostDialState.STARTED
+                && s == PostDialState.STARTED) {
+            acquireWakeLock();
+            Message msg = mHandler.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT);
+            mHandler.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS);
+        } else if (mPostDialState == PostDialState.STARTED
+                && s != PostDialState.STARTED) {
+            mHandler.removeMessages(EVENT_WAKE_LOCK_TIMEOUT);
+            releaseWakeLock();
+        }
+        mPostDialState = s;
+        notifyPostDialListeners();
+    }
+
+    private void
+    createWakeLock(Context context) {
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+    }
+
+    private void
+    acquireWakeLock() {
+        Rlog.d(LOG_TAG, "acquireWakeLock");
+        mPartialWakeLock.acquire();
+    }
+
+    void
+    releaseWakeLock() {
+        if (mPartialWakeLock != null) {
+            synchronized (mPartialWakeLock) {
+                if (mPartialWakeLock.isHeld()) {
+                    Rlog.d(LOG_TAG, "releaseWakeLock");
+                    mPartialWakeLock.release();
+                }
+            }
+        }
+    }
+
+    private void fetchDtmfToneDelay(Phone phone) {
+        CarrierConfigManager configMgr = (CarrierConfigManager)
+                phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle b = configMgr.getConfigForSubId(phone.getSubId());
+        if (b != null) {
+            mDtmfToneDelay = b.getInt(CarrierConfigManager.KEY_IMS_DTMF_TONE_DELAY_INT);
+        }
+    }
+
+    @Override
+    public int getNumberPresentation() {
+        return mNumberPresentation;
+    }
+
+    @Override
+    public UUSInfo getUUSInfo() {
+        return mUusInfo;
+    }
+
+    @Override
+    public Connection getOrigConnection() {
+        return null;
+    }
+
+    @Override
+    public synchronized boolean isMultiparty() {
+        return mImsCall != null && mImsCall.isMultiparty();
+    }
+
+    /**
+     * Where {@link #isMultiparty()} is {@code true}, determines if this {@link ImsCall} is the
+     * origin of the conference call (i.e. {@code #isConferenceHost()} is {@code true}), or if this
+     * {@link ImsCall} is a member of a conference hosted on another device.
+     *
+     * @return {@code true} if this call is the origin of the conference call it is a member of,
+     *      {@code false} otherwise.
+     */
+    @Override
+    public synchronized boolean isConferenceHost() {
+        return mImsCall != null && mImsCall.isConferenceHost();
+    }
+
+    @Override
+    public boolean isMemberOfPeerConference() {
+        return !isConferenceHost();
+    }
+
+    public synchronized ImsCall getImsCall() {
+        return mImsCall;
+    }
+
+    public synchronized void setImsCall(ImsCall imsCall) {
+        mImsCall = imsCall;
+    }
+
+    public void changeParent(ImsPhoneCall parent) {
+        mParent = parent;
+    }
+
+    /**
+     * @return {@code true} if the {@link ImsPhoneConnection} or its media capabilities have been
+     *     changed, and {@code false} otherwise.
+     */
+    public boolean update(ImsCall imsCall, ImsPhoneCall.State state) {
+        if (state == ImsPhoneCall.State.ACTIVE) {
+            // If the state of the call is active, but there is a pending request to the RIL to hold
+            // the call, we will skip this update.  This is really a signalling delay or failure
+            // from the RIL, but we will prevent it from going through as we will end up erroneously
+            // making this call active when really it should be on hold.
+            if (imsCall.isPendingHold()) {
+                Rlog.w(LOG_TAG, "update : state is ACTIVE, but call is pending hold, skipping");
+                return false;
+            }
+
+            if (mParent.getState().isRinging() || mParent.getState().isDialing()) {
+                onConnectedInOrOut();
+            }
+
+            if (mParent.getState().isRinging() || mParent == mOwner.mBackgroundCall) {
+                //mForegroundCall should be IDLE
+                //when accepting WAITING call
+                //before accept WAITING call,
+                //the ACTIVE call should be held ahead
+                mParent.detach(this);
+                mParent = mOwner.mForegroundCall;
+                mParent.attach(this);
+            }
+        } else if (state == ImsPhoneCall.State.HOLDING) {
+            onStartedHolding();
+        }
+
+        boolean updateParent = mParent.update(this, imsCall, state);
+        boolean updateAddressDisplay = updateAddressDisplay(imsCall);
+        boolean updateMediaCapabilities = updateMediaCapabilities(imsCall);
+        boolean updateExtras = updateExtras(imsCall);
+
+        return updateParent || updateAddressDisplay || updateMediaCapabilities || updateExtras;
+    }
+
+    @Override
+    public int getPreciseDisconnectCause() {
+        return mPreciseDisconnectCause;
+    }
+
+    public void setPreciseDisconnectCause(int cause) {
+        mPreciseDisconnectCause = cause;
+    }
+
+    /**
+     * Notifies this Connection of a request to disconnect a participant of the conference managed
+     * by the connection.
+     *
+     * @param endpoint the {@link android.net.Uri} of the participant to disconnect.
+     */
+    @Override
+    public void onDisconnectConferenceParticipant(Uri endpoint) {
+        ImsCall imsCall = getImsCall();
+        if (imsCall == null) {
+            return;
+        }
+        try {
+            imsCall.removeParticipants(new String[]{endpoint.toString()});
+        } catch (ImsException e) {
+            // No session in place -- no change
+            Rlog.e(LOG_TAG, "onDisconnectConferenceParticipant: no session in place. "+
+                    "Failed to disconnect endpoint = " + endpoint);
+        }
+    }
+
+    /**
+     * Sets the conference connect time.  Used when an {@code ImsConference} is created to out of
+     * this phone connection.
+     *
+     * @param conferenceConnectTime The conference connect time.
+     */
+    public void setConferenceConnectTime(long conferenceConnectTime) {
+        mConferenceConnectTime = conferenceConnectTime;
+    }
+
+    /**
+     * @return The conference connect time.
+     */
+    public long getConferenceConnectTime() {
+        return mConferenceConnectTime;
+    }
+
+    /**
+     * Check for a change in the address display related fields for the {@link ImsCall}, and
+     * update the {@link ImsPhoneConnection} with this information.
+     *
+     * @param imsCall The call to check for changes in address display fields.
+     * @return Whether the address display fields have been changed.
+     */
+    public boolean updateAddressDisplay(ImsCall imsCall) {
+        if (imsCall == null) {
+            return false;
+        }
+
+        boolean changed = false;
+        ImsCallProfile callProfile = imsCall.getCallProfile();
+        if (callProfile != null && isIncoming()) {
+            // Only look for changes to the address for incoming calls.  The originating identity
+            // can change for outgoing calls due to, for example, a call being forwarded to
+            // voicemail.  This address change does not need to be presented to the user.
+            String address = callProfile.getCallExtra(ImsCallProfile.EXTRA_OI);
+            String name = callProfile.getCallExtra(ImsCallProfile.EXTRA_CNA);
+            int nump = ImsCallProfile.OIRToPresentation(
+                    callProfile.getCallExtraInt(ImsCallProfile.EXTRA_OIR));
+            int namep = ImsCallProfile.OIRToPresentation(
+                    callProfile.getCallExtraInt(ImsCallProfile.EXTRA_CNAP));
+            if (Phone.DEBUG_PHONE) {
+                Rlog.d(LOG_TAG, "updateAddressDisplay: callId = " + getTelecomCallId()
+                        + " address = " + Rlog.pii(LOG_TAG, address) + " name = " + name
+                        + " nump = " + nump + " namep = " + namep);
+            }
+            if (!mIsMergeInProcess) {
+                // Only process changes to the name and address when a merge is not in process.
+                // When call A initiated a merge with call B to form a conference C, there is a
+                // point in time when the ImsCall transfers the conference call session into A,
+                // at which point the ImsConferenceController creates the conference in Telecom.
+                // For some carriers C will have a unique conference URI address.  Swapping the
+                // conference session into A, which is about to be disconnected, to be logged to
+                // the call log using the conference address.  To prevent this we suppress updates
+                // to the call address while a merge is in process.
+                if (!equalsBaseDialString(mAddress, address)) {
+                    mAddress = address;
+                    changed = true;
+                }
+                if (TextUtils.isEmpty(name)) {
+                    if (!TextUtils.isEmpty(mCnapName)) {
+                        mCnapName = "";
+                        changed = true;
+                    }
+                } else if (!name.equals(mCnapName)) {
+                    mCnapName = name;
+                    changed = true;
+                }
+                if (mNumberPresentation != nump) {
+                    mNumberPresentation = nump;
+                    changed = true;
+                }
+                if (mCnapNamePresentation != namep) {
+                    mCnapNamePresentation = namep;
+                    changed = true;
+                }
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Check for a change in the video capabilities and audio quality for the {@link ImsCall}, and
+     * update the {@link ImsPhoneConnection} with this information.
+     *
+     * @param imsCall The call to check for changes in media capabilities.
+     * @return Whether the media capabilities have been changed.
+     */
+    public boolean updateMediaCapabilities(ImsCall imsCall) {
+        if (imsCall == null) {
+            return false;
+        }
+
+        boolean changed = false;
+
+        try {
+            // The actual call profile (negotiated between local and peer).
+            ImsCallProfile negotiatedCallProfile = imsCall.getCallProfile();
+
+            if (negotiatedCallProfile != null) {
+                int oldVideoState = getVideoState();
+                int newVideoState = ImsCallProfile
+                        .getVideoStateFromImsCallProfile(negotiatedCallProfile);
+
+                if (oldVideoState != newVideoState) {
+                    // The video state has changed.  See also code in onReceiveSessionModifyResponse
+                    // below.  When the video enters a paused state, subsequent changes to the video
+                    // state will not be reported by the modem.  In onReceiveSessionModifyResponse
+                    // we will be updating the current video state while paused to include any
+                    // changes the modem reports via the video provider.  When the video enters an
+                    // unpaused state, we will resume passing the video states from the modem as is.
+                    if (VideoProfile.isPaused(oldVideoState) &&
+                            !VideoProfile.isPaused(newVideoState)) {
+                        // Video entered un-paused state; recognize updates from now on; we want to
+                        // ensure that the new un-paused state is propagated to Telecom, so change
+                        // this now.
+                        mShouldIgnoreVideoStateChanges = false;
+                    }
+
+                    if (!mShouldIgnoreVideoStateChanges) {
+                        updateVideoState(newVideoState);
+                        changed = true;
+                    } else {
+                        Rlog.d(LOG_TAG, "updateMediaCapabilities - ignoring video state change " +
+                                "due to paused state.");
+                    }
+
+                    if (!VideoProfile.isPaused(oldVideoState) &&
+                            VideoProfile.isPaused(newVideoState)) {
+                        // Video entered pause state; ignore updates until un-paused.  We do this
+                        // after setVideoState is called above to ensure Telecom is notified that
+                        // the device has entered paused state.
+                        mShouldIgnoreVideoStateChanges = true;
+                    }
+                }
+            }
+
+            // Check for a change in the capabilities for the call and update
+            // {@link ImsPhoneConnection} with this information.
+            int capabilities = getConnectionCapabilities();
+
+            // Use carrier config to determine if downgrading directly to audio-only is supported.
+            if (mOwner.isCarrierDowngradeOfVtCallSupported()) {
+                capabilities = addCapability(capabilities,
+                        Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE |
+                                Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL);
+            } else {
+                capabilities = removeCapability(capabilities,
+                        Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE |
+                                Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL);
+            }
+
+            // Get the current local call capabilities which might be voice or video or both.
+            ImsCallProfile localCallProfile = imsCall.getLocalCallProfile();
+            Rlog.v(LOG_TAG, "update localCallProfile=" + localCallProfile);
+            if (localCallProfile != null) {
+                capabilities = applyLocalCallCapabilities(localCallProfile, capabilities);
+            }
+
+            // Get the current remote call capabilities which might be voice or video or both.
+            ImsCallProfile remoteCallProfile = imsCall.getRemoteCallProfile();
+            Rlog.v(LOG_TAG, "update remoteCallProfile=" + remoteCallProfile);
+            if (remoteCallProfile != null) {
+                capabilities = applyRemoteCallCapabilities(remoteCallProfile, capabilities);
+            }
+            if (getConnectionCapabilities() != capabilities) {
+                setConnectionCapabilities(capabilities);
+                changed = true;
+            }
+
+            int newAudioQuality =
+                    getAudioQualityFromCallProfile(localCallProfile, remoteCallProfile);
+            if (getAudioQuality() != newAudioQuality) {
+                setAudioQuality(newAudioQuality);
+                changed = true;
+            }
+        } catch (ImsException e) {
+            // No session in place -- no change
+        }
+
+        return changed;
+    }
+
+    private void updateVideoState(int newVideoState) {
+        if (mImsVideoCallProviderWrapper != null) {
+            mImsVideoCallProviderWrapper.onVideoStateChanged(newVideoState);
+        }
+        setVideoState(newVideoState);
+    }
+
+    public void sendRttModifyRequest(android.telecom.Connection.RttTextStream textStream) {
+        getImsCall().sendRttModifyRequest();
+        setCurrentRttTextStream(textStream);
+    }
+
+    /**
+     * Sends the user's response to a remotely-issued RTT upgrade request
+     *
+     * @param textStream A valid {@link android.telecom.Connection.RttTextStream} if the user
+     *                   accepts, {@code null} if not.
+     */
+    public void sendRttModifyResponse(android.telecom.Connection.RttTextStream textStream) {
+        boolean accept = textStream != null;
+        ImsCall imsCall = getImsCall();
+
+        imsCall.sendRttModifyResponse(accept);
+        if (accept) {
+            setCurrentRttTextStream(textStream);
+            startRttTextProcessing();
+        } else {
+            Rlog.e(LOG_TAG, "sendRttModifyResponse: foreground call has no connections");
+        }
+    }
+
+    public void onRttMessageReceived(String message) {
+        getOrCreateRttTextHandler().sendToInCall(message);
+    }
+
+    public void setCurrentRttTextStream(android.telecom.Connection.RttTextStream rttTextStream) {
+        mRttTextStream = rttTextStream;
+    }
+
+    public void startRttTextProcessing() {
+        getOrCreateRttTextHandler().initialize(mRttTextStream);
+    }
+
+    private ImsRttTextHandler getOrCreateRttTextHandler() {
+        if (mRttTextHandler != null) {
+            return mRttTextHandler;
+        }
+        mRttTextHandler = new ImsRttTextHandler(Looper.getMainLooper(),
+                (message) -> getImsCall().sendRttMessage(message));
+        return mRttTextHandler;
+    }
+
+    /**
+     * Updates the wifi state based on the {@link ImsCallProfile#EXTRA_CALL_RAT_TYPE}.
+     * The call is considered to be a WIFI call if the extra value is
+     * {@link ServiceState#RIL_RADIO_TECHNOLOGY_IWLAN}.
+     *
+     * @param extras The ImsCallProfile extras.
+     */
+    private void updateWifiStateFromExtras(Bundle extras) {
+        if (extras.containsKey(ImsCallProfile.EXTRA_CALL_RAT_TYPE) ||
+                extras.containsKey(ImsCallProfile.EXTRA_CALL_RAT_TYPE_ALT)) {
+
+            ImsCall call = getImsCall();
+            boolean isWifi = false;
+            if (call != null) {
+                isWifi = call.isWifiCall();
+            }
+
+            // Report any changes
+            if (isWifi() != isWifi) {
+                setWifi(isWifi);
+            }
+        }
+    }
+
+    /**
+     * Check for a change in call extras of {@link ImsCall}, and
+     * update the {@link ImsPhoneConnection} accordingly.
+     *
+     * @param imsCall The call to check for changes in extras.
+     * @return Whether the extras fields have been changed.
+     */
+     boolean updateExtras(ImsCall imsCall) {
+        if (imsCall == null) {
+            return false;
+        }
+
+        final ImsCallProfile callProfile = imsCall.getCallProfile();
+        final Bundle extras = callProfile != null ? callProfile.mCallExtras : null;
+        if (extras == null && DBG) {
+            Rlog.d(LOG_TAG, "Call profile extras are null.");
+        }
+
+        final boolean changed = !areBundlesEqual(extras, mExtras);
+        if (changed) {
+            updateWifiStateFromExtras(extras);
+
+            mExtras.clear();
+            mExtras.putAll(extras);
+            setConnectionExtras(mExtras);
+        }
+        return changed;
+    }
+
+    private static boolean areBundlesEqual(Bundle extras, Bundle newExtras) {
+        if (extras == null || newExtras == null) {
+            return extras == newExtras;
+        }
+
+        if (extras.size() != newExtras.size()) {
+            return false;
+        }
+
+        for(String key : extras.keySet()) {
+            if (key != null) {
+                final Object value = extras.get(key);
+                final Object newValue = newExtras.get(key);
+                if (!Objects.equals(value, newValue)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Determines the {@link ImsPhoneConnection} audio quality based on the local and remote
+     * {@link ImsCallProfile}. Indicate a HD audio call if the local stream profile
+     * is AMR_WB, EVRC_WB, EVS_WB, EVS_SWB, EVS_FB and
+     * there is no remote restrict cause.
+     *
+     * @param localCallProfile The local call profile.
+     * @param remoteCallProfile The remote call profile.
+     * @return The audio quality.
+     */
+    private int getAudioQualityFromCallProfile(
+            ImsCallProfile localCallProfile, ImsCallProfile remoteCallProfile) {
+        if (localCallProfile == null || remoteCallProfile == null
+                || localCallProfile.mMediaProfile == null) {
+            return AUDIO_QUALITY_STANDARD;
+        }
+
+        final boolean isEvsCodecHighDef = (localCallProfile.mMediaProfile.mAudioQuality
+                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB
+                || localCallProfile.mMediaProfile.mAudioQuality
+                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB
+                || localCallProfile.mMediaProfile.mAudioQuality
+                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB);
+
+        final boolean isHighDef = (localCallProfile.mMediaProfile.mAudioQuality
+                        == ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB
+                || localCallProfile.mMediaProfile.mAudioQuality
+                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB
+                || isEvsCodecHighDef)
+                && remoteCallProfile.mRestrictCause == ImsCallProfile.CALL_RESTRICT_CAUSE_NONE;
+        return isHighDef ? AUDIO_QUALITY_HIGH_DEFINITION : AUDIO_QUALITY_STANDARD;
+    }
+
+    /**
+     * Provides a string representation of the {@link ImsPhoneConnection}.  Primarily intended for
+     * use in log statements.
+     *
+     * @return String representation of call.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[ImsPhoneConnection objId: ");
+        sb.append(System.identityHashCode(this));
+        sb.append(" telecomCallID: ");
+        sb.append(getTelecomCallId());
+        sb.append(" address: ");
+        sb.append(Rlog.pii(LOG_TAG, getAddress()));
+        sb.append(" ImsCall: ");
+        synchronized (this) {
+            if (mImsCall == null) {
+                sb.append("null");
+            } else {
+                sb.append(mImsCall);
+            }
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    @Override
+    public void setVideoProvider(android.telecom.Connection.VideoProvider videoProvider) {
+        super.setVideoProvider(videoProvider);
+
+        if (videoProvider instanceof ImsVideoCallProviderWrapper) {
+            mImsVideoCallProviderWrapper = (ImsVideoCallProviderWrapper) videoProvider;
+        }
+    }
+
+    /**
+     * Indicates whether current phone connection is emergency or not
+     * @return boolean: true if emergency, false otherwise
+     */
+    protected boolean isEmergency() {
+        return mIsEmergency;
+    }
+
+    /**
+     * Handles notifications from the {@link ImsVideoCallProviderWrapper} of session modification
+     * responses received.
+     *
+     * @param status The status of the original request.
+     * @param requestProfile The requested video profile.
+     * @param responseProfile The response upon video profile.
+     */
+    @Override
+    public void onReceiveSessionModifyResponse(int status, VideoProfile requestProfile,
+            VideoProfile responseProfile) {
+        if (status == android.telecom.Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS &&
+                mShouldIgnoreVideoStateChanges) {
+            int currentVideoState = getVideoState();
+            int newVideoState = responseProfile.getVideoState();
+
+            // If the current video state is paused, the modem will not send us any changes to
+            // the TX and RX bits of the video state.  Until the video is un-paused we will
+            // "fake out" the video state by applying the changes that the modem reports via a
+            // response.
+
+            // First, find out whether there was a change to the TX or RX bits:
+            int changedBits = currentVideoState ^ newVideoState;
+            changedBits &= VideoProfile.STATE_BIDIRECTIONAL;
+            if (changedBits == 0) {
+                // No applicable change, bail out.
+                return;
+            }
+
+            // Turn off any existing bits that changed.
+            currentVideoState &= ~(changedBits & currentVideoState);
+            // Turn on any new bits that turned on.
+            currentVideoState |= changedBits & newVideoState;
+
+            Rlog.d(LOG_TAG, "onReceiveSessionModifyResponse : received " +
+                    VideoProfile.videoStateToString(requestProfile.getVideoState()) +
+                    " / " +
+                    VideoProfile.videoStateToString(responseProfile.getVideoState()) +
+                    " while paused ; sending new videoState = " +
+                    VideoProfile.videoStateToString(currentVideoState));
+            setVideoState(currentVideoState);
+        }
+    }
+
+    /**
+     * Issues a request to pause the video using {@link VideoProfile#STATE_PAUSED} from a source
+     * other than the InCall UI.
+     *
+     * @param source The source of the pause request.
+     */
+    public void pauseVideo(int source) {
+        if (mImsVideoCallProviderWrapper == null) {
+            return;
+        }
+
+        mImsVideoCallProviderWrapper.pauseVideo(getVideoState(), source);
+    }
+
+    /**
+     * Issues a request to resume the video using {@link VideoProfile#STATE_PAUSED} from a source
+     * other than the InCall UI.
+     *
+     * @param source The source of the resume request.
+     */
+    public void resumeVideo(int source) {
+        if (mImsVideoCallProviderWrapper == null) {
+            return;
+        }
+
+        mImsVideoCallProviderWrapper.resumeVideo(getVideoState(), source);
+    }
+
+    /**
+     * Determines if a specified source has issued a pause request.
+     *
+     * @param source The source.
+     * @return {@code true} if the source issued a pause request, {@code false} otherwise.
+     */
+    public boolean wasVideoPausedFromSource(int source) {
+        if (mImsVideoCallProviderWrapper == null) {
+            return false;
+        }
+
+        return mImsVideoCallProviderWrapper.wasVideoPausedFromSource(source);
+    }
+
+    /**
+     * Mark the call as in the process of being merged and inform the UI of the merge start.
+     */
+    public void handleMergeStart() {
+        mIsMergeInProcess = true;
+        onConnectionEvent(android.telecom.Connection.EVENT_MERGE_START, null);
+    }
+
+    /**
+     * Mark the call as done merging and inform the UI of the merge start.
+     */
+    public void handleMergeComplete() {
+        mIsMergeInProcess = false;
+        onConnectionEvent(android.telecom.Connection.EVENT_MERGE_COMPLETE, null);
+    }
+
+    public void changeToPausedState() {
+        int newVideoState = getVideoState() | VideoProfile.STATE_PAUSED;
+        Rlog.i(LOG_TAG, "ImsPhoneConnection: changeToPausedState - setting paused bit; "
+                + "newVideoState=" + VideoProfile.videoStateToString(newVideoState));
+        updateVideoState(newVideoState);
+        mShouldIgnoreVideoStateChanges = true;
+    }
+
+    public void changeToUnPausedState() {
+        int newVideoState = getVideoState() & ~VideoProfile.STATE_PAUSED;
+        Rlog.i(LOG_TAG, "ImsPhoneConnection: changeToUnPausedState - unsetting paused bit; "
+                + "newVideoState=" + VideoProfile.videoStateToString(newVideoState));
+        updateVideoState(newVideoState);
+        mShouldIgnoreVideoStateChanges = false;
+    }
+
+    public void handleDataEnabledChange(boolean isDataEnabled) {
+        mIsVideoEnabled = isDataEnabled;
+        Rlog.i(LOG_TAG, "handleDataEnabledChange: isDataEnabled=" + isDataEnabled
+                + "; updating local video availability.");
+        updateMediaCapabilities(getImsCall());
+        if (mImsVideoCallProviderWrapper != null) {
+            mImsVideoCallProviderWrapper.setIsVideoEnabled(
+                    hasCapabilities(Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL));
+        }
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneFactory.java b/com/android/internal/telephony/imsphone/ImsPhoneFactory.java
new file mode 100644
index 0000000..9f81a69
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneNotifier;
+
+import android.content.Context;
+import android.telephony.Rlog;
+
+/**
+ * {@hide}
+ */
+public class ImsPhoneFactory {
+
+    /**
+     * Makes a {@link ImsPhone} object.
+     * @param context {@code Context} needed to create a Phone object
+     * @param phoneNotifier {@code PhoneNotifier} needed to create a Phone
+     *      object
+     * @return the {@code ImsPhone} object
+     */
+    public static ImsPhone makePhone(Context context,
+            PhoneNotifier phoneNotifier, Phone defaultPhone) {
+
+        try {
+            return new ImsPhone(context, phoneNotifier, defaultPhone);
+        } catch (Exception e) {
+            Rlog.e("VoltePhoneFactory", "makePhone", e);
+            return null;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
new file mode 100644
index 0000000..4e3957e
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
@@ -0,0 +1,1709 @@
+/*
+ * 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.internal.telephony.imsphone;
+
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_DATA;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_DATA_ASYNC;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_DATA_SYNC;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_FAX;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_MAX;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_NONE;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_PACKET;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_PAD;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_SMS;
+import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_VOICE;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+
+import com.android.ims.ImsException;
+import com.android.ims.ImsSsInfo;
+import com.android.ims.ImsUtInterface;
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.uicc.IccRecords;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The motto for this file is:
+ *
+ * "NOTE:    By using the # as a separator, most cases are expected to be unambiguous."
+ *   -- TS 22.030 6.5.2
+ *
+ * {@hide}
+ *
+ */
+public final class ImsPhoneMmiCode extends Handler implements MmiCode {
+    static final String LOG_TAG = "ImsPhoneMmiCode";
+
+    //***** Constants
+
+    // Max Size of the Short Code (aka Short String from TS 22.030 6.5.2)
+    private static final int MAX_LENGTH_SHORT_CODE = 2;
+
+    // TS 22.030 6.5.2 Every Short String USSD command will end with #-key
+    // (known as #-String)
+    private static final char END_OF_USSD_COMMAND = '#';
+
+    // From TS 22.030 6.5.2
+    private static final String ACTION_ACTIVATE = "*";
+    private static final String ACTION_DEACTIVATE = "#";
+    private static final String ACTION_INTERROGATE = "*#";
+    private static final String ACTION_REGISTER = "**";
+    private static final String ACTION_ERASURE = "##";
+
+    // Supp Service codes from TS 22.030 Annex B
+
+    //Called line presentation
+    private static final String SC_CLIP    = "30";
+    private static final String SC_CLIR    = "31";
+    private static final String SC_COLP    = "76";
+    private static final String SC_COLR    = "77";
+
+    //Calling name presentation
+    private static final String SC_CNAP    = "300";
+
+    // Call Forwarding
+    private static final String SC_CFU     = "21";
+    private static final String SC_CFB     = "67";
+    private static final String SC_CFNRy   = "61";
+    private static final String SC_CFNR    = "62";
+    // Call Forwarding unconditional Timer
+    private static final String SC_CFUT     = "22";
+
+    private static final String SC_CF_All = "002";
+    private static final String SC_CF_All_Conditional = "004";
+
+    // Call Waiting
+    private static final String SC_WAIT     = "43";
+
+    // Call Barring
+    private static final String SC_BAOC         = "33";
+    private static final String SC_BAOIC        = "331";
+    private static final String SC_BAOICxH      = "332";
+    private static final String SC_BAIC         = "35";
+    private static final String SC_BAICr        = "351";
+
+    private static final String SC_BA_ALL       = "330";
+    private static final String SC_BA_MO        = "333";
+    private static final String SC_BA_MT        = "353";
+
+    // Incoming/Anonymous call barring
+    private static final String SC_BS_MT        = "156";
+    private static final String SC_BAICa        = "157";
+
+    // Supp Service Password registration
+    private static final String SC_PWD          = "03";
+
+    // PIN/PIN2/PUK/PUK2
+    private static final String SC_PIN          = "04";
+    private static final String SC_PIN2         = "042";
+    private static final String SC_PUK          = "05";
+    private static final String SC_PUK2         = "052";
+
+    //***** Event Constants
+
+    private static final int EVENT_SET_COMPLETE            = 0;
+    private static final int EVENT_QUERY_CF_COMPLETE       = 1;
+    private static final int EVENT_USSD_COMPLETE           = 2;
+    private static final int EVENT_QUERY_COMPLETE          = 3;
+    private static final int EVENT_SET_CFF_COMPLETE        = 4;
+    private static final int EVENT_USSD_CANCEL_COMPLETE    = 5;
+    private static final int EVENT_GET_CLIR_COMPLETE       = 6;
+    private static final int EVENT_SUPP_SVC_QUERY_COMPLETE = 7;
+    private static final int EVENT_QUERY_ICB_COMPLETE      = 10;
+
+    //***** Calling Line Presentation Constants
+    private static final int NUM_PRESENTATION_ALLOWED     = 0;
+    private static final int NUM_PRESENTATION_RESTRICTED  = 1;
+
+    //***** Supplementary Service Query Bundle Keys
+    // Used by IMS Service layer to put supp. serv. query
+    // responses into the ssInfo Bundle.
+    public static final String UT_BUNDLE_KEY_CLIR = "queryClir";
+    public static final String UT_BUNDLE_KEY_SSINFO = "imsSsInfo";
+
+    //***** Calling Line Identity Restriction Constants
+    // The 'm' parameter from TS 27.007 7.7
+    private static final int CLIR_NOT_PROVISIONED                    = 0;
+    private static final int CLIR_PROVISIONED_PERMANENT              = 1;
+    private static final int CLIR_PRESENTATION_RESTRICTED_TEMPORARY  = 3;
+    private static final int CLIR_PRESENTATION_ALLOWED_TEMPORARY     = 4;
+    // The 'n' parameter from TS 27.007 7.7
+    private static final int CLIR_DEFAULT     = 0;
+    private static final int CLIR_INVOCATION  = 1;
+    private static final int CLIR_SUPPRESSION = 2;
+
+    //***** Instance Variables
+
+    private ImsPhone mPhone;
+    private Context mContext;
+    private IccRecords mIccRecords;
+
+    private String mAction;              // One of ACTION_*
+    private String mSc;                  // Service Code
+    private String mSia, mSib, mSic;       // Service Info a,b,c
+    private String mPoundString;         // Entire MMI string up to and including #
+    private String mDialingNumber;
+    private String mPwd;                 // For password registration
+    private ResultReceiver mCallbackReceiver;
+
+    private boolean mIsPendingUSSD;
+
+    private boolean mIsUssdRequest;
+
+    private boolean mIsCallFwdReg;
+    private State mState = State.PENDING;
+    private CharSequence mMessage;
+    //resgister/erasure of ICB (Specific DN)
+    static final String IcbDnMmi = "Specific Incoming Call Barring";
+    //ICB (Anonymous)
+    static final String IcbAnonymousMmi = "Anonymous Incoming Call Barring";
+    //***** Class Variables
+
+
+    // See TS 22.030 6.5.2 "Structure of the MMI"
+
+    private static Pattern sPatternSuppService = Pattern.compile(
+        "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)");
+/*       1  2                    3          4  5       6   7         8    9     10  11             12
+
+         1 = Full string up to and including #
+         2 = action (activation/interrogation/registration/erasure)
+         3 = service code
+         5 = SIA
+         7 = SIB
+         9 = SIC
+         10 = dialing number
+*/
+
+    private static final int MATCH_GROUP_POUND_STRING = 1;
+
+    private static final int MATCH_GROUP_ACTION = 2;
+                        //(activation/interrogation/registration/erasure)
+
+    private static final int MATCH_GROUP_SERVICE_CODE = 3;
+    private static final int MATCH_GROUP_SIA = 5;
+    private static final int MATCH_GROUP_SIB = 7;
+    private static final int MATCH_GROUP_SIC = 9;
+    private static final int MATCH_GROUP_PWD_CONFIRM = 11;
+    private static final int MATCH_GROUP_DIALING_NUMBER = 12;
+    static private String[] sTwoDigitNumberPattern;
+
+    //***** Public Class methods
+
+    /**
+     * Some dial strings in GSM are defined to do non-call setup
+     * things, such as modify or query supplementary service settings (eg, call
+     * forwarding). These are generally referred to as "MMI codes".
+     * We look to see if the dial string contains a valid MMI code (potentially
+     * with a dial string at the end as well) and return info here.
+     *
+     * If the dial string contains no MMI code, we return an instance with
+     * only "dialingNumber" set
+     *
+     * Please see flow chart in TS 22.030 6.5.3.2
+     */
+
+    static ImsPhoneMmiCode newFromDialString(String dialString, ImsPhone phone) {
+       return newFromDialString(dialString, phone, null);
+    }
+
+    static ImsPhoneMmiCode newFromDialString(String dialString,
+                                             ImsPhone phone, ResultReceiver wrappedCallback) {
+        Matcher m;
+        ImsPhoneMmiCode ret = null;
+
+        if (phone.getDefaultPhone().getServiceState().getVoiceRoaming()
+                && phone.getDefaultPhone().supportsConversionOfCdmaCallerIdMmiCodesWhileRoaming()) {
+            /* The CDMA MMI coded dialString will be converted to a 3GPP MMI Coded dialString
+               so that it can be processed by the matcher and code below
+             */
+            dialString = convertCdmaMmiCodesTo3gppMmiCodes(dialString);
+        }
+
+        m = sPatternSuppService.matcher(dialString);
+
+        // Is this formatted like a standard supplementary service code?
+        if (m.matches()) {
+            ret = new ImsPhoneMmiCode(phone);
+            ret.mPoundString = makeEmptyNull(m.group(MATCH_GROUP_POUND_STRING));
+            ret.mAction = makeEmptyNull(m.group(MATCH_GROUP_ACTION));
+            ret.mSc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE));
+            ret.mSia = makeEmptyNull(m.group(MATCH_GROUP_SIA));
+            ret.mSib = makeEmptyNull(m.group(MATCH_GROUP_SIB));
+            ret.mSic = makeEmptyNull(m.group(MATCH_GROUP_SIC));
+            ret.mPwd = makeEmptyNull(m.group(MATCH_GROUP_PWD_CONFIRM));
+            ret.mDialingNumber = makeEmptyNull(m.group(MATCH_GROUP_DIALING_NUMBER));
+            ret.mCallbackReceiver = wrappedCallback;
+            // According to TS 22.030 6.5.2 "Structure of the MMI",
+            // the dialing number should not ending with #.
+            // The dialing number ending # is treated as unique USSD,
+            // eg, *400#16 digit number# to recharge the prepaid card
+            // in India operator(Mumbai MTNL)
+            if (ret.mDialingNumber != null &&
+                    ret.mDialingNumber.endsWith("#") &&
+                    dialString.endsWith("#")){
+                ret = new ImsPhoneMmiCode(phone);
+                ret.mPoundString = dialString;
+            }
+        } else if (dialString.endsWith("#")) {
+            // TS 22.030 sec 6.5.3.2
+            // "Entry of any characters defined in the 3GPP TS 23.038 [8] Default Alphabet
+            // (up to the maximum defined in 3GPP TS 24.080 [10]), followed by #SEND".
+
+            ret = new ImsPhoneMmiCode(phone);
+            ret.mPoundString = dialString;
+        } else if (isTwoDigitShortCode(phone.getContext(), dialString)) {
+            //Is a country-specific exception to short codes as defined in TS 22.030, 6.5.3.2
+            ret = null;
+        } else if (isShortCode(dialString, phone)) {
+            // this may be a short code, as defined in TS 22.030, 6.5.3.2
+            ret = new ImsPhoneMmiCode(phone);
+            ret.mDialingNumber = dialString;
+        }
+
+        return ret;
+    }
+
+    private static String convertCdmaMmiCodesTo3gppMmiCodes(String dialString) {
+        Matcher m;
+        m = sPatternCdmaMmiCodeWhileRoaming.matcher(dialString);
+        if (m.matches()) {
+            String serviceCode = makeEmptyNull(m.group(MATCH_GROUP_CDMA_MMI_CODE_SERVICE_CODE));
+            String prefix = m.group(MATCH_GROUP_CDMA_MMI_CODE_NUMBER_PREFIX);
+            String number = makeEmptyNull(m.group(MATCH_GROUP_CDMA_MMI_CODE_NUMBER));
+
+            if (serviceCode.equals("67") && number != null) {
+                // "#31#number" to invoke CLIR
+                dialString = ACTION_DEACTIVATE + SC_CLIR + ACTION_DEACTIVATE + prefix + number;
+            } else if (serviceCode.equals("82") && number != null) {
+                // "*31#number" to suppress CLIR
+                dialString = ACTION_ACTIVATE + SC_CLIR + ACTION_DEACTIVATE + prefix + number;
+            }
+        }
+        return dialString;
+    }
+
+    static ImsPhoneMmiCode
+    newNetworkInitiatedUssd(String ussdMessage, boolean isUssdRequest, ImsPhone phone) {
+        ImsPhoneMmiCode ret;
+
+        ret = new ImsPhoneMmiCode(phone);
+
+        ret.mMessage = ussdMessage;
+        ret.mIsUssdRequest = isUssdRequest;
+
+        // If it's a request, set to PENDING so that it's cancelable.
+        if (isUssdRequest) {
+            ret.mIsPendingUSSD = true;
+            ret.mState = State.PENDING;
+        } else {
+            ret.mState = State.COMPLETE;
+        }
+
+        return ret;
+    }
+
+    static ImsPhoneMmiCode newFromUssdUserInput(String ussdMessge, ImsPhone phone) {
+        ImsPhoneMmiCode ret = new ImsPhoneMmiCode(phone);
+
+        ret.mMessage = ussdMessge;
+        ret.mState = State.PENDING;
+        ret.mIsPendingUSSD = true;
+
+        return ret;
+    }
+
+    //***** Private Class methods
+
+    /** make empty strings be null.
+     *  Regexp returns empty strings for empty groups
+     */
+    private static String
+    makeEmptyNull (String s) {
+        if (s != null && s.length() == 0) return null;
+
+        return s;
+    }
+
+    static boolean isScMatchesSuppServType(String dialString) {
+        boolean isMatch = false;
+        Matcher m = sPatternSuppService.matcher(dialString);
+        if (m.matches()) {
+            String sc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE));
+            if (sc.equals(SC_CFUT)) {
+                isMatch = true;
+            } else if(sc.equals(SC_BS_MT)) {
+                isMatch = true;
+            }
+        }
+        return isMatch;
+    }
+
+    /** returns true of the string is empty or null */
+    private static boolean
+    isEmptyOrNull(CharSequence s) {
+        return s == null || (s.length() == 0);
+    }
+
+    private static int
+    scToCallForwardReason(String sc) {
+        if (sc == null) {
+            throw new RuntimeException ("invalid call forward sc");
+        }
+
+        if (sc.equals(SC_CF_All)) {
+           return CommandsInterface.CF_REASON_ALL;
+        } else if (sc.equals(SC_CFU)) {
+            return CommandsInterface.CF_REASON_UNCONDITIONAL;
+        } else if (sc.equals(SC_CFB)) {
+            return CommandsInterface.CF_REASON_BUSY;
+        } else if (sc.equals(SC_CFNR)) {
+            return CommandsInterface.CF_REASON_NOT_REACHABLE;
+        } else if (sc.equals(SC_CFNRy)) {
+            return CommandsInterface.CF_REASON_NO_REPLY;
+        } else if (sc.equals(SC_CF_All_Conditional)) {
+           return CommandsInterface.CF_REASON_ALL_CONDITIONAL;
+        } else {
+            throw new RuntimeException ("invalid call forward sc");
+        }
+    }
+
+    private static int
+    siToServiceClass(String si) {
+        if (si == null || si.length() == 0) {
+                return  SERVICE_CLASS_NONE;
+        } else {
+            // NumberFormatException should cause MMI fail
+            int serviceCode = Integer.parseInt(si, 10);
+
+            switch (serviceCode) {
+                case 10: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX  + SERVICE_CLASS_VOICE;
+                case 11: return SERVICE_CLASS_VOICE;
+                case 12: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX;
+                case 13: return SERVICE_CLASS_FAX;
+
+                case 16: return SERVICE_CLASS_SMS;
+
+                case 19: return SERVICE_CLASS_FAX + SERVICE_CLASS_VOICE;
+
+                case 20: return SERVICE_CLASS_DATA_ASYNC + SERVICE_CLASS_DATA_SYNC;
+
+                case 21: return SERVICE_CLASS_PAD + SERVICE_CLASS_DATA_ASYNC;
+                case 22: return SERVICE_CLASS_PACKET + SERVICE_CLASS_DATA_SYNC;
+                case 24: return SERVICE_CLASS_DATA_SYNC;
+                case 25: return SERVICE_CLASS_DATA_ASYNC;
+                case 26: return SERVICE_CLASS_DATA_SYNC + SERVICE_CLASS_VOICE;
+                case 99: return SERVICE_CLASS_PACKET;
+
+                default:
+                    throw new RuntimeException("unsupported MMI service code " + si);
+            }
+        }
+    }
+
+    private static int
+    siToTime (String si) {
+        if (si == null || si.length() == 0) {
+            return 0;
+        } else {
+            // NumberFormatException should cause MMI fail
+            return Integer.parseInt(si, 10);
+        }
+    }
+
+    static boolean
+    isServiceCodeCallForwarding(String sc) {
+        return sc != null &&
+                (sc.equals(SC_CFU)
+                || sc.equals(SC_CFB) || sc.equals(SC_CFNRy)
+                || sc.equals(SC_CFNR) || sc.equals(SC_CF_All)
+                || sc.equals(SC_CF_All_Conditional));
+    }
+
+    static boolean
+    isServiceCodeCallBarring(String sc) {
+        Resources resource = Resources.getSystem();
+        if (sc != null) {
+            String[] barringMMI = resource.getStringArray(
+                com.android.internal.R.array.config_callBarringMMI);
+            if (barringMMI != null) {
+                for (String match : barringMMI) {
+                    if (sc.equals(match)) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    static String
+    scToBarringFacility(String sc) {
+        if (sc == null) {
+            throw new RuntimeException ("invalid call barring sc");
+        }
+
+        if (sc.equals(SC_BAOC)) {
+            return CommandsInterface.CB_FACILITY_BAOC;
+        } else if (sc.equals(SC_BAOIC)) {
+            return CommandsInterface.CB_FACILITY_BAOIC;
+        } else if (sc.equals(SC_BAOICxH)) {
+            return CommandsInterface.CB_FACILITY_BAOICxH;
+        } else if (sc.equals(SC_BAIC)) {
+            return CommandsInterface.CB_FACILITY_BAIC;
+        } else if (sc.equals(SC_BAICr)) {
+            return CommandsInterface.CB_FACILITY_BAICr;
+        } else if (sc.equals(SC_BA_ALL)) {
+            return CommandsInterface.CB_FACILITY_BA_ALL;
+        } else if (sc.equals(SC_BA_MO)) {
+            return CommandsInterface.CB_FACILITY_BA_MO;
+        } else if (sc.equals(SC_BA_MT)) {
+            return CommandsInterface.CB_FACILITY_BA_MT;
+        } else {
+            throw new RuntimeException ("invalid call barring sc");
+        }
+    }
+
+    //***** Constructor
+
+    ImsPhoneMmiCode(ImsPhone phone) {
+        // The telephony unit-test cases may create ImsPhoneMmiCode's
+        // in secondary threads
+        super(phone.getHandler().getLooper());
+        mPhone = phone;
+        mContext = phone.getContext();
+        mIccRecords = mPhone.mDefaultPhone.getIccRecords();
+    }
+
+    //***** MmiCode implementation
+
+    @Override
+    public State
+    getState() {
+        return mState;
+    }
+
+    @Override
+    public CharSequence
+    getMessage() {
+        return mMessage;
+    }
+
+    @Override
+    public Phone getPhone() { return mPhone; }
+
+    // inherited javadoc suffices
+    @Override
+    public void
+    cancel() {
+        // Complete or failed cannot be cancelled
+        if (mState == State.COMPLETE || mState == State.FAILED) {
+            return;
+        }
+
+        mState = State.CANCELLED;
+
+        if (mIsPendingUSSD) {
+            mPhone.cancelUSSD();
+        } else {
+            mPhone.onMMIDone (this);
+        }
+
+    }
+
+    @Override
+    public boolean isCancelable() {
+        /* Can only cancel pending USSD sessions. */
+        return mIsPendingUSSD;
+    }
+
+    //***** Instance Methods
+
+    String getDialingNumber() {
+        return mDialingNumber;
+    }
+
+    /** Does this dial string contain a structured or unstructured MMI code? */
+    boolean
+    isMMI() {
+        return mPoundString != null;
+    }
+
+    /* Is this a 1 or 2 digit "short code" as defined in TS 22.030 sec 6.5.3.2? */
+    boolean
+    isShortCode() {
+        return mPoundString == null
+                    && mDialingNumber != null && mDialingNumber.length() <= 2;
+
+    }
+
+    @Override
+    public String getDialString() {
+        return mPoundString;
+    }
+
+    static private boolean
+    isTwoDigitShortCode(Context context, String dialString) {
+        Rlog.d(LOG_TAG, "isTwoDigitShortCode");
+
+        if (dialString == null || dialString.length() > 2) return false;
+
+        if (sTwoDigitNumberPattern == null) {
+            sTwoDigitNumberPattern = context.getResources().getStringArray(
+                    com.android.internal.R.array.config_twoDigitNumberPattern);
+        }
+
+        for (String dialnumber : sTwoDigitNumberPattern) {
+            Rlog.d(LOG_TAG, "Two Digit Number Pattern " + dialnumber);
+            if (dialString.equals(dialnumber)) {
+                Rlog.d(LOG_TAG, "Two Digit Number Pattern -true");
+                return true;
+            }
+        }
+        Rlog.d(LOG_TAG, "Two Digit Number Pattern -false");
+        return false;
+    }
+
+    /**
+     * Helper function for newFromDialString. Returns true if dialString appears
+     * to be a short code AND conditions are correct for it to be treated as
+     * such.
+     */
+    static private boolean isShortCode(String dialString, ImsPhone phone) {
+        // Refer to TS 22.030 Figure 3.5.3.2:
+        if (dialString == null) {
+            return false;
+        }
+
+        // Illegal dial string characters will give a ZERO length.
+        // At this point we do not want to crash as any application with
+        // call privileges may send a non dial string.
+        // It return false as when the dialString is equal to NULL.
+        if (dialString.length() == 0) {
+            return false;
+        }
+
+        if (PhoneNumberUtils.isLocalEmergencyNumber(phone.getContext(), dialString)) {
+            return false;
+        } else {
+            return isShortCodeUSSD(dialString, phone);
+        }
+    }
+
+    /**
+     * Helper function for isShortCode. Returns true if dialString appears to be
+     * a short code and it is a USSD structure
+     *
+     * According to the 3PGG TS 22.030 specification Figure 3.5.3.2: A 1 or 2
+     * digit "short code" is treated as USSD if it is entered while on a call or
+     * does not satisfy the condition (exactly 2 digits && starts with '1'), there
+     * are however exceptions to this rule (see below)
+     *
+     * Exception (1) to Call initiation is: If the user of the device is already in a call
+     * and enters a Short String without any #-key at the end and the length of the Short String is
+     * equal or less then the MAX_LENGTH_SHORT_CODE [constant that is equal to 2]
+     *
+     * The phone shall initiate a USSD/SS commands.
+     */
+    static private boolean isShortCodeUSSD(String dialString, ImsPhone phone) {
+        if (dialString != null && dialString.length() <= MAX_LENGTH_SHORT_CODE) {
+            if (phone.isInCall()) {
+                return true;
+            }
+
+            if (dialString.length() != MAX_LENGTH_SHORT_CODE ||
+                    dialString.charAt(0) != '1') {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related
+     */
+    public boolean isPinPukCommand() {
+        return mSc != null && (mSc.equals(SC_PIN) || mSc.equals(SC_PIN2)
+                              || mSc.equals(SC_PUK) || mSc.equals(SC_PUK2));
+    }
+
+    /**
+     * See TS 22.030 Annex B.
+     * In temporary mode, to suppress CLIR for a single call, enter:
+     *      " * 31 # [called number] SEND "
+     *  In temporary mode, to invoke CLIR for a single call enter:
+     *       " # 31 # [called number] SEND "
+     */
+    boolean
+    isTemporaryModeCLIR() {
+        return mSc != null && mSc.equals(SC_CLIR) && mDialingNumber != null
+                && (isActivate() || isDeactivate());
+    }
+
+    /**
+     * returns CommandsInterface.CLIR_*
+     * See also isTemporaryModeCLIR()
+     */
+    int
+    getCLIRMode() {
+        if (mSc != null && mSc.equals(SC_CLIR)) {
+            if (isActivate()) {
+                return CommandsInterface.CLIR_SUPPRESSION;
+            } else if (isDeactivate()) {
+                return CommandsInterface.CLIR_INVOCATION;
+            }
+        }
+
+        return CommandsInterface.CLIR_DEFAULT;
+    }
+
+    boolean isActivate() {
+        return mAction != null && mAction.equals(ACTION_ACTIVATE);
+    }
+
+    boolean isDeactivate() {
+        return mAction != null && mAction.equals(ACTION_DEACTIVATE);
+    }
+
+    boolean isInterrogate() {
+        return mAction != null && mAction.equals(ACTION_INTERROGATE);
+    }
+
+    boolean isRegister() {
+        return mAction != null && mAction.equals(ACTION_REGISTER);
+    }
+
+    boolean isErasure() {
+        return mAction != null && mAction.equals(ACTION_ERASURE);
+    }
+
+    /**
+     * Returns true if this is a USSD code that's been submitted to the
+     * network...eg, after processCode() is called
+     */
+    public boolean isPendingUSSD() {
+        return mIsPendingUSSD;
+    }
+
+    @Override
+    public boolean isUssdRequest() {
+        return mIsUssdRequest;
+    }
+
+    boolean
+    isSupportedOverImsPhone() {
+        if (isShortCode()) return true;
+        else if (isServiceCodeCallForwarding(mSc)
+                || isServiceCodeCallBarring(mSc)
+                || (mSc != null && mSc.equals(SC_WAIT))
+                || (mSc != null && mSc.equals(SC_CLIR))
+                || (mSc != null && mSc.equals(SC_CLIP))
+                || (mSc != null && mSc.equals(SC_COLR))
+                || (mSc != null && mSc.equals(SC_COLP))
+                || (mSc != null && mSc.equals(SC_BS_MT))
+                || (mSc != null && mSc.equals(SC_BAICa))) {
+
+            try {
+                int serviceClass = siToServiceClass(mSib);
+                if (serviceClass != SERVICE_CLASS_NONE
+                        && serviceClass != SERVICE_CLASS_VOICE) {
+                    return false;
+                }
+                return true;
+            } catch (RuntimeException exc) {
+                Rlog.d(LOG_TAG, "Invalid service class " + exc);
+            }
+        } else if (isPinPukCommand()
+                || (mSc != null
+                    && (mSc.equals(SC_PWD) || mSc.equals(SC_CLIP) || mSc.equals(SC_CLIR)))) {
+            return false;
+        } else if (mPoundString != null) return true;
+
+        return false;
+    }
+
+    /*
+     * The below actions are IMS/Volte CallBarring actions.We have not defined
+     * these actions in ImscommandInterface.However we have reused existing
+     * actions of CallForwarding as, both CF and CB actions are used for same
+     * purpose.
+     */
+    public int callBarAction(String dialingNumber) {
+        if (isActivate()) {
+            return CommandsInterface.CF_ACTION_ENABLE;
+        } else if (isDeactivate()) {
+            return CommandsInterface.CF_ACTION_DISABLE;
+        } else if (isRegister()) {
+            if (!isEmptyOrNull(dialingNumber)) {
+                return CommandsInterface.CF_ACTION_REGISTRATION;
+            } else {
+                throw new RuntimeException ("invalid action");
+            }
+        } else if (isErasure()) {
+            return CommandsInterface.CF_ACTION_ERASURE;
+        } else {
+            throw new RuntimeException ("invalid action");
+        }
+    }
+
+    /** Process a MMI code or short code...anything that isn't a dialing number */
+    public void
+    processCode () throws CallStateException {
+        try {
+            if (isShortCode()) {
+                Rlog.d(LOG_TAG, "processCode: isShortCode");
+
+                // These just get treated as USSD.
+                Rlog.d(LOG_TAG, "processCode: Sending short code '"
+                       + mDialingNumber + "' over CS pipe.");
+                throw new CallStateException(Phone.CS_FALLBACK);
+            } else if (isServiceCodeCallForwarding(mSc)) {
+                Rlog.d(LOG_TAG, "processCode: is CF");
+
+                String dialingNumber = mSia;
+                int reason = scToCallForwardReason(mSc);
+                int serviceClass = siToServiceClass(mSib);
+                int time = siToTime(mSic);
+
+                if (isInterrogate()) {
+                    mPhone.getCallForwardingOption(reason,
+                            obtainMessage(EVENT_QUERY_CF_COMPLETE, this));
+                } else {
+                    int cfAction;
+
+                    if (isActivate()) {
+                        // 3GPP TS 22.030 6.5.2
+                        // a call forwarding request with a single * would be
+                        // interpreted as registration if containing a forwarded-to
+                        // number, or an activation if not
+                        if (isEmptyOrNull(dialingNumber)) {
+                            cfAction = CommandsInterface.CF_ACTION_ENABLE;
+                            mIsCallFwdReg = false;
+                        } else {
+                            cfAction = CommandsInterface.CF_ACTION_REGISTRATION;
+                            mIsCallFwdReg = true;
+                        }
+                    } else if (isDeactivate()) {
+                        cfAction = CommandsInterface.CF_ACTION_DISABLE;
+                    } else if (isRegister()) {
+                        cfAction = CommandsInterface.CF_ACTION_REGISTRATION;
+                    } else if (isErasure()) {
+                        cfAction = CommandsInterface.CF_ACTION_ERASURE;
+                    } else {
+                        throw new RuntimeException ("invalid action");
+                    }
+
+                    int isSettingUnconditional =
+                            ((reason == CommandsInterface.CF_REASON_UNCONDITIONAL) ||
+                             (reason == CommandsInterface.CF_REASON_ALL)) ? 1 : 0;
+
+                    int isEnableDesired =
+                        ((cfAction == CommandsInterface.CF_ACTION_ENABLE) ||
+                                (cfAction == CommandsInterface.CF_ACTION_REGISTRATION)) ? 1 : 0;
+
+                    Rlog.d(LOG_TAG, "processCode: is CF setCallForward");
+                    mPhone.setCallForwardingOption(cfAction, reason,
+                            dialingNumber, serviceClass, time, obtainMessage(
+                                    EVENT_SET_CFF_COMPLETE,
+                                    isSettingUnconditional,
+                                    isEnableDesired, this));
+                }
+            } else if (isServiceCodeCallBarring(mSc)) {
+                // sia = password
+                // sib = basic service group
+                // service group is not supported
+
+                String password = mSia;
+                String facility = scToBarringFacility(mSc);
+
+                if (isInterrogate()) {
+                    mPhone.getCallBarring(facility,
+                            obtainMessage(EVENT_SUPP_SVC_QUERY_COMPLETE, this));
+                } else if (isActivate() || isDeactivate()) {
+                    mPhone.setCallBarring(facility, isActivate(), password,
+                            obtainMessage(EVENT_SET_COMPLETE, this));
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mSc != null && mSc.equals(SC_CLIR)) {
+                // NOTE: Since these supplementary services are accessed only
+                //       via MMI codes, methods have not been added to ImsPhone.
+                //       Only the UT interface handle is used.
+                if (isActivate()) {
+                    try {
+                        mPhone.mCT.getUtInterface().updateCLIR(CommandsInterface.CLIR_INVOCATION,
+                            obtainMessage(EVENT_SET_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for updateCLIR.");
+                    }
+                } else if (isDeactivate()) {
+                    try {
+                        mPhone.mCT.getUtInterface().updateCLIR(CommandsInterface.CLIR_SUPPRESSION,
+                            obtainMessage(EVENT_SET_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for updateCLIR.");
+                    }
+                } else if (isInterrogate()) {
+                    try {
+                        mPhone.mCT.getUtInterface()
+                            .queryCLIR(obtainMessage(EVENT_GET_CLIR_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for queryCLIR.");
+                    }
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mSc != null && mSc.equals(SC_CLIP)) {
+                // NOTE: Refer to the note above.
+                if (isInterrogate()) {
+                    try {
+                        mPhone.mCT.getUtInterface()
+                            .queryCLIP(obtainMessage(EVENT_SUPP_SVC_QUERY_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for queryCLIP.");
+                    }
+                } else if (isActivate() || isDeactivate()) {
+                    try {
+                        mPhone.mCT.getUtInterface().updateCLIP(isActivate(),
+                                obtainMessage(EVENT_SET_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for updateCLIP.");
+                    }
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mSc != null && mSc.equals(SC_COLP)) {
+                // NOTE: Refer to the note above.
+                if (isInterrogate()) {
+                    try {
+                        mPhone.mCT.getUtInterface()
+                            .queryCOLP(obtainMessage(EVENT_SUPP_SVC_QUERY_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for queryCOLP.");
+                    }
+                } else if (isActivate() || isDeactivate()) {
+                    try {
+                        mPhone.mCT.getUtInterface().updateCOLP(isActivate(),
+                                 obtainMessage(EVENT_SET_COMPLETE, this));
+                     } catch (ImsException e) {
+                         Rlog.d(LOG_TAG, "processCode: Could not get UT handle for updateCOLP.");
+                     }
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mSc != null && mSc.equals(SC_COLR)) {
+                // NOTE: Refer to the note above.
+                if (isActivate()) {
+                    try {
+                        mPhone.mCT.getUtInterface().updateCOLR(NUM_PRESENTATION_RESTRICTED,
+                                obtainMessage(EVENT_SET_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for updateCOLR.");
+                    }
+                } else if (isDeactivate()) {
+                    try {
+                        mPhone.mCT.getUtInterface().updateCOLR(NUM_PRESENTATION_ALLOWED,
+                                obtainMessage(EVENT_SET_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for updateCOLR.");
+                    }
+                } else if (isInterrogate()) {
+                    try {
+                        mPhone.mCT.getUtInterface()
+                            .queryCOLR(obtainMessage(EVENT_SUPP_SVC_QUERY_COMPLETE, this));
+                    } catch (ImsException e) {
+                        Rlog.d(LOG_TAG, "processCode: Could not get UT handle for queryCOLR.");
+                    }
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mSc != null && (mSc.equals(SC_BS_MT))) {
+                try {
+                    if (isInterrogate()) {
+                        mPhone.mCT.getUtInterface()
+                        .queryCallBarring(ImsUtInterface.CB_BS_MT,
+                                          obtainMessage(EVENT_QUERY_ICB_COMPLETE,this));
+                    } else {
+                        processIcbMmiCodeForUpdate();
+                    }
+                 // TODO: isRegister() case needs to be handled.
+                } catch (ImsException e) {
+                    Rlog.d(LOG_TAG, "processCode: Could not get UT handle for ICB.");
+                }
+            } else if (mSc != null && mSc.equals(SC_BAICa)) {
+                int callAction =0;
+                // TODO: Should we route through queryCallBarring() here?
+                try {
+                    if (isInterrogate()) {
+                        mPhone.mCT.getUtInterface()
+                        .queryCallBarring(ImsUtInterface.CB_BIC_ACR,
+                                          obtainMessage(EVENT_QUERY_ICB_COMPLETE,this));
+                    } else {
+                        if (isActivate()) {
+                            callAction = CommandsInterface.CF_ACTION_ENABLE;
+                        } else if (isDeactivate()) {
+                            callAction = CommandsInterface.CF_ACTION_DISABLE;
+                        }
+                        mPhone.mCT.getUtInterface()
+                                .updateCallBarring(ImsUtInterface.CB_BIC_ACR,
+                                callAction,
+                                obtainMessage(EVENT_SET_COMPLETE,this),
+                                null);
+                    }
+                } catch (ImsException e) {
+                    Rlog.d(LOG_TAG, "processCode: Could not get UT handle for ICBa.");
+                }
+            } else if (mSc != null && mSc.equals(SC_WAIT)) {
+                // sia = basic service group
+                int serviceClass = siToServiceClass(mSib);
+
+                if (isActivate() || isDeactivate()) {
+                    mPhone.setCallWaiting(isActivate(), serviceClass,
+                            obtainMessage(EVENT_SET_COMPLETE, this));
+                } else if (isInterrogate()) {
+                    mPhone.getCallWaiting(obtainMessage(EVENT_QUERY_COMPLETE, this));
+                } else {
+                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
+                }
+            } else if (mPoundString != null) {
+                Rlog.d(LOG_TAG, "processCode: Sending pound string '"
+                       + mDialingNumber + "' over CS pipe.");
+                throw new CallStateException(Phone.CS_FALLBACK);
+            } else {
+                Rlog.d(LOG_TAG, "processCode: invalid or unsupported MMI");
+                throw new RuntimeException ("Invalid or Unsupported MMI Code");
+            }
+        } catch (RuntimeException exc) {
+            mState = State.FAILED;
+            mMessage = mContext.getText(com.android.internal.R.string.mmiError);
+            Rlog.d(LOG_TAG, "processCode: RuntimeException = " + exc);
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    /**
+     * Called from ImsPhone
+     *
+     * An unsolicited USSD NOTIFY or REQUEST has come in matching
+     * up with this pending USSD request
+     *
+     * Note: If REQUEST, this exchange is complete, but the session remains
+     *       active (ie, the network expects user input).
+     */
+    void
+    onUssdFinished(String ussdMessage, boolean isUssdRequest) {
+        if (mState == State.PENDING) {
+            if (TextUtils.isEmpty(ussdMessage)) {
+                mMessage = mContext.getText(com.android.internal.R.string.mmiComplete);
+                Rlog.v(LOG_TAG, "onUssdFinished: no message; using: " + mMessage);
+            } else {
+                Rlog.v(LOG_TAG, "onUssdFinished: message: " + ussdMessage);
+                mMessage = ussdMessage;
+            }
+            mIsUssdRequest = isUssdRequest;
+            // If it's a request, leave it PENDING so that it's cancelable.
+            if (!isUssdRequest) {
+                mState = State.COMPLETE;
+            }
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    /**
+     * Called from ImsPhone
+     *
+     * The radio has reset, and this is still pending
+     */
+
+    void
+    onUssdFinishedError() {
+        if (mState == State.PENDING) {
+            mState = State.FAILED;
+            mMessage = mContext.getText(com.android.internal.R.string.mmiError);
+            Rlog.d(LOG_TAG, "onUssdFinishedError: mmi=" + this);
+            mPhone.onMMIDone(this);
+        }
+    }
+
+    void sendUssd(String ussdMessage) {
+        // Treat this as a USSD string
+        mIsPendingUSSD = true;
+
+        // Note that unlike most everything else, the USSD complete
+        // response does not complete this MMI code...we wait for
+        // an unsolicited USSD "Notify" or "Request".
+        // The matching up of this is done in ImsPhone.
+
+        mPhone.sendUSSD(ussdMessage,
+            obtainMessage(EVENT_USSD_COMPLETE, this));
+    }
+
+    /** Called from ImsPhone.handleMessage; not a Handler subclass */
+    @Override
+    public void
+    handleMessage (Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+            case EVENT_SET_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+
+                onSetComplete(msg, ar);
+                break;
+
+            case EVENT_SET_CFF_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+
+                /*
+                * msg.arg1 = 1 means to set unconditional voice call forwarding
+                * msg.arg2 = 1 means to enable voice call forwarding
+                */
+                if ((ar.exception == null) && (msg.arg1 == 1)) {
+                    boolean cffEnabled = (msg.arg2 == 1);
+                    if (mIccRecords != null) {
+                        mPhone.setVoiceCallForwardingFlag(1, cffEnabled, mDialingNumber);
+                    }
+                }
+
+                onSetComplete(msg, ar);
+                break;
+
+            case EVENT_QUERY_CF_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onQueryCfComplete(ar);
+                break;
+
+            case EVENT_QUERY_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onQueryComplete(ar);
+                break;
+
+            case EVENT_USSD_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+
+                if (ar.exception != null) {
+                    mState = State.FAILED;
+                    mMessage = getErrorMessage(ar);
+
+                    mPhone.onMMIDone(this);
+                }
+
+                // Note that unlike most everything else, the USSD complete
+                // response does not complete this MMI code...we wait for
+                // an unsolicited USSD "Notify" or "Request".
+                // The matching up of this is done in ImsPhone.
+
+                break;
+
+            case EVENT_USSD_CANCEL_COMPLETE:
+                mPhone.onMMIDone(this);
+                break;
+
+            case EVENT_SUPP_SVC_QUERY_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onSuppSvcQueryComplete(ar);
+                break;
+
+            case EVENT_QUERY_ICB_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onIcbQueryComplete(ar);
+                break;
+
+            case EVENT_GET_CLIR_COMPLETE:
+                ar = (AsyncResult) (msg.obj);
+                onQueryClirComplete(ar);
+                break;
+
+            default:
+                break;
+        }
+    }
+
+    //***** Private instance methods
+
+    private void
+    processIcbMmiCodeForUpdate () {
+        String dialingNumber = mSia;
+        String[] icbNum = null;
+        int callAction;
+        if (dialingNumber != null) {
+            icbNum = dialingNumber.split("\\$");
+        }
+        callAction = callBarAction(dialingNumber);
+
+        try {
+            mPhone.mCT.getUtInterface()
+            .updateCallBarring(ImsUtInterface.CB_BS_MT,
+                               callAction,
+                               obtainMessage(EVENT_SET_COMPLETE,this),
+                               icbNum);
+        } catch (ImsException e) {
+            Rlog.d(LOG_TAG, "processIcbMmiCodeForUpdate:Could not get UT handle for updating ICB.");
+        }
+    }
+
+    private CharSequence getErrorMessage(AsyncResult ar) {
+        return mContext.getText(com.android.internal.R.string.mmiError);
+    }
+
+    private CharSequence getScString() {
+        if (mSc != null) {
+            if (isServiceCodeCallBarring(mSc)) {
+                return mContext.getText(com.android.internal.R.string.BaMmi);
+            } else if (isServiceCodeCallForwarding(mSc)) {
+                return mContext.getText(com.android.internal.R.string.CfMmi);
+            } else if (mSc.equals(SC_PWD)) {
+                return mContext.getText(com.android.internal.R.string.PwdMmi);
+            } else if (mSc.equals(SC_WAIT)) {
+                return mContext.getText(com.android.internal.R.string.CwMmi);
+            } else if (mSc.equals(SC_CLIP)) {
+                return mContext.getText(com.android.internal.R.string.ClipMmi);
+            } else if (mSc.equals(SC_CLIR)) {
+                return mContext.getText(com.android.internal.R.string.ClirMmi);
+            } else if (mSc.equals(SC_COLP)) {
+                return mContext.getText(com.android.internal.R.string.ColpMmi);
+            } else if (mSc.equals(SC_COLR)) {
+                return mContext.getText(com.android.internal.R.string.ColrMmi);
+            } else if (mSc.equals(SC_BS_MT)) {
+                return IcbDnMmi;
+            } else if (mSc.equals(SC_BAICa)) {
+                return IcbAnonymousMmi;
+            }
+        }
+
+        return "";
+    }
+
+    private void
+    onSetComplete(Message msg, AsyncResult ar){
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+
+            if (ar.exception instanceof CommandException) {
+                CommandException err = (CommandException) ar.exception;
+                if (err.getCommandError() == CommandException.Error.PASSWORD_INCORRECT) {
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.passwordIncorrect));
+                } else if (err.getMessage() != null) {
+                    sb.append(err.getMessage());
+                } else {
+                    sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+                }
+            } else {
+                ImsException error = (ImsException) ar.exception;
+                if (error.getMessage() != null) {
+                    sb.append(error.getMessage());
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            }
+        } else if (isActivate()) {
+            mState = State.COMPLETE;
+            if (mIsCallFwdReg) {
+                sb.append(mContext.getText(
+                        com.android.internal.R.string.serviceRegistered));
+            } else {
+                sb.append(mContext.getText(
+                        com.android.internal.R.string.serviceEnabled));
+            }
+        } else if (isDeactivate()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceDisabled));
+        } else if (isRegister()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceRegistered));
+        } else if (isErasure()) {
+            mState = State.COMPLETE;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.serviceErased));
+        } else {
+            mState = State.FAILED;
+            sb.append(mContext.getText(
+                    com.android.internal.R.string.mmiError));
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onSetComplete: mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    /**
+     * @param serviceClass 1 bit of the service class bit vectory
+     * @return String to be used for call forward query MMI response text.
+     *        Returns null if unrecognized
+     */
+
+    private CharSequence
+    serviceClassToCFString (int serviceClass) {
+        switch (serviceClass) {
+            case SERVICE_CLASS_VOICE:
+                return mContext.getText(com.android.internal.R.string.serviceClassVoice);
+            case SERVICE_CLASS_DATA:
+                return mContext.getText(com.android.internal.R.string.serviceClassData);
+            case SERVICE_CLASS_FAX:
+                return mContext.getText(com.android.internal.R.string.serviceClassFAX);
+            case SERVICE_CLASS_SMS:
+                return mContext.getText(com.android.internal.R.string.serviceClassSMS);
+            case SERVICE_CLASS_DATA_SYNC:
+                return mContext.getText(com.android.internal.R.string.serviceClassDataSync);
+            case SERVICE_CLASS_DATA_ASYNC:
+                return mContext.getText(com.android.internal.R.string.serviceClassDataAsync);
+            case SERVICE_CLASS_PACKET:
+                return mContext.getText(com.android.internal.R.string.serviceClassPacket);
+            case SERVICE_CLASS_PAD:
+                return mContext.getText(com.android.internal.R.string.serviceClassPAD);
+            default:
+                return null;
+        }
+    }
+
+    /** one CallForwardInfo + serviceClassMask -> one line of text */
+    private CharSequence
+    makeCFQueryResultMessage(CallForwardInfo info, int serviceClassMask) {
+        CharSequence template;
+        String sources[] = {"{0}", "{1}", "{2}"};
+        CharSequence destinations[] = new CharSequence[3];
+        boolean needTimeTemplate;
+
+        // CF_REASON_NO_REPLY also has a time value associated with
+        // it. All others don't.
+
+        needTimeTemplate =
+            (info.reason == CommandsInterface.CF_REASON_NO_REPLY);
+
+        if (info.status == 1) {
+            if (needTimeTemplate) {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateForwardedTime);
+            } else {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateForwarded);
+            }
+        } else if (info.status == 0 && isEmptyOrNull(info.number)) {
+            template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateNotForwarded);
+        } else { /* (info.status == 0) && !isEmptyOrNull(info.number) */
+            // A call forward record that is not active but contains
+            // a phone number is considered "registered"
+
+            if (needTimeTemplate) {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateRegisteredTime);
+            } else {
+                template = mContext.getText(
+                        com.android.internal.R.string.cfTemplateRegistered);
+            }
+        }
+
+        // In the template (from strings.xmls)
+        //         {0} is one of "bearerServiceCode*"
+        //        {1} is dialing number
+        //      {2} is time in seconds
+
+        destinations[0] = serviceClassToCFString(info.serviceClass & serviceClassMask);
+        destinations[1] = PhoneNumberUtils.stringFromStringAndTOA(info.number, info.toa);
+        destinations[2] = Integer.toString(info.timeSeconds);
+
+        if (info.reason == CommandsInterface.CF_REASON_UNCONDITIONAL &&
+                (info.serviceClass & serviceClassMask)
+                        == CommandsInterface.SERVICE_CLASS_VOICE) {
+            boolean cffEnabled = (info.status == 1);
+            if (mIccRecords != null) {
+                mPhone.setVoiceCallForwardingFlag(1, cffEnabled, info.number);
+            }
+        }
+
+        return TextUtils.replace(template, sources, destinations);
+    }
+
+
+    private void
+    onQueryCfComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+
+            if (ar.exception instanceof ImsException) {
+                ImsException error = (ImsException) ar.exception;
+                if (error.getMessage() != null) {
+                    sb.append(error.getMessage());
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            }
+            else {
+                sb.append(getErrorMessage(ar));
+            }
+        } else {
+            CallForwardInfo infos[];
+
+            infos = (CallForwardInfo[]) ar.result;
+
+            if (infos.length == 0) {
+                // Assume the default is not active
+                sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+
+                // Set unconditional CFF in SIM to false
+                if (mIccRecords != null) {
+                    mPhone.setVoiceCallForwardingFlag(1, false, null);
+                }
+            } else {
+
+                SpannableStringBuilder tb = new SpannableStringBuilder();
+
+                // Each bit in the service class gets its own result line
+                // The service classes may be split up over multiple
+                // CallForwardInfos. So, for each service class, find out
+                // which CallForwardInfo represents it and then build
+                // the response text based on that
+
+                for (int serviceClassMask = 1
+                            ; serviceClassMask <= SERVICE_CLASS_MAX
+                            ; serviceClassMask <<= 1
+                ) {
+                    for (int i = 0, s = infos.length; i < s ; i++) {
+                        if ((serviceClassMask & infos[i].serviceClass) != 0) {
+                            tb.append(makeCFQueryResultMessage(infos[i],
+                                            serviceClassMask));
+                            tb.append("\n");
+                        }
+                    }
+                }
+                sb.append(tb);
+            }
+
+            mState = State.COMPLETE;
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onQueryCfComplete: mmi=" + this);
+        mPhone.onMMIDone(this);
+
+    }
+
+    private void onSuppSvcQueryComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+
+            if (ar.exception instanceof ImsException) {
+                ImsException error = (ImsException) ar.exception;
+                if (error.getMessage() != null) {
+                    sb.append(error.getMessage());
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            } else {
+                sb.append(getErrorMessage(ar));
+            }
+        } else {
+            mState = State.FAILED;
+            ImsSsInfo ssInfo = null;
+            if (ar.result instanceof Bundle) {
+                Rlog.d(LOG_TAG, "onSuppSvcQueryComplete: Received CLIP/COLP/COLR Response.");
+                // Response for CLIP, COLP and COLR queries.
+                Bundle ssInfoResp = (Bundle) ar.result;
+                ssInfo = (ImsSsInfo) ssInfoResp.getParcelable(UT_BUNDLE_KEY_SSINFO);
+                if (ssInfo != null) {
+                    Rlog.d(LOG_TAG,
+                            "onSuppSvcQueryComplete: ImsSsInfo mStatus = " + ssInfo.mStatus);
+                    if (ssInfo.mStatus == ImsSsInfo.DISABLED) {
+                        sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+                        mState = State.COMPLETE;
+                    } else if (ssInfo.mStatus == ImsSsInfo.ENABLED) {
+                        sb.append(mContext.getText(com.android.internal.R.string.serviceEnabled));
+                        mState = State.COMPLETE;
+                    } else {
+                        sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+                    }
+                } else {
+                    sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+                }
+
+            } else {
+                Rlog.d(LOG_TAG, "onSuppSvcQueryComplete: Received Call Barring Response.");
+                // Response for Call Barring queries.
+                int[] cbInfos = (int[]) ar.result;
+                if (cbInfos[0] == 1) {
+                    sb.append(mContext.getText(com.android.internal.R.string.serviceEnabled));
+                    mState = State.COMPLETE;
+                } else {
+                    sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+                    mState = State.COMPLETE;
+                }
+            }
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onSuppSvcQueryComplete mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    private void onIcbQueryComplete(AsyncResult ar) {
+        Rlog.d(LOG_TAG, "onIcbQueryComplete mmi=" + this);
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+
+            if (ar.exception instanceof ImsException) {
+                ImsException error = (ImsException) ar.exception;
+                if (error.getMessage() != null) {
+                    sb.append(error.getMessage());
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            } else {
+                sb.append(getErrorMessage(ar));
+            }
+        } else {
+            ImsSsInfo[] infos = (ImsSsInfo[])ar.result;
+            if (infos.length == 0) {
+                sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+            } else {
+                for (int i = 0, s = infos.length; i < s ; i++) {
+                    if (infos[i].mIcbNum !=null) {
+                        sb.append("Num: " + infos[i].mIcbNum + " status: "
+                                + infos[i].mStatus + "\n");
+                    } else if (infos[i].mStatus == 1) {
+                        sb.append(mContext.getText(com.android.internal
+                                .R.string.serviceEnabled));
+                    } else {
+                        sb.append(mContext.getText(com.android.internal
+                                .R.string.serviceDisabled));
+                    }
+                }
+            }
+            mState = State.COMPLETE;
+        }
+        mMessage = sb;
+        mPhone.onMMIDone(this);
+    }
+
+    private void onQueryClirComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+        mState = State.FAILED;
+
+        if (ar.exception != null) {
+
+            if (ar.exception instanceof ImsException) {
+                ImsException error = (ImsException) ar.exception;
+                if (error.getMessage() != null) {
+                    sb.append(error.getMessage());
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            }
+        } else {
+            Bundle ssInfo = (Bundle) ar.result;
+            int[] clirInfo = ssInfo.getIntArray(UT_BUNDLE_KEY_CLIR);
+            // clirInfo[0] = The 'n' parameter from TS 27.007 7.7
+            // clirInfo[1] = The 'm' parameter from TS 27.007 7.7
+            Rlog.d(LOG_TAG, "onQueryClirComplete: CLIR param n=" + clirInfo[0]
+                    + " m=" + clirInfo[1]);
+
+            // 'm' parameter.
+            switch (clirInfo[1]) {
+                case CLIR_NOT_PROVISIONED:
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.serviceNotProvisioned));
+                    mState = State.COMPLETE;
+                    break;
+                case CLIR_PROVISIONED_PERMANENT:
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.CLIRPermanent));
+                    mState = State.COMPLETE;
+                    break;
+                case CLIR_PRESENTATION_RESTRICTED_TEMPORARY:
+                    // 'n' parameter.
+                    switch (clirInfo[0]) {
+                        case CLIR_DEFAULT:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOnNextCallOn));
+                            mState = State.COMPLETE;
+                            break;
+                        case CLIR_INVOCATION:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOnNextCallOn));
+                            mState = State.COMPLETE;
+                            break;
+                        case CLIR_SUPPRESSION:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOnNextCallOff));
+                            mState = State.COMPLETE;
+                            break;
+                        default:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.mmiError));
+                            mState = State.FAILED;
+                    }
+                    break;
+                case CLIR_PRESENTATION_ALLOWED_TEMPORARY:
+                    // 'n' parameter.
+                    switch (clirInfo[0]) {
+                        case CLIR_DEFAULT:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOffNextCallOff));
+                            mState = State.COMPLETE;
+                            break;
+                        case CLIR_INVOCATION:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOffNextCallOn));
+                            mState = State.COMPLETE;
+                            break;
+                        case CLIR_SUPPRESSION:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.CLIRDefaultOffNextCallOff));
+                            mState = State.COMPLETE;
+                            break;
+                        default:
+                            sb.append(mContext.getText(
+                                    com.android.internal.R.string.mmiError));
+                            mState = State.FAILED;
+                    }
+                    break;
+                default:
+                    sb.append(mContext.getText(
+                            com.android.internal.R.string.mmiError));
+                    mState = State.FAILED;
+            }
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onQueryClirComplete mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    private void
+    onQueryComplete(AsyncResult ar) {
+        StringBuilder sb = new StringBuilder(getScString());
+        sb.append("\n");
+
+        if (ar.exception != null) {
+            mState = State.FAILED;
+
+            if (ar.exception instanceof ImsException) {
+                ImsException error = (ImsException) ar.exception;
+                if (error.getMessage() != null) {
+                    sb.append(error.getMessage());
+                } else {
+                    sb.append(getErrorMessage(ar));
+                }
+            } else {
+                sb.append(getErrorMessage(ar));
+            }
+
+        } else {
+            int[] ints = (int[])ar.result;
+
+            if (ints.length != 0) {
+                if (ints[0] == 0) {
+                    sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
+                } else if (mSc.equals(SC_WAIT)) {
+                    // Call Waiting includes additional data in the response.
+                    sb.append(createQueryCallWaitingResultMessage(ints[1]));
+                } else if (ints[0] == 1) {
+                    // for all other services, treat it as a boolean
+                    sb.append(mContext.getText(com.android.internal.R.string.serviceEnabled));
+                } else {
+                    sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+                }
+            } else {
+                sb.append(mContext.getText(com.android.internal.R.string.mmiError));
+            }
+            mState = State.COMPLETE;
+        }
+
+        mMessage = sb;
+        Rlog.d(LOG_TAG, "onQueryComplete mmi=" + this);
+        mPhone.onMMIDone(this);
+    }
+
+    private CharSequence
+    createQueryCallWaitingResultMessage(int serviceClass) {
+        StringBuilder sb = new StringBuilder(
+                mContext.getText(com.android.internal.R.string.serviceEnabledFor));
+
+        for (int classMask = 1
+                    ; classMask <= SERVICE_CLASS_MAX
+                    ; classMask <<= 1
+        ) {
+            if ((classMask & serviceClass) != 0) {
+                sb.append("\n");
+                sb.append(serviceClassToCFString(classMask & serviceClass));
+            }
+        }
+        return sb;
+    }
+
+    @Override
+    public ResultReceiver getUssdCallbackReceiver() {
+        return this.mCallbackReceiver;
+    }
+
+    /***
+     * TODO: It would be nice to have a method here that can take in a dialstring and
+     * figure out if there is an MMI code embedded within it.  This code would replace
+     * some of the string parsing functionality in the Phone App's
+     * SpecialCharSequenceMgr class.
+     */
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("ImsPhoneMmiCode {");
+
+        sb.append("State=" + getState());
+        if (mAction != null) sb.append(" action=" + mAction);
+        if (mSc != null) sb.append(" sc=" + mSc);
+        if (mSia != null) sb.append(" sia=" + mSia);
+        if (mSib != null) sb.append(" sib=" + mSib);
+        if (mSic != null) sb.append(" sic=" + mSic);
+        if (mPoundString != null) sb.append(" poundString=" + Rlog.pii(LOG_TAG, mPoundString));
+        if (mDialingNumber != null) sb.append(" dialingNumber="
+                + Rlog.pii(LOG_TAG, mDialingNumber));
+        if (mPwd != null) sb.append(" pwd=" + Rlog.pii(LOG_TAG, mPwd));
+        if (mCallbackReceiver != null) sb.append(" hasReceiver");
+        sb.append("}");
+        return sb.toString();
+    }
+}
diff --git a/com/android/internal/telephony/imsphone/ImsPullCall.java b/com/android/internal/telephony/imsphone/ImsPullCall.java
new file mode 100644
index 0000000..dbb4036
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsPullCall.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.imsphone;
+
+import com.android.ims.ImsCallProfile;
+
+/**
+ * Interface implemented by modules which are capable of performing a pull of an external call.
+ * This is used to break the dependency between {@link ImsExternalCallTracker} and
+ * {@link ImsPhoneCallTracker}.
+ *
+ * @hide
+ */
+public interface ImsPullCall {
+    /**
+     * Initiate a pull of a call which has the specified phone number.
+     *
+     * @param number The phone number of the call to be pulled.
+     * @param videoState The video state of the call to be pulled.
+     * @param dialogId The {@link ImsExternalConnection#getCallId()} dialog Id associated with the
+     *                 call to be pulled.
+     */
+    void pullExternalCall(String number, int videoState, int dialogId);
+}
diff --git a/com/android/internal/telephony/imsphone/ImsRttTextHandler.java b/com/android/internal/telephony/imsphone/ImsRttTextHandler.java
new file mode 100644
index 0000000..68a832b
--- /dev/null
+++ b/com/android/internal/telephony/imsphone/ImsRttTextHandler.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.imsphone;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telecom.Connection;
+import android.telephony.Rlog;
+
+import java.io.IOException;
+
+public class ImsRttTextHandler extends Handler {
+    public interface NetworkWriter {
+        void write(String s);
+    }
+
+    private static final String LOG_TAG = "ImsRttTextHandler";
+    // RTT buffering and sending tuning constants.
+    // TODO: put this in carrier config?
+
+    // These count Unicode codepoints, not Java char types.
+    public static final int MAX_CODEPOINTS_PER_SECOND = 30;
+    // Assuming that we do not exceed the rate limit, this is the maximum time between when a
+    // piece of text is received and when it is actually sent over the network.
+    public static final int MAX_BUFFERING_DELAY_MILLIS = 200;
+    // Assuming that we do not exceed the rate limit, this is the maximum size we will allow
+    // the buffer to grow to before sending as many as we can.
+    public static final int MAX_BUFFERED_CHARACTER_COUNT = 5;
+    private static final int MILLIS_PER_SECOND = 1000;
+
+    // Messages for the handler.
+    // Initializes the text handler. Should have an RttTextStream set in msg.obj
+    private static final int INITIALIZE = 1;
+    // Appends a string to the buffer to send to the network. Should have the string in msg.obj
+    private static final int APPEND_TO_NETWORK_BUFFER = 2;
+    // Send a string received from the network to the in-call app. Should have the string in
+    // msg.obj.
+    private static final int SEND_TO_INCALL = 3;
+    // Send as many characters as possible, as constrained by the rate limit. No extra data.
+    private static final int ATTEMPT_SEND_TO_NETWORK = 4;
+    // Indicates that N characters were sent a second ago and should be ignored by the rate
+    // limiter. msg.arg1 should be set to N.
+    private static final int EXPIRE_SENT_CODEPOINT_COUNT = 5;
+    // Indicates that the call is over and we should teardown everything we have set up.
+    private static final int TEARDOWN = 6;
+
+    private Connection.RttTextStream mRttTextStream;
+
+    private class InCallReaderThread extends Thread {
+        private final Connection.RttTextStream mReaderThreadRttTextStream;
+
+        public InCallReaderThread(Connection.RttTextStream textStream) {
+            mReaderThreadRttTextStream = textStream;
+        }
+
+        @Override
+        public void run() {
+            while (true) {
+                String charsReceived;
+                try {
+                    charsReceived = mReaderThreadRttTextStream.read();
+                } catch (IOException e) {
+                    Rlog.e(LOG_TAG, "RttReaderThread - IOException encountered " +
+                            "reading from in-call: %s", e);
+                    obtainMessage(TEARDOWN).sendToTarget();
+                    break;
+                }
+                if (charsReceived == null) {
+                    if (Thread.currentThread().isInterrupted()) {
+                        Rlog.i(LOG_TAG, "RttReaderThread - Thread interrupted. Finishing.");
+                        break;
+                    }
+                    Rlog.e(LOG_TAG, "RttReaderThread - Stream closed unexpectedly. Attempt to " +
+                            "reinitialize.");
+                    obtainMessage(TEARDOWN).sendToTarget();
+                    break;
+                }
+                if (charsReceived.length() == 0) {
+                    continue;
+                }
+                obtainMessage(APPEND_TO_NETWORK_BUFFER, charsReceived)
+                        .sendToTarget();
+            }
+        }
+    }
+
+    private int mCodepointsAvailableForTransmission = MAX_CODEPOINTS_PER_SECOND;
+    private StringBuffer mBufferedTextToNetwork = new StringBuffer();
+    private InCallReaderThread mReaderThread;
+    // This is only ever used when the pipes fail and we have to re-setup. Messages received
+    // from the network are buffered here until Telecom gets back to us with the new pipes.
+    private StringBuffer mBufferedTextToIncall = new StringBuffer();
+    private final NetworkWriter mNetworkWriter;
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case INITIALIZE:
+                if (mRttTextStream != null || mReaderThread != null) {
+                    Rlog.e(LOG_TAG, "RTT text stream already initialized. Ignoring.");
+                    return;
+                }
+                mRttTextStream = (Connection.RttTextStream) msg.obj;
+                mReaderThread = new InCallReaderThread(mRttTextStream);
+                mReaderThread.start();
+                break;
+            case SEND_TO_INCALL:
+                String messageToIncall = (String) msg.obj;
+                try {
+                    mRttTextStream.write(messageToIncall);
+                } catch (IOException e) {
+                    Rlog.e(LOG_TAG, "IOException encountered writing to in-call: %s", e);
+                    obtainMessage(TEARDOWN).sendToTarget();
+                    mBufferedTextToIncall.append(messageToIncall);
+                }
+                break;
+            case APPEND_TO_NETWORK_BUFFER:
+                // First, append the text-to-send to the string buffer
+                mBufferedTextToNetwork.append((String) msg.obj);
+                // Check to see how many codepoints we have buffered. If we have more than 5,
+                // send immediately, otherwise, wait until a timeout happens.
+                int numCodepointsBuffered = mBufferedTextToNetwork
+                        .codePointCount(0, mBufferedTextToNetwork.length());
+                if (numCodepointsBuffered >= MAX_BUFFERED_CHARACTER_COUNT) {
+                    sendMessageAtFrontOfQueue(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
+                } else {
+                    sendEmptyMessageDelayed(
+                            ATTEMPT_SEND_TO_NETWORK, MAX_BUFFERING_DELAY_MILLIS);
+                }
+                break;
+            case ATTEMPT_SEND_TO_NETWORK:
+                // Check to see how many codepoints we can send, and send that many.
+                int numCodePointsAvailableInBuffer = mBufferedTextToNetwork.codePointCount(0,
+                        mBufferedTextToNetwork.length());
+                int numCodePointsSent = Math.min(numCodePointsAvailableInBuffer,
+                        mCodepointsAvailableForTransmission);
+                if (numCodePointsSent == 0) {
+                    break;
+                }
+                int endSendIndex = mBufferedTextToNetwork.offsetByCodePoints(0,
+                        numCodePointsSent);
+
+                String stringToSend = mBufferedTextToNetwork.substring(0, endSendIndex);
+
+                mBufferedTextToNetwork.delete(0, endSendIndex);
+                mNetworkWriter.write(stringToSend);
+                mCodepointsAvailableForTransmission -= numCodePointsSent;
+                sendMessageDelayed(
+                        obtainMessage(EXPIRE_SENT_CODEPOINT_COUNT, numCodePointsSent, 0),
+                        MILLIS_PER_SECOND);
+                break;
+            case EXPIRE_SENT_CODEPOINT_COUNT:
+                mCodepointsAvailableForTransmission += msg.arg1;
+                if (mCodepointsAvailableForTransmission > 0) {
+                    sendMessageAtFrontOfQueue(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
+                }
+                break;
+            case TEARDOWN:
+                try {
+                    if (mReaderThread != null) {
+                        mReaderThread.join(1000);
+                    }
+                } catch (InterruptedException e) {
+                    // Ignore and assume it'll finish on its own.
+                }
+                mReaderThread = null;
+                mRttTextStream = null;
+                break;
+        }
+    }
+
+    public ImsRttTextHandler(Looper looper, NetworkWriter networkWriter) {
+        super(looper);
+        mNetworkWriter = networkWriter;
+    }
+
+    public void sendToInCall(String msg) {
+        obtainMessage(SEND_TO_INCALL, msg).sendToTarget();
+    }
+
+    public void initialize(Connection.RttTextStream rttTextStream) {
+        obtainMessage(INITIALIZE, rttTextStream).sendToTarget();
+    }
+
+    public void tearDown() {
+        obtainMessage(TEARDOWN).sendToTarget();
+    }
+}
diff --git a/com/android/internal/telephony/metrics/CallSessionEventBuilder.java b/com/android/internal/telephony/metrics/CallSessionEventBuilder.java
new file mode 100644
index 0000000..a8221b4
--- /dev/null
+++ b/com/android/internal/telephony/metrics/CallSessionEventBuilder.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.metrics;
+
+import com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities;
+import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
+import com.android.internal.telephony.nano.TelephonyProto.ImsReasonInfo;
+import com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyServiceState;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonySettings;
+
+public class CallSessionEventBuilder {
+    private final TelephonyCallSession.Event mEvent = new TelephonyCallSession.Event();
+
+    public TelephonyCallSession.Event build() {
+        return mEvent;
+    }
+
+    public CallSessionEventBuilder(int type) {
+        mEvent.type = type;
+    }
+
+    public CallSessionEventBuilder setDelay(int delay) {
+        mEvent.delay = delay;
+        return this;
+    }
+
+    public CallSessionEventBuilder setRilRequest(int rilRequestType) {
+        mEvent.rilRequest = rilRequestType;
+        return this;
+    }
+
+    public CallSessionEventBuilder setRilRequestId(int rilRequestId) {
+        mEvent.rilRequestId = rilRequestId;
+        return this;
+    }
+
+    public CallSessionEventBuilder setRilError(int rilError) {
+        mEvent.error = rilError;
+        return this;
+    }
+
+    public CallSessionEventBuilder setCallIndex(int callIndex) {
+        mEvent.callIndex = callIndex;
+        return this;
+    }
+
+    public CallSessionEventBuilder setCallState(int state) {
+        mEvent.callState = state;
+        return this;
+    }
+
+    public CallSessionEventBuilder setSrvccState(int srvccState) {
+        mEvent.srvccState = srvccState;
+        return this;
+    }
+
+    public CallSessionEventBuilder setImsCommand(int imsCommand) {
+        mEvent.imsCommand = imsCommand;
+        return this;
+    }
+
+    public CallSessionEventBuilder setImsReasonInfo(ImsReasonInfo reasonInfo) {
+        mEvent.reasonInfo = reasonInfo;
+        return this;
+    }
+
+    public CallSessionEventBuilder setSrcAccessTech(int tech) {
+        mEvent.srcAccessTech = tech;
+        return this;
+    }
+
+    public CallSessionEventBuilder setTargetAccessTech(int tech) {
+        mEvent.targetAccessTech = tech;
+        return this;
+    }
+
+    public CallSessionEventBuilder setSettings(TelephonySettings settings) {
+        mEvent.settings = settings;
+        return this;
+    }
+
+    public CallSessionEventBuilder setServiceState(TelephonyServiceState state) {
+        mEvent.serviceState = state;
+        return this;
+    }
+
+    public CallSessionEventBuilder setImsConnectionState(ImsConnectionState state) {
+        mEvent.imsConnectionState = state;
+        return this;
+    }
+
+    public CallSessionEventBuilder setImsCapabilities(ImsCapabilities capabilities) {
+        mEvent.imsCapabilities = capabilities;
+        return this;
+    }
+
+    public CallSessionEventBuilder setDataCalls(RilDataCall[] dataCalls) {
+        mEvent.dataCalls = dataCalls;
+        return this;
+    }
+
+    public CallSessionEventBuilder setPhoneState(int phoneState) {
+        mEvent.phoneState = phoneState;
+        return this;
+    }
+
+    public CallSessionEventBuilder setNITZ(long timestamp) {
+        mEvent.nitzTimestampMillis = timestamp;
+        return this;
+    }
+
+    public CallSessionEventBuilder setRilCalls(RilCall[] rilCalls) {
+        mEvent.calls = rilCalls;
+        return this;
+    }
+}
diff --git a/com/android/internal/telephony/metrics/InProgressCallSession.java b/com/android/internal/telephony/metrics/InProgressCallSession.java
new file mode 100644
index 0000000..748fe7d
--- /dev/null
+++ b/com/android/internal/telephony/metrics/InProgressCallSession.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.metrics;
+
+import android.os.SystemClock;
+
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/** The ongoing Call session */
+public class InProgressCallSession {
+
+    /** Maximum events stored in the session */
+    private static final int MAX_EVENTS = 300;
+
+    /** Phone id */
+    public final int phoneId;
+
+    /** Call session events */
+    public final Deque<TelephonyCallSession.Event> events;
+
+    /** Call session starting system time in minute */
+    public final int startSystemTimeMin;
+
+    /** Call session starting elapsed time in milliseconds */
+    public final long startElapsedTimeMs;
+
+    /** The last event's time */
+    private long mLastElapsedTimeMs;
+
+    /** Indicating events dropped */
+    private boolean mEventsDropped = false;
+
+    /** Last known phone state */
+    private int mLastKnownPhoneState;
+
+    /** Check if events dropped */
+    public boolean isEventsDropped() { return mEventsDropped; }
+
+    /**
+     * Constructor
+     *
+     * @param phoneId Phone id
+     */
+    public InProgressCallSession(int phoneId) {
+        this.phoneId = phoneId;
+        events = new ArrayDeque<>();
+        // Save session start with lowered precision due to the privacy requirements
+        startSystemTimeMin = TelephonyMetrics.roundSessionStart(System.currentTimeMillis());
+        startElapsedTimeMs = SystemClock.elapsedRealtime();
+        mLastElapsedTimeMs = startElapsedTimeMs;
+    }
+
+    /**
+     * Add event
+     *
+     * @param builder Event builder
+     */
+    public void addEvent(CallSessionEventBuilder builder) {
+        addEvent(SystemClock.elapsedRealtime(), builder);
+    }
+
+    /**
+     * Add event
+     *
+     * @param timestamp Timestamp to be recoded with the event
+     * @param builder Event builder
+     */
+    synchronized public void addEvent(long timestamp, CallSessionEventBuilder builder) {
+        if (events.size() >= MAX_EVENTS) {
+            events.removeFirst();
+            mEventsDropped = true;
+        }
+
+        builder.setDelay(TelephonyMetrics.toPrivacyFuzzedTimeInterval(
+                mLastElapsedTimeMs, timestamp));
+
+        events.add(builder.build());
+        mLastElapsedTimeMs = timestamp;
+    }
+
+    /**
+     * Check if the Call Session contains CS calls
+     * @return true if there are CS calls in the call list
+     */
+    public boolean containsCsCalls() {
+        for (TelephonyCallSession.Event event : events) {
+            if (event.type == TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Set Phone State
+     * @param state
+     */
+    public void setLastKnownPhoneState(int state) {
+        mLastKnownPhoneState = state;
+    }
+
+    /**
+     * Checks if Phone is in Idle state
+     * @return true if device is in Phone is idle state.
+     *
+     */
+    public boolean isPhoneIdle() {
+        return (mLastKnownPhoneState == TelephonyCallSession.Event.PhoneState.STATE_IDLE);
+    }
+}
diff --git a/com/android/internal/telephony/metrics/InProgressSmsSession.java b/com/android/internal/telephony/metrics/InProgressSmsSession.java
new file mode 100644
index 0000000..9da6536
--- /dev/null
+++ b/com/android/internal/telephony/metrics/InProgressSmsSession.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.metrics;
+
+import android.os.SystemClock;
+
+import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** The ongoing SMS session */
+public class InProgressSmsSession {
+
+    /** Maximum events stored in the session */
+    private static final int MAX_EVENTS = 20;
+
+    /** Phone id */
+    public final int phoneId;
+
+    /** SMS session events */
+    public final Deque<SmsSession.Event> events;
+
+    /** Sms session starting system time in minute */
+    public final int startSystemTimeMin;
+
+    /** Sms session starting elapsed time in milliseconds */
+    public final long startElapsedTimeMs;
+
+    /** The last event's time */
+    private long mLastElapsedTimeMs;
+
+    /** Indicating events dropped */
+    private boolean mEventsDropped = false;
+
+    /** The expected SMS response #. One session could contain multiple SMS requests/responses. */
+    private AtomicInteger mNumExpectedResponses = new AtomicInteger(0);
+
+    /** Increase the expected response # */
+    public void increaseExpectedResponse() {
+        mNumExpectedResponses.incrementAndGet();
+    }
+
+    /** Decrease the expected response # */
+    public void decreaseExpectedResponse() {
+        mNumExpectedResponses.decrementAndGet();
+    }
+
+    /** Get the expected response # */
+    public int getNumExpectedResponses() {
+        return mNumExpectedResponses.get();
+    }
+
+    /** Check if events dropped */
+    public boolean isEventsDropped() { return mEventsDropped; }
+
+    /**
+     * Constructor
+     *
+     * @param phoneId Phone id
+     */
+    public InProgressSmsSession(int phoneId) {
+        this.phoneId = phoneId;
+        events = new ArrayDeque<>();
+        // Save session start with lowered precision due to the privacy requirements
+        startSystemTimeMin = TelephonyMetrics.roundSessionStart(System.currentTimeMillis());
+        startElapsedTimeMs = SystemClock.elapsedRealtime();
+        mLastElapsedTimeMs = startElapsedTimeMs;
+    }
+
+    /**
+     * Add event
+     *
+     * @param builder Event builder
+     */
+    public void addEvent(SmsSessionEventBuilder builder) {
+        addEvent(SystemClock.elapsedRealtime(), builder);
+    }
+
+    /**
+     * Add event
+     *
+     * @param timestamp Timestamp to be recoded with the event
+     * @param builder Event builder
+     */
+    public synchronized void addEvent(long timestamp, SmsSessionEventBuilder builder) {
+        if (events.size() >= MAX_EVENTS) {
+            events.removeFirst();
+            mEventsDropped = true;
+        }
+
+        builder.setDelay(TelephonyMetrics.toPrivacyFuzzedTimeInterval(
+                mLastElapsedTimeMs, timestamp));
+
+        events.add(builder.build());
+        mLastElapsedTimeMs = timestamp;
+    }
+}
diff --git a/com/android/internal/telephony/metrics/SmsSessionEventBuilder.java b/com/android/internal/telephony/metrics/SmsSessionEventBuilder.java
new file mode 100644
index 0000000..5004ce7
--- /dev/null
+++ b/com/android/internal/telephony/metrics/SmsSessionEventBuilder.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.metrics;
+
+import com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities;
+import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
+import com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
+import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyServiceState;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonySettings;
+
+public class SmsSessionEventBuilder {
+    SmsSession.Event mEvent = new SmsSession.Event();
+
+    public SmsSession.Event build() {
+        return mEvent;
+    }
+
+    public SmsSessionEventBuilder(int type) {
+        mEvent.type = type;
+    }
+
+    public SmsSessionEventBuilder setDelay(int delay) {
+        mEvent.delay = delay;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setTech(int tech) {
+        mEvent.tech = tech;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setErrorCode(int code) {
+        mEvent.errorCode = code;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setRilErrno(int errno) {
+        mEvent.error = errno;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setSettings(TelephonySettings settings) {
+        mEvent.settings = settings;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setServiceState(TelephonyServiceState state) {
+        mEvent.serviceState = state;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setImsConnectionState(ImsConnectionState state) {
+        mEvent.imsConnectionState = state;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setImsCapabilities(ImsCapabilities capabilities) {
+        mEvent.imsCapabilities = capabilities;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setDataCalls(RilDataCall[] dataCalls) {
+        mEvent.dataCalls = dataCalls;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setRilRequestId(int id) {
+        mEvent.rilRequestId = id;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setFormat(int format) {
+        mEvent.format = format;
+        return this;
+    }
+
+    public SmsSessionEventBuilder setCellBroadcastMessage(SmsSession.Event.CBMessage msg) {
+        mEvent.cellBroadcastMessage = msg;
+        return this;
+    }
+}
diff --git a/com/android/internal/telephony/metrics/TelephonyEventBuilder.java b/com/android/internal/telephony/metrics/TelephonyEventBuilder.java
new file mode 100644
index 0000000..6530802
--- /dev/null
+++ b/com/android/internal/telephony/metrics/TelephonyEventBuilder.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.metrics;
+
+import static com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities;
+import static com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
+import static com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.ModemRestart;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilDeactivateDataCall;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCall;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCallResponse;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyServiceState;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonySettings;
+
+import android.os.SystemClock;
+
+public class TelephonyEventBuilder {
+    private final TelephonyEvent mEvent = new TelephonyEvent();
+
+    public TelephonyEvent build() {
+        return mEvent;
+    }
+
+    public TelephonyEventBuilder(int phoneId) {
+        this(SystemClock.elapsedRealtime(), phoneId);
+    }
+
+    public TelephonyEventBuilder(long timestamp, int phoneId) {
+        mEvent.timestampMillis = timestamp;
+        mEvent.phoneId = phoneId;
+    }
+
+    public TelephonyEventBuilder setSettings(TelephonySettings settings) {
+        mEvent.type = TelephonyEvent.Type.SETTINGS_CHANGED;
+        mEvent.settings = settings;
+        return this;
+    }
+
+    public TelephonyEventBuilder setServiceState(TelephonyServiceState state) {
+        mEvent.type = TelephonyEvent.Type.RIL_SERVICE_STATE_CHANGED;
+        mEvent.serviceState = state;
+        return this;
+    }
+
+    public TelephonyEventBuilder setImsConnectionState(ImsConnectionState state) {
+        mEvent.type = TelephonyEvent.Type.IMS_CONNECTION_STATE_CHANGED;
+        mEvent.imsConnectionState = state;
+        return this;
+    }
+
+    public TelephonyEventBuilder setImsCapabilities(ImsCapabilities capabilities) {
+        mEvent.type = TelephonyEvent.Type.IMS_CAPABILITIES_CHANGED;
+        mEvent.imsCapabilities = capabilities;
+        return this;
+    }
+
+    public TelephonyEventBuilder setDataStallRecoveryAction(int action) {
+        mEvent.type = TelephonyEvent.Type.DATA_STALL_ACTION;
+        mEvent.dataStallAction = action;
+        return this;
+    }
+
+    public TelephonyEventBuilder setSetupDataCall(RilSetupDataCall request) {
+        mEvent.type = TelephonyEvent.Type.DATA_CALL_SETUP;
+        mEvent.setupDataCall = request;
+        return this;
+    }
+
+    public TelephonyEventBuilder setSetupDataCallResponse(RilSetupDataCallResponse rsp) {
+        mEvent.type = TelephonyEvent.Type.DATA_CALL_SETUP_RESPONSE;
+        mEvent.setupDataCallResponse = rsp;
+        return this;
+    }
+
+    public TelephonyEventBuilder setDeactivateDataCall(RilDeactivateDataCall request) {
+        mEvent.type = TelephonyEvent.Type.DATA_CALL_DEACTIVATE;
+        mEvent.deactivateDataCall = request;
+        return this;
+    }
+
+    public TelephonyEventBuilder setDeactivateDataCallResponse(int errno) {
+        mEvent.type = TelephonyEvent.Type.DATA_CALL_DEACTIVATE_RESPONSE;
+        mEvent.error = errno;
+        return this;
+    }
+
+    public TelephonyEventBuilder setDataCalls(RilDataCall[] dataCalls) {
+        mEvent.type = TelephonyEvent.Type.DATA_CALL_LIST_CHANGED;
+        mEvent.dataCalls = dataCalls;
+        return this;
+    }
+
+    public TelephonyEventBuilder setNITZ(long timestamp) {
+        mEvent.type = TelephonyEvent.Type.NITZ_TIME;
+        mEvent.nitzTimestampMillis = timestamp;
+        return this;
+    }
+
+    public TelephonyEventBuilder setModemRestart(ModemRestart modemRestart) {
+        mEvent.type = TelephonyEvent.Type.MODEM_RESTART;
+        mEvent.modemRestart = modemRestart;
+        return this;
+    }
+}
diff --git a/com/android/internal/telephony/metrics/TelephonyMetrics.java b/com/android/internal/telephony/metrics/TelephonyMetrics.java
new file mode 100644
index 0000000..bbb595c
--- /dev/null
+++ b/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -0,0 +1,1768 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.metrics;
+
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_ANSWER;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_CDMA_SEND_SMS;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DEACTIVATE_DATA_CALL;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DIAL;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP;
+import static com.android.internal.telephony.RILConstants
+        .RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_IMS_SEND_SMS;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SEND_SMS;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SEND_SMS_EXPECT_MORE;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SETUP_DATA_CALL;
+import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_IP;
+import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_IPV4V6;
+import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_IPV6;
+import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_PPP;
+import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_UNKNOWN;
+
+import android.os.Build;
+import android.os.SystemClock;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyHistogram;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.SparseArray;
+
+import com.android.ims.ImsConfig;
+import com.android.ims.ImsReasonInfo;
+import com.android.ims.internal.ImsCallSession;
+import com.android.internal.telephony.GsmCdmaConnection;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RIL;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.SmsResponse;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.dataconnection.DataCallResponse;
+import com.android.internal.telephony.imsphone.ImsPhoneCall;
+import com.android.internal.telephony.nano.TelephonyProto;
+import com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities;
+import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
+import com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
+import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.CallState;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall.Type;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.ModemRestart;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilDeactivateDataCall;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCall;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCallResponse;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCallResponse
+        .RilDataCallFailCause;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyLog;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyServiceState;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonySettings;
+import com.android.internal.telephony.nano.TelephonyProto.TimeInterval;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Telephony metrics holds all metrics events and convert it into telephony proto buf.
+ * @hide
+ */
+public class TelephonyMetrics {
+
+    private static final String TAG = TelephonyMetrics.class.getSimpleName();
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    /** Maximum telephony events stored */
+    private static final int MAX_TELEPHONY_EVENTS = 1000;
+
+    /** Maximum call sessions stored */
+    private static final int MAX_COMPLETED_CALL_SESSIONS = 50;
+
+    /** Maximum sms sessions stored */
+    private static final int MAX_COMPLETED_SMS_SESSIONS = 500;
+
+    /** For reducing the timing precision for privacy purposes */
+    private static final int SESSION_START_PRECISION_MINUTES = 5;
+
+    /** The TelephonyMetrics singleton instance */
+    private static TelephonyMetrics sInstance;
+
+    /** Telephony events */
+    private final Deque<TelephonyEvent> mTelephonyEvents = new ArrayDeque<>();
+
+    /**
+     * In progress call sessions. Note that each phone can only have up to 1 in progress call
+     * session (might contains multiple calls). Having a sparse array in case we need to support
+     * DSDA in the future.
+     */
+    private final SparseArray<InProgressCallSession> mInProgressCallSessions = new SparseArray<>();
+
+    /** The completed call sessions */
+    private final Deque<TelephonyCallSession> mCompletedCallSessions = new ArrayDeque<>();
+
+    /** The in-progress SMS sessions. When finished, it will be moved into the completed sessions */
+    private final SparseArray<InProgressSmsSession> mInProgressSmsSessions = new SparseArray<>();
+
+    /** The completed SMS sessions */
+    private final Deque<SmsSession> mCompletedSmsSessions = new ArrayDeque<>();
+
+    /** Last service state. This is for injecting the base of a new log or a new call/sms session */
+    private final SparseArray<TelephonyServiceState> mLastServiceState = new SparseArray<>();
+
+    /**
+     * Last ims capabilities. This is for injecting the base of a new log or a new call/sms
+     * session
+     */
+    private final SparseArray<ImsCapabilities> mLastImsCapabilities = new SparseArray<>();
+
+    /**
+     * Last IMS connection state. This is for injecting the base of a new log or a new call/sms
+     * session
+     */
+    private final SparseArray<ImsConnectionState> mLastImsConnectionState = new SparseArray<>();
+
+    /**
+     * Last settings state. This is for deduping same settings event logged.
+     */
+    private final SparseArray<TelephonySettings> mLastSettings = new SparseArray<>();
+
+    /** The start system time of the TelephonyLog in milliseconds*/
+    private long mStartSystemTimeMs;
+
+    /** The start elapsed time of the TelephonyLog in milliseconds*/
+    private long mStartElapsedTimeMs;
+
+    /** Indicating if some of the telephony events are dropped in this log */
+    private boolean mTelephonyEventsDropped = false;
+
+    public TelephonyMetrics() {
+        reset();
+    }
+
+    /**
+     * Get the singleton instance of telephony metrics.
+     *
+     * @return The instance
+     */
+    public synchronized static TelephonyMetrics getInstance() {
+        if (sInstance == null) {
+            sInstance = new TelephonyMetrics();
+        }
+
+        return sInstance;
+    }
+
+    /**
+     * Dump the state of various objects, add calls to other objects as desired.
+     *
+     * @param fd File descriptor
+     * @param pw Print writer
+     * @param args Arguments
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (args != null && args.length > 0) {
+            switch (args[0]) {
+                case "--metrics":
+                    printAllMetrics(pw);
+                    break;
+                case "--metricsproto":
+                    pw.println(convertProtoToBase64String(buildProto()));
+                    reset();
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Convert the telephony event to string
+     *
+     * @param event The event in integer
+     * @return The event in string
+     */
+    private static String telephonyEventToString(int event) {
+        switch (event) {
+            case TelephonyEvent.Type.UNKNOWN:
+                return "UNKNOWN";
+            case TelephonyEvent.Type.SETTINGS_CHANGED:
+                return "SETTINGS_CHANGED";
+            case TelephonyEvent.Type.RIL_SERVICE_STATE_CHANGED:
+                return "RIL_SERVICE_STATE_CHANGED";
+            case TelephonyEvent.Type.IMS_CONNECTION_STATE_CHANGED:
+                return "IMS_CONNECTION_STATE_CHANGED";
+            case TelephonyEvent.Type.IMS_CAPABILITIES_CHANGED:
+                return "IMS_CAPABILITIES_CHANGED";
+            case TelephonyEvent.Type.DATA_CALL_SETUP:
+                return "DATA_CALL_SETUP";
+            case TelephonyEvent.Type.DATA_CALL_SETUP_RESPONSE:
+                return "DATA_CALL_SETUP_RESPONSE";
+            case TelephonyEvent.Type.DATA_CALL_LIST_CHANGED:
+                return "DATA_CALL_LIST_CHANGED";
+            case TelephonyEvent.Type.DATA_CALL_DEACTIVATE:
+                return "DATA_CALL_DEACTIVATE";
+            case TelephonyEvent.Type.DATA_CALL_DEACTIVATE_RESPONSE:
+                return "DATA_CALL_DEACTIVATE_RESPONSE";
+            case TelephonyEvent.Type.DATA_STALL_ACTION:
+                return "DATA_STALL_ACTION";
+            case TelephonyEvent.Type.MODEM_RESTART:
+                return "MODEM_RESTART";
+            default:
+                return Integer.toString(event);
+        }
+    }
+
+    /**
+     * Convert the call session event into string
+     *
+     * @param event The event in integer
+     * @return The event in String
+     */
+    private static String callSessionEventToString(int event) {
+        switch (event) {
+            case TelephonyCallSession.Event.Type.EVENT_UNKNOWN:
+                return "EVENT_UNKNOWN";
+            case TelephonyCallSession.Event.Type.SETTINGS_CHANGED:
+                return "SETTINGS_CHANGED";
+            case TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED:
+                return "RIL_SERVICE_STATE_CHANGED";
+            case TelephonyCallSession.Event.Type.IMS_CONNECTION_STATE_CHANGED:
+                return "IMS_CONNECTION_STATE_CHANGED";
+            case TelephonyCallSession.Event.Type.IMS_CAPABILITIES_CHANGED:
+                return "IMS_CAPABILITIES_CHANGED";
+            case TelephonyCallSession.Event.Type.DATA_CALL_LIST_CHANGED:
+                return "DATA_CALL_LIST_CHANGED";
+            case TelephonyCallSession.Event.Type.RIL_REQUEST:
+                return "RIL_REQUEST";
+            case TelephonyCallSession.Event.Type.RIL_RESPONSE:
+                return "RIL_RESPONSE";
+            case TelephonyCallSession.Event.Type.RIL_CALL_RING:
+                return "RIL_CALL_RING";
+            case TelephonyCallSession.Event.Type.RIL_CALL_SRVCC:
+                return "RIL_CALL_SRVCC";
+            case TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED:
+                return "RIL_CALL_LIST_CHANGED";
+            case TelephonyCallSession.Event.Type.IMS_COMMAND:
+                return "IMS_COMMAND";
+            case TelephonyCallSession.Event.Type.IMS_COMMAND_RECEIVED:
+                return "IMS_COMMAND_RECEIVED";
+            case TelephonyCallSession.Event.Type.IMS_COMMAND_FAILED:
+                return "IMS_COMMAND_FAILED";
+            case TelephonyCallSession.Event.Type.IMS_COMMAND_COMPLETE:
+                return "IMS_COMMAND_COMPLETE";
+            case TelephonyCallSession.Event.Type.IMS_CALL_RECEIVE:
+                return "IMS_CALL_RECEIVE";
+            case TelephonyCallSession.Event.Type.IMS_CALL_STATE_CHANGED:
+                return "IMS_CALL_STATE_CHANGED";
+            case TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED:
+                return "IMS_CALL_TERMINATED";
+            case TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER:
+                return "IMS_CALL_HANDOVER";
+            case TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER_FAILED:
+                return "IMS_CALL_HANDOVER_FAILED";
+            case TelephonyCallSession.Event.Type.PHONE_STATE_CHANGED:
+                return "PHONE_STATE_CHANGED";
+            case TelephonyCallSession.Event.Type.NITZ_TIME:
+                return "NITZ_TIME";
+            default:
+                return Integer.toString(event);
+        }
+    }
+
+    /**
+     * Convert the SMS session event into string
+     * @param event The event in integer
+     * @return The event in String
+     */
+    private static String smsSessionEventToString(int event) {
+        switch (event) {
+            case SmsSession.Event.Type.EVENT_UNKNOWN:
+                return "EVENT_UNKNOWN";
+            case SmsSession.Event.Type.SETTINGS_CHANGED:
+                return "SETTINGS_CHANGED";
+            case SmsSession.Event.Type.RIL_SERVICE_STATE_CHANGED:
+                return "RIL_SERVICE_STATE_CHANGED";
+            case SmsSession.Event.Type.IMS_CONNECTION_STATE_CHANGED:
+                return "IMS_CONNECTION_STATE_CHANGED";
+            case SmsSession.Event.Type.IMS_CAPABILITIES_CHANGED:
+                return "IMS_CAPABILITIES_CHANGED";
+            case SmsSession.Event.Type.DATA_CALL_LIST_CHANGED:
+                return "DATA_CALL_LIST_CHANGED";
+            case SmsSession.Event.Type.SMS_SEND:
+                return "SMS_SEND";
+            case SmsSession.Event.Type.SMS_SEND_RESULT:
+                return "SMS_SEND_RESULT";
+            case SmsSession.Event.Type.SMS_RECEIVED:
+                return "SMS_RECEIVED";
+            default:
+                return Integer.toString(event);
+        }
+    }
+
+    /**
+     * Print all metrics data for debugging purposes
+     *
+     * @param rawWriter Print writer
+     */
+    private synchronized void printAllMetrics(PrintWriter rawWriter) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, "  ");
+
+        pw.println("Telephony metrics proto:");
+        pw.println("------------------------------------------");
+        pw.println("Telephony events:");
+        pw.increaseIndent();
+        for (TelephonyEvent event : mTelephonyEvents) {
+            pw.print(event.timestampMillis);
+            pw.print(" [");
+            pw.print(event.phoneId);
+            pw.print("] ");
+
+            pw.print("T=");
+            if (event.type == TelephonyEvent.Type.RIL_SERVICE_STATE_CHANGED) {
+                pw.print(telephonyEventToString(event.type)
+                        + "(" + event.serviceState.dataRat + ")");
+            } else {
+                pw.print(telephonyEventToString(event.type));
+            }
+
+            pw.println("");
+        }
+
+        pw.decreaseIndent();
+        pw.println("Call sessions:");
+        pw.increaseIndent();
+
+        for (TelephonyCallSession callSession : mCompletedCallSessions) {
+            pw.println("Start time in minutes: " + callSession.startTimeMinutes);
+            pw.println("Events dropped: " + callSession.eventsDropped);
+
+            pw.println("Events: ");
+            pw.increaseIndent();
+            for (TelephonyCallSession.Event event : callSession.events) {
+                pw.print(event.delay);
+                pw.print(" T=");
+                if (event.type == TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED) {
+                    pw.println(callSessionEventToString(event.type)
+                            + "(" + event.serviceState.dataRat + ")");
+                } else if (event.type == TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED) {
+                    pw.println(callSessionEventToString(event.type));
+                    pw.increaseIndent();
+                    for (RilCall call : event.calls) {
+                        pw.println(call.index + ". Type = " + call.type + " State = "
+                                + call.state + " End Reason " + call.callEndReason
+                                + " isMultiparty = " + call.isMultiparty);
+                    }
+                    pw.decreaseIndent();
+                } else {
+                    pw.println(callSessionEventToString(event.type));
+                }
+            }
+            pw.decreaseIndent();
+        }
+
+        pw.decreaseIndent();
+        pw.println("Sms sessions:");
+        pw.increaseIndent();
+
+        int count = 0;
+        for (SmsSession smsSession : mCompletedSmsSessions) {
+            count++;
+            pw.print("[" + count + "] Start time in minutes: "
+                    + smsSession.startTimeMinutes);
+
+            if (smsSession.eventsDropped) {
+                pw.println(", events dropped: " + smsSession.eventsDropped);
+            }
+            pw.println("Events: ");
+            pw.increaseIndent();
+            for (SmsSession.Event event : smsSession.events) {
+                pw.print(event.delay);
+                pw.print(" T=");
+                pw.println(smsSessionEventToString(event.type));
+            }
+            pw.decreaseIndent();
+        }
+
+        pw.decreaseIndent();
+    }
+
+    /**
+     * Convert the telephony proto into Base-64 encoded string
+     *
+     * @param proto Telephony proto
+     * @return Encoded string
+     */
+    private static String convertProtoToBase64String(TelephonyLog proto) {
+        return Base64.encodeToString(
+                TelephonyProto.TelephonyLog.toByteArray(proto), Base64.DEFAULT);
+    }
+
+    /**
+     * Reset all events and sessions
+     */
+    private synchronized void reset() {
+        mTelephonyEvents.clear();
+        mCompletedCallSessions.clear();
+        mCompletedSmsSessions.clear();
+
+        mTelephonyEventsDropped = false;
+
+        mStartSystemTimeMs = System.currentTimeMillis();
+        mStartElapsedTimeMs = SystemClock.elapsedRealtime();
+
+        // Insert the last known service state, ims capabilities, and ims connection states as the
+        // base.
+        for (int i = 0; i < mLastServiceState.size(); i++) {
+            final int key = mLastServiceState.keyAt(i);
+
+            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
+                    .setServiceState(mLastServiceState.get(key)).build();
+            addTelephonyEvent(event);
+        }
+
+        for (int i = 0; i < mLastImsCapabilities.size(); i++) {
+            final int key = mLastImsCapabilities.keyAt(i);
+
+            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
+                    .setImsCapabilities(mLastImsCapabilities.get(key)).build();
+            addTelephonyEvent(event);
+        }
+
+        for (int i = 0; i < mLastImsConnectionState.size(); i++) {
+            final int key = mLastImsConnectionState.keyAt(i);
+
+            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
+                    .setImsConnectionState(mLastImsConnectionState.get(key)).build();
+            addTelephonyEvent(event);
+        }
+    }
+
+    /**
+     * Build the telephony proto
+     *
+     * @return Telephony proto
+     */
+    private synchronized TelephonyLog buildProto() {
+
+        TelephonyLog log = new TelephonyLog();
+        // Build telephony events
+        log.events = new TelephonyEvent[mTelephonyEvents.size()];
+        mTelephonyEvents.toArray(log.events);
+        log.eventsDropped = mTelephonyEventsDropped;
+
+        // Build call sessions
+        log.callSessions = new TelephonyCallSession[mCompletedCallSessions.size()];
+        mCompletedCallSessions.toArray(log.callSessions);
+
+        // Build SMS sessions
+        log.smsSessions = new SmsSession[mCompletedSmsSessions.size()];
+        mCompletedSmsSessions.toArray(log.smsSessions);
+
+        // Build histogram. Currently we only support RIL histograms.
+        List<TelephonyHistogram> rilHistograms = RIL.getTelephonyRILTimingHistograms();
+        log.histograms = new TelephonyProto.TelephonyHistogram[rilHistograms.size()];
+        for (int i = 0; i < rilHistograms.size(); i++) {
+            log.histograms[i] = new TelephonyProto.TelephonyHistogram();
+            TelephonyHistogram rilHistogram = rilHistograms.get(i);
+            TelephonyProto.TelephonyHistogram histogramProto = log.histograms[i];
+
+            histogramProto.category = rilHistogram.getCategory();
+            histogramProto.id = rilHistogram.getId();
+            histogramProto.minTimeMillis = rilHistogram.getMinTime();
+            histogramProto.maxTimeMillis = rilHistogram.getMaxTime();
+            histogramProto.avgTimeMillis = rilHistogram.getAverageTime();
+            histogramProto.count = rilHistogram.getSampleCount();
+            histogramProto.bucketCount = rilHistogram.getBucketCount();
+            histogramProto.bucketEndPoints = rilHistogram.getBucketEndPoints();
+            histogramProto.bucketCounters = rilHistogram.getBucketCounters();
+        }
+
+        // Log the starting system time
+        log.startTime = new TelephonyProto.Time();
+        log.startTime.systemTimestampMillis = mStartSystemTimeMs;
+        log.startTime.elapsedTimestampMillis = mStartElapsedTimeMs;
+
+        log.endTime = new TelephonyProto.Time();
+        log.endTime.systemTimestampMillis = System.currentTimeMillis();
+        log.endTime.elapsedTimestampMillis = SystemClock.elapsedRealtime();
+
+        return log;
+    }
+
+    /**
+     * Reduce precision to meet privacy requirements.
+     *
+     * @param timestamp timestamp in milliseconds
+     * @return Precision reduced timestamp in minutes
+     */
+    static int roundSessionStart(long timestamp) {
+        return (int) ((timestamp) / (MINUTE_IN_MILLIS * SESSION_START_PRECISION_MINUTES)
+                * (SESSION_START_PRECISION_MINUTES));
+    }
+
+    /**
+     * Get the time interval with reduced prevision
+     *
+     * @param previousTimestamp Previous timestamp in milliseconds
+     * @param currentTimestamp Current timestamp in milliseconds
+     * @return The time interval
+     */
+    static int toPrivacyFuzzedTimeInterval(long previousTimestamp, long currentTimestamp) {
+        long diff = currentTimestamp - previousTimestamp;
+        if (diff < 0) {
+            return TimeInterval.TI_UNKNOWN;
+        } else if (diff <= 10) {
+            return TimeInterval.TI_10_MILLIS;
+        } else if (diff <= 20) {
+            return TimeInterval.TI_20_MILLIS;
+        } else if (diff <= 50) {
+            return TimeInterval.TI_50_MILLIS;
+        } else if (diff <= 100) {
+            return TimeInterval.TI_100_MILLIS;
+        } else if (diff <= 200) {
+            return TimeInterval.TI_200_MILLIS;
+        } else if (diff <= 500) {
+            return TimeInterval.TI_500_MILLIS;
+        } else if (diff <= 1000) {
+            return TimeInterval.TI_1_SEC;
+        } else if (diff <= 2000) {
+            return TimeInterval.TI_2_SEC;
+        } else if (diff <= 5000) {
+            return TimeInterval.TI_5_SEC;
+        } else if (diff <= 10000) {
+            return TimeInterval.TI_10_SEC;
+        } else if (diff <= 30000) {
+            return TimeInterval.TI_30_SEC;
+        } else if (diff <= 60000) {
+            return TimeInterval.TI_1_MINUTE;
+        } else if (diff <= 180000) {
+            return TimeInterval.TI_3_MINUTES;
+        } else if (diff <= 600000) {
+            return TimeInterval.TI_10_MINUTES;
+        } else if (diff <= 1800000) {
+            return TimeInterval.TI_30_MINUTES;
+        } else if (diff <= 3600000) {
+            return TimeInterval.TI_1_HOUR;
+        } else if (diff <= 7200000) {
+            return TimeInterval.TI_2_HOURS;
+        } else if (diff <= 14400000) {
+            return TimeInterval.TI_4_HOURS;
+        } else {
+            return TimeInterval.TI_MANY_HOURS;
+        }
+    }
+
+    /**
+     * Convert the service state into service state proto
+     *
+     * @param serviceState Service state
+     * @return Service state proto
+     */
+    private TelephonyServiceState toServiceStateProto(ServiceState serviceState) {
+        TelephonyServiceState ssProto = new TelephonyServiceState();
+
+        ssProto.voiceRoamingType = serviceState.getVoiceRoamingType();
+        ssProto.dataRoamingType = serviceState.getDataRoamingType();
+
+        ssProto.voiceOperator = new TelephonyServiceState.TelephonyOperator();
+
+        if (serviceState.getVoiceOperatorAlphaLong() != null) {
+            ssProto.voiceOperator.alphaLong = serviceState.getVoiceOperatorAlphaLong();
+        }
+
+        if (serviceState.getVoiceOperatorAlphaShort() != null) {
+            ssProto.voiceOperator.alphaShort = serviceState.getVoiceOperatorAlphaShort();
+        }
+
+        if (serviceState.getVoiceOperatorNumeric() != null) {
+            ssProto.voiceOperator.numeric = serviceState.getVoiceOperatorNumeric();
+        }
+
+        ssProto.dataOperator = new TelephonyServiceState.TelephonyOperator();
+
+        if (serviceState.getDataOperatorAlphaLong() != null) {
+            ssProto.dataOperator.alphaLong = serviceState.getDataOperatorAlphaLong();
+        }
+
+        if (serviceState.getDataOperatorAlphaShort() != null) {
+            ssProto.dataOperator.alphaShort = serviceState.getDataOperatorAlphaShort();
+        }
+
+        if (serviceState.getDataOperatorNumeric() != null) {
+            ssProto.dataOperator.numeric = serviceState.getDataOperatorNumeric();
+        }
+
+        ssProto.voiceRat = serviceState.getRilVoiceRadioTechnology();
+        ssProto.dataRat = serviceState.getRilDataRadioTechnology();
+        return ssProto;
+    }
+
+    /**
+     * Annotate the call session with events
+     *
+     * @param timestamp Event timestamp
+     * @param phoneId Phone id
+     * @param eventBuilder Call session event builder
+     */
+    private synchronized void annotateInProgressCallSession(long timestamp, int phoneId,
+                                                            CallSessionEventBuilder eventBuilder) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession != null) {
+            callSession.addEvent(timestamp, eventBuilder);
+        }
+    }
+
+    /**
+     * Annotate the SMS session with events
+     *
+     * @param timestamp Event timestamp
+     * @param phoneId Phone id
+     * @param eventBuilder SMS session event builder
+     */
+    private synchronized void annotateInProgressSmsSession(long timestamp, int phoneId,
+                                                           SmsSessionEventBuilder eventBuilder) {
+        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
+        if (smsSession != null) {
+            smsSession.addEvent(timestamp, eventBuilder);
+        }
+    }
+
+    /**
+     * Create the call session if there isn't any existing one
+     *
+     * @param phoneId Phone id
+     * @return The call session
+     */
+    private synchronized InProgressCallSession startNewCallSessionIfNeeded(int phoneId) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            if (VDBG) Rlog.v(TAG, "Starting a new call session on phone " + phoneId);
+            callSession = new InProgressCallSession(phoneId);
+            mInProgressCallSessions.append(phoneId, callSession);
+
+            // Insert the latest service state, ims capabilities, and ims connection states as the
+            // base.
+            TelephonyServiceState serviceState = mLastServiceState.get(phoneId);
+            if (serviceState != null) {
+                callSession.addEvent(callSession.startElapsedTimeMs, new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
+                        .setServiceState(serviceState));
+            }
+
+            ImsCapabilities imsCapabilities = mLastImsCapabilities.get(phoneId);
+            if (imsCapabilities != null) {
+                callSession.addEvent(callSession.startElapsedTimeMs, new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.IMS_CAPABILITIES_CHANGED)
+                        .setImsCapabilities(imsCapabilities));
+            }
+
+            ImsConnectionState imsConnectionState = mLastImsConnectionState.get(phoneId);
+            if (imsConnectionState != null) {
+                callSession.addEvent(callSession.startElapsedTimeMs, new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
+                        .setImsConnectionState(imsConnectionState));
+            }
+        }
+        return callSession;
+    }
+
+    /**
+     * Create the SMS session if there isn't any existing one
+     *
+     * @param phoneId Phone id
+     * @return The SMS session
+     */
+    private synchronized InProgressSmsSession startNewSmsSessionIfNeeded(int phoneId) {
+        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
+        if (smsSession == null) {
+            if (VDBG) Rlog.v(TAG, "Starting a new sms session on phone " + phoneId);
+            smsSession = new InProgressSmsSession(phoneId);
+            mInProgressSmsSessions.append(phoneId, smsSession);
+
+            // Insert the latest service state, ims capabilities, and ims connection state as the
+            // base.
+            TelephonyServiceState serviceState = mLastServiceState.get(phoneId);
+            if (serviceState != null) {
+                smsSession.addEvent(smsSession.startElapsedTimeMs, new SmsSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
+                        .setServiceState(serviceState));
+            }
+
+            ImsCapabilities imsCapabilities = mLastImsCapabilities.get(phoneId);
+            if (imsCapabilities != null) {
+                smsSession.addEvent(smsSession.startElapsedTimeMs, new SmsSessionEventBuilder(
+                        SmsSession.Event.Type.IMS_CAPABILITIES_CHANGED)
+                        .setImsCapabilities(imsCapabilities));
+            }
+
+            ImsConnectionState imsConnectionState = mLastImsConnectionState.get(phoneId);
+            if (imsConnectionState != null) {
+                smsSession.addEvent(smsSession.startElapsedTimeMs, new SmsSessionEventBuilder(
+                        SmsSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
+                        .setImsConnectionState(imsConnectionState));
+            }
+        }
+        return smsSession;
+    }
+
+    /**
+     * Finish the call session and move it into the completed session
+     *
+     * @param inProgressCallSession The in progress call session
+     */
+    private synchronized void finishCallSession(InProgressCallSession inProgressCallSession) {
+        TelephonyCallSession callSession = new TelephonyCallSession();
+        callSession.events = new TelephonyCallSession.Event[inProgressCallSession.events.size()];
+        inProgressCallSession.events.toArray(callSession.events);
+        callSession.startTimeMinutes = inProgressCallSession.startSystemTimeMin;
+        callSession.phoneId = inProgressCallSession.phoneId;
+        callSession.eventsDropped = inProgressCallSession.isEventsDropped();
+        if (mCompletedCallSessions.size() >= MAX_COMPLETED_CALL_SESSIONS) {
+            mCompletedCallSessions.removeFirst();
+        }
+        mCompletedCallSessions.add(callSession);
+        mInProgressCallSessions.remove(inProgressCallSession.phoneId);
+        if (VDBG) Rlog.v(TAG, "Call session finished");
+    }
+
+    /**
+     * Finish the SMS session and move it into the completed session
+     *
+     * @param inProgressSmsSession The in progress SMS session
+     */
+    private synchronized void finishSmsSessionIfNeeded(InProgressSmsSession inProgressSmsSession) {
+        if (inProgressSmsSession.getNumExpectedResponses() == 0) {
+            SmsSession smsSession = new SmsSession();
+            smsSession.events = new SmsSession.Event[inProgressSmsSession.events.size()];
+            inProgressSmsSession.events.toArray(smsSession.events);
+            smsSession.startTimeMinutes = inProgressSmsSession.startSystemTimeMin;
+            smsSession.phoneId = inProgressSmsSession.phoneId;
+            smsSession.eventsDropped = inProgressSmsSession.isEventsDropped();
+            if (mCompletedSmsSessions.size() >= MAX_COMPLETED_SMS_SESSIONS) {
+                mCompletedSmsSessions.removeFirst();
+            }
+            mCompletedSmsSessions.add(smsSession);
+            mInProgressSmsSessions.remove(inProgressSmsSession.phoneId);
+            if (VDBG) Rlog.v(TAG, "SMS session finished");
+        }
+    }
+
+    /**
+     * Add telephony event into the queue
+     *
+     * @param event Telephony event
+     */
+    private synchronized void addTelephonyEvent(TelephonyEvent event) {
+        if (mTelephonyEvents.size() >= MAX_TELEPHONY_EVENTS) {
+            mTelephonyEvents.removeFirst();
+            mTelephonyEventsDropped = true;
+        }
+        mTelephonyEvents.add(event);
+    }
+
+    /**
+     * Write service changed event
+     *
+     * @param phoneId Phone id
+     * @param serviceState Service state
+     */
+    public synchronized void writeServiceStateChanged(int phoneId, ServiceState serviceState) {
+
+        TelephonyEvent event = new TelephonyEventBuilder(phoneId)
+                .setServiceState(toServiceStateProto(serviceState)).build();
+
+        // If service state doesn't change, we don't log the event.
+        if (mLastServiceState.get(phoneId) != null &&
+                Arrays.equals(TelephonyServiceState.toByteArray(mLastServiceState.get(phoneId)),
+                        TelephonyServiceState.toByteArray(event.serviceState))) {
+            return;
+        }
+
+        mLastServiceState.put(phoneId, event.serviceState);
+        addTelephonyEvent(event);
+
+        annotateInProgressCallSession(event.timestampMillis, phoneId,
+                new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
+                        .setServiceState(event.serviceState));
+        annotateInProgressSmsSession(event.timestampMillis, phoneId,
+                new SmsSessionEventBuilder(
+                        SmsSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
+                        .setServiceState(event.serviceState));
+    }
+
+    /**
+     * Write data stall event
+     *
+     * @param phoneId Phone id
+     * @param recoveryAction Data stall recovery action
+     */
+    public void writeDataStallEvent(int phoneId, int recoveryAction) {
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
+                .setDataStallRecoveryAction(recoveryAction).build());
+    }
+
+    /**
+     * Write IMS feature settings changed event
+     *
+     * @param phoneId Phone id
+     * @param feature IMS feature
+     * @param network The IMS network type
+     * @param value The settings. 0 indicates disabled, otherwise enabled.
+     * @param status IMS operation status. See OperationStatusConstants for details.
+     */
+    public void writeImsSetFeatureValue(int phoneId, int feature, int network, int value,
+                                        int status) {
+        TelephonySettings s = new TelephonySettings();
+        switch (feature) {
+            case ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE:
+                s.isEnhanced4GLteModeEnabled = (value != 0);
+                break;
+            case ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_WIFI:
+                s.isWifiCallingEnabled = (value != 0);
+                break;
+            case ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_LTE:
+                s.isVtOverLteEnabled = (value != 0);
+                break;
+            case ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_WIFI:
+                s.isVtOverWifiEnabled = (value != 0);
+                break;
+        }
+
+        // If the settings don't change, we don't log the event.
+        if (mLastSettings.get(phoneId) != null &&
+                Arrays.equals(TelephonySettings.toByteArray(mLastSettings.get(phoneId)),
+                        TelephonySettings.toByteArray(s))) {
+            return;
+        }
+
+        mLastSettings.put(phoneId, s);
+
+        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setSettings(s).build();
+        addTelephonyEvent(event);
+
+        annotateInProgressCallSession(event.timestampMillis, phoneId,
+                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.SETTINGS_CHANGED)
+                        .setSettings(s));
+        annotateInProgressSmsSession(event.timestampMillis, phoneId,
+                new SmsSessionEventBuilder(SmsSession.Event.Type.SETTINGS_CHANGED)
+                        .setSettings(s));
+    }
+
+    /**
+     * Write the preferred network settings changed event
+     *
+     * @param phoneId Phone id
+     * @param networkType The preferred network
+     */
+    public void writeSetPreferredNetworkType(int phoneId, int networkType) {
+        TelephonySettings s = new TelephonySettings();
+        s.preferredNetworkMode = networkType + 1;
+
+        // If the settings don't change, we don't log the event.
+        if (mLastSettings.get(phoneId) != null &&
+                Arrays.equals(TelephonySettings.toByteArray(mLastSettings.get(phoneId)),
+                        TelephonySettings.toByteArray(s))) {
+            return;
+        }
+
+        mLastSettings.put(phoneId, s);
+
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setSettings(s).build());
+    }
+
+    /**
+     * Write the IMS connection state changed event
+     *
+     * @param phoneId Phone id
+     * @param state IMS connection state
+     * @param reasonInfo The reason info. Only used for disconnected state.
+     */
+    public synchronized void writeOnImsConnectionState(int phoneId, int state,
+                                                       ImsReasonInfo reasonInfo) {
+        ImsConnectionState imsState = new ImsConnectionState();
+        imsState.state = state;
+
+        if (reasonInfo != null) {
+            TelephonyProto.ImsReasonInfo ri = new TelephonyProto.ImsReasonInfo();
+
+            ri.reasonCode = reasonInfo.getCode();
+            ri.extraCode = reasonInfo.getExtraCode();
+            String extraMessage = reasonInfo.getExtraMessage();
+            if (extraMessage != null) {
+                ri.extraMessage = extraMessage;
+            }
+
+            imsState.reasonInfo = ri;
+        }
+
+        // If the connection state does not change, do not log it.
+        if (mLastImsConnectionState.get(phoneId) != null &&
+                Arrays.equals(ImsConnectionState.toByteArray(mLastImsConnectionState.get(phoneId)),
+                        ImsConnectionState.toByteArray(imsState))) {
+            return;
+        }
+
+        mLastImsConnectionState.put(phoneId, imsState);
+
+        TelephonyEvent event = new TelephonyEventBuilder(phoneId)
+                .setImsConnectionState(imsState).build();
+        addTelephonyEvent(event);
+
+        annotateInProgressCallSession(event.timestampMillis, phoneId,
+                new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
+                        .setImsConnectionState(event.imsConnectionState));
+        annotateInProgressSmsSession(event.timestampMillis, phoneId,
+                new SmsSessionEventBuilder(
+                        SmsSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
+                        .setImsConnectionState(event.imsConnectionState));
+    }
+
+    /**
+     * Write the IMS capabilities changed event
+     *
+     * @param phoneId Phone id
+     * @param capabilities IMS capabilities array
+     */
+    public synchronized void writeOnImsCapabilities(int phoneId, boolean[] capabilities) {
+        ImsCapabilities cap = new ImsCapabilities();
+
+        cap.voiceOverLte = capabilities[0];
+        cap.videoOverLte = capabilities[1];
+        cap.voiceOverWifi = capabilities[2];
+        cap.videoOverWifi = capabilities[3];
+        cap.utOverLte = capabilities[4];
+        cap.utOverWifi = capabilities[5];
+
+        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setImsCapabilities(cap).build();
+
+        // If the capabilities don't change, we don't log the event.
+        if (mLastImsCapabilities.get(phoneId) != null &&
+                Arrays.equals(ImsCapabilities.toByteArray(mLastImsCapabilities.get(phoneId)),
+                ImsCapabilities.toByteArray(cap))) {
+            return;
+        }
+
+        mLastImsCapabilities.put(phoneId, cap);
+        addTelephonyEvent(event);
+
+        annotateInProgressCallSession(event.timestampMillis, phoneId,
+                new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.IMS_CAPABILITIES_CHANGED)
+                        .setImsCapabilities(event.imsCapabilities));
+        annotateInProgressSmsSession(event.timestampMillis, phoneId,
+                new SmsSessionEventBuilder(
+                        SmsSession.Event.Type.IMS_CAPABILITIES_CHANGED)
+                        .setImsCapabilities(event.imsCapabilities));
+    }
+
+    /**
+     * Convert PDP type into the enumeration
+     *
+     * @param type PDP type
+     * @return The proto defined enumeration
+     */
+    private int toPdpType(String type) {
+        switch (type) {
+            case "IP":
+                return PDP_TYPE_IP;
+            case "IPV6":
+                return PDP_TYPE_IPV6;
+            case "IPV4V6":
+                return PDP_TYPE_IPV4V6;
+            case "PPP":
+                return PDP_TYPE_PPP;
+        }
+        Rlog.e(TAG, "Unknown type: " + type);
+        return PDP_UNKNOWN;
+    }
+
+    /**
+     * Write setup data call event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param radioTechnology The data call RAT
+     * @param profile Data profile
+     * @param apn APN in string
+     * @param authType Authentication type
+     * @param protocol Data connection protocol
+     */
+    public void writeRilSetupDataCall(int phoneId, int rilSerial, int radioTechnology, int profile,
+                                      String apn, int authType, String protocol) {
+
+        RilSetupDataCall setupDataCall = new RilSetupDataCall();
+        setupDataCall.rat = radioTechnology;
+        setupDataCall.dataProfile = profile + 1;  // off by 1 between proto and RIL constants.
+        if (apn != null) {
+            setupDataCall.apn = apn;
+        }
+        if (protocol != null) {
+            setupDataCall.type = toPdpType(protocol);
+        }
+
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setSetupDataCall(
+                setupDataCall).build());
+    }
+
+    /**
+     * Write data call deactivate event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param cid call id
+     * @param reason Deactivate reason
+     */
+    public void writeRilDeactivateDataCall(int phoneId, int rilSerial, int cid, int reason) {
+
+        RilDeactivateDataCall deactivateDataCall = new RilDeactivateDataCall();
+        deactivateDataCall.cid = cid;
+        deactivateDataCall.reason = reason + 1;
+
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setDeactivateDataCall(
+                deactivateDataCall).build());
+    }
+
+    /**
+     * Write get data call list event
+     *
+     * @param phoneId Phone id
+     * @param dcsList Data call list
+     */
+    public void writeRilDataCallList(int phoneId, ArrayList<DataCallResponse> dcsList) {
+
+        RilDataCall[] dataCalls = new RilDataCall[dcsList.size()];
+
+        for (int i = 0; i < dcsList.size(); i++) {
+            dataCalls[i] = new RilDataCall();
+            dataCalls[i].cid = dcsList.get(i).cid;
+            if (!TextUtils.isEmpty(dcsList.get(i).ifname)) {
+                dataCalls[i].iframe = dcsList.get(i).ifname;
+            }
+            if (!TextUtils.isEmpty(dcsList.get(i).type)) {
+                dataCalls[i].type = toPdpType(dcsList.get(i).type);
+            }
+        }
+
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setDataCalls(dataCalls).build());
+    }
+
+    /**
+     * Write CS call list event
+     *
+     * @param phoneId    Phone id
+     * @param connections Array of GsmCdmaConnection objects
+     */
+    public void writeRilCallList(int phoneId, ArrayList<GsmCdmaConnection> connections) {
+        if (VDBG) {
+            Rlog.v(TAG, "Logging CallList Changed Connections Size = " + connections.size());
+        }
+        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "writeRilCallList: Call session is missing");
+        } else {
+            RilCall[] calls = convertConnectionsToRilCalls(connections);
+            callSession.addEvent(
+                    new CallSessionEventBuilder(
+                            TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED)
+                            .setRilCalls(calls)
+            );
+            if (VDBG) Rlog.v(TAG, "Logged Call list changed");
+            if (callSession.isPhoneIdle() && disconnectReasonsKnown(calls)) {
+                finishCallSession(callSession);
+            }
+        }
+    }
+
+    private boolean disconnectReasonsKnown(RilCall[] calls) {
+        for (RilCall call : calls) {
+            if (call.callEndReason == 0) return false;
+        }
+        return true;
+    }
+
+    private RilCall[] convertConnectionsToRilCalls(ArrayList<GsmCdmaConnection> mConnections) {
+        RilCall[] calls = new RilCall[mConnections.size()];
+        for (int i = 0; i < mConnections.size(); i++) {
+            calls[i] = new RilCall();
+            calls[i].index = i;
+            convertConnectionToRilCall(mConnections.get(i), calls[i]);
+        }
+        return calls;
+    }
+
+    private void convertConnectionToRilCall(GsmCdmaConnection conn, RilCall call) {
+        if (conn.isIncoming()) {
+            call.type = Type.MT;
+        } else {
+            call.type = Type.MO;
+        }
+        switch (conn.getState()) {
+            case IDLE:
+                call.state = CallState.CALL_IDLE;
+                break;
+            case ACTIVE:
+                call.state = CallState.CALL_ACTIVE;
+                break;
+            case HOLDING:
+                call.state = CallState.CALL_HOLDING;
+                break;
+            case DIALING:
+                call.state = CallState.CALL_DIALING;
+                break;
+            case ALERTING:
+                call.state = CallState.CALL_ALERTING;
+                break;
+            case INCOMING:
+                call.state = CallState.CALL_INCOMING;
+                break;
+            case WAITING:
+                call.state = CallState.CALL_WAITING;
+                break;
+            case DISCONNECTED:
+                call.state = CallState.CALL_DISCONNECTED;
+                break;
+            case DISCONNECTING:
+                call.state = CallState.CALL_DISCONNECTING;
+                break;
+            default:
+                call.state = CallState.CALL_UNKNOWN;
+                break;
+        }
+        call.callEndReason = conn.getDisconnectCause();
+        call.isMultiparty = conn.isMultiparty();
+    }
+
+    /**
+     * Write dial event
+     *
+     * @param phoneId Phone id
+     * @param conn Connection object created to track this call
+     * @param clirMode CLIR (Calling Line Identification Restriction) mode
+     * @param uusInfo User-to-User signaling Info
+     */
+    public void writeRilDial(int phoneId, GsmCdmaConnection conn, int clirMode, UUSInfo uusInfo) {
+
+        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
+        if (VDBG) Rlog.v(TAG, "Logging Dial Connection = " + conn);
+        if (callSession == null) {
+            Rlog.e(TAG, "writeRilDial: Call session is missing");
+        } else {
+            RilCall[] calls = new RilCall[1];
+            calls[0] = new RilCall();
+            calls[0].index = -1;
+            convertConnectionToRilCall(conn, calls[0]);
+            callSession.addEvent(callSession.startElapsedTimeMs,
+                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_REQUEST)
+                            .setRilRequest(TelephonyCallSession.Event.RilRequest.RIL_REQUEST_DIAL)
+                            .setRilCalls(calls));
+            if (VDBG) Rlog.v(TAG, "Logged Dial event");
+        }
+    }
+
+    /**
+     * Write incoming call event
+     *
+     * @param phoneId Phone id
+     * @param response Unused today
+     */
+    public void writeRilCallRing(int phoneId, char[] response) {
+        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
+
+        callSession.addEvent(callSession.startElapsedTimeMs,
+                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_CALL_RING));
+    }
+
+    /**
+     * Write call hangup event
+     *
+     * @param phoneId Phone id
+     * @param conn Connection object associated with the call that is being hung-up
+     * @param callId Call id
+     */
+    public void writeRilHangup(int phoneId, GsmCdmaConnection conn, int callId) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "writeRilHangup: Call session is missing");
+        } else {
+            RilCall[] calls = new RilCall[1];
+            calls[0] = new RilCall();
+            calls[0].index = callId;
+            convertConnectionToRilCall(conn, calls[0]);
+            callSession.addEvent(
+                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_REQUEST)
+                            .setRilRequest(TelephonyCallSession.Event.RilRequest.RIL_REQUEST_HANGUP)
+                            .setRilCalls(calls));
+            if (VDBG) Rlog.v(TAG, "Logged Hangup event");
+        }
+    }
+
+    /**
+     * Write call answer event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     */
+    public void writeRilAnswer(int phoneId, int rilSerial) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "writeRilAnswer: Call session is missing");
+        } else {
+            callSession.addEvent(
+                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_REQUEST)
+                            .setRilRequest(TelephonyCallSession.Event.RilRequest.RIL_REQUEST_ANSWER)
+                            .setRilRequestId(rilSerial));
+        }
+    }
+
+    /**
+     * Write IMS call SRVCC event
+     *
+     * @param phoneId Phone id
+     * @param rilSrvccState SRVCC state
+     */
+    public void writeRilSrvcc(int phoneId, int rilSrvccState) {
+        InProgressCallSession callSession =  mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "writeRilSrvcc: Call session is missing");
+        } else {
+            callSession.addEvent(
+                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_CALL_SRVCC)
+                            .setSrvccState(rilSrvccState + 1));
+        }
+    }
+
+    /**
+     * Convert RIL request into proto defined RIL request
+     *
+     * @param r RIL request
+     * @return RIL request defined in call session proto
+     */
+    private int toCallSessionRilRequest(int r) {
+        switch (r) {
+            case RILConstants.RIL_REQUEST_DIAL:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_DIAL;
+
+            case RILConstants.RIL_REQUEST_ANSWER:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_ANSWER;
+
+            case RILConstants.RIL_REQUEST_HANGUP:
+            case RILConstants.RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND:
+            case RILConstants.RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_HANGUP;
+
+            case RILConstants.RIL_REQUEST_SET_CALL_WAITING:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_SET_CALL_WAITING;
+
+            case RILConstants.RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_SWITCH_HOLDING_AND_ACTIVE;
+
+            case RILConstants.RIL_REQUEST_CDMA_FLASH:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_CDMA_FLASH;
+
+            case RILConstants.RIL_REQUEST_CONFERENCE:
+                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_CONFERENCE;
+        }
+        Rlog.e(TAG, "Unknown RIL request: " + r);
+        return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_UNKNOWN;
+    }
+
+    /**
+     * Write setup data call response event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param rilError RIL error
+     * @param rilRequest RIL request
+     * @param response Data call response
+     */
+    private void writeOnSetupDataCallResponse(int phoneId, int rilSerial, int rilError,
+                                              int rilRequest, DataCallResponse response) {
+
+        RilSetupDataCallResponse setupDataCallResponse = new RilSetupDataCallResponse();
+        RilDataCall dataCall = new RilDataCall();
+
+        if (response != null) {
+            setupDataCallResponse.status =
+                    (response.status == 0 ? RilDataCallFailCause.PDP_FAIL_NONE : response.status);
+            setupDataCallResponse.suggestedRetryTimeMillis = response.suggestedRetryTime;
+
+            dataCall.cid = response.cid;
+            if (!TextUtils.isEmpty(response.type)) {
+                dataCall.type = toPdpType(response.type);
+            }
+
+            if (!TextUtils.isEmpty(response.ifname)) {
+                dataCall.iframe = response.ifname;
+            }
+        }
+        setupDataCallResponse.call = dataCall;
+
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
+                .setSetupDataCallResponse(setupDataCallResponse).build());
+    }
+
+    /**
+     * Write call related solicited response event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param rilError RIL error
+     * @param rilRequest RIL request
+     */
+    private void writeOnCallSolicitedResponse(int phoneId, int rilSerial, int rilError,
+                                              int rilRequest) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "writeOnCallSolicitedResponse: Call session is missing");
+        } else {
+            callSession.addEvent(new CallSessionEventBuilder(
+                    TelephonyCallSession.Event.Type.RIL_RESPONSE)
+                    .setRilRequest(toCallSessionRilRequest(rilRequest))
+                    .setRilRequestId(rilSerial)
+                    .setRilError(rilError + 1));
+        }
+    }
+
+    /**
+     * Write SMS related solicited response event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param rilError RIL error
+     * @param response SMS response
+     */
+    private synchronized void writeOnSmsSolicitedResponse(int phoneId, int rilSerial, int rilError,
+                                                          SmsResponse response) {
+
+        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
+        if (smsSession == null) {
+            Rlog.e(TAG, "SMS session is missing");
+        } else {
+
+            int errorCode = 0;
+            if (response != null) {
+                errorCode = response.mErrorCode;
+            }
+
+            smsSession.addEvent(new SmsSessionEventBuilder(
+                    SmsSession.Event.Type.SMS_SEND_RESULT)
+                    .setErrorCode(errorCode)
+                    .setRilErrno(rilError + 1)
+                    .setRilRequestId(rilSerial)
+            );
+
+            smsSession.decreaseExpectedResponse();
+            finishSmsSessionIfNeeded(smsSession);
+        }
+    }
+
+    /**
+     * Write deactivate data call response event
+     *
+     * @param phoneId Phone id
+     * @param rilError RIL error
+     */
+    private void writeOnDeactivateDataCallResponse(int phoneId, int rilError) {
+        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
+                .setDeactivateDataCallResponse(rilError + 1).build());
+    }
+
+    /**
+     * Write RIL solicited response event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param rilError RIL error
+     * @param rilRequest RIL request
+     * @param ret The returned RIL response
+     */
+    public void writeOnRilSolicitedResponse(int phoneId, int rilSerial, int rilError,
+                                            int rilRequest, Object ret) {
+        switch (rilRequest) {
+            case RIL_REQUEST_SETUP_DATA_CALL:
+                DataCallResponse dataCall = (DataCallResponse) ret;
+                writeOnSetupDataCallResponse(phoneId, rilSerial, rilError, rilRequest, dataCall);
+                break;
+            case RIL_REQUEST_DEACTIVATE_DATA_CALL:
+                writeOnDeactivateDataCallResponse(phoneId, rilError);
+                break;
+            case RIL_REQUEST_HANGUP:
+            case RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND:
+            case RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND:
+            case RIL_REQUEST_DIAL:
+            case RIL_REQUEST_ANSWER:
+                writeOnCallSolicitedResponse(phoneId, rilSerial, rilError, rilRequest);
+                break;
+            case RIL_REQUEST_SEND_SMS:
+            case RIL_REQUEST_SEND_SMS_EXPECT_MORE:
+            case RIL_REQUEST_CDMA_SEND_SMS:
+            case RIL_REQUEST_IMS_SEND_SMS:
+                SmsResponse smsResponse = (SmsResponse) ret;
+                writeOnSmsSolicitedResponse(phoneId, rilSerial, rilError, smsResponse);
+                break;
+        }
+    }
+
+    /**
+     * Write phone state changed event
+     *
+     * @param phoneId Phone id
+     * @param phoneState Phone state. See PhoneConstants.State for the details.
+     */
+    public void writePhoneState(int phoneId, PhoneConstants.State phoneState) {
+        int state;
+        switch (phoneState) {
+            case IDLE:
+                state = TelephonyCallSession.Event.PhoneState.STATE_IDLE;
+                break;
+            case RINGING:
+                state = TelephonyCallSession.Event.PhoneState.STATE_RINGING;
+                break;
+            case OFFHOOK:
+                state = TelephonyCallSession.Event.PhoneState.STATE_OFFHOOK;
+                break;
+            default:
+                state = TelephonyCallSession.Event.PhoneState.STATE_UNKNOWN;
+                break;
+        }
+
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "writePhoneState: Call session is missing");
+        } else {
+            // For CS Calls Finish the Call Session after Receiving the Last Call Fail Cause
+            // For IMS calls we receive the Disconnect Cause along with Call End event.
+            // So we can finish the call session here.
+            callSession.setLastKnownPhoneState(state);
+            if ((state == TelephonyCallSession.Event.PhoneState.STATE_IDLE)
+                    && (!callSession.containsCsCalls())) {
+                finishCallSession(callSession);
+            }
+            callSession.addEvent(new CallSessionEventBuilder(
+                    TelephonyCallSession.Event.Type.PHONE_STATE_CHANGED)
+                    .setPhoneState(state));
+        }
+    }
+
+    /**
+     * Extracts the call ID from an ImsSession.
+     *
+     * @param session The session.
+     * @return The call ID for the session, or -1 if none was found.
+     */
+    private int getCallId(ImsCallSession session) {
+        if (session == null) {
+            return -1;
+        }
+
+        try {
+            return Integer.parseInt(session.getCallId());
+        } catch (NumberFormatException nfe) {
+            return -1;
+        }
+    }
+
+    /**
+     * Write IMS call state changed event
+     *
+     * @param phoneId Phone id
+     * @param session IMS call session
+     * @param callState IMS call state
+     */
+    public void writeImsCallState(int phoneId, ImsCallSession session,
+                                  ImsPhoneCall.State callState) {
+        int state;
+        switch (callState) {
+            case IDLE:
+                state = TelephonyCallSession.Event.CallState.CALL_IDLE; break;
+            case ACTIVE:
+                state = TelephonyCallSession.Event.CallState.CALL_ACTIVE; break;
+            case HOLDING:
+                state = TelephonyCallSession.Event.CallState.CALL_HOLDING; break;
+            case DIALING:
+                state = TelephonyCallSession.Event.CallState.CALL_DIALING; break;
+            case ALERTING:
+                state = TelephonyCallSession.Event.CallState.CALL_ALERTING; break;
+            case INCOMING:
+                state = TelephonyCallSession.Event.CallState.CALL_INCOMING; break;
+            case WAITING:
+                state = TelephonyCallSession.Event.CallState.CALL_WAITING; break;
+            case DISCONNECTED:
+                state = TelephonyCallSession.Event.CallState.CALL_DISCONNECTED; break;
+            case DISCONNECTING:
+                state = TelephonyCallSession.Event.CallState.CALL_DISCONNECTING; break;
+            default:
+                state = TelephonyCallSession.Event.CallState.CALL_UNKNOWN; break;
+        }
+
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "Call session is missing");
+        } else {
+            callSession.addEvent(new CallSessionEventBuilder(
+                    TelephonyCallSession.Event.Type.IMS_CALL_STATE_CHANGED)
+                    .setCallIndex(getCallId(session))
+                    .setCallState(state));
+        }
+    }
+
+    /**
+     * Write IMS call start event
+     *
+     * @param phoneId Phone id
+     * @param session IMS call session
+     */
+    public void writeOnImsCallStart(int phoneId, ImsCallSession session) {
+        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
+
+        callSession.addEvent(
+                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_COMMAND)
+                        .setCallIndex(getCallId(session))
+                        .setImsCommand(TelephonyCallSession.Event.ImsCommand.IMS_CMD_START));
+    }
+
+    /**
+     * Write IMS incoming call event
+     *
+     * @param phoneId Phone id
+     * @param session IMS call session
+     */
+    public void writeOnImsCallReceive(int phoneId, ImsCallSession session) {
+        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
+
+        callSession.addEvent(
+                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_CALL_RECEIVE)
+                        .setCallIndex(getCallId(session)));
+    }
+
+    /**
+     * Write IMS command event
+     *
+     * @param phoneId Phone id
+     * @param session IMS call session
+     * @param command IMS command
+     */
+    public void writeOnImsCommand(int phoneId, ImsCallSession session, int command) {
+
+        InProgressCallSession callSession =  mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "Call session is missing");
+        } else {
+            callSession.addEvent(
+                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_COMMAND)
+                            .setCallIndex(getCallId(session))
+                            .setImsCommand(command));
+        }
+    }
+
+    /**
+     * Convert IMS reason info into proto
+     *
+     * @param reasonInfo IMS reason info
+     * @return Converted proto
+     */
+    private TelephonyProto.ImsReasonInfo toImsReasonInfoProto(ImsReasonInfo reasonInfo) {
+        TelephonyProto.ImsReasonInfo ri = new TelephonyProto.ImsReasonInfo();
+        if (reasonInfo != null) {
+            ri.reasonCode = reasonInfo.getCode();
+            ri.extraCode = reasonInfo.getExtraCode();
+            String extraMessage = reasonInfo.getExtraMessage();
+            if (extraMessage != null) {
+                ri.extraMessage = extraMessage;
+            }
+        }
+        return ri;
+    }
+
+    /**
+     * Write IMS call end event
+     *
+     * @param phoneId Phone id
+     * @param session IMS call session
+     * @param reasonInfo Call end reason
+     */
+    public void writeOnImsCallTerminated(int phoneId, ImsCallSession session,
+                                         ImsReasonInfo reasonInfo) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "Call session is missing");
+        } else {
+            callSession.addEvent(
+                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED)
+                            .setCallIndex(getCallId(session))
+                            .setImsReasonInfo(toImsReasonInfoProto(reasonInfo)));
+        }
+    }
+
+    /**
+     * Write IMS call hangover event
+     *
+     * @param phoneId Phone id
+     * @param eventType hangover type
+     * @param session IMS call session
+     * @param srcAccessTech Hangover starting RAT
+     * @param targetAccessTech Hangover destination RAT
+     * @param reasonInfo Hangover reason
+     */
+    public void writeOnImsCallHandoverEvent(int phoneId, int eventType, ImsCallSession session,
+                                            int srcAccessTech, int targetAccessTech,
+                                            ImsReasonInfo reasonInfo) {
+        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
+        if (callSession == null) {
+            Rlog.e(TAG, "Call session is missing");
+        } else {
+            callSession.addEvent(
+                    new CallSessionEventBuilder(eventType)
+                            .setCallIndex(getCallId(session))
+                            .setSrcAccessTech(srcAccessTech)
+                            .setTargetAccessTech(targetAccessTech)
+                            .setImsReasonInfo(toImsReasonInfoProto(reasonInfo)));
+        }
+    }
+
+    /**
+     * Write Send SMS event
+     *
+     * @param phoneId Phone id
+     * @param rilSerial RIL request serial number
+     * @param tech SMS RAT
+     * @param format SMS format. Either 3GPP or 3GPP2.
+     */
+    public synchronized void writeRilSendSms(int phoneId, int rilSerial, int tech, int format) {
+        InProgressSmsSession smsSession = startNewSmsSessionIfNeeded(phoneId);
+
+        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_SEND)
+                .setTech(tech)
+                .setRilRequestId(rilSerial)
+                .setFormat(format)
+        );
+
+        smsSession.increaseExpectedResponse();
+    }
+
+    /**
+     * Write incoming SMS event
+     *
+     * @param phoneId Phone id
+     * @param tech SMS RAT
+     * @param format SMS format. Either 3GPP or 3GPP2.
+     */
+    public synchronized void writeRilNewSms(int phoneId, int tech, int format) {
+        InProgressSmsSession smsSession = startNewSmsSessionIfNeeded(phoneId);
+
+        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_RECEIVED)
+                .setTech(tech)
+                .setFormat(format)
+        );
+
+        finishSmsSessionIfNeeded(smsSession);
+    }
+
+    /**
+     * Write incoming Broadcast SMS event
+     *
+     * @param phoneId Phone id
+     * @param format CB msg format
+     * @param priority CB msg priority
+     * @param isCMAS true if msg is CMAS
+     * @param isETWS true if msg is ETWS
+     * @param serviceCategory Service category of CB msg
+     */
+    public synchronized void writeNewCBSms(int phoneId, int format, int priority, boolean isCMAS,
+                                           boolean isETWS, int serviceCategory) {
+        InProgressSmsSession smsSession = startNewSmsSessionIfNeeded(phoneId);
+
+        int type;
+        if (isCMAS) {
+            type = SmsSession.Event.CBMessageType.CMAS;
+        } else if (isETWS) {
+            type = SmsSession.Event.CBMessageType.ETWS;
+        } else {
+            type = SmsSession.Event.CBMessageType.OTHER;
+        }
+
+        SmsSession.Event.CBMessage cbm = new SmsSession.Event.CBMessage();
+        cbm.msgFormat = format;
+        cbm.msgPriority = priority + 1;
+        cbm.msgType = type;
+        cbm.serviceCategory = serviceCategory;
+
+        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.CB_SMS_RECEIVED)
+                .setCellBroadcastMessage(cbm)
+        );
+
+        finishSmsSessionIfNeeded(smsSession);
+    }
+
+    /**
+     * Write NITZ event
+     *
+     * @param phoneId Phone id
+     * @param timestamp NITZ time in milliseconds
+     */
+    public void writeNITZEvent(int phoneId, long timestamp) {
+        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setNITZ(timestamp).build();
+        addTelephonyEvent(event);
+
+        annotateInProgressCallSession(event.timestampMillis, phoneId,
+                new CallSessionEventBuilder(
+                        TelephonyCallSession.Event.Type.NITZ_TIME)
+                        .setNITZ(timestamp));
+    }
+
+    /**
+     * Write Modem Restart event
+     *
+     * @param phoneId Phone id
+     * @param reason Reason for the modem reset.
+     */
+    public void writeModemRestartEvent(int phoneId, String reason) {
+        final ModemRestart modemRestart = new ModemRestart();
+        String basebandVersion = Build.getRadioVersion();
+        if (basebandVersion != null) modemRestart.basebandVersion = basebandVersion;
+        if (reason != null) modemRestart.reason = reason;
+        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setModemRestart(
+                modemRestart).build();
+        addTelephonyEvent(event);
+    }
+
+    //TODO: Expand the proto in the future
+    public void writeOnImsCallProgressing(int phoneId, ImsCallSession session) {}
+    public void writeOnImsCallStarted(int phoneId, ImsCallSession session) {}
+    public void writeOnImsCallStartFailed(int phoneId, ImsCallSession session,
+                                          ImsReasonInfo reasonInfo) {}
+    public void writeOnImsCallHeld(int phoneId, ImsCallSession session) {}
+    public void writeOnImsCallHoldReceived(int phoneId, ImsCallSession session) {}
+    public void writeOnImsCallHoldFailed(int phoneId, ImsCallSession session,
+                                         ImsReasonInfo reasonInfo) {}
+    public void writeOnImsCallResumed(int phoneId, ImsCallSession session) {}
+    public void writeOnImsCallResumeReceived(int phoneId, ImsCallSession session) {}
+    public void writeOnImsCallResumeFailed(int phoneId, ImsCallSession session,
+                                           ImsReasonInfo reasonInfo) {}
+    public void writeOnRilTimeoutResponse(int phoneId, int rilSerial, int rilRequest) {}
+}
diff --git a/com/android/internal/telephony/sip/SipCallBase.java b/com/android/internal/telephony/sip/SipCallBase.java
new file mode 100644
index 0000000..395692a
--- /dev/null
+++ b/com/android/internal/telephony/sip/SipCallBase.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.sip;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import java.util.Iterator;
+import java.util.List;
+
+abstract class SipCallBase extends Call {
+
+    @Override
+    public List<Connection> getConnections() {
+        // FIXME should return Collections.unmodifiableList();
+        return mConnections;
+    }
+
+    @Override
+    public boolean isMultiparty() {
+        return mConnections.size() > 1;
+    }
+
+    @Override
+    public String toString() {
+        return mState.toString() + ":" + super.toString();
+    }
+}
diff --git a/com/android/internal/telephony/sip/SipCommandInterface.java b/com/android/internal/telephony/sip/SipCommandInterface.java
new file mode 100644
index 0000000..fe1d7c5
--- /dev/null
+++ b/com/android/internal/telephony/sip/SipCommandInterface.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.sip;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+
+import com.android.internal.telephony.BaseCommands;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.dataconnection.DataProfile;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+
+import java.util.List;
+
+/**
+ * SIP doesn't need CommandsInterface. The class does nothing but made to work
+ * with Phone's constructor.
+ */
+class SipCommandInterface extends BaseCommands implements CommandsInterface {
+    SipCommandInterface(Context context) {
+        super(context);
+    }
+
+    @Override public void setOnNITZTime(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void getIccCardStatus(Message result) {
+    }
+
+    @Override
+    public void supplyIccPin(String pin, Message result) {
+    }
+
+    @Override
+    public void supplyIccPuk(String puk, String newPin, Message result) {
+    }
+
+    @Override
+    public void supplyIccPin2(String pin, Message result) {
+    }
+
+    @Override
+    public void supplyIccPuk2(String puk, String newPin2, Message result) {
+    }
+
+    @Override
+    public void changeIccPin(String oldPin, String newPin, Message result) {
+    }
+
+    @Override
+    public void changeIccPin2(String oldPin2, String newPin2, Message result) {
+    }
+
+    @Override
+    public void changeBarringPassword(String facility, String oldPwd,
+            String newPwd, Message result) {
+    }
+
+    @Override
+    public void supplyNetworkDepersonalization(String netpin, Message result) {
+    }
+
+    @Override
+    public void getCurrentCalls(Message result) {
+    }
+
+    @Override
+    @Deprecated public void getPDPContextList(Message result) {
+    }
+
+    @Override
+    public void getDataCallList(Message result) {
+    }
+
+    @Override
+    public void dial(String address, int clirMode, Message result) {
+    }
+
+    @Override
+    public void dial(String address, int clirMode, UUSInfo uusInfo,
+            Message result) {
+    }
+
+    @Override
+    public void getIMSI(Message result) {
+    }
+
+    @Override
+    public void getIMSIForApp(String aid, Message result) {
+    }
+
+    @Override
+    public void getIMEI(Message result) {
+    }
+
+    @Override
+    public void getIMEISV(Message result) {
+    }
+
+
+    @Override
+    public void hangupConnection (int gsmIndex, Message result) {
+    }
+
+    @Override
+    public void hangupWaitingOrBackground (Message result) {
+    }
+
+    @Override
+    public void hangupForegroundResumeBackground (Message result) {
+    }
+
+    @Override
+    public void switchWaitingOrHoldingAndActive (Message result) {
+    }
+
+    @Override
+    public void conference (Message result) {
+    }
+
+
+    @Override
+    public void setPreferredVoicePrivacy(boolean enable, Message result) {
+    }
+
+    @Override
+    public void getPreferredVoicePrivacy(Message result) {
+    }
+
+    @Override
+    public void separateConnection (int gsmIndex, Message result) {
+    }
+
+    @Override
+    public void acceptCall (Message result) {
+    }
+
+    @Override
+    public void rejectCall (Message result) {
+    }
+
+    @Override
+    public void explicitCallTransfer (Message result) {
+    }
+
+    @Override
+    public void getLastCallFailCause (Message result) {
+    }
+
+    @Deprecated
+    @Override
+    public void getLastPdpFailCause (Message result) {
+    }
+
+    @Override
+    public void getLastDataCallFailCause (Message result) {
+    }
+
+    @Override
+    public void setMute (boolean enableMute, Message response) {
+    }
+
+    @Override
+    public void getMute (Message response) {
+    }
+
+    @Override
+    public void getSignalStrength (Message result) {
+    }
+
+    @Override
+    public void getVoiceRegistrationState (Message result) {
+    }
+
+    @Override
+    public void getDataRegistrationState (Message result) {
+    }
+
+    @Override
+    public void getOperator(Message result) {
+    }
+
+    @Override
+    public void sendDtmf(char c, Message result) {
+    }
+
+    @Override
+    public void startDtmf(char c, Message result) {
+    }
+
+    @Override
+    public void stopDtmf(Message result) {
+    }
+
+    @Override
+    public void sendBurstDtmf(String dtmfString, int on, int off,
+            Message result) {
+    }
+
+    @Override
+    public void sendSMS (String smscPDU, String pdu, Message result) {
+    }
+
+    @Override
+    public void sendSMSExpectMore (String smscPDU, String pdu, Message result) {
+    }
+
+    @Override
+    public void sendCdmaSms(byte[] pdu, Message result) {
+    }
+
+    @Override
+    public void sendImsGsmSms (String smscPDU, String pdu,
+            int retry, int messageRef, Message response) {
+    }
+
+    @Override
+    public void sendImsCdmaSms(byte[] pdu, int retry, int messageRef,
+            Message response) {
+    }
+
+    @Override
+    public void getImsRegistrationState (Message result) {
+    }
+
+    @Override
+    public void deleteSmsOnSim(int index, Message response) {
+    }
+
+    @Override
+    public void deleteSmsOnRuim(int index, Message response) {
+    }
+
+    @Override
+    public void writeSmsToSim(int status, String smsc, String pdu, Message response) {
+    }
+
+    @Override
+    public void writeSmsToRuim(int status, String pdu, Message response) {
+    }
+
+    @Override
+    public void setupDataCall(int radioTechnology, DataProfile dataProfile, boolean isRoaming,
+                              boolean allowRoaming, Message result) {
+    }
+
+    @Override
+    public void deactivateDataCall(int cid, int reason, Message result) {
+    }
+
+    @Override
+    public void setRadioPower(boolean on, Message result) {
+    }
+
+    @Override
+    public void setSuppServiceNotifications(boolean enable, Message result) {
+    }
+
+    @Override
+    public void acknowledgeLastIncomingGsmSms(boolean success, int cause,
+            Message result) {
+    }
+
+    @Override
+    public void acknowledgeLastIncomingCdmaSms(boolean success, int cause,
+            Message result) {
+    }
+
+    @Override
+    public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu,
+            Message result) {
+    }
+
+    @Override
+    public void iccIO (int command, int fileid, String path, int p1, int p2,
+            int p3, String data, String pin2, Message result) {
+    }
+    @Override
+    public void iccIOForApp (int command, int fileid, String path, int p1, int p2,
+            int p3, String data, String pin2, String aid, Message result) {
+    }
+
+    @Override
+    public void getCLIR(Message result) {
+    }
+
+    @Override
+    public void setCLIR(int clirMode, Message result) {
+    }
+
+    @Override
+    public void queryCallWaiting(int serviceClass, Message response) {
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, int serviceClass,
+            Message response) {
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message response) {
+    }
+
+    @Override
+    public void setNetworkSelectionModeManual(
+            String operatorNumeric, Message response) {
+    }
+
+    @Override
+    public void getNetworkSelectionMode(Message response) {
+    }
+
+    @Override
+    public void getAvailableNetworks(Message response) {
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message response) {
+    }
+
+    @Override
+    public void stopNetworkScan(Message response) {
+    }
+
+    @Override
+    public void setCallForward(int action, int cfReason, int serviceClass,
+                String number, int timeSeconds, Message response) {
+    }
+
+    @Override
+    public void queryCallForwardStatus(int cfReason, int serviceClass,
+            String number, Message response) {
+    }
+
+    @Override
+    public void queryCLIP(Message response) {
+    }
+
+    @Override
+    public void getBasebandVersion (Message response) {
+    }
+
+    @Override
+    public void queryFacilityLock(String facility, String password,
+            int serviceClass, Message response) {
+    }
+
+    @Override
+    public void queryFacilityLockForApp(String facility, String password,
+            int serviceClass, String appId, Message response) {
+    }
+
+    @Override
+    public void setFacilityLock(String facility, boolean lockState,
+            String password, int serviceClass, Message response) {
+    }
+
+    @Override
+    public void setFacilityLockForApp(String facility, boolean lockState,
+            String password, int serviceClass, String appId, Message response) {
+    }
+
+    @Override
+    public void sendUSSD (String ussdString, Message response) {
+    }
+
+    @Override
+    public void cancelPendingUssd (Message response) {
+    }
+
+    @Override
+    public void resetRadio(Message result) {
+    }
+
+    @Override
+    public void invokeOemRilRequestRaw(byte[] data, Message response) {
+    }
+
+    @Override
+    public void invokeOemRilRequestStrings(String[] strings, Message response) {
+    }
+
+    @Override
+    public void setBandMode (int bandMode, Message response) {
+    }
+
+    @Override
+    public void queryAvailableBandMode (Message response) {
+    }
+
+    @Override
+    public void sendTerminalResponse(String contents, Message response) {
+    }
+
+    @Override
+    public void sendEnvelope(String contents, Message response) {
+    }
+
+    @Override
+    public void sendEnvelopeWithStatus(String contents, Message response) {
+    }
+
+    @Override
+    public void handleCallSetupRequestFromSim(
+            boolean accept, Message response) {
+    }
+
+    @Override
+    public void setPreferredNetworkType(int networkType , Message response) {
+    }
+
+    @Override
+    public void getPreferredNetworkType(Message response) {
+    }
+
+    @Override
+    public void setLocationUpdates(boolean enable, Message response) {
+    }
+
+    @Override
+    public void getSmscAddress(Message result) {
+    }
+
+    @Override
+    public void setSmscAddress(String address, Message result) {
+    }
+
+    @Override
+    public void reportSmsMemoryStatus(boolean available, Message result) {
+    }
+
+    @Override
+    public void reportStkServiceIsRunning(Message result) {
+    }
+
+    @Override
+    public void getCdmaSubscriptionSource(Message response) {
+    }
+
+    @Override
+    public void getGsmBroadcastConfig(Message response) {
+    }
+
+    @Override
+    public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) {
+    }
+
+    @Override
+    public void setGsmBroadcastActivation(boolean activate, Message response) {
+    }
+
+    // ***** Methods for CDMA support
+    @Override
+    public void getDeviceIdentity(Message response) {
+    }
+
+    @Override
+    public void getCDMASubscription(Message response) {
+    }
+
+    @Override
+    public void setPhoneType(int phoneType) { //Set by GsmCdmaPhone
+    }
+
+    @Override
+    public void queryCdmaRoamingPreference(Message response) {
+    }
+
+    @Override
+    public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) {
+    }
+
+    @Override
+    public void setCdmaSubscriptionSource(int cdmaSubscription , Message response) {
+    }
+
+    @Override
+    public void queryTTYMode(Message response) {
+    }
+
+    @Override
+    public void setTTYMode(int ttyMode, Message response) {
+    }
+
+    @Override
+    public void sendCDMAFeatureCode(String FeatureCode, Message response) {
+    }
+
+    @Override
+    public void getCdmaBroadcastConfig(Message response) {
+    }
+
+    @Override
+    public void setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs, Message response) {
+    }
+
+    @Override
+    public void setCdmaBroadcastActivation(boolean activate, Message response) {
+    }
+
+    @Override
+    public void exitEmergencyCallbackMode(Message response) {
+    }
+
+    @Override
+    public void supplyIccPinForApp(String pin, String aid, Message response) {
+    }
+
+    @Override
+    public void supplyIccPukForApp(String puk, String newPin, String aid, Message response) {
+    }
+
+    @Override
+    public void supplyIccPin2ForApp(String pin2, String aid, Message response) {
+    }
+
+    @Override
+    public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message response) {
+    }
+
+    @Override
+    public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message response) {
+    }
+
+    @Override
+    public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr,
+            Message response) {
+    }
+
+    @Override
+    public void requestIsimAuthentication(String nonce, Message response) {
+    }
+
+    @Override
+    public void requestIccSimAuthentication(int authContext, String data, String aid, Message response) {
+    }
+
+    @Override
+    public void getVoiceRadioTechnology(Message result) {
+    }
+
+    @Override
+    public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming, Message result) {
+    }
+
+    @Override
+    public void setDataProfile(DataProfile[] dps, boolean isRoaming, Message result) {
+    }
+
+    @Override
+    public void iccOpenLogicalChannel(String AID, int p2, Message response) {
+    }
+
+    @Override
+    public void iccCloseLogicalChannel(int channel, Message response) {
+    }
+
+    @Override
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction,
+            int p1, int p2, int p3, String data, Message response) {
+    }
+
+    @Override
+    public void iccTransmitApduBasicChannel(int cla, int instruction, int p1, int p2,
+            int p3, String data, Message response) {
+    }
+
+    @Override
+    public void nvReadItem(int itemID, Message response) {
+    }
+
+    @Override
+    public void nvWriteItem(int itemID, String itemValue, Message response) {
+    }
+
+    @Override
+    public void nvWriteCdmaPrl(byte[] preferredRoamingList, Message response) {
+    }
+
+    @Override
+    public void nvResetConfig(int resetType, Message response) {
+    }
+
+    @Override
+    public void getHardwareConfig(Message result) {
+    }
+
+    @Override
+    public void requestShutdown(Message result) {
+    }
+
+    @Override
+    public void startLceService(int reportIntervalMs, boolean pullMode, Message result) {
+    }
+
+    @Override
+    public void stopLceService(Message result) {
+    }
+
+    @Override
+    public void pullLceData(Message result) {
+    }
+
+    @Override
+    public void getModemActivityInfo(Message result) {
+    }
+
+    @Override
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                Message result) {
+    }
+
+    @Override
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message result) {
+    }
+
+    @Override
+    public void getAllowedCarriers(Message result) {
+    }
+
+    @Override
+    public void sendDeviceState(int stateType, boolean state, Message result) {
+    }
+
+    @Override
+    public void setUnsolResponseFilter(int filter, Message result){
+    }
+
+    @Override
+    public void setSimCardPower(int state, Message result) {
+    }
+}
diff --git a/com/android/internal/telephony/sip/SipConnectionBase.java b/com/android/internal/telephony/sip/SipConnectionBase.java
new file mode 100644
index 0000000..acf6d36
--- /dev/null
+++ b/com/android/internal/telephony/sip/SipConnectionBase.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.sip;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.UUSInfo;
+
+import android.os.SystemClock;
+import android.telephony.DisconnectCause;
+import android.telephony.Rlog;
+import android.telephony.PhoneNumberUtils;
+
+abstract class SipConnectionBase extends Connection {
+    private static final String LOG_TAG = "SipConnBase";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    /*
+     * These time/timespan values are based on System.currentTimeMillis(),
+     * i.e., "wall clock" time.
+     */
+    private long mCreateTime;
+    private long mConnectTime;
+    private long mDisconnectTime;
+
+    /*
+     * These time/timespan values are based on SystemClock.elapsedRealTime(),
+     * i.e., time since boot.  They are appropriate for comparison and
+     * calculating deltas.
+     */
+    private long mConnectTimeReal;
+    private long mDuration = -1L;
+    private long mHoldingStartTime;  // The time when the Connection last transitioned
+                            // into HOLDING
+
+    SipConnectionBase(String dialString) {
+        super(PhoneConstants.PHONE_TYPE_SIP);
+        if (DBG) log("SipConnectionBase: ctor dialString=" + SipPhone.hidePii(dialString));
+        mPostDialString = PhoneNumberUtils.extractPostDialPortion(dialString);
+
+        mCreateTime = System.currentTimeMillis();
+    }
+
+    protected void setState(Call.State state) {
+        if (DBG) log("setState: state=" + state);
+        switch (state) {
+            case ACTIVE:
+                if (mConnectTime == 0) {
+                    mConnectTimeReal = SystemClock.elapsedRealtime();
+                    mConnectTime = System.currentTimeMillis();
+                }
+                break;
+            case DISCONNECTED:
+                mDuration = getDurationMillis();
+                mDisconnectTime = System.currentTimeMillis();
+                break;
+            case HOLDING:
+                mHoldingStartTime = SystemClock.elapsedRealtime();
+                break;
+            default:
+                // Ignore
+                break;
+        }
+    }
+
+    @Override
+    public long getCreateTime() {
+        if (VDBG) log("getCreateTime: ret=" + mCreateTime);
+        return mCreateTime;
+    }
+
+    @Override
+    public long getConnectTime() {
+        if (VDBG) log("getConnectTime: ret=" + mConnectTime);
+        return mConnectTime;
+    }
+
+    @Override
+    public long getDisconnectTime() {
+        if (VDBG) log("getDisconnectTime: ret=" + mDisconnectTime);
+        return mDisconnectTime;
+    }
+
+    @Override
+    public long getDurationMillis() {
+        long dur;
+        if (mConnectTimeReal == 0) {
+            dur = 0;
+        } else if (mDuration < 0) {
+            dur = SystemClock.elapsedRealtime() - mConnectTimeReal;
+        } else {
+            dur = mDuration;
+        }
+        if (VDBG) log("getDurationMillis: ret=" + dur);
+        return dur;
+    }
+
+    @Override
+    public long getHoldDurationMillis() {
+        long dur;
+        if (getState() != Call.State.HOLDING) {
+            // If not holding, return 0
+            dur = 0;
+        } else {
+            dur = SystemClock.elapsedRealtime() - mHoldingStartTime;
+        }
+        if (VDBG) log("getHoldDurationMillis: ret=" + dur);
+        return dur;
+    }
+
+    void setDisconnectCause(int cause) {
+        if (DBG) log("setDisconnectCause: prev=" + mCause + " new=" + cause);
+        mCause = cause;
+    }
+
+    @Override
+    public String getVendorDisconnectCause() {
+      return null;
+    }
+
+    @Override
+    public void proceedAfterWaitChar() {
+        if (DBG) log("proceedAfterWaitChar: ignore");
+    }
+
+    @Override
+    public void proceedAfterWildChar(String str) {
+        if (DBG) log("proceedAfterWildChar: ignore");
+    }
+
+    @Override
+    public void cancelPostDial() {
+        if (DBG) log("cancelPostDial: ignore");
+    }
+
+    protected abstract Phone getPhone();
+
+    private void log(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    @Override
+    public int getNumberPresentation() {
+        // TODO: add PRESENTATION_URL
+        if (VDBG) log("getNumberPresentation: ret=PRESENTATION_ALLOWED");
+        return PhoneConstants.PRESENTATION_ALLOWED;
+    }
+
+    @Override
+    public UUSInfo getUUSInfo() {
+        // FIXME: what's this for SIP?
+        if (VDBG) log("getUUSInfo: ? ret=null");
+        return null;
+    }
+
+    @Override
+    public int getPreciseDisconnectCause() {
+        return 0;
+    }
+
+    @Override
+    public long getHoldingStartTime() {
+        return mHoldingStartTime;
+    }
+
+    @Override
+    public long getConnectTimeReal() {
+        return mConnectTimeReal;
+    }
+
+    @Override
+    public Connection getOrigConnection() {
+        return null;
+    }
+
+    @Override
+    public boolean isMultiparty() {
+        return false;
+    }
+}
diff --git a/com/android/internal/telephony/sip/SipPhone.java b/com/android/internal/telephony/sip/SipPhone.java
new file mode 100644
index 0000000..9a4df0c
--- /dev/null
+++ b/com/android/internal/telephony/sip/SipPhone.java
@@ -0,0 +1,1084 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.sip;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.rtp.AudioGroup;
+import android.net.sip.SipAudioCall;
+import android.net.sip.SipErrorCode;
+import android.net.sip.SipException;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSession;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneNotifier;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ */
+public class SipPhone extends SipPhoneBase {
+    private static final String LOG_TAG = "SipPhone";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+    private static final int TIMEOUT_MAKE_CALL = 15; // in seconds
+    private static final int TIMEOUT_ANSWER_CALL = 8; // in seconds
+    private static final int TIMEOUT_HOLD_CALL = 15; // in seconds
+    // Minimum time needed between hold/unhold requests.
+    private static final long TIMEOUT_HOLD_PROCESSING = 1000; // ms
+
+    // A call that is ringing or (call) waiting
+    private SipCall mRingingCall = new SipCall();
+    private SipCall mForegroundCall = new SipCall();
+    private SipCall mBackgroundCall = new SipCall();
+
+    private SipManager mSipManager;
+    private SipProfile mProfile;
+
+    private long mTimeOfLastValidHoldRequest = System.currentTimeMillis();
+
+    SipPhone (Context context, PhoneNotifier notifier, SipProfile profile) {
+        super("SIP:" + profile.getUriString(), context, notifier);
+
+        if (DBG) log("new SipPhone: " + hidePii(profile.getUriString()));
+        mRingingCall = new SipCall();
+        mForegroundCall = new SipCall();
+        mBackgroundCall = new SipCall();
+        mProfile = profile;
+        mSipManager = SipManager.newInstance(context);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) return true;
+        if (!(o instanceof SipPhone)) return false;
+        SipPhone that = (SipPhone) o;
+        return mProfile.getUriString().equals(that.mProfile.getUriString());
+    }
+
+    public String getSipUri() {
+        return mProfile.getUriString();
+    }
+
+    public boolean equals(SipPhone phone) {
+        return getSipUri().equals(phone.getSipUri());
+    }
+
+    public Connection takeIncomingCall(Object incomingCall) {
+        // FIXME: Is synchronizing on the class necessary, should we use a mLockObj?
+        // Also there are many things not synchronized, of course
+        // this may be true of GsmCdmaPhone too!!!
+        synchronized (SipPhone.class) {
+            if (!(incomingCall instanceof SipAudioCall)) {
+                if (DBG) log("takeIncomingCall: ret=null, not a SipAudioCall");
+                return null;
+            }
+            if (mRingingCall.getState().isAlive()) {
+                if (DBG) log("takeIncomingCall: ret=null, ringingCall not alive");
+                return null;
+            }
+
+            // FIXME: is it true that we cannot take any incoming call if
+            // both foreground and background are active
+            if (mForegroundCall.getState().isAlive()
+                    && mBackgroundCall.getState().isAlive()) {
+                if (DBG) {
+                    log("takeIncomingCall: ret=null," + " foreground and background both alive");
+                }
+                return null;
+            }
+
+            try {
+                SipAudioCall sipAudioCall = (SipAudioCall) incomingCall;
+                if (DBG) log("takeIncomingCall: taking call from: "
+                        + hidePii(sipAudioCall.getPeerProfile().getUriString()));
+                String localUri = sipAudioCall.getLocalProfile().getUriString();
+                if (localUri.equals(mProfile.getUriString())) {
+                    boolean makeCallWait = mForegroundCall.getState().isAlive();
+                    SipConnection connection = mRingingCall.initIncomingCall(sipAudioCall,
+                            makeCallWait);
+                    if (sipAudioCall.getState() != SipSession.State.INCOMING_CALL) {
+                        // Peer cancelled the call!
+                        if (DBG) log("    takeIncomingCall: call cancelled !!");
+                        mRingingCall.reset();
+                        connection = null;
+                    }
+                    return connection;
+                }
+            } catch (Exception e) {
+                // Peer may cancel the call at any time during the time we hook
+                // up ringingCall with sipAudioCall. Clean up ringingCall when
+                // that happens.
+                if (DBG) log("    takeIncomingCall: exception e=" + e);
+                mRingingCall.reset();
+            }
+            if (DBG) log("takeIncomingCall: NOT taking !!");
+            return null;
+        }
+    }
+
+    @Override
+    public void acceptCall(int videoState) throws CallStateException {
+        synchronized (SipPhone.class) {
+            if ((mRingingCall.getState() == Call.State.INCOMING) ||
+                    (mRingingCall.getState() == Call.State.WAITING)) {
+                if (DBG) log("acceptCall: accepting");
+                // Always unmute when answering a new call
+                mRingingCall.setMute(false);
+                mRingingCall.acceptCall();
+            } else {
+                if (DBG) {
+                    log("acceptCall:" +
+                        " throw CallStateException(\"phone not ringing\")");
+                }
+                throw new CallStateException("phone not ringing");
+            }
+        }
+    }
+
+    @Override
+    public void rejectCall() throws CallStateException {
+        synchronized (SipPhone.class) {
+            if (mRingingCall.getState().isRinging()) {
+                if (DBG) log("rejectCall: rejecting");
+                mRingingCall.rejectCall();
+            } else {
+                if (DBG) {
+                    log("rejectCall:" +
+                        " throw CallStateException(\"phone not ringing\")");
+                }
+                throw new CallStateException("phone not ringing");
+            }
+        }
+    }
+
+    @Override
+    public Connection dial(String dialString, int videoState) throws CallStateException {
+        synchronized (SipPhone.class) {
+            return dialInternal(dialString, videoState);
+        }
+    }
+
+    private Connection dialInternal(String dialString, int videoState)
+            throws CallStateException {
+        if (DBG) log("dialInternal: dialString=" + hidePii(dialString));
+        clearDisconnected();
+
+        if (!canDial()) {
+            throw new CallStateException("dialInternal: cannot dial in current state");
+        }
+        if (mForegroundCall.getState() == SipCall.State.ACTIVE) {
+            switchHoldingAndActive();
+        }
+        if (mForegroundCall.getState() != SipCall.State.IDLE) {
+            //we should have failed in !canDial() above before we get here
+            throw new CallStateException("cannot dial in current state");
+        }
+
+        mForegroundCall.setMute(false);
+        try {
+            Connection c = mForegroundCall.dial(dialString);
+            return c;
+        } catch (SipException e) {
+            loge("dialInternal: ", e);
+            throw new CallStateException("dial error: " + e);
+        }
+    }
+
+    @Override
+    public void switchHoldingAndActive() throws CallStateException {
+        // Wait for at least TIMEOUT_HOLD_PROCESSING ms to occur before sending hold/unhold requests
+        // to prevent spamming the SipAudioCall state machine and putting it into an invalid state.
+        if (!isHoldTimeoutExpired()) {
+            if (DBG) log("switchHoldingAndActive: Disregarded! Under " + TIMEOUT_HOLD_PROCESSING +
+                    " ms...");
+            return;
+        }
+        if (DBG) log("switchHoldingAndActive: switch fg and bg");
+        synchronized (SipPhone.class) {
+            mForegroundCall.switchWith(mBackgroundCall);
+            if (mBackgroundCall.getState().isAlive()) mBackgroundCall.hold();
+            if (mForegroundCall.getState().isAlive()) mForegroundCall.unhold();
+        }
+    }
+
+    @Override
+    public boolean canConference() {
+        if (DBG) log("canConference: ret=true");
+        return true;
+    }
+
+    @Override
+    public void conference() throws CallStateException {
+        synchronized (SipPhone.class) {
+            if ((mForegroundCall.getState() != SipCall.State.ACTIVE)
+                    || (mForegroundCall.getState() != SipCall.State.ACTIVE)) {
+                throw new CallStateException("wrong state to merge calls: fg="
+                        + mForegroundCall.getState() + ", bg="
+                        + mBackgroundCall.getState());
+            }
+            if (DBG) log("conference: merge fg & bg");
+            mForegroundCall.merge(mBackgroundCall);
+        }
+    }
+
+    public void conference(Call that) throws CallStateException {
+        synchronized (SipPhone.class) {
+            if (!(that instanceof SipCall)) {
+                throw new CallStateException("expect " + SipCall.class
+                        + ", cannot merge with " + that.getClass());
+            }
+            mForegroundCall.merge((SipCall) that);
+        }
+    }
+
+    @Override
+    public boolean canTransfer() {
+        return false;
+    }
+
+    @Override
+    public void explicitCallTransfer() {
+        //mCT.explicitCallTransfer();
+    }
+
+    @Override
+    public void clearDisconnected() {
+        synchronized (SipPhone.class) {
+            mRingingCall.clearDisconnected();
+            mForegroundCall.clearDisconnected();
+            mBackgroundCall.clearDisconnected();
+
+            updatePhoneState();
+            notifyPreciseCallStateChanged();
+        }
+    }
+
+    @Override
+    public void sendDtmf(char c) {
+        if (!PhoneNumberUtils.is12Key(c)) {
+            loge("sendDtmf called with invalid character '" + c + "'");
+        } else if (mForegroundCall.getState().isAlive()) {
+            synchronized (SipPhone.class) {
+                mForegroundCall.sendDtmf(c);
+            }
+        }
+    }
+
+    @Override
+    public void startDtmf(char c) {
+        if (!PhoneNumberUtils.is12Key(c)) {
+            loge("startDtmf called with invalid character '" + c + "'");
+        } else {
+            sendDtmf(c);
+        }
+    }
+
+    @Override
+    public void stopDtmf() {
+        // no op
+    }
+
+    public void sendBurstDtmf(String dtmfString) {
+        loge("sendBurstDtmf() is a CDMA method");
+    }
+
+    @Override
+    public void getOutgoingCallerIdDisplay(Message onComplete) {
+        // FIXME: what to reply?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode,
+                                           Message onComplete) {
+        // FIXME: what's this for SIP?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void getCallWaiting(Message onComplete) {
+        // FIXME: what to reply?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, Message onComplete) {
+        // FIXME: what to reply?
+        loge("call waiting not supported");
+    }
+
+    @Override
+    public void setEchoSuppressionEnabled() {
+        // Echo suppression may not be available on every device. So, check
+        // whether it is supported
+        synchronized (SipPhone.class) {
+            AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+            String echoSuppression = audioManager.getParameters("ec_supported");
+            if (echoSuppression.contains("off")) {
+                mForegroundCall.setAudioGroupMode();
+            }
+        }
+    }
+
+    @Override
+    public void setMute(boolean muted) {
+        synchronized (SipPhone.class) {
+            mForegroundCall.setMute(muted);
+        }
+    }
+
+    @Override
+    public boolean getMute() {
+        return (mForegroundCall.getState().isAlive()
+                ? mForegroundCall.getMute()
+                : mBackgroundCall.getMute());
+    }
+
+    @Override
+    public Call getForegroundCall() {
+        return mForegroundCall;
+    }
+
+    @Override
+    public Call getBackgroundCall() {
+        return mBackgroundCall;
+    }
+
+    @Override
+    public Call getRingingCall() {
+        return mRingingCall;
+    }
+
+    @Override
+    public ServiceState getServiceState() {
+        // FIXME: we may need to provide this when data connectivity is lost
+        // or when server is down
+        return super.getServiceState();
+    }
+
+    private String getUriString(SipProfile p) {
+        // SipProfile.getUriString() may contain "SIP:" and port
+        return p.getUserName() + "@" + getSipDomain(p);
+    }
+
+    private String getSipDomain(SipProfile p) {
+        String domain = p.getSipDomain();
+        // TODO: move this to SipProfile
+        if (domain.endsWith(":5060")) {
+            return domain.substring(0, domain.length() - 5);
+        } else {
+            return domain;
+        }
+    }
+
+    private static Call.State getCallStateFrom(SipAudioCall sipAudioCall) {
+        if (sipAudioCall.isOnHold()) return Call.State.HOLDING;
+        int sessionState = sipAudioCall.getState();
+        switch (sessionState) {
+            case SipSession.State.READY_TO_CALL:            return Call.State.IDLE;
+            case SipSession.State.INCOMING_CALL:
+            case SipSession.State.INCOMING_CALL_ANSWERING:  return Call.State.INCOMING;
+            case SipSession.State.OUTGOING_CALL:            return Call.State.DIALING;
+            case SipSession.State.OUTGOING_CALL_RING_BACK:  return Call.State.ALERTING;
+            case SipSession.State.OUTGOING_CALL_CANCELING:  return Call.State.DISCONNECTING;
+            case SipSession.State.IN_CALL:                  return Call.State.ACTIVE;
+            default:
+                slog("illegal connection state: " + sessionState);
+                return Call.State.DISCONNECTED;
+        }
+    }
+
+    private synchronized boolean isHoldTimeoutExpired() {
+        long currTime = System.currentTimeMillis();
+        if ((currTime - mTimeOfLastValidHoldRequest) > TIMEOUT_HOLD_PROCESSING) {
+            mTimeOfLastValidHoldRequest = currTime;
+            return true;
+        }
+        return false;
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private static void slog(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, s);
+    }
+
+    private void loge(String s, Exception e) {
+        Rlog.e(LOG_TAG, s, e);
+    }
+
+    private class SipCall extends SipCallBase {
+        private static final String SC_TAG = "SipCall";
+        private static final boolean SC_DBG = true;
+        private static final boolean SC_VDBG = false; // STOPSHIP if true
+
+        void reset() {
+            if (SC_DBG) log("reset");
+            mConnections.clear();
+            setState(Call.State.IDLE);
+        }
+
+        void switchWith(SipCall that) {
+            if (SC_DBG) log("switchWith");
+            synchronized (SipPhone.class) {
+                SipCall tmp = new SipCall();
+                tmp.takeOver(this);
+                this.takeOver(that);
+                that.takeOver(tmp);
+            }
+        }
+
+        private void takeOver(SipCall that) {
+            if (SC_DBG) log("takeOver");
+            mConnections = that.mConnections;
+            mState = that.mState;
+            for (Connection c : mConnections) {
+                ((SipConnection) c).changeOwner(this);
+            }
+        }
+
+        @Override
+        public Phone getPhone() {
+            return SipPhone.this;
+        }
+
+        @Override
+        public List<Connection> getConnections() {
+            if (SC_VDBG) log("getConnections");
+            synchronized (SipPhone.class) {
+                // FIXME should return Collections.unmodifiableList();
+                return mConnections;
+            }
+        }
+
+        Connection dial(String originalNumber) throws SipException {
+            if (SC_DBG) log("dial: num=" + (SC_VDBG ? originalNumber : "xxx"));
+            // TODO: Should this be synchronized?
+            String calleeSipUri = originalNumber;
+            if (!calleeSipUri.contains("@")) {
+                String replaceStr = Pattern.quote(mProfile.getUserName() + "@");
+                calleeSipUri = mProfile.getUriString().replaceFirst(replaceStr,
+                        calleeSipUri + "@");
+            }
+            try {
+                SipProfile callee =
+                        new SipProfile.Builder(calleeSipUri).build();
+                SipConnection c = new SipConnection(this, callee,
+                        originalNumber);
+                c.dial();
+                mConnections.add(c);
+                setState(Call.State.DIALING);
+                return c;
+            } catch (ParseException e) {
+                throw new SipException("dial", e);
+            }
+        }
+
+        @Override
+        public void hangup() throws CallStateException {
+            synchronized (SipPhone.class) {
+                if (mState.isAlive()) {
+                    if (SC_DBG) log("hangup: call " + getState()
+                            + ": " + this + " on phone " + getPhone());
+                    setState(State.DISCONNECTING);
+                    CallStateException excp = null;
+                    for (Connection c : mConnections) {
+                        try {
+                            c.hangup();
+                        } catch (CallStateException e) {
+                            excp = e;
+                        }
+                    }
+                    if (excp != null) throw excp;
+                } else {
+                    if (SC_DBG) log("hangup: dead call " + getState()
+                            + ": " + this + " on phone " + getPhone());
+                }
+            }
+        }
+
+        SipConnection initIncomingCall(SipAudioCall sipAudioCall, boolean makeCallWait) {
+            SipProfile callee = sipAudioCall.getPeerProfile();
+            SipConnection c = new SipConnection(this, callee);
+            mConnections.add(c);
+
+            Call.State newState = makeCallWait ? State.WAITING : State.INCOMING;
+            c.initIncomingCall(sipAudioCall, newState);
+
+            setState(newState);
+            notifyNewRingingConnectionP(c);
+            return c;
+        }
+
+        void rejectCall() throws CallStateException {
+            if (SC_DBG) log("rejectCall:");
+            hangup();
+        }
+
+        void acceptCall() throws CallStateException {
+            if (SC_DBG) log("acceptCall: accepting");
+            if (this != mRingingCall) {
+                throw new CallStateException("acceptCall() in a non-ringing call");
+            }
+            if (mConnections.size() != 1) {
+                throw new CallStateException("acceptCall() in a conf call");
+            }
+            ((SipConnection) mConnections.get(0)).acceptCall();
+        }
+
+        private boolean isSpeakerOn() {
+            Boolean ret = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                    .isSpeakerphoneOn();
+            if (SC_VDBG) log("isSpeakerOn: ret=" + ret);
+            return ret;
+        }
+
+        void setAudioGroupMode() {
+            AudioGroup audioGroup = getAudioGroup();
+            if (audioGroup == null) {
+                if (SC_DBG) log("setAudioGroupMode: audioGroup == null ignore");
+                return;
+            }
+            int mode = audioGroup.getMode();
+            if (mState == State.HOLDING) {
+                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+            } else if (getMute()) {
+                audioGroup.setMode(AudioGroup.MODE_MUTED);
+            } else if (isSpeakerOn()) {
+                audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
+            } else {
+                audioGroup.setMode(AudioGroup.MODE_NORMAL);
+            }
+            if (SC_DBG) log(String.format(
+                    "setAudioGroupMode change: %d --> %d", mode,
+                    audioGroup.getMode()));
+        }
+
+        void hold() throws CallStateException {
+            if (SC_DBG) log("hold:");
+            setState(State.HOLDING);
+            for (Connection c : mConnections) ((SipConnection) c).hold();
+            setAudioGroupMode();
+        }
+
+        void unhold() throws CallStateException {
+            if (SC_DBG) log("unhold:");
+            setState(State.ACTIVE);
+            AudioGroup audioGroup = new AudioGroup();
+            for (Connection c : mConnections) {
+                ((SipConnection) c).unhold(audioGroup);
+            }
+            setAudioGroupMode();
+        }
+
+        void setMute(boolean muted) {
+            if (SC_DBG) log("setMute: muted=" + muted);
+            for (Connection c : mConnections) {
+                ((SipConnection) c).setMute(muted);
+            }
+        }
+
+        boolean getMute() {
+            boolean ret = mConnections.isEmpty()
+                    ? false
+                    : ((SipConnection) mConnections.get(0)).getMute();
+            if (SC_DBG) log("getMute: ret=" + ret);
+            return ret;
+        }
+
+        void merge(SipCall that) throws CallStateException {
+            if (SC_DBG) log("merge:");
+            AudioGroup audioGroup = getAudioGroup();
+
+            // copy to an array to avoid concurrent modification as connections
+            // in that.connections will be removed in add(SipConnection).
+            Connection[] cc = that.mConnections.toArray(
+                    new Connection[that.mConnections.size()]);
+            for (Connection c : cc) {
+                SipConnection conn = (SipConnection) c;
+                add(conn);
+                if (conn.getState() == Call.State.HOLDING) {
+                    conn.unhold(audioGroup);
+                }
+            }
+            that.setState(Call.State.IDLE);
+        }
+
+        private void add(SipConnection conn) {
+            if (SC_DBG) log("add:");
+            SipCall call = conn.getCall();
+            if (call == this) return;
+            if (call != null) call.mConnections.remove(conn);
+
+            mConnections.add(conn);
+            conn.changeOwner(this);
+        }
+
+        void sendDtmf(char c) {
+            if (SC_DBG) log("sendDtmf: c=" + c);
+            AudioGroup audioGroup = getAudioGroup();
+            if (audioGroup == null) {
+                if (SC_DBG) log("sendDtmf: audioGroup == null, ignore c=" + c);
+                return;
+            }
+            audioGroup.sendDtmf(convertDtmf(c));
+        }
+
+        private int convertDtmf(char c) {
+            int code = c - '0';
+            if ((code < 0) || (code > 9)) {
+                switch (c) {
+                    case '*': return 10;
+                    case '#': return 11;
+                    case 'A': return 12;
+                    case 'B': return 13;
+                    case 'C': return 14;
+                    case 'D': return 15;
+                    default:
+                        throw new IllegalArgumentException(
+                                "invalid DTMF char: " + (int) c);
+                }
+            }
+            return code;
+        }
+
+        @Override
+        protected void setState(State newState) {
+            if (mState != newState) {
+                if (SC_DBG) log("setState: cur state" + mState
+                        + " --> " + newState + ": " + this + ": on phone "
+                        + getPhone() + " " + mConnections.size());
+
+                if (newState == Call.State.ALERTING) {
+                    mState = newState; // need in ALERTING to enable ringback
+                    startRingbackTone();
+                } else if (mState == Call.State.ALERTING) {
+                    stopRingbackTone();
+                }
+                mState = newState;
+                updatePhoneState();
+                notifyPreciseCallStateChanged();
+            }
+        }
+
+        void onConnectionStateChanged(SipConnection conn) {
+            // this can be called back when a conf call is formed
+            if (SC_DBG) log("onConnectionStateChanged: conn=" + conn);
+            if (mState != State.ACTIVE) {
+                setState(conn.getState());
+            }
+        }
+
+        void onConnectionEnded(SipConnection conn) {
+            // set state to DISCONNECTED only when all conns are disconnected
+            if (SC_DBG) log("onConnectionEnded: conn=" + conn);
+            if (mState != State.DISCONNECTED) {
+                boolean allConnectionsDisconnected = true;
+                if (SC_DBG) log("---check connections: "
+                        + mConnections.size());
+                for (Connection c : mConnections) {
+                    if (SC_DBG) log("   state=" + c.getState() + ": "
+                            + c);
+                    if (c.getState() != State.DISCONNECTED) {
+                        allConnectionsDisconnected = false;
+                        break;
+                    }
+                }
+                if (allConnectionsDisconnected) setState(State.DISCONNECTED);
+            }
+            notifyDisconnectP(conn);
+        }
+
+        private AudioGroup getAudioGroup() {
+            if (mConnections.isEmpty()) return null;
+            return ((SipConnection) mConnections.get(0)).getAudioGroup();
+        }
+
+        private void log(String s) {
+            Rlog.d(SC_TAG, s);
+        }
+    }
+
+    private class SipConnection extends SipConnectionBase {
+        private static final String SCN_TAG = "SipConnection";
+        private static final boolean SCN_DBG = true;
+
+        private SipCall mOwner;
+        private SipAudioCall mSipAudioCall;
+        private Call.State mState = Call.State.IDLE;
+        private SipProfile mPeer;
+        private boolean mIncoming = false;
+        private String mOriginalNumber; // may be a PSTN number
+
+        private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() {
+            @Override
+            protected void onCallEnded(int cause) {
+                if (getDisconnectCause() != DisconnectCause.LOCAL) {
+                    setDisconnectCause(cause);
+                }
+                synchronized (SipPhone.class) {
+                    setState(Call.State.DISCONNECTED);
+                    SipAudioCall sipAudioCall = mSipAudioCall;
+                    // FIXME: This goes null and is synchronized, but many uses aren't sync'd
+                    mSipAudioCall = null;
+                    String sessionState = (sipAudioCall == null)
+                            ? ""
+                            : (sipAudioCall.getState() + ", ");
+                    if (SCN_DBG) log("[SipAudioCallAdapter] onCallEnded: "
+                            + hidePii(mPeer.getUriString()) + ": " + sessionState
+                            + "cause: " + getDisconnectCause() + ", on phone "
+                            + getPhone());
+                    if (sipAudioCall != null) {
+                        sipAudioCall.setListener(null);
+                        sipAudioCall.close();
+                    }
+                    mOwner.onConnectionEnded(SipConnection.this);
+                }
+            }
+
+            @Override
+            public void onCallEstablished(SipAudioCall call) {
+                onChanged(call);
+                // Race onChanged synchronized this isn't
+                if (mState == Call.State.ACTIVE) call.startAudio();
+            }
+
+            @Override
+            public void onCallHeld(SipAudioCall call) {
+                onChanged(call);
+                // Race onChanged synchronized this isn't
+                if (mState == Call.State.HOLDING) call.startAudio();
+            }
+
+            @Override
+            public void onChanged(SipAudioCall call) {
+                synchronized (SipPhone.class) {
+                    Call.State newState = getCallStateFrom(call);
+                    if (mState == newState) return;
+                    if (newState == Call.State.INCOMING) {
+                        setState(mOwner.getState()); // INCOMING or WAITING
+                    } else {
+                        if (mOwner == mRingingCall) {
+                            if (mRingingCall.getState() == Call.State.WAITING) {
+                                try {
+                                    switchHoldingAndActive();
+                                } catch (CallStateException e) {
+                                    // disconnect the call.
+                                    onCallEnded(DisconnectCause.LOCAL);
+                                    return;
+                                }
+                            }
+                            mForegroundCall.switchWith(mRingingCall);
+                        }
+                        setState(newState);
+                    }
+                    mOwner.onConnectionStateChanged(SipConnection.this);
+                    if (SCN_DBG) {
+                        log("onChanged: " + hidePii(mPeer.getUriString()) + ": " + mState
+                                + " on phone " + getPhone());
+                    }
+                }
+            }
+
+            @Override
+            protected void onError(int cause) {
+                if (SCN_DBG) log("onError: " + cause);
+                onCallEnded(cause);
+            }
+        };
+
+        public SipConnection(SipCall owner, SipProfile callee,
+                String originalNumber) {
+            super(originalNumber);
+            mOwner = owner;
+            mPeer = callee;
+            mOriginalNumber = originalNumber;
+        }
+
+        public SipConnection(SipCall owner, SipProfile callee) {
+            this(owner, callee, getUriString(callee));
+        }
+
+        @Override
+        public String getCnapName() {
+            String displayName = mPeer.getDisplayName();
+            return TextUtils.isEmpty(displayName) ? null
+                                                  : displayName;
+        }
+
+        @Override
+        public int getNumberPresentation() {
+            return PhoneConstants.PRESENTATION_ALLOWED;
+        }
+
+        void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) {
+            setState(newState);
+            mSipAudioCall = sipAudioCall;
+            sipAudioCall.setListener(mAdapter); // call back to set state
+            mIncoming = true;
+        }
+
+        void acceptCall() throws CallStateException {
+            try {
+                mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL);
+            } catch (SipException e) {
+                throw new CallStateException("acceptCall(): " + e);
+            }
+        }
+
+        void changeOwner(SipCall owner) {
+            mOwner = owner;
+        }
+
+        AudioGroup getAudioGroup() {
+            if (mSipAudioCall == null) return null;
+            return mSipAudioCall.getAudioGroup();
+        }
+
+        void dial() throws SipException {
+            setState(Call.State.DIALING);
+            mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null,
+                    TIMEOUT_MAKE_CALL);
+            mSipAudioCall.setListener(mAdapter);
+        }
+
+        void hold() throws CallStateException {
+            setState(Call.State.HOLDING);
+            try {
+                mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL);
+            } catch (SipException e) {
+                throw new CallStateException("hold(): " + e);
+            }
+        }
+
+        void unhold(AudioGroup audioGroup) throws CallStateException {
+            mSipAudioCall.setAudioGroup(audioGroup);
+            setState(Call.State.ACTIVE);
+            try {
+                mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL);
+            } catch (SipException e) {
+                throw new CallStateException("unhold(): " + e);
+            }
+        }
+
+        void setMute(boolean muted) {
+            if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) {
+                if (SCN_DBG) log("setState: prev muted=" + !muted + " new muted=" + muted);
+                mSipAudioCall.toggleMute();
+            }
+        }
+
+        boolean getMute() {
+            return (mSipAudioCall == null) ? false
+                                           : mSipAudioCall.isMuted();
+        }
+
+        @Override
+        protected void setState(Call.State state) {
+            if (state == mState) return;
+            super.setState(state);
+            mState = state;
+        }
+
+        @Override
+        public Call.State getState() {
+            return mState;
+        }
+
+        @Override
+        public boolean isIncoming() {
+            return mIncoming;
+        }
+
+        @Override
+        public String getAddress() {
+            // Phone app uses this to query caller ID. Return the original dial
+            // number (which may be a PSTN number) instead of the peer's SIP
+            // URI.
+            return mOriginalNumber;
+        }
+
+        @Override
+        public SipCall getCall() {
+            return mOwner;
+        }
+
+        @Override
+        protected Phone getPhone() {
+            return mOwner.getPhone();
+        }
+
+        @Override
+        public void hangup() throws CallStateException {
+            synchronized (SipPhone.class) {
+                if (SCN_DBG) {
+                    log("hangup: conn=" + hidePii(mPeer.getUriString())
+                            + ": " + mState + ": on phone "
+                            + getPhone().getPhoneName());
+                }
+                if (!mState.isAlive()) return;
+                try {
+                    SipAudioCall sipAudioCall = mSipAudioCall;
+                    if (sipAudioCall != null) {
+                        sipAudioCall.setListener(null);
+                        sipAudioCall.endCall();
+                    }
+                } catch (SipException e) {
+                    throw new CallStateException("hangup(): " + e);
+                } finally {
+                    mAdapter.onCallEnded(((mState == Call.State.INCOMING)
+                            || (mState == Call.State.WAITING))
+                            ? DisconnectCause.INCOMING_REJECTED
+                            : DisconnectCause.LOCAL);
+                }
+            }
+        }
+
+        @Override
+        public void separate() throws CallStateException {
+            synchronized (SipPhone.class) {
+                SipCall call = (getPhone() == SipPhone.this)
+                        ? (SipCall) getBackgroundCall()
+                        : (SipCall) getForegroundCall();
+                if (call.getState() != Call.State.IDLE) {
+                    throw new CallStateException(
+                            "cannot put conn back to a call in non-idle state: "
+                            + call.getState());
+                }
+                if (SCN_DBG) log("separate: conn="
+                        + mPeer.getUriString() + " from " + mOwner + " back to "
+                        + call);
+
+                // separate the AudioGroup and connection from the original call
+                Phone originalPhone = getPhone();
+                AudioGroup audioGroup = call.getAudioGroup(); // may be null
+                call.add(this);
+                mSipAudioCall.setAudioGroup(audioGroup);
+
+                // put the original call to bg; and the separated call becomes
+                // fg if it was in bg
+                originalPhone.switchHoldingAndActive();
+
+                // start audio and notify the phone app of the state change
+                call = (SipCall) getForegroundCall();
+                mSipAudioCall.startAudio();
+                call.onConnectionStateChanged(this);
+            }
+        }
+
+        private void log(String s) {
+            Rlog.d(SCN_TAG, s);
+        }
+    }
+
+    private abstract class SipAudioCallAdapter extends SipAudioCall.Listener {
+        private static final String SACA_TAG = "SipAudioCallAdapter";
+        private static final boolean SACA_DBG = true;
+        /** Call ended with cause defined in {@link DisconnectCause}. */
+        protected abstract void onCallEnded(int cause);
+        /** Call failed with cause defined in {@link DisconnectCause}. */
+        protected abstract void onError(int cause);
+
+        @Override
+        public void onCallEnded(SipAudioCall call) {
+            if (SACA_DBG) log("onCallEnded: call=" + call);
+            onCallEnded(call.isInCall()
+                    ? DisconnectCause.NORMAL
+                    : DisconnectCause.INCOMING_MISSED);
+        }
+
+        @Override
+        public void onCallBusy(SipAudioCall call) {
+            if (SACA_DBG) log("onCallBusy: call=" + call);
+            onCallEnded(DisconnectCause.BUSY);
+        }
+
+        @Override
+        public void onError(SipAudioCall call, int errorCode,
+                String errorMessage) {
+            if (SACA_DBG) {
+                log("onError: call=" + call + " code="+ SipErrorCode.toString(errorCode)
+                    + ": " + errorMessage);
+            }
+            switch (errorCode) {
+                case SipErrorCode.SERVER_UNREACHABLE:
+                    onError(DisconnectCause.SERVER_UNREACHABLE);
+                    break;
+                case SipErrorCode.PEER_NOT_REACHABLE:
+                    onError(DisconnectCause.NUMBER_UNREACHABLE);
+                    break;
+                case SipErrorCode.INVALID_REMOTE_URI:
+                    onError(DisconnectCause.INVALID_NUMBER);
+                    break;
+                case SipErrorCode.TIME_OUT:
+                case SipErrorCode.TRANSACTION_TERMINTED:
+                    onError(DisconnectCause.TIMED_OUT);
+                    break;
+                case SipErrorCode.DATA_CONNECTION_LOST:
+                    onError(DisconnectCause.LOST_SIGNAL);
+                    break;
+                case SipErrorCode.INVALID_CREDENTIALS:
+                    onError(DisconnectCause.INVALID_CREDENTIALS);
+                    break;
+                case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION:
+                    onError(DisconnectCause.OUT_OF_NETWORK);
+                    break;
+                case SipErrorCode.SERVER_ERROR:
+                    onError(DisconnectCause.SERVER_ERROR);
+                    break;
+                case SipErrorCode.SOCKET_ERROR:
+                case SipErrorCode.CLIENT_ERROR:
+                default:
+                    onError(DisconnectCause.ERROR_UNSPECIFIED);
+            }
+        }
+
+        private void log(String s) {
+            Rlog.d(SACA_TAG, s);
+        }
+    }
+
+    public static String hidePii(String s) {
+        return VDBG ? Rlog.pii(LOG_TAG, s) : "xxxxx";
+    }
+}
diff --git a/com/android/internal/telephony/sip/SipPhoneBase.java b/com/android/internal/telephony/sip/SipPhoneBase.java
new file mode 100644
index 0000000..7bdfad8
--- /dev/null
+++ b/com/android/internal/telephony/sip/SipPhoneBase.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.sip;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RegistrantList;
+import android.os.ResultReceiver;
+import android.os.SystemProperties;
+import android.os.WorkSource;
+import android.telephony.CellLocation;
+import android.telephony.NetworkScanRequest;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.IccPhoneBookInterfaceManager;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.OperatorInfo;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneNotifier;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.dataconnection.DataConnection;
+import com.android.internal.telephony.uicc.IccFileHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class SipPhoneBase extends Phone {
+    private static final String LOG_TAG = "SipPhoneBase";
+
+    private RegistrantList mRingbackRegistrants = new RegistrantList();
+    private PhoneConstants.State mState = PhoneConstants.State.IDLE;
+
+    public SipPhoneBase(String name, Context context, PhoneNotifier notifier) {
+        super(name, notifier, context, new SipCommandInterface(context), false);
+    }
+
+    @Override
+    public abstract Call getForegroundCall();
+
+    @Override
+    public abstract Call getBackgroundCall();
+
+    @Override
+    public abstract Call getRingingCall();
+
+    @Override
+    public Connection dial(String dialString, UUSInfo uusInfo, int videoState, Bundle intentExtras)
+            throws CallStateException {
+        // ignore UUSInfo
+        return dial(dialString, videoState);
+    }
+
+    void migrateFrom(SipPhoneBase from) {
+        super.migrateFrom(from);
+        migrate(mRingbackRegistrants, from.mRingbackRegistrants);
+    }
+
+    @Override
+    public void registerForRingbackTone(Handler h, int what, Object obj) {
+        mRingbackRegistrants.addUnique(h, what, obj);
+    }
+
+    @Override
+    public void unregisterForRingbackTone(Handler h) {
+        mRingbackRegistrants.remove(h);
+    }
+
+    @Override
+    public void startRingbackTone() {
+        AsyncResult result = new AsyncResult(null, Boolean.TRUE, null);
+        mRingbackRegistrants.notifyRegistrants(result);
+    }
+
+    @Override
+    public void stopRingbackTone() {
+        AsyncResult result = new AsyncResult(null, Boolean.FALSE, null);
+        mRingbackRegistrants.notifyRegistrants(result);
+    }
+
+    @Override
+    public ServiceState getServiceState() {
+        // FIXME: we may need to provide this when data connectivity is lost
+        // or when server is down
+        ServiceState s = new ServiceState();
+        s.setVoiceRegState(ServiceState.STATE_IN_SERVICE);
+        return s;
+    }
+
+    @Override
+    public CellLocation getCellLocation(WorkSource workSource) {
+        return null;
+    }
+
+    @Override
+    public PhoneConstants.State getState() {
+        return mState;
+    }
+
+    @Override
+    public int getPhoneType() {
+        return PhoneConstants.PHONE_TYPE_SIP;
+    }
+
+    @Override
+    public SignalStrength getSignalStrength() {
+        return new SignalStrength();
+    }
+
+    @Override
+    public boolean getMessageWaitingIndicator() {
+        return false;
+    }
+
+    @Override
+    public boolean getCallForwardingIndicator() {
+        return false;
+    }
+
+    @Override
+    public List<? extends MmiCode> getPendingMmiCodes() {
+        return new ArrayList<MmiCode>(0);
+    }
+
+    @Override
+    public PhoneConstants.DataState getDataConnectionState() {
+        return PhoneConstants.DataState.DISCONNECTED;
+    }
+
+    @Override
+    public PhoneConstants.DataState getDataConnectionState(String apnType) {
+        return PhoneConstants.DataState.DISCONNECTED;
+    }
+
+    @Override
+    public DataActivityState getDataActivityState() {
+        return DataActivityState.NONE;
+    }
+
+    /**
+     * SIP phones do not have a subscription id, so do not notify of specific phone state changes.
+     */
+    /* package */ void notifyPhoneStateChanged() {
+        // Do nothing.
+    }
+
+    /**
+     * Notify registrants of a change in the call state. This notifies changes in
+     * {@link com.android.internal.telephony.Call.State}. Use this when changes
+     * in the precise call state are needed, else use notifyPhoneStateChanged.
+     */
+    /* package */ void notifyPreciseCallStateChanged() {
+        /* we'd love it if this was package-scoped*/
+        super.notifyPreciseCallStateChangedP();
+    }
+
+    void notifyNewRingingConnection(Connection c) {
+        super.notifyNewRingingConnectionP(c);
+    }
+
+    void notifyDisconnect(Connection cn) {
+        mDisconnectRegistrants.notifyResult(cn);
+    }
+
+    void notifyUnknownConnection() {
+        mUnknownConnectionRegistrants.notifyResult(this);
+    }
+
+    void notifySuppServiceFailed(SuppService code) {
+        mSuppServiceFailedRegistrants.notifyResult(code);
+    }
+
+    void notifyServiceStateChanged(ServiceState ss) {
+        super.notifyServiceStateChangedP(ss);
+    }
+
+    @Override
+    public void notifyCallForwardingIndicator() {
+        mNotifier.notifyCallForwardingChanged(this);
+    }
+
+    public boolean canDial() {
+        int serviceState = getServiceState().getState();
+        Rlog.v(LOG_TAG, "canDial(): serviceState = " + serviceState);
+        if (serviceState == ServiceState.STATE_POWER_OFF) return false;
+
+        String disableCall = SystemProperties.get(
+                TelephonyProperties.PROPERTY_DISABLE_CALL, "false");
+        Rlog.v(LOG_TAG, "canDial(): disableCall = " + disableCall);
+        if (disableCall.equals("true")) return false;
+
+        Rlog.v(LOG_TAG, "canDial(): ringingCall: " + getRingingCall().getState());
+        Rlog.v(LOG_TAG, "canDial(): foregndCall: " + getForegroundCall().getState());
+        Rlog.v(LOG_TAG, "canDial(): backgndCall: " + getBackgroundCall().getState());
+        return !getRingingCall().isRinging()
+                && (!getForegroundCall().getState().isAlive()
+                    || !getBackgroundCall().getState().isAlive());
+    }
+
+    @Override
+    public boolean handleInCallMmiCommands(String dialString) {
+        return false;
+    }
+
+    boolean isInCall() {
+        Call.State foregroundCallState = getForegroundCall().getState();
+        Call.State backgroundCallState = getBackgroundCall().getState();
+        Call.State ringingCallState = getRingingCall().getState();
+
+       return (foregroundCallState.isAlive() || backgroundCallState.isAlive()
+            || ringingCallState.isAlive());
+    }
+
+    @Override
+    public boolean handlePinMmi(String dialString) {
+        return false;
+    }
+
+    @Override
+    public boolean handleUssdRequest(String dialString, ResultReceiver wrappedCallback) {
+        return false;
+    }
+
+    @Override
+    public void sendUssdResponse(String ussdMessge) {
+    }
+
+    @Override
+    public void registerForSuppServiceNotification(
+            Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void unregisterForSuppServiceNotification(Handler h) {
+    }
+
+    @Override
+    public void setRadioPower(boolean power) {
+    }
+
+    @Override
+    public String getVoiceMailNumber() {
+        return null;
+    }
+
+    @Override
+    public String getVoiceMailAlphaTag() {
+        return null;
+    }
+
+    @Override
+    public String getDeviceId() {
+        return null;
+    }
+
+    @Override
+    public String getDeviceSvn() {
+        return null;
+    }
+
+    @Override
+    public String getImei() {
+        return null;
+    }
+
+    @Override
+    public String getEsn() {
+        Rlog.e(LOG_TAG, "[SipPhone] getEsn() is a CDMA method");
+        return "0";
+    }
+
+    @Override
+    public String getMeid() {
+        Rlog.e(LOG_TAG, "[SipPhone] getMeid() is a CDMA method");
+        return "0";
+    }
+
+    @Override
+    public String getSubscriberId() {
+        return null;
+    }
+
+    @Override
+    public String getGroupIdLevel1() {
+        return null;
+    }
+
+    @Override
+    public String getGroupIdLevel2() {
+        return null;
+    }
+
+    @Override
+    public String getIccSerialNumber() {
+        return null;
+    }
+
+    @Override
+    public String getLine1Number() {
+        return null;
+    }
+
+    @Override
+    public String getLine1AlphaTag() {
+        return null;
+    }
+
+    @Override
+    public boolean setLine1Number(String alphaTag, String number, Message onComplete) {
+        // FIXME: what to reply for SIP?
+        return false;
+    }
+
+    @Override
+    public void setVoiceMailNumber(String alphaTag, String voiceMailNumber,
+            Message onComplete) {
+        // FIXME: what to reply for SIP?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void getCallForwardingOption(int commandInterfaceCFReason, Message onComplete) {
+    }
+
+    @Override
+    public void setCallForwardingOption(int commandInterfaceCFAction,
+            int commandInterfaceCFReason, String dialingNumber,
+            int timerSeconds, Message onComplete) {
+    }
+
+    @Override
+    public void getOutgoingCallerIdDisplay(Message onComplete) {
+        // FIXME: what to reply?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode,
+                                           Message onComplete) {
+        // FIXME: what's this for SIP?
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void getCallWaiting(Message onComplete) {
+        AsyncResult.forMessage(onComplete, null, null);
+        onComplete.sendToTarget();
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, Message onComplete) {
+        Rlog.e(LOG_TAG, "call waiting not supported");
+    }
+
+    @Override
+    public boolean getIccRecordsLoaded() {
+        return false;
+    }
+
+    @Override
+    public IccCard getIccCard() {
+        return null;
+    }
+
+    @Override
+    public void getAvailableNetworks(Message response) {
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message response) {
+    }
+
+    @Override
+    public void stopNetworkScan(Message response) {
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message response) {
+    }
+
+    @Override
+    public void selectNetworkManually(OperatorInfo network, boolean persistSelection,
+            Message response) {
+    }
+
+    @Override
+    public void setOnPostDialCharacter(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void getDataCallList(Message response) {
+    }
+
+    public List<DataConnection> getCurrentDataConnectionList () {
+        return null;
+    }
+
+    @Override
+    public void updateServiceLocation() {
+    }
+
+    @Override
+    public void enableLocationUpdates() {
+    }
+
+    @Override
+    public void disableLocationUpdates() {
+    }
+
+    @Override
+    public boolean getDataRoamingEnabled() {
+        return false;
+    }
+
+    @Override
+    public void setDataRoamingEnabled(boolean enable) {
+    }
+
+    @Override
+    public boolean getDataEnabled() {
+        return false;
+    }
+
+    @Override
+    public void setDataEnabled(boolean enable) {
+    }
+
+    public boolean enableDataConnectivity() {
+        return false;
+    }
+
+    public boolean disableDataConnectivity() {
+        return false;
+    }
+
+    @Override
+    public boolean isDataAllowed() {
+        return false;
+    }
+
+    public void saveClirSetting(int commandInterfaceCLIRMode) {
+    }
+
+    @Override
+    public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(){
+        return null;
+    }
+
+    @Override
+    public IccFileHandler getIccFileHandler(){
+        return null;
+    }
+
+    @Override
+    public void activateCellBroadcastSms(int activate, Message response) {
+        Rlog.e(LOG_TAG, "Error! This functionality is not implemented for SIP.");
+    }
+
+    @Override
+    public void getCellBroadcastSmsConfig(Message response) {
+        Rlog.e(LOG_TAG, "Error! This functionality is not implemented for SIP.");
+    }
+
+    @Override
+    public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response){
+        Rlog.e(LOG_TAG, "Error! This functionality is not implemented for SIP.");
+    }
+
+    //@Override
+    @Override
+    public boolean needsOtaServiceProvisioning() {
+        // FIXME: what's this for SIP?
+        return false;
+    }
+
+    //@Override
+    @Override
+    public LinkProperties getLinkProperties(String apnType) {
+        // FIXME: what's this for SIP?
+        return null;
+    }
+
+    /**
+     * Determines if video calling is enabled.  Always {@code false} for SIP.
+     *
+     * @return {@code false} since SIP does not support video calling.
+     */
+    @Override
+    public boolean isVideoEnabled() {
+        return false;
+    }
+
+    void updatePhoneState() {
+        PhoneConstants.State oldState = mState;
+
+        if (getRingingCall().isRinging()) {
+            mState = PhoneConstants.State.RINGING;
+        } else if (getForegroundCall().isIdle()
+                && getBackgroundCall().isIdle()) {
+            mState = PhoneConstants.State.IDLE;
+        } else {
+            mState = PhoneConstants.State.OFFHOOK;
+        }
+
+        if (mState != oldState) {
+            Rlog.d(LOG_TAG, " ^^^ new phone state: " + mState);
+            notifyPhoneStateChanged();
+        }
+    }
+
+    @Override
+    protected void onUpdateIccAvailability() {
+    }
+
+    @Override
+    public void sendEmergencyCallStateChange(boolean callActive) {
+    }
+
+    @Override
+    public void setBroadcastEmergencyCallStateChanges(boolean broadcast) {
+    }
+}
diff --git a/com/android/internal/telephony/sip/SipPhoneFactory.java b/com/android/internal/telephony/sip/SipPhoneFactory.java
new file mode 100644
index 0000000..3383bed
--- /dev/null
+++ b/com/android/internal/telephony/sip/SipPhoneFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 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.internal.telephony.sip;
+
+import com.android.internal.telephony.PhoneNotifier;
+
+import android.content.Context;
+import android.net.sip.SipProfile;
+import android.telephony.Rlog;
+
+import java.text.ParseException;
+
+/**
+ * {@hide}
+ */
+public class SipPhoneFactory {
+    /**
+     * Makes a {@link SipPhone} object.
+     * @param sipUri the local SIP URI the phone runs on
+     * @param context {@code Context} needed to create a Phone object
+     * @param phoneNotifier {@code PhoneNotifier} needed to create a Phone
+     *      object
+     * @return the {@code SipPhone} object or null if the SIP URI is not valid
+     */
+    public static SipPhone makePhone(String sipUri, Context context,
+            PhoneNotifier phoneNotifier) {
+        try {
+            SipProfile profile = new SipProfile.Builder(sipUri).build();
+            return new SipPhone(context, phoneNotifier, profile);
+        } catch (ParseException e) {
+            Rlog.w("SipPhoneFactory", "makePhone", e);
+            return null;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/test/ModelInterpreter.java b/com/android/internal/telephony/test/ModelInterpreter.java
new file mode 100644
index 0000000..7930b56
--- /dev/null
+++ b/com/android/internal/telephony/test/ModelInterpreter.java
@@ -0,0 +1,696 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.test;
+
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.Rlog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+
+// Also in ATChannel.java
+class LineReader
+{
+    /**
+     * Not threadsafe
+     * Assumes input is ASCII
+     */
+
+    //***** Constants
+
+    // For what it's worth, this is also the size of an
+    // OMAP CSMI mailbox
+    static final int BUFFER_SIZE = 0x1000;
+
+    // just to prevent constant allocations
+    byte mBuffer[] = new byte[BUFFER_SIZE];
+
+    //***** Instance Variables
+
+    InputStream mInStream;
+
+    LineReader (InputStream s)
+    {
+        mInStream = s;
+    }
+
+    String
+    getNextLine()
+    {
+        return getNextLine(false);
+    }
+
+    String
+    getNextLineCtrlZ()
+    {
+        return getNextLine(true);
+    }
+
+    /**
+     * Note: doesn't return the last incomplete line read on EOF, since
+     * it doesn't typically matter anyway
+     *
+     * Returns NULL on EOF
+     */
+
+    String
+    getNextLine(boolean ctrlZ)
+    {
+        int i = 0;
+
+        try {
+            for (;;) {
+                int result;
+
+                result = mInStream.read();
+
+                if (result < 0) {
+                    return null;
+                }
+
+                if (ctrlZ && result == 0x1a) {
+                    break;
+                } else if (result == '\r' || result == '\n') {
+                    if (i == 0) {
+                        // Skip leading cr/lf
+                        continue;
+                    } else {
+                        break;
+                    }
+                }
+
+                mBuffer[i++] = (byte)result;
+            }
+        } catch (IOException ex) {
+            return null;
+        } catch (IndexOutOfBoundsException ex) {
+            System.err.println("ATChannel: buffer overflow");
+        }
+
+        try {
+            return new String(mBuffer, 0, i, "US-ASCII");
+        } catch (UnsupportedEncodingException ex) {
+            System.err.println("ATChannel: implausable UnsupportedEncodingException");
+            return null;
+        }
+    }
+}
+
+
+
+class InterpreterEx extends Exception
+{
+    public
+    InterpreterEx (String result)
+    {
+        mResult = result;
+    }
+
+    String mResult;
+}
+
+public class ModelInterpreter
+            implements Runnable, SimulatedRadioControl
+{
+    static final int MAX_CALLS = 6;
+
+    /** number of msec between dialing -> alerting and alerting->active */
+    static final int CONNECTING_PAUSE_MSEC = 5 * 100;
+
+    static final String LOG_TAG = "ModelInterpreter";
+
+    //***** Instance Variables
+
+    InputStream mIn;
+    OutputStream mOut;
+    LineReader mLineReader;
+    ServerSocket mSS;
+
+    private String mFinalResponse;
+
+    SimulatedGsmCallState mSimulatedCallState;
+
+    HandlerThread mHandlerThread;
+
+    int mPausedResponseCount;
+    Object mPausedResponseMonitor = new Object();
+
+    //***** Events
+
+    static final int PROGRESS_CALL_STATE        = 1;
+
+    //***** Constructor
+
+    public
+    ModelInterpreter (InputStream in, OutputStream out)
+    {
+        mIn = in;
+        mOut = out;
+
+        init();
+    }
+
+    public
+    ModelInterpreter (InetSocketAddress sa) throws java.io.IOException
+    {
+        mSS = new ServerSocket();
+
+        mSS.setReuseAddress(true);
+        mSS.bind(sa);
+
+        init();
+    }
+
+    private void
+    init()
+    {
+        new Thread(this, "ModelInterpreter").start();
+        mHandlerThread = new HandlerThread("ModelInterpreter");
+        mHandlerThread.start();
+        Looper looper = mHandlerThread.getLooper();
+        mSimulatedCallState = new SimulatedGsmCallState(looper);
+    }
+
+    //***** Runnable Implementation
+
+    @Override
+    public void run()
+    {
+        for (;;) {
+            if (mSS != null) {
+                Socket s;
+
+                try {
+                    s = mSS.accept();
+                } catch (java.io.IOException ex) {
+                    Rlog.w(LOG_TAG,
+                        "IOException on socket.accept(); stopping", ex);
+                    return;
+                }
+
+                try {
+                    mIn = s.getInputStream();
+                    mOut = s.getOutputStream();
+                } catch (java.io.IOException ex) {
+                    Rlog.w(LOG_TAG,
+                        "IOException on accepted socket(); re-listening", ex);
+                    continue;
+                }
+
+                Rlog.i(LOG_TAG, "New connection accepted");
+            }
+
+
+            mLineReader = new LineReader (mIn);
+
+            println ("Welcome");
+
+            for (;;) {
+                String line;
+
+                line = mLineReader.getNextLine();
+
+                //System.out.println("MI<< " + line);
+
+                if (line == null) {
+                    break;
+                }
+
+                synchronized(mPausedResponseMonitor) {
+                    while (mPausedResponseCount > 0) {
+                        try {
+                            mPausedResponseMonitor.wait();
+                        } catch (InterruptedException ex) {
+                        }
+                    }
+                }
+
+                synchronized (this) {
+                    try {
+                        mFinalResponse = "OK";
+                        processLine(line);
+                        println(mFinalResponse);
+                    } catch (InterpreterEx ex) {
+                        println(ex.mResult);
+                    } catch (RuntimeException ex) {
+                        ex.printStackTrace();
+                        println("ERROR");
+                    }
+                }
+            }
+
+            Rlog.i(LOG_TAG, "Disconnected");
+
+            if (mSS == null) {
+                // no reconnect in this case
+                break;
+            }
+        }
+    }
+
+
+    //***** Instance Methods
+
+    /** Start the simulated phone ringing */
+    @Override
+    public void
+    triggerRing(String number)
+    {
+        synchronized (this) {
+            boolean success;
+
+            success = mSimulatedCallState.triggerRing(number);
+
+            if (success) {
+                println ("RING");
+            }
+        }
+    }
+
+    /** If a call is DIALING or ALERTING, progress it to the next state */
+    @Override
+    public void
+    progressConnectingCallState()
+    {
+        mSimulatedCallState.progressConnectingCallState();
+    }
+
+
+    /** If a call is DIALING or ALERTING, progress it all the way to ACTIVE */
+    @Override
+    public void
+    progressConnectingToActive()
+    {
+        mSimulatedCallState.progressConnectingToActive();
+    }
+
+    /** automatically progress mobile originated calls to ACTIVE.
+     *  default to true
+     */
+    @Override
+    public void
+    setAutoProgressConnectingCall(boolean b)
+    {
+        mSimulatedCallState.setAutoProgressConnectingCall(b);
+    }
+
+    @Override
+    public void
+    setNextDialFailImmediately(boolean b)
+    {
+        mSimulatedCallState.setNextDialFailImmediately(b);
+    }
+
+    @Override
+    public void setNextCallFailCause(int gsmCause)
+    {
+        //FIXME implement
+    }
+
+
+    /** hangup ringing, dialing, or actuve calls */
+    @Override
+    public void
+    triggerHangupForeground()
+    {
+        boolean success;
+
+        success = mSimulatedCallState.triggerHangupForeground();
+
+        if (success) {
+            println ("NO CARRIER");
+        }
+    }
+
+    /** hangup holding calls */
+    @Override
+    public void
+    triggerHangupBackground()
+    {
+        boolean success;
+
+        success = mSimulatedCallState.triggerHangupBackground();
+
+        if (success) {
+            println ("NO CARRIER");
+        }
+    }
+
+    /** hangup all */
+
+    @Override
+    public void
+    triggerHangupAll()
+    {
+        boolean success;
+
+        success = mSimulatedCallState.triggerHangupAll();
+
+        if (success) {
+            println ("NO CARRIER");
+        }
+    }
+
+    public void
+    sendUnsolicited (String unsol)
+    {
+        synchronized (this) {
+            println(unsol);
+        }
+    }
+
+    @Override
+    public void triggerSsn(int a, int b) {}
+    @Override
+    public void triggerIncomingUssd(String statusCode, String message) {}
+
+    @Override
+    public void
+    triggerIncomingSMS(String message)
+    {
+/**************
+        StringBuilder pdu = new StringBuilder();
+
+        pdu.append ("00");      //SMSC address - 0 bytes
+
+        pdu.append ("04");      // Message type indicator
+
+        // source address: +18005551212
+        pdu.append("918100551521F0");
+
+        // protocol ID and data coding scheme
+        pdu.append("0000");
+
+        Calendar c = Calendar.getInstance();
+
+        pdu.append (c.
+
+
+
+        synchronized (this) {
+            println("+CMT: ,1\r" + pdu.toString());
+        }
+
+**************/
+    }
+
+    @Override
+    public void
+    pauseResponses()
+    {
+        synchronized(mPausedResponseMonitor) {
+            mPausedResponseCount++;
+        }
+    }
+
+    @Override
+    public void
+    resumeResponses()
+    {
+        synchronized(mPausedResponseMonitor) {
+            mPausedResponseCount--;
+
+            if (mPausedResponseCount == 0) {
+                mPausedResponseMonitor.notifyAll();
+            }
+        }
+    }
+
+    //***** Private Instance Methods
+
+    private void
+    onAnswer() throws InterpreterEx
+    {
+        boolean success;
+
+        success = mSimulatedCallState.onAnswer();
+
+        if (!success) {
+            throw new InterpreterEx("ERROR");
+        }
+    }
+
+    private void
+    onHangup() throws InterpreterEx
+    {
+        boolean success = false;
+
+        success = mSimulatedCallState.onAnswer();
+
+        if (!success) {
+            throw new InterpreterEx("ERROR");
+        }
+
+        mFinalResponse = "NO CARRIER";
+    }
+
+    private void
+    onCHLD(String command) throws InterpreterEx
+    {
+        // command starts with "+CHLD="
+        char c0;
+        char c1 = 0;
+        boolean success;
+
+        c0 = command.charAt(6);
+
+        if (command.length() >= 8) {
+            c1 = command.charAt(7);
+        }
+
+        success = mSimulatedCallState.onChld(c0, c1);
+
+        if (!success) {
+            throw new InterpreterEx("ERROR");
+        }
+    }
+
+    private void
+    onDial(String command) throws InterpreterEx
+    {
+        boolean success;
+
+        success = mSimulatedCallState.onDial(command.substring(1));
+
+        if (!success) {
+            throw new InterpreterEx("ERROR");
+        }
+    }
+
+    private void
+    onCLCC()
+    {
+        List<String> lines;
+
+        lines = mSimulatedCallState.getClccLines();
+
+        for (int i = 0, s = lines.size() ; i < s ; i++) {
+            println (lines.get(i));
+        }
+    }
+
+    private void
+    onSMSSend(String command)
+    {
+        String pdu;
+
+        print ("> ");
+        pdu = mLineReader.getNextLineCtrlZ();
+
+        println("+CMGS: 1");
+    }
+
+    void
+    processLine (String line) throws InterpreterEx
+    {
+        String[] commands;
+
+        commands = splitCommands(line);
+
+        for (int i = 0; i < commands.length ; i++) {
+            String command = commands[i];
+
+            if (command.equals("A")) {
+                onAnswer();
+            } else if (command.equals("H")) {
+                onHangup();
+            } else if (command.startsWith("+CHLD=")) {
+                onCHLD(command);
+            } else if (command.equals("+CLCC")) {
+                onCLCC();
+            } else if (command.startsWith("D")) {
+                onDial(command);
+            } else if (command.startsWith("+CMGS=")) {
+                onSMSSend(command);
+            } else {
+                boolean found = false;
+
+                for (int j = 0; j < sDefaultResponses.length ; j++) {
+                    if (command.equals(sDefaultResponses[j][0])) {
+                        String r = sDefaultResponses[j][1];
+                        if (r != null) {
+                            println(r);
+                        }
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    throw new InterpreterEx ("ERROR");
+                }
+            }
+        }
+    }
+
+
+    String[]
+    splitCommands(String line) throws InterpreterEx
+    {
+        if (!line.startsWith ("AT")) {
+            throw new InterpreterEx("ERROR");
+        }
+
+        if (line.length() == 2) {
+            // Just AT by itself
+            return new String[0];
+        }
+
+        String ret[] = new String[1];
+
+        //TODO fix case here too
+        ret[0] = line.substring(2);
+
+        return ret;
+/****
+        try {
+            // i = 2 to skip over AT
+            for (int i = 2, s = line.length() ; i < s ; i++) {
+                // r"|([A-RT-Z]\d?)" # Normal commands eg ATA or I0
+                // r"|(&[A-Z]\d*)" # & commands eg &C
+                // r"|(S\d+(=\d+)?)" # S registers
+                // r"((\+|%)\w+(\?|=([^;]+(;|$)))?)" # extended command eg +CREG=2
+
+
+            }
+        } catch (StringIndexOutOfBoundsException ex) {
+            throw new InterpreterEx ("ERROR");
+        }
+***/
+    }
+
+    void
+    println (String s)
+    {
+        synchronized(this) {
+            try {
+                byte[] bytes =  s.getBytes("US-ASCII");
+
+                //System.out.println("MI>> " + s);
+
+                mOut.write(bytes);
+                mOut.write('\r');
+            } catch (IOException ex) {
+                ex.printStackTrace();
+            }
+        }
+    }
+
+    void
+    print (String s)
+    {
+        synchronized(this) {
+            try {
+                byte[] bytes =  s.getBytes("US-ASCII");
+
+                //System.out.println("MI>> " + s + " (no <cr>)");
+
+                mOut.write(bytes);
+            } catch (IOException ex) {
+                ex.printStackTrace();
+            }
+        }
+    }
+
+
+    @Override
+    public void
+    shutdown()
+    {
+        Looper looper = mHandlerThread.getLooper();
+        if (looper != null) {
+            looper.quit();
+        }
+
+        try {
+            mIn.close();
+        } catch (IOException ex) {
+        }
+        try {
+            mOut.close();
+        } catch (IOException ex) {
+        }
+    }
+
+
+    static final String [][] sDefaultResponses = {
+        {"E0Q0V1",   null},
+        {"+CMEE=2",  null},
+        {"+CREG=2",  null},
+        {"+CGREG=2", null},
+        {"+CCWA=1",  null},
+        {"+COPS=0",  null},
+        {"+CFUN=1",  null},
+        {"+CGMI",    "+CGMI: Android Model AT Interpreter\r"},
+        {"+CGMM",    "+CGMM: Android Model AT Interpreter\r"},
+        {"+CGMR",    "+CGMR: 1.0\r"},
+        {"+CGSN",    "000000000000000\r"},
+        {"+CIMI",    "320720000000000\r"},
+        {"+CSCS=?",  "+CSCS: (\"HEX\",\"UCS2\")\r"},
+        {"+CFUN?",   "+CFUN: 1\r"},
+        {"+COPS=3,0;+COPS?;+COPS=3,1;+COPS?;+COPS=3,2;+COPS?",
+                "+COPS: 0,0,\"Android\"\r"
+                + "+COPS: 0,1,\"Android\"\r"
+                + "+COPS: 0,2,\"310995\"\r"},
+        {"+CREG?",   "+CREG: 2,5, \"0113\", \"6614\"\r"},
+        {"+CGREG?",  "+CGREG: 2,0\r"},
+        {"+CSQ",     "+CSQ: 16,99\r"},
+        {"+CNMI?",   "+CNMI: 1,2,2,1,1\r"},
+        {"+CLIR?",   "+CLIR: 1,3\r"},
+        {"%CPVWI=2", "%CPVWI: 0\r"},
+        {"+CUSD=1,\"#646#\"",  "+CUSD=0,\"You have used 23 minutes\"\r"},
+        {"+CRSM=176,12258,0,0,10", "+CRSM: 144,0,981062200050259429F6\r"},
+        {"+CRSM=192,12258,0,0,15", "+CRSM: 144,0,0000000A2FE204000FF55501020000\r"},
+
+        /* EF[ADN] */
+        {"+CRSM=192,28474,0,0,15", "+CRSM: 144,0,0000005a6f3a040011f5220102011e\r"},
+        {"+CRSM=178,28474,1,4,30", "+CRSM: 144,0,437573746f6d65722043617265ffffff07818100398799f7ffffffffffff\r"},
+        {"+CRSM=178,28474,2,4,30", "+CRSM: 144,0,566f696365204d61696cffffffffffff07918150367742f3ffffffffffff\r"},
+        {"+CRSM=178,28474,3,4,30", "+CRSM: 144,0,4164676a6dffffffffffffffffffffff0b918188551512c221436587ff01\r"},
+        {"+CRSM=178,28474,4,4,30", "+CRSM: 144,0,810101c1ffffffffffffffffffffffff068114455245f8ffffffffffffff\r"},
+        /* EF[EXT1] */
+        {"+CRSM=192,28490,0,0,15", "+CRSM: 144,0,000000416f4a040011f5550102010d\r"},
+        {"+CRSM=178,28490,1,4,13", "+CRSM: 144,0,0206092143658709ffffffffff\r"}
+    };
+}
diff --git a/com/android/internal/telephony/test/SimulatedCommands.java b/com/android/internal/telephony/test/SimulatedCommands.java
new file mode 100644
index 0000000..0de2bec
--- /dev/null
+++ b/com/android/internal/telephony/test/SimulatedCommands.java
@@ -0,0 +1,2172 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.test;
+
+import android.hardware.radio.V1_0.DataRegStateResult;
+import android.hardware.radio.V1_0.VoiceRegStateResult;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.SystemClock;
+import android.os.WorkSource;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoGsm;
+import android.telephony.IccOpenLogicalChannelResponse;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.BaseCommands;
+import com.android.internal.telephony.CallFailCause;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.LastCallFailCause;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RadioCapability;
+import com.android.internal.telephony.SmsResponse;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.dataconnection.DataCallResponse;
+import com.android.internal.telephony.dataconnection.DataProfile;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.uicc.IccCardStatus;
+import com.android.internal.telephony.uicc.IccIoResult;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class SimulatedCommands extends BaseCommands
+        implements CommandsInterface, SimulatedRadioControl {
+    private final static String LOG_TAG = "SimulatedCommands";
+
+    private enum SimLockState {
+        NONE,
+        REQUIRE_PIN,
+        REQUIRE_PUK,
+        SIM_PERM_LOCKED
+    }
+
+    private enum SimFdnState {
+        NONE,
+        REQUIRE_PIN2,
+        REQUIRE_PUK2,
+        SIM_PERM_LOCKED
+    }
+
+    private final static SimLockState INITIAL_LOCK_STATE = SimLockState.NONE;
+    public final static String DEFAULT_SIM_PIN_CODE = "1234";
+    private final static String SIM_PUK_CODE = "12345678";
+    private final static SimFdnState INITIAL_FDN_STATE = SimFdnState.NONE;
+    public final static String DEFAULT_SIM_PIN2_CODE = "5678";
+    private final static String SIM_PUK2_CODE = "87654321";
+    public final static String FAKE_LONG_NAME = "Fake long name";
+    public final static String FAKE_SHORT_NAME = "Fake short name";
+    public final static String FAKE_MCC_MNC = "310260";
+    public final static String FAKE_IMEI = "012345678901234";
+    public final static String FAKE_IMEISV = "99";
+    public final static String FAKE_ESN = "1234";
+    public final static String FAKE_MEID = "1234";
+    public final static int DEFAULT_PIN1_ATTEMPT = 5;
+    public final static int DEFAULT_PIN2_ATTEMPT = 5;
+
+    private String mImei;
+    private String mImeiSv;
+
+    //***** Instance Variables
+
+    SimulatedGsmCallState simulatedCallState;
+    HandlerThread mHandlerThread;
+    SimLockState mSimLockedState;
+    boolean mSimLockEnabled;
+    int mPinUnlockAttempts;
+    int mPukUnlockAttempts;
+    String mPinCode;
+    int mPin1attemptsRemaining = DEFAULT_PIN1_ATTEMPT;
+    SimFdnState mSimFdnEnabledState;
+    boolean mSimFdnEnabled;
+    int mPin2UnlockAttempts;
+    int mPuk2UnlockAttempts;
+    int mNetworkType;
+    String mPin2Code;
+    boolean mSsnNotifyOn = false;
+    private int mVoiceRegState = ServiceState.RIL_REG_STATE_HOME;
+    private int mVoiceRadioTech = ServiceState.RIL_RADIO_TECHNOLOGY_UMTS;
+    private int mDataRegState = ServiceState.RIL_REG_STATE_HOME;
+    private int mDataRadioTech = ServiceState.RIL_RADIO_TECHNOLOGY_UMTS;
+    private SignalStrength mSignalStrength;
+    private List<CellInfo> mCellInfoList;
+    private int[] mImsRegState;
+    private IccCardStatus mIccCardStatus;
+    private IccIoResult mIccIoResultForApduLogicalChannel;
+    private int mChannelId = IccOpenLogicalChannelResponse.INVALID_CHANNEL;
+
+    int mPausedResponseCount;
+    ArrayList<Message> mPausedResponses = new ArrayList<Message>();
+
+    int mNextCallFailCause = CallFailCause.NORMAL_CLEARING;
+
+    private boolean mDcSuccess = true;
+    private DataCallResponse mDcResponse;
+
+    //***** Constructor
+    public
+    SimulatedCommands() {
+        super(null);  // Don't log statistics
+        mHandlerThread = new HandlerThread("SimulatedCommands");
+        mHandlerThread.start();
+        Looper looper = mHandlerThread.getLooper();
+
+        simulatedCallState = new SimulatedGsmCallState(looper);
+
+        setRadioState(RadioState.RADIO_ON);
+        mSimLockedState = INITIAL_LOCK_STATE;
+        mSimLockEnabled = (mSimLockedState != SimLockState.NONE);
+        mPinCode = DEFAULT_SIM_PIN_CODE;
+        mSimFdnEnabledState = INITIAL_FDN_STATE;
+        mSimFdnEnabled = (mSimFdnEnabledState != SimFdnState.NONE);
+        mPin2Code = DEFAULT_SIM_PIN2_CODE;
+    }
+
+    public void dispose() {
+        if (mHandlerThread != null) {
+            mHandlerThread.quit();
+        }
+    }
+
+    private void log(String str) {
+        Rlog.d(LOG_TAG, str);
+    }
+
+    //***** CommandsInterface implementation
+
+    @Override
+    public void getIccCardStatus(Message result) {
+        if(mIccCardStatus!=null) {
+            resultSuccess(result, mIccCardStatus);
+        } else {
+            resultFail(result, null, new RuntimeException("IccCardStatus not set"));
+        }
+    }
+
+    @Override
+    public void supplyIccPin(String pin, Message result)  {
+        if (mSimLockedState != SimLockState.REQUIRE_PIN) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin: wrong state, state=" +
+                    mSimLockedState);
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+            return;
+        }
+
+        if (pin != null && pin.equals(mPinCode)) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin: success!");
+            mPinUnlockAttempts = 0;
+            mSimLockedState = SimLockState.NONE;
+            mIccStatusChangedRegistrants.notifyRegistrants();
+
+            resultSuccess(result, null);
+
+            return;
+        }
+
+        if (result != null) {
+            mPinUnlockAttempts ++;
+
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin: failed! attempt=" +
+                    mPinUnlockAttempts);
+            if (mPinUnlockAttempts >= DEFAULT_PIN1_ATTEMPT) {
+                Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin: set state to REQUIRE_PUK");
+                mSimLockedState = SimLockState.REQUIRE_PUK;
+            }
+
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+        }
+    }
+
+    @Override
+    public void supplyIccPuk(String puk, String newPin, Message result)  {
+        if (mSimLockedState != SimLockState.REQUIRE_PUK) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk: wrong state, state=" +
+                    mSimLockedState);
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+            return;
+        }
+
+        if (puk != null && puk.equals(SIM_PUK_CODE)) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk: success!");
+            mSimLockedState = SimLockState.NONE;
+            mPukUnlockAttempts = 0;
+            mIccStatusChangedRegistrants.notifyRegistrants();
+
+            resultSuccess(result, null);
+            return;
+        }
+
+        if (result != null) {
+            mPukUnlockAttempts ++;
+
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk: failed! attempt=" +
+                    mPukUnlockAttempts);
+            if (mPukUnlockAttempts >= 10) {
+                Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk: set state to SIM_PERM_LOCKED");
+                mSimLockedState = SimLockState.SIM_PERM_LOCKED;
+            }
+
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+        }
+    }
+
+    @Override
+    public void supplyIccPin2(String pin2, Message result)  {
+        if (mSimFdnEnabledState != SimFdnState.REQUIRE_PIN2) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin2: wrong state, state=" +
+                    mSimFdnEnabledState);
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+            return;
+        }
+
+        if (pin2 != null && pin2.equals(mPin2Code)) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin2: success!");
+            mPin2UnlockAttempts = 0;
+            mSimFdnEnabledState = SimFdnState.NONE;
+
+            resultSuccess(result, null);
+            return;
+        }
+
+        if (result != null) {
+            mPin2UnlockAttempts ++;
+
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin2: failed! attempt=" +
+                    mPin2UnlockAttempts);
+            if (mPin2UnlockAttempts >= DEFAULT_PIN2_ATTEMPT) {
+                Rlog.i(LOG_TAG, "[SimCmd] supplyIccPin2: set state to REQUIRE_PUK2");
+                mSimFdnEnabledState = SimFdnState.REQUIRE_PUK2;
+            }
+
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+        }
+    }
+
+    @Override
+    public void supplyIccPuk2(String puk2, String newPin2, Message result)  {
+        if (mSimFdnEnabledState != SimFdnState.REQUIRE_PUK2) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk2: wrong state, state=" +
+                    mSimLockedState);
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+            return;
+        }
+
+        if (puk2 != null && puk2.equals(SIM_PUK2_CODE)) {
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk2: success!");
+            mSimFdnEnabledState = SimFdnState.NONE;
+            mPuk2UnlockAttempts = 0;
+
+            resultSuccess(result, null);
+            return;
+        }
+
+        if (result != null) {
+            mPuk2UnlockAttempts ++;
+
+            Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk2: failed! attempt=" +
+                    mPuk2UnlockAttempts);
+            if (mPuk2UnlockAttempts >= 10) {
+                Rlog.i(LOG_TAG, "[SimCmd] supplyIccPuk2: set state to SIM_PERM_LOCKED");
+                mSimFdnEnabledState = SimFdnState.SIM_PERM_LOCKED;
+            }
+
+            CommandException ex = new CommandException(
+                    CommandException.Error.PASSWORD_INCORRECT);
+            resultFail(result, null, ex);
+        }
+    }
+
+    @Override
+    public void changeIccPin(String oldPin, String newPin, Message result)  {
+        if (oldPin != null && oldPin.equals(mPinCode)) {
+            mPinCode = newPin;
+            resultSuccess(result, null);
+
+            return;
+        }
+
+        Rlog.i(LOG_TAG, "[SimCmd] changeIccPin: pin failed!");
+
+        CommandException ex = new CommandException(
+                CommandException.Error.PASSWORD_INCORRECT);
+        resultFail(result, null, ex);
+    }
+
+    @Override
+    public void changeIccPin2(String oldPin2, String newPin2, Message result) {
+        if (oldPin2 != null && oldPin2.equals(mPin2Code)) {
+            mPin2Code = newPin2;
+            resultSuccess(result, null);
+
+            return;
+        }
+
+        Rlog.i(LOG_TAG, "[SimCmd] changeIccPin2: pin2 failed!");
+
+        CommandException ex = new CommandException(
+                CommandException.Error.PASSWORD_INCORRECT);
+        resultFail(result, null, ex);
+    }
+
+    @Override
+    public void
+    changeBarringPassword(String facility, String oldPwd, String newPwd, Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void
+    setSuppServiceNotifications(boolean enable, Message result) {
+        resultSuccess(result, null);
+
+        if (enable && mSsnNotifyOn) {
+            Rlog.w(LOG_TAG, "Supp Service Notifications already enabled!");
+        }
+
+        mSsnNotifyOn = enable;
+    }
+
+    @Override
+    public void queryFacilityLock(String facility, String pin,
+                                   int serviceClass, Message result) {
+        queryFacilityLockForApp(facility, pin, serviceClass, null, result);
+    }
+
+    @Override
+    public void queryFacilityLockForApp(String facility, String pin, int serviceClass,
+            String appId, Message result) {
+        if (facility != null && facility.equals(CommandsInterface.CB_FACILITY_BA_SIM)) {
+            if (result != null) {
+                int[] r = new int[1];
+                r[0] = (mSimLockEnabled ? 1 : 0);
+                Rlog.i(LOG_TAG, "[SimCmd] queryFacilityLock: SIM is "
+                        + (r[0] == 0 ? "unlocked" : "locked"));
+                resultSuccess(result, r);
+            }
+            return;
+        } else if (facility != null && facility.equals(CommandsInterface.CB_FACILITY_BA_FD)) {
+            if (result != null) {
+                int[] r = new int[1];
+                r[0] = (mSimFdnEnabled ? 1 : 0);
+                Rlog.i(LOG_TAG, "[SimCmd] queryFacilityLock: FDN is "
+                        + (r[0] == 0 ? "disabled" : "enabled"));
+                resultSuccess(result, r);
+            }
+            return;
+        }
+
+        unimplemented(result);
+    }
+
+    @Override
+    public void setFacilityLock(String facility, boolean lockEnabled, String pin, int serviceClass,
+            Message result) {
+        setFacilityLockForApp(facility, lockEnabled, pin, serviceClass, null, result);
+    }
+
+    @Override
+    public void setFacilityLockForApp(String facility, boolean lockEnabled,
+                                 String pin, int serviceClass, String appId,
+                                 Message result) {
+        if (facility != null &&
+                facility.equals(CommandsInterface.CB_FACILITY_BA_SIM)) {
+            if (pin != null && pin.equals(mPinCode)) {
+                Rlog.i(LOG_TAG, "[SimCmd] setFacilityLock: pin is valid");
+                mSimLockEnabled = lockEnabled;
+
+                resultSuccess(result, null);
+
+                return;
+            }
+
+            Rlog.i(LOG_TAG, "[SimCmd] setFacilityLock: pin failed!");
+
+            CommandException ex = new CommandException(
+                    CommandException.Error.GENERIC_FAILURE);
+            resultFail(result, null, ex);
+
+            return;
+        }  else if (facility != null &&
+                facility.equals(CommandsInterface.CB_FACILITY_BA_FD)) {
+            if (pin != null && pin.equals(mPin2Code)) {
+                Rlog.i(LOG_TAG, "[SimCmd] setFacilityLock: pin2 is valid");
+                mSimFdnEnabled = lockEnabled;
+
+                resultSuccess(result, null);
+
+                return;
+            }
+
+            Rlog.i(LOG_TAG, "[SimCmd] setFacilityLock: pin2 failed!");
+
+            CommandException ex = new CommandException(
+                    CommandException.Error.GENERIC_FAILURE);
+            resultFail(result, null, ex);
+
+            return;
+        }
+
+        unimplemented(result);
+    }
+
+    @Override
+    public void supplyNetworkDepersonalization(String netpin, Message result) {
+        unimplemented(result);
+    }
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result contains a List of DriverCall
+     *      The ar.result List is sorted by DriverCall.index
+     */
+    @Override
+    public void getCurrentCalls (Message result) {
+        SimulatedCommandsVerifier.getInstance().getCurrentCalls(result);
+        if ((mState == RadioState.RADIO_ON) && !isSimLocked()) {
+            //Rlog.i("GSM", "[SimCmds] getCurrentCalls");
+            resultSuccess(result, simulatedCallState.getDriverCalls());
+        } else {
+            //Rlog.i("GSM", "[SimCmds] getCurrentCalls: RADIO_OFF or SIM not ready!");
+            resultFail(result, null,
+                new CommandException(CommandException.Error.RADIO_NOT_AVAILABLE));
+        }
+    }
+
+    /**
+     *  @deprecated
+     */
+    @Deprecated
+    @Override
+    public void getPDPContextList(Message result) {
+        getDataCallList(result);
+    }
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result contains a List of DataCallResponse
+     */
+    @Override
+    public void getDataCallList(Message result) {
+        resultSuccess(result, new ArrayList<DataCallResponse>(0));
+    }
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     *
+     * CLIR_DEFAULT     == on "use subscription default value"
+     * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation)
+     * CLIR_INVOCATION  == on "CLIR invocation" (restrict CLI presentation)
+     */
+    @Override
+    public void dial (String address, int clirMode, Message result) {
+        SimulatedCommandsVerifier.getInstance().dial(address, clirMode, result);
+        simulatedCallState.onDial(address);
+
+        resultSuccess(result, null);
+    }
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     *
+     * CLIR_DEFAULT     == on "use subscription default value"
+     * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation)
+     * CLIR_INVOCATION  == on "CLIR invocation" (restrict CLI presentation)
+     */
+    @Override
+    public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) {
+        SimulatedCommandsVerifier.getInstance().dial(address, clirMode, uusInfo, result);
+        simulatedCallState.onDial(address);
+
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void getIMSI(Message result) {
+        getIMSIForApp(null, result);
+    }
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is String containing IMSI on success
+     */
+    @Override
+    public void getIMSIForApp(String aid, Message result) {
+        resultSuccess(result, "012345678901234");
+    }
+
+    public void setIMEI(String imei) {
+        mImei = imei;
+    }
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is String containing IMEI on success
+     */
+    @Override
+    public void getIMEI(Message result) {
+        SimulatedCommandsVerifier.getInstance().getIMEI(result);
+        resultSuccess(result, mImei != null ? mImei : FAKE_IMEI);
+    }
+
+    public void setIMEISV(String imeisv) {
+        mImeiSv = imeisv;
+    }
+
+    /**
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is String containing IMEISV on success
+     */
+    @Override
+    public void getIMEISV(Message result) {
+        SimulatedCommandsVerifier.getInstance().getIMEISV(result);
+        resultSuccess(result, mImeiSv != null ? mImeiSv : FAKE_IMEISV);
+    }
+
+    /**
+     * Hang up one individual connection.
+     *  returned message
+     *  retMsg.obj = AsyncResult ar
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     *
+     *  3GPP 22.030 6.5.5
+     *  "Releases a specific active call X"
+     */
+    @Override
+    public void hangupConnection (int gsmIndex, Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('1', (char)('0'+gsmIndex));
+
+        if (!success){
+            Rlog.i("GSM", "[SimCmd] hangupConnection: resultFail");
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            Rlog.i("GSM", "[SimCmd] hangupConnection: resultSuccess");
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * 3GPP 22.030 6.5.5
+     *  "Releases all held calls or sets User Determined User Busy (UDUB)
+     *   for a waiting call."
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void hangupWaitingOrBackground (Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('0', '\0');
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Releases all active calls (if any exist) and accepts
+     *  the other (held or waiting) call."
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void hangupForegroundResumeBackground (Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('1', '\0');
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Places all active calls (if any exist) on hold and accepts
+     *  the other (held or waiting) call."
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void switchWaitingOrHoldingAndActive (Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('2', '\0');
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Adds a held call to the conversation"
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void conference (Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('3', '\0');
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Connects the two calls and disconnects the subscriber from both calls"
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void explicitCallTransfer (Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('4', '\0');
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * 3GPP 22.030 6.5.5
+     * "Places all active calls on hold except call X with which
+     *  communication shall be supported."
+     */
+    @Override
+    public void separateConnection (int gsmIndex, Message result) {
+        boolean success;
+
+        char ch = (char)(gsmIndex + '0');
+        success = simulatedCallState.onChld('2', ch);
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     *
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void acceptCall (Message result) {
+        boolean success;
+
+        SimulatedCommandsVerifier.getInstance().acceptCall(result);
+        success = simulatedCallState.onAnswer();
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     *  also known as UDUB
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void rejectCall (Message result) {
+        boolean success;
+
+        success = simulatedCallState.onChld('0', '\0');
+
+        if (!success){
+            resultFail(result, null, new RuntimeException("Hangup Error"));
+        } else {
+            resultSuccess(result, null);
+        }
+    }
+
+    /**
+     * cause code returned as Integer in Message.obj.response
+     * Returns integer cause code defined in TS 24.008
+     * Annex H or closest approximation.
+     * Most significant codes:
+     * - Any defined in 22.001 F.4 (for generating busy/congestion)
+     * - Cause 68: ACM >= ACMMax
+     */
+    @Override
+    public void getLastCallFailCause (Message result) {
+        LastCallFailCause mFailCause = new LastCallFailCause();
+        mFailCause.causeCode = mNextCallFailCause;
+        resultSuccess(result, mFailCause);
+    }
+
+    /**
+     * @deprecated
+     */
+    @Deprecated
+    @Override
+    public void getLastPdpFailCause (Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void getLastDataCallFailCause(Message result) {
+        //
+        unimplemented(result);
+    }
+
+    @Override
+    public void setMute (boolean enableMute, Message result) {unimplemented(result);}
+
+    @Override
+    public void getMute (Message result) {unimplemented(result);}
+
+    public void setSignalStrength(SignalStrength signalStrength) {
+        mSignalStrength = signalStrength;
+    }
+
+    @Override
+    public void getSignalStrength (Message result) {
+
+        if (mSignalStrength == null) {
+            mSignalStrength = new SignalStrength(
+                20, // gsmSignalStrength
+                0,  // gsmBitErrorRate
+                -1, // cdmaDbm
+                -1, // cdmaEcio
+                -1, // evdoDbm
+                -1, // evdoEcio
+                -1, // evdoSnr
+                99, // lteSignalStrength
+                SignalStrength.INVALID,     // lteRsrp
+                SignalStrength.INVALID,     // lteRsrq
+                SignalStrength.INVALID,     // lteRssnr
+                SignalStrength.INVALID,     // lteCqi
+                SignalStrength.INVALID,     // tdScdmaRscp
+                true                        // gsmFlag
+            );
+        }
+
+        resultSuccess(result, mSignalStrength);
+    }
+
+     /**
+     * Assign a specified band for RF configuration.
+     *
+     * @param bandMode one of BM_*_BAND
+     * @param result is callback message
+     */
+    @Override
+    public void setBandMode (int bandMode, Message result) {
+        resultSuccess(result, null);
+    }
+
+    /**
+     * Query the list of band mode supported by RF.
+     *
+     * @param result is callback message
+     *        ((AsyncResult)response.obj).result  is an int[] where int[0] is
+     *        the size of the array and the rest of each element representing
+     *        one available BM_*_BAND
+     */
+    @Override
+    public void queryAvailableBandMode (Message result) {
+        int ret[] = new int [4];
+
+        ret[0] = 4;
+        ret[1] = Phone.BM_US_BAND;
+        ret[2] = Phone.BM_JPN_BAND;
+        ret[3] = Phone.BM_AUS_BAND;
+
+        resultSuccess(result, ret);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sendTerminalResponse(String contents, Message response) {
+        resultSuccess(response, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sendEnvelope(String contents, Message response) {
+        resultSuccess(response, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sendEnvelopeWithStatus(String contents, Message response) {
+        resultSuccess(response, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void handleCallSetupRequestFromSim(
+            boolean accept, Message response) {
+        resultSuccess(response, null);
+    }
+
+    public void setVoiceRadioTech(int voiceRadioTech) {
+        mVoiceRadioTech = voiceRadioTech;
+    }
+
+    public void setVoiceRegState(int voiceRegState) {
+        mVoiceRegState = voiceRegState;
+    }
+
+    /**
+     * response.obj.result is an String[14]
+     * See ril.h for details
+     *
+     * Please note that registration state 4 ("unknown") is treated
+     * as "out of service" above
+     */
+    @Override
+    public void getVoiceRegistrationState(Message result) {
+        mGetVoiceRegistrationStateCallCount.incrementAndGet();
+
+        VoiceRegStateResult ret = new VoiceRegStateResult();
+        ret.regState = mVoiceRegState;
+        ret.rat = mVoiceRadioTech;
+
+        resultSuccess(result, ret);
+    }
+
+    private final AtomicInteger mGetVoiceRegistrationStateCallCount = new AtomicInteger(0);
+
+    @VisibleForTesting
+    public int getGetVoiceRegistrationStateCallCount() {
+        return mGetVoiceRegistrationStateCallCount.get();
+    }
+
+    public void setDataRadioTech(int radioTech) {
+        mDataRadioTech = radioTech;
+    }
+
+    public void setDataRegState(int dataRegState) {
+        mDataRegState = dataRegState;
+    }
+
+    @Override
+    public void getDataRegistrationState (Message result) {
+        mGetDataRegistrationStateCallCount.incrementAndGet();
+
+        DataRegStateResult ret = new DataRegStateResult();
+        ret.regState = mDataRegState;
+        ret.rat = mDataRadioTech;
+
+        resultSuccess(result, ret);
+    }
+
+    private final AtomicInteger mGetDataRegistrationStateCallCount = new AtomicInteger(0);
+
+    @VisibleForTesting
+    public int getGetDataRegistrationStateCallCount() {
+        return mGetDataRegistrationStateCallCount.get();
+    }
+
+    /**
+     * response.obj.result is a String[3]
+     * response.obj.result[0] is long alpha or null if unregistered
+     * response.obj.result[1] is short alpha or null if unregistered
+     * response.obj.result[2] is numeric or null if unregistered
+     */
+    @Override
+    public void getOperator(Message result) {
+        mGetOperatorCallCount.incrementAndGet();
+        String[] ret = new String[3];
+
+        ret[0] = FAKE_LONG_NAME;
+        ret[1] = FAKE_SHORT_NAME;
+        ret[2] = FAKE_MCC_MNC;
+
+        resultSuccess(result, ret);
+    }
+
+    private final AtomicInteger mGetOperatorCallCount = new AtomicInteger(0);
+
+    @VisibleForTesting
+    public int getGetOperatorCallCount() {
+        final int count = mGetOperatorCallCount.get();
+        return mGetOperatorCallCount.get();
+    }
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void sendDtmf(char c, Message result) {
+        resultSuccess(result, null);
+    }
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void startDtmf(char c, Message result) {
+        resultSuccess(result, null);
+    }
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void stopDtmf(Message result) {
+        resultSuccess(result, null);
+    }
+
+    /**
+     *  ar.exception carries exception on failure
+     *  ar.userObject contains the original value of result.obj
+     *  ar.result is null on success and failure
+     */
+    @Override
+    public void sendBurstDtmf(String dtmfString, int on, int off, Message result) {
+        SimulatedCommandsVerifier.getInstance().sendBurstDtmf(dtmfString, on, off, result);
+        resultSuccess(result, null);
+    }
+
+    /**
+     * smscPDU is smsc address in PDU form GSM BCD format prefixed
+     *      by a length byte (as expected by TS 27.005) or NULL for default SMSC
+     * pdu is SMS in PDU format as an ASCII hex string
+     *      less the SMSC address
+     */
+    @Override
+    public void sendSMS (String smscPDU, String pdu, Message result) {
+        SimulatedCommandsVerifier.getInstance().sendSMS(smscPDU, pdu, result);
+        resultSuccess(result, new SmsResponse(0 /*messageRef*/, null, 0));
+    }
+
+    /**
+     * Send an SMS message, Identical to sendSMS,
+     * except that more messages are expected to be sent soon
+     * smscPDU is smsc address in PDU form GSM BCD format prefixed
+     *      by a length byte (as expected by TS 27.005) or NULL for default SMSC
+     * pdu is SMS in PDU format as an ASCII hex string
+     *      less the SMSC address
+     */
+    @Override
+    public void sendSMSExpectMore (String smscPDU, String pdu, Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void deleteSmsOnSim(int index, Message response) {
+        Rlog.d(LOG_TAG, "Delete message at index " + index);
+        unimplemented(response);
+    }
+
+    @Override
+    public void deleteSmsOnRuim(int index, Message response) {
+        Rlog.d(LOG_TAG, "Delete RUIM message at index " + index);
+        unimplemented(response);
+    }
+
+    @Override
+    public void writeSmsToSim(int status, String smsc, String pdu, Message response) {
+        Rlog.d(LOG_TAG, "Write SMS to SIM with status " + status);
+        unimplemented(response);
+    }
+
+    @Override
+    public void writeSmsToRuim(int status, String pdu, Message response) {
+        Rlog.d(LOG_TAG, "Write SMS to RUIM with status " + status);
+        unimplemented(response);
+    }
+
+    public void setDataCallResponse(final boolean success, final DataCallResponse dcResponse) {
+        mDcResponse = dcResponse;
+        mDcSuccess = success;
+    }
+
+    public void triggerNITZupdate(String NITZStr) {
+        if (NITZStr != null) {
+            mNITZTimeRegistrant.notifyRegistrant(new AsyncResult (null, new Object[]{NITZStr,
+                    SystemClock.elapsedRealtime()}, null));
+        }
+    }
+
+    @Override
+    public void setupDataCall(int radioTechnology, DataProfile dataProfile, boolean isRoaming,
+                              boolean allowRoaming, Message result) {
+
+        SimulatedCommandsVerifier.getInstance().setupDataCall(radioTechnology, dataProfile,
+                isRoaming, allowRoaming, result);
+
+        if (mDcResponse == null) {
+            mDcResponse = new DataCallResponse(0, -1, 1, 2, "IP", "rmnet_data7",
+                    "12.34.56.78", "98.76.54.32", "11.22.33.44", "", 1440);
+        }
+
+        if (mDcSuccess) {
+            resultSuccess(result, mDcResponse);
+        } else {
+            resultFail(result, mDcResponse, new RuntimeException("Setup data call failed!"));
+        }
+    }
+
+    @Override
+    public void deactivateDataCall(int cid, int reason, Message result) {
+        SimulatedCommandsVerifier.getInstance().deactivateDataCall(cid, reason, result);
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void setPreferredNetworkType(int networkType , Message result) {
+        SimulatedCommandsVerifier.getInstance().setPreferredNetworkType(networkType, result);
+        mNetworkType = networkType;
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void getPreferredNetworkType(Message result) {
+        SimulatedCommandsVerifier.getInstance().getPreferredNetworkType(result);
+        int ret[] = new int[1];
+
+        ret[0] = mNetworkType;
+        resultSuccess(result, ret);
+    }
+
+    @Override
+    public void getNeighboringCids(Message result, WorkSource workSource) {
+        int ret[] = new int[7];
+
+        ret[0] = 6;
+        for (int i = 1; i<7; i++) {
+            ret[i] = i;
+        }
+        resultSuccess(result, ret);
+    }
+
+    @Override
+    public void setLocationUpdates(boolean enable, Message response) {
+        SimulatedCommandsVerifier.getInstance().setLocationUpdates(enable, response);
+        resultSuccess(response, null);
+    }
+
+    @Override
+    public void getSmscAddress(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void setSmscAddress(String address, Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void reportSmsMemoryStatus(boolean available, Message result) {
+        resultSuccess(result, null);
+        SimulatedCommandsVerifier.getInstance().reportSmsMemoryStatus(available, result);
+    }
+
+    @Override
+    public void reportStkServiceIsRunning(Message result) {
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void getCdmaSubscriptionSource(Message result) {
+        unimplemented(result);
+    }
+
+    private boolean isSimLocked() {
+        if (mSimLockedState != SimLockState.NONE) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void setRadioPower(boolean on, Message result) {
+        if(on) {
+            setRadioState(RadioState.RADIO_ON);
+        } else {
+            setRadioState(RadioState.RADIO_OFF);
+        }
+    }
+
+
+    @Override
+    public void acknowledgeLastIncomingGsmSms(boolean success, int cause, Message result) {
+        unimplemented(result);
+        SimulatedCommandsVerifier.getInstance().
+                acknowledgeLastIncomingGsmSms(success, cause, result);
+    }
+
+    @Override
+    public void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu,
+            Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void iccIO(int command, int fileid, String path, int p1, int p2, int p3, String data,
+            String pin2, Message response) {
+        iccIOForApp(command, fileid, path, p1, p2, p3, data, pin2, null, response);
+    }
+
+    /**
+     * parameters equivalent to 27.007 AT+CRSM command
+     * response.obj will be an AsyncResult
+     * response.obj.userObj will be a SimIoResult on success
+     */
+    @Override
+    public void iccIOForApp (int command, int fileid, String path, int p1, int p2,
+                       int p3, String data, String pin2, String aid, Message result) {
+        unimplemented(result);
+    }
+
+    /**
+     * (AsyncResult)response.obj).result is an int[] with element [0] set to
+     * 1 for "CLIP is provisioned", and 0 for "CLIP is not provisioned".
+     *
+     * @param response is callback message
+     */
+    @Override
+    public void queryCLIP(Message response) { unimplemented(response); }
+
+
+    /**
+     * response.obj will be a an int[2]
+     *
+     * response.obj[0] will be TS 27.007 +CLIR parameter 'n'
+     *  0 presentation indicator is used according to the subscription of the CLIR service
+     *  1 CLIR invocation
+     *  2 CLIR suppression
+     *
+     * response.obj[1] will be TS 27.007 +CLIR parameter 'm'
+     *  0 CLIR not provisioned
+     *  1 CLIR provisioned in permanent mode
+     *  2 unknown (e.g. no network, etc.)
+     *  3 CLIR temporary mode presentation restricted
+     *  4 CLIR temporary mode presentation allowed
+     */
+
+    @Override
+    public void getCLIR(Message result) {unimplemented(result);}
+
+    /**
+     * clirMode is one of the CLIR_* constants above
+     *
+     * response.obj is null
+     */
+
+    @Override
+    public void setCLIR(int clirMode, Message result) {unimplemented(result);}
+
+    /**
+     * (AsyncResult)response.obj).result is an int[] with element [0] set to
+     * 0 for disabled, 1 for enabled.
+     *
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param response is callback message
+     */
+
+    @Override
+    public void queryCallWaiting(int serviceClass, Message response) {
+        unimplemented(response);
+    }
+
+    /**
+     * @param enable is true to enable, false to disable
+     * @param serviceClass is a sum of SERVICE_CLASS_*
+     * @param response is callback message
+     */
+
+    @Override
+    public void setCallWaiting(boolean enable, int serviceClass,
+            Message response) {
+        unimplemented(response);
+    }
+
+    /**
+     * @param action is one of CF_ACTION_*
+     * @param cfReason is one of CF_REASON_*
+     * @param serviceClass is a sum of SERVICE_CLASSS_*
+     */
+    @Override
+    public void setCallForward(int action, int cfReason, int serviceClass,
+            String number, int timeSeconds, Message result) {
+        SimulatedCommandsVerifier.getInstance().setCallForward(action, cfReason, serviceClass,
+                number, timeSeconds, result);
+        resultSuccess(result, null);
+    }
+
+    /**
+     * cfReason is one of CF_REASON_*
+     *
+     * ((AsyncResult)response.obj).result will be an array of
+     * CallForwardInfo's
+     *
+     * An array of length 0 means "disabled for all codes"
+     */
+    @Override
+    public void queryCallForwardStatus(int cfReason, int serviceClass,
+            String number, Message result) {
+        SimulatedCommandsVerifier.getInstance().queryCallForwardStatus(cfReason, serviceClass,
+                number, result);
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message result) {unimplemented(result);}
+    @Override
+    public void exitEmergencyCallbackMode(Message result) {unimplemented(result);}
+    @Override
+    public void setNetworkSelectionModeManual(
+            String operatorNumeric, Message result) {unimplemented(result);}
+
+    /**
+     * Queries whether the current network selection mode is automatic
+     * or manual
+     *
+     * ((AsyncResult)response.obj).result  is an int[] with element [0] being
+     * a 0 for automatic selection and a 1 for manual selection
+     */
+
+    @Override
+    public void getNetworkSelectionMode(Message result) {
+        SimulatedCommandsVerifier.getInstance().getNetworkSelectionMode(result);
+        getNetworkSelectionModeCallCount.incrementAndGet();
+        int ret[] = new int[1];
+
+        ret[0] = 0;
+        resultSuccess(result, ret);
+    }
+
+    private final AtomicInteger getNetworkSelectionModeCallCount = new AtomicInteger(0);
+
+    @VisibleForTesting
+    public int getGetNetworkSelectionModeCallCount() {
+        return getNetworkSelectionModeCallCount.get();
+    }
+
+    /**
+     * Queries the currently available networks
+     *
+     * ((AsyncResult)response.obj).result  is a List of NetworkInfo objects
+     */
+    @Override
+    public void getAvailableNetworks(Message result) {
+        unimplemented(result);
+    }
+
+    /**
+     * Starts a network scan
+     */
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message result) {
+        unimplemented(result);
+    }
+
+    /**
+     * Stops an ongoing network scan
+     */
+    @Override
+    public void stopNetworkScan(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void getBasebandVersion (Message result) {
+        SimulatedCommandsVerifier.getInstance().getBasebandVersion(result);
+        resultSuccess(result, "SimulatedCommands");
+    }
+
+    /**
+     * Simulates an Stk Call Control Alpha message
+     * @param alphaString Alpha string to send.
+     */
+    public void triggerIncomingStkCcAlpha(String alphaString) {
+        if (mCatCcAlphaRegistrant != null) {
+            mCatCcAlphaRegistrant.notifyResult(alphaString);
+        }
+    }
+
+    public void sendStkCcAplha(String alphaString) {
+        triggerIncomingStkCcAlpha(alphaString);
+    }
+
+    /**
+     * Simulates an incoming USSD message
+     * @param statusCode  Status code string. See <code>setOnUSSD</code>
+     * in CommandsInterface.java
+     * @param message Message text to send or null if none
+     */
+    @Override
+    public void triggerIncomingUssd(String statusCode, String message) {
+        if (mUSSDRegistrant != null) {
+            String[] result = {statusCode, message};
+            mUSSDRegistrant.notifyResult(result);
+        }
+    }
+
+
+    @Override
+    public void sendUSSD (String ussdString, Message result) {
+
+        // We simulate this particular sequence
+        if (ussdString.equals("#646#")) {
+            resultSuccess(result, null);
+
+            // 0 == USSD-Notify
+            triggerIncomingUssd("0", "You have NNN minutes remaining.");
+        } else {
+            resultSuccess(result, null);
+
+            triggerIncomingUssd("0", "All Done");
+        }
+    }
+
+    // inherited javadoc suffices
+    @Override
+    public void cancelPendingUssd (Message response) {
+        resultSuccess(response, null);
+    }
+
+
+    @Override
+    public void resetRadio(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void invokeOemRilRequestRaw(byte[] data, Message response) {
+        // Just echo back data
+        if (response != null) {
+            AsyncResult.forMessage(response).result = data;
+            response.sendToTarget();
+        }
+    }
+
+    @Override
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                Message response) {
+        // Just echo back data
+        if (response != null) {
+            AsyncResult.forMessage(response).result = imsiEncryptionInfo;
+            response.sendToTarget();
+        }
+    }
+
+    @Override
+    public void invokeOemRilRequestStrings(String[] strings, Message response) {
+        // Just echo back data
+        if (response != null) {
+            AsyncResult.forMessage(response).result = strings;
+            response.sendToTarget();
+        }
+    }
+
+    //***** SimulatedRadioControl
+
+
+    /** Start the simulated phone ringing */
+    @Override
+    public void
+    triggerRing(String number) {
+        simulatedCallState.triggerRing(number);
+        mCallStateRegistrants.notifyRegistrants();
+    }
+
+    @Override
+    public void
+    progressConnectingCallState() {
+        simulatedCallState.progressConnectingCallState();
+        mCallStateRegistrants.notifyRegistrants();
+    }
+
+    /** If a call is DIALING or ALERTING, progress it all the way to ACTIVE */
+    @Override
+    public void
+    progressConnectingToActive() {
+        simulatedCallState.progressConnectingToActive();
+        mCallStateRegistrants.notifyRegistrants();
+    }
+
+    /** automatically progress mobile originated calls to ACTIVE.
+     *  default to true
+     */
+    @Override
+    public void
+    setAutoProgressConnectingCall(boolean b) {
+        simulatedCallState.setAutoProgressConnectingCall(b);
+    }
+
+    @Override
+    public void
+    setNextDialFailImmediately(boolean b) {
+        simulatedCallState.setNextDialFailImmediately(b);
+    }
+
+    @Override
+    public void
+    setNextCallFailCause(int gsmCause) {
+        mNextCallFailCause = gsmCause;
+    }
+
+    @Override
+    public void
+    triggerHangupForeground() {
+        simulatedCallState.triggerHangupForeground();
+        mCallStateRegistrants.notifyRegistrants();
+    }
+
+    /** hangup holding calls */
+    @Override
+    public void
+    triggerHangupBackground() {
+        simulatedCallState.triggerHangupBackground();
+        mCallStateRegistrants.notifyRegistrants();
+    }
+
+    @Override
+    public void triggerSsn(int type, int code) {
+        SuppServiceNotification not = new SuppServiceNotification();
+        not.notificationType = type;
+        not.code = code;
+        mSsnRegistrant.notifyRegistrant(new AsyncResult(null, not, null));
+    }
+
+    @Override
+    public void
+    shutdown() {
+        setRadioState(RadioState.RADIO_UNAVAILABLE);
+        Looper looper = mHandlerThread.getLooper();
+        if (looper != null) {
+            looper.quit();
+        }
+    }
+
+    /** hangup all */
+
+    @Override
+    public void
+    triggerHangupAll() {
+        simulatedCallState.triggerHangupAll();
+        mCallStateRegistrants.notifyRegistrants();
+    }
+
+    @Override
+    public void
+    triggerIncomingSMS(String message) {
+        //TODO
+    }
+
+    @Override
+    public void
+    pauseResponses() {
+        mPausedResponseCount++;
+    }
+
+    @Override
+    public void
+    resumeResponses() {
+        mPausedResponseCount--;
+
+        if (mPausedResponseCount == 0) {
+            for (int i = 0, s = mPausedResponses.size(); i < s ; i++) {
+                mPausedResponses.get(i).sendToTarget();
+            }
+            mPausedResponses.clear();
+        } else {
+            Rlog.e("GSM", "SimulatedCommands.resumeResponses < 0");
+        }
+    }
+
+    //***** Private Methods
+
+    private void unimplemented(Message result) {
+        if (result != null) {
+            AsyncResult.forMessage(result).exception
+                = new RuntimeException("Unimplemented");
+
+            if (mPausedResponseCount > 0) {
+                mPausedResponses.add(result);
+            } else {
+                result.sendToTarget();
+            }
+        }
+    }
+
+    private void resultSuccess(Message result, Object ret) {
+        if (result != null) {
+            AsyncResult.forMessage(result).result = ret;
+            if (mPausedResponseCount > 0) {
+                mPausedResponses.add(result);
+            } else {
+                result.sendToTarget();
+            }
+        }
+    }
+
+    private void resultFail(Message result, Object ret, Throwable tr) {
+        if (result != null) {
+            AsyncResult.forMessage(result, ret, tr);
+            if (mPausedResponseCount > 0) {
+                mPausedResponses.add(result);
+            } else {
+                result.sendToTarget();
+            }
+        }
+    }
+
+    // ***** Methods for CDMA support
+    @Override
+    public void
+    getDeviceIdentity(Message response) {
+        SimulatedCommandsVerifier.getInstance().getDeviceIdentity(response);
+        resultSuccess(response, new String[] {FAKE_IMEI, FAKE_IMEISV, FAKE_ESN, FAKE_MEID});
+    }
+
+    @Override
+    public void
+    getCDMASubscription(Message result) {
+        String ret[] = new String[5];
+        ret[0] = "123";
+        ret[1] = "456";
+        ret[2] = "789";
+        ret[3] = "234";
+        ret[4] = "345";
+        resultSuccess(result, ret);
+    }
+
+    @Override
+    public void
+    setCdmaSubscriptionSource(int cdmaSubscriptionType, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void queryCdmaRoamingPreference(Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void
+    setPhoneType(int phoneType) {
+    }
+
+    @Override
+    public void getPreferredVoicePrivacy(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void setPreferredVoicePrivacy(boolean enable, Message result) {
+        unimplemented(result);
+    }
+
+    /**
+     *  Set the TTY mode
+     *
+     * @param ttyMode is one of the following:
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+     * @param response is callback message
+     */
+    @Override
+    public void setTTYMode(int ttyMode, Message response) {
+        Rlog.w(LOG_TAG, "Not implemented in SimulatedCommands");
+        unimplemented(response);
+    }
+
+    /**
+     *  Query the TTY mode
+     * (AsyncResult)response.obj).result is an int[] with element [0] set to
+     * tty mode:
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO}
+     * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+     * @param response is callback message
+     */
+    @Override
+    public void queryTTYMode(Message response) {
+        unimplemented(response);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sendCDMAFeatureCode(String FeatureCode, Message response) {
+        unimplemented(response);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sendCdmaSms(byte[] pdu, Message response){
+        SimulatedCommandsVerifier.getInstance().sendCdmaSms(pdu, response);
+        resultSuccess(response, null);
+    }
+
+    @Override
+    public void setCdmaBroadcastActivation(boolean activate, Message response) {
+        unimplemented(response);
+
+    }
+
+    @Override
+    public void getCdmaBroadcastConfig(Message response) {
+        unimplemented(response);
+
+    }
+
+    @Override
+    public void setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs, Message response) {
+        unimplemented(response);
+    }
+
+    public void forceDataDormancy(Message response) {
+        unimplemented(response);
+    }
+
+
+    @Override
+    public void setGsmBroadcastActivation(boolean activate, Message response) {
+        unimplemented(response);
+    }
+
+
+    @Override
+    public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void getGsmBroadcastConfig(Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void supplyIccPinForApp(String pin, String aid, Message response) {
+        SimulatedCommandsVerifier.getInstance().supplyIccPinForApp(pin, aid, response);
+        if (mPinCode != null && mPinCode.equals(pin)) {
+            resultSuccess(response, null);
+            return;
+        }
+
+        Rlog.i(LOG_TAG, "[SimCmd] supplyIccPinForApp: pin failed!");
+        CommandException ex = new CommandException(
+                CommandException.Error.PASSWORD_INCORRECT);
+        resultFail(response, new int[]{
+                (--mPin1attemptsRemaining < 0) ? 0 : mPin1attemptsRemaining}, ex);
+    }
+
+    @Override
+    public void supplyIccPukForApp(String puk, String newPin, String aid, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void supplyIccPin2ForApp(String pin2, String aid, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message response) {
+        SimulatedCommandsVerifier.getInstance().changeIccPinForApp(oldPin, newPin, aidPtr,
+                response);
+        changeIccPin(oldPin, newPin, response);
+    }
+
+    @Override
+    public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr,
+            Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void requestIsimAuthentication(String nonce, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void requestIccSimAuthentication(int authContext, String data, String aid, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void getVoiceRadioTechnology(Message response) {
+        SimulatedCommandsVerifier.getInstance().getVoiceRadioTechnology(response);
+        int ret[] = new int[1];
+        ret[0] = mVoiceRadioTech;
+        resultSuccess(response, ret);
+    }
+
+    public void setCellInfoList(List<CellInfo> list) {
+        mCellInfoList = list;
+    }
+
+    @Override
+    public void getCellInfoList(Message response, WorkSource WorkSource) {
+        if (mCellInfoList == null) {
+            Parcel p = Parcel.obtain();
+            p.writeInt(1);
+            p.writeInt(1);
+            p.writeInt(2);
+            p.writeLong(1453510289108L);
+            p.writeInt(310);
+            p.writeInt(260);
+            p.writeInt(123);
+            p.writeInt(456);
+            p.writeInt(99);
+            p.writeInt(3);
+            p.setDataPosition(0);
+
+            CellInfoGsm cellInfo = CellInfoGsm.CREATOR.createFromParcel(p);
+
+            ArrayList<CellInfo> mCellInfoList = new ArrayList();
+            mCellInfoList.add(cellInfo);
+        }
+
+        resultSuccess(response, mCellInfoList);
+    }
+
+    @Override
+    public int getRilVersion() {
+        return 11;
+    }
+
+    @Override
+    public void setCellInfoListRate(int rateInMillis, Message response, WorkSource workSource) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming, Message result) {
+    }
+
+    @Override
+    public void setDataProfile(DataProfile[] dps, boolean isRoaming, Message result) {
+    }
+
+    public void setImsRegistrationState(int[] regState) {
+        mImsRegState = regState;
+    }
+
+    @Override
+    public void getImsRegistrationState(Message response) {
+        if (mImsRegState == null) {
+            mImsRegState = new int[]{1, PhoneConstants.PHONE_TYPE_NONE};
+        }
+
+        resultSuccess(response, mImsRegState);
+    }
+
+    @Override
+    public void sendImsCdmaSms(byte[] pdu, int retry, int messageRef,
+            Message response){
+        SimulatedCommandsVerifier.getInstance().sendImsCdmaSms(pdu, retry, messageRef, response);
+        resultSuccess(response, new SmsResponse(0 /*messageRef*/, null, 0));
+    }
+
+    @Override
+    public void sendImsGsmSms(String smscPDU, String pdu,
+            int retry, int messageRef, Message response){
+        SimulatedCommandsVerifier.getInstance().sendImsGsmSms(smscPDU, pdu, retry, messageRef,
+                response);
+        resultSuccess(response, new SmsResponse(0 /*messageRef*/, null, 0));
+    }
+
+    @Override
+    public void iccOpenLogicalChannel(String AID, int p2, Message response) {
+        SimulatedCommandsVerifier.getInstance().iccOpenLogicalChannel(AID, p2, response);
+        Object result = new int[]{mChannelId};
+        resultSuccess(response, result);
+    }
+
+    @Override
+    public void iccCloseLogicalChannel(int channel, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction,
+                                              int p1, int p2, int p3, String data,
+                                              Message response) {
+        SimulatedCommandsVerifier.getInstance().iccTransmitApduLogicalChannel(channel, cla,
+                instruction, p1, p2, p3, data, response);
+        if(mIccIoResultForApduLogicalChannel!=null) {
+            resultSuccess(response, mIccIoResultForApduLogicalChannel);
+        }else {
+            resultFail(response, null, new RuntimeException("IccIoResult not set"));
+        }
+    }
+
+    @Override
+    public void iccTransmitApduBasicChannel(int cla, int instruction, int p1, int p2,
+            int p3, String data, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void nvReadItem(int itemID, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void nvWriteItem(int itemID, String itemValue, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void nvWriteCdmaPrl(byte[] preferredRoamingList, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void nvResetConfig(int resetType, Message response) {
+        unimplemented(response);
+    }
+
+    @Override
+    public void getHardwareConfig(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void requestShutdown(Message result) {
+        setRadioState(RadioState.RADIO_UNAVAILABLE);
+    }
+
+    @Override
+    public void startLceService(int report_interval_ms, boolean pullMode, Message result) {
+        SimulatedCommandsVerifier.getInstance().startLceService(report_interval_ms, pullMode,
+                result);
+        unimplemented(result);
+    }
+
+    @Override
+    public void stopLceService(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void pullLceData(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void getModemActivityInfo(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void getAllowedCarriers(Message result) {
+        unimplemented(result);
+    }
+
+    @Override
+    public void getRadioCapability(Message result) {
+        SimulatedCommandsVerifier.getInstance().getRadioCapability(result);
+        resultSuccess(result, new RadioCapability(0, 0, 0, 0xFFFF, null, 0));
+    }
+    public void notifySmsStatus(Object result) {
+        if (mSmsStatusRegistrant != null) {
+            mSmsStatusRegistrant.notifyRegistrant(new AsyncResult(null, result, null));
+        }
+    }
+
+    public void notifyGsmBroadcastSms(Object result) {
+        if (mGsmBroadcastSmsRegistrant != null) {
+            mGsmBroadcastSmsRegistrant.notifyRegistrant(new AsyncResult(null, result, null));
+        }
+    }
+
+    public void notifyIccSmsFull() {
+        if (mIccSmsFullRegistrant != null) {
+            mIccSmsFullRegistrant.notifyRegistrant();
+        }
+    }
+
+    public void notifyEmergencyCallbackMode() {
+        if (mEmergencyCallbackModeRegistrant != null) {
+            mEmergencyCallbackModeRegistrant.notifyRegistrant();
+        }
+    }
+
+    @Override
+    public void setEmergencyCallbackMode(Handler h, int what, Object obj) {
+        SimulatedCommandsVerifier.getInstance().setEmergencyCallbackMode(h, what, obj);
+        super.setEmergencyCallbackMode(h, what, obj);
+    }
+
+    public void notifyExitEmergencyCallbackMode() {
+        if (mExitEmergencyCallbackModeRegistrants != null) {
+            mExitEmergencyCallbackModeRegistrants.notifyRegistrants(
+                    new AsyncResult (null, null, null));
+        }
+    }
+
+    public void notifyImsNetworkStateChanged() {
+        if(mImsNetworkStateChangedRegistrants != null) {
+            mImsNetworkStateChangedRegistrants.notifyRegistrants();
+        }
+    }
+
+    public void notifyModemReset() {
+        if (mModemResetRegistrants != null) {
+            mModemResetRegistrants.notifyRegistrants(new AsyncResult(null, "Test", null));
+        }
+    }
+
+    @Override
+    public void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj) {
+        SimulatedCommandsVerifier.getInstance().registerForExitEmergencyCallbackMode(h, what, obj);
+        super.registerForExitEmergencyCallbackMode(h, what, obj);
+    }
+
+    public void notifyRadioOn() {
+        mOnRegistrants.notifyRegistrants();
+    }
+
+    @VisibleForTesting
+    public void notifyNetworkStateChanged() {
+        mNetworkStateRegistrants.notifyRegistrants();
+    }
+
+    @VisibleForTesting
+    public void notifyOtaProvisionStatusChanged() {
+        if (mOtaProvisionRegistrants != null) {
+            int ret[] = new int[1];
+            ret[0] = Phone.CDMA_OTA_PROVISION_STATUS_COMMITTED;
+            mOtaProvisionRegistrants.notifyRegistrants(new AsyncResult(null, ret, null));
+        }
+    }
+
+    public void notifySignalStrength() {
+        if (mSignalStrength == null) {
+            mSignalStrength = new SignalStrength(
+                    20, // gsmSignalStrength
+                    0,  // gsmBitErrorRate
+                    -1, // cdmaDbm
+                    -1, // cdmaEcio
+                    -1, // evdoDbm
+                    -1, // evdoEcio
+                    -1, // evdoSnr
+                    99, // lteSignalStrength
+                    SignalStrength.INVALID,     // lteRsrp
+                    SignalStrength.INVALID,     // lteRsrq
+                    SignalStrength.INVALID,     // lteRssnr
+                    SignalStrength.INVALID,     // lteCqi
+                    SignalStrength.INVALID,     // tdScdmaRscp
+                    true                        // gsmFlag
+            );
+        }
+
+        if (mSignalStrengthRegistrant != null) {
+            mSignalStrengthRegistrant.notifyRegistrant(
+                    new AsyncResult (null, mSignalStrength, null));
+        }
+    }
+
+    public void setIccCardStatus(IccCardStatus iccCardStatus){
+        mIccCardStatus = iccCardStatus;
+    }
+
+    public void setIccIoResultForApduLogicalChannel(IccIoResult iccIoResult) {
+        mIccIoResultForApduLogicalChannel = iccIoResult;
+    }
+
+    public void setOpenChannelId(int channelId) {
+        mChannelId = channelId;
+    }
+
+    public void setPin1RemainingAttempt(int pin1attemptsRemaining) {
+        mPin1attemptsRemaining = pin1attemptsRemaining;
+    }
+
+    private AtomicBoolean mAllowed = new AtomicBoolean(false);
+
+    @Override
+    public void setDataAllowed(boolean allowed, Message result) {
+        log("setDataAllowed = " + allowed);
+        mAllowed.set(allowed);
+        resultSuccess(result, null);
+    }
+
+    @VisibleForTesting
+    public boolean isDataAllowed() {
+        return mAllowed.get();
+    }
+
+    @Override
+    public void registerForPcoData(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void unregisterForPcoData(Handler h) {
+    }
+
+    @Override
+    public void registerForModemReset(Handler h, int what, Object obj) {
+        SimulatedCommandsVerifier.getInstance().registerForModemReset(h, what, obj);
+        super.registerForModemReset(h, what, obj);
+    }
+
+    @Override
+    public void sendDeviceState(int stateType, boolean state, Message result) {
+        SimulatedCommandsVerifier.getInstance().sendDeviceState(stateType, state, result);
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void setUnsolResponseFilter(int filter, Message result) {
+        SimulatedCommandsVerifier.getInstance().setUnsolResponseFilter(filter, result);
+        resultSuccess(result, null);
+    }
+
+    @Override
+    public void setSimCardPower(int state, Message result) {
+    }
+
+    @VisibleForTesting
+    public void triggerRestrictedStateChanged(int restrictedState) {
+        if (mRestrictedStateRegistrant != null) {
+            mRestrictedStateRegistrant.notifyRegistrant(
+                    new AsyncResult(null, restrictedState, null));
+        }
+    }
+
+    @Override
+    public void setOnRestrictedStateChanged(Handler h, int what, Object obj) {
+        super.setOnRestrictedStateChanged(h, what, obj);
+        SimulatedCommandsVerifier.getInstance().setOnRestrictedStateChanged(h, what, obj);
+    }
+}
diff --git a/com/android/internal/telephony/test/SimulatedCommandsVerifier.java b/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
new file mode 100644
index 0000000..d746259
--- /dev/null
+++ b/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
@@ -0,0 +1,1411 @@
+/*
+ * Copyright (C) 2015 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.internal.telephony.test;
+
+import android.os.Handler;
+import android.os.Message;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.NetworkScanRequest;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.RadioCapability;
+import com.android.internal.telephony.UUSInfo;
+import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
+import com.android.internal.telephony.dataconnection.DataProfile;
+import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+
+import java.util.List;
+
+public class SimulatedCommandsVerifier implements CommandsInterface {
+    private static SimulatedCommandsVerifier sInstance;
+
+    private SimulatedCommandsVerifier() {
+
+    }
+
+    public static SimulatedCommandsVerifier getInstance() {
+        if (sInstance == null) {
+            sInstance = new SimulatedCommandsVerifier();
+        }
+        return sInstance;
+    }
+
+    @Override
+    public RadioState getRadioState() {
+        return null;
+    }
+
+    @Override
+    public void getImsRegistrationState(Message result) {
+
+    }
+
+    @Override
+    public void registerForRadioStateChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForRadioStateChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForVoiceRadioTechChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForVoiceRadioTechChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForImsNetworkStateChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForImsNetworkStateChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForOn(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForOn(Handler h) {
+
+    }
+
+    @Override
+    public void registerForAvailable(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForAvailable(Handler h) {
+
+    }
+
+    @Override
+    public void registerForNotAvailable(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForNotAvailable(Handler h) {
+
+    }
+
+    @Override
+    public void registerForOffOrNotAvailable(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForOffOrNotAvailable(Handler h) {
+
+    }
+
+    @Override
+    public void registerForIccStatusChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForIccStatusChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForCallStateChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForCallStateChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForNetworkStateChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForNetworkStateChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForDataCallListChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForDataCallListChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForInCallVoicePrivacyOn(Handler h) {
+
+    }
+
+    @Override
+    public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForInCallVoicePrivacyOff(Handler h) {
+
+    }
+
+    @Override
+    public void registerForSrvccStateChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForSrvccStateChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForSubscriptionStatusChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForSubscriptionStatusChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForHardwareConfigChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForHardwareConfigChanged(Handler h) {
+
+    }
+
+    @Override
+    public void setOnNewGsmSms(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnNewGsmSms(Handler h) {
+
+    }
+
+    @Override
+    public void setOnNewCdmaSms(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnNewCdmaSms(Handler h) {
+
+    }
+
+    @Override
+    public void setOnNewGsmBroadcastSms(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnNewGsmBroadcastSms(Handler h) {
+
+    }
+
+    @Override
+    public void setOnSmsOnSim(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnSmsOnSim(Handler h) {
+
+    }
+
+    @Override
+    public void setOnSmsStatus(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnSmsStatus(Handler h) {
+
+    }
+
+    @Override
+    public void setOnNITZTime(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnNITZTime(Handler h) {
+
+    }
+
+    @Override
+    public void setOnUSSD(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnUSSD(Handler h) {
+
+    }
+
+    @Override
+    public void setOnSignalStrengthUpdate(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnSignalStrengthUpdate(Handler h) {
+
+    }
+
+    @Override
+    public void setOnIccSmsFull(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnIccSmsFull(Handler h) {
+
+    }
+
+    @Override
+    public void registerForIccRefresh(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForIccRefresh(Handler h) {
+
+    }
+
+    @Override
+    public void setOnIccRefresh(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unsetOnIccRefresh(Handler h) {
+
+    }
+
+    @Override
+    public void setOnCallRing(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnCallRing(Handler h) {
+
+    }
+
+    @Override
+    public void setOnRestrictedStateChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnRestrictedStateChanged(Handler h) {
+
+    }
+
+    @Override
+    public void setOnSuppServiceNotification(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnSuppServiceNotification(Handler h) {
+
+    }
+
+    @Override
+    public void setOnCatSessionEnd(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnCatSessionEnd(Handler h) {
+
+    }
+
+    @Override
+    public void setOnCatProactiveCmd(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnCatProactiveCmd(Handler h) {
+
+    }
+
+    @Override
+    public void setOnCatEvent(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnCatEvent(Handler h) {
+
+    }
+
+    @Override
+    public void setOnCatCallSetUp(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnCatCallSetUp(Handler h) {
+
+    }
+
+    @Override
+    public void setSuppServiceNotifications(boolean enable, Message result) {
+
+    }
+
+    @Override
+    public void setOnCatCcAlphaNotify(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnCatCcAlphaNotify(Handler h) {
+
+    }
+
+    @Override
+    public void setOnSs(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnSs(Handler h) {
+
+    }
+
+    @Override
+    public void registerForDisplayInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForDisplayInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerForCallWaitingInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForCallWaitingInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerForSignalInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForSignalInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerForNumberInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForNumberInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerForRedirectedNumberInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForRedirectedNumberInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerForLineControlInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForLineControlInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerFoT53ClirlInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForT53ClirInfo(Handler h) {
+
+    }
+
+    @Override
+    public void registerForT53AudioControlInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForT53AudioControlInfo(Handler h) {
+
+    }
+
+    @Override
+    public void setEmergencyCallbackMode(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void registerForCdmaOtaProvision(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForCdmaOtaProvision(Handler h) {
+
+    }
+
+    @Override
+    public void registerForRingbackTone(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForRingbackTone(Handler h) {
+
+    }
+
+    @Override
+    public void registerForResendIncallMute(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForResendIncallMute(Handler h) {
+
+    }
+
+    @Override
+    public void registerForCdmaSubscriptionChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForCdmaSubscriptionChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForCdmaPrlChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForCdmaPrlChanged(Handler h) {
+
+    }
+
+    @Override
+    public void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForExitEmergencyCallbackMode(Handler h) {
+
+    }
+
+    @Override
+    public void registerForRilConnected(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForRilConnected(Handler h) {
+
+    }
+
+    @Override
+    public void supplyIccPin(String pin, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPinForApp(String pin, String aid, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPuk(String puk, String newPin, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPukForApp(String puk, String newPin, String aid, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPin2(String pin2, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPin2ForApp(String pin2, String aid, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPuk2(String puk2, String newPin2, Message result) {
+
+    }
+
+    @Override
+    public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message result) {
+
+    }
+
+    @Override
+    public void changeIccPin(String oldPin, String newPin, Message result) {
+
+    }
+
+    @Override
+    public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message result) {
+
+    }
+
+    @Override
+    public void changeIccPin2(String oldPin2, String newPin2, Message result) {
+
+    }
+
+    @Override
+    public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr, Message result) {
+
+    }
+
+    @Override
+    public void changeBarringPassword(String facility, String oldPwd, String newPwd,
+                                      Message result) {
+
+    }
+
+    @Override
+    public void supplyNetworkDepersonalization(String netpin, Message result) {
+
+    }
+
+    @Override
+    public void getCurrentCalls(Message result) {
+
+    }
+
+    @Override
+    public void getPDPContextList(Message result) {
+
+    }
+
+    @Override
+    public void getDataCallList(Message result) {
+
+    }
+
+    @Override
+    public void dial(String address, int clirMode, Message result) {
+
+    }
+
+    @Override
+    public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) {
+
+    }
+
+    @Override
+    public void getIMSI(Message result) {
+
+    }
+
+    @Override
+    public void getIMSIForApp(String aid, Message result) {
+
+    }
+
+    @Override
+    public void getIMEI(Message result) {
+
+    }
+
+    @Override
+    public void getIMEISV(Message result) {
+
+    }
+
+    @Override
+    public void hangupConnection(int gsmIndex, Message result) {
+
+    }
+
+    @Override
+    public void hangupWaitingOrBackground(Message result) {
+
+    }
+
+    @Override
+    public void hangupForegroundResumeBackground(Message result) {
+
+    }
+
+    @Override
+    public void switchWaitingOrHoldingAndActive(Message result) {
+
+    }
+
+    @Override
+    public void conference(Message result) {
+
+    }
+
+    @Override
+    public void setPreferredVoicePrivacy(boolean enable, Message result) {
+
+    }
+
+    @Override
+    public void getPreferredVoicePrivacy(Message result) {
+
+    }
+
+    @Override
+    public void separateConnection(int gsmIndex, Message result) {
+
+    }
+
+    @Override
+    public void acceptCall(Message result) {
+
+    }
+
+    @Override
+    public void rejectCall(Message result) {
+
+    }
+
+    @Override
+    public void explicitCallTransfer(Message result) {
+
+    }
+
+    @Override
+    public void getLastCallFailCause(Message result) {
+
+    }
+
+    @Override
+    public void getLastPdpFailCause(Message result) {
+
+    }
+
+    @Override
+    public void getLastDataCallFailCause(Message result) {
+
+    }
+
+    @Override
+    public void setMute(boolean enableMute, Message response) {
+
+    }
+
+    @Override
+    public void getMute(Message response) {
+
+    }
+
+    @Override
+    public void getSignalStrength(Message response) {
+
+    }
+
+    @Override
+    public void getVoiceRegistrationState(Message response) {
+
+    }
+
+    @Override
+    public void getDataRegistrationState(Message response) {
+
+    }
+
+    @Override
+    public void getOperator(Message response) {
+
+    }
+
+    @Override
+    public void sendDtmf(char c, Message result) {
+
+    }
+
+    @Override
+    public void startDtmf(char c, Message result) {
+
+    }
+
+    @Override
+    public void stopDtmf(Message result) {
+
+    }
+
+    @Override
+    public void sendBurstDtmf(String dtmfString, int on, int off, Message result) {
+
+    }
+
+    @Override
+    public void sendSMS(String smscPDU, String pdu, Message response) {
+
+    }
+
+    @Override
+    public void sendSMSExpectMore(String smscPDU, String pdu, Message response) {
+
+    }
+
+    @Override
+    public void sendCdmaSms(byte[] pdu, Message response) {
+
+    }
+
+    @Override
+    public void sendImsGsmSms(String smscPDU, String pdu, int retry, int messageRef,
+                              Message response) {
+
+    }
+
+    @Override
+    public void sendImsCdmaSms(byte[] pdu, int retry, int messageRef, Message response) {
+
+    }
+
+    @Override
+    public void deleteSmsOnSim(int index, Message response) {
+
+    }
+
+    @Override
+    public void deleteSmsOnRuim(int index, Message response) {
+
+    }
+
+    @Override
+    public void writeSmsToSim(int status, String smsc, String pdu, Message response) {
+
+    }
+
+    @Override
+    public void writeSmsToRuim(int status, String pdu, Message response) {
+
+    }
+
+    @Override
+    public void setRadioPower(boolean on, Message response) {
+
+    }
+
+    @Override
+    public void acknowledgeLastIncomingGsmSms(boolean success, int cause, Message response) {
+
+    }
+
+    @Override
+    public void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message response) {
+
+    }
+
+    @Override
+    public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, Message response) {
+
+    }
+
+    @Override
+    public void iccIO(int command, int fileid, String path, int p1, int p2, int p3, String data,
+                      String pin2, Message response) {
+
+    }
+
+    @Override
+    public void iccIOForApp(int command, int fileid, String path, int p1, int p2, int p3,
+                            String data, String pin2, String aid, Message response) {
+
+    }
+
+    @Override
+    public void queryCLIP(Message response) {
+
+    }
+
+    @Override
+    public void getCLIR(Message response) {
+
+    }
+
+    @Override
+    public void setCLIR(int clirMode, Message response) {
+
+    }
+
+    @Override
+    public void queryCallWaiting(int serviceClass, Message response) {
+
+    }
+
+    @Override
+    public void setCallWaiting(boolean enable, int serviceClass, Message response) {
+
+    }
+
+    @Override
+    public void setCallForward(int action, int cfReason, int serviceClass, String number,
+                               int timeSeconds, Message response) {
+
+    }
+
+    @Override
+    public void queryCallForwardStatus(int cfReason, int serviceClass, String number,
+                                       Message response) {
+
+    }
+
+    @Override
+    public void setNetworkSelectionModeAutomatic(Message response) {
+
+    }
+
+    @Override
+    public void setNetworkSelectionModeManual(String operatorNumeric, Message response) {
+
+    }
+
+    @Override
+    public void getNetworkSelectionMode(Message response) {
+
+    }
+
+    @Override
+    public void getAvailableNetworks(Message response) {
+
+    }
+
+    @Override
+    public void startNetworkScan(NetworkScanRequest nsr, Message response) {
+
+    }
+
+    @Override
+    public void stopNetworkScan(Message response) {
+
+    }
+
+    @Override
+    public void getBasebandVersion(Message response) {
+
+    }
+
+    @Override
+    public void queryFacilityLock(String facility, String password, int serviceClass,
+                                  Message response) {
+
+    }
+
+    @Override
+    public void queryFacilityLockForApp(String facility, String password, int serviceClass,
+                                        String appId, Message response) {
+
+    }
+
+    @Override
+    public void setFacilityLock(String facility, boolean lockState, String password,
+                                int serviceClass, Message response) {
+
+    }
+
+    @Override
+    public void setFacilityLockForApp(String facility, boolean lockState, String password,
+                                      int serviceClass, String appId, Message response) {
+
+    }
+
+    @Override
+    public void sendUSSD(String ussdString, Message response) {
+
+    }
+
+    @Override
+    public void cancelPendingUssd(Message response) {
+
+    }
+
+    @Override
+    public void resetRadio(Message result) {
+
+    }
+
+    @Override
+    public void setBandMode(int bandMode, Message response) {
+
+    }
+
+    @Override
+    public void queryAvailableBandMode(Message response) {
+
+    }
+
+    @Override
+    public void setPreferredNetworkType(int networkType, Message response) {
+
+    }
+
+    @Override
+    public void getPreferredNetworkType(Message response) {
+
+    }
+
+    @Override
+    public void setLocationUpdates(boolean enable, Message response) {
+
+    }
+
+    @Override
+    public void getSmscAddress(Message result) {
+
+    }
+
+    @Override
+    public void setSmscAddress(String address, Message result) {
+
+    }
+
+    @Override
+    public void reportSmsMemoryStatus(boolean available, Message result) {
+
+    }
+
+    @Override
+    public void reportStkServiceIsRunning(Message result) {
+
+    }
+
+    @Override
+    public void invokeOemRilRequestRaw(byte[] data, Message response) {
+
+    }
+
+    @Override
+    public void invokeOemRilRequestStrings(String[] strings, Message response) {
+
+    }
+
+    @Override
+    public void setOnUnsolOemHookRaw(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unSetOnUnsolOemHookRaw(Handler h) {
+
+    }
+
+    @Override
+    public void sendTerminalResponse(String contents, Message response) {
+
+    }
+
+    @Override
+    public void sendEnvelope(String contents, Message response) {
+
+    }
+
+    @Override
+    public void sendEnvelopeWithStatus(String contents, Message response) {
+
+    }
+
+    @Override
+    public void handleCallSetupRequestFromSim(boolean accept, Message response) {
+
+    }
+
+    @Override
+    public void setGsmBroadcastActivation(boolean activate, Message result) {
+
+    }
+
+    @Override
+    public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) {
+
+    }
+
+    @Override
+    public void getGsmBroadcastConfig(Message response) {
+
+    }
+
+    @Override
+    public void getDeviceIdentity(Message response) {
+
+    }
+
+    @Override
+    public void getCDMASubscription(Message response) {
+
+    }
+
+    @Override
+    public void sendCDMAFeatureCode(String FeatureCode, Message response) {
+
+    }
+
+    @Override
+    public void setPhoneType(int phoneType) {
+
+    }
+
+    @Override
+    public void queryCdmaRoamingPreference(Message response) {
+
+    }
+
+    @Override
+    public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) {
+
+    }
+
+    @Override
+    public void setCdmaSubscriptionSource(int cdmaSubscriptionType, Message response) {
+
+    }
+
+    @Override
+    public void getCdmaSubscriptionSource(Message response) {
+
+    }
+
+    @Override
+    public void setTTYMode(int ttyMode, Message response) {
+
+    }
+
+    @Override
+    public void queryTTYMode(Message response) {
+
+    }
+
+    @Override
+    public void setupDataCall(int radioTechnology, DataProfile dataProfile, boolean isRoaming,
+                              boolean allowRoaming, Message result) {
+    }
+
+    @Override
+    public void deactivateDataCall(int cid, int reason, Message result) {
+
+    }
+
+    @Override
+    public void setCdmaBroadcastActivation(boolean activate, Message result) {
+
+    }
+
+    @Override
+    public void setCdmaBroadcastConfig(CdmaSmsBroadcastConfigInfo[] configs, Message response) {
+
+    }
+
+    @Override
+    public void getCdmaBroadcastConfig(Message result) {
+
+    }
+
+    @Override
+    public void exitEmergencyCallbackMode(Message response) {
+
+    }
+
+    @Override
+    public void getIccCardStatus(Message result) {
+
+    }
+
+    @Override
+    public int getLteOnCdmaMode() {
+        return 0;
+    }
+
+    @Override
+    public void requestIsimAuthentication(String nonce, Message response) {
+
+    }
+
+    @Override
+    public void requestIccSimAuthentication(int authContext, String data, String aid,
+                                            Message response) {
+
+    }
+
+    @Override
+    public void getVoiceRadioTechnology(Message result) {
+
+    }
+
+    @Override
+    public void registerForCellInfoList(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForCellInfoList(Handler h) {
+
+    }
+
+    @Override
+    public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming, Message result) {
+
+    }
+
+    @Override
+    public void setDataProfile(DataProfile[] dps, boolean isRoaming, Message result) {
+
+    }
+
+    @Override
+    public void testingEmergencyCall() {
+
+    }
+
+    @Override
+    public void iccOpenLogicalChannel(String AID, int p2, Message response) {
+
+    }
+
+    @Override
+    public void iccCloseLogicalChannel(int channel, Message response) {
+
+    }
+
+    @Override
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int instruction, int p1,
+                                              int p2, int p3, String data, Message response) {
+
+    }
+
+    @Override
+    public void iccTransmitApduBasicChannel(int cla, int instruction, int p1, int p2, int p3,
+                                            String data, Message response) {
+
+    }
+
+    @Override
+    public void nvReadItem(int itemID, Message response) {
+
+    }
+
+    @Override
+    public void nvWriteItem(int itemID, String itemValue, Message response) {
+
+    }
+
+    @Override
+    public void nvWriteCdmaPrl(byte[] preferredRoamingList, Message response) {
+
+    }
+
+    @Override
+    public void nvResetConfig(int resetType, Message response) {
+
+    }
+
+    @Override
+    public void getHardwareConfig(Message result) {
+
+    }
+
+    @Override
+    public int getRilVersion() {
+        return 0;
+    }
+
+    @Override
+    public void setUiccSubscription(int slotId, int appIndex, int subId, int subStatus,
+                                    Message result) {
+
+    }
+
+    @Override
+    public void setDataAllowed(boolean allowed, Message result) {
+
+    }
+
+    @Override
+    public void requestShutdown(Message result) {
+
+    }
+
+    @Override
+    public void setRadioCapability(RadioCapability rc, Message result) {
+
+    }
+
+    @Override
+    public void getRadioCapability(Message result) {
+
+    }
+
+    @Override
+    public void registerForRadioCapabilityChanged(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForRadioCapabilityChanged(Handler h) {
+
+    }
+
+    @Override
+    public void startLceService(int reportIntervalMs, boolean pullMode, Message result) {
+
+    }
+
+    @Override
+    public void stopLceService(Message result) {
+
+    }
+
+    @Override
+    public void pullLceData(Message result) {
+
+    }
+
+    @Override
+    public void registerForLceInfo(Handler h, int what, Object obj) {
+
+    }
+
+    @Override
+    public void unregisterForLceInfo(Handler h) {
+
+    }
+
+    @Override
+    public void getModemActivityInfo(Message result) {
+
+    }
+
+    @Override
+    public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
+                                                Message result) {
+
+    }
+
+    @Override
+    public void setAllowedCarriers(List<CarrierIdentifier> carriers, Message result) {
+
+    }
+
+    @Override
+    public void getAllowedCarriers(Message result) {
+
+    }
+
+    @Override
+    public void registerForPcoData(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void unregisterForPcoData(Handler h) {
+    }
+
+    @Override
+    public void registerForModemReset(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void unregisterForModemReset(Handler h) {
+    }
+
+    @Override
+    public void sendDeviceState(int stateType, boolean state, Message result) {
+    }
+
+    @Override
+    public void setUnsolResponseFilter(int filter, Message result){
+    }
+
+    @Override
+    public void setSimCardPower(int state, Message result) {
+    }
+
+    @Override
+    public void registerForCarrierInfoForImsiEncryption(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void registerForNetworkScanResult(Handler h, int what, Object obj) {
+    }
+
+    @Override
+    public void unregisterForNetworkScanResult(Handler h) {
+    }
+
+    @Override
+    public void unregisterForCarrierInfoForImsiEncryption(Handler h) {
+    }
+}
diff --git a/com/android/internal/telephony/test/SimulatedGsmCallState.java b/com/android/internal/telephony/test/SimulatedGsmCallState.java
new file mode 100644
index 0000000..75e84c4
--- /dev/null
+++ b/com/android/internal/telephony/test/SimulatedGsmCallState.java
@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.test;
+
+import android.os.Looper;
+import android.os.Message;
+import android.os.Handler;
+import android.telephony.PhoneNumberUtils;
+import com.android.internal.telephony.ATParseEx;
+import com.android.internal.telephony.DriverCall;
+import java.util.List;
+import java.util.ArrayList;
+
+import android.telephony.Rlog;
+
+class CallInfo {
+    enum State {
+        ACTIVE(0),
+        HOLDING(1),
+        DIALING(2),    // MO call only
+        ALERTING(3),   // MO call only
+        INCOMING(4),   // MT call only
+        WAITING(5);    // MT call only
+
+        State(int value) {mValue = value;}
+
+        private final int mValue;
+        public int value() {return mValue;}
+    }
+
+    boolean mIsMT;
+    State mState;
+    boolean mIsMpty;
+    String mNumber;
+    int mTOA;
+
+    CallInfo (boolean isMT, State state, boolean isMpty, String number) {
+        mIsMT = isMT;
+        mState = state;
+        mIsMpty = isMpty;
+        mNumber = number;
+
+        if (number.length() > 0 && number.charAt(0) == '+') {
+            mTOA = PhoneNumberUtils.TOA_International;
+        } else {
+            mTOA = PhoneNumberUtils.TOA_Unknown;
+        }
+    }
+
+    static CallInfo
+    createOutgoingCall(String number) {
+        return new CallInfo (false, State.DIALING, false, number);
+    }
+
+    static CallInfo
+    createIncomingCall(String number) {
+        return new CallInfo (true, State.INCOMING, false, number);
+    }
+
+    String
+    toCLCCLine(int index) {
+        return
+            "+CLCC: "
+            + index + "," + (mIsMT ? "1" : "0") +","
+            + mState.value() + ",0," + (mIsMpty ? "1" : "0")
+            + ",\"" + mNumber + "\"," + mTOA;
+    }
+
+    DriverCall
+    toDriverCall(int index) {
+        DriverCall ret;
+
+        ret = new DriverCall();
+
+        ret.index = index;
+        ret.isMT = mIsMT;
+
+        try {
+            ret.state = DriverCall.stateFromCLCC(mState.value());
+        } catch (ATParseEx ex) {
+            throw new RuntimeException("should never happen", ex);
+        }
+
+        ret.isMpty = mIsMpty;
+        ret.number = mNumber;
+        ret.TOA = mTOA;
+        ret.isVoice = true;
+        ret.als = 0;
+
+        return ret;
+    }
+
+
+    boolean
+    isActiveOrHeld() {
+        return mState == State.ACTIVE || mState == State.HOLDING;
+    }
+
+    boolean
+    isConnecting() {
+        return mState == State.DIALING || mState == State.ALERTING;
+    }
+
+    boolean
+    isRinging() {
+        return mState == State.INCOMING || mState == State.WAITING;
+    }
+
+}
+
+class InvalidStateEx extends Exception {
+    InvalidStateEx() {
+
+    }
+}
+
+
+class SimulatedGsmCallState extends Handler {
+    //***** Instance Variables
+
+    CallInfo mCalls[] = new CallInfo[MAX_CALLS];
+
+    private boolean mAutoProgressConnecting = true;
+    private boolean mNextDialFailImmediately;
+
+
+    //***** Event Constants
+
+    static final int EVENT_PROGRESS_CALL_STATE = 1;
+
+    //***** Constants
+
+    static final int MAX_CALLS = 7;
+    /** number of msec between dialing -> alerting and alerting->active */
+    static final int CONNECTING_PAUSE_MSEC = 5 * 100;
+
+
+    //***** Overridden from Handler
+
+    public SimulatedGsmCallState(Looper looper) {
+        super(looper);
+    }
+
+    @Override
+    public void
+    handleMessage(Message msg) {
+        synchronized(this) { switch (msg.what) {
+            // PLEASE REMEMBER
+            // calls may have hung up by the time delayed events happen
+
+            case EVENT_PROGRESS_CALL_STATE:
+                progressConnectingCallState();
+            break;
+        }}
+    }
+
+    //***** Public Methods
+
+    /**
+     * Start the simulated phone ringing
+     * true if succeeded, false if failed
+     */
+    public boolean
+    triggerRing(String number) {
+        synchronized (this) {
+            int empty = -1;
+            boolean isCallWaiting = false;
+
+            // ensure there aren't already calls INCOMING or WAITING
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call == null && empty < 0) {
+                    empty = i;
+                } else if (call != null
+                    && (call.mState == CallInfo.State.INCOMING
+                        || call.mState == CallInfo.State.WAITING)
+                ) {
+                    Rlog.w("ModelInterpreter",
+                        "triggerRing failed; phone already ringing");
+                    return false;
+                } else if (call != null) {
+                    isCallWaiting = true;
+                }
+            }
+
+            if (empty < 0 ) {
+                Rlog.w("ModelInterpreter", "triggerRing failed; all full");
+                return false;
+            }
+
+            mCalls[empty] = CallInfo.createIncomingCall(
+                PhoneNumberUtils.extractNetworkPortion(number));
+
+            if (isCallWaiting) {
+                mCalls[empty].mState = CallInfo.State.WAITING;
+            }
+
+        }
+        return true;
+    }
+
+    /** If a call is DIALING or ALERTING, progress it to the next state */
+    public void
+    progressConnectingCallState() {
+        synchronized (this)  {
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call != null && call.mState == CallInfo.State.DIALING) {
+                    call.mState = CallInfo.State.ALERTING;
+
+                    if (mAutoProgressConnecting) {
+                        sendMessageDelayed(
+                                obtainMessage(EVENT_PROGRESS_CALL_STATE, call),
+                                CONNECTING_PAUSE_MSEC);
+                    }
+                    break;
+                } else if (call != null
+                        && call.mState == CallInfo.State.ALERTING
+                ) {
+                    call.mState = CallInfo.State.ACTIVE;
+                    break;
+                }
+            }
+        }
+    }
+
+    /** If a call is DIALING or ALERTING, progress it all the way to ACTIVE */
+    public void
+    progressConnectingToActive() {
+        synchronized (this)  {
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call != null && (call.mState == CallInfo.State.DIALING
+                    || call.mState == CallInfo.State.ALERTING)
+                ) {
+                    call.mState = CallInfo.State.ACTIVE;
+                    break;
+                }
+            }
+        }
+    }
+
+    /** automatically progress mobile originated calls to ACTIVE.
+     *  default to true
+     */
+    public void
+    setAutoProgressConnectingCall(boolean b) {
+        mAutoProgressConnecting = b;
+    }
+
+    public void
+    setNextDialFailImmediately(boolean b) {
+        mNextDialFailImmediately = b;
+    }
+
+    /**
+     * hangup ringing, dialing, or active calls
+     * returns true if call was hung up, false if not
+     */
+    public boolean
+    triggerHangupForeground() {
+        synchronized (this) {
+            boolean found;
+
+            found = false;
+
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call != null
+                    && (call.mState == CallInfo.State.INCOMING
+                        || call.mState == CallInfo.State.WAITING)
+                ) {
+                    mCalls[i] = null;
+                    found = true;
+                }
+            }
+
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call != null
+                    && (call.mState == CallInfo.State.DIALING
+                        || call.mState == CallInfo.State.ACTIVE
+                        || call.mState == CallInfo.State.ALERTING)
+                ) {
+                    mCalls[i] = null;
+                    found = true;
+                }
+            }
+            return found;
+        }
+    }
+
+    /**
+     * hangup holding calls
+     * returns true if call was hung up, false if not
+     */
+    public boolean
+    triggerHangupBackground() {
+        synchronized (this) {
+            boolean found = false;
+
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call != null && call.mState == CallInfo.State.HOLDING) {
+                    mCalls[i] = null;
+                    found = true;
+                }
+            }
+
+            return found;
+        }
+    }
+
+    /**
+     * hangup all
+     * returns true if call was hung up, false if not
+     */
+    public boolean
+    triggerHangupAll() {
+        synchronized(this) {
+            boolean found = false;
+
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (mCalls[i] != null) {
+                    found = true;
+                }
+
+                mCalls[i] = null;
+            }
+
+            return found;
+        }
+    }
+
+    public boolean
+    onAnswer() {
+        synchronized (this) {
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo call = mCalls[i];
+
+                if (call != null
+                    && (call.mState == CallInfo.State.INCOMING
+                        || call.mState == CallInfo.State.WAITING)
+                ) {
+                    return switchActiveAndHeldOrWaiting();
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public boolean
+    onHangup() {
+        boolean found = false;
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo call = mCalls[i];
+
+            if (call != null && call.mState != CallInfo.State.WAITING) {
+                mCalls[i] = null;
+                found = true;
+            }
+        }
+
+        return found;
+    }
+
+    public boolean
+    onChld(char c0, char c1) {
+        boolean ret;
+        int callIndex = 0;
+
+        if (c1 != 0) {
+            callIndex = c1 - '1';
+
+            if (callIndex < 0 || callIndex >= mCalls.length) {
+                return false;
+            }
+        }
+
+        switch (c0) {
+            case '0':
+                ret = releaseHeldOrUDUB();
+            break;
+            case '1':
+                if (c1 <= 0) {
+                    ret = releaseActiveAcceptHeldOrWaiting();
+                } else {
+                    if (mCalls[callIndex] == null) {
+                        ret = false;
+                    } else {
+                        mCalls[callIndex] = null;
+                        ret = true;
+                    }
+                }
+            break;
+            case '2':
+                if (c1 <= 0) {
+                    ret = switchActiveAndHeldOrWaiting();
+                } else {
+                    ret = separateCall(callIndex);
+                }
+            break;
+            case '3':
+                ret = conference();
+            break;
+            case '4':
+                ret = explicitCallTransfer();
+            break;
+            case '5':
+                if (true) { //just so javac doesnt complain about break
+                    //CCBS not impled
+                    ret = false;
+                }
+            break;
+            default:
+                ret = false;
+
+        }
+
+        return ret;
+    }
+
+    public boolean
+    releaseHeldOrUDUB() {
+        boolean found = false;
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null && c.isRinging()) {
+                found = true;
+                mCalls[i] = null;
+                break;
+            }
+        }
+
+        if (!found) {
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo c = mCalls[i];
+
+                if (c != null && c.mState == CallInfo.State.HOLDING) {
+                    found = true;
+                    mCalls[i] = null;
+                    // don't stop...there may be more than one
+                }
+            }
+        }
+
+        return true;
+    }
+
+
+    public boolean
+    releaseActiveAcceptHeldOrWaiting() {
+        boolean foundHeld = false;
+        boolean foundActive = false;
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null && c.mState == CallInfo.State.ACTIVE) {
+                mCalls[i] = null;
+                foundActive = true;
+            }
+        }
+
+        if (!foundActive) {
+            // FIXME this may not actually be how most basebands react
+            // CHLD=1 may not hang up dialing/alerting calls
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                CallInfo c = mCalls[i];
+
+                if (c != null
+                        && (c.mState == CallInfo.State.DIALING
+                            || c.mState == CallInfo.State.ALERTING)
+                ) {
+                    mCalls[i] = null;
+                    foundActive = true;
+                }
+            }
+        }
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null && c.mState == CallInfo.State.HOLDING) {
+                c.mState = CallInfo.State.ACTIVE;
+                foundHeld = true;
+            }
+        }
+
+        if (foundHeld) {
+            return true;
+        }
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null && c.isRinging()) {
+                c.mState = CallInfo.State.ACTIVE;
+                return true;
+            }
+        }
+
+        return true;
+    }
+
+    public boolean
+    switchActiveAndHeldOrWaiting() {
+        boolean hasHeld = false;
+
+        // first, are there held calls?
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null && c.mState == CallInfo.State.HOLDING) {
+                hasHeld = true;
+                break;
+            }
+        }
+
+        // Now, switch
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null) {
+                if (c.mState == CallInfo.State.ACTIVE) {
+                    c.mState = CallInfo.State.HOLDING;
+                } else if (c.mState == CallInfo.State.HOLDING) {
+                    c.mState = CallInfo.State.ACTIVE;
+                } else if (!hasHeld && c.isRinging())  {
+                    c.mState = CallInfo.State.ACTIVE;
+                }
+            }
+        }
+
+        return true;
+    }
+
+
+    public boolean
+    separateCall(int index) {
+        try {
+            CallInfo c;
+
+            c = mCalls[index];
+
+            if (c == null || c.isConnecting() || countActiveLines() != 1) {
+                return false;
+            }
+
+            c.mState = CallInfo.State.ACTIVE;
+            c.mIsMpty = false;
+
+            for (int i = 0 ; i < mCalls.length ; i++) {
+                int countHeld=0, lastHeld=0;
+
+                if (i != index) {
+                    CallInfo cb = mCalls[i];
+
+                    if (cb != null && cb.mState == CallInfo.State.ACTIVE) {
+                        cb.mState = CallInfo.State.HOLDING;
+                        countHeld++;
+                        lastHeld = i;
+                    }
+                }
+
+                if (countHeld == 1) {
+                    // if there's only one left, clear the MPTY flag
+                    mCalls[lastHeld].mIsMpty = false;
+                }
+            }
+
+            return true;
+        } catch (InvalidStateEx ex) {
+            return false;
+        }
+    }
+
+
+
+    public boolean
+    conference() {
+        int countCalls = 0;
+
+        // if there's connecting calls, we can't do this yet
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null) {
+                countCalls++;
+
+                if (c.isConnecting()) {
+                    return false;
+                }
+            }
+        }
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null) {
+                c.mState = CallInfo.State.ACTIVE;
+                if (countCalls > 0) {
+                    c.mIsMpty = true;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public boolean
+    explicitCallTransfer() {
+        int countCalls = 0;
+
+        // if there's connecting calls, we can't do this yet
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null) {
+                countCalls++;
+
+                if (c.isConnecting()) {
+                    return false;
+                }
+            }
+        }
+
+        // disconnect the subscriber from both calls
+        return triggerHangupAll();
+    }
+
+    public boolean
+    onDial(String address) {
+        CallInfo call;
+        int freeSlot = -1;
+
+        Rlog.d("GSM", "SC> dial '" + address + "'");
+
+        if (mNextDialFailImmediately) {
+            mNextDialFailImmediately = false;
+
+            Rlog.d("GSM", "SC< dial fail (per request)");
+            return false;
+        }
+
+        String phNum = PhoneNumberUtils.extractNetworkPortion(address);
+
+        if (phNum.length() == 0) {
+            Rlog.d("GSM", "SC< dial fail (invalid ph num)");
+            return false;
+        }
+
+        // Ignore setting up GPRS
+        if (phNum.startsWith("*99") && phNum.endsWith("#")) {
+            Rlog.d("GSM", "SC< dial ignored (gprs)");
+            return true;
+        }
+
+        // There can be at most 1 active "line" when we initiate
+        // a new call
+        try {
+            if (countActiveLines() > 1) {
+                Rlog.d("GSM", "SC< dial fail (invalid call state)");
+                return false;
+            }
+        } catch (InvalidStateEx ex) {
+            Rlog.d("GSM", "SC< dial fail (invalid call state)");
+            return false;
+        }
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            if (freeSlot < 0 && mCalls[i] == null) {
+                freeSlot = i;
+            }
+
+            if (mCalls[i] != null && !mCalls[i].isActiveOrHeld()) {
+                // Can't make outgoing calls when there is a ringing or
+                // connecting outgoing call
+                Rlog.d("GSM", "SC< dial fail (invalid call state)");
+                return false;
+            } else if (mCalls[i] != null && mCalls[i].mState == CallInfo.State.ACTIVE) {
+                // All active calls behome held
+                mCalls[i].mState = CallInfo.State.HOLDING;
+            }
+        }
+
+        if (freeSlot < 0) {
+            Rlog.d("GSM", "SC< dial fail (invalid call state)");
+            return false;
+        }
+
+        mCalls[freeSlot] = CallInfo.createOutgoingCall(phNum);
+
+        if (mAutoProgressConnecting) {
+            sendMessageDelayed(
+                    obtainMessage(EVENT_PROGRESS_CALL_STATE, mCalls[freeSlot]),
+                    CONNECTING_PAUSE_MSEC);
+        }
+
+        Rlog.d("GSM", "SC< dial (slot = " + freeSlot + ")");
+
+        return true;
+    }
+
+    public List<DriverCall>
+    getDriverCalls() {
+        ArrayList<DriverCall> ret = new ArrayList<DriverCall>(mCalls.length);
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null) {
+                DriverCall dc;
+
+                dc = c.toDriverCall(i + 1);
+                ret.add(dc);
+            }
+        }
+
+        Rlog.d("GSM", "SC< getDriverCalls " + ret);
+
+        return ret;
+    }
+
+    public List<String>
+    getClccLines() {
+        ArrayList<String> ret = new ArrayList<String>(mCalls.length);
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo c = mCalls[i];
+
+            if (c != null) {
+                ret.add((c.toCLCCLine(i + 1)));
+            }
+        }
+
+        return ret;
+    }
+
+    private int
+    countActiveLines() throws InvalidStateEx {
+        boolean hasMpty = false;
+        boolean hasHeld = false;
+        boolean hasActive = false;
+        boolean hasConnecting = false;
+        boolean hasRinging = false;
+        boolean mptyIsHeld = false;
+
+        for (int i = 0 ; i < mCalls.length ; i++) {
+            CallInfo call = mCalls[i];
+
+            if (call != null) {
+                if (!hasMpty && call.mIsMpty) {
+                    mptyIsHeld = call.mState == CallInfo.State.HOLDING;
+                } else if (call.mIsMpty && mptyIsHeld
+                    && call.mState == CallInfo.State.ACTIVE
+                ) {
+                    Rlog.e("ModelInterpreter", "Invalid state");
+                    throw new InvalidStateEx();
+                } else if (!call.mIsMpty && hasMpty && mptyIsHeld
+                    && call.mState == CallInfo.State.HOLDING
+                ) {
+                    Rlog.e("ModelInterpreter", "Invalid state");
+                    throw new InvalidStateEx();
+                }
+
+                hasMpty |= call.mIsMpty;
+                hasHeld |= call.mState == CallInfo.State.HOLDING;
+                hasActive |= call.mState == CallInfo.State.ACTIVE;
+                hasConnecting |= call.isConnecting();
+                hasRinging |= call.isRinging();
+            }
+        }
+
+        int ret = 0;
+
+        if (hasHeld) ret++;
+        if (hasActive) ret++;
+        if (hasConnecting) ret++;
+        if (hasRinging) ret++;
+
+        return ret;
+    }
+
+}
diff --git a/com/android/internal/telephony/test/SimulatedRadioControl.java b/com/android/internal/telephony/test/SimulatedRadioControl.java
new file mode 100644
index 0000000..054d370
--- /dev/null
+++ b/com/android/internal/telephony/test/SimulatedRadioControl.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.test;
+
+public interface SimulatedRadioControl
+{
+    public void triggerRing(String number);
+
+    public void progressConnectingCallState();
+
+    public void progressConnectingToActive();
+
+    public void setAutoProgressConnectingCall(boolean b);
+
+    public void setNextDialFailImmediately(boolean b);
+
+    public void setNextCallFailCause(int gsmCause);
+
+    public void triggerHangupForeground();
+
+    public void triggerHangupBackground();
+
+    public void triggerHangupAll();
+
+    public void triggerIncomingSMS(String message);
+
+    public void shutdown();
+
+    /** Pause responses to async requests until (ref-counted) resumeResponses() */
+    public void pauseResponses();
+
+    /** see pauseResponses */
+    public void resumeResponses();
+
+    public void triggerSsn(int type, int code);
+
+    /** Generates an incoming USSD message. */
+    public void triggerIncomingUssd(String statusCode, String message);
+}
diff --git a/com/android/internal/telephony/test/TestConferenceEventPackageParser.java b/com/android/internal/telephony/test/TestConferenceEventPackageParser.java
new file mode 100644
index 0000000..735ed17
--- /dev/null
+++ b/com/android/internal/telephony/test/TestConferenceEventPackageParser.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony.test;
+
+import com.android.ims.ImsConferenceState;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Xml;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Implements a basic XML parser used to parse test IMS conference event packages which can be
+ * injected into the IMS framework via the {@link com.android.internal.telephony.TelephonyTester}.
+ * <pre>
+ * {@code
+ * <xml>
+ *     <participant>
+ *         <user>tel:+16505551212</user>
+ *         <display-text>Joe Q. Public</display-text>
+ *         <endpoint>sip:[email protected]</endpoint>
+ *         <status>connected</status>
+ *     </participant>
+ * </xml>
+ * }
+ * </pre>
+ * <p>
+ * Note: This XML format is similar to the information stored in the
+ * {@link com.android.ims.ImsConferenceState} parcelable.  The {@code status} values expected in the
+ * XML are those found in the {@code ImsConferenceState} class (e.g.
+ * {@link com.android.ims.ImsConferenceState#STATUS_CONNECTED}).
+ * <p>
+ * Place a file formatted similar to above in /data/data/com.android.phone/files/ and invoke the
+ * following command while you have an ongoing IMS call:
+ * <pre>
+ *     adb shell am broadcast
+ *          -a com.android.internal.telephony.TestConferenceEventPackage
+ *          -e filename test.xml
+ * </pre>
+ */
+public class TestConferenceEventPackageParser {
+    private static final String LOG_TAG = "TestConferenceEventPackageParser";
+    private static final String PARTICIPANT_TAG = "participant";
+
+    /**
+     * The XML input stream to parse.
+     */
+    private InputStream mInputStream;
+
+    /**
+     * Constructs an input of the conference event package parser for the given input stream.
+     *
+     * @param inputStream The input stream.
+     */
+    public TestConferenceEventPackageParser(InputStream inputStream) {
+        mInputStream = inputStream;
+    }
+
+    /**
+     * Parses the conference event package XML file and returns an
+     * {@link com.android.ims.ImsConferenceState} instance containing the participants described in
+     * the XML file.
+     *
+     * @return The {@link com.android.ims.ImsConferenceState} instance.
+     */
+    public ImsConferenceState parse() {
+        ImsConferenceState conferenceState = new ImsConferenceState();
+
+        XmlPullParser parser;
+        try {
+            parser = Xml.newPullParser();
+            parser.setInput(mInputStream, null);
+            parser.nextTag();
+
+            int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                if (parser.getName().equals(PARTICIPANT_TAG)) {
+                    Log.v(LOG_TAG, "Found participant.");
+                    Bundle participant = parseParticipant(parser);
+                    conferenceState.mParticipants.put(participant.getString(
+                            ImsConferenceState.ENDPOINT), participant);
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(LOG_TAG, "Failed to read test conference event package from XML file", e);
+            return null;
+        } finally {
+            try {
+                mInputStream.close();
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "Failed to close test conference event package InputStream", e);
+                return null;
+            }
+        }
+
+        return conferenceState;
+    }
+
+    /**
+     * Parses a participant record from a conference event package XML file.
+     *
+     * @param parser The XML parser.
+     * @return {@link Bundle} containing the participant information.
+     */
+    private Bundle parseParticipant(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        Bundle bundle = new Bundle();
+
+        String user = "";
+        String displayText = "";
+        String endpoint = "";
+        String status = "";
+
+        int outerDepth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            if (parser.getName().equals(ImsConferenceState.USER)) {
+                parser.next();
+                user = parser.getText();
+            } else if (parser.getName().equals(ImsConferenceState.DISPLAY_TEXT)) {
+                parser.next();
+                displayText = parser.getText();
+            }  else if (parser.getName().equals(ImsConferenceState.ENDPOINT)) {
+                parser.next();
+                endpoint = parser.getText();
+            }  else if (parser.getName().equals(ImsConferenceState.STATUS)) {
+                parser.next();
+                status = parser.getText();
+            }
+        }
+
+        Log.v(LOG_TAG, "User: "+user);
+        Log.v(LOG_TAG, "DisplayText: "+displayText);
+        Log.v(LOG_TAG, "Endpoint: "+endpoint);
+        Log.v(LOG_TAG, "Status: "+status);
+
+        bundle.putString(ImsConferenceState.USER, user);
+        bundle.putString(ImsConferenceState.DISPLAY_TEXT, displayText);
+        bundle.putString(ImsConferenceState.ENDPOINT, endpoint);
+        bundle.putString(ImsConferenceState.STATUS, status);
+
+        return bundle;
+    }
+}
diff --git a/com/android/internal/telephony/uicc/AdnRecord.java b/com/android/internal/telephony/uicc/AdnRecord.java
new file mode 100644
index 0000000..203236c
--- /dev/null
+++ b/com/android/internal/telephony/uicc/AdnRecord.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.GsmAlphabet;
+
+import java.util.Arrays;
+
+
+/**
+ *
+ * Used to load or store ADNs (Abbreviated Dialing Numbers).
+ *
+ * {@hide}
+ *
+ */
+public class AdnRecord implements Parcelable {
+    static final String LOG_TAG = "AdnRecord";
+
+    //***** Instance Variables
+
+    String mAlphaTag = null;
+    String mNumber = null;
+    String[] mEmails;
+    int mExtRecord = 0xff;
+    int mEfid;                   // or 0 if none
+    int mRecordNumber;           // or 0 if none
+
+
+    //***** Constants
+
+    // In an ADN record, everything but the alpha identifier
+    // is in a footer that's 14 bytes
+    static final int FOOTER_SIZE_BYTES = 14;
+
+    // Maximum size of the un-extended number field
+    static final int MAX_NUMBER_SIZE_BYTES = 11;
+
+    static final int EXT_RECORD_LENGTH_BYTES = 13;
+    static final int EXT_RECORD_TYPE_ADDITIONAL_DATA = 2;
+    static final int EXT_RECORD_TYPE_MASK = 3;
+    static final int MAX_EXT_CALLED_PARTY_LENGTH = 0xa;
+
+    // ADN offset
+    static final int ADN_BCD_NUMBER_LENGTH = 0;
+    static final int ADN_TON_AND_NPI = 1;
+    static final int ADN_DIALING_NUMBER_START = 2;
+    static final int ADN_DIALING_NUMBER_END = 11;
+    static final int ADN_CAPABILITY_ID = 12;
+    static final int ADN_EXTENSION_ID = 13;
+
+    //***** Static Methods
+
+    public static final Parcelable.Creator<AdnRecord> CREATOR
+            = new Parcelable.Creator<AdnRecord>() {
+        @Override
+        public AdnRecord createFromParcel(Parcel source) {
+            int efid;
+            int recordNumber;
+            String alphaTag;
+            String number;
+            String[] emails;
+
+            efid = source.readInt();
+            recordNumber = source.readInt();
+            alphaTag = source.readString();
+            number = source.readString();
+            emails = source.readStringArray();
+
+            return new AdnRecord(efid, recordNumber, alphaTag, number, emails);
+        }
+
+        @Override
+        public AdnRecord[] newArray(int size) {
+            return new AdnRecord[size];
+        }
+    };
+
+
+    //***** Constructor
+    public AdnRecord (byte[] record) {
+        this(0, 0, record);
+    }
+
+    public AdnRecord (int efid, int recordNumber, byte[] record) {
+        this.mEfid = efid;
+        this.mRecordNumber = recordNumber;
+        parseRecord(record);
+    }
+
+    public AdnRecord (String alphaTag, String number) {
+        this(0, 0, alphaTag, number);
+    }
+
+    public AdnRecord (String alphaTag, String number, String[] emails) {
+        this(0, 0, alphaTag, number, emails);
+    }
+
+    public AdnRecord (int efid, int recordNumber, String alphaTag, String number, String[] emails) {
+        this.mEfid = efid;
+        this.mRecordNumber = recordNumber;
+        this.mAlphaTag = alphaTag;
+        this.mNumber = number;
+        this.mEmails = emails;
+    }
+
+    public AdnRecord(int efid, int recordNumber, String alphaTag, String number) {
+        this.mEfid = efid;
+        this.mRecordNumber = recordNumber;
+        this.mAlphaTag = alphaTag;
+        this.mNumber = number;
+        this.mEmails = null;
+    }
+
+    //***** Instance Methods
+
+    public String getAlphaTag() {
+        return mAlphaTag;
+    }
+
+    public int getEfid() {
+        return mEfid;
+    }
+
+    public int getRecId() {
+        return mRecordNumber;
+    }
+
+    public String getNumber() {
+        return mNumber;
+    }
+
+    public void setNumber(String number) {
+        mNumber = number;
+    }
+
+    public String[] getEmails() {
+        return mEmails;
+    }
+
+    public void setEmails(String[] emails) {
+        this.mEmails = emails;
+    }
+
+    @Override
+    public String toString() {
+        return "ADN Record '" + mAlphaTag + "' '" + Rlog.pii(LOG_TAG, mNumber) + " "
+                + Rlog.pii(LOG_TAG, mEmails) + "'";
+    }
+
+    public boolean isEmpty() {
+        return TextUtils.isEmpty(mAlphaTag) && TextUtils.isEmpty(mNumber) && mEmails == null;
+    }
+
+    public boolean hasExtendedRecord() {
+        return mExtRecord != 0 && mExtRecord != 0xff;
+    }
+
+    /** Helper function for {@link #isEqual}. */
+    private static boolean stringCompareNullEqualsEmpty(String s1, String s2) {
+        if (s1 == s2) {
+            return true;
+        }
+        if (s1 == null) {
+            s1 = "";
+        }
+        if (s2 == null) {
+            s2 = "";
+        }
+        return (s1.equals(s2));
+    }
+
+    public boolean isEqual(AdnRecord adn) {
+        return ( stringCompareNullEqualsEmpty(mAlphaTag, adn.mAlphaTag) &&
+                stringCompareNullEqualsEmpty(mNumber, adn.mNumber) &&
+                Arrays.equals(mEmails, adn.mEmails));
+    }
+    //***** Parcelable Implementation
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mEfid);
+        dest.writeInt(mRecordNumber);
+        dest.writeString(mAlphaTag);
+        dest.writeString(mNumber);
+        dest.writeStringArray(mEmails);
+    }
+
+    /**
+     * Build adn hex byte array based on record size
+     * The format of byte array is defined in 51.011 10.5.1
+     *
+     * @param recordSize is the size X of EF record
+     * @return hex byte[recordSize] to be written to EF record
+     *          return null for wrong format of dialing number or tag
+     */
+    public byte[] buildAdnString(int recordSize) {
+        byte[] bcdNumber;
+        byte[] byteTag;
+        byte[] adnString;
+        int footerOffset = recordSize - FOOTER_SIZE_BYTES;
+
+        // create an empty record
+        adnString = new byte[recordSize];
+        for (int i = 0; i < recordSize; i++) {
+            adnString[i] = (byte) 0xFF;
+        }
+
+        if (TextUtils.isEmpty(mNumber)) {
+            Rlog.w(LOG_TAG, "[buildAdnString] Empty dialing number");
+            return adnString;   // return the empty record (for delete)
+        } else if (mNumber.length()
+                > (ADN_DIALING_NUMBER_END - ADN_DIALING_NUMBER_START + 1) * 2) {
+            Rlog.w(LOG_TAG,
+                    "[buildAdnString] Max length of dialing number is 20");
+            return null;
+        }
+
+        byteTag = !TextUtils.isEmpty(mAlphaTag) ? GsmAlphabet.stringToGsm8BitPacked(mAlphaTag)
+                : new byte[0];
+
+        if (byteTag.length > footerOffset) {
+            Rlog.w(LOG_TAG, "[buildAdnString] Max length of tag is " + footerOffset);
+            return null;
+        } else {
+            bcdNumber = PhoneNumberUtils.numberToCalledPartyBCD(mNumber);
+
+            System.arraycopy(bcdNumber, 0, adnString,
+                    footerOffset + ADN_TON_AND_NPI, bcdNumber.length);
+
+            adnString[footerOffset + ADN_BCD_NUMBER_LENGTH]
+                    = (byte) (bcdNumber.length);
+            adnString[footerOffset + ADN_CAPABILITY_ID]
+                    = (byte) 0xFF; // Capability Id
+            adnString[footerOffset + ADN_EXTENSION_ID]
+                    = (byte) 0xFF; // Extension Record Id
+
+            if (byteTag.length > 0) {
+                System.arraycopy(byteTag, 0, adnString, 0, byteTag.length);
+            }
+
+            return adnString;
+        }
+    }
+
+    /**
+     * See TS 51.011 10.5.10
+     */
+    public void
+    appendExtRecord (byte[] extRecord) {
+        try {
+            if (extRecord.length != EXT_RECORD_LENGTH_BYTES) {
+                return;
+            }
+
+            if ((extRecord[0] & EXT_RECORD_TYPE_MASK)
+                    != EXT_RECORD_TYPE_ADDITIONAL_DATA) {
+                return;
+            }
+
+            if ((0xff & extRecord[1]) > MAX_EXT_CALLED_PARTY_LENGTH) {
+                // invalid or empty record
+                return;
+            }
+
+            mNumber += PhoneNumberUtils.calledPartyBCDFragmentToString(
+                                        extRecord, 2, 0xff & extRecord[1]);
+
+            // We don't support ext record chaining.
+
+        } catch (RuntimeException ex) {
+            Rlog.w(LOG_TAG, "Error parsing AdnRecord ext record", ex);
+        }
+    }
+
+    //***** Private Methods
+
+    /**
+     * alphaTag and number are set to null on invalid format
+     */
+    private void
+    parseRecord(byte[] record) {
+        try {
+            mAlphaTag = IccUtils.adnStringFieldToString(
+                            record, 0, record.length - FOOTER_SIZE_BYTES);
+
+            int footerOffset = record.length - FOOTER_SIZE_BYTES;
+
+            int numberLength = 0xff & record[footerOffset];
+
+            if (numberLength > MAX_NUMBER_SIZE_BYTES) {
+                // Invalid number length
+                mNumber = "";
+                return;
+            }
+
+            // Please note 51.011 10.5.1:
+            //
+            // "If the Dialling Number/SSC String does not contain
+            // a dialling number, e.g. a control string deactivating
+            // a service, the TON/NPI byte shall be set to 'FF' by
+            // the ME (see note 2)."
+
+            mNumber = PhoneNumberUtils.calledPartyBCDToString(
+                            record, footerOffset + 1, numberLength);
+
+
+            mExtRecord = 0xff & record[record.length - 1];
+
+            mEmails = null;
+
+        } catch (RuntimeException ex) {
+            Rlog.w(LOG_TAG, "Error parsing AdnRecord", ex);
+            mNumber = "";
+            mAlphaTag = "";
+            mEmails = null;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/AdnRecordCache.java b/com/android/internal/telephony/uicc/AdnRecordCache.java
new file mode 100644
index 0000000..ce3545a
--- /dev/null
+++ b/com/android/internal/telephony/uicc/AdnRecordCache.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.util.SparseArray;
+
+import com.android.internal.telephony.gsm.UsimPhoneBookManager;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * {@hide}
+ */
+public class AdnRecordCache extends Handler implements IccConstants {
+    //***** Instance Variables
+
+    private IccFileHandler mFh;
+    private UsimPhoneBookManager mUsimPhoneBookManager;
+
+    // Indexed by EF ID
+    SparseArray<ArrayList<AdnRecord>> mAdnLikeFiles
+        = new SparseArray<ArrayList<AdnRecord>>();
+
+    // People waiting for ADN-like files to be loaded
+    SparseArray<ArrayList<Message>> mAdnLikeWaiters
+        = new SparseArray<ArrayList<Message>>();
+
+    // People waiting for adn record to be updated
+    SparseArray<Message> mUserWriteResponse = new SparseArray<Message>();
+
+    //***** Event Constants
+
+    static final int EVENT_LOAD_ALL_ADN_LIKE_DONE = 1;
+    static final int EVENT_UPDATE_ADN_DONE = 2;
+
+    //***** Constructor
+
+
+
+    AdnRecordCache(IccFileHandler fh) {
+        mFh = fh;
+        mUsimPhoneBookManager = new UsimPhoneBookManager(mFh, this);
+    }
+
+    //***** Called from SIMRecords
+
+    /**
+     * Called from SIMRecords.onRadioNotAvailable and SIMRecords.handleSimRefresh.
+     */
+    public void reset() {
+        mAdnLikeFiles.clear();
+        mUsimPhoneBookManager.reset();
+
+        clearWaiters();
+        clearUserWriters();
+
+    }
+
+    private void clearWaiters() {
+        int size = mAdnLikeWaiters.size();
+        for (int i = 0; i < size; i++) {
+            ArrayList<Message> waiters = mAdnLikeWaiters.valueAt(i);
+            AsyncResult ar = new AsyncResult(null, null, new RuntimeException("AdnCache reset"));
+            notifyWaiters(waiters, ar);
+        }
+        mAdnLikeWaiters.clear();
+    }
+
+    private void clearUserWriters() {
+        int size = mUserWriteResponse.size();
+        for (int i = 0; i < size; i++) {
+            sendErrorResponse(mUserWriteResponse.valueAt(i), "AdnCace reset");
+        }
+        mUserWriteResponse.clear();
+    }
+
+    /**
+     * @return List of AdnRecords for efid if we've already loaded them this
+     * radio session, or null if we haven't
+     */
+    public ArrayList<AdnRecord>
+    getRecordsIfLoaded(int efid) {
+        return mAdnLikeFiles.get(efid);
+    }
+
+    /**
+     * Returns extension ef associated with ADN-like EF or -1 if
+     * we don't know.
+     *
+     * See 3GPP TS 51.011 for this mapping
+     */
+    public int extensionEfForEf(int efid) {
+        switch (efid) {
+            case EF_MBDN: return EF_EXT6;
+            case EF_ADN: return EF_EXT1;
+            case EF_SDN: return EF_EXT3;
+            case EF_FDN: return EF_EXT2;
+            case EF_MSISDN: return EF_EXT1;
+            case EF_PBR: return 0; // The EF PBR doesn't have an extension record
+            default: return -1;
+        }
+    }
+
+    private void sendErrorResponse(Message response, String errString) {
+        if (response != null) {
+            Exception e = new RuntimeException(errString);
+            AsyncResult.forMessage(response).exception = e;
+            response.sendToTarget();
+        }
+    }
+
+    /**
+     * Update an ADN-like record in EF by record index
+     *
+     * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN
+     * @param adn is the new adn to be stored
+     * @param recordIndex is the 1-based adn record index
+     * @param pin2 is required to update EF_FDN, otherwise must be null
+     * @param response message to be posted when done
+     *        response.exception hold the exception in error
+     */
+    public void updateAdnByIndex(int efid, AdnRecord adn, int recordIndex, String pin2,
+            Message response) {
+
+        int extensionEF = extensionEfForEf(efid);
+        if (extensionEF < 0) {
+            sendErrorResponse(response, "EF is not known ADN-like EF:0x" +
+                    Integer.toHexString(efid).toUpperCase());
+            return;
+        }
+
+        Message pendingResponse = mUserWriteResponse.get(efid);
+        if (pendingResponse != null) {
+            sendErrorResponse(response, "Have pending update for EF:0x" +
+                    Integer.toHexString(efid).toUpperCase());
+            return;
+        }
+
+        mUserWriteResponse.put(efid, response);
+
+        new AdnRecordLoader(mFh).updateEF(adn, efid, extensionEF,
+                recordIndex, pin2,
+                obtainMessage(EVENT_UPDATE_ADN_DONE, efid, recordIndex, adn));
+    }
+
+    /**
+     * Replace oldAdn with newAdn in ADN-like record in EF
+     *
+     * The ADN-like records must be read through requestLoadAllAdnLike() before
+     *
+     * @param efid must be one of EF_ADN, EF_FDN, and EF_SDN
+     * @param oldAdn is the adn to be replaced
+     *        If oldAdn.isEmpty() is ture, it insert the newAdn
+     * @param newAdn is the adn to be stored
+     *        If newAdn.isEmpty() is true, it delete the oldAdn
+     * @param pin2 is required to update EF_FDN, otherwise must be null
+     * @param response message to be posted when done
+     *        response.exception hold the exception in error
+     */
+    public void updateAdnBySearch(int efid, AdnRecord oldAdn, AdnRecord newAdn,
+            String pin2, Message response) {
+
+        int extensionEF;
+        extensionEF = extensionEfForEf(efid);
+
+        if (extensionEF < 0) {
+            sendErrorResponse(response, "EF is not known ADN-like EF:0x" +
+                    Integer.toHexString(efid).toUpperCase());
+            return;
+        }
+
+        ArrayList<AdnRecord>  oldAdnList;
+
+        if (efid == EF_PBR) {
+            oldAdnList = mUsimPhoneBookManager.loadEfFilesFromUsim();
+        } else {
+            oldAdnList = getRecordsIfLoaded(efid);
+        }
+
+        if (oldAdnList == null) {
+            sendErrorResponse(response, "Adn list not exist for EF:0x" +
+                    Integer.toHexString(efid).toUpperCase());
+            return;
+        }
+
+        int index = -1;
+        int count = 1;
+        for (Iterator<AdnRecord> it = oldAdnList.iterator(); it.hasNext(); ) {
+            if (oldAdn.isEqual(it.next())) {
+                index = count;
+                break;
+            }
+            count++;
+        }
+
+        if (index == -1) {
+            sendErrorResponse(response, "Adn record don't exist for " + oldAdn);
+            return;
+        }
+
+        if (efid == EF_PBR) {
+            AdnRecord foundAdn = oldAdnList.get(index-1);
+            efid = foundAdn.mEfid;
+            extensionEF = foundAdn.mExtRecord;
+            index = foundAdn.mRecordNumber;
+
+            newAdn.mEfid = efid;
+            newAdn.mExtRecord = extensionEF;
+            newAdn.mRecordNumber = index;
+        }
+
+        Message pendingResponse = mUserWriteResponse.get(efid);
+
+        if (pendingResponse != null) {
+            sendErrorResponse(response, "Have pending update for EF:0x" +
+                    Integer.toHexString(efid).toUpperCase());
+            return;
+        }
+
+        mUserWriteResponse.put(efid, response);
+
+        new AdnRecordLoader(mFh).updateEF(newAdn, efid, extensionEF,
+                index, pin2,
+                obtainMessage(EVENT_UPDATE_ADN_DONE, efid, index, newAdn));
+    }
+
+
+    /**
+     * Responds with exception (in response) if efid is not a known ADN-like
+     * record
+     */
+    public void
+    requestLoadAllAdnLike (int efid, int extensionEf, Message response) {
+        ArrayList<Message> waiters;
+        ArrayList<AdnRecord> result;
+
+        if (efid == EF_PBR) {
+            result = mUsimPhoneBookManager.loadEfFilesFromUsim();
+        } else {
+            result = getRecordsIfLoaded(efid);
+        }
+
+        // Have we already loaded this efid?
+        if (result != null) {
+            if (response != null) {
+                AsyncResult.forMessage(response).result = result;
+                response.sendToTarget();
+            }
+
+            return;
+        }
+
+        // Have we already *started* loading this efid?
+
+        waiters = mAdnLikeWaiters.get(efid);
+
+        if (waiters != null) {
+            // There's a pending request for this EF already
+            // just add ourselves to it
+
+            waiters.add(response);
+            return;
+        }
+
+        // Start loading efid
+
+        waiters = new ArrayList<Message>();
+        waiters.add(response);
+
+        mAdnLikeWaiters.put(efid, waiters);
+
+
+        if (extensionEf < 0) {
+            // respond with error if not known ADN-like record
+
+            if (response != null) {
+                AsyncResult.forMessage(response).exception
+                    = new RuntimeException("EF is not known ADN-like EF:0x" +
+                        Integer.toHexString(efid).toUpperCase());
+                response.sendToTarget();
+            }
+
+            return;
+        }
+
+        new AdnRecordLoader(mFh).loadAllFromEF(efid, extensionEf,
+            obtainMessage(EVENT_LOAD_ALL_ADN_LIKE_DONE, efid, 0));
+    }
+
+    //***** Private methods
+
+    private void
+    notifyWaiters(ArrayList<Message> waiters, AsyncResult ar) {
+
+        if (waiters == null) {
+            return;
+        }
+
+        for (int i = 0, s = waiters.size() ; i < s ; i++) {
+            Message waiter = waiters.get(i);
+
+            AsyncResult.forMessage(waiter, ar.result, ar.exception);
+            waiter.sendToTarget();
+        }
+    }
+
+    //***** Overridden from Handler
+
+    @Override
+    public void
+    handleMessage(Message msg) {
+        AsyncResult ar;
+        int efid;
+
+        switch(msg.what) {
+            case EVENT_LOAD_ALL_ADN_LIKE_DONE:
+                /* arg1 is efid, obj.result is ArrayList<AdnRecord>*/
+                ar = (AsyncResult) msg.obj;
+                efid = msg.arg1;
+                ArrayList<Message> waiters;
+
+                waiters = mAdnLikeWaiters.get(efid);
+                mAdnLikeWaiters.delete(efid);
+
+                if (ar.exception == null) {
+                    mAdnLikeFiles.put(efid, (ArrayList<AdnRecord>) ar.result);
+                }
+                notifyWaiters(waiters, ar);
+                break;
+            case EVENT_UPDATE_ADN_DONE:
+                ar = (AsyncResult)msg.obj;
+                efid = msg.arg1;
+                int index = msg.arg2;
+                AdnRecord adn = (AdnRecord) (ar.userObj);
+
+                if (ar.exception == null) {
+                    mAdnLikeFiles.get(efid).set(index - 1, adn);
+                    mUsimPhoneBookManager.invalidateCache();
+                }
+
+                Message response = mUserWriteResponse.get(efid);
+                mUserWriteResponse.delete(efid);
+
+                // response may be cleared when simrecord is reset,
+                // so we should check if it is null.
+                if (response != null) {
+                    AsyncResult.forMessage(response, null, ar.exception);
+                    response.sendToTarget();
+                }
+                break;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/AdnRecordLoader.java b/com/android/internal/telephony/uicc/AdnRecordLoader.java
new file mode 100644
index 0000000..eb5e9ce
--- /dev/null
+++ b/com/android/internal/telephony/uicc/AdnRecordLoader.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import java.util.ArrayList;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.uicc.IccConstants;
+
+public class AdnRecordLoader extends Handler {
+    final static String LOG_TAG = "AdnRecordLoader";
+    final static boolean VDBG = false;
+
+    //***** Instance Variables
+
+    private IccFileHandler mFh;
+    int mEf;
+    int mExtensionEF;
+    int mPendingExtLoads;
+    Message mUserResponse;
+    String mPin2;
+
+    // For "load one"
+    int mRecordNumber;
+
+    // for "load all"
+    ArrayList<AdnRecord> mAdns; // only valid after EVENT_ADN_LOAD_ALL_DONE
+
+    // Either an AdnRecord or a reference to adns depending
+    // if this is a load one or load all operation
+    Object mResult;
+
+    //***** Event Constants
+
+    static final int EVENT_ADN_LOAD_DONE = 1;
+    static final int EVENT_EXT_RECORD_LOAD_DONE = 2;
+    static final int EVENT_ADN_LOAD_ALL_DONE = 3;
+    static final int EVENT_EF_LINEAR_RECORD_SIZE_DONE = 4;
+    static final int EVENT_UPDATE_RECORD_DONE = 5;
+
+    //***** Constructor
+
+    AdnRecordLoader(IccFileHandler fh) {
+        // The telephony unit-test cases may create AdnRecords
+        // in secondary threads
+        super(Looper.getMainLooper());
+        mFh = fh;
+    }
+
+    private String getEFPath(int efid) {
+        if (efid == IccConstants.EF_ADN) {
+            return IccConstants.MF_SIM + IccConstants.DF_TELECOM;
+        }
+
+        return null;
+    }
+
+    /**
+     * Resulting AdnRecord is placed in response.obj.result
+     * or response.obj.exception is set
+     */
+    public void
+    loadFromEF(int ef, int extensionEF, int recordNumber,
+                Message response) {
+        mEf = ef;
+        mExtensionEF = extensionEF;
+        mRecordNumber = recordNumber;
+        mUserResponse = response;
+
+       mFh.loadEFLinearFixed(
+               ef, getEFPath(ef), recordNumber,
+               obtainMessage(EVENT_ADN_LOAD_DONE));
+    }
+
+
+    /**
+     * Resulting ArrayList&lt;adnRecord> is placed in response.obj.result
+     * or response.obj.exception is set
+     */
+    public void
+    loadAllFromEF(int ef, int extensionEF,
+                Message response) {
+        mEf = ef;
+        mExtensionEF = extensionEF;
+        mUserResponse = response;
+
+        /* If we are loading from EF_ADN, specifically
+         * specify the path as well, since, on some cards,
+         * the fileid is not unique.
+         */
+        mFh.loadEFLinearFixedAll(
+                ef, getEFPath(ef),
+                obtainMessage(EVENT_ADN_LOAD_ALL_DONE));
+    }
+
+    /**
+     * Write adn to a EF SIM record
+     * It will get the record size of EF record and compose hex adn array
+     * then write the hex array to EF record
+     *
+     * @param adn is set with alphaTag and phone number
+     * @param ef EF fileid
+     * @param extensionEF extension EF fileid
+     * @param recordNumber 1-based record index
+     * @param pin2 for CHV2 operations, must be null if pin2 is not needed
+     * @param response will be sent to its handler when completed
+     */
+    public void
+    updateEF(AdnRecord adn, int ef, int extensionEF, int recordNumber,
+            String pin2, Message response) {
+        mEf = ef;
+        mExtensionEF = extensionEF;
+        mRecordNumber = recordNumber;
+        mUserResponse = response;
+        mPin2 = pin2;
+ 
+        mFh.getEFLinearRecordSize( ef, getEFPath(ef),
+                obtainMessage(EVENT_EF_LINEAR_RECORD_SIZE_DONE, adn));
+     }
+
+    //***** Overridden from Handler
+
+    @Override
+    public void
+    handleMessage(Message msg) {
+        AsyncResult ar;
+        byte data[];
+        AdnRecord adn;
+
+        try {
+            switch (msg.what) {
+                case EVENT_EF_LINEAR_RECORD_SIZE_DONE:
+                    ar = (AsyncResult)(msg.obj);
+                    adn = (AdnRecord)(ar.userObj);
+
+                    if (ar.exception != null) {
+                        throw new RuntimeException("get EF record size failed",
+                                ar.exception);
+                    }
+
+                    int[] recordSize = (int[])ar.result;
+                    // recordSize is int[3] array
+                    // int[0]  is the record length
+                    // int[1]  is the total length of the EF file
+                    // int[2]  is the number of records in the EF file
+                    // So int[0] * int[2] = int[1]
+                   if (recordSize.length != 3 || mRecordNumber > recordSize[2]) {
+                        throw new RuntimeException("get wrong EF record size format",
+                                ar.exception);
+                    }
+
+                    data = adn.buildAdnString(recordSize[0]);
+
+                    if(data == null) {
+                        throw new RuntimeException("wrong ADN format",
+                                ar.exception);
+                    }
+
+
+                    mFh.updateEFLinearFixed(mEf, getEFPath(mEf), mRecordNumber,
+                            data, mPin2, obtainMessage(EVENT_UPDATE_RECORD_DONE));
+
+                    mPendingExtLoads = 1;
+
+                    break;
+                case EVENT_UPDATE_RECORD_DONE:
+                    ar = (AsyncResult)(msg.obj);
+                    if (ar.exception != null) {
+                        throw new RuntimeException("update EF adn record failed",
+                                ar.exception);
+                    }
+                    mPendingExtLoads = 0;
+                    mResult = null;
+                    break;
+                case EVENT_ADN_LOAD_DONE:
+                    ar = (AsyncResult)(msg.obj);
+                    data = (byte[])(ar.result);
+
+                    if (ar.exception != null) {
+                        throw new RuntimeException("load failed", ar.exception);
+                    }
+
+                    if (VDBG) {
+                        Rlog.d(LOG_TAG,"ADN EF: 0x"
+                            + Integer.toHexString(mEf)
+                            + ":" + mRecordNumber
+                            + "\n" + IccUtils.bytesToHexString(data));
+                    }
+
+                    adn = new AdnRecord(mEf, mRecordNumber, data);
+                    mResult = adn;
+
+                    if (adn.hasExtendedRecord()) {
+                        // If we have a valid value in the ext record field,
+                        // we're not done yet: we need to read the corresponding
+                        // ext record and append it
+
+                        mPendingExtLoads = 1;
+
+                        mFh.loadEFLinearFixed(
+                            mExtensionEF, adn.mExtRecord,
+                            obtainMessage(EVENT_EXT_RECORD_LOAD_DONE, adn));
+                    }
+                break;
+
+                case EVENT_EXT_RECORD_LOAD_DONE:
+                    ar = (AsyncResult)(msg.obj);
+                    data = (byte[])(ar.result);
+                    adn = (AdnRecord)(ar.userObj);
+
+                    if (ar.exception == null) {
+                        Rlog.d(LOG_TAG,"ADN extension EF: 0x"
+                                + Integer.toHexString(mExtensionEF)
+                                + ":" + adn.mExtRecord
+                                + "\n" + IccUtils.bytesToHexString(data));
+
+                        adn.appendExtRecord(data);
+                    }
+                    else {
+                        // If we can't get the rest of the number from EF_EXT1, rather than
+                        // providing the partial number, we clear the number since it's not
+                        // dialable anyway. Do not throw exception here otherwise the rest
+                        // of the good records will be dropped.
+
+                        Rlog.e(LOG_TAG, "Failed to read ext record. Clear the number now.");
+                        adn.setNumber("");
+                    }
+
+                    mPendingExtLoads--;
+                    // result should have been set in
+                    // EVENT_ADN_LOAD_DONE or EVENT_ADN_LOAD_ALL_DONE
+                break;
+
+                case EVENT_ADN_LOAD_ALL_DONE:
+                    ar = (AsyncResult)(msg.obj);
+                    ArrayList<byte[]> datas = (ArrayList<byte[]>)(ar.result);
+
+                    if (ar.exception != null) {
+                        throw new RuntimeException("load failed", ar.exception);
+                    }
+
+                    mAdns = new ArrayList<AdnRecord>(datas.size());
+                    mResult = mAdns;
+                    mPendingExtLoads = 0;
+
+                    for(int i = 0, s = datas.size() ; i < s ; i++) {
+                        adn = new AdnRecord(mEf, 1 + i, datas.get(i));
+                        mAdns.add(adn);
+
+                        if (adn.hasExtendedRecord()) {
+                            // If we have a valid value in the ext record field,
+                            // we're not done yet: we need to read the corresponding
+                            // ext record and append it
+
+                            mPendingExtLoads++;
+
+                            mFh.loadEFLinearFixed(
+                                mExtensionEF, adn.mExtRecord,
+                                obtainMessage(EVENT_EXT_RECORD_LOAD_DONE, adn));
+                        }
+                    }
+                break;
+            }
+        } catch (RuntimeException exc) {
+            if (mUserResponse != null) {
+                AsyncResult.forMessage(mUserResponse)
+                                .exception = exc;
+                mUserResponse.sendToTarget();
+                // Loading is all or nothing--either every load succeeds
+                // or we fail the whole thing.
+                mUserResponse = null;
+            }
+            return;
+        }
+
+        if (mUserResponse != null && mPendingExtLoads == 0) {
+            AsyncResult.forMessage(mUserResponse).result
+                = mResult;
+
+            mUserResponse.sendToTarget();
+            mUserResponse = null;
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/CarrierTestOverride.java b/com/android/internal/telephony/uicc/CarrierTestOverride.java
new file mode 100644
index 0000000..18d2937
--- /dev/null
+++ b/com/android/internal/telephony/uicc/CarrierTestOverride.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.uicc;
+
+import android.os.Environment;
+import android.telephony.Rlog;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Provide a machanism to override MVNO paramteres under CarrierConfig through a config file.
+ */
+public class CarrierTestOverride {
+    static final String LOG_TAG = "CarrierTestOverride";
+
+    /**
+     * Config file that can be created and adb-pushed by tester/developer
+     *
+     * Sample xml:
+     * <carrierTestOverrides>
+       <carrierTestOverride key="isInTestMode" value="true"/>
+       <carrierTestOverride key="gid1" value="bae0000000000000"/>
+       <carrierTestOverride key="gid2" value="ffffffffffffffff"/>
+       <carrierTestOverride key="imsi" value="310010123456789"/>
+       <carrierTestOverride key="spn" value="Verizon"/>
+       </carrierTestOverrides>
+     */
+    static final String DATA_CARRIER_TEST_OVERRIDE_PATH =
+            "/user_de/0/com.android.phone/files/carrier_test_conf.xml";
+    static final String CARRIER_TEST_XML_HEADER = "carrierTestOverrides";
+    static final String CARRIER_TEST_XML_SUBHEADER = "carrierTestOverride";
+    static final String CARRIER_TEST_XML_ITEM_KEY = "key";
+    static final String CARRIER_TEST_XML_ITEM_VALUE = "value";
+    static final String CARRIER_TEST_XML_ITEM_KEY_STRING_ISINTESTMODE = "isInTestMode";
+    static final String CARRIER_TEST_XML_ITEM_KEY_STRING_GID1 = "gid1";
+    static final String CARRIER_TEST_XML_ITEM_KEY_STRING_GID2 = "gid2";
+    static final String CARRIER_TEST_XML_ITEM_KEY_STRING_IMSI = "imsi";
+    static final String CARRIER_TEST_XML_ITEM_KEY_STRING_SPN = "spn";
+
+    private HashMap<String, String> mCarrierTestParamMap;
+
+    CarrierTestOverride() {
+        mCarrierTestParamMap = new HashMap<String, String>();
+        loadCarrierTestOverrides();
+    }
+
+    boolean isInTestMode() {
+        return mCarrierTestParamMap.containsKey(CARRIER_TEST_XML_ITEM_KEY_STRING_ISINTESTMODE)
+                && mCarrierTestParamMap.get(CARRIER_TEST_XML_ITEM_KEY_STRING_ISINTESTMODE)
+                .equals("true");
+    }
+
+    String getFakeSpn() {
+        try {
+            String spn = mCarrierTestParamMap.get(CARRIER_TEST_XML_ITEM_KEY_STRING_SPN);
+            Rlog.d(LOG_TAG, "reading spn from CarrierTestConfig file: " + spn);
+            return spn;
+        } catch (NullPointerException e) {
+            Rlog.w(LOG_TAG, "No spn in CarrierTestConfig file ");
+            return null;
+        }
+    }
+
+    String getFakeIMSI() {
+        try {
+            String imsi = mCarrierTestParamMap.get(CARRIER_TEST_XML_ITEM_KEY_STRING_IMSI);
+            Rlog.d(LOG_TAG, "reading imsi from CarrierTestConfig file: " + imsi);
+            return imsi;
+        } catch (NullPointerException e) {
+            Rlog.w(LOG_TAG, "No imsi in CarrierTestConfig file ");
+            return null;
+        }
+    }
+
+    String getFakeGid1() {
+        try {
+            String gid1 = mCarrierTestParamMap.get(CARRIER_TEST_XML_ITEM_KEY_STRING_GID1);
+            Rlog.d(LOG_TAG, "reading gid1 from CarrierTestConfig file: " + gid1);
+            return gid1;
+        } catch (NullPointerException e) {
+            Rlog.w(LOG_TAG, "No gid1 in CarrierTestConfig file ");
+            return null;
+        }
+    }
+
+    String getFakeGid2() {
+        try {
+            String gid2 = mCarrierTestParamMap.get(CARRIER_TEST_XML_ITEM_KEY_STRING_GID2);
+            Rlog.d(LOG_TAG, "reading gid2 from CarrierTestConfig file: " + gid2);
+            return gid2;
+        } catch (NullPointerException e) {
+            Rlog.w(LOG_TAG, "No gid2 in CarrierTestConfig file ");
+            return null;
+        }
+    }
+
+    private void loadCarrierTestOverrides() {
+
+        FileReader carrierTestConfigReader;
+
+        File carrierTestConfigFile = new File(Environment.getDataDirectory(),
+                DATA_CARRIER_TEST_OVERRIDE_PATH);
+
+        try {
+            carrierTestConfigReader = new FileReader(carrierTestConfigFile);
+            Rlog.d(LOG_TAG, "CarrierTestConfig file Modified Timestamp: "
+                    + carrierTestConfigFile.lastModified());
+        } catch (FileNotFoundException e) {
+            Rlog.w(LOG_TAG, "Can not open " + carrierTestConfigFile.getAbsolutePath());
+            return;
+        }
+
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(carrierTestConfigReader);
+
+            XmlUtils.beginDocument(parser, CARRIER_TEST_XML_HEADER);
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+
+                String name = parser.getName();
+                if (!CARRIER_TEST_XML_SUBHEADER.equals(name)) {
+                    break;
+                }
+
+                String key = parser.getAttributeValue(null, CARRIER_TEST_XML_ITEM_KEY);
+                String value = parser.getAttributeValue(null, CARRIER_TEST_XML_ITEM_VALUE);
+
+                Rlog.d(LOG_TAG,
+                        "extracting key-values from CarrierTestConfig file: " + key + "|" + value);
+                mCarrierTestParamMap.put(key, value);
+            }
+            carrierTestConfigReader.close();
+        } catch (XmlPullParserException e) {
+            Rlog.w(LOG_TAG, "Exception in carrier_test_conf parser " + e);
+        } catch (IOException e) {
+            Rlog.w(LOG_TAG, "Exception in carrier_test_conf parser " + e);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/CsimFileHandler.java b/com/android/internal/telephony/uicc/CsimFileHandler.java
new file mode 100644
index 0000000..e45afa9
--- /dev/null
+++ b/com/android/internal/telephony/uicc/CsimFileHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2006, 2012 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.internal.telephony.uicc;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+
+/**
+ * {@hide}
+ * This class should be used to access files in CSIM ADF
+ */
+public final class CsimFileHandler extends IccFileHandler implements IccConstants {
+    static final String LOG_TAG = "CsimFH";
+
+    public CsimFileHandler(UiccCardApplication app, String aid, CommandsInterface ci) {
+        super(app, aid, ci);
+    }
+
+    @Override
+    protected String getEFPath(int efid) {
+        switch(efid) {
+        case EF_SMS:
+        case EF_CST:
+        case EF_FDN:
+        case EF_MSISDN:
+        case EF_RUIM_SPN:
+        case EF_CSIM_LI:
+        case EF_CSIM_MDN:
+        case EF_CSIM_IMSIM:
+        case EF_CSIM_CDMAHOME:
+        case EF_CSIM_EPRL:
+        case EF_CSIM_MIPUPP:
+            return MF_SIM + DF_ADF;
+        }
+        String path = getCommonIccEFPath(efid);
+        if (path == null) {
+            // The EFids in UICC phone book entries are decided by the card manufacturer.
+            // So if we don't match any of the cases above and if its a UICC return
+            // the global 3g phone book path.
+            return MF_SIM + DF_TELECOM + DF_PHONEBOOK;
+        }
+        return path;
+    }
+
+    @Override
+    protected void logd(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    @Override
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccCardApplicationStatus.java b/com/android/internal/telephony/uicc/IccCardApplicationStatus.java
new file mode 100644
index 0000000..f1b0e43
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccCardApplicationStatus.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.uicc.IccCardStatus.PinState;
+
+
+/**
+ * See also RIL_AppStatus in include/telephony/ril.h
+ *
+ * {@hide}
+ */
+public class IccCardApplicationStatus {
+    // TODO: Replace with constants from PhoneConstants.APPTYPE_xxx
+    public enum AppType{
+        APPTYPE_UNKNOWN,
+        APPTYPE_SIM,
+        APPTYPE_USIM,
+        APPTYPE_RUIM,
+        APPTYPE_CSIM,
+        APPTYPE_ISIM
+    }
+
+    public enum AppState{
+        APPSTATE_UNKNOWN,
+        APPSTATE_DETECTED,
+        APPSTATE_PIN,
+        APPSTATE_PUK,
+        APPSTATE_SUBSCRIPTION_PERSO,
+        APPSTATE_READY;
+
+        boolean isPinRequired() {
+            return this == APPSTATE_PIN;
+        }
+
+        boolean isPukRequired() {
+            return this == APPSTATE_PUK;
+        }
+
+        boolean isSubscriptionPersoEnabled() {
+            return this == APPSTATE_SUBSCRIPTION_PERSO;
+        }
+
+        boolean isAppReady() {
+            return this == APPSTATE_READY;
+        }
+
+        boolean isAppNotReady() {
+            return this == APPSTATE_UNKNOWN  ||
+                   this == APPSTATE_DETECTED;
+        }
+    }
+
+    public enum PersoSubState{
+        PERSOSUBSTATE_UNKNOWN,
+        PERSOSUBSTATE_IN_PROGRESS,
+        PERSOSUBSTATE_READY,
+        PERSOSUBSTATE_SIM_NETWORK,
+        PERSOSUBSTATE_SIM_NETWORK_SUBSET,
+        PERSOSUBSTATE_SIM_CORPORATE,
+        PERSOSUBSTATE_SIM_SERVICE_PROVIDER,
+        PERSOSUBSTATE_SIM_SIM,
+        PERSOSUBSTATE_SIM_NETWORK_PUK,
+        PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK,
+        PERSOSUBSTATE_SIM_CORPORATE_PUK,
+        PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK,
+        PERSOSUBSTATE_SIM_SIM_PUK,
+        PERSOSUBSTATE_RUIM_NETWORK1,
+        PERSOSUBSTATE_RUIM_NETWORK2,
+        PERSOSUBSTATE_RUIM_HRPD,
+        PERSOSUBSTATE_RUIM_CORPORATE,
+        PERSOSUBSTATE_RUIM_SERVICE_PROVIDER,
+        PERSOSUBSTATE_RUIM_RUIM,
+        PERSOSUBSTATE_RUIM_NETWORK1_PUK,
+        PERSOSUBSTATE_RUIM_NETWORK2_PUK,
+        PERSOSUBSTATE_RUIM_HRPD_PUK,
+        PERSOSUBSTATE_RUIM_CORPORATE_PUK,
+        PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK,
+        PERSOSUBSTATE_RUIM_RUIM_PUK;
+
+        boolean isPersoSubStateUnknown() {
+            return this == PERSOSUBSTATE_UNKNOWN;
+        }
+    }
+
+    public AppType        app_type;
+    public AppState       app_state;
+    // applicable only if app_state == RIL_APPSTATE_SUBSCRIPTION_PERSO
+    public PersoSubState  perso_substate;
+    // null terminated string, e.g., from 0xA0, 0x00 -> 0x41, 0x30, 0x30, 0x30 */
+    public String         aid;
+    // null terminated string
+    public String         app_label;
+    // applicable to USIM and CSIM
+    public int            pin1_replaced;
+    public PinState       pin1;
+    public PinState       pin2;
+
+    public AppType AppTypeFromRILInt(int type) {
+        AppType newType;
+        /* RIL_AppType ril.h */
+        switch(type) {
+            case 0: newType = AppType.APPTYPE_UNKNOWN; break;
+            case 1: newType = AppType.APPTYPE_SIM;     break;
+            case 2: newType = AppType.APPTYPE_USIM;    break;
+            case 3: newType = AppType.APPTYPE_RUIM;    break;
+            case 4: newType = AppType.APPTYPE_CSIM;    break;
+            case 5: newType = AppType.APPTYPE_ISIM;    break;
+            default:
+                newType = AppType.APPTYPE_UNKNOWN;
+                loge("AppTypeFromRILInt: bad RIL_AppType: " + type + " use APPTYPE_UNKNOWN");
+        }
+        return newType;
+    }
+
+    public AppState AppStateFromRILInt(int state) {
+        AppState newState;
+        /* RIL_AppState ril.h */
+        switch(state) {
+            case 0: newState = AppState.APPSTATE_UNKNOWN;  break;
+            case 1: newState = AppState.APPSTATE_DETECTED; break;
+            case 2: newState = AppState.APPSTATE_PIN; break;
+            case 3: newState = AppState.APPSTATE_PUK; break;
+            case 4: newState = AppState.APPSTATE_SUBSCRIPTION_PERSO; break;
+            case 5: newState = AppState.APPSTATE_READY; break;
+            default:
+                newState = AppState.APPSTATE_UNKNOWN;
+                loge("AppStateFromRILInt: bad state: " + state + " use APPSTATE_UNKNOWN");
+        }
+        return newState;
+    }
+
+    public PersoSubState PersoSubstateFromRILInt(int substate) {
+        PersoSubState newSubState;
+        /* RIL_PeroSubstate ril.h */
+        switch(substate) {
+            case 0:  newSubState = PersoSubState.PERSOSUBSTATE_UNKNOWN;  break;
+            case 1:  newSubState = PersoSubState.PERSOSUBSTATE_IN_PROGRESS; break;
+            case 2:  newSubState = PersoSubState.PERSOSUBSTATE_READY; break;
+            case 3:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK; break;
+            case 4:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK_SUBSET; break;
+            case 5:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_CORPORATE; break;
+            case 6:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_SERVICE_PROVIDER; break;
+            case 7:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_SIM;  break;
+            case 8:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK_PUK; break;
+            case 9:  newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK; break;
+            case 10: newSubState = PersoSubState.PERSOSUBSTATE_SIM_CORPORATE_PUK; break;
+            case 11: newSubState = PersoSubState.PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK; break;
+            case 12: newSubState = PersoSubState.PERSOSUBSTATE_SIM_SIM_PUK; break;
+            case 13: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK1; break;
+            case 14: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK2; break;
+            case 15: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_HRPD; break;
+            case 16: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_CORPORATE; break;
+            case 17: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_SERVICE_PROVIDER; break;
+            case 18: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_RUIM; break;
+            case 19: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK1_PUK; break;
+            case 20: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK2_PUK; break;
+            case 21: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_HRPD_PUK ; break;
+            case 22: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_CORPORATE_PUK; break;
+            case 23: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK; break;
+            case 24: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_RUIM_PUK; break;
+            default:
+                newSubState = PersoSubState.PERSOSUBSTATE_UNKNOWN;
+                loge("PersoSubstateFromRILInt: bad substate: " + substate
+                        + " use PERSOSUBSTATE_UNKNOWN");
+        }
+        return newSubState;
+    }
+
+    public PinState PinStateFromRILInt(int state) {
+        PinState newPinState;
+        switch(state) {
+            case 0:
+                newPinState = PinState.PINSTATE_UNKNOWN;
+                break;
+            case 1:
+                newPinState = PinState.PINSTATE_ENABLED_NOT_VERIFIED;
+                break;
+            case 2:
+                newPinState = PinState.PINSTATE_ENABLED_VERIFIED;
+                break;
+            case 3:
+                newPinState = PinState.PINSTATE_DISABLED;
+                break;
+            case 4:
+                newPinState = PinState.PINSTATE_ENABLED_BLOCKED;
+                break;
+            case 5:
+                newPinState = PinState.PINSTATE_ENABLED_PERM_BLOCKED;
+                break;
+            default:
+                newPinState = PinState.PINSTATE_UNKNOWN;
+                loge("PinStateFromRILInt: bad pin state: " + state + " use PINSTATE_UNKNOWN");
+        }
+        return newPinState;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append("{").append(app_type).append(",").append(app_state);
+        if (app_state == AppState.APPSTATE_SUBSCRIPTION_PERSO) {
+            sb.append(",").append(perso_substate);
+        }
+        if (app_type == AppType.APPTYPE_CSIM ||
+                app_type == AppType.APPTYPE_USIM ||
+                app_type == AppType.APPTYPE_ISIM) {
+            sb.append(",pin1=").append(pin1);
+            sb.append(",pin2=").append(pin2);
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    private void loge(String s) {
+        Rlog.e("IccCardApplicationStatus", s);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccCardProxy.java b/com/android/internal/telephony/uicc/IccCardProxy.java
new file mode 100644
index 0000000..241f211
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccCardProxy.java
@@ -0,0 +1,986 @@
+/*
+ * Copyright (C) 2012 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.internal.telephony.uicc;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.UserHandle;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.CommandsInterface.RadioState;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.IccCardConstants.State;
+import com.android.internal.telephony.IntentBroadcaster;
+import com.android.internal.telephony.MccTable;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.PersoSubState;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
+import com.android.internal.telephony.uicc.IccCardStatus.PinState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * @Deprecated use {@link UiccController}.getUiccCard instead.
+ *
+ * The Phone App assumes that there is only one icc card, and one icc application
+ * available at a time. Moreover, it assumes such object (represented with IccCard)
+ * is available all the time (whether {@link RILConstants#RIL_REQUEST_GET_SIM_STATUS} returned
+ * or not, whether card has desired application or not, whether there really is a card in the
+ * slot or not).
+ *
+ * UiccController, however, can handle multiple instances of icc objects (multiple
+ * {@link UiccCardApplication}, multiple {@link IccFileHandler}, multiple {@link IccRecords})
+ * created and destroyed dynamically during phone operation.
+ *
+ * This class implements the IccCard interface that is always available (right after default
+ * phone object is constructed) to expose the current (based on voice radio technology)
+ * application on the uicc card, so that external apps won't break.
+ */
+
+public class IccCardProxy extends Handler implements IccCard {
+    private static final boolean DBG = true;
+    private static final String LOG_TAG = "IccCardProxy";
+
+    private static final int EVENT_RADIO_OFF_OR_UNAVAILABLE = 1;
+    private static final int EVENT_RADIO_ON = 2;
+    private static final int EVENT_ICC_CHANGED = 3;
+    private static final int EVENT_ICC_ABSENT = 4;
+    private static final int EVENT_ICC_LOCKED = 5;
+    private static final int EVENT_APP_READY = 6;
+    private static final int EVENT_RECORDS_LOADED = 7;
+    private static final int EVENT_IMSI_READY = 8;
+    private static final int EVENT_NETWORK_LOCKED = 9;
+    private static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 11;
+
+    private static final int EVENT_ICC_RECORD_EVENTS = 500;
+    private static final int EVENT_SUBSCRIPTION_ACTIVATED = 501;
+    private static final int EVENT_SUBSCRIPTION_DEACTIVATED = 502;
+    private static final int EVENT_CARRIER_PRIVILEGES_LOADED = 503;
+
+    private Integer mPhoneId = null;
+
+    private final Object mLock = new Object();
+    private Context mContext;
+    private CommandsInterface mCi;
+    private TelephonyManager mTelephonyManager;
+
+    private RegistrantList mAbsentRegistrants = new RegistrantList();
+    private RegistrantList mPinLockedRegistrants = new RegistrantList();
+    private RegistrantList mNetworkLockedRegistrants = new RegistrantList();
+
+    private int mCurrentAppType = UiccController.APP_FAM_3GPP; //default to 3gpp?
+    private UiccController mUiccController = null;
+    private UiccCard mUiccCard = null;
+    private UiccCardApplication mUiccApplication = null;
+    private IccRecords mIccRecords = null;
+    private CdmaSubscriptionSourceManager mCdmaSSM = null;
+    private RadioState mRadioState = RadioState.RADIO_UNAVAILABLE;
+    private boolean mQuietMode = false; // when set to true IccCardProxy will not broadcast
+                                        // ACTION_SIM_STATE_CHANGED intents
+    private boolean mInitialized = false;
+    private State mExternalState = State.UNKNOWN;
+
+    public static final String ACTION_INTERNAL_SIM_STATE_CHANGED = "android.intent.action.internal_sim_state_changed";
+
+    public IccCardProxy(Context context, CommandsInterface ci, int phoneId) {
+        if (DBG) log("ctor: ci=" + ci + " phoneId=" + phoneId);
+        mContext = context;
+        mCi = ci;
+        mPhoneId = phoneId;
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(
+                Context.TELEPHONY_SERVICE);
+        mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(context,
+                ci, this, EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null);
+        mUiccController = UiccController.getInstance();
+        mUiccController.registerForIccChanged(this, EVENT_ICC_CHANGED, null);
+        ci.registerForOn(this,EVENT_RADIO_ON, null);
+        ci.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_UNAVAILABLE, null);
+
+        resetProperties();
+    }
+
+    public void dispose() {
+        synchronized (mLock) {
+            log("Disposing");
+            //Cleanup icc references
+            mUiccController.unregisterForIccChanged(this);
+            mUiccController = null;
+            mCi.unregisterForOn(this);
+            mCi.unregisterForOffOrNotAvailable(this);
+            mCdmaSSM.dispose(this);
+        }
+    }
+
+    /*
+     * The card application that the external world sees will be based on the
+     * voice radio technology only!
+     */
+    public void setVoiceRadioTech(int radioTech) {
+        synchronized (mLock) {
+            if (DBG) {
+                log("Setting radio tech " + ServiceState.rilRadioTechnologyToString(radioTech));
+            }
+            if (ServiceState.isGsm(radioTech)) {
+                mCurrentAppType = UiccController.APP_FAM_3GPP;
+            } else {
+                mCurrentAppType = UiccController.APP_FAM_3GPP2;
+            }
+            updateQuietMode();
+        }
+    }
+
+    /**
+     * In case of 3gpp2 we need to find out if subscription used is coming from
+     * NV in which case we shouldn't broadcast any sim states changes.
+     */
+    private void updateQuietMode() {
+        synchronized (mLock) {
+            boolean oldQuietMode = mQuietMode;
+            boolean newQuietMode;
+            int cdmaSource = Phone.CDMA_SUBSCRIPTION_UNKNOWN;
+            boolean isLteOnCdmaMode = TelephonyManager.getLteOnCdmaModeStatic()
+                    == PhoneConstants.LTE_ON_CDMA_TRUE;
+            if (mCurrentAppType == UiccController.APP_FAM_3GPP) {
+                newQuietMode = false;
+                if (DBG) log("updateQuietMode: 3GPP subscription -> newQuietMode=" + newQuietMode);
+            } else {
+                if (isLteOnCdmaMode) {
+                    log("updateQuietMode: is cdma/lte device, force IccCardProxy into 3gpp mode");
+                    mCurrentAppType = UiccController.APP_FAM_3GPP;
+                }
+                cdmaSource = mCdmaSSM != null ?
+                        mCdmaSSM.getCdmaSubscriptionSource() : Phone.CDMA_SUBSCRIPTION_UNKNOWN;
+
+                newQuietMode = (cdmaSource == Phone.CDMA_SUBSCRIPTION_NV)
+                        && (mCurrentAppType == UiccController.APP_FAM_3GPP2)
+                        && !isLteOnCdmaMode;
+                if (DBG) {
+                    log("updateQuietMode: cdmaSource=" + cdmaSource
+                            + " mCurrentAppType=" + mCurrentAppType
+                            + " isLteOnCdmaMode=" + isLteOnCdmaMode
+                            + " newQuietMode=" + newQuietMode);
+                }
+            }
+
+            if (mQuietMode == false && newQuietMode == true) {
+                // Last thing to do before switching to quiet mode is
+                // broadcast ICC_READY
+                log("Switching to QuietMode.");
+                setExternalState(State.READY);
+                mQuietMode = newQuietMode;
+            } else if (mQuietMode == true && newQuietMode == false) {
+                if (DBG) {
+                    log("updateQuietMode: Switching out from QuietMode."
+                            + " Force broadcast of current state=" + mExternalState);
+                }
+                mQuietMode = newQuietMode;
+                setExternalState(mExternalState, true);
+            } else {
+                if (DBG) log("updateQuietMode: no changes don't setExternalState");
+            }
+            if (DBG) {
+                log("updateQuietMode: QuietMode is " + mQuietMode + " (app_type="
+                    + mCurrentAppType + " isLteOnCdmaMode=" + isLteOnCdmaMode
+                    + " cdmaSource=" + cdmaSource + ")");
+            }
+            mInitialized = true;
+            sendMessage(obtainMessage(EVENT_ICC_CHANGED));
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case EVENT_RADIO_OFF_OR_UNAVAILABLE:
+                mRadioState = mCi.getRadioState();
+                updateExternalState();
+                break;
+            case EVENT_RADIO_ON:
+                mRadioState = RadioState.RADIO_ON;
+                if (!mInitialized) {
+                    updateQuietMode();
+                } else {
+                    // updateQuietMode() triggers ICC_CHANGED, which eventually
+                    // calls updateExternalState; thus, we don't need this in the
+                    // above case
+                    updateExternalState();
+                }
+                break;
+            case EVENT_ICC_CHANGED:
+                if (mInitialized) {
+                    updateIccAvailability();
+                }
+                break;
+            case EVENT_ICC_ABSENT:
+                mAbsentRegistrants.notifyRegistrants();
+                setExternalState(State.ABSENT);
+                break;
+            case EVENT_ICC_LOCKED:
+                processLockedState();
+                break;
+            case EVENT_APP_READY:
+                setExternalState(State.READY);
+                break;
+            case EVENT_RECORDS_LOADED:
+                // Update the MCC/MNC.
+                if (mIccRecords != null) {
+                    String operator = mIccRecords.getOperatorNumeric();
+                    log("operator=" + operator + " mPhoneId=" + mPhoneId);
+
+                    if (!TextUtils.isEmpty(operator)) {
+                        mTelephonyManager.setSimOperatorNumericForPhone(mPhoneId, operator);
+                        String countryCode = operator.substring(0,3);
+                        if (countryCode != null) {
+                            mTelephonyManager.setSimCountryIsoForPhone(mPhoneId,
+                                    MccTable.countryCodeForMcc(Integer.parseInt(countryCode)));
+                        } else {
+                            loge("EVENT_RECORDS_LOADED Country code is null");
+                        }
+                    } else {
+                        loge("EVENT_RECORDS_LOADED Operator name is null");
+                    }
+                }
+                if (mUiccCard != null && !mUiccCard.areCarrierPriviligeRulesLoaded()) {
+                    mUiccCard.registerForCarrierPrivilegeRulesLoaded(
+                            this, EVENT_CARRIER_PRIVILEGES_LOADED, null);
+                } else {
+                    onRecordsLoaded();
+                }
+                break;
+            case EVENT_IMSI_READY:
+                broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_IMSI, null);
+                break;
+            case EVENT_NETWORK_LOCKED:
+                mNetworkLockedRegistrants.notifyRegistrants();
+                setExternalState(State.NETWORK_LOCKED);
+                break;
+            case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED:
+                updateQuietMode();
+                break;
+            case EVENT_SUBSCRIPTION_ACTIVATED:
+                log("EVENT_SUBSCRIPTION_ACTIVATED");
+                onSubscriptionActivated();
+                break;
+
+            case EVENT_SUBSCRIPTION_DEACTIVATED:
+                log("EVENT_SUBSCRIPTION_DEACTIVATED");
+                onSubscriptionDeactivated();
+                break;
+
+            case EVENT_ICC_RECORD_EVENTS:
+                if ((mCurrentAppType == UiccController.APP_FAM_3GPP) && (mIccRecords != null)) {
+                    AsyncResult ar = (AsyncResult)msg.obj;
+                    int eventCode = (Integer) ar.result;
+                    if (eventCode == SIMRecords.EVENT_SPN) {
+                        mTelephonyManager.setSimOperatorNameForPhone(
+                                mPhoneId, mIccRecords.getServiceProviderName());
+                    }
+                }
+                break;
+
+            case EVENT_CARRIER_PRIVILEGES_LOADED:
+                log("EVENT_CARRIER_PRIVILEGES_LOADED");
+                if (mUiccCard != null) {
+                    mUiccCard.unregisterForCarrierPrivilegeRulesLoaded(this);
+                }
+                onRecordsLoaded();
+                break;
+
+            default:
+                loge("Unhandled message with number: " + msg.what);
+                break;
+        }
+    }
+
+    private void onSubscriptionActivated() {
+        updateIccAvailability();
+        updateStateProperty();
+    }
+
+    private void onSubscriptionDeactivated() {
+        resetProperties();
+        updateIccAvailability();
+        updateStateProperty();
+    }
+
+    private void onRecordsLoaded() {
+        broadcastInternalIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_LOADED, null);
+    }
+
+    private void updateIccAvailability() {
+        synchronized (mLock) {
+            UiccCard newCard = mUiccController.getUiccCard(mPhoneId);
+            UiccCardApplication newApp = null;
+            IccRecords newRecords = null;
+            if (newCard != null) {
+                newApp = newCard.getApplication(mCurrentAppType);
+                if (newApp != null) {
+                    newRecords = newApp.getIccRecords();
+                }
+            }
+
+            if (mIccRecords != newRecords || mUiccApplication != newApp || mUiccCard != newCard) {
+                if (DBG) log("Icc changed. Reregistering.");
+                unregisterUiccCardEvents();
+                mUiccCard = newCard;
+                mUiccApplication = newApp;
+                mIccRecords = newRecords;
+                registerUiccCardEvents();
+            }
+            updateExternalState();
+        }
+    }
+
+    void resetProperties() {
+        if (mCurrentAppType == UiccController.APP_FAM_3GPP) {
+            log("update icc_operator_numeric=" + "");
+            mTelephonyManager.setSimOperatorNumericForPhone(mPhoneId, "");
+            mTelephonyManager.setSimCountryIsoForPhone(mPhoneId, "");
+            mTelephonyManager.setSimOperatorNameForPhone(mPhoneId, "");
+         }
+    }
+
+    private void HandleDetectedState() {
+    // CAF_MSIM SAND
+//        setExternalState(State.DETECTED, false);
+    }
+
+    private void updateExternalState() {
+
+        // mUiccCard could be null at bootup, before valid card states have
+        // been received from UiccController.
+        if (mUiccCard == null) {
+            setExternalState(State.UNKNOWN);
+            return;
+        }
+
+        if (mUiccCard.getCardState() == CardState.CARDSTATE_ABSENT) {
+            /*
+             * Both IccCardProxy and UiccController are registered for
+             * RadioState changes. When the UiccController receives a radio
+             * state changed to Unknown it will dispose of all of the IccCard
+             * objects, which will then notify the IccCardProxy and the null
+             * object will force the state to unknown. However, because the
+             * IccCardProxy is also registered for RadioState changes, it will
+             * recieve that signal first. By triggering on radio state changes
+             * directly, we reduce the time window during which the modem is
+             * UNAVAILABLE but the IccStatus is reported as something valid.
+             * This is not ideal.
+             */
+            if (mRadioState == RadioState.RADIO_UNAVAILABLE) {
+                setExternalState(State.UNKNOWN);
+            } else {
+                setExternalState(State.ABSENT);
+            }
+            return;
+        }
+
+        if (mUiccCard.getCardState() == CardState.CARDSTATE_ERROR) {
+            setExternalState(State.CARD_IO_ERROR);
+            return;
+        }
+
+        if (mUiccCard.getCardState() == CardState.CARDSTATE_RESTRICTED) {
+            setExternalState(State.CARD_RESTRICTED);
+            return;
+        }
+
+        if (mUiccApplication == null) {
+            setExternalState(State.NOT_READY);
+            return;
+        }
+
+        // By process of elimination, the UICC Card State = PRESENT
+        switch (mUiccApplication.getState()) {
+            case APPSTATE_UNKNOWN:
+                /*
+                 * APPSTATE_UNKNOWN is a catch-all state reported whenever the app
+                 * is not explicitly in one of the other states. To differentiate the
+                 * case where we know that there is a card present, but the APP is not
+                 * ready, we choose NOT_READY here instead of unknown. This is possible
+                 * in at least two cases:
+                 * 1) A transient during the process of the SIM bringup
+                 * 2) There is no valid App on the SIM to load, which can be the case with an
+                 *    eSIM/soft SIM.
+                 */
+                setExternalState(State.NOT_READY);
+                break;
+            case APPSTATE_DETECTED:
+                HandleDetectedState();
+                break;
+            case APPSTATE_PIN:
+                setExternalState(State.PIN_REQUIRED);
+                break;
+            case APPSTATE_PUK:
+                PinState pin1State = mUiccApplication.getPin1State();
+                if (pin1State.isPermBlocked()) {
+                    setExternalState(State.PERM_DISABLED);
+                    return;
+                }
+                setExternalState(State.PUK_REQUIRED);
+                break;
+            case APPSTATE_SUBSCRIPTION_PERSO:
+                if (mUiccApplication.getPersoSubState() ==
+                        PersoSubState.PERSOSUBSTATE_SIM_NETWORK) {
+                    setExternalState(State.NETWORK_LOCKED);
+                }
+                // Otherwise don't change external SIM state.
+                break;
+            case APPSTATE_READY:
+                setExternalState(State.READY);
+                break;
+        }
+    }
+
+    private void registerUiccCardEvents() {
+        if (mUiccCard != null) {
+            mUiccCard.registerForAbsent(this, EVENT_ICC_ABSENT, null);
+        }
+        if (mUiccApplication != null) {
+            mUiccApplication.registerForReady(this, EVENT_APP_READY, null);
+            mUiccApplication.registerForLocked(this, EVENT_ICC_LOCKED, null);
+            mUiccApplication.registerForNetworkLocked(this, EVENT_NETWORK_LOCKED, null);
+        }
+        if (mIccRecords != null) {
+            mIccRecords.registerForImsiReady(this, EVENT_IMSI_READY, null);
+            mIccRecords.registerForRecordsLoaded(this, EVENT_RECORDS_LOADED, null);
+            mIccRecords.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null);
+        }
+    }
+
+    private void unregisterUiccCardEvents() {
+        if (mUiccCard != null) mUiccCard.unregisterForAbsent(this);
+        if (mUiccCard != null) mUiccCard.unregisterForCarrierPrivilegeRulesLoaded(this);
+        if (mUiccApplication != null) mUiccApplication.unregisterForReady(this);
+        if (mUiccApplication != null) mUiccApplication.unregisterForLocked(this);
+        if (mUiccApplication != null) mUiccApplication.unregisterForNetworkLocked(this);
+        if (mIccRecords != null) mIccRecords.unregisterForImsiReady(this);
+        if (mIccRecords != null) mIccRecords.unregisterForRecordsLoaded(this);
+        if (mIccRecords != null) mIccRecords.unregisterForRecordsEvents(this);
+    }
+
+    private void updateStateProperty() {
+        mTelephonyManager.setSimStateForPhone(mPhoneId, getState().toString());
+    }
+
+    private void broadcastIccStateChangedIntent(String value, String reason) {
+        synchronized (mLock) {
+            if (mPhoneId == null || !SubscriptionManager.isValidSlotIndex(mPhoneId)) {
+                loge("broadcastIccStateChangedIntent: mPhoneId=" + mPhoneId
+                        + " is invalid; Return!!");
+                return;
+            }
+
+            if (mQuietMode) {
+                log("broadcastIccStateChangedIntent: QuietMode"
+                        + " NOT Broadcasting intent ACTION_SIM_STATE_CHANGED "
+                        + " value=" +  value + " reason=" + reason);
+                return;
+            }
+
+            Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+            // TODO - we'd like this intent to have a single snapshot of all sim state,
+            // but until then this should not use REPLACE_PENDING or we may lose
+            // information
+            // intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            intent.putExtra(PhoneConstants.PHONE_NAME_KEY, "Phone");
+            intent.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, value);
+            intent.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason);
+            SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhoneId);
+            log("broadcastIccStateChangedIntent intent ACTION_SIM_STATE_CHANGED value=" + value
+                + " reason=" + reason + " for mPhoneId=" + mPhoneId);
+            IntentBroadcaster.getInstance().broadcastStickyIntent(intent, mPhoneId);
+        }
+    }
+
+    private void broadcastInternalIccStateChangedIntent(String value, String reason) {
+        synchronized (mLock) {
+            if (mPhoneId == null) {
+                loge("broadcastInternalIccStateChangedIntent: Card Index is not set; Return!!");
+                return;
+            }
+
+            Intent intent = new Intent(ACTION_INTERNAL_SIM_STATE_CHANGED);
+            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                    | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            intent.putExtra(PhoneConstants.PHONE_NAME_KEY, "Phone");
+            intent.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, value);
+            intent.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason);
+            intent.putExtra(PhoneConstants.PHONE_KEY, mPhoneId);  // SubId may not be valid.
+            log("Sending intent ACTION_INTERNAL_SIM_STATE_CHANGED value=" + value
+                    + " for mPhoneId : " + mPhoneId);
+            ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+        }
+    }
+
+    private void setExternalState(State newState, boolean override) {
+        synchronized (mLock) {
+            if (mPhoneId == null || !SubscriptionManager.isValidSlotIndex(mPhoneId)) {
+                loge("setExternalState: mPhoneId=" + mPhoneId + " is invalid; Return!!");
+                return;
+            }
+
+            if (!override && newState == mExternalState) {
+                log("setExternalState: !override and newstate unchanged from " + newState);
+                return;
+            }
+            mExternalState = newState;
+            log("setExternalState: set mPhoneId=" + mPhoneId + " mExternalState=" + mExternalState);
+            mTelephonyManager.setSimStateForPhone(mPhoneId, getState().toString());
+
+            // For locked states, we should be sending internal broadcast.
+            if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(
+                        getIccStateIntentString(mExternalState))) {
+                broadcastInternalIccStateChangedIntent(getIccStateIntentString(mExternalState),
+                        getIccStateReason(mExternalState));
+            } else {
+                broadcastIccStateChangedIntent(getIccStateIntentString(mExternalState),
+                        getIccStateReason(mExternalState));
+            }
+            // TODO: Need to notify registrants for other states as well.
+            if ( State.ABSENT == mExternalState) {
+                mAbsentRegistrants.notifyRegistrants();
+            }
+        }
+    }
+
+    private void processLockedState() {
+        synchronized (mLock) {
+            if (mUiccApplication == null) {
+                //Don't need to do anything if non-existent application is locked
+                return;
+            }
+            PinState pin1State = mUiccApplication.getPin1State();
+            if (pin1State == PinState.PINSTATE_ENABLED_PERM_BLOCKED) {
+                setExternalState(State.PERM_DISABLED);
+                return;
+            }
+
+            AppState appState = mUiccApplication.getState();
+            switch (appState) {
+                case APPSTATE_PIN:
+                    mPinLockedRegistrants.notifyRegistrants();
+                    setExternalState(State.PIN_REQUIRED);
+                    break;
+                case APPSTATE_PUK:
+                    setExternalState(State.PUK_REQUIRED);
+                    break;
+                case APPSTATE_DETECTED:
+                case APPSTATE_READY:
+                case APPSTATE_SUBSCRIPTION_PERSO:
+                case APPSTATE_UNKNOWN:
+                    // Neither required
+                    break;
+            }
+        }
+    }
+
+    private void setExternalState(State newState) {
+        setExternalState(newState, false);
+    }
+
+    public boolean getIccRecordsLoaded() {
+        synchronized (mLock) {
+            if (mIccRecords != null) {
+                return mIccRecords.getRecordsLoaded();
+            }
+            return false;
+        }
+    }
+
+    private String getIccStateIntentString(State state) {
+        switch (state) {
+            case ABSENT: return IccCardConstants.INTENT_VALUE_ICC_ABSENT;
+            case PIN_REQUIRED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+            case PUK_REQUIRED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+            case NETWORK_LOCKED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+            case READY: return IccCardConstants.INTENT_VALUE_ICC_READY;
+            case NOT_READY: return IccCardConstants.INTENT_VALUE_ICC_NOT_READY;
+            case PERM_DISABLED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+            case CARD_IO_ERROR: return IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR;
+            case CARD_RESTRICTED: return IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED;
+            default: return IccCardConstants.INTENT_VALUE_ICC_UNKNOWN;
+        }
+    }
+
+    /**
+     * Locked state have a reason (PIN, PUK, NETWORK, PERM_DISABLED, CARD_IO_ERROR)
+     * @return reason
+     */
+    private String getIccStateReason(State state) {
+        switch (state) {
+            case PIN_REQUIRED: return IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN;
+            case PUK_REQUIRED: return IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK;
+            case NETWORK_LOCKED: return IccCardConstants.INTENT_VALUE_LOCKED_NETWORK;
+            case PERM_DISABLED: return IccCardConstants.INTENT_VALUE_ABSENT_ON_PERM_DISABLED;
+            case CARD_IO_ERROR: return IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR;
+            case CARD_RESTRICTED: return IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED;
+            default: return null;
+       }
+    }
+
+    /* IccCard interface implementation */
+    @Override
+    public State getState() {
+        synchronized (mLock) {
+            return mExternalState;
+        }
+    }
+
+    @Override
+    public IccRecords getIccRecords() {
+        synchronized (mLock) {
+            return mIccRecords;
+        }
+    }
+
+    @Override
+    public IccFileHandler getIccFileHandler() {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                return mUiccApplication.getIccFileHandler();
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Notifies handler of any transition into State.ABSENT
+     */
+    @Override
+    public void registerForAbsent(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+
+            mAbsentRegistrants.add(r);
+
+            if (getState() == State.ABSENT) {
+                r.notifyRegistrant();
+            }
+        }
+    }
+
+    @Override
+    public void unregisterForAbsent(Handler h) {
+        synchronized (mLock) {
+            mAbsentRegistrants.remove(h);
+        }
+    }
+
+    /**
+     * Notifies handler of any transition into State.NETWORK_LOCKED
+     */
+    @Override
+    public void registerForNetworkLocked(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+
+            mNetworkLockedRegistrants.add(r);
+
+            if (getState() == State.NETWORK_LOCKED) {
+                r.notifyRegistrant();
+            }
+        }
+    }
+
+    @Override
+    public void unregisterForNetworkLocked(Handler h) {
+        synchronized (mLock) {
+            mNetworkLockedRegistrants.remove(h);
+        }
+    }
+
+    /**
+     * Notifies handler of any transition into State.isPinLocked()
+     */
+    @Override
+    public void registerForLocked(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+
+            mPinLockedRegistrants.add(r);
+
+            if (getState().isPinLocked()) {
+                r.notifyRegistrant();
+            }
+        }
+    }
+
+    @Override
+    public void unregisterForLocked(Handler h) {
+        synchronized (mLock) {
+            mPinLockedRegistrants.remove(h);
+        }
+    }
+
+    @Override
+    public void supplyPin(String pin, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.supplyPin(pin, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void supplyPuk(String puk, String newPin, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.supplyPuk(puk, newPin, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void supplyPin2(String pin2, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.supplyPin2(pin2, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void supplyPuk2(String puk2, String newPin2, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.supplyPuk2(puk2, newPin2, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void supplyNetworkDepersonalization(String pin, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.supplyNetworkDepersonalization(pin, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("CommandsInterface is not set.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public boolean getIccLockEnabled() {
+        synchronized (mLock) {
+            /* defaults to false, if ICC is absent/deactivated */
+            Boolean retValue = mUiccApplication != null ?
+                    mUiccApplication.getIccLockEnabled() : false;
+            return retValue;
+        }
+    }
+
+    @Override
+    public boolean getIccFdnEnabled() {
+        synchronized (mLock) {
+            Boolean retValue = mUiccApplication != null ?
+                    mUiccApplication.getIccFdnEnabled() : false;
+            return retValue;
+        }
+    }
+
+    public boolean getIccFdnAvailable() {
+        boolean retValue = mUiccApplication != null ? mUiccApplication.getIccFdnAvailable() : false;
+        return retValue;
+    }
+
+    public boolean getIccPin2Blocked() {
+        /* defaults to disabled */
+        Boolean retValue = mUiccApplication != null ? mUiccApplication.getIccPin2Blocked() : false;
+        return retValue;
+    }
+
+    public boolean getIccPuk2Blocked() {
+        /* defaults to disabled */
+        Boolean retValue = mUiccApplication != null ? mUiccApplication.getIccPuk2Blocked() : false;
+        return retValue;
+    }
+
+    @Override
+    public void setIccLockEnabled(boolean enabled, String password, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.setIccLockEnabled(enabled, password, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void setIccFdnEnabled(boolean enabled, String password, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.setIccFdnEnabled(enabled, password, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void changeIccLockPassword(String oldPassword, String newPassword, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.changeIccLockPassword(oldPassword, newPassword, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void changeIccFdnPassword(String oldPassword, String newPassword, Message onComplete) {
+        synchronized (mLock) {
+            if (mUiccApplication != null) {
+                mUiccApplication.changeIccFdnPassword(oldPassword, newPassword, onComplete);
+            } else if (onComplete != null) {
+                Exception e = new RuntimeException("ICC card is absent.");
+                AsyncResult.forMessage(onComplete).exception = e;
+                onComplete.sendToTarget();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public String getServiceProviderName() {
+        synchronized (mLock) {
+            if (mIccRecords != null) {
+                return mIccRecords.getServiceProviderName();
+            }
+            return null;
+        }
+    }
+
+    @Override
+    public boolean isApplicationOnIcc(IccCardApplicationStatus.AppType type) {
+        synchronized (mLock) {
+            Boolean retValue = mUiccCard != null ? mUiccCard.isApplicationOnIcc(type) : false;
+            return retValue;
+        }
+    }
+
+    @Override
+    public boolean hasIccCard() {
+        synchronized (mLock) {
+            if (mUiccCard != null && mUiccCard.getCardState() != CardState.CARDSTATE_ABSENT) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    private void setSystemProperty(String property, String value) {
+        TelephonyManager.setTelephonyProperty(mPhoneId, property, value);
+    }
+
+    public IccRecords getIccRecord() {
+        return mIccRecords;
+    }
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("IccCardProxy: " + this);
+        pw.println(" mContext=" + mContext);
+        pw.println(" mCi=" + mCi);
+        pw.println(" mAbsentRegistrants: size=" + mAbsentRegistrants.size());
+        for (int i = 0; i < mAbsentRegistrants.size(); i++) {
+            pw.println("  mAbsentRegistrants[" + i + "]="
+                    + ((Registrant)mAbsentRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mPinLockedRegistrants: size=" + mPinLockedRegistrants.size());
+        for (int i = 0; i < mPinLockedRegistrants.size(); i++) {
+            pw.println("  mPinLockedRegistrants[" + i + "]="
+                    + ((Registrant)mPinLockedRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mNetworkLockedRegistrants: size=" + mNetworkLockedRegistrants.size());
+        for (int i = 0; i < mNetworkLockedRegistrants.size(); i++) {
+            pw.println("  mNetworkLockedRegistrants[" + i + "]="
+                    + ((Registrant)mNetworkLockedRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mCurrentAppType=" + mCurrentAppType);
+        pw.println(" mUiccController=" + mUiccController);
+        pw.println(" mUiccCard=" + mUiccCard);
+        pw.println(" mUiccApplication=" + mUiccApplication);
+        pw.println(" mIccRecords=" + mIccRecords);
+        pw.println(" mCdmaSSM=" + mCdmaSSM);
+        pw.println(" mRadioState=" + mRadioState);
+        pw.println(" mQuietMode=" + mQuietMode);
+        pw.println(" mInitialized=" + mInitialized);
+        pw.println(" mExternalState=" + mExternalState);
+
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccCardStatus.java b/com/android/internal/telephony/uicc/IccCardStatus.java
new file mode 100644
index 0000000..f14f21d
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccCardStatus.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+/**
+ * See also RIL_CardStatus in include/telephony/ril.h
+ *
+ * {@hide}
+ */
+public class IccCardStatus {
+    public static final int CARD_MAX_APPS = 8;
+
+    public enum CardState {
+        CARDSTATE_ABSENT,
+        CARDSTATE_PRESENT,
+        CARDSTATE_ERROR,
+        CARDSTATE_RESTRICTED;
+
+        boolean isCardPresent() {
+            return this == CARDSTATE_PRESENT ||
+                this == CARDSTATE_RESTRICTED;
+        }
+    }
+
+    public enum PinState {
+        PINSTATE_UNKNOWN,
+        PINSTATE_ENABLED_NOT_VERIFIED,
+        PINSTATE_ENABLED_VERIFIED,
+        PINSTATE_DISABLED,
+        PINSTATE_ENABLED_BLOCKED,
+        PINSTATE_ENABLED_PERM_BLOCKED;
+
+        boolean isPermBlocked() {
+            return this == PINSTATE_ENABLED_PERM_BLOCKED;
+        }
+
+        boolean isPinRequired() {
+            return this == PINSTATE_ENABLED_NOT_VERIFIED;
+        }
+
+        boolean isPukRequired() {
+            return this == PINSTATE_ENABLED_BLOCKED;
+        }
+    }
+
+    public CardState  mCardState;
+    public PinState   mUniversalPinState;
+    public int        mGsmUmtsSubscriptionAppIndex;
+    public int        mCdmaSubscriptionAppIndex;
+    public int        mImsSubscriptionAppIndex;
+
+    public IccCardApplicationStatus[] mApplications;
+
+    public void setCardState(int state) {
+        switch(state) {
+        case 0:
+            mCardState = CardState.CARDSTATE_ABSENT;
+            break;
+        case 1:
+            mCardState = CardState.CARDSTATE_PRESENT;
+            break;
+        case 2:
+            mCardState = CardState.CARDSTATE_ERROR;
+            break;
+        case 3:
+            mCardState = CardState.CARDSTATE_RESTRICTED;
+            break;
+        default:
+            throw new RuntimeException("Unrecognized RIL_CardState: " + state);
+        }
+    }
+
+    public void setUniversalPinState(int state) {
+        switch(state) {
+        case 0:
+            mUniversalPinState = PinState.PINSTATE_UNKNOWN;
+            break;
+        case 1:
+            mUniversalPinState = PinState.PINSTATE_ENABLED_NOT_VERIFIED;
+            break;
+        case 2:
+            mUniversalPinState = PinState.PINSTATE_ENABLED_VERIFIED;
+            break;
+        case 3:
+            mUniversalPinState = PinState.PINSTATE_DISABLED;
+            break;
+        case 4:
+            mUniversalPinState = PinState.PINSTATE_ENABLED_BLOCKED;
+            break;
+        case 5:
+            mUniversalPinState = PinState.PINSTATE_ENABLED_PERM_BLOCKED;
+            break;
+        default:
+            throw new RuntimeException("Unrecognized RIL_PinState: " + state);
+        }
+    }
+
+    @Override
+    public String toString() {
+        IccCardApplicationStatus app;
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("IccCardState {").append(mCardState).append(",")
+        .append(mUniversalPinState)
+        .append(",num_apps=").append(mApplications.length);
+
+        sb.append(",gsm_id=").append(mGsmUmtsSubscriptionAppIndex);
+        if (mApplications != null
+                && mGsmUmtsSubscriptionAppIndex >= 0
+                && mGsmUmtsSubscriptionAppIndex < mApplications.length) {
+            app = mApplications[mGsmUmtsSubscriptionAppIndex];
+            sb.append(app == null ? "null" : app);
+        }
+
+        sb.append(",cdma_id=").append(mCdmaSubscriptionAppIndex);
+        if (mApplications != null
+                && mCdmaSubscriptionAppIndex >= 0
+                && mCdmaSubscriptionAppIndex < mApplications.length) {
+            app = mApplications[mCdmaSubscriptionAppIndex];
+            sb.append(app == null ? "null" : app);
+        }
+
+        sb.append(",ims_id=").append(mImsSubscriptionAppIndex);
+        if (mApplications != null
+                && mImsSubscriptionAppIndex >= 0
+                && mImsSubscriptionAppIndex < mApplications.length) {
+            app = mApplications[mImsSubscriptionAppIndex];
+            sb.append(app == null ? "null" : app);
+        }
+
+        sb.append("}");
+        return sb.toString();
+    }
+
+}
diff --git a/com/android/internal/telephony/uicc/IccConstants.java b/com/android/internal/telephony/uicc/IccConstants.java
new file mode 100644
index 0000000..0f41f1e
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccConstants.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+/**
+ * {@hide}
+ */
+public interface IccConstants {
+    // GSM SIM file ids from TS 51.011
+    static final int EF_ADN = 0x6F3A;
+    static final int EF_FDN = 0x6F3B;
+    static final int EF_GID1 = 0x6F3E;
+    static final int EF_GID2 = 0x6F3F;
+    static final int EF_SDN = 0x6F49;
+    static final int EF_EXT1 = 0x6F4A;
+    static final int EF_EXT2 = 0x6F4B;
+    static final int EF_EXT3 = 0x6F4C;
+    static final int EF_EXT5 = 0x6F4E;
+    static final int EF_EXT6 = 0x6FC8;   // Ext record for EF[MBDN]
+    static final int EF_MWIS = 0x6FCA;
+    static final int EF_MBDN = 0x6FC7;
+    static final int EF_PNN = 0x6FC5;
+    static final int EF_OPL = 0x6FC6;
+    static final int EF_SPN = 0x6F46;
+    static final int EF_SMS = 0x6F3C;
+    static final int EF_ICCID = 0x2FE2;
+    static final int EF_AD = 0x6FAD;
+    static final int EF_MBI = 0x6FC9;
+    static final int EF_MSISDN = 0x6F40;
+    static final int EF_SPDI = 0x6FCD;
+    static final int EF_SST = 0x6F38;
+    static final int EF_CFIS = 0x6FCB;
+    static final int EF_IMG = 0x4F20;
+
+    // USIM SIM file ids from TS 131.102
+    public static final int EF_PBR = 0x4F30;
+    public static final int EF_LI = 0x6F05;
+
+    // GSM SIM file ids from CPHS (phase 2, version 4.2) CPHS4_2.WW6
+    static final int EF_MAILBOX_CPHS = 0x6F17;
+    static final int EF_VOICE_MAIL_INDICATOR_CPHS = 0x6F11;
+    static final int EF_CFF_CPHS = 0x6F13;
+    static final int EF_SPN_CPHS = 0x6F14;
+    static final int EF_SPN_SHORT_CPHS = 0x6F18;
+    static final int EF_INFO_CPHS = 0x6F16;
+    static final int EF_CSP_CPHS = 0x6F15;
+
+    // CDMA RUIM file ids from 3GPP2 C.S0023-0
+    static final int EF_CST = 0x6F32;
+    static final int EF_RUIM_SPN =0x6F41;
+
+    // ETSI TS.102.221
+    static final int EF_PL = 0x2F05;
+    // 3GPP2 C.S0065
+    static final int EF_CSIM_LI = 0x6F3A;
+    static final int EF_CSIM_SPN =0x6F41;
+    static final int EF_CSIM_MDN = 0x6F44;
+    static final int EF_CSIM_IMSIM = 0x6F22;
+    static final int EF_CSIM_CDMAHOME = 0x6F28;
+    static final int EF_CSIM_EPRL = 0x6F5A;
+    static final int EF_CSIM_MIPUPP = 0x6F4D;
+
+    //ISIM access
+    static final int EF_IMPU = 0x6F04;
+    static final int EF_IMPI = 0x6F02;
+    static final int EF_DOMAIN = 0x6F03;
+    static final int EF_IST = 0x6F07;
+    static final int EF_PCSCF = 0x6F09;
+    static final int EF_PSI = 0x6FE5;
+
+    //PLMN Selection Information w/ Access Technology TS 131.102
+    static final int EF_PLMN_W_ACT = 0x6F60;
+    static final int EF_OPLMN_W_ACT = 0x6F61;
+    static final int EF_HPLMN_W_ACT = 0x6F62;
+
+    //Equivalent Home and Forbidden PLMN Lists TS 131.102
+    static final int EF_EHPLMN = 0x6FD9;
+    static final int EF_FPLMN = 0x6F7B;
+
+    // Last Roaming Selection Indicator
+    static final int EF_LRPLMNSI = 0x6FDC;
+
+    //Search interval for higher priority PLMNs
+    static final int EF_HPPLMN = 0x6F31;
+
+    static final String MF_SIM = "3F00";
+    static final String DF_TELECOM = "7F10";
+    static final String DF_PHONEBOOK = "5F3A";
+    static final String DF_GRAPHICS = "5F50";
+    static final String DF_GSM = "7F20";
+    static final String DF_CDMA = "7F25";
+
+    //UICC access
+    static final String DF_ADF = "7FFF";
+}
diff --git a/com/android/internal/telephony/uicc/IccException.java b/com/android/internal/telephony/uicc/IccException.java
new file mode 100644
index 0000000..1ba5b28
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+/**
+ * {@hide}
+ */
+public class IccException extends Exception {
+    public IccException() {
+
+    }
+
+    public IccException(String s) {
+        super(s);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccFileHandler.java b/com/android/internal/telephony/uicc/IccFileHandler.java
new file mode 100644
index 0000000..33dd381
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccFileHandler.java
@@ -0,0 +1,641 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.uicc;
+
+import android.os.*;
+
+import com.android.internal.telephony.CommandsInterface;
+
+import java.util.ArrayList;
+
+/**
+ * {@hide}
+ */
+public abstract class IccFileHandler extends Handler implements IccConstants {
+    private static final boolean VDBG = false;
+
+    //from TS 11.11 9.1 or elsewhere
+    static protected final int COMMAND_READ_BINARY = 0xb0;
+    static protected final int COMMAND_UPDATE_BINARY = 0xd6;
+    static protected final int COMMAND_READ_RECORD = 0xb2;
+    static protected final int COMMAND_UPDATE_RECORD = 0xdc;
+    static protected final int COMMAND_SEEK = 0xa2;
+    static protected final int COMMAND_GET_RESPONSE = 0xc0;
+
+    // from TS 11.11 9.2.5
+    static protected final int READ_RECORD_MODE_ABSOLUTE = 4;
+
+    //***** types of files  TS 11.11 9.3
+    static protected final int EF_TYPE_TRANSPARENT = 0;
+    static protected final int EF_TYPE_LINEAR_FIXED = 1;
+    static protected final int EF_TYPE_CYCLIC = 3;
+
+    //***** types of files  TS 11.11 9.3
+    static protected final int TYPE_RFU = 0;
+    static protected final int TYPE_MF  = 1;
+    static protected final int TYPE_DF  = 2;
+    static protected final int TYPE_EF  = 4;
+
+    // size of GET_RESPONSE for EF's
+    static protected final int GET_RESPONSE_EF_SIZE_BYTES = 15;
+    static protected final int GET_RESPONSE_EF_IMG_SIZE_BYTES = 10;
+
+    // Byte order received in response to COMMAND_GET_RESPONSE
+    // Refer TS 51.011 Section 9.2.1
+    static protected final int RESPONSE_DATA_RFU_1 = 0;
+    static protected final int RESPONSE_DATA_RFU_2 = 1;
+
+    static protected final int RESPONSE_DATA_FILE_SIZE_1 = 2;
+    static protected final int RESPONSE_DATA_FILE_SIZE_2 = 3;
+
+    static protected final int RESPONSE_DATA_FILE_ID_1 = 4;
+    static protected final int RESPONSE_DATA_FILE_ID_2 = 5;
+    static protected final int RESPONSE_DATA_FILE_TYPE = 6;
+    static protected final int RESPONSE_DATA_RFU_3 = 7;
+    static protected final int RESPONSE_DATA_ACCESS_CONDITION_1 = 8;
+    static protected final int RESPONSE_DATA_ACCESS_CONDITION_2 = 9;
+    static protected final int RESPONSE_DATA_ACCESS_CONDITION_3 = 10;
+    static protected final int RESPONSE_DATA_FILE_STATUS = 11;
+    static protected final int RESPONSE_DATA_LENGTH = 12;
+    static protected final int RESPONSE_DATA_STRUCTURE = 13;
+    static protected final int RESPONSE_DATA_RECORD_LENGTH = 14;
+
+
+    //***** Events
+
+    /** Finished retrieving size of transparent EF; start loading. */
+    static protected final int EVENT_GET_BINARY_SIZE_DONE = 4;
+    /** Finished loading contents of transparent EF; post result. */
+    static protected final int EVENT_READ_BINARY_DONE = 5;
+    /** Finished retrieving size of records for linear-fixed EF; now load. */
+    static protected final int EVENT_GET_RECORD_SIZE_DONE = 6;
+    /** Finished loading single record from a linear-fixed EF; post result. */
+    static protected final int EVENT_READ_RECORD_DONE = 7;
+    /** Finished retrieving record size; post result. */
+    static protected final int EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE = 8;
+    /** Finished retrieving image instance record; post result. */
+    static protected final int EVENT_READ_IMG_DONE = 9;
+    /** Finished retrieving icon data; post result. */
+    static protected final int EVENT_READ_ICON_DONE = 10;
+    /** Finished retrieving size of record for EFimg now. */
+    static protected final int EVENT_GET_RECORD_SIZE_IMG_DONE = 11;
+
+     // member variables
+    protected final CommandsInterface mCi;
+    protected final UiccCardApplication mParentApp;
+    protected final String mAid;
+
+    static class LoadLinearFixedContext {
+
+        int mEfid;
+        int mRecordNum, mRecordSize, mCountRecords;
+        boolean mLoadAll;
+        String mPath;
+
+        Message mOnLoaded;
+
+        ArrayList<byte[]> results;
+
+        LoadLinearFixedContext(int efid, int recordNum, Message onLoaded) {
+            mEfid = efid;
+            mRecordNum = recordNum;
+            mOnLoaded = onLoaded;
+            mLoadAll = false;
+            mPath = null;
+        }
+
+        LoadLinearFixedContext(int efid, int recordNum, String path, Message onLoaded) {
+            mEfid = efid;
+            mRecordNum = recordNum;
+            mOnLoaded = onLoaded;
+            mLoadAll = false;
+            mPath = path;
+        }
+
+        LoadLinearFixedContext(int efid, String path, Message onLoaded) {
+            mEfid = efid;
+            mRecordNum = 1;
+            mLoadAll = true;
+            mOnLoaded = onLoaded;
+            mPath = path;
+        }
+
+        LoadLinearFixedContext(int efid, Message onLoaded) {
+            mEfid = efid;
+            mRecordNum = 1;
+            mLoadAll = true;
+            mOnLoaded = onLoaded;
+            mPath = null;
+        }
+    }
+
+    /**
+     * Default constructor
+     */
+    protected IccFileHandler(UiccCardApplication app, String aid, CommandsInterface ci) {
+        mParentApp = app;
+        mAid = aid;
+        mCi = ci;
+    }
+
+    public void dispose() {
+    }
+
+    //***** Public Methods
+
+    /**
+     * Load a record from a SIM Linear Fixed EF
+     *
+     * @param fileid EF id
+     * @param path Path of the EF on the card
+     * @param recordNum 1-based (not 0-based) record number
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is the byte[]
+     *
+     */
+    public void loadEFLinearFixed(int fileid, String path, int recordNum, Message onLoaded) {
+        String efPath = (path == null) ? getEFPath(fileid) : path;
+        Message response
+                = obtainMessage(EVENT_GET_RECORD_SIZE_DONE,
+                        new LoadLinearFixedContext(fileid, recordNum, efPath, onLoaded));
+
+        mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, efPath,
+                        0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response);
+    }
+
+    /**
+     * Load a record from a SIM Linear Fixed EF
+     *
+     * @param fileid EF id
+     * @param recordNum 1-based (not 0-based) record number
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is the byte[]
+     *
+     */
+    public void loadEFLinearFixed(int fileid, int recordNum, Message onLoaded) {
+        loadEFLinearFixed(fileid, getEFPath(fileid), recordNum, onLoaded);
+    }
+
+    /**
+     * Load a image instance record from a SIM Linear Fixed EF-IMG
+     *
+     * @param recordNum 1-based (not 0-based) record number
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is the byte[]
+     *
+     */
+    public void loadEFImgLinearFixed(int recordNum, Message onLoaded) {
+        Message response = obtainMessage(EVENT_GET_RECORD_SIZE_IMG_DONE,
+                new LoadLinearFixedContext(IccConstants.EF_IMG, recordNum,
+                        onLoaded));
+
+        mCi.iccIOForApp(COMMAND_GET_RESPONSE, IccConstants.EF_IMG,
+                    getEFPath(IccConstants.EF_IMG), recordNum,
+                    READ_RECORD_MODE_ABSOLUTE, GET_RESPONSE_EF_IMG_SIZE_BYTES,
+                    null, null, mAid, response);
+    }
+
+    /**
+     * get record size for a linear fixed EF
+     *
+     * @param fileid EF id
+     * @param path Path of the EF on the card
+     * @param onLoaded ((AsnyncResult)(onLoaded.obj)).result is the recordSize[]
+     *        int[0] is the record length int[1] is the total length of the EF
+     *        file int[3] is the number of records in the EF file So int[0] *
+     *        int[3] = int[1]
+     */
+    public void getEFLinearRecordSize(int fileid, String path, Message onLoaded) {
+        String efPath = (path == null) ? getEFPath(fileid) : path;
+        Message response
+                = obtainMessage(EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE,
+                        new LoadLinearFixedContext(fileid, efPath, onLoaded));
+        mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, efPath,
+                    0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response);
+    }
+
+    /**
+     * get record size for a linear fixed EF
+     *
+     * @param fileid EF id
+     * @param onLoaded ((AsnyncResult)(onLoaded.obj)).result is the recordSize[]
+     *        int[0] is the record length int[1] is the total length of the EF
+     *        file int[3] is the number of records in the EF file So int[0] *
+     *        int[3] = int[1]
+     */
+    public void getEFLinearRecordSize(int fileid, Message onLoaded) {
+        getEFLinearRecordSize(fileid, getEFPath(fileid), onLoaded);
+    }
+
+    /**
+     * Load all records from a SIM Linear Fixed EF
+     *
+     * @param fileid EF id
+     * @param path Path of the EF on the card
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is an ArrayList<byte[]>
+     *
+     */
+    public void loadEFLinearFixedAll(int fileid, String path, Message onLoaded) {
+        String efPath = (path == null) ? getEFPath(fileid) : path;
+        Message response = obtainMessage(EVENT_GET_RECORD_SIZE_DONE,
+                        new LoadLinearFixedContext(fileid, efPath, onLoaded));
+
+        mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, efPath,
+                        0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response);
+    }
+
+    /**
+     * Load all records from a SIM Linear Fixed EF
+     *
+     * @param fileid EF id
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is an ArrayList<byte[]>
+     *
+     */
+    public void loadEFLinearFixedAll(int fileid, Message onLoaded) {
+        loadEFLinearFixedAll(fileid, getEFPath(fileid), onLoaded);
+    }
+
+    /**
+     * Load a SIM Transparent EF
+     *
+     * @param fileid EF id
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is the byte[]
+     *
+     */
+
+    public void loadEFTransparent(int fileid, Message onLoaded) {
+        Message response = obtainMessage(EVENT_GET_BINARY_SIZE_DONE,
+                        fileid, 0, onLoaded);
+
+        mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, getEFPath(fileid),
+                        0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response);
+    }
+
+    /**
+     * Load first @size bytes from SIM Transparent EF
+     *
+     * @param fileid EF id
+     * @param size
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is the byte[]
+     *
+     */
+    public void loadEFTransparent(int fileid, int size, Message onLoaded) {
+        Message response = obtainMessage(EVENT_READ_BINARY_DONE,
+                        fileid, 0, onLoaded);
+
+        mCi.iccIOForApp(COMMAND_READ_BINARY, fileid, getEFPath(fileid),
+                        0, 0, size, null, null, mAid, response);
+    }
+
+    /**
+     * Load a SIM Transparent EF-IMG. Used right after loadEFImgLinearFixed to
+     * retrive STK's icon data.
+     *
+     * @param fileid EF id
+     * @param onLoaded
+     *
+     * ((AsyncResult)(onLoaded.obj)).result is the byte[]
+     *
+     */
+    public void loadEFImgTransparent(int fileid, int highOffset, int lowOffset,
+            int length, Message onLoaded) {
+        Message response = obtainMessage(EVENT_READ_ICON_DONE, fileid, 0,
+                onLoaded);
+
+        logd("IccFileHandler: loadEFImgTransparent fileid = " + fileid
+                + " filePath = " + getEFPath(EF_IMG) + " highOffset = " + highOffset
+                + " lowOffset = " + lowOffset + " length = " + length);
+
+        /* Per TS 31.102, for displaying of Icon, under
+         * DF Telecom and DF Graphics , EF instance(s) (4FXX,transparent files)
+         * are present. The possible image file identifiers (EF instance) for
+         * EF img ( 4F20, linear fixed file) are : 4F01 ... 4F05.
+         * It should be MF_SIM + DF_TELECOM + DF_GRAPHICS, same path as EF IMG
+         */
+        mCi.iccIOForApp(COMMAND_READ_BINARY, fileid, getEFPath(EF_IMG),
+                highOffset, lowOffset, length, null, null, mAid, response);
+    }
+
+    /**
+     * Update a record in a linear fixed EF
+     * @param fileid EF id
+     * @param path Path of the EF on the card
+     * @param recordNum 1-based (not 0-based) record number
+     * @param data must be exactly as long as the record in the EF
+     * @param pin2 for CHV2 operations, otherwist must be null
+     * @param onComplete onComplete.obj will be an AsyncResult
+     *                   onComplete.obj.userObj will be a IccIoResult on success
+     */
+    public void updateEFLinearFixed(int fileid, String path, int recordNum, byte[] data,
+            String pin2, Message onComplete) {
+        String efPath = (path == null) ? getEFPath(fileid) : path;
+        mCi.iccIOForApp(COMMAND_UPDATE_RECORD, fileid, efPath,
+                        recordNum, READ_RECORD_MODE_ABSOLUTE, data.length,
+                        IccUtils.bytesToHexString(data), pin2, mAid, onComplete);
+    }
+
+    /**
+     * Update a record in a linear fixed EF
+     * @param fileid EF id
+     * @param recordNum 1-based (not 0-based) record number
+     * @param data must be exactly as long as the record in the EF
+     * @param pin2 for CHV2 operations, otherwist must be null
+     * @param onComplete onComplete.obj will be an AsyncResult
+     *                   onComplete.obj.userObj will be a IccIoResult on success
+     */
+    public void updateEFLinearFixed(int fileid, int recordNum, byte[] data,
+            String pin2, Message onComplete) {
+        mCi.iccIOForApp(COMMAND_UPDATE_RECORD, fileid, getEFPath(fileid),
+                        recordNum, READ_RECORD_MODE_ABSOLUTE, data.length,
+                        IccUtils.bytesToHexString(data), pin2, mAid, onComplete);
+    }
+
+    /**
+     * Update a transparent EF
+     * @param fileid EF id
+     * @param data must be exactly as long as the EF
+     */
+    public void updateEFTransparent(int fileid, byte[] data, Message onComplete) {
+        mCi.iccIOForApp(COMMAND_UPDATE_BINARY, fileid, getEFPath(fileid),
+                        0, 0, data.length,
+                        IccUtils.bytesToHexString(data), null, mAid, onComplete);
+    }
+
+
+    //***** Abstract Methods
+
+
+    //***** Private Methods
+
+    private void sendResult(Message response, Object result, Throwable ex) {
+        if (response == null) {
+            return;
+        }
+
+        AsyncResult.forMessage(response, result, ex);
+
+        response.sendToTarget();
+    }
+
+    private boolean processException(Message response, AsyncResult ar) {
+        IccException iccException;
+        boolean flag = false;
+        IccIoResult result = (IccIoResult) ar.result;
+        if (ar.exception != null) {
+            sendResult(response, null, ar.exception);
+            flag = true;
+        } else {
+            iccException = result.getException();
+            if (iccException != null) {
+                sendResult(response, null, iccException);
+                flag = true;
+            }
+        }
+        return flag;
+    }
+
+    //***** Overridden from Handler
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+        IccIoResult result;
+        Message response = null;
+        String str;
+        LoadLinearFixedContext lc;
+
+        byte data[];
+        int size;
+        int fileid;
+        int recordSize[];
+        String path = null;
+
+        try {
+            switch (msg.what) {
+            case EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE:
+                ar = (AsyncResult)msg.obj;
+                lc = (LoadLinearFixedContext) ar.userObj;
+                result = (IccIoResult) ar.result;
+                response = lc.mOnLoaded;
+
+                if (processException(response, (AsyncResult) msg.obj)) {
+                    break;
+                }
+
+                data = result.payload;
+
+                if (TYPE_EF != data[RESPONSE_DATA_FILE_TYPE] ||
+                    EF_TYPE_LINEAR_FIXED != data[RESPONSE_DATA_STRUCTURE]) {
+                    throw new IccFileTypeMismatch();
+                }
+
+                recordSize = new int[3];
+                recordSize[0] = data[RESPONSE_DATA_RECORD_LENGTH] & 0xFF;
+                recordSize[1] = ((data[RESPONSE_DATA_FILE_SIZE_1] & 0xff) << 8)
+                       + (data[RESPONSE_DATA_FILE_SIZE_2] & 0xff);
+                recordSize[2] = recordSize[1] / recordSize[0];
+
+                sendResult(response, recordSize, null);
+                break;
+
+             case EVENT_GET_RECORD_SIZE_IMG_DONE:
+             case EVENT_GET_RECORD_SIZE_DONE:
+                ar = (AsyncResult)msg.obj;
+                lc = (LoadLinearFixedContext) ar.userObj;
+                result = (IccIoResult) ar.result;
+                response = lc.mOnLoaded;
+
+                if (processException(response, (AsyncResult) msg.obj)) {
+                    loge("exception caught from EVENT_GET_RECORD_SIZE");
+                    break;
+                }
+
+                data = result.payload;
+                path = lc.mPath;
+
+                if (TYPE_EF != data[RESPONSE_DATA_FILE_TYPE]) {
+                    throw new IccFileTypeMismatch();
+                }
+
+                if (EF_TYPE_LINEAR_FIXED != data[RESPONSE_DATA_STRUCTURE]) {
+                    throw new IccFileTypeMismatch();
+                }
+
+                lc.mRecordSize = data[RESPONSE_DATA_RECORD_LENGTH] & 0xFF;
+
+                size = ((data[RESPONSE_DATA_FILE_SIZE_1] & 0xff) << 8)
+                       + (data[RESPONSE_DATA_FILE_SIZE_2] & 0xff);
+
+                lc.mCountRecords = size / lc.mRecordSize;
+
+                 if (lc.mLoadAll) {
+                     lc.results = new ArrayList<byte[]>(lc.mCountRecords);
+                 }
+
+                 if (path == null) {
+                     path = getEFPath(lc.mEfid);
+                 }
+                 mCi.iccIOForApp(COMMAND_READ_RECORD, lc.mEfid, path,
+                         lc.mRecordNum,
+                         READ_RECORD_MODE_ABSOLUTE,
+                         lc.mRecordSize, null, null, mAid,
+                         obtainMessage(EVENT_READ_RECORD_DONE, lc));
+                 break;
+            case EVENT_GET_BINARY_SIZE_DONE:
+                ar = (AsyncResult)msg.obj;
+                response = (Message) ar.userObj;
+                result = (IccIoResult) ar.result;
+
+                if (processException(response, (AsyncResult) msg.obj)) {
+                    break;
+                }
+
+                data = result.payload;
+
+                fileid = msg.arg1;
+
+                if (VDBG) {
+                    logd(String.format("Contents of the Select Response for command %x: ", fileid)
+                            + IccUtils.bytesToHexString(data));
+                }
+
+                if (TYPE_EF != data[RESPONSE_DATA_FILE_TYPE]) {
+                    throw new IccFileTypeMismatch();
+                }
+
+                if (EF_TYPE_TRANSPARENT != data[RESPONSE_DATA_STRUCTURE]) {
+                    throw new IccFileTypeMismatch();
+                }
+
+                size = ((data[RESPONSE_DATA_FILE_SIZE_1] & 0xff) << 8)
+                       + (data[RESPONSE_DATA_FILE_SIZE_2] & 0xff);
+
+                mCi.iccIOForApp(COMMAND_READ_BINARY, fileid, getEFPath(fileid),
+                                0, 0, size, null, null, mAid,
+                                obtainMessage(EVENT_READ_BINARY_DONE,
+                                              fileid, 0, response));
+            break;
+
+            case EVENT_READ_IMG_DONE:
+            case EVENT_READ_RECORD_DONE:
+
+                ar = (AsyncResult)msg.obj;
+                lc = (LoadLinearFixedContext) ar.userObj;
+                result = (IccIoResult) ar.result;
+                response = lc.mOnLoaded;
+                path = lc.mPath;
+
+                if (processException(response, (AsyncResult) msg.obj)) {
+                    break;
+                }
+
+                if (!lc.mLoadAll) {
+                    sendResult(response, result.payload, null);
+                } else {
+                    lc.results.add(result.payload);
+
+                    lc.mRecordNum++;
+
+                    if (lc.mRecordNum > lc.mCountRecords) {
+                        sendResult(response, lc.results, null);
+                    } else {
+                        if (path == null) {
+                            path = getEFPath(lc.mEfid);
+                        }
+
+                        mCi.iccIOForApp(COMMAND_READ_RECORD, lc.mEfid, path,
+                                    lc.mRecordNum,
+                                    READ_RECORD_MODE_ABSOLUTE,
+                                    lc.mRecordSize, null, null, mAid,
+                                    obtainMessage(EVENT_READ_RECORD_DONE, lc));
+                    }
+                }
+
+            break;
+
+            case EVENT_READ_BINARY_DONE:
+            case EVENT_READ_ICON_DONE:
+                ar = (AsyncResult)msg.obj;
+                response = (Message) ar.userObj;
+                result = (IccIoResult) ar.result;
+
+                if (processException(response, (AsyncResult) msg.obj)) {
+                    break;
+                }
+
+                sendResult(response, result.payload, null);
+            break;
+
+        }} catch (Exception exc) {
+            if (response != null) {
+                sendResult(response, null, exc);
+            } else {
+                loge("uncaught exception" + exc);
+            }
+        }
+    }
+
+    /**
+     * Returns the root path of the EF file.
+     * i.e returns MasterFile + DFfile as a string.
+     * Ex: For EF_ADN on a SIM, it will return "3F007F10"
+     * This function handles only EFids that are common to
+     * RUIM, SIM, USIM and other types of Icc cards.
+     *
+     * @param efid of path to retrieve
+     * @return root path of the file.
+     */
+    protected String getCommonIccEFPath(int efid) {
+        switch(efid) {
+        case EF_ADN:
+        case EF_FDN:
+        case EF_MSISDN:
+        case EF_SDN:
+        case EF_EXT1:
+        case EF_EXT2:
+        case EF_EXT3:
+        case EF_PSI:
+            return MF_SIM + DF_TELECOM;
+
+        case EF_ICCID:
+        case EF_PL:
+            return MF_SIM;
+        case EF_PBR:
+            // we only support global phonebook.
+            return MF_SIM + DF_TELECOM + DF_PHONEBOOK;
+        case EF_IMG:
+            return MF_SIM + DF_TELECOM + DF_GRAPHICS;
+        }
+        return null;
+    }
+
+    protected abstract String getEFPath(int efid);
+    protected abstract void logd(String s);
+    protected abstract void loge(String s);
+
+}
diff --git a/com/android/internal/telephony/uicc/IccFileNotFound.java b/com/android/internal/telephony/uicc/IccFileNotFound.java
new file mode 100644
index 0000000..08bfcf9
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccFileNotFound.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+
+/**
+ * {@hide}
+ */
+public class IccFileNotFound extends IccException {
+    IccFileNotFound() {
+
+    }
+
+    IccFileNotFound(String s) {
+        super(s);
+    }
+
+    IccFileNotFound(int ef) {
+        super("ICC EF Not Found 0x" + Integer.toHexString(ef));
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccFileTypeMismatch.java b/com/android/internal/telephony/uicc/IccFileTypeMismatch.java
new file mode 100644
index 0000000..7e4794e
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccFileTypeMismatch.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+
+/**
+ * {@hide}
+ */
+public class IccFileTypeMismatch extends IccException {
+    public IccFileTypeMismatch() {
+
+    }
+
+    public IccFileTypeMismatch(String s) {
+        super(s);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccIoResult.java b/com/android/internal/telephony/uicc/IccIoResult.java
new file mode 100644
index 0000000..4a35e14
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccIoResult.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+
+/**
+ * {@hide}
+ */
+public class
+IccIoResult {
+
+    private static final String UNKNOWN_ERROR = "unknown";
+
+    private String getErrorString() {
+        // Errors from 3gpp 11.11 9.4.1
+        // Additional Errors from ETSI 102.221
+        //
+        // All error codes below are copied directly from their respective specification
+        // without modification except in cases where necessary string formatting has been omitted.
+        switch(sw1) {
+            case 0x62:
+                switch(sw2) {
+                    case 0x00: return "No information given,"
+                               + " state of non volatile memory unchanged";
+                    case 0x81: return "Part of returned data may be corrupted";
+                    case 0x82: return "End of file/record reached before reading Le bytes";
+                    case 0x83: return "Selected file invalidated";
+                    case 0x84: return "Selected file in termination state";
+                    case 0xF1: return "More data available";
+                    case 0xF2: return "More data available and proactive command pending";
+                    case 0xF3: return "Response data available";
+                }
+                break;
+            case 0x63:
+                if (sw2 >> 4 == 0x0C) {
+                    return "Command successful but after using an internal"
+                        + "update retry routine but Verification failed";
+                }
+                switch(sw2) {
+                    case 0xF1: return "More data expected";
+                    case 0xF2: return "More data expected and proactive command pending";
+                }
+                break;
+            case 0x64:
+                switch(sw2) {
+                    case 0x00: return "No information given,"
+                               + " state of non-volatile memory unchanged";
+                }
+                break;
+            case 0x65:
+                switch(sw2) {
+                    case 0x00: return "No information given, state of non-volatile memory changed";
+                    case 0x81: return "Memory problem";
+                }
+                break;
+            case 0x67:
+                switch(sw2) {
+                    case 0x00: return "incorrect parameter P3";
+                    default: return "The interpretation of this status word is command dependent";
+                }
+                // break;
+            case 0x6B: return "incorrect parameter P1 or P2";
+            case 0x6D: return "unknown instruction code given in the command";
+            case 0x6E: return "wrong instruction class given in the command";
+            case 0x6F:
+                switch(sw2) {
+                    case 0x00: return "technical problem with no diagnostic given";
+                    default: return "The interpretation of this status word is command dependent";
+                }
+                // break;
+            case 0x68:
+                switch(sw2) {
+                    case 0x00: return "No information given";
+                    case 0x81: return "Logical channel not supported";
+                    case 0x82: return "Secure messaging not supported";
+                }
+                break;
+            case 0x69:
+                switch(sw2) {
+                    case 0x00: return "No information given";
+                    case 0x81: return "Command incompatible with file structure";
+                    case 0x82: return "Security status not satisfied";
+                    case 0x83: return "Authentication/PIN method blocked";
+                    case 0x84: return "Referenced data invalidated";
+                    case 0x85: return "Conditions of use not satisfied";
+                    case 0x86: return "Command not allowed (no EF selected)";
+                    case 0x89: return "Command not allowed - secure channel -"
+                               + " security not satisfied";
+                }
+                break;
+            case 0x6A:
+                switch(sw2) {
+                    case 0x80: return "Incorrect parameters in the data field";
+                    case 0x81: return "Function not supported";
+                    case 0x82: return "File not found";
+                    case 0x83: return "Record not found";
+                    case 0x84: return "Not enough memory space";
+                    case 0x86: return "Incorrect parameters P1 to P2";
+                    case 0x87: return "Lc inconsistent with P1 to P2";
+                    case 0x88: return "Referenced data not found";
+                }
+                break;
+            case 0x90: return null; // success
+            case 0x91: return null; // success
+            //Status Code 0x92 has contradictory meanings from 11.11 and 102.221 10.2.1.1
+            case 0x92:
+                if (sw2 >> 4 == 0) {
+                    return "command successful but after using an internal update retry routine";
+                }
+                switch(sw2) {
+                    case 0x40: return "memory problem";
+                }
+                break;
+            case 0x93:
+                switch(sw2) {
+                    case 0x00:
+                        return "SIM Application Toolkit is busy. Command cannot be executed"
+                            + " at present, further normal commands are allowed.";
+                }
+                break;
+            case 0x94:
+                switch(sw2) {
+                    case 0x00: return "no EF selected";
+                    case 0x02: return "out f range (invalid address)";
+                    case 0x04: return "file ID not found/pattern not found";
+                    case 0x08: return "file is inconsistent with the command";
+                }
+                break;
+            case 0x98:
+                switch(sw2) {
+                    case 0x02: return "no CHV initialized";
+                    case 0x04: return "access condition not fulfilled/"
+                            + "unsuccessful CHV verification, at least one attempt left/"
+                            + "unsuccessful UNBLOCK CHV verification, at least one attempt left/"
+                            + "authentication failed";
+                    case 0x08: return "in contradiction with CHV status";
+                    case 0x10: return "in contradiction with invalidation status";
+                    case 0x40: return "unsuccessful CHV verification, no attempt left/"
+                            + "unsuccessful UNBLOCK CHV verification, no attempt left/"
+                            + "CHV blocked"
+                            + "UNBLOCK CHV blocked";
+                    case 0x50: return "increase cannot be performed, Max value reached";
+                }
+                break;
+            case 0x9E: return null; // success
+            case 0x9F: return null; // success
+        }
+        return UNKNOWN_ERROR;
+    }
+
+
+    public int sw1;
+    public int sw2;
+
+    public byte[] payload;
+
+    public IccIoResult(int sw1, int sw2, byte[] payload) {
+        this.sw1 = sw1;
+        this.sw2 = sw2;
+        this.payload = payload;
+    }
+
+    public IccIoResult(int sw1, int sw2, String hexString) {
+        this(sw1, sw2, IccUtils.hexStringToBytes(hexString));
+    }
+
+    @Override
+    public String toString() {
+        return "IccIoResult sw1:0x" + Integer.toHexString(sw1) + " sw2:0x"
+                + Integer.toHexString(sw2) + ((!success()) ? " Error: " + getErrorString() : "");
+    }
+
+    /**
+     * true if this operation was successful
+     * See GSM 11.11 Section 9.4
+     * (the fun stuff is absent in 51.011)
+     */
+    public boolean success() {
+        return sw1 == 0x90 || sw1 == 0x91 || sw1 == 0x9e || sw1 == 0x9f;
+    }
+
+    /**
+     * Returns exception on error or null if success
+     */
+    public IccException getException() {
+        if (success()) return null;
+
+        switch (sw1) {
+            case 0x94:
+                if (sw2 == 0x08) {
+                    return new IccFileTypeMismatch();
+                } else {
+                    return new IccFileNotFound();
+                }
+            default:
+                return new IccException("sw1:" + sw1 + " sw2:" + sw2);
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccRecords.java b/com/android/internal/telephony/uicc/IccRecords.java
new file mode 100644
index 0000000..af26f5c
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccRecords.java
@@ -0,0 +1,834 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionInfo;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * {@hide}
+ */
+public abstract class IccRecords extends Handler implements IccConstants {
+    protected static final boolean DBG = true;
+    protected static final boolean VDBG = false; // STOPSHIP if true
+
+    // ***** Instance Variables
+    protected AtomicBoolean mDestroyed = new AtomicBoolean(false);
+    protected Context mContext;
+    protected CommandsInterface mCi;
+    protected IccFileHandler mFh;
+    protected UiccCardApplication mParentApp;
+    protected TelephonyManager mTelephonyManager;
+
+    protected RegistrantList mRecordsLoadedRegistrants = new RegistrantList();
+    protected RegistrantList mImsiReadyRegistrants = new RegistrantList();
+    protected RegistrantList mRecordsEventsRegistrants = new RegistrantList();
+    protected RegistrantList mNewSmsRegistrants = new RegistrantList();
+    protected RegistrantList mNetworkSelectionModeAutomaticRegistrants = new RegistrantList();
+
+    protected int mRecordsToLoad;  // number of pending load requests
+
+    protected AdnRecordCache mAdnCache;
+
+    // ***** Cached SIM State; cleared on channel close
+
+    protected boolean mRecordsRequested = false; // true if we've made requests for the sim records
+
+    protected String mIccId;  // Includes only decimals (no hex)
+    protected String mFullIccId;  // Includes hex characters in ICCID
+    protected String mMsisdn = null;  // My mobile number
+    protected String mMsisdnTag = null;
+    protected String mNewMsisdn = null;
+    protected String mNewMsisdnTag = null;
+    protected String mVoiceMailNum = null;
+    protected String mVoiceMailTag = null;
+    protected String mNewVoiceMailNum = null;
+    protected String mNewVoiceMailTag = null;
+    protected boolean mIsVoiceMailFixed = false;
+    protected String mImsi;
+    protected String mFakeImsi;
+    private IccIoResult auth_rsp;
+
+    protected int mMncLength = UNINITIALIZED;
+    protected int mMailboxIndex = 0; // 0 is no mailbox dailing number associated
+
+    private String mSpn;
+    private String mFakeSpn;
+
+    protected String mGid1;
+    protected String mFakeGid1;
+    protected String mGid2;
+    protected String mFakeGid2;
+
+    protected String mPrefLang;
+
+    protected PlmnActRecord[] mHplmnActRecords;
+    protected PlmnActRecord[] mOplmnActRecords;
+    protected PlmnActRecord[] mPlmnActRecords;
+
+    protected String[] mEhplmns;
+    protected String[] mFplmns;
+
+    private final Object mLock = new Object();
+
+    CarrierTestOverride mCarrierTestOverride;
+
+    //Arbitrary offset for the Handler
+    protected static final int HANDLER_ACTION_BASE = 0x12E500;
+    protected static final int HANDLER_ACTION_NONE = HANDLER_ACTION_BASE + 0;
+    protected static final int HANDLER_ACTION_SEND_RESPONSE = HANDLER_ACTION_BASE + 1;
+    protected static AtomicInteger sNextRequestId = new AtomicInteger(1);
+    protected final HashMap<Integer, Message> mPendingResponses = new HashMap<>();
+
+    // ***** Constants
+
+    // Markers for mncLength
+    protected static final int UNINITIALIZED = -1;
+    protected static final int UNKNOWN = 0;
+
+    // Bitmasks for SPN display rules.
+    public static final int SPN_RULE_SHOW_SPN  = 0x01;
+    public static final int SPN_RULE_SHOW_PLMN = 0x02;
+
+    // ***** Event Constants
+    public static final int EVENT_MWI = 0; // Message Waiting indication
+    public static final int EVENT_CFI = 1; // Call Forwarding indication
+    public static final int EVENT_SPN = 2; // Service Provider Name
+
+    public static final int EVENT_GET_ICC_RECORD_DONE = 100;
+    protected static final int EVENT_APP_READY = 1;
+    private static final int EVENT_AKA_AUTHENTICATE_DONE          = 90;
+
+    public static final int CALL_FORWARDING_STATUS_DISABLED = 0;
+    public static final int CALL_FORWARDING_STATUS_ENABLED = 1;
+    public static final int CALL_FORWARDING_STATUS_UNKNOWN = -1;
+
+    public static final int DEFAULT_VOICE_MESSAGE_COUNT = -2;
+    public static final int UNKNOWN_VOICE_MESSAGE_COUNT = -1;
+
+    @Override
+    public String toString() {
+        String iccIdToPrint = SubscriptionInfo.givePrintableIccid(mFullIccId);
+        return "mDestroyed=" + mDestroyed
+                + " mContext=" + mContext
+                + " mCi=" + mCi
+                + " mFh=" + mFh
+                + " mParentApp=" + mParentApp
+                + " recordsLoadedRegistrants=" + mRecordsLoadedRegistrants
+                + " mImsiReadyRegistrants=" + mImsiReadyRegistrants
+                + " mRecordsEventsRegistrants=" + mRecordsEventsRegistrants
+                + " mNewSmsRegistrants=" + mNewSmsRegistrants
+                + " mNetworkSelectionModeAutomaticRegistrants="
+                        + mNetworkSelectionModeAutomaticRegistrants
+                + " recordsToLoad=" + mRecordsToLoad
+                + " adnCache=" + mAdnCache
+                + " recordsRequested=" + mRecordsRequested
+                + " iccid=" + iccIdToPrint
+                + " msisdnTag=" + mMsisdnTag
+                + " voiceMailNum=" + Rlog.pii(VDBG, mVoiceMailNum)
+                + " voiceMailTag=" + mVoiceMailTag
+                + " voiceMailNum=" + Rlog.pii(VDBG, mNewVoiceMailNum)
+                + " newVoiceMailTag=" + mNewVoiceMailTag
+                + " isVoiceMailFixed=" + mIsVoiceMailFixed
+                + " mImsi=" + ((mImsi != null) ?
+                mImsi.substring(0, 6) + Rlog.pii(VDBG, mImsi.substring(6)) : "null")
+                + (mCarrierTestOverride.isInTestMode()
+                ? (" mFakeImsi=" + ((mFakeImsi != null) ? mFakeImsi : "null")) : "")
+                + " mncLength=" + mMncLength
+                + " mailboxIndex=" + mMailboxIndex
+                + " spn=" + mSpn
+                + (mCarrierTestOverride.isInTestMode()
+                ? (" mFakeSpn=" + ((mFakeSpn != null) ? mFakeSpn : "null")) : "");
+
+    }
+
+    /**
+     * Generic ICC record loaded callback. Subclasses can call EF load methods on
+     * {@link IccFileHandler} passing a Message for onLoaded with the what field set to
+     * {@link #EVENT_GET_ICC_RECORD_DONE} and the obj field set to an instance
+     * of this interface. The {@link #handleMessage} method in this class will print a
+     * log message using {@link #getEfName()} and decrement {@link #mRecordsToLoad}.
+     *
+     * If the record load was successful, {@link #onRecordLoaded} will be called with the result.
+     * Otherwise, an error log message will be output by {@link #handleMessage} and
+     * {@link #onRecordLoaded} will not be called.
+     */
+    public interface IccRecordLoaded {
+        String getEfName();
+        void onRecordLoaded(AsyncResult ar);
+    }
+
+    // ***** Constructor
+    public IccRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
+        mContext = c;
+        mCi = ci;
+        mFh = app.getIccFileHandler();
+        mParentApp = app;
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(
+                Context.TELEPHONY_SERVICE);
+
+        mCarrierTestOverride = new CarrierTestOverride();
+
+        if (mCarrierTestOverride.isInTestMode()) {
+            mFakeImsi = mCarrierTestOverride.getFakeIMSI();
+            log("load mFakeImsi: " + mFakeImsi);
+
+            mFakeGid1 = mCarrierTestOverride.getFakeGid1();
+            log("load mFakeGid1: " + mFakeGid1);
+
+            mFakeGid2 = mCarrierTestOverride.getFakeGid2();
+            log("load mFakeGid2: " + mFakeGid2);
+
+            mFakeSpn = mCarrierTestOverride.getFakeSpn();
+            log("load mFakeSpn: " + mFakeSpn);
+        }
+    }
+
+    /**
+     * Call when the IccRecords object is no longer going to be used.
+     */
+    public void dispose() {
+        mDestroyed.set(true);
+
+        // It is possible that there is another thread waiting for the response
+        // to requestIccSimAuthentication() in getIccSimChallengeResponse().
+        auth_rsp = null;
+        synchronized (mLock) {
+            mLock.notifyAll();
+        }
+
+        mParentApp = null;
+        mFh = null;
+        mCi = null;
+        mContext = null;
+    }
+
+    public abstract void onReady();
+
+    //***** Public Methods
+    public AdnRecordCache getAdnCache() {
+        return mAdnCache;
+    }
+
+    /**
+     * Adds a message to the pending requests list by generating a unique
+     * (integer) hash key and returning it. The message should never be null.
+     */
+    public int storePendingResponseMessage(Message msg) {
+        int key = sNextRequestId.getAndIncrement();
+        synchronized (mPendingResponses) {
+            mPendingResponses.put(key, msg);
+        }
+        return key;
+    }
+
+    /**
+     * Returns the pending request, if any or null
+     */
+    public Message retrievePendingResponseMessage(Integer key) {
+        Message m;
+        synchronized (mPendingResponses) {
+            return mPendingResponses.remove(key);
+        }
+    }
+
+    /**
+     * Returns the ICC ID stripped at the first hex character. Some SIMs have ICC IDs
+     * containing hex digits; {@link #getFullIccId()} should be used to get the full ID including
+     * hex digits.
+     * @return ICC ID without hex digits
+     */
+    public String getIccId() {
+        return mIccId;
+    }
+
+    /**
+     * Returns the full ICC ID including hex digits.
+     * @return full ICC ID including hex digits
+     */
+    public String getFullIccId() {
+        return mFullIccId;
+    }
+
+    public void registerForRecordsLoaded(Handler h, int what, Object obj) {
+        if (mDestroyed.get()) {
+            return;
+        }
+
+        Registrant r = new Registrant(h, what, obj);
+        mRecordsLoadedRegistrants.add(r);
+
+        if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+            r.notifyRegistrant(new AsyncResult(null, null, null));
+        }
+    }
+    public void unregisterForRecordsLoaded(Handler h) {
+        mRecordsLoadedRegistrants.remove(h);
+    }
+
+    public void registerForImsiReady(Handler h, int what, Object obj) {
+        if (mDestroyed.get()) {
+            return;
+        }
+
+        Registrant r = new Registrant(h, what, obj);
+        mImsiReadyRegistrants.add(r);
+
+        if (getIMSI() != null) {
+            r.notifyRegistrant(new AsyncResult(null, null, null));
+        }
+    }
+    public void unregisterForImsiReady(Handler h) {
+        mImsiReadyRegistrants.remove(h);
+    }
+
+    public void registerForRecordsEvents(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mRecordsEventsRegistrants.add(r);
+
+        /* Notify registrant of all the possible events. This is to make sure registrant is
+        notified even if event occurred in the past. */
+        r.notifyResult(EVENT_MWI);
+        r.notifyResult(EVENT_CFI);
+    }
+    public void unregisterForRecordsEvents(Handler h) {
+        mRecordsEventsRegistrants.remove(h);
+    }
+
+    public void registerForNewSms(Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mNewSmsRegistrants.add(r);
+    }
+    public void unregisterForNewSms(Handler h) {
+        mNewSmsRegistrants.remove(h);
+    }
+
+    public void registerForNetworkSelectionModeAutomatic(
+            Handler h, int what, Object obj) {
+        Registrant r = new Registrant (h, what, obj);
+        mNetworkSelectionModeAutomaticRegistrants.add(r);
+    }
+    public void unregisterForNetworkSelectionModeAutomatic(Handler h) {
+        mNetworkSelectionModeAutomaticRegistrants.remove(h);
+    }
+
+    /**
+     * Get the International Mobile Subscriber ID (IMSI) on a SIM
+     * for GSM, UMTS and like networks. Default is null if IMSI is
+     * not supported or unavailable.
+     *
+     * @return null if SIM is not yet ready or unavailable
+     */
+    public String getIMSI() {
+        if (mCarrierTestOverride.isInTestMode() && mFakeImsi != null) {
+            return mFakeImsi;
+        } else {
+            return mImsi;
+        }
+    }
+
+    /**
+     * Imsi could be set by ServiceStateTrackers in case of cdma
+     * @param imsi
+     */
+    public void setImsi(String imsi) {
+        mImsi = imsi;
+        mImsiReadyRegistrants.notifyRegistrants();
+    }
+
+    /**
+     * Get the Network Access ID (NAI) on a CSIM for CDMA like networks. Default is null if IMSI is
+     * not supported or unavailable.
+     *
+     * @return null if NAI is not yet ready or unavailable
+     */
+    public String getNAI() {
+        return null;
+    }
+
+    public String getMsisdnNumber() {
+        return mMsisdn;
+    }
+
+    /**
+     * Get the Group Identifier Level 1 (GID1) on a SIM for GSM.
+     * @return null if SIM is not yet ready
+     */
+    public String getGid1() {
+        if (mCarrierTestOverride.isInTestMode() && mFakeGid1 != null) {
+            return mFakeGid1;
+        } else {
+            return mGid1;
+        }
+    }
+
+    /**
+     * Get the Group Identifier Level 2 (GID2) on a SIM.
+     * @return null if SIM is not yet ready
+     */
+    public String getGid2() {
+        if (mCarrierTestOverride.isInTestMode() && mFakeGid2 != null) {
+            return mFakeGid2;
+        } else {
+            return mGid2;
+        }
+    }
+
+    public void setMsisdnNumber(String alphaTag, String number,
+            Message onComplete) {
+        loge("setMsisdn() should not be invoked on base IccRecords");
+        // synthesize a "File Not Found" exception and return it
+        AsyncResult.forMessage(onComplete).exception =
+            (new IccIoResult(0x6A, 0x82, (byte[]) null)).getException();
+        onComplete.sendToTarget();
+    }
+
+    public String getMsisdnAlphaTag() {
+        return mMsisdnTag;
+    }
+
+    public String getVoiceMailNumber() {
+        return mVoiceMailNum;
+    }
+
+    /**
+     * Return Service Provider Name stored in SIM (EF_SPN=0x6F46) or in RUIM (EF_RUIM_SPN=0x6F41).
+     *
+     * @return null if SIM is not yet ready or no RUIM entry
+     */
+    public String getServiceProviderName() {
+        if (mCarrierTestOverride.isInTestMode() && mFakeSpn != null) {
+            return mFakeSpn;
+        }
+        String providerName = mSpn;
+
+        // Check for null pointers, mParentApp can be null after dispose,
+        // which did occur after removing a SIM.
+        UiccCardApplication parentApp = mParentApp;
+        if (parentApp != null) {
+            UiccCard card = parentApp.getUiccCard();
+            if (card != null) {
+                String brandOverride = card.getOperatorBrandOverride();
+                if (brandOverride != null) {
+                    log("getServiceProviderName: override, providerName=" + providerName);
+                    providerName = brandOverride;
+                } else {
+                    log("getServiceProviderName: no brandOverride, providerName=" + providerName);
+                }
+            } else {
+                log("getServiceProviderName: card is null, providerName=" + providerName);
+            }
+        } else {
+            log("getServiceProviderName: mParentApp is null, providerName=" + providerName);
+        }
+        return providerName;
+    }
+
+    protected void setServiceProviderName(String spn) {
+        mSpn = spn;
+    }
+
+    /**
+     * Set voice mail number to SIM record
+     *
+     * The voice mail number can be stored either in EF_MBDN (TS 51.011) or
+     * EF_MAILBOX_CPHS (CPHS 4.2)
+     *
+     * If EF_MBDN is available, store the voice mail number to EF_MBDN
+     *
+     * If EF_MAILBOX_CPHS is enabled, store the voice mail number to EF_CHPS
+     *
+     * So the voice mail number will be stored in both EFs if both are available
+     *
+     * Return error only if both EF_MBDN and EF_MAILBOX_CPHS fail.
+     *
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param alphaTag alpha-tagging of the dailing nubmer (upto 10 characters)
+     * @param voiceNumber dailing nubmer (upto 20 digits)
+     *        if the number is start with '+', then set to international TOA
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    public abstract void setVoiceMailNumber(String alphaTag, String voiceNumber,
+            Message onComplete);
+
+    public String getVoiceMailAlphaTag() {
+        return mVoiceMailTag;
+    }
+
+    /**
+     * Sets the SIM voice message waiting indicator records
+     * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported
+     * @param countWaiting The number of messages waiting, if known. Use
+     *                     -1 to indicate that an unknown number of
+     *                      messages are waiting
+     */
+    public abstract void setVoiceMessageWaiting(int line, int countWaiting);
+
+    /**
+     * Called by GsmCdmaPhone to update VoiceMail count
+     */
+    public abstract int getVoiceMessageCount();
+
+    /**
+     * Called by STK Service when REFRESH is received.
+     * @param fileChanged indicates whether any files changed
+     * @param fileList if non-null, a list of EF files that changed
+     */
+    public abstract void onRefresh(boolean fileChanged, int[] fileList);
+
+    /**
+     * Called by subclasses (SimRecords and RuimRecords) whenever
+     * IccRefreshResponse.REFRESH_RESULT_INIT event received
+     */
+    protected void onIccRefreshInit() {
+        mAdnCache.reset();
+        mMncLength = UNINITIALIZED;
+        UiccCardApplication parentApp = mParentApp;
+        if ((parentApp != null) &&
+                (parentApp.getState() == AppState.APPSTATE_READY)) {
+            // This will cause files to be reread
+            sendMessage(obtainMessage(EVENT_APP_READY));
+        }
+    }
+
+    public boolean getRecordsLoaded() {
+        if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    //***** Overridden from Handler
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+            case EVENT_GET_ICC_RECORD_DONE:
+                try {
+                    ar = (AsyncResult) msg.obj;
+                    IccRecordLoaded recordLoaded = (IccRecordLoaded) ar.userObj;
+                    if (DBG) log(recordLoaded.getEfName() + " LOADED");
+
+                    if (ar.exception != null) {
+                        loge("Record Load Exception: " + ar.exception);
+                    } else {
+                        recordLoaded.onRecordLoaded(ar);
+                    }
+                }catch (RuntimeException exc) {
+                    // I don't want these exceptions to be fatal
+                    loge("Exception parsing SIM record: " + exc);
+                } finally {
+                    // Count up record load responses even if they are fails
+                    onRecordLoaded();
+                }
+                break;
+
+            case EVENT_AKA_AUTHENTICATE_DONE:
+                ar = (AsyncResult)msg.obj;
+                auth_rsp = null;
+                if (DBG) log("EVENT_AKA_AUTHENTICATE_DONE");
+                if (ar.exception != null) {
+                    loge("Exception ICC SIM AKA: " + ar.exception);
+                } else {
+                    try {
+                        auth_rsp = (IccIoResult)ar.result;
+                        if (DBG) log("ICC SIM AKA: auth_rsp = " + auth_rsp);
+                    } catch (Exception e) {
+                        loge("Failed to parse ICC SIM AKA contents: " + e);
+                    }
+                }
+                synchronized (mLock) {
+                    mLock.notifyAll();
+                }
+
+                break;
+
+            default:
+                super.handleMessage(msg);
+        }
+    }
+
+    /**
+     * Returns the SIM language derived from the EF-LI and EF-PL sim records.
+     */
+    public String getSimLanguage() {
+        return mPrefLang;
+    }
+
+    protected void setSimLanguage(byte[] efLi, byte[] efPl) {
+        String[] locales = mContext.getAssets().getLocales();
+        try {
+            mPrefLang = findBestLanguage(efLi, locales);
+        } catch (UnsupportedEncodingException uee) {
+            log("Unable to parse EF-LI: " + Arrays.toString(efLi));
+        }
+
+        if (mPrefLang == null) {
+            try {
+                mPrefLang = findBestLanguage(efPl, locales);
+            } catch (UnsupportedEncodingException uee) {
+                log("Unable to parse EF-PL: " + Arrays.toString(efLi));
+            }
+        }
+    }
+
+    protected static String findBestLanguage(byte[] languages, String[] locales)
+            throws UnsupportedEncodingException {
+        if ((languages == null) || (locales == null)) return null;
+
+        // Each 2-bytes consists of one language
+        for (int i = 0; (i + 1) < languages.length; i += 2) {
+            String lang = new String(languages, i, 2, "ISO-8859-1");
+            for (int j = 0; j < locales.length; j++) {
+                if (locales[j] != null && locales[j].length() >= 2 &&
+                        locales[j].substring(0, 2).equalsIgnoreCase(lang)) {
+                    return lang;
+                }
+            }
+        }
+
+        // no match found. return null
+        return null;
+    }
+
+    protected abstract void onRecordLoaded();
+
+    protected abstract void onAllRecordsLoaded();
+
+    /**
+     * Returns the SpnDisplayRule based on settings on the SIM and the
+     * specified plmn (currently-registered PLMN).  See TS 22.101 Annex A
+     * and TS 51.011 10.3.11 for details.
+     *
+     * If the SPN is not found on the SIM, the rule is always PLMN_ONLY.
+     * Generally used for GSM/UMTS and the like SIMs.
+     */
+    public abstract int getDisplayRule(String plmn);
+
+    /**
+     * Return true if "Restriction of menu options for manual PLMN selection"
+     * bit is set or EF_CSP data is unavailable, return false otherwise.
+     * Generally used for GSM/UMTS and the like SIMs.
+     */
+    public boolean isCspPlmnEnabled() {
+        return false;
+    }
+
+    /**
+     * Returns the 5 or 6 digit MCC/MNC of the operator that
+     * provided the SIM card. Returns null of SIM is not yet ready
+     * or is not valid for the type of IccCard. Generally used for
+     * GSM/UMTS and the like SIMS
+     */
+    public String getOperatorNumeric() {
+        return null;
+    }
+
+    /**
+     * Get the current Voice call forwarding flag for GSM/UMTS and the like SIMs
+     *
+     * @return CALL_FORWARDING_STATUS_XXX (DISABLED/ENABLED/UNKNOWN)
+     */
+    public int getVoiceCallForwardingFlag() {
+        return CALL_FORWARDING_STATUS_UNKNOWN;
+    }
+
+    /**
+     * Set the voice call forwarding flag for GSM/UMTS and the like SIMs
+     *
+     * @param line to enable/disable
+     * @param enable
+     * @param number to which CFU is enabled
+     */
+    public void setVoiceCallForwardingFlag(int line, boolean enable, String number) {
+    }
+
+    /**
+     * Indicates wether SIM is in provisioned state or not.
+     * Overridden only if SIM can be dynamically provisioned via OTA.
+     *
+     * @return true if provisioned
+     */
+    public boolean isProvisioned () {
+        return true;
+    }
+
+    /**
+     * Write string to log file
+     *
+     * @param s is the string to write
+     */
+    protected abstract void log(String s);
+
+    /**
+     * Write error string to log file.
+     *
+     * @param s is the string to write
+     */
+    protected abstract void loge(String s);
+
+    /**
+     * Return an interface to retrieve the ISIM records for IMS, if available.
+     * @return the interface to retrieve the ISIM records, or null if not supported
+     */
+    public IsimRecords getIsimRecords() {
+        return null;
+    }
+
+    public UsimServiceTable getUsimServiceTable() {
+        return null;
+    }
+
+    protected void setSystemProperty(String key, String val) {
+        TelephonyManager.getDefault().setTelephonyProperty(mParentApp.getPhoneId(), key, val);
+
+        log("[key, value]=" + key + ", " +  val);
+    }
+
+    /**
+     * Returns the response of the SIM application on the UICC to authentication
+     * challenge/response algorithm. The data string and challenge response are
+     * Base64 encoded Strings.
+     * Can support EAP-SIM, EAP-AKA with results encoded per 3GPP TS 31.102.
+     *
+     * @param authContext parameter P2 that specifies the authentication context per 3GPP TS 31.102 (Section 7.1.2)
+     * @param data authentication challenge data
+     * @return challenge response
+     */
+    public String getIccSimChallengeResponse(int authContext, String data) {
+        if (DBG) log("getIccSimChallengeResponse:");
+
+        try {
+            synchronized(mLock) {
+                CommandsInterface ci = mCi;
+                UiccCardApplication parentApp = mParentApp;
+                if (ci != null && parentApp != null) {
+                    ci.requestIccSimAuthentication(authContext, data,
+                            parentApp.getAid(),
+                            obtainMessage(EVENT_AKA_AUTHENTICATE_DONE));
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException e) {
+                        loge("getIccSimChallengeResponse: Fail, interrupted"
+                                + " while trying to request Icc Sim Auth");
+                        return null;
+                    }
+                } else {
+                    loge( "getIccSimChallengeResponse: "
+                            + "Fail, ci or parentApp is null");
+                    return null;
+                }
+            }
+        } catch(Exception e) {
+            loge( "getIccSimChallengeResponse: "
+                    + "Fail while trying to request Icc Sim Auth");
+            return null;
+        }
+
+        if (auth_rsp == null) {
+            loge("getIccSimChallengeResponse: No authentication response");
+            return null;
+        }
+
+        if (DBG) log("getIccSimChallengeResponse: return auth_rsp");
+
+        return android.util.Base64.encodeToString(auth_rsp.payload, android.util.Base64.NO_WRAP);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("IccRecords: " + this);
+        pw.println(" mDestroyed=" + mDestroyed);
+        pw.println(" mCi=" + mCi);
+        pw.println(" mFh=" + mFh);
+        pw.println(" mParentApp=" + mParentApp);
+        pw.println(" recordsLoadedRegistrants: size=" + mRecordsLoadedRegistrants.size());
+        for (int i = 0; i < mRecordsLoadedRegistrants.size(); i++) {
+            pw.println("  recordsLoadedRegistrants[" + i + "]="
+                    + ((Registrant)mRecordsLoadedRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mImsiReadyRegistrants: size=" + mImsiReadyRegistrants.size());
+        for (int i = 0; i < mImsiReadyRegistrants.size(); i++) {
+            pw.println("  mImsiReadyRegistrants[" + i + "]="
+                    + ((Registrant)mImsiReadyRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mRecordsEventsRegistrants: size=" + mRecordsEventsRegistrants.size());
+        for (int i = 0; i < mRecordsEventsRegistrants.size(); i++) {
+            pw.println("  mRecordsEventsRegistrants[" + i + "]="
+                    + ((Registrant)mRecordsEventsRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mNewSmsRegistrants: size=" + mNewSmsRegistrants.size());
+        for (int i = 0; i < mNewSmsRegistrants.size(); i++) {
+            pw.println("  mNewSmsRegistrants[" + i + "]="
+                    + ((Registrant)mNewSmsRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mNetworkSelectionModeAutomaticRegistrants: size="
+                + mNetworkSelectionModeAutomaticRegistrants.size());
+        for (int i = 0; i < mNetworkSelectionModeAutomaticRegistrants.size(); i++) {
+            pw.println("  mNetworkSelectionModeAutomaticRegistrants[" + i + "]="
+                    + ((Registrant)mNetworkSelectionModeAutomaticRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mRecordsRequested=" + mRecordsRequested);
+        pw.println(" mRecordsToLoad=" + mRecordsToLoad);
+        pw.println(" mRdnCache=" + mAdnCache);
+
+        String iccIdToPrint = SubscriptionInfo.givePrintableIccid(mFullIccId);
+        pw.println(" iccid=" + iccIdToPrint);
+        pw.println(" mMsisdn=" + Rlog.pii(VDBG, mMsisdn));
+        pw.println(" mMsisdnTag=" + mMsisdnTag);
+        pw.println(" mVoiceMailNum=" + Rlog.pii(VDBG, mVoiceMailNum));
+        pw.println(" mVoiceMailTag=" + mVoiceMailTag);
+        pw.println(" mNewVoiceMailNum=" + Rlog.pii(VDBG, mNewVoiceMailNum));
+        pw.println(" mNewVoiceMailTag=" + mNewVoiceMailTag);
+        pw.println(" mIsVoiceMailFixed=" + mIsVoiceMailFixed);
+        pw.println(" mImsi=" + ((mImsi != null) ?
+                mImsi.substring(0, 6) + Rlog.pii(VDBG, mImsi.substring(6)) : "null"));
+        if (mCarrierTestOverride.isInTestMode()) {
+            pw.println(" mFakeImsi=" + ((mFakeImsi != null) ? mFakeImsi : "null"));
+        }
+        pw.println(" mMncLength=" + mMncLength);
+        pw.println(" mMailboxIndex=" + mMailboxIndex);
+        pw.println(" mSpn=" + mSpn);
+        if (mCarrierTestOverride.isInTestMode()) {
+            pw.println(" mFakeSpn=" + ((mFakeSpn != null) ? mFakeSpn : "null"));
+        }
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccRefreshResponse.java b/com/android/internal/telephony/uicc/IccRefreshResponse.java
new file mode 100644
index 0000000..c1d29f8
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccRefreshResponse.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.uicc;
+
+/**
+ * See also RIL_SimRefresh in include/telephony/ril.h
+ *
+ * {@hide}
+ */
+
+public class IccRefreshResponse {
+
+    public static final int REFRESH_RESULT_FILE_UPDATE = 0; /* Single file was updated */
+    public static final int REFRESH_RESULT_INIT = 1;        /* The Icc has been initialized */
+    public static final int REFRESH_RESULT_RESET = 2;       /* The Icc was reset */
+
+    public int             refreshResult;      /* Sim Refresh result */
+    public int             efId;               /* EFID */
+    public String          aid;                /* null terminated string, e.g.,
+                                                  from 0xA0, 0x00 -> 0x41,
+                                                  0x30, 0x30, 0x30 */
+                                               /* Example: a0000000871002f310ffff89080000ff */
+
+    @Override
+    public String toString() {
+        return "{" + refreshResult + ", " + aid +", " + efId + "}";
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccServiceTable.java b/com/android/internal/telephony/uicc/IccServiceTable.java
new file mode 100644
index 0000000..a96d499
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccServiceTable.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.uicc;
+
+import android.telephony.Rlog;
+
+/**
+ * Wrapper class for an ICC EF containing a bit field of enabled services.
+ */
+public abstract class IccServiceTable {
+    protected final byte[] mServiceTable;
+
+    protected IccServiceTable(byte[] table) {
+        mServiceTable = table;
+    }
+
+    // Get the class name to use for log strings
+    protected abstract String getTag();
+
+    // Get the array of enums to use for toString
+    protected abstract Object[] getValues();
+
+    /**
+     * Returns if the specified service is available.
+     * @param service the service number as a zero-based offset (the enum ordinal)
+     * @return true if the service is available; false otherwise
+     */
+    protected boolean isAvailable(int service) {
+        int offset = service / 8;
+        if (offset >= mServiceTable.length) {
+            // Note: Enums are zero-based, but the TS service numbering is one-based
+            Rlog.e(getTag(), "isAvailable for service " + (service + 1) + " fails, max service is " +
+                    (mServiceTable.length * 8));
+            return false;
+        }
+        int bit = service % 8;
+        return (mServiceTable[offset] & (1 << bit)) != 0;
+    }
+
+    @Override
+    public String toString() {
+        Object[] values = getValues();
+        int numBytes = mServiceTable.length;
+        StringBuilder builder = new StringBuilder(getTag()).append('[')
+                .append(numBytes * 8).append("]={ ");
+
+        boolean addComma = false;
+        for (int i = 0; i < numBytes; i++) {
+            byte currentByte = mServiceTable[i];
+            for (int bit = 0; bit < 8; bit++) {
+                if ((currentByte & (1 << bit)) != 0) {
+                    if (addComma) {
+                        builder.append(", ");
+                    } else {
+                        addComma = true;
+                    }
+                    int ordinal = (i * 8) + bit;
+                    if (ordinal < values.length) {
+                        builder.append(values[ordinal]);
+                    } else {
+                        builder.append('#').append(ordinal + 1);    // service number (one-based)
+                    }
+                }
+            }
+        }
+        return builder.append(" }").toString();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccUtils.java b/com/android/internal/telephony/uicc/IccUtils.java
new file mode 100644
index 0000000..62d570c
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccUtils.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.GsmAlphabet;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Various methods, useful for dealing with SIM data.
+ */
+public class IccUtils {
+    static final String LOG_TAG="IccUtils";
+
+    /**
+     * Many fields in GSM SIM's are stored as nibble-swizzled BCD
+     *
+     * Assumes left-justified field that may be padded right with 0xf
+     * values.
+     *
+     * Stops on invalid BCD value, returning string so far
+     */
+    public static String
+    bcdToString(byte[] data, int offset, int length) {
+        StringBuilder ret = new StringBuilder(length*2);
+
+        for (int i = offset ; i < offset + length ; i++) {
+            int v;
+
+            v = data[i] & 0xf;
+            if (v > 9)  break;
+            ret.append((char)('0' + v));
+
+            v = (data[i] >> 4) & 0xf;
+            // Some PLMNs have 'f' as high nibble, ignore it
+            if (v == 0xf) continue;
+            if (v > 9)  break;
+            ret.append((char)('0' + v));
+        }
+
+        return ret.toString();
+    }
+
+    /**
+     * PLMN (MCC/MNC) is encoded as per 24.008 10.5.1.3
+     * Returns a concatenated string of MCC+MNC, stripping
+     * all invalid character 'f'
+     */
+    public static String bcdPlmnToString(byte[] data, int offset) {
+        if (offset + 3 > data.length) {
+            return null;
+        }
+        byte[] trans = new byte[3];
+        trans[0] = (byte) ((data[0 + offset] << 4) | ((data[0 + offset] >> 4) & 0xF));
+        trans[1] = (byte) ((data[1 + offset] << 4) | (data[2 + offset] & 0xF));
+        trans[2] = (byte) ((data[2 + offset] & 0xF0) | ((data[1 + offset] >> 4) & 0xF));
+        String ret = bytesToHexString(trans);
+
+        // For a valid plmn we trim all character 'f'
+        if (ret.contains("f")) {
+            ret = ret.replaceAll("f", "");
+        }
+        return ret;
+    }
+
+    /**
+     * Some fields (like ICC ID) in GSM SIMs are stored as nibble-swizzled BCH
+     */
+    public static String
+    bchToString(byte[] data, int offset, int length) {
+        StringBuilder ret = new StringBuilder(length*2);
+
+        for (int i = offset ; i < offset + length ; i++) {
+            int v;
+
+            v = data[i] & 0xf;
+            ret.append("0123456789abcdef".charAt(v));
+
+            v = (data[i] >> 4) & 0xf;
+            ret.append("0123456789abcdef".charAt(v));
+        }
+
+        return ret.toString();
+    }
+
+    /**
+     * Decode cdma byte into String.
+     */
+    public static String
+    cdmaBcdToString(byte[] data, int offset, int length) {
+        StringBuilder ret = new StringBuilder(length);
+
+        int count = 0;
+        for (int i = offset; count < length; i++) {
+            int v;
+            v = data[i] & 0xf;
+            if (v > 9)  v = 0;
+            ret.append((char)('0' + v));
+
+            if (++count == length) break;
+
+            v = (data[i] >> 4) & 0xf;
+            if (v > 9)  v = 0;
+            ret.append((char)('0' + v));
+            ++count;
+        }
+        return ret.toString();
+    }
+
+    /**
+     * Decodes a GSM-style BCD byte, returning an int ranging from 0-99.
+     *
+     * In GSM land, the least significant BCD digit is stored in the most
+     * significant nibble.
+     *
+     * Out-of-range digits are treated as 0 for the sake of the time stamp,
+     * because of this:
+     *
+     * TS 23.040 section 9.2.3.11
+     * "if the MS receives a non-integer value in the SCTS, it shall
+     * assume the digit is set to 0 but shall store the entire field
+     * exactly as received"
+     */
+    public static int
+    gsmBcdByteToInt(byte b) {
+        int ret = 0;
+
+        // treat out-of-range BCD values as 0
+        if ((b & 0xf0) <= 0x90) {
+            ret = (b >> 4) & 0xf;
+        }
+
+        if ((b & 0x0f) <= 0x09) {
+            ret +=  (b & 0xf) * 10;
+        }
+
+        return ret;
+    }
+
+    /**
+     * Decodes a CDMA style BCD byte like {@link #gsmBcdByteToInt}, but
+     * opposite nibble format. The least significant BCD digit
+     * is in the least significant nibble and the most significant
+     * is in the most significant nibble.
+     */
+    public static int
+    cdmaBcdByteToInt(byte b) {
+        int ret = 0;
+
+        // treat out-of-range BCD values as 0
+        if ((b & 0xf0) <= 0x90) {
+            ret = ((b >> 4) & 0xf) * 10;
+        }
+
+        if ((b & 0x0f) <= 0x09) {
+            ret +=  (b & 0xf);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Decodes a string field that's formatted like the EF[ADN] alpha
+     * identifier
+     *
+     * From TS 51.011 10.5.1:
+     *   Coding:
+     *       this alpha tagging shall use either
+     *      -    the SMS default 7 bit coded alphabet as defined in
+     *          TS 23.038 [12] with bit 8 set to 0. The alpha identifier
+     *          shall be left justified. Unused bytes shall be set to 'FF'; or
+     *      -    one of the UCS2 coded options as defined in annex B.
+     *
+     * Annex B from TS 11.11 V8.13.0:
+     *      1)  If the first octet in the alpha string is '80', then the
+     *          remaining octets are 16 bit UCS2 characters ...
+     *      2)  if the first octet in the alpha string is '81', then the
+     *          second octet contains a value indicating the number of
+     *          characters in the string, and the third octet contains an
+     *          8 bit number which defines bits 15 to 8 of a 16 bit
+     *          base pointer, where bit 16 is set to zero and bits 7 to 1
+     *          are also set to zero.  These sixteen bits constitute a
+     *          base pointer to a "half page" in the UCS2 code space, to be
+     *          used with some or all of the remaining octets in the string.
+     *          The fourth and subsequent octets contain codings as follows:
+     *          If bit 8 of the octet is set to zero, the remaining 7 bits
+     *          of the octet contain a GSM Default Alphabet character,
+     *          whereas if bit 8 of the octet is set to one, then the
+     *          remaining seven bits are an offset value added to the
+     *          16 bit base pointer defined earlier...
+     *      3)  If the first octet of the alpha string is set to '82', then
+     *          the second octet contains a value indicating the number of
+     *          characters in the string, and the third and fourth octets
+     *          contain a 16 bit number which defines the complete 16 bit
+     *          base pointer to a "half page" in the UCS2 code space...
+     */
+    public static String
+    adnStringFieldToString(byte[] data, int offset, int length) {
+        if (length == 0) {
+            return "";
+        }
+        if (length >= 1) {
+            if (data[offset] == (byte) 0x80) {
+                int ucslen = (length - 1) / 2;
+                String ret = null;
+
+                try {
+                    ret = new String(data, offset + 1, ucslen * 2, "utf-16be");
+                } catch (UnsupportedEncodingException ex) {
+                    Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException",
+                          ex);
+                }
+
+                if (ret != null) {
+                    // trim off trailing FFFF characters
+
+                    ucslen = ret.length();
+                    while (ucslen > 0 && ret.charAt(ucslen - 1) == '\uFFFF')
+                        ucslen--;
+
+                    return ret.substring(0, ucslen);
+                }
+            }
+        }
+
+        boolean isucs2 = false;
+        char base = '\0';
+        int len = 0;
+
+        if (length >= 3 && data[offset] == (byte) 0x81) {
+            len = data[offset + 1] & 0xFF;
+            if (len > length - 3)
+                len = length - 3;
+
+            base = (char) ((data[offset + 2] & 0xFF) << 7);
+            offset += 3;
+            isucs2 = true;
+        } else if (length >= 4 && data[offset] == (byte) 0x82) {
+            len = data[offset + 1] & 0xFF;
+            if (len > length - 4)
+                len = length - 4;
+
+            base = (char) (((data[offset + 2] & 0xFF) << 8) |
+                            (data[offset + 3] & 0xFF));
+            offset += 4;
+            isucs2 = true;
+        }
+
+        if (isucs2) {
+            StringBuilder ret = new StringBuilder();
+
+            while (len > 0) {
+                // UCS2 subset case
+
+                if (data[offset] < 0) {
+                    ret.append((char) (base + (data[offset] & 0x7F)));
+                    offset++;
+                    len--;
+                }
+
+                // GSM character set case
+
+                int count = 0;
+                while (count < len && data[offset + count] >= 0)
+                    count++;
+
+                ret.append(GsmAlphabet.gsm8BitUnpackedToString(data,
+                           offset, count));
+
+                offset += count;
+                len -= count;
+            }
+
+            return ret.toString();
+        }
+
+        Resources resource = Resources.getSystem();
+        String defaultCharset = "";
+        try {
+            defaultCharset =
+                    resource.getString(com.android.internal.R.string.gsm_alphabet_default_charset);
+        } catch (NotFoundException e) {
+            // Ignore Exception and defaultCharset is set to a empty string.
+        }
+        return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim());
+    }
+
+    static int
+    hexCharToInt(char c) {
+        if (c >= '0' && c <= '9') return (c - '0');
+        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
+        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+
+        throw new RuntimeException ("invalid hex char '" + c + "'");
+    }
+
+    /**
+     * Converts a hex String to a byte array.
+     *
+     * @param s A string of hexadecimal characters, must be an even number of
+     *          chars long
+     *
+     * @return byte array representation
+     *
+     * @throws RuntimeException on invalid format
+     */
+    public static byte[]
+    hexStringToBytes(String s) {
+        byte[] ret;
+
+        if (s == null) return null;
+
+        int sz = s.length();
+
+        ret = new byte[sz/2];
+
+        for (int i=0 ; i <sz ; i+=2) {
+            ret[i/2] = (byte) ((hexCharToInt(s.charAt(i)) << 4)
+                                | hexCharToInt(s.charAt(i+1)));
+        }
+
+        return ret;
+    }
+
+
+    /**
+     * Converts a byte array into a String of hexadecimal characters.
+     *
+     * @param bytes an array of bytes
+     *
+     * @return hex string representation of bytes array
+     */
+    public static String
+    bytesToHexString(byte[] bytes) {
+        if (bytes == null) return null;
+
+        StringBuilder ret = new StringBuilder(2*bytes.length);
+
+        for (int i = 0 ; i < bytes.length ; i++) {
+            int b;
+
+            b = 0x0f & (bytes[i] >> 4);
+
+            ret.append("0123456789abcdef".charAt(b));
+
+            b = 0x0f & bytes[i];
+
+            ret.append("0123456789abcdef".charAt(b));
+        }
+
+        return ret.toString();
+    }
+
+
+    /**
+     * Convert a TS 24.008 Section 10.5.3.5a Network Name field to a string
+     * "offset" points to "octet 3", the coding scheme byte
+     * empty string returned on decode error
+     */
+    public static String
+    networkNameToString(byte[] data, int offset, int length) {
+        String ret;
+
+        if ((data[offset] & 0x80) != 0x80 || length < 1) {
+            return "";
+        }
+
+        switch ((data[offset] >>> 4) & 0x7) {
+            case 0:
+                // SMS character set
+                int countSeptets;
+                int unusedBits = data[offset] & 7;
+                countSeptets = (((length - 1) * 8) - unusedBits) / 7 ;
+                ret =  GsmAlphabet.gsm7BitPackedToString(data, offset + 1, countSeptets);
+            break;
+            case 1:
+                // UCS2
+                try {
+                    ret = new String(data,
+                            offset + 1, length - 1, "utf-16");
+                } catch (UnsupportedEncodingException ex) {
+                    ret = "";
+                    Rlog.e(LOG_TAG,"implausible UnsupportedEncodingException", ex);
+                }
+            break;
+
+            // unsupported encoding
+            default:
+                ret = "";
+            break;
+        }
+
+        // "Add CI"
+        // "The MS should add the letters for the Country's Initials and
+        //  a separator (e.g. a space) to the text string"
+
+        if ((data[offset] & 0x40) != 0) {
+            // FIXME(mkf) add country initials here
+
+        }
+
+        return ret;
+    }
+
+    /**
+     * Convert a TS 131.102 image instance of code scheme '11' into Bitmap
+     * @param data The raw data
+     * @param length The length of image body
+     * @return The bitmap
+     */
+    public static Bitmap parseToBnW(byte[] data, int length){
+        int valueIndex = 0;
+        int width = data[valueIndex++] & 0xFF;
+        int height = data[valueIndex++] & 0xFF;
+        int numOfPixels = width*height;
+
+        int[] pixels = new int[numOfPixels];
+
+        int pixelIndex = 0;
+        int bitIndex = 7;
+        byte currentByte = 0x00;
+        while (pixelIndex < numOfPixels) {
+            // reassign data and index for every byte (8 bits).
+            if (pixelIndex % 8 == 0) {
+                currentByte = data[valueIndex++];
+                bitIndex = 7;
+            }
+            pixels[pixelIndex++] = bitToRGB((currentByte >> bitIndex-- ) & 0x01);
+        }
+
+        if (pixelIndex != numOfPixels) {
+            Rlog.e(LOG_TAG, "parse end and size error");
+        }
+        return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
+    }
+
+    private static int bitToRGB(int bit){
+        if(bit == 1){
+            return Color.WHITE;
+        } else {
+            return Color.BLACK;
+        }
+    }
+
+    /**
+     * a TS 131.102 image instance of code scheme '11' into color Bitmap
+     *
+     * @param data The raw data
+     * @param length the length of image body
+     * @param transparency with or without transparency
+     * @return The color bitmap
+     */
+    public static Bitmap parseToRGB(byte[] data, int length,
+            boolean transparency) {
+        int valueIndex = 0;
+        int width = data[valueIndex++] & 0xFF;
+        int height = data[valueIndex++] & 0xFF;
+        int bits = data[valueIndex++] & 0xFF;
+        int colorNumber = data[valueIndex++] & 0xFF;
+        int clutOffset = ((data[valueIndex++] & 0xFF) << 8)
+                | (data[valueIndex++] & 0xFF);
+
+        int[] colorIndexArray = getCLUT(data, clutOffset, colorNumber);
+        if (true == transparency) {
+            colorIndexArray[colorNumber - 1] = Color.TRANSPARENT;
+        }
+
+        int[] resultArray = null;
+        if (0 == (8 % bits)) {
+            resultArray = mapTo2OrderBitColor(data, valueIndex,
+                    (width * height), colorIndexArray, bits);
+        } else {
+            resultArray = mapToNon2OrderBitColor(data, valueIndex,
+                    (width * height), colorIndexArray, bits);
+        }
+
+        return Bitmap.createBitmap(resultArray, width, height,
+                Bitmap.Config.RGB_565);
+    }
+
+    private static int[] mapTo2OrderBitColor(byte[] data, int valueIndex,
+            int length, int[] colorArray, int bits) {
+        if (0 != (8 % bits)) {
+            Rlog.e(LOG_TAG, "not event number of color");
+            return mapToNon2OrderBitColor(data, valueIndex, length, colorArray,
+                    bits);
+        }
+
+        int mask = 0x01;
+        switch (bits) {
+        case 1:
+            mask = 0x01;
+            break;
+        case 2:
+            mask = 0x03;
+            break;
+        case 4:
+            mask = 0x0F;
+            break;
+        case 8:
+            mask = 0xFF;
+            break;
+        }
+
+        int[] resultArray = new int[length];
+        int resultIndex = 0;
+        int run = 8 / bits;
+        while (resultIndex < length) {
+            byte tempByte = data[valueIndex++];
+            for (int runIndex = 0; runIndex < run; ++runIndex) {
+                int offset = run - runIndex - 1;
+                resultArray[resultIndex++] = colorArray[(tempByte >> (offset * bits))
+                        & mask];
+            }
+        }
+        return resultArray;
+    }
+
+    private static int[] mapToNon2OrderBitColor(byte[] data, int valueIndex,
+            int length, int[] colorArray, int bits) {
+        if (0 == (8 % bits)) {
+            Rlog.e(LOG_TAG, "not odd number of color");
+            return mapTo2OrderBitColor(data, valueIndex, length, colorArray,
+                    bits);
+        }
+
+        int[] resultArray = new int[length];
+        // TODO fix me:
+        return resultArray;
+    }
+
+    private static int[] getCLUT(byte[] rawData, int offset, int number) {
+        if (null == rawData) {
+            return null;
+        }
+
+        int[] result = new int[number];
+        int endIndex = offset + (number * 3); // 1 color use 3 bytes
+        int valueIndex = offset;
+        int colorIndex = 0;
+        int alpha = 0xff << 24;
+        do {
+            result[colorIndex++] = alpha
+                    | ((rawData[valueIndex++] & 0xFF) << 16)
+                    | ((rawData[valueIndex++] & 0xFF) << 8)
+                    | ((rawData[valueIndex++] & 0xFF));
+        } while (valueIndex < endIndex);
+        return result;
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IccVmFixedException.java b/com/android/internal/telephony/uicc/IccVmFixedException.java
new file mode 100644
index 0000000..f3f3fbb
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccVmFixedException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.uicc;
+
+
+/**
+ * {@hide}
+ */
+public final class IccVmFixedException extends IccException {
+    IccVmFixedException()
+    {
+
+    }
+
+    public IccVmFixedException(String s)
+    {
+        super(s);
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/telephony/uicc/IccVmNotSupportedException.java b/com/android/internal/telephony/uicc/IccVmNotSupportedException.java
new file mode 100644
index 0000000..faa5831
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccVmNotSupportedException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.uicc;
+
+
+/**
+ * {@hide}
+ */
+public final class IccVmNotSupportedException extends IccException {
+    IccVmNotSupportedException()
+    {
+
+    }
+
+    public IccVmNotSupportedException(String s)
+    {
+        super(s);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IsimFileHandler.java b/com/android/internal/telephony/uicc/IsimFileHandler.java
new file mode 100644
index 0000000..6fe16c9
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IsimFileHandler.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2006, 2012 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.internal.telephony.uicc;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+
+/**
+ * {@hide}
+ * This class should be used to access files in ISIM ADF
+ */
+public final class IsimFileHandler extends IccFileHandler implements IccConstants {
+    static final String LOG_TAG = "IsimFH";
+
+    public IsimFileHandler(UiccCardApplication app, String aid, CommandsInterface ci) {
+        super(app, aid, ci);
+    }
+
+    @Override
+    protected String getEFPath(int efid) {
+        switch(efid) {
+        case EF_IMPI:
+        case EF_IMPU:
+        case EF_DOMAIN:
+        case EF_IST:
+        case EF_PCSCF:
+            return MF_SIM + DF_ADF;
+        }
+        String path = getCommonIccEFPath(efid);
+        return path;
+    }
+
+    @Override
+    protected void logd(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    @Override
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/IsimRecords.java b/com/android/internal/telephony/uicc/IsimRecords.java
new file mode 100644
index 0000000..b176d4e
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IsimRecords.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.uicc;
+
+/**
+ * {@hide}
+ */
+public interface IsimRecords {
+
+    /**
+     * Return the IMS private user identity (IMPI).
+     * Returns null if the IMPI hasn't been loaded or isn't present on the ISIM.
+     * @return the IMS private user identity string, or null if not available
+     */
+    String getIsimImpi();
+
+    /**
+     * Return the IMS home network domain name.
+     * Returns null if the IMS domain hasn't been loaded or isn't present on the ISIM.
+     * @return the IMS home network domain name, or null if not available
+     */
+    String getIsimDomain();
+
+    /**
+     * Return an array of IMS public user identities (IMPU).
+     * Returns null if the IMPU hasn't been loaded or isn't present on the ISIM.
+     * @return an array of IMS public user identity strings, or null if not available
+     */
+    String[] getIsimImpu();
+
+    /**
+     * Returns the IMS Service Table (IST) that was loaded from the ISIM.
+     * @return IMS Service Table or null if not present or not loaded
+     */
+    String getIsimIst();
+
+    /**
+     * Returns the IMS Proxy Call Session Control Function(PCSCF) that were loaded from the ISIM.
+     * @return an array of  PCSCF strings with one PCSCF per string, or null if
+     *      not present or not loaded
+     */
+    String[] getIsimPcscf();
+
+    /**
+     * Returns the response of ISIM Authetification through RIL.
+     * Returns null if the Authentification hasn't been successed or isn't present iphonesubinfo.
+     * @return the response of ISIM Authetification, or null if not available
+     */
+    String getIsimChallengeResponse(String nonce);
+}
diff --git a/com/android/internal/telephony/uicc/IsimUiccRecords.java b/com/android/internal/telephony/uicc/IsimUiccRecords.java
new file mode 100644
index 0000000..194d259
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IsimUiccRecords.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.uicc;
+
+import static com.android.internal.telephony.uicc.IccConstants.EF_DOMAIN;
+import static com.android.internal.telephony.uicc.IccConstants.EF_IMPI;
+import static com.android.internal.telephony.uicc.IccConstants.EF_IMPU;
+import static com.android.internal.telephony.uicc.IccConstants.EF_IST;
+import static com.android.internal.telephony.uicc.IccConstants.EF_PCSCF;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.gsm.SimTlv;
+//import com.android.internal.telephony.gsm.VoiceMailConstants;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * {@hide}
+ */
+public class IsimUiccRecords extends IccRecords implements IsimRecords {
+    protected static final String LOG_TAG = "IsimUiccRecords";
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false; // STOPSHIP if true
+    private static final boolean DUMP_RECORDS = false;  // Note: PII is logged when this is true
+                                                        // STOPSHIP if true
+    public static final String INTENT_ISIM_REFRESH = "com.android.intent.isim_refresh";
+
+    private static final int EVENT_APP_READY = 1;
+    private static final int EVENT_ISIM_REFRESH = 31;
+    private static final int EVENT_ISIM_AUTHENTICATE_DONE          = 91;
+
+    // ISIM EF records (see 3GPP TS 31.103)
+    private String mIsimImpi;               // IMS private user identity
+    private String mIsimDomain;             // IMS home network domain name
+    private String[] mIsimImpu;             // IMS public user identity(s)
+    private String mIsimIst;                // IMS Service Table
+    private String[] mIsimPcscf;            // IMS Proxy Call Session Control Function
+    private String auth_rsp;
+
+    private final Object mLock = new Object();
+
+    private static final int TAG_ISIM_VALUE = 0x80;     // From 3GPP TS 31.103
+
+    @Override
+    public String toString() {
+        return "IsimUiccRecords: " + super.toString()
+                + (DUMP_RECORDS ? (" mIsimImpi=" + mIsimImpi
+                + " mIsimDomain=" + mIsimDomain
+                + " mIsimImpu=" + mIsimImpu
+                + " mIsimIst=" + mIsimIst
+                + " mIsimPcscf=" + mIsimPcscf) : "");
+    }
+
+    public IsimUiccRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
+        super(app, c, ci);
+
+        mRecordsRequested = false;  // No load request is made till SIM ready
+
+        // recordsToLoad is set to 0 because no requests are made yet
+        mRecordsToLoad = 0;
+        // Start off by setting empty state
+        resetRecords();
+        mCi.registerForIccRefresh(this, EVENT_ISIM_REFRESH, null);
+
+        mParentApp.registerForReady(this, EVENT_APP_READY, null);
+        if (DBG) log("IsimUiccRecords X ctor this=" + this);
+    }
+
+    @Override
+    public void dispose() {
+        log("Disposing " + this);
+        //Unregister for all events
+        mCi.unregisterForIccRefresh(this);
+        mParentApp.unregisterForReady(this);
+        resetRecords();
+        super.dispose();
+    }
+
+    // ***** Overridden from Handler
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        if (mDestroyed.get()) {
+            Rlog.e(LOG_TAG, "Received message " + msg +
+                    "[" + msg.what + "] while being destroyed. Ignoring.");
+            return;
+        }
+        loge("IsimUiccRecords: handleMessage " + msg + "[" + msg.what + "] ");
+
+        try {
+            switch (msg.what) {
+                case EVENT_APP_READY:
+                    onReady();
+                    break;
+
+                case EVENT_ISIM_REFRESH:
+                    ar = (AsyncResult)msg.obj;
+                    loge("ISim REFRESH(EVENT_ISIM_REFRESH) with exception: " + ar.exception);
+                    if (ar.exception == null) {
+                        Intent intent = new Intent(INTENT_ISIM_REFRESH);
+                        loge("send ISim REFRESH: " + INTENT_ISIM_REFRESH);
+                        mContext.sendBroadcast(intent);
+                        handleIsimRefresh((IccRefreshResponse)ar.result);
+                    }
+                    break;
+
+                case EVENT_ISIM_AUTHENTICATE_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    log("EVENT_ISIM_AUTHENTICATE_DONE");
+                    if (ar.exception != null) {
+                        log("Exception ISIM AKA: " + ar.exception);
+                    } else {
+                        try {
+                            auth_rsp = (String)ar.result;
+                            log("ISIM AKA: auth_rsp = " + auth_rsp);
+                        } catch (Exception e) {
+                            log("Failed to parse ISIM AKA contents: " + e);
+                        }
+                    }
+                    synchronized (mLock) {
+                        mLock.notifyAll();
+                    }
+
+                    break;
+
+                default:
+                    super.handleMessage(msg);   // IccRecords handles generic record load responses
+
+            }
+        } catch (RuntimeException exc) {
+            // I don't want these exceptions to be fatal
+            Rlog.w(LOG_TAG, "Exception parsing SIM record", exc);
+        }
+    }
+
+    protected void fetchIsimRecords() {
+        mRecordsRequested = true;
+
+        mFh.loadEFTransparent(EF_IMPI, obtainMessage(
+                IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimImpiLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFLinearFixedAll(EF_IMPU, obtainMessage(
+                IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimImpuLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_DOMAIN, obtainMessage(
+                IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimDomainLoaded()));
+        mRecordsToLoad++;
+        mFh.loadEFTransparent(EF_IST, obtainMessage(
+                    IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimIstLoaded()));
+        mRecordsToLoad++;
+        mFh.loadEFLinearFixedAll(EF_PCSCF, obtainMessage(
+                    IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimPcscfLoaded()));
+        mRecordsToLoad++;
+
+        if (DBG) log("fetchIsimRecords " + mRecordsToLoad + " requested: " + mRecordsRequested);
+    }
+
+    protected void resetRecords() {
+        // recordsRequested is set to false indicating that the SIM
+        // read requests made so far are not valid. This is set to
+        // true only when fresh set of read requests are made.
+        mIsimImpi = null;
+        mIsimDomain = null;
+        mIsimImpu = null;
+        mIsimIst = null;
+        mIsimPcscf = null;
+        auth_rsp = null;
+
+        mRecordsRequested = false;
+    }
+
+    private class EfIsimImpiLoaded implements IccRecords.IccRecordLoaded {
+        public String getEfName() {
+            return "EF_ISIM_IMPI";
+        }
+        public void onRecordLoaded(AsyncResult ar) {
+            byte[] data = (byte[]) ar.result;
+            mIsimImpi = isimTlvToString(data);
+            if (DUMP_RECORDS) log("EF_IMPI=" + mIsimImpi);
+        }
+    }
+
+    private class EfIsimImpuLoaded implements IccRecords.IccRecordLoaded {
+        public String getEfName() {
+            return "EF_ISIM_IMPU";
+        }
+        public void onRecordLoaded(AsyncResult ar) {
+            ArrayList<byte[]> impuList = (ArrayList<byte[]>) ar.result;
+            if (DBG) log("EF_IMPU record count: " + impuList.size());
+            mIsimImpu = new String[impuList.size()];
+            int i = 0;
+            for (byte[] identity : impuList) {
+                String impu = isimTlvToString(identity);
+                if (DUMP_RECORDS) log("EF_IMPU[" + i + "]=" + impu);
+                mIsimImpu[i++] = impu;
+            }
+        }
+    }
+
+    private class EfIsimDomainLoaded implements IccRecords.IccRecordLoaded {
+        public String getEfName() {
+            return "EF_ISIM_DOMAIN";
+        }
+        public void onRecordLoaded(AsyncResult ar) {
+            byte[] data = (byte[]) ar.result;
+            mIsimDomain = isimTlvToString(data);
+            if (DUMP_RECORDS) log("EF_DOMAIN=" + mIsimDomain);
+        }
+    }
+
+    private class EfIsimIstLoaded implements IccRecords.IccRecordLoaded {
+        public String getEfName() {
+            return "EF_ISIM_IST";
+        }
+        public void onRecordLoaded(AsyncResult ar) {
+            byte[] data = (byte[]) ar.result;
+            mIsimIst = IccUtils.bytesToHexString(data);
+            if (DUMP_RECORDS) log("EF_IST=" + mIsimIst);
+        }
+    }
+    private class EfIsimPcscfLoaded implements IccRecords.IccRecordLoaded {
+        public String getEfName() {
+            return "EF_ISIM_PCSCF";
+        }
+        public void onRecordLoaded(AsyncResult ar) {
+            ArrayList<byte[]> pcscflist = (ArrayList<byte[]>) ar.result;
+            if (DBG) log("EF_PCSCF record count: " + pcscflist.size());
+            mIsimPcscf = new String[pcscflist.size()];
+            int i = 0;
+            for (byte[] identity : pcscflist) {
+                String pcscf = isimTlvToString(identity);
+                if (DUMP_RECORDS) log("EF_PCSCF[" + i + "]=" + pcscf);
+                mIsimPcscf[i++] = pcscf;
+            }
+        }
+    }
+
+    /**
+     * ISIM records for IMS are stored inside a Tag-Length-Value record as a UTF-8 string
+     * with tag value 0x80.
+     * @param record the byte array containing the IMS data string
+     * @return the decoded String value, or null if the record can't be decoded
+     */
+    private static String isimTlvToString(byte[] record) {
+        SimTlv tlv = new SimTlv(record, 0, record.length);
+        do {
+            if (tlv.getTag() == TAG_ISIM_VALUE) {
+                return new String(tlv.getData(), Charset.forName("UTF-8"));
+            }
+        } while (tlv.nextObject());
+
+        if (VDBG) {
+            Rlog.d(LOG_TAG, "[ISIM] can't find TLV. record = " + IccUtils.bytesToHexString(record));
+        }
+        return null;
+    }
+
+    @Override
+    protected void onRecordLoaded() {
+        // One record loaded successfully or failed, In either case
+        // we need to update the recordsToLoad count
+        mRecordsToLoad -= 1;
+        if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
+
+        if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+            onAllRecordsLoaded();
+        } else if (mRecordsToLoad < 0) {
+            loge("recordsToLoad <0, programmer error suspected");
+            mRecordsToLoad = 0;
+        }
+    }
+
+    @Override
+    protected void onAllRecordsLoaded() {
+       if (DBG) log("record load complete");
+        mRecordsLoadedRegistrants.notifyRegistrants(
+                new AsyncResult(null, null, null));
+    }
+
+    private void handleFileUpdate(int efid) {
+        switch (efid) {
+            case EF_IMPI:
+                mFh.loadEFTransparent(EF_IMPI, obtainMessage(
+                            IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimImpiLoaded()));
+                mRecordsToLoad++;
+                break;
+
+            case EF_IMPU:
+                mFh.loadEFLinearFixedAll(EF_IMPU, obtainMessage(
+                            IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimImpuLoaded()));
+                mRecordsToLoad++;
+            break;
+
+            case EF_DOMAIN:
+                mFh.loadEFTransparent(EF_DOMAIN, obtainMessage(
+                            IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimDomainLoaded()));
+                mRecordsToLoad++;
+            break;
+
+            case EF_IST:
+                mFh.loadEFTransparent(EF_IST, obtainMessage(
+                            IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimIstLoaded()));
+                mRecordsToLoad++;
+            break;
+
+            case EF_PCSCF:
+                mFh.loadEFLinearFixedAll(EF_PCSCF, obtainMessage(
+                            IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimPcscfLoaded()));
+                mRecordsToLoad++;
+
+            default:
+                fetchIsimRecords();
+                break;
+        }
+    }
+
+    private void handleIsimRefresh(IccRefreshResponse refreshResponse) {
+        if (refreshResponse == null) {
+            if (DBG) log("handleIsimRefresh received without input");
+            return;
+        }
+
+        if (!TextUtils.isEmpty(refreshResponse.aid)
+                && !refreshResponse.aid.equals(mParentApp.getAid())) {
+            // This is for different app. Ignore.
+            if (DBG) log("handleIsimRefresh received different app");
+            return;
+        }
+
+        switch (refreshResponse.refreshResult) {
+            case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
+                if (DBG) log("handleIsimRefresh with REFRESH_RESULT_FILE_UPDATE");
+                handleFileUpdate(refreshResponse.efId);
+                break;
+
+            case IccRefreshResponse.REFRESH_RESULT_INIT:
+                if (DBG) log("handleIsimRefresh with REFRESH_RESULT_INIT");
+                // need to reload all files (that we care about)
+                // onIccRefreshInit();
+                fetchIsimRecords();
+                break;
+
+            case IccRefreshResponse.REFRESH_RESULT_RESET:
+                // Refresh reset is handled by the UiccCard object.
+                if (DBG) log("handleIsimRefresh with REFRESH_RESULT_RESET");
+                break;
+
+            default:
+                // unknown refresh operation
+                if (DBG) log("handleIsimRefresh with unknown operation");
+                break;
+        }
+    }
+
+    /**
+     * Return the IMS private user identity (IMPI).
+     * Returns null if the IMPI hasn't been loaded or isn't present on the ISIM.
+     * @return the IMS private user identity string, or null if not available
+     */
+    @Override
+    public String getIsimImpi() {
+        return mIsimImpi;
+    }
+
+    /**
+     * Return the IMS home network domain name.
+     * Returns null if the IMS domain hasn't been loaded or isn't present on the ISIM.
+     * @return the IMS home network domain name, or null if not available
+     */
+    @Override
+    public String getIsimDomain() {
+        return mIsimDomain;
+    }
+
+    /**
+     * Return an array of IMS public user identities (IMPU).
+     * Returns null if the IMPU hasn't been loaded or isn't present on the ISIM.
+     * @return an array of IMS public user identity strings, or null if not available
+     */
+    @Override
+    public String[] getIsimImpu() {
+        return (mIsimImpu != null) ? mIsimImpu.clone() : null;
+    }
+
+    /**
+     * Returns the IMS Service Table (IST) that was loaded from the ISIM.
+     * @return IMS Service Table or null if not present or not loaded
+     */
+    @Override
+    public String getIsimIst() {
+        return mIsimIst;
+    }
+
+    /**
+     * Returns the IMS Proxy Call Session Control Function(PCSCF) that were loaded from the ISIM.
+     * @return an array of  PCSCF strings with one PCSCF per string, or null if
+     *      not present or not loaded
+     */
+    @Override
+    public String[] getIsimPcscf() {
+        return (mIsimPcscf != null) ? mIsimPcscf.clone() : null;
+    }
+
+    /**
+     * Returns the response of ISIM Authetification through RIL.
+     * Returns null if the Authentification hasn't been successed or isn't present iphonesubinfo.
+     * @return the response of ISIM Authetification, or null if not available
+     */
+    @Override
+    public String getIsimChallengeResponse(String nonce){
+        if (DBG) log("getIsimChallengeResponse-nonce:"+nonce);
+        try {
+            synchronized(mLock) {
+                mCi.requestIsimAuthentication(nonce,obtainMessage(EVENT_ISIM_AUTHENTICATE_DONE));
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    log("interrupted while trying to request Isim Auth");
+                }
+            }
+        } catch(Exception e) {
+            if (DBG) log( "Fail while trying to request Isim Auth");
+            return null;
+        }
+
+        if (DBG) log("getIsimChallengeResponse-auth_rsp"+auth_rsp);
+
+        return auth_rsp;
+    }
+
+    @Override
+    public int getDisplayRule(String plmn) {
+        // Not applicable to Isim
+        return 0;
+    }
+
+    @Override
+    public void onReady() {
+        fetchIsimRecords();
+    }
+
+    @Override
+    public void onRefresh(boolean fileChanged, int[] fileList) {
+        if (fileChanged) {
+            // A future optimization would be to inspect fileList and
+            // only reload those files that we care about.  For now,
+            // just re-fetch all SIM records that we cache.
+            fetchIsimRecords();
+        }
+    }
+
+    @Override
+    public void setVoiceMailNumber(String alphaTag, String voiceNumber,
+            Message onComplete) {
+        // Not applicable to Isim
+    }
+
+    @Override
+    public void setVoiceMessageWaiting(int line, int countWaiting) {
+        // Not applicable to Isim
+    }
+
+    @Override
+    protected void log(String s) {
+        if (DBG) Rlog.d(LOG_TAG, "[ISIM] " + s);
+    }
+
+    @Override
+    protected void loge(String s) {
+        if (DBG) Rlog.e(LOG_TAG, "[ISIM] " + s);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("IsimRecords: " + this);
+        pw.println(" extends:");
+        super.dump(fd, pw, args);
+        if (DUMP_RECORDS) {
+            pw.println(" mIsimImpi=" + mIsimImpi);
+            pw.println(" mIsimDomain=" + mIsimDomain);
+            pw.println(" mIsimImpu[]=" + Arrays.toString(mIsimImpu));
+            pw.println(" mIsimIst" + mIsimIst);
+            pw.println(" mIsimPcscf" + mIsimPcscf);
+        }
+        pw.flush();
+    }
+
+    @Override
+    public int getVoiceMessageCount() {
+        return 0; // Not applicable to Isim
+    }
+
+}
diff --git a/com/android/internal/telephony/uicc/PlmnActRecord.java b/com/android/internal/telephony/uicc/PlmnActRecord.java
new file mode 100644
index 0000000..2218280
--- /dev/null
+++ b/com/android/internal/telephony/uicc/PlmnActRecord.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.Rlog;
+
+import java.util.Arrays;
+
+/**
+ * {@hide}
+ */
+public class PlmnActRecord implements Parcelable {
+    private static final String LOG_TAG = "PlmnActRecord";
+
+    // Values specified in 3GPP 31.102 sec. 4.2.5
+    public static final int ACCESS_TECH_UTRAN = 0x8000;
+    public static final int ACCESS_TECH_EUTRAN = 0x4000;
+    public static final int ACCESS_TECH_GSM = 0x0080;
+    public static final int ACCESS_TECH_GSM_COMPACT = 0x0040;
+    public static final int ACCESS_TECH_CDMA2000_HRPD = 0x0020;
+    public static final int ACCESS_TECH_CDMA2000_1XRTT = 0x0010;
+    public static final int ACCESS_TECH_RESERVED = 0x3F0F;
+
+    public static final int ENCODED_LENGTH = 5;
+
+    public final String plmn;
+    public final int accessTechs;
+
+    private static final boolean VDBG = false;
+
+    public static final Parcelable.Creator<PlmnActRecord> CREATOR =
+            new Parcelable.Creator<PlmnActRecord>() {
+        @Override
+        public PlmnActRecord createFromParcel(Parcel source) {
+            return new PlmnActRecord(source.readString(), source.readInt());
+        }
+
+        @Override
+        public PlmnActRecord[] newArray(int size) {
+            return new PlmnActRecord[size];
+        }
+    };
+
+    /* From 3gpp 31.102 section 4.2.5
+     * Bytes 0-2 bcd-encoded PLMN-ID
+     * Bytes 3-4 bitfield of access technologies
+     */
+    public PlmnActRecord(byte[] bytes, int offset) {
+        if (VDBG) Rlog.v(LOG_TAG, "Creating PlmnActRecord " + offset);
+        this.plmn = IccUtils.bcdPlmnToString(bytes, offset);
+        this.accessTechs = ((int) bytes[offset + 3] << 8) | bytes[offset + 4];
+    }
+
+    private PlmnActRecord(String plmn, int accessTechs) {
+        this.plmn = plmn;
+        this.accessTechs = accessTechs;
+    }
+
+    private String accessTechString() {
+        if (accessTechs == 0) {
+            return "NONE";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        if ((accessTechs & ACCESS_TECH_UTRAN) != 0) {
+            sb.append("UTRAN|");
+        }
+        if ((accessTechs & ACCESS_TECH_EUTRAN) != 0) {
+            sb.append("EUTRAN|");
+        }
+        if ((accessTechs & ACCESS_TECH_GSM) != 0) {
+            sb.append("GSM|");
+        }
+        if ((accessTechs & ACCESS_TECH_GSM_COMPACT) != 0) {
+            sb.append("GSM_COMPACT|");
+        }
+        if ((accessTechs & ACCESS_TECH_CDMA2000_HRPD) != 0) {
+            sb.append("CDMA2000_HRPD|");
+        }
+        if ((accessTechs & ACCESS_TECH_CDMA2000_1XRTT) != 0) {
+            sb.append("CDMA2000_1XRTT|");
+        }
+        if ((accessTechs & ACCESS_TECH_RESERVED) != 0) {
+            sb.append(String.format("UNKNOWN:%x|", accessTechs & ACCESS_TECH_RESERVED));
+        }
+        // Trim the tailing pipe character
+        return sb.substring(0, sb.length() - 1);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{PLMN=%s,AccessTechs=%s}", plmn, accessTechString());
+    }
+
+    /**
+     * Convenience method for extracting all records from encoded bytes
+     */
+    public static PlmnActRecord[] getRecords(byte[] recordBytes) {
+        if (recordBytes == null || recordBytes.length == 0
+                || recordBytes.length % ENCODED_LENGTH != 0) {
+            Rlog.e(LOG_TAG, "Malformed PlmnActRecord, bytes: "
+                    + ((recordBytes != null) ? Arrays.toString(recordBytes) : null));
+            return null;
+        }
+        int numRecords = recordBytes.length / ENCODED_LENGTH;
+        if (VDBG) Rlog.v(LOG_TAG, "Extracting Logs, count=" + numRecords);
+
+        PlmnActRecord[] records = new PlmnActRecord[numRecords];
+
+        for(int i = 0; i < numRecords; i++) {
+            records[i] = new PlmnActRecord(recordBytes, i * ENCODED_LENGTH);
+        }
+        return records;
+    }
+
+    // Parcelable Implementation
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(plmn);
+        dest.writeInt(accessTechs);
+    }
+
+}
diff --git a/com/android/internal/telephony/uicc/RuimFileHandler.java b/com/android/internal/telephony/uicc/RuimFileHandler.java
new file mode 100644
index 0000000..58e939f
--- /dev/null
+++ b/com/android/internal/telephony/uicc/RuimFileHandler.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.uicc;
+
+import android.os.*;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+
+/**
+ * {@hide}
+ */
+public final class RuimFileHandler extends IccFileHandler {
+    static final String LOG_TAG = "RuimFH";
+
+    //***** Instance Variables
+
+    //***** Constructor
+    public RuimFileHandler(UiccCardApplication app, String aid, CommandsInterface ci) {
+        super(app, aid, ci);
+    }
+
+    //***** Overridden from IccFileHandler
+
+    @Override
+    public void loadEFImgTransparent(int fileid, int highOffset, int lowOffset,
+            int length, Message onLoaded) {
+        Message response = obtainMessage(EVENT_READ_ICON_DONE, fileid, 0,
+                onLoaded);
+
+        /* Per TS 31.102, for displaying of Icon, under
+         * DF Telecom and DF Graphics , EF instance(s) (4FXX,transparent files)
+         * are present. The possible image file identifiers (EF instance) for
+         * EF img ( 4F20, linear fixed file) are : 4F01 ... 4F05.
+         * It should be MF_SIM + DF_TELECOM + DF_GRAPHICS, same path as EF IMG
+         */
+        mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, getEFPath(EF_IMG), 0, 0,
+                GET_RESPONSE_EF_IMG_SIZE_BYTES, null, null,
+                mAid, response);
+    }
+
+    @Override
+    protected String getEFPath(int efid) {
+        switch(efid) {
+        case EF_SMS:
+        case EF_CST:
+        case EF_RUIM_SPN:
+        case EF_CSIM_LI:
+        case EF_CSIM_MDN:
+        case EF_CSIM_IMSIM:
+        case EF_CSIM_CDMAHOME:
+        case EF_CSIM_EPRL:
+        case EF_CSIM_MIPUPP:
+            return MF_SIM + DF_CDMA;
+        }
+        return getCommonIccEFPath(efid);
+    }
+
+    @Override
+    protected void logd(String msg) {
+        Rlog.d(LOG_TAG, "[RuimFileHandler] " + msg);
+    }
+
+    @Override
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, "[RuimFileHandler] " + msg);
+    }
+
+}
diff --git a/com/android/internal/telephony/uicc/RuimRecords.java b/com/android/internal/telephony/uicc/RuimRecords.java
new file mode 100644
index 0000000..b303ca8
--- /dev/null
+++ b/com/android/internal/telephony/uicc/RuimRecords.java
@@ -0,0 +1,995 @@
+/*
+ * Copyright (C) 2008 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.internal.telephony.uicc;
+
+import static com.android.internal.telephony.TelephonyProperties.PROPERTY_TEST_CSIM;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.MccTable;
+import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.cdma.sms.UserData;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+import com.android.internal.util.BitwiseInputStream;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * {@hide}
+ */
+public class RuimRecords extends IccRecords {
+    static final String LOG_TAG = "RuimRecords";
+
+    private boolean  mOtaCommited=false;
+
+    // ***** Instance Variables
+
+    private String mMyMobileNumber;
+    private String mMin2Min1;
+
+    private String mPrlVersion;
+    // From CSIM application
+    private byte[] mEFpl = null;
+    private byte[] mEFli = null;
+    boolean mCsimSpnDisplayCondition = false;
+    private String mMdn;
+    private String mMin;
+    private String mHomeSystemId;
+    private String mHomeNetworkId;
+    private String mNai;
+
+    @Override
+    public String toString() {
+        return "RuimRecords: " + super.toString()
+                + " m_ota_commited" + mOtaCommited
+                + " mMyMobileNumber=" + "xxxx"
+                + " mMin2Min1=" + mMin2Min1
+                + " mPrlVersion=" + mPrlVersion
+                + " mEFpl=" + mEFpl
+                + " mEFli=" + mEFli
+                + " mCsimSpnDisplayCondition=" + mCsimSpnDisplayCondition
+                + " mMdn=" + mMdn
+                + " mMin=" + mMin
+                + " mHomeSystemId=" + mHomeSystemId
+                + " mHomeNetworkId=" + mHomeNetworkId;
+    }
+
+    // ***** Event Constants
+    private static final int EVENT_GET_IMSI_DONE = 3;
+    private static final int EVENT_GET_DEVICE_IDENTITY_DONE = 4;
+    private static final int EVENT_GET_ICCID_DONE = 5;
+    private static final int EVENT_GET_CDMA_SUBSCRIPTION_DONE = 10;
+    private static final int EVENT_UPDATE_DONE = 14;
+    private static final int EVENT_GET_SST_DONE = 17;
+    private static final int EVENT_GET_ALL_SMS_DONE = 18;
+    private static final int EVENT_MARK_SMS_READ_DONE = 19;
+
+    private static final int EVENT_SMS_ON_RUIM = 21;
+    private static final int EVENT_GET_SMS_DONE = 22;
+
+    private static final int EVENT_RUIM_REFRESH = 31;
+
+    public RuimRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
+        super(app, c, ci);
+
+        mAdnCache = new AdnRecordCache(mFh);
+
+        mRecordsRequested = false;  // No load request is made till SIM ready
+
+        // recordsToLoad is set to 0 because no requests are made yet
+        mRecordsToLoad = 0;
+
+        // NOTE the EVENT_SMS_ON_RUIM is not registered
+        mCi.registerForIccRefresh(this, EVENT_RUIM_REFRESH, null);
+
+        // Start off by setting empty state
+        resetRecords();
+
+        mParentApp.registerForReady(this, EVENT_APP_READY, null);
+        if (DBG) log("RuimRecords X ctor this=" + this);
+    }
+
+    @Override
+    public void dispose() {
+        if (DBG) log("Disposing RuimRecords " + this);
+        //Unregister for all events
+        mCi.unregisterForIccRefresh(this);
+        mParentApp.unregisterForReady(this);
+        resetRecords();
+        super.dispose();
+    }
+
+    @Override
+    protected void finalize() {
+        if(DBG) log("RuimRecords finalized");
+    }
+
+    protected void resetRecords() {
+        mMncLength = UNINITIALIZED;
+        log("setting0 mMncLength" + mMncLength);
+        mIccId = null;
+        mFullIccId = null;
+
+        mAdnCache.reset();
+
+        // Don't clean up PROPERTY_ICC_OPERATOR_ISO_COUNTRY and
+        // PROPERTY_ICC_OPERATOR_NUMERIC here. Since not all CDMA
+        // devices have RUIM, these properties should keep the original
+        // values, e.g. build time settings, when there is no RUIM but
+        // set new values when RUIM is available and loaded.
+
+        // recordsRequested is set to false indicating that the SIM
+        // read requests made so far are not valid. This is set to
+        // true only when fresh set of read requests are made.
+        mRecordsRequested = false;
+    }
+
+    public String getMdnNumber() {
+        return mMyMobileNumber;
+    }
+
+    public String getCdmaMin() {
+         return mMin2Min1;
+    }
+
+    /** Returns null if RUIM is not yet ready */
+    public String getPrlVersion() {
+        return mPrlVersion;
+    }
+
+    @Override
+    /** Returns null if RUIM is not yet ready */
+    public String getNAI() {
+        return mNai;
+    }
+
+    @Override
+    public void setVoiceMailNumber(String alphaTag, String voiceNumber, Message onComplete){
+        // In CDMA this is Operator/OEM dependent
+        AsyncResult.forMessage((onComplete)).exception =
+                new IccException("setVoiceMailNumber not implemented");
+        onComplete.sendToTarget();
+        loge("method setVoiceMailNumber is not implemented");
+    }
+
+    /**
+     * Called by CCAT Service when REFRESH is received.
+     * @param fileChanged indicates whether any files changed
+     * @param fileList if non-null, a list of EF files that changed
+     */
+    @Override
+    public void onRefresh(boolean fileChanged, int[] fileList) {
+        if (fileChanged) {
+            // A future optimization would be to inspect fileList and
+            // only reload those files that we care about.  For now,
+            // just re-fetch all RUIM records that we cache.
+            fetchRuimRecords();
+        }
+    }
+
+    private int adjstMinDigits (int digits) {
+        // Per C.S0005 section 2.3.1.
+        digits += 111;
+        digits = (digits % 10 == 0)?(digits - 10):digits;
+        digits = ((digits / 10) % 10 == 0)?(digits - 100):digits;
+        digits = ((digits / 100) % 10 == 0)?(digits - 1000):digits;
+        return digits;
+    }
+
+    /**
+     * Returns the 5 or 6 digit MCC/MNC of the operator that
+     *  provided the RUIM card. Returns null of RUIM is not yet ready
+     */
+    public String getRUIMOperatorNumeric() {
+        String imsi = getIMSI();
+
+        if (imsi == null) {
+            return null;
+        }
+
+        if (mMncLength != UNINITIALIZED && mMncLength != UNKNOWN) {
+            // Length = length of MCC + length of MNC
+            // length of mcc = 3 (3GPP2 C.S0005 - Section 2.3)
+            return imsi.substring(0, 3 + mMncLength);
+        }
+
+        // Guess the MNC length based on the MCC if we don't
+        // have a valid value in ef[ad]
+
+        int mcc = Integer.parseInt(imsi.substring(0, 3));
+        return imsi.substring(0, 3 + MccTable.smallestDigitsMccForMnc(mcc));
+    }
+
+    // Refer to ETSI TS 102.221
+    private class EfPlLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_PL";
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            mEFpl = (byte[]) ar.result;
+            if (DBG) log("EF_PL=" + IccUtils.bytesToHexString(mEFpl));
+        }
+    }
+
+    // Refer to C.S0065 5.2.26
+    private class EfCsimLiLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_LI";
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            mEFli = (byte[]) ar.result;
+            // convert csim efli data to iso 639 format
+            for (int i = 0; i < mEFli.length; i+=2) {
+                switch(mEFli[i+1]) {
+                case 0x01: mEFli[i] = 'e'; mEFli[i+1] = 'n';break;
+                case 0x02: mEFli[i] = 'f'; mEFli[i+1] = 'r';break;
+                case 0x03: mEFli[i] = 'e'; mEFli[i+1] = 's';break;
+                case 0x04: mEFli[i] = 'j'; mEFli[i+1] = 'a';break;
+                case 0x05: mEFli[i] = 'k'; mEFli[i+1] = 'o';break;
+                case 0x06: mEFli[i] = 'z'; mEFli[i+1] = 'h';break;
+                case 0x07: mEFli[i] = 'h'; mEFli[i+1] = 'e';break;
+                default: mEFli[i] = ' '; mEFli[i+1] = ' ';
+                }
+            }
+
+            if (DBG) log("EF_LI=" + IccUtils.bytesToHexString(mEFli));
+        }
+    }
+
+    // Refer to C.S0065 5.2.32
+    private class EfCsimSpnLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_SPN";
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            byte[] data = (byte[]) ar.result;
+            if (DBG) log("CSIM_SPN=" +
+                         IccUtils.bytesToHexString(data));
+
+            // C.S0065 for EF_SPN decoding
+            mCsimSpnDisplayCondition = ((0x01 & data[0]) != 0);
+
+            int encoding = data[1];
+            int language = data[2];
+            byte[] spnData = new byte[32];
+            int len = ((data.length - 3) < 32) ? (data.length - 3) : 32;
+            System.arraycopy(data, 3, spnData, 0, len);
+
+            int numBytes;
+            for (numBytes = 0; numBytes < spnData.length; numBytes++) {
+                if ((spnData[numBytes] & 0xFF) == 0xFF) break;
+            }
+
+            if (numBytes == 0) {
+                setServiceProviderName("");
+                return;
+            }
+            try {
+                switch (encoding) {
+                case UserData.ENCODING_OCTET:
+                case UserData.ENCODING_LATIN:
+                    setServiceProviderName(new String(spnData, 0, numBytes, "ISO-8859-1"));
+                    break;
+                case UserData.ENCODING_IA5:
+                case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                    setServiceProviderName(
+                            GsmAlphabet.gsm7BitPackedToString(spnData, 0, (numBytes*8)/7));
+                    break;
+                case UserData.ENCODING_7BIT_ASCII:
+                    String spn = new String(spnData, 0, numBytes, "US-ASCII");
+                    // To address issues with incorrect encoding scheme
+                    // programmed in some commercial CSIM cards, the decoded
+                    // SPN is checked to have characters in printable ASCII
+                    // range. If not, they are decoded with
+                    // ENCODING_GSM_7BIT_ALPHABET scheme.
+                    if (TextUtils.isPrintableAsciiOnly(spn)) {
+                        setServiceProviderName(spn);
+                    } else {
+                        if (DBG) log("Some corruption in SPN decoding = " + spn);
+                        if (DBG) log("Using ENCODING_GSM_7BIT_ALPHABET scheme...");
+                        setServiceProviderName(
+                                GsmAlphabet.gsm7BitPackedToString(spnData, 0, (numBytes * 8) / 7));
+                    }
+                break;
+                case UserData.ENCODING_UNICODE_16:
+                    setServiceProviderName(new String(spnData, 0, numBytes, "utf-16"));
+                    break;
+                default:
+                    log("SPN encoding not supported");
+                }
+            } catch(Exception e) {
+                log("spn decode error: " + e);
+            }
+            if (DBG) log("spn=" + getServiceProviderName());
+            if (DBG) log("spnCondition=" + mCsimSpnDisplayCondition);
+            mTelephonyManager.setSimOperatorNameForPhone(
+                    mParentApp.getPhoneId(), getServiceProviderName());
+        }
+    }
+
+    private class EfCsimMdnLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_MDN";
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            byte[] data = (byte[]) ar.result;
+            if (DBG) log("CSIM_MDN=" + IccUtils.bytesToHexString(data));
+            // Refer to C.S0065 5.2.35
+            int mdnDigitsNum = 0x0F & data[0];
+            mMdn = IccUtils.cdmaBcdToString(data, 1, mdnDigitsNum);
+            if (DBG) log("CSIM MDN=" + mMdn);
+        }
+    }
+
+    private class EfCsimImsimLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_IMSIM";
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            byte[] data = (byte[]) ar.result;
+            if (VDBG) log("CSIM_IMSIM=" + IccUtils.bytesToHexString(data));
+            // C.S0065 section 5.2.2 for IMSI_M encoding
+            // C.S0005 section 2.3.1 for MIN encoding in IMSI_M.
+            boolean provisioned = ((data[7] & 0x80) == 0x80);
+
+            if (provisioned) {
+                int first3digits = ((0x03 & data[2]) << 8) + (0xFF & data[1]);
+                int second3digits = (((0xFF & data[5]) << 8) | (0xFF & data[4])) >> 6;
+                int digit7 = 0x0F & (data[4] >> 2);
+                if (digit7 > 0x09) digit7 = 0;
+                int last3digits = ((0x03 & data[4]) << 8) | (0xFF & data[3]);
+                first3digits = adjstMinDigits(first3digits);
+                second3digits = adjstMinDigits(second3digits);
+                last3digits = adjstMinDigits(last3digits);
+
+                StringBuilder builder = new StringBuilder();
+                builder.append(String.format(Locale.US, "%03d", first3digits));
+                builder.append(String.format(Locale.US, "%03d", second3digits));
+                builder.append(String.format(Locale.US, "%d", digit7));
+                builder.append(String.format(Locale.US, "%03d", last3digits));
+                mMin = builder.toString();
+                if (DBG) log("min present=" + mMin);
+            } else {
+                if (DBG) log("min not present");
+            }
+        }
+    }
+
+    private class EfCsimCdmaHomeLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_CDMAHOME";
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            // Per C.S0065 section 5.2.8
+            ArrayList<byte[]> dataList = (ArrayList<byte[]>) ar.result;
+            if (DBG) log("CSIM_CDMAHOME data size=" + dataList.size());
+            if (dataList.isEmpty()) {
+                return;
+            }
+            StringBuilder sidBuf = new StringBuilder();
+            StringBuilder nidBuf = new StringBuilder();
+
+            for (byte[] data : dataList) {
+                if (data.length == 5) {
+                    int sid = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF);
+                    int nid = ((data[3] & 0xFF) << 8) | (data[2] & 0xFF);
+                    sidBuf.append(sid).append(',');
+                    nidBuf.append(nid).append(',');
+                }
+            }
+            // remove trailing ","
+            sidBuf.setLength(sidBuf.length()-1);
+            nidBuf.setLength(nidBuf.length()-1);
+
+            mHomeSystemId = sidBuf.toString();
+            mHomeNetworkId = nidBuf.toString();
+        }
+    }
+
+    private class EfCsimEprlLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_EPRL";
+        }
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            onGetCSimEprlDone(ar);
+        }
+    }
+
+    private void onGetCSimEprlDone(AsyncResult ar) {
+        // C.S0065 section 5.2.57 for EFeprl encoding
+        // C.S0016 section 3.5.5 for PRL format.
+        byte[] data = (byte[]) ar.result;
+        if (DBG) log("CSIM_EPRL=" + IccUtils.bytesToHexString(data));
+
+        // Only need the first 4 bytes of record
+        if (data.length > 3) {
+            int prlId = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
+            mPrlVersion = Integer.toString(prlId);
+        }
+        if (DBG) log("CSIM PRL version=" + mPrlVersion);
+    }
+
+    private class EfCsimMipUppLoaded implements IccRecordLoaded {
+        @Override
+        public String getEfName() {
+            return "EF_CSIM_MIPUPP";
+        }
+
+        boolean checkLengthLegal(int length, int expectLength) {
+            if(length < expectLength) {
+                Log.e(LOG_TAG, "CSIM MIPUPP format error, length = " + length  +
+                        "expected length at least =" + expectLength);
+                return false;
+            } else {
+                return true;
+            }
+        }
+
+        @Override
+        public void onRecordLoaded(AsyncResult ar) {
+            // 3GPP2 C.S0065 section 5.2.24
+            byte[] data = (byte[]) ar.result;
+
+            if(data.length < 1) {
+                Log.e(LOG_TAG,"MIPUPP read error");
+                return;
+            }
+
+            BitwiseInputStream bitStream = new BitwiseInputStream(data);
+            try {
+                int  mipUppLength = bitStream.read(8);
+                //transfer length from byte to bit
+                mipUppLength = (mipUppLength << 3);
+
+                if (!checkLengthLegal(mipUppLength, 1)) {
+                    return;
+                }
+                //parse the MIPUPP body 3GPP2 C.S0016-C 3.5.8.6
+                int retryInfoInclude = bitStream.read(1);
+                mipUppLength--;
+
+                if(retryInfoInclude == 1) {
+                    if (!checkLengthLegal(mipUppLength, 11)) {
+                        return;
+                    }
+                    bitStream.skip(11); //not used now
+                    //transfer length from byte to bit
+                    mipUppLength -= 11;
+                }
+
+                if (!checkLengthLegal(mipUppLength, 4)) {
+                    return;
+                }
+                int numNai = bitStream.read(4);
+                mipUppLength -= 4;
+
+                //start parse NAI body
+                for(int index = 0; index < numNai; index++) {
+                    if (!checkLengthLegal(mipUppLength, 4)) {
+                        return;
+                    }
+                    int naiEntryIndex = bitStream.read(4);
+                    mipUppLength -= 4;
+
+                    if (!checkLengthLegal(mipUppLength, 8)) {
+                        return;
+                    }
+                    int naiLength = bitStream.read(8);
+                    mipUppLength -= 8;
+
+                    if(naiEntryIndex == 0) {
+                        //we find the one!
+                        if (!checkLengthLegal(mipUppLength, naiLength << 3)) {
+                            return;
+                        }
+                        char naiCharArray[] = new char[naiLength];
+                        for(int index1 = 0; index1 < naiLength; index1++) {
+                            naiCharArray[index1] = (char)(bitStream.read(8) & 0xFF);
+                        }
+                        mNai =  new String(naiCharArray);
+                        if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
+                            Log.v(LOG_TAG,"MIPUPP Nai = " + mNai);
+                        }
+                        return; //need not parsing further
+                    } else {
+                        //ignore this NAI body
+                        if (!checkLengthLegal(mipUppLength, (naiLength << 3) + 102)) {
+                            return;
+                        }
+                        bitStream.skip((naiLength << 3) + 101);//not used
+                        int mnAaaSpiIndicator = bitStream.read(1);
+                        mipUppLength -= ((naiLength << 3) + 102);
+
+                        if(mnAaaSpiIndicator == 1) {
+                            if (!checkLengthLegal(mipUppLength, 32)) {
+                                return;
+                            }
+                            bitStream.skip(32); //not used
+                            mipUppLength -= 32;
+                        }
+
+                        //MN-HA_AUTH_ALGORITHM
+                        if (!checkLengthLegal(mipUppLength, 5)) {
+                            return;
+                        }
+                        bitStream.skip(4);
+                        mipUppLength -= 4;
+                        int mnHaSpiIndicator = bitStream.read(1);
+                        mipUppLength--;
+
+                        if(mnHaSpiIndicator == 1) {
+                            if (!checkLengthLegal(mipUppLength, 32)) {
+                                return;
+                            }
+                            bitStream.skip(32);
+                            mipUppLength -= 32;
+                        }
+                    }
+                }
+            } catch(Exception e) {
+              Log.e(LOG_TAG,"MIPUPP read Exception error!");
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        byte data[];
+
+        boolean isRecordLoadResponse = false;
+
+        if (mDestroyed.get()) {
+            loge("Received message " + msg +
+                    "[" + msg.what + "] while being destroyed. Ignoring.");
+            return;
+        }
+
+        try { switch (msg.what) {
+            case EVENT_APP_READY:
+                onReady();
+                break;
+
+            case EVENT_GET_DEVICE_IDENTITY_DONE:
+                log("Event EVENT_GET_DEVICE_IDENTITY_DONE Received");
+            break;
+
+            /* IO events */
+            case EVENT_GET_IMSI_DONE:
+                isRecordLoadResponse = true;
+
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception != null) {
+                    loge("Exception querying IMSI, Exception:" + ar.exception);
+                    break;
+                }
+
+                mImsi = (String) ar.result;
+
+                // IMSI (MCC+MNC+MSIN) is at least 6 digits, but not more
+                // than 15 (and usually 15).
+                if (mImsi != null && (mImsi.length() < 6 || mImsi.length() > 15)) {
+                    loge("invalid IMSI " + mImsi);
+                    mImsi = null;
+                }
+
+                // FIXME: CSIM IMSI may not contain the MNC.
+                if (false) {
+                    log("IMSI: " + mImsi.substring(0, 6) + "xxxxxxxxx");
+
+                    String operatorNumeric = getRUIMOperatorNumeric();
+                    if (operatorNumeric != null) {
+                        if (operatorNumeric.length() <= 6) {
+                            log("update mccmnc=" + operatorNumeric);
+                            MccTable.updateMccMncConfiguration(mContext, operatorNumeric, false);
+                        }
+                    }
+                } else {
+                    String operatorNumeric = getRUIMOperatorNumeric();
+                    log("NO update mccmnc=" + operatorNumeric);
+                }
+
+            break;
+
+            case EVENT_GET_CDMA_SUBSCRIPTION_DONE:
+                ar = (AsyncResult)msg.obj;
+                String localTemp[] = (String[])ar.result;
+                if (ar.exception != null) {
+                    break;
+                }
+
+                mMyMobileNumber = localTemp[0];
+                mMin2Min1 = localTemp[3];
+                mPrlVersion = localTemp[4];
+
+                log("MDN: " + mMyMobileNumber + " MIN: " + mMin2Min1);
+
+            break;
+
+            case EVENT_GET_ICCID_DONE:
+                isRecordLoadResponse = true;
+
+                ar = (AsyncResult)msg.obj;
+                data = (byte[])ar.result;
+
+                if (ar.exception != null) {
+                    break;
+                }
+
+                mIccId = IccUtils.bcdToString(data, 0, data.length);
+                mFullIccId = IccUtils.bchToString(data, 0, data.length);
+
+                log("iccid: " + SubscriptionInfo.givePrintableIccid(mFullIccId));
+
+            break;
+
+            case EVENT_UPDATE_DONE:
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception != null) {
+                    Rlog.i(LOG_TAG, "RuimRecords update failed", ar.exception);
+                }
+            break;
+
+            case EVENT_GET_ALL_SMS_DONE:
+            case EVENT_MARK_SMS_READ_DONE:
+            case EVENT_SMS_ON_RUIM:
+            case EVENT_GET_SMS_DONE:
+                Rlog.w(LOG_TAG, "Event not supported: " + msg.what);
+                break;
+
+            // TODO: probably EF_CST should be read instead
+            case EVENT_GET_SST_DONE:
+                log("Event EVENT_GET_SST_DONE Received");
+            break;
+
+            case EVENT_RUIM_REFRESH:
+                isRecordLoadResponse = false;
+                ar = (AsyncResult)msg.obj;
+                if (ar.exception == null) {
+                    handleRuimRefresh((IccRefreshResponse)ar.result);
+                }
+                break;
+
+            default:
+                super.handleMessage(msg);   // IccRecords handles generic record load responses
+
+        }}catch (RuntimeException exc) {
+            // I don't want these exceptions to be fatal
+            Rlog.w(LOG_TAG, "Exception parsing RUIM record", exc);
+        } finally {
+            // Count up record load responses even if they are fails
+            if (isRecordLoadResponse) {
+                onRecordLoaded();
+            }
+        }
+    }
+
+    /**
+     * Returns an array of languages we have assets for.
+     *
+     * NOTE: This array will have duplicates. If this method will be caused
+     * frequently or in a tight loop, it can be rewritten for efficiency.
+     */
+    private static String[] getAssetLanguages(Context ctx) {
+        final String[] locales = ctx.getAssets().getLocales();
+        final String[] localeLangs = new String[locales.length];
+        for (int i = 0; i < locales.length; ++i) {
+            final String localeStr = locales[i];
+            final int separator = localeStr.indexOf('-');
+            if (separator < 0) {
+                localeLangs[i] = localeStr;
+            } else {
+                localeLangs[i] = localeStr.substring(0, separator);
+            }
+        }
+
+        return localeLangs;
+    }
+
+    @Override
+    protected void onRecordLoaded() {
+        // One record loaded successfully or failed, In either case
+        // we need to update the recordsToLoad count
+        mRecordsToLoad -= 1;
+        if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
+
+        if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+            onAllRecordsLoaded();
+        } else if (mRecordsToLoad < 0) {
+            loge("recordsToLoad <0, programmer error suspected");
+            mRecordsToLoad = 0;
+        }
+    }
+
+    @Override
+    protected void onAllRecordsLoaded() {
+        if (DBG) log("record load complete");
+
+        // Further records that can be inserted are Operator/OEM dependent
+
+        // FIXME: CSIM IMSI may not contain the MNC.
+        if (false) {
+            String operator = getRUIMOperatorNumeric();
+            if (!TextUtils.isEmpty(operator)) {
+                log("onAllRecordsLoaded set 'gsm.sim.operator.numeric' to operator='" +
+                        operator + "'");
+                log("update icc_operator_numeric=" + operator);
+                mTelephonyManager.setSimOperatorNumericForPhone(
+                        mParentApp.getPhoneId(), operator);
+            } else {
+                log("onAllRecordsLoaded empty 'gsm.sim.operator.numeric' skipping");
+            }
+
+            String imsi = getIMSI();
+
+            if (!TextUtils.isEmpty(imsi)) {
+                log("onAllRecordsLoaded set mcc imsi=" + (VDBG ? ("=" + imsi) : ""));
+                mTelephonyManager.setSimCountryIsoForPhone(
+                        mParentApp.getPhoneId(),
+                        MccTable.countryCodeForMcc(
+                        Integer.parseInt(imsi.substring(0, 3))));
+            } else {
+                log("onAllRecordsLoaded empty imsi skipping setting mcc");
+            }
+        }
+
+        Resources resource = Resources.getSystem();
+        if (resource.getBoolean(com.android.internal.R.bool.config_use_sim_language_file)) {
+            setSimLanguage(mEFli, mEFpl);
+        }
+
+        mRecordsLoadedRegistrants.notifyRegistrants(
+            new AsyncResult(null, null, null));
+
+        // TODO: The below is hacky since the SubscriptionController may not be ready at this time.
+        if (!TextUtils.isEmpty(mMdn)) {
+            int phoneId = mParentApp.getUiccCard().getPhoneId();
+            int subId = SubscriptionController.getInstance().getSubIdUsingPhoneId(phoneId);
+            if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                SubscriptionManager.from(mContext).setDisplayNumber(mMdn, subId);
+            } else {
+                log("Cannot call setDisplayNumber: invalid subId");
+            }
+        }
+    }
+
+    @Override
+    public void onReady() {
+        fetchRuimRecords();
+
+        mCi.getCDMASubscription(obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_DONE));
+    }
+
+
+    private void fetchRuimRecords() {
+        mRecordsRequested = true;
+
+        if (DBG) log("fetchRuimRecords " + mRecordsToLoad);
+
+        mCi.getIMSIForApp(mParentApp.getAid(), obtainMessage(EVENT_GET_IMSI_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_ICCID,
+                obtainMessage(EVENT_GET_ICCID_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_PL,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfPlLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_CSIM_LI,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimLiLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_CSIM_SPN,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimSpnLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFLinearFixed(EF_CSIM_MDN, 1,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimMdnLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_CSIM_IMSIM,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimImsimLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFLinearFixedAll(EF_CSIM_CDMAHOME,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimCdmaHomeLoaded()));
+        mRecordsToLoad++;
+
+        // Entire PRL could be huge. We are only interested in
+        // the first 4 bytes of the record.
+        mFh.loadEFTransparent(EF_CSIM_EPRL, 4,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimEprlLoaded()));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_CSIM_MIPUPP,
+                obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimMipUppLoaded()));
+        mRecordsToLoad++;
+
+        if (DBG) log("fetchRuimRecords " + mRecordsToLoad + " requested: " + mRecordsRequested);
+        // Further records that can be inserted are Operator/OEM dependent
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * No Display rule for RUIMs yet.
+     */
+    @Override
+    public int getDisplayRule(String plmn) {
+        // TODO together with spn
+        return 0;
+    }
+
+    @Override
+    public boolean isProvisioned() {
+        // If UICC card has CSIM app, look for MDN and MIN field
+        // to determine if the SIM is provisioned.  Otherwise,
+        // consider the SIM is provisioned. (for case of ordinal
+        // USIM only UICC.)
+        // If PROPERTY_TEST_CSIM is defined, bypess provision check
+        // and consider the SIM is provisioned.
+        if (SystemProperties.getBoolean(PROPERTY_TEST_CSIM, false)) {
+            return true;
+        }
+
+        if (mParentApp == null) {
+            return false;
+        }
+
+        if (mParentApp.getType() == AppType.APPTYPE_CSIM &&
+            ((mMdn == null) || (mMin == null))) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void setVoiceMessageWaiting(int line, int countWaiting) {
+        // Will be used in future to store voice mail count in UIM
+        // C.S0023-D_v1.0 does not have a file id in UIM for MWI
+        log("RuimRecords:setVoiceMessageWaiting - NOP for CDMA");
+    }
+
+    @Override
+    public int getVoiceMessageCount() {
+        // Will be used in future to retrieve voice mail count for UIM
+        // C.S0023-D_v1.0 does not have a file id in UIM for MWI
+        log("RuimRecords:getVoiceMessageCount - NOP for CDMA");
+        return 0;
+    }
+
+    private void handleRuimRefresh(IccRefreshResponse refreshResponse) {
+        if (refreshResponse == null) {
+            if (DBG) log("handleRuimRefresh received without input");
+            return;
+        }
+
+        if (!TextUtils.isEmpty(refreshResponse.aid)
+                && !refreshResponse.aid.equals(mParentApp.getAid())) {
+            // This is for different app. Ignore.
+            return;
+        }
+
+        switch (refreshResponse.refreshResult) {
+            case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
+                if (DBG) log("handleRuimRefresh with SIM_REFRESH_FILE_UPDATED");
+                mAdnCache.reset();
+                fetchRuimRecords();
+                break;
+            case IccRefreshResponse.REFRESH_RESULT_INIT:
+                if (DBG) log("handleRuimRefresh with SIM_REFRESH_INIT");
+                // need to reload all files (that we care about)
+                onIccRefreshInit();
+                break;
+            case IccRefreshResponse.REFRESH_RESULT_RESET:
+                // Refresh reset is handled by the UiccCard object.
+                if (DBG) log("handleRuimRefresh with SIM_REFRESH_RESET");
+                break;
+            default:
+                // unknown refresh operation
+                if (DBG) log("handleRuimRefresh with unknown operation");
+                break;
+        }
+    }
+
+    public String getMdn() {
+        return mMdn;
+    }
+
+    public String getMin() {
+        return mMin;
+    }
+
+    public String getSid() {
+        return mHomeSystemId;
+    }
+
+    public String getNid() {
+        return mHomeNetworkId;
+    }
+
+    public boolean getCsimSpnDisplayCondition() {
+        return mCsimSpnDisplayCondition;
+    }
+    @Override
+    protected void log(String s) {
+        Rlog.d(LOG_TAG, "[RuimRecords] " + s);
+    }
+
+    @Override
+    protected void loge(String s) {
+        Rlog.e(LOG_TAG, "[RuimRecords] " + s);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("RuimRecords: " + this);
+        pw.println(" extends:");
+        super.dump(fd, pw, args);
+        pw.println(" mOtaCommited=" + mOtaCommited);
+        pw.println(" mMyMobileNumber=" + mMyMobileNumber);
+        pw.println(" mMin2Min1=" + mMin2Min1);
+        pw.println(" mPrlVersion=" + mPrlVersion);
+        pw.println(" mEFpl[]=" + Arrays.toString(mEFpl));
+        pw.println(" mEFli[]=" + Arrays.toString(mEFli));
+        pw.println(" mCsimSpnDisplayCondition=" + mCsimSpnDisplayCondition);
+        pw.println(" mMdn=" + mMdn);
+        pw.println(" mMin=" + mMin);
+        pw.println(" mHomeSystemId=" + mHomeSystemId);
+        pw.println(" mHomeNetworkId=" + mHomeNetworkId);
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/SIMFileHandler.java b/com/android/internal/telephony/uicc/SIMFileHandler.java
new file mode 100644
index 0000000..3be0c99
--- /dev/null
+++ b/com/android/internal/telephony/uicc/SIMFileHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+
+/**
+ * {@hide}
+ */
+public final class SIMFileHandler extends IccFileHandler implements IccConstants {
+    static final String LOG_TAG = "SIMFileHandler";
+
+    //***** Instance Variables
+
+    //***** Constructor
+
+    public SIMFileHandler(UiccCardApplication app, String aid, CommandsInterface ci) {
+        super(app, aid, ci);
+    }
+
+    //***** Overridden from IccFileHandler
+
+    @Override
+    protected String getEFPath(int efid) {
+        // TODO(): DF_GSM can be 7F20 or 7F21 to handle backward compatibility.
+        // Implement this after discussion with OEMs.
+        switch(efid) {
+        case EF_SMS:
+            return MF_SIM + DF_TELECOM;
+
+        case EF_EXT6:
+        case EF_MWIS:
+        case EF_MBI:
+        case EF_SPN:
+        case EF_AD:
+        case EF_MBDN:
+        case EF_PNN:
+        case EF_SPDI:
+        case EF_SST:
+        case EF_CFIS:
+        case EF_GID1:
+        case EF_GID2:
+        case EF_MAILBOX_CPHS:
+        case EF_VOICE_MAIL_INDICATOR_CPHS:
+        case EF_CFF_CPHS:
+        case EF_SPN_CPHS:
+        case EF_SPN_SHORT_CPHS:
+        case EF_INFO_CPHS:
+        case EF_CSP_CPHS:
+            return MF_SIM + DF_GSM;
+        }
+        String path = getCommonIccEFPath(efid);
+        if (path == null) {
+            Rlog.e(LOG_TAG, "Error: EF Path being returned in null");
+        }
+        return path;
+    }
+
+    @Override
+    protected void logd(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    @Override
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/SIMRecords.java b/com/android/internal/telephony/uicc/SIMRecords.java
new file mode 100644
index 0000000..724b478
--- /dev/null
+++ b/com/android/internal/telephony/uicc/SIMRecords.java
@@ -0,0 +1,2192 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+import android.telephony.SmsMessage;
+import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.MccTable;
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.gsm.SimTlv;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * {@hide}
+ */
+public class SIMRecords extends IccRecords {
+    protected static final String LOG_TAG = "SIMRecords";
+
+    private static final boolean CRASH_RIL = false;
+
+    private static final boolean VDBG = false;
+
+    // ***** Instance Variables
+
+    VoiceMailConstants mVmConfig;
+
+
+    SpnOverride mSpnOverride;
+
+    // ***** Cached SIM State; cleared on channel close
+
+    private int mCallForwardingStatus;
+
+
+    /**
+     * States only used by getSpnFsm FSM
+     */
+    private GetSpnFsmState mSpnState;
+
+    /** CPHS service information (See CPHS 4.2 B.3.1.1)
+     *  It will be set in onSimReady if reading GET_CPHS_INFO successfully
+     *  mCphsInfo[0] is CPHS Phase
+     *  mCphsInfo[1] and mCphsInfo[2] is CPHS Service Table
+     */
+    private byte[] mCphsInfo = null;
+    boolean mCspPlmnEnabled = true;
+
+    byte[] mEfMWIS = null;
+    byte[] mEfCPHS_MWI =null;
+    byte[] mEfCff = null;
+    byte[] mEfCfis = null;
+
+    byte[] mEfLi = null;
+    byte[] mEfPl = null;
+
+    int mSpnDisplayCondition;
+    // Numeric network codes listed in TS 51.011 EF[SPDI]
+    ArrayList<String> mSpdiNetworks = null;
+
+    String mPnnHomeName = null;
+
+    UsimServiceTable mUsimServiceTable;
+
+    @Override
+    public String toString() {
+        return "SimRecords: " + super.toString()
+                + " mVmConfig" + mVmConfig
+                + " mSpnOverride=" + mSpnOverride
+                + " callForwardingEnabled=" + mCallForwardingStatus
+                + " spnState=" + mSpnState
+                + " mCphsInfo=" + mCphsInfo
+                + " mCspPlmnEnabled=" + mCspPlmnEnabled
+                + " efMWIS=" + mEfMWIS
+                + " efCPHS_MWI=" + mEfCPHS_MWI
+                + " mEfCff=" + mEfCff
+                + " mEfCfis=" + mEfCfis
+                + " getOperatorNumeric=" + getOperatorNumeric();
+    }
+
+    // ***** Constants
+
+    // From TS 51.011 EF[SPDI] section
+    static final int TAG_SPDI = 0xA3;
+    static final int TAG_SPDI_PLMN_LIST = 0x80;
+
+    // Full Name IEI from TS 24.008
+    static final int TAG_FULL_NETWORK_NAME = 0x43;
+
+    // Short Name IEI from TS 24.008
+    static final int TAG_SHORT_NETWORK_NAME = 0x45;
+
+    // active CFF from CPHS 4.2 B.4.5
+    static final int CFF_UNCONDITIONAL_ACTIVE = 0x0a;
+    static final int CFF_UNCONDITIONAL_DEACTIVE = 0x05;
+    static final int CFF_LINE1_MASK = 0x0f;
+    static final int CFF_LINE1_RESET = 0xf0;
+
+    // CPHS Service Table (See CPHS 4.2 B.3.1)
+    private static final int CPHS_SST_MBN_MASK = 0x30;
+    private static final int CPHS_SST_MBN_ENABLED = 0x30;
+
+    // EF_CFIS related constants
+    // Spec reference TS 51.011 section 10.3.46.
+    private static final int CFIS_BCD_NUMBER_LENGTH_OFFSET = 2;
+    private static final int CFIS_TON_NPI_OFFSET = 3;
+    private static final int CFIS_ADN_CAPABILITY_ID_OFFSET = 14;
+    private static final int CFIS_ADN_EXTENSION_ID_OFFSET = 15;
+
+    // ***** Event Constants
+    private static final int SIM_RECORD_EVENT_BASE = 0x00;
+    private static final int EVENT_GET_IMSI_DONE = 3 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_ICCID_DONE = 4 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_MBI_DONE = 5 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_MBDN_DONE = 6 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_MWIS_DONE = 7 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE = 8 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_AD_DONE = 9 + SIM_RECORD_EVENT_BASE; // Admin data on SIM
+    private static final int EVENT_GET_MSISDN_DONE = 10 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_CPHS_MAILBOX_DONE = 11 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_SPN_DONE = 12 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_SPDI_DONE = 13 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_UPDATE_DONE = 14 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_PNN_DONE = 15 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_SST_DONE = 17 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_ALL_SMS_DONE = 18 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_MARK_SMS_READ_DONE = 19 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_SET_MBDN_DONE = 20 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_SMS_ON_SIM = 21 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_SMS_DONE = 22 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_CFF_DONE = 24 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_SET_CPHS_MAILBOX_DONE = 25 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_INFO_CPHS_DONE = 26 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_SET_MSISDN_DONE = 30 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_CFIS_DONE = 32 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_CSP_CPHS_DONE = 33 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_GID1_DONE = 34 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_GID2_DONE = 36 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_PLMN_W_ACT_DONE = 37 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_OPLMN_W_ACT_DONE = 38 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_HPLMN_W_ACT_DONE = 39 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_EHPLMN_DONE = 40 + SIM_RECORD_EVENT_BASE;
+    private static final int EVENT_GET_FPLMN_DONE = 41 + SIM_RECORD_EVENT_BASE;
+
+    // TODO: Possibly move these to IccRecords.java
+    private static final int SYSTEM_EVENT_BASE = 0x100;
+    private static final int EVENT_CARRIER_CONFIG_CHANGED = 1 + SYSTEM_EVENT_BASE;
+    private static final int EVENT_APP_LOCKED = 2 + SYSTEM_EVENT_BASE;
+    private static final int EVENT_SIM_REFRESH = 3 + SYSTEM_EVENT_BASE;
+
+    // Lookup table for carriers known to produce SIMs which incorrectly indicate MNC length.
+
+    private static final String[] MCCMNC_CODES_HAVING_3DIGITS_MNC = {
+        "302370", "302720", "310260",
+        "405025", "405026", "405027", "405028", "405029", "405030", "405031", "405032",
+        "405033", "405034", "405035", "405036", "405037", "405038", "405039", "405040",
+        "405041", "405042", "405043", "405044", "405045", "405046", "405047", "405750",
+        "405751", "405752", "405753", "405754", "405755", "405756", "405799", "405800",
+        "405801", "405802", "405803", "405804", "405805", "405806", "405807", "405808",
+        "405809", "405810", "405811", "405812", "405813", "405814", "405815", "405816",
+        "405817", "405818", "405819", "405820", "405821", "405822", "405823", "405824",
+        "405825", "405826", "405827", "405828", "405829", "405830", "405831", "405832",
+        "405833", "405834", "405835", "405836", "405837", "405838", "405839", "405840",
+        "405841", "405842", "405843", "405844", "405845", "405846", "405847", "405848",
+        "405849", "405850", "405851", "405852", "405853", "405854", "405855", "405856",
+        "405857", "405858", "405859", "405860", "405861", "405862", "405863", "405864",
+        "405865", "405866", "405867", "405868", "405869", "405870", "405871", "405872",
+        "405873", "405874", "405875", "405876", "405877", "405878", "405879", "405880",
+        "405881", "405882", "405883", "405884", "405885", "405886", "405908", "405909",
+        "405910", "405911", "405912", "405913", "405914", "405915", "405916", "405917",
+        "405918", "405919", "405920", "405921", "405922", "405923", "405924", "405925",
+        "405926", "405927", "405928", "405929", "405930", "405931", "405932", "502142",
+        "502143", "502145", "502146", "502147", "502148"
+    };
+
+    // ***** Constructor
+
+    public SIMRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
+        super(app, c, ci);
+
+        mAdnCache = new AdnRecordCache(mFh);
+
+        mVmConfig = new VoiceMailConstants();
+        mSpnOverride = new SpnOverride();
+
+        mRecordsRequested = false;  // No load request is made till SIM ready
+
+        // recordsToLoad is set to 0 because no requests are made yet
+        mRecordsToLoad = 0;
+
+        mCi.setOnSmsOnSim(this, EVENT_SMS_ON_SIM, null);
+        mCi.registerForIccRefresh(this, EVENT_SIM_REFRESH, null);
+
+        // Start off by setting empty state
+        resetRecords();
+        mParentApp.registerForReady(this, EVENT_APP_READY, null);
+        mParentApp.registerForLocked(this, EVENT_APP_LOCKED, null);
+        if (DBG) log("SIMRecords X ctor this=" + this);
+
+        IntentFilter intentfilter = new IntentFilter();
+        intentfilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        c.registerReceiver(mReceiver, intentfilter);
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                sendMessage(obtainMessage(EVENT_CARRIER_CONFIG_CHANGED));
+            }
+        }
+    };
+
+    @Override
+    public void dispose() {
+        if (DBG) log("Disposing SIMRecords this=" + this);
+        //Unregister for all events
+        mCi.unregisterForIccRefresh(this);
+        mCi.unSetOnSmsOnSim(this);
+        mParentApp.unregisterForReady(this);
+        mParentApp.unregisterForLocked(this);
+        mContext.unregisterReceiver(mReceiver);
+        resetRecords();
+        super.dispose();
+    }
+
+    @Override
+    protected void finalize() {
+        if (DBG) log("finalized");
+    }
+
+    protected void resetRecords() {
+        mImsi = null;
+        mMsisdn = null;
+        mVoiceMailNum = null;
+        mMncLength = UNINITIALIZED;
+        log("setting0 mMncLength" + mMncLength);
+        mIccId = null;
+        mFullIccId = null;
+        // -1 means no EF_SPN found; treat accordingly.
+        mSpnDisplayCondition = -1;
+        mEfMWIS = null;
+        mEfCPHS_MWI = null;
+        mSpdiNetworks = null;
+        mPnnHomeName = null;
+        mGid1 = null;
+        mGid2 = null;
+        mPlmnActRecords = null;
+        mOplmnActRecords = null;
+        mHplmnActRecords = null;
+        mFplmns = null;
+        mEhplmns = null;
+
+        mAdnCache.reset();
+
+        log("SIMRecords: onRadioOffOrNotAvailable set 'gsm.sim.operator.numeric' to operator=null");
+        log("update icc_operator_numeric=" + null);
+        mTelephonyManager.setSimOperatorNumericForPhone(mParentApp.getPhoneId(), "");
+        mTelephonyManager.setSimOperatorNameForPhone(mParentApp.getPhoneId(), "");
+        mTelephonyManager.setSimCountryIsoForPhone(mParentApp.getPhoneId(), "");
+
+        // recordsRequested is set to false indicating that the SIM
+        // read requests made so far are not valid. This is set to
+        // true only when fresh set of read requests are made.
+        mRecordsRequested = false;
+    }
+
+    //***** Public Methods
+
+    @Override
+    public String getMsisdnNumber() {
+        return mMsisdn;
+    }
+
+    @Override
+    public UsimServiceTable getUsimServiceTable() {
+        return mUsimServiceTable;
+    }
+
+    private int getExtFromEf(int ef) {
+        int ext;
+        switch (ef) {
+            case EF_MSISDN:
+                /* For USIM apps use EXT5. (TS 31.102 Section 4.2.37) */
+                if (mParentApp.getType() == AppType.APPTYPE_USIM) {
+                    ext = EF_EXT5;
+                } else {
+                    ext = EF_EXT1;
+                }
+                break;
+            default:
+                ext = EF_EXT1;
+        }
+        return ext;
+    }
+
+    /**
+     * Set subscriber number to SIM record
+     *
+     * The subscriber number is stored in EF_MSISDN (TS 51.011)
+     *
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param alphaTag alpha-tagging of the dailing nubmer (up to 10 characters)
+     * @param number dialing number (up to 20 digits)
+     *        if the number starts with '+', then set to international TOA
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    @Override
+    public void setMsisdnNumber(String alphaTag, String number,
+            Message onComplete) {
+
+        // If the SIM card is locked by PIN, we will set EF_MSISDN fail.
+        // In that case, msisdn and msisdnTag should not be update.
+        mNewMsisdn = number;
+        mNewMsisdnTag = alphaTag;
+
+        if(DBG) log("Set MSISDN: " + mNewMsisdnTag + " " + /*mNewMsisdn*/
+                Rlog.pii(LOG_TAG, mNewMsisdn));
+
+        AdnRecord adn = new AdnRecord(mNewMsisdnTag, mNewMsisdn);
+
+        new AdnRecordLoader(mFh).updateEF(adn, EF_MSISDN, getExtFromEf(EF_MSISDN), 1, null,
+                obtainMessage(EVENT_SET_MSISDN_DONE, onComplete));
+    }
+
+    @Override
+    public String getMsisdnAlphaTag() {
+        return mMsisdnTag;
+    }
+
+    @Override
+    public String getVoiceMailNumber() {
+        return mVoiceMailNum;
+    }
+
+    /**
+     * Set voice mail number to SIM record
+     *
+     * The voice mail number can be stored either in EF_MBDN (TS 51.011) or
+     * EF_MAILBOX_CPHS (CPHS 4.2)
+     *
+     * If EF_MBDN is available, store the voice mail number to EF_MBDN
+     *
+     * If EF_MAILBOX_CPHS is enabled, store the voice mail number to EF_CHPS
+     *
+     * So the voice mail number will be stored in both EFs if both are available
+     *
+     * Return error only if both EF_MBDN and EF_MAILBOX_CPHS fail.
+     *
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param alphaTag alpha-tagging of the dailing nubmer (upto 10 characters)
+     * @param voiceNumber dailing nubmer (upto 20 digits)
+     *        if the number is start with '+', then set to international TOA
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    @Override
+    public void setVoiceMailNumber(String alphaTag, String voiceNumber,
+            Message onComplete) {
+        if (mIsVoiceMailFixed) {
+            AsyncResult.forMessage((onComplete)).exception =
+                    new IccVmFixedException("Voicemail number is fixed by operator");
+            onComplete.sendToTarget();
+            return;
+        }
+
+        mNewVoiceMailNum = voiceNumber;
+        mNewVoiceMailTag = alphaTag;
+
+        AdnRecord adn = new AdnRecord(mNewVoiceMailTag, mNewVoiceMailNum);
+
+        if (mMailboxIndex != 0 && mMailboxIndex != 0xff) {
+
+            new AdnRecordLoader(mFh).updateEF(adn, EF_MBDN, EF_EXT6,
+                    mMailboxIndex, null,
+                    obtainMessage(EVENT_SET_MBDN_DONE, onComplete));
+
+        } else if (isCphsMailboxEnabled()) {
+
+            new AdnRecordLoader(mFh).updateEF(adn, EF_MAILBOX_CPHS,
+                    EF_EXT1, 1, null,
+                    obtainMessage(EVENT_SET_CPHS_MAILBOX_DONE, onComplete));
+
+        } else {
+            AsyncResult.forMessage((onComplete)).exception =
+                    new IccVmNotSupportedException("Update SIM voice mailbox error");
+            onComplete.sendToTarget();
+        }
+    }
+
+    @Override
+    public String getVoiceMailAlphaTag()
+    {
+        return mVoiceMailTag;
+    }
+
+    /**
+     * Sets the SIM voice message waiting indicator records
+     * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported
+     * @param countWaiting The number of messages waiting, if known. Use
+     *                     -1 to indicate that an unknown number of
+     *                      messages are waiting
+     */
+    @Override
+    public void
+    setVoiceMessageWaiting(int line, int countWaiting) {
+        if (line != 1) {
+            // only profile 1 is supported
+            return;
+        }
+
+        try {
+            if (mEfMWIS != null) {
+                // TS 51.011 10.3.45
+
+                // lsb of byte 0 is 'voicemail' status
+                mEfMWIS[0] = (byte)((mEfMWIS[0] & 0xfe)
+                                    | (countWaiting == 0 ? 0 : 1));
+
+                // byte 1 is the number of voice messages waiting
+                if (countWaiting < 0) {
+                    // The spec does not define what this should be
+                    // if we don't know the count
+                    mEfMWIS[1] = 0;
+                } else {
+                    mEfMWIS[1] = (byte) countWaiting;
+                }
+
+                mFh.updateEFLinearFixed(
+                    EF_MWIS, 1, mEfMWIS, null,
+                    obtainMessage (EVENT_UPDATE_DONE, EF_MWIS, 0));
+            }
+
+            if (mEfCPHS_MWI != null) {
+                    // Refer CPHS4_2.WW6 B4.2.3
+                mEfCPHS_MWI[0] = (byte)((mEfCPHS_MWI[0] & 0xf0)
+                            | (countWaiting == 0 ? 0x5 : 0xa));
+                mFh.updateEFTransparent(
+                    EF_VOICE_MAIL_INDICATOR_CPHS, mEfCPHS_MWI,
+                    obtainMessage (EVENT_UPDATE_DONE, EF_VOICE_MAIL_INDICATOR_CPHS));
+            }
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            logw("Error saving voice mail state to SIM. Probably malformed SIM record", ex);
+        }
+    }
+
+    // Validate data is !null.
+    private boolean validEfCfis(byte[] data) {
+        if (data != null) {
+            if (data[0] < 1 || data[0] > 4) {
+                // The MSP (Multiple Subscriber Profile) byte should be between
+                // 1 and 4 according to ETSI TS 131 102 v11.3.0 section 4.2.64.
+                logw("MSP byte: " + data[0] + " is not between 1 and 4", null);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public int getVoiceMessageCount() {
+        boolean voiceMailWaiting = false;
+        int countVoiceMessages = DEFAULT_VOICE_MESSAGE_COUNT;
+        if (mEfMWIS != null) {
+            // Use this data if the EF[MWIS] exists and
+            // has been loaded
+            // Refer TS 51.011 Section 10.3.45 for the content description
+            voiceMailWaiting = ((mEfMWIS[0] & 0x01) != 0);
+            countVoiceMessages = mEfMWIS[1] & 0xff;
+
+            if (voiceMailWaiting && (countVoiceMessages == 0 || countVoiceMessages == 0xff)) {
+                // Unknown count = -1
+                countVoiceMessages = UNKNOWN_VOICE_MESSAGE_COUNT;
+            }
+            if (DBG) log(" VoiceMessageCount from SIM MWIS = " + countVoiceMessages);
+        } else if (mEfCPHS_MWI != null) {
+            // use voice mail count from CPHS
+            int indicator = (int) (mEfCPHS_MWI[0] & 0xf);
+
+            // Refer CPHS4_2.WW6 B4.2.3
+            if (indicator == 0xA) {
+                // Unknown count = -1
+                countVoiceMessages = UNKNOWN_VOICE_MESSAGE_COUNT;
+            } else if (indicator == 0x5) {
+                countVoiceMessages = 0;
+            }
+            if (DBG) log(" VoiceMessageCount from SIM CPHS = " + countVoiceMessages);
+        }
+        return countVoiceMessages;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getVoiceCallForwardingFlag() {
+        return mCallForwardingStatus;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setVoiceCallForwardingFlag(int line, boolean enable, String dialNumber) {
+
+        if (line != 1) return; // only line 1 is supported
+
+        mCallForwardingStatus = enable ? CALL_FORWARDING_STATUS_ENABLED :
+                CALL_FORWARDING_STATUS_DISABLED;
+
+        mRecordsEventsRegistrants.notifyResult(EVENT_CFI);
+
+        try {
+            if (validEfCfis(mEfCfis)) {
+                // lsb is of byte f1 is voice status
+                if (enable) {
+                    mEfCfis[1] |= 1;
+                } else {
+                    mEfCfis[1] &= 0xfe;
+                }
+
+                log("setVoiceCallForwardingFlag: enable=" + enable
+                        + " mEfCfis=" + IccUtils.bytesToHexString(mEfCfis));
+
+                // Update dialNumber if not empty and CFU is enabled.
+                // Spec reference for EF_CFIS contents, TS 51.011 section 10.3.46.
+                if (enable && !TextUtils.isEmpty(dialNumber)) {
+                    logv("EF_CFIS: updating cf number, " + Rlog.pii(LOG_TAG, dialNumber));
+                    byte[] bcdNumber = PhoneNumberUtils.numberToCalledPartyBCD(dialNumber);
+
+                    System.arraycopy(bcdNumber, 0, mEfCfis, CFIS_TON_NPI_OFFSET, bcdNumber.length);
+
+                    mEfCfis[CFIS_BCD_NUMBER_LENGTH_OFFSET] = (byte) (bcdNumber.length);
+                    mEfCfis[CFIS_ADN_CAPABILITY_ID_OFFSET] = (byte) 0xFF;
+                    mEfCfis[CFIS_ADN_EXTENSION_ID_OFFSET] = (byte) 0xFF;
+                }
+
+                mFh.updateEFLinearFixed(
+                        EF_CFIS, 1, mEfCfis, null,
+                        obtainMessage (EVENT_UPDATE_DONE, EF_CFIS));
+            } else {
+                log("setVoiceCallForwardingFlag: ignoring enable=" + enable
+                        + " invalid mEfCfis=" + IccUtils.bytesToHexString(mEfCfis));
+            }
+
+            if (mEfCff != null) {
+                if (enable) {
+                    mEfCff[0] = (byte) ((mEfCff[0] & CFF_LINE1_RESET)
+                            | CFF_UNCONDITIONAL_ACTIVE);
+                } else {
+                    mEfCff[0] = (byte) ((mEfCff[0] & CFF_LINE1_RESET)
+                            | CFF_UNCONDITIONAL_DEACTIVE);
+                }
+
+                mFh.updateEFTransparent(
+                        EF_CFF_CPHS, mEfCff,
+                        obtainMessage (EVENT_UPDATE_DONE, EF_CFF_CPHS));
+            }
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            logw("Error saving call forwarding flag to SIM. "
+                            + "Probably malformed SIM record", ex);
+
+        }
+    }
+
+    /**
+     * Called by STK Service when REFRESH is received.
+     * @param fileChanged indicates whether any files changed
+     * @param fileList if non-null, a list of EF files that changed
+     */
+    @Override
+    public void onRefresh(boolean fileChanged, int[] fileList) {
+        if (fileChanged) {
+            // A future optimization would be to inspect fileList and
+            // only reload those files that we care about.  For now,
+            // just re-fetch all SIM records that we cache.
+            fetchSimRecords();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getOperatorNumeric() {
+        String imsi = getIMSI();
+        if (imsi == null) {
+            log("getOperatorNumeric: IMSI == null");
+            return null;
+        }
+        if (mMncLength == UNINITIALIZED || mMncLength == UNKNOWN) {
+            log("getSIMOperatorNumeric: bad mncLength");
+            return null;
+        }
+
+        // Length = length of MCC + length of MNC
+        // length of mcc = 3 (TS 23.003 Section 2.2)
+        if (imsi.length() >= 3 + mMncLength) {
+            return imsi.substring(0, 3 + mMncLength);
+        } else {
+            return null;
+        }
+    }
+
+    // ***** Overridden from Handler
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+        AdnRecord adn;
+
+        byte data[];
+
+        boolean isRecordLoadResponse = false;
+
+        if (mDestroyed.get()) {
+            loge("Received message " + msg + "[" + msg.what + "] " +
+                    " while being destroyed. Ignoring.");
+            return;
+        }
+
+        try {
+            switch (msg.what) {
+                case EVENT_APP_READY:
+                    onReady();
+                    break;
+
+                case EVENT_APP_LOCKED:
+                    onLocked();
+                    break;
+
+                /* IO events */
+                case EVENT_GET_IMSI_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception != null) {
+                        loge("Exception querying IMSI, Exception:" + ar.exception);
+                        break;
+                    }
+
+                    mImsi = (String) ar.result;
+
+                    // IMSI (MCC+MNC+MSIN) is at least 6 digits, but not more
+                    // than 15 (and usually 15).
+                    if (mImsi != null && (mImsi.length() < 6 || mImsi.length() > 15)) {
+                        loge("invalid IMSI " + mImsi);
+                        mImsi = null;
+                    }
+
+                    log("IMSI: mMncLength=" + mMncLength);
+
+                    if (mImsi != null && mImsi.length() >= 6) {
+                        log("IMSI: " + mImsi.substring(0, 6)
+                                + Rlog.pii(LOG_TAG, mImsi.substring(6)));
+                    }
+
+                    String imsi = getIMSI();
+
+                    if (((mMncLength == UNKNOWN) || (mMncLength == 2))
+                            && ((imsi != null) && (imsi.length() >= 6))) {
+                        String mccmncCode = imsi.substring(0, 6);
+                        for (String mccmnc : MCCMNC_CODES_HAVING_3DIGITS_MNC) {
+                            if (mccmnc.equals(mccmncCode)) {
+                                mMncLength = 3;
+                                log("IMSI: setting1 mMncLength=" + mMncLength);
+                                break;
+                            }
+                        }
+                    }
+
+                    if (mMncLength == UNKNOWN) {
+                        // the SIM has told us all it knows, but it didn't know the mnc length.
+                        // guess using the mcc
+                        try {
+                            int mcc = Integer.parseInt(imsi.substring(0, 3));
+                            mMncLength = MccTable.smallestDigitsMccForMnc(mcc);
+                            log("setting2 mMncLength=" + mMncLength);
+                        } catch (NumberFormatException e) {
+                            mMncLength = UNKNOWN;
+                            loge("Corrupt IMSI! setting3 mMncLength=" + mMncLength);
+                        }
+                    }
+
+                    if (mMncLength != UNKNOWN && mMncLength != UNINITIALIZED
+                            && imsi.length() >= 3 + mMncLength) {
+                        log("update mccmnc=" + imsi.substring(0, 3 + mMncLength));
+                        // finally have both the imsi and the mncLength and
+                        // can parse the imsi properly
+                        MccTable.updateMccMncConfiguration(mContext,
+                                imsi.substring(0, 3 + mMncLength), false);
+                    }
+                    mImsiReadyRegistrants.notifyRegistrants();
+                    break;
+
+                case EVENT_GET_MBI_DONE:
+                    boolean isValidMbdn;
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    isValidMbdn = false;
+                    if (ar.exception == null) {
+                        // Refer TS 51.011 Section 10.3.44 for content details
+                        log("EF_MBI: " + IccUtils.bytesToHexString(data));
+
+                        // Voice mail record number stored first
+                        mMailboxIndex = data[0] & 0xff;
+
+                        // check if dailing numbe id valid
+                        if (mMailboxIndex != 0 && mMailboxIndex != 0xff) {
+                            log("Got valid mailbox number for MBDN");
+                            isValidMbdn = true;
+                        }
+                    }
+
+                    // one more record to load
+                    mRecordsToLoad += 1;
+
+                    if (isValidMbdn) {
+                        // Note: MBDN was not included in NUM_OF_SIM_RECORDS_LOADED
+                        new AdnRecordLoader(mFh).loadFromEF(EF_MBDN, EF_EXT6,
+                                mMailboxIndex, obtainMessage(EVENT_GET_MBDN_DONE));
+                    } else {
+                        // If this EF not present, try mailbox as in CPHS standard
+                        // CPHS (CPHS4_2.WW6) is a european standard.
+                        new AdnRecordLoader(mFh).loadFromEF(EF_MAILBOX_CPHS,
+                                EF_EXT1, 1,
+                                obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE));
+                    }
+
+                    break;
+                case EVENT_GET_CPHS_MAILBOX_DONE:
+                case EVENT_GET_MBDN_DONE:
+                    //Resetting the voice mail number and voice mail tag to null
+                    //as these should be updated from the data read from EF_MBDN.
+                    //If they are not reset, incase of invalid data/exception these
+                    //variables are retaining their previous values and are
+                    //causing invalid voice mailbox info display to user.
+                    mVoiceMailNum = null;
+                    mVoiceMailTag = null;
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception != null) {
+
+                        log("Invalid or missing EF"
+                                + ((msg.what == EVENT_GET_CPHS_MAILBOX_DONE)
+                                    ? "[MAILBOX]" : "[MBDN]"));
+
+                        // Bug #645770 fall back to CPHS
+                        // FIXME should use SST to decide
+
+                        if (msg.what == EVENT_GET_MBDN_DONE) {
+                            //load CPHS on fail...
+                            // FIXME right now, only load line1's CPHS voice mail entry
+
+                            mRecordsToLoad += 1;
+                            new AdnRecordLoader(mFh).loadFromEF(
+                                    EF_MAILBOX_CPHS, EF_EXT1, 1,
+                                    obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE));
+                        }
+                        break;
+                    }
+
+                    adn = (AdnRecord) ar.result;
+
+                    log("VM: " + adn
+                            + ((msg.what == EVENT_GET_CPHS_MAILBOX_DONE)
+                                ? " EF[MAILBOX]" : " EF[MBDN]"));
+
+                    if (adn.isEmpty() && msg.what == EVENT_GET_MBDN_DONE) {
+                        // Bug #645770 fall back to CPHS
+                        // FIXME should use SST to decide
+                        // FIXME right now, only load line1's CPHS voice mail entry
+                        mRecordsToLoad += 1;
+                        new AdnRecordLoader(mFh).loadFromEF(
+                                EF_MAILBOX_CPHS, EF_EXT1, 1,
+                                obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE));
+
+                        break;
+                    }
+
+                    mVoiceMailNum = adn.getNumber();
+                    mVoiceMailTag = adn.getAlphaTag();
+                    break;
+
+                case EVENT_GET_MSISDN_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception != null) {
+                        log("Invalid or missing EF[MSISDN]");
+                        break;
+                    }
+
+                    adn = (AdnRecord) ar.result;
+
+                    mMsisdn = adn.getNumber();
+                    mMsisdnTag = adn.getAlphaTag();
+
+                    log("MSISDN: " + /*mMsisdn*/ Rlog.pii(LOG_TAG, mMsisdn));
+                    break;
+
+                case EVENT_SET_MSISDN_DONE:
+                    isRecordLoadResponse = false;
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception == null) {
+                        mMsisdn = mNewMsisdn;
+                        mMsisdnTag = mNewMsisdnTag;
+                        log("Success to update EF[MSISDN]");
+                    }
+
+                    if (ar.userObj != null) {
+                        AsyncResult.forMessage(((Message) ar.userObj)).exception = ar.exception;
+                        ((Message) ar.userObj).sendToTarget();
+                    }
+                    break;
+
+                case EVENT_GET_MWIS_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (DBG) log("EF_MWIS : " + IccUtils.bytesToHexString(data));
+
+                    if (ar.exception != null) {
+                        if (DBG) log("EVENT_GET_MWIS_DONE exception = " + ar.exception);
+                        break;
+                    }
+
+                    if ((data[0] & 0xff) == 0xff) {
+                        if (DBG) log("SIMRecords: Uninitialized record MWIS");
+                        break;
+                    }
+
+                    mEfMWIS = data;
+                    break;
+
+                case EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (DBG) log("EF_CPHS_MWI: " + IccUtils.bytesToHexString(data));
+
+                    if (ar.exception != null) {
+                        if (DBG) {
+                            log("EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE exception = "
+                                    + ar.exception);
+                        }
+                        break;
+                    }
+
+                    mEfCPHS_MWI = data;
+                    break;
+
+                case EVENT_GET_ICCID_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        break;
+                    }
+
+                    mIccId = IccUtils.bcdToString(data, 0, data.length);
+                    mFullIccId = IccUtils.bchToString(data, 0, data.length);
+
+                    log("iccid: " + SubscriptionInfo.givePrintableIccid(mFullIccId));
+                    break;
+
+
+                case EVENT_GET_AD_DONE:
+                    try {
+                        isRecordLoadResponse = true;
+
+                        if (mCarrierTestOverride.isInTestMode() && getIMSI() != null) {
+                            imsi = getIMSI();
+                            try {
+                                int mcc = Integer.parseInt(imsi.substring(0, 3));
+                                mMncLength = MccTable.smallestDigitsMccForMnc(mcc);
+                                log("[TestMode] mMncLength=" + mMncLength);
+                            } catch (NumberFormatException e) {
+                                mMncLength = UNKNOWN;
+                                loge("[TestMode] Corrupt IMSI! mMncLength=" + mMncLength);
+                            }
+                        } else {
+                            ar = (AsyncResult) msg.obj;
+                            data = (byte[]) ar.result;
+
+                            if (ar.exception != null) {
+                                break;
+                            }
+
+                            log("EF_AD: " + IccUtils.bytesToHexString(data));
+
+                            if (data.length < 3) {
+                                log("Corrupt AD data on SIM");
+                                break;
+                            }
+
+                            if (data.length == 3) {
+                                log("MNC length not present in EF_AD");
+                                break;
+                            }
+
+                            mMncLength = data[3] & 0xf;
+                            log("setting4 mMncLength=" + mMncLength);
+                        }
+
+                        if (mMncLength == 0xf) {
+                            mMncLength = UNKNOWN;
+                            log("setting5 mMncLength=" + mMncLength);
+                        } else if (mMncLength != 2 && mMncLength != 3) {
+                            mMncLength = UNINITIALIZED;
+                            log("setting5 mMncLength=" + mMncLength);
+                        }
+                    } finally {
+
+                        // IMSI could be a value reading from Sim or a fake IMSI if in the test mode
+                        imsi = getIMSI();
+
+                        if (((mMncLength == UNINITIALIZED) || (mMncLength == UNKNOWN)
+                                    || (mMncLength == 2)) && ((imsi != null)
+                                    && (imsi.length() >= 6))) {
+                            String mccmncCode = imsi.substring(0, 6);
+                            log("mccmncCode=" + mccmncCode);
+                            for (String mccmnc : MCCMNC_CODES_HAVING_3DIGITS_MNC) {
+                                if (mccmnc.equals(mccmncCode)) {
+                                    mMncLength = 3;
+                                    log("setting6 mMncLength=" + mMncLength);
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (mMncLength == UNKNOWN || mMncLength == UNINITIALIZED) {
+                            if (imsi != null) {
+                                try {
+                                    int mcc = Integer.parseInt(imsi.substring(0, 3));
+
+                                    mMncLength = MccTable.smallestDigitsMccForMnc(mcc);
+                                    log("setting7 mMncLength=" + mMncLength);
+                                } catch (NumberFormatException e) {
+                                    mMncLength = UNKNOWN;
+                                    loge("Corrupt IMSI! setting8 mMncLength=" + mMncLength);
+                                }
+                            } else {
+                                // Indicate we got this info, but it didn't contain the length.
+                                mMncLength = UNKNOWN;
+                                log("MNC length not present in EF_AD setting9 "
+                                        + "mMncLength=" + mMncLength);
+                            }
+                        }
+                        if (imsi != null && mMncLength != UNKNOWN
+                                && imsi.length() >= 3 + mMncLength) {
+                            // finally have both imsi and the length of the mnc and can parse
+                            // the imsi properly
+                            log("update mccmnc=" + imsi.substring(0, 3 + mMncLength));
+                            MccTable.updateMccMncConfiguration(mContext,
+                                    imsi.substring(0, 3 + mMncLength), false);
+                        }
+                    }
+                    break;
+
+                case EVENT_GET_SPN_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    getSpnFsm(false, ar);
+                    break;
+
+                case EVENT_GET_CFF_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        mEfCff = null;
+                    } else {
+                        log("EF_CFF_CPHS: " + IccUtils.bytesToHexString(data));
+                        mEfCff = data;
+                    }
+
+                    break;
+
+                case EVENT_GET_SPDI_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        break;
+                    }
+
+                    parseEfSpdi(data);
+                    break;
+
+                case EVENT_UPDATE_DONE:
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception != null) {
+                        logw("update failed. ", ar.exception);
+                    }
+                    break;
+
+                case EVENT_GET_PNN_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        break;
+                    }
+
+                    SimTlv tlv = new SimTlv(data, 0, data.length);
+
+                    for (; tlv.isValidObject(); tlv.nextObject()) {
+                        if (tlv.getTag() == TAG_FULL_NETWORK_NAME) {
+                            mPnnHomeName = IccUtils.networkNameToString(
+                                    tlv.getData(), 0, tlv.getData().length);
+                            break;
+                        }
+                    }
+                    break;
+
+                case EVENT_GET_ALL_SMS_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception != null) {
+                        break;
+                    }
+
+                    handleSmses((ArrayList<byte []>) ar.result);
+                    break;
+
+                case EVENT_MARK_SMS_READ_DONE:
+                    Rlog.i("ENF", "marked read: sms " + msg.arg1);
+                    break;
+
+
+                case EVENT_SMS_ON_SIM:
+                    isRecordLoadResponse = false;
+
+                    ar = (AsyncResult) msg.obj;
+
+                    Integer index = (Integer) ar.result;
+
+                    if (ar.exception != null || index == null) {
+                        loge("Error on SMS_ON_SIM with exp "
+                                + ar.exception + " index " + index);
+                    } else {
+                        log("READ EF_SMS RECORD index=" + index);
+                        mFh.loadEFLinearFixed(EF_SMS, index, obtainMessage(EVENT_GET_SMS_DONE));
+                    }
+                    break;
+
+                case EVENT_GET_SMS_DONE:
+                    isRecordLoadResponse = false;
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception == null) {
+                        handleSms((byte[]) ar.result);
+                    } else {
+                        loge("Error on GET_SMS with exp " + ar.exception);
+                    }
+                    break;
+                case EVENT_GET_SST_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        break;
+                    }
+
+                    mUsimServiceTable = new UsimServiceTable(data);
+                    if (DBG) log("SST: " + mUsimServiceTable);
+                    break;
+
+                case EVENT_GET_INFO_CPHS_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception != null) {
+                        break;
+                    }
+
+                    mCphsInfo = (byte[]) ar.result;
+
+                    if (DBG) log("iCPHS: " + IccUtils.bytesToHexString(mCphsInfo));
+                    break;
+
+                case EVENT_SET_MBDN_DONE:
+                    isRecordLoadResponse = false;
+                    ar = (AsyncResult) msg.obj;
+
+                    if (DBG) log("EVENT_SET_MBDN_DONE ex:" + ar.exception);
+                    if (ar.exception == null) {
+                        mVoiceMailNum = mNewVoiceMailNum;
+                        mVoiceMailTag = mNewVoiceMailTag;
+                    }
+
+                    if (isCphsMailboxEnabled()) {
+                        adn = new AdnRecord(mVoiceMailTag, mVoiceMailNum);
+                        Message onCphsCompleted = (Message) ar.userObj;
+
+                        /* write to cphs mailbox whenever it is available but
+                        * we only need notify caller once if both updating are
+                        * successful.
+                        *
+                        * so if set_mbdn successful, notify caller here and set
+                        * onCphsCompleted to null
+                        */
+                        if (ar.exception == null && ar.userObj != null) {
+                            AsyncResult.forMessage(((Message) ar.userObj)).exception = null;
+                            ((Message) ar.userObj).sendToTarget();
+
+                            if (DBG) log("Callback with MBDN successful.");
+
+                            onCphsCompleted = null;
+                        }
+
+                        new AdnRecordLoader(mFh)
+                                .updateEF(adn, EF_MAILBOX_CPHS, EF_EXT1, 1, null,
+                                obtainMessage(EVENT_SET_CPHS_MAILBOX_DONE,
+                                        onCphsCompleted));
+                    } else {
+                        if (ar.userObj != null) {
+                            CarrierConfigManager configLoader = (CarrierConfigManager)
+                                    mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+                            if (ar.exception != null && configLoader != null
+                                    && configLoader.getConfig().getBoolean(
+                                    CarrierConfigManager.KEY_EDITABLE_VOICEMAIL_NUMBER_BOOL)) {
+                                // GsmCdmaPhone will store vm number on device
+                                // when IccVmNotSupportedException occurred
+                                AsyncResult.forMessage(((Message) ar.userObj)).exception =
+                                        new IccVmNotSupportedException(
+                                            "Update SIM voice mailbox error");
+                            } else {
+                                AsyncResult.forMessage(((Message) ar.userObj))
+                                    .exception = ar.exception;
+                            }
+                            ((Message) ar.userObj).sendToTarget();
+                        }
+                    }
+                    break;
+                case EVENT_SET_CPHS_MAILBOX_DONE:
+                    isRecordLoadResponse = false;
+                    ar = (AsyncResult) msg.obj;
+                    if (ar.exception == null) {
+                        mVoiceMailNum = mNewVoiceMailNum;
+                        mVoiceMailTag = mNewVoiceMailTag;
+                    } else {
+                        if (DBG) log("Set CPHS MailBox with exception: " + ar.exception);
+                    }
+                    if (ar.userObj != null) {
+                        if (DBG) log("Callback with CPHS MB successful.");
+                        AsyncResult.forMessage(((Message) ar.userObj)).exception
+                                = ar.exception;
+                        ((Message) ar.userObj).sendToTarget();
+                    }
+                    break;
+                case EVENT_SIM_REFRESH:
+                    isRecordLoadResponse = false;
+                    ar = (AsyncResult) msg.obj;
+                    if (DBG) log("Sim REFRESH with exception: " + ar.exception);
+                    if (ar.exception == null) {
+                        handleSimRefresh((IccRefreshResponse) ar.result);
+                    }
+                    break;
+                case EVENT_GET_CFIS_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        mEfCfis = null;
+                    } else {
+                        log("EF_CFIS: " + IccUtils.bytesToHexString(data));
+                        mEfCfis = data;
+                    }
+
+                    break;
+
+                case EVENT_GET_CSP_CPHS_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+
+                    if (ar.exception != null) {
+                        loge("Exception in fetching EF_CSP data " + ar.exception);
+                        break;
+                    }
+
+                    data = (byte[]) ar.result;
+
+                    log("EF_CSP: " + IccUtils.bytesToHexString(data));
+                    handleEfCspData(data);
+                    break;
+
+                case EVENT_GET_GID1_DONE:
+                    isRecordLoadResponse = true;
+
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        loge("Exception in get GID1 " + ar.exception);
+                        mGid1 = null;
+                        break;
+                    }
+
+                    mGid1 = IccUtils.bytesToHexString(data);
+
+                    log("GID1: " + mGid1);
+
+                    break;
+
+                case EVENT_GET_GID2_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null) {
+                        loge("Exception in get GID2 " + ar.exception);
+                        mGid2 = null;
+                        break;
+                    }
+
+                    mGid2 = IccUtils.bytesToHexString(data);
+
+                    log("GID2: " + mGid2);
+
+                    break;
+
+                case EVENT_GET_PLMN_W_ACT_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null || data == null) {
+                        loge("Failed getting User PLMN with Access Tech Records: " + ar.exception);
+                        break;
+                    } else {
+                        log("Received a PlmnActRecord, raw=" + IccUtils.bytesToHexString(data));
+                        mPlmnActRecords = PlmnActRecord.getRecords(data);
+                        if (VDBG) log("PlmnActRecords=" + Arrays.toString(mPlmnActRecords));
+                    }
+                    break;
+
+                case EVENT_GET_OPLMN_W_ACT_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null || data == null) {
+                        loge("Failed getting Operator PLMN with Access Tech Records: "
+                                + ar.exception);
+                        break;
+                    } else {
+                        log("Received a PlmnActRecord, raw=" + IccUtils.bytesToHexString(data));
+                        mOplmnActRecords = PlmnActRecord.getRecords(data);
+                        if (VDBG) log("OplmnActRecord[]=" + Arrays.toString(mOplmnActRecords));
+                    }
+                    break;
+
+                case EVENT_GET_HPLMN_W_ACT_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+
+                    if (ar.exception != null || data == null) {
+                        loge("Failed getting Home PLMN with Access Tech Records: " + ar.exception);
+                        break;
+                    } else {
+                        log("Received a PlmnActRecord, raw=" + IccUtils.bytesToHexString(data));
+                        mHplmnActRecords = PlmnActRecord.getRecords(data);
+                        log("HplmnActRecord[]=" + Arrays.toString(mHplmnActRecords));
+                    }
+                    break;
+
+                case EVENT_GET_EHPLMN_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+                    if (ar.exception != null || data == null) {
+                        loge("Failed getting Equivalent Home PLMNs: " + ar.exception);
+                        break;
+                    } else {
+                        mEhplmns = parseBcdPlmnList(data, "Equivalent Home");
+                    }
+                    break;
+
+                case EVENT_GET_FPLMN_DONE:
+                    isRecordLoadResponse = true;
+                    ar = (AsyncResult) msg.obj;
+                    data = (byte[]) ar.result;
+                    if (ar.exception != null || data == null) {
+                        loge("Failed getting Forbidden PLMNs: " + ar.exception);
+                        break;
+                    } else {
+                        mFplmns = parseBcdPlmnList(data, "Forbidden");
+                    }
+                    if (msg.arg1 == HANDLER_ACTION_SEND_RESPONSE) {
+                        if (VDBG) logv("getForbiddenPlmns(): send async response");
+                        isRecordLoadResponse = false;
+                        Message response = retrievePendingResponseMessage(msg.arg2);
+                        if (response != null) {
+                            AsyncResult.forMessage(
+                                    response, Arrays.copyOf(mFplmns, mFplmns.length), null);
+                            response.sendToTarget();
+                        } else {
+                            loge("Failed to retrieve a response message for FPLMN");
+                            break;
+                        }
+                    }
+                    break;
+
+                case EVENT_CARRIER_CONFIG_CHANGED:
+                    handleCarrierNameOverride();
+                    break;
+
+                default:
+                    super.handleMessage(msg);   // IccRecords handles generic record load responses
+            }
+        } catch (RuntimeException exc) {
+            // I don't want these exceptions to be fatal
+            logw("Exception parsing SIM record", exc);
+        } finally {
+            // Count up record load responses even if they are fails
+            if (isRecordLoadResponse) {
+                onRecordLoaded();
+            }
+        }
+    }
+
+    private class EfPlLoaded implements IccRecordLoaded {
+        public String getEfName() {
+            return "EF_PL";
+        }
+
+        public void onRecordLoaded(AsyncResult ar) {
+            mEfPl = (byte[]) ar.result;
+            if (DBG) log("EF_PL=" + IccUtils.bytesToHexString(mEfPl));
+        }
+    }
+
+    private class EfUsimLiLoaded implements IccRecordLoaded {
+        public String getEfName() {
+            return "EF_LI";
+        }
+
+        public void onRecordLoaded(AsyncResult ar) {
+            mEfLi = (byte[]) ar.result;
+            if (DBG) log("EF_LI=" + IccUtils.bytesToHexString(mEfLi));
+        }
+    }
+
+    private void handleFileUpdate(int efid) {
+        switch(efid) {
+            case EF_MBDN:
+                mRecordsToLoad++;
+                new AdnRecordLoader(mFh).loadFromEF(EF_MBDN, EF_EXT6,
+                        mMailboxIndex, obtainMessage(EVENT_GET_MBDN_DONE));
+                break;
+            case EF_MAILBOX_CPHS:
+                mRecordsToLoad++;
+                new AdnRecordLoader(mFh).loadFromEF(EF_MAILBOX_CPHS, EF_EXT1,
+                        1, obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE));
+                break;
+            case EF_CSP_CPHS:
+                mRecordsToLoad++;
+                log("[CSP] SIM Refresh for EF_CSP_CPHS");
+                mFh.loadEFTransparent(EF_CSP_CPHS,
+                        obtainMessage(EVENT_GET_CSP_CPHS_DONE));
+                break;
+            case EF_FDN:
+                if (DBG) log("SIM Refresh called for EF_FDN");
+                mParentApp.queryFdn();
+                mAdnCache.reset();
+                break;
+            case EF_MSISDN:
+                mRecordsToLoad++;
+                log("SIM Refresh called for EF_MSISDN");
+                new AdnRecordLoader(mFh).loadFromEF(EF_MSISDN, getExtFromEf(EF_MSISDN), 1,
+                        obtainMessage(EVENT_GET_MSISDN_DONE));
+                break;
+            case EF_CFIS:
+            case EF_CFF_CPHS:
+                log("SIM Refresh called for EF_CFIS or EF_CFF_CPHS");
+                loadCallForwardingRecords();
+                break;
+            default:
+                // For now, fetch all records if this is not a
+                // voicemail number.
+                // TODO: Handle other cases, instead of fetching all.
+                mAdnCache.reset();
+                fetchSimRecords();
+                break;
+        }
+    }
+
+    private void handleSimRefresh(IccRefreshResponse refreshResponse){
+        if (refreshResponse == null) {
+            if (DBG) log("handleSimRefresh received without input");
+            return;
+        }
+
+        if (!TextUtils.isEmpty(refreshResponse.aid)
+                && !refreshResponse.aid.equals(mParentApp.getAid())) {
+            // This is for different app. Ignore.
+            return;
+        }
+
+        switch (refreshResponse.refreshResult) {
+            case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
+                if (DBG) log("handleSimRefresh with SIM_FILE_UPDATED");
+                handleFileUpdate(refreshResponse.efId);
+                break;
+            case IccRefreshResponse.REFRESH_RESULT_INIT:
+                if (DBG) log("handleSimRefresh with SIM_REFRESH_INIT");
+                // need to reload all files (that we care about)
+                onIccRefreshInit();
+                break;
+            case IccRefreshResponse.REFRESH_RESULT_RESET:
+                // Refresh reset is handled by the UiccCard object.
+                if (DBG) log("handleSimRefresh with SIM_REFRESH_RESET");
+                break;
+            default:
+                // unknown refresh operation
+                if (DBG) log("handleSimRefresh with unknown operation");
+                break;
+        }
+    }
+
+    /**
+     * Dispatch 3GPP format message to registrant ({@code GsmCdmaPhone}) to pass to the 3GPP SMS
+     * dispatcher for delivery.
+     */
+    private int dispatchGsmMessage(SmsMessage message) {
+        mNewSmsRegistrants.notifyResult(message);
+        return 0;
+    }
+
+    private void handleSms(byte[] ba) {
+        if (ba[0] != 0)
+            Rlog.d("ENF", "status : " + ba[0]);
+
+        // 3GPP TS 51.011 v5.0.0 (20011-12)  10.5.3
+        // 3 == "received by MS from network; message to be read"
+        if (ba[0] == 3) {
+            int n = ba.length;
+
+            // Note: Data may include trailing FF's.  That's OK; message
+            // should still parse correctly.
+            byte[] pdu = new byte[n - 1];
+            System.arraycopy(ba, 1, pdu, 0, n - 1);
+            SmsMessage message = SmsMessage.createFromPdu(pdu, SmsConstants.FORMAT_3GPP);
+
+            dispatchGsmMessage(message);
+        }
+    }
+
+
+    private void handleSmses(ArrayList<byte[]> messages) {
+        int count = messages.size();
+
+        for (int i = 0; i < count; i++) {
+            byte[] ba = messages.get(i);
+
+            if (ba[0] != 0)
+                Rlog.i("ENF", "status " + i + ": " + ba[0]);
+
+            // 3GPP TS 51.011 v5.0.0 (20011-12)  10.5.3
+            // 3 == "received by MS from network; message to be read"
+
+            if (ba[0] == 3) {
+                int n = ba.length;
+
+                // Note: Data may include trailing FF's.  That's OK; message
+                // should still parse correctly.
+                byte[] pdu = new byte[n - 1];
+                System.arraycopy(ba, 1, pdu, 0, n - 1);
+                SmsMessage message = SmsMessage.createFromPdu(pdu, SmsConstants.FORMAT_3GPP);
+
+                dispatchGsmMessage(message);
+
+                // 3GPP TS 51.011 v5.0.0 (20011-12)  10.5.3
+                // 1 == "received by MS from network; message read"
+
+                ba[0] = 1;
+
+                if (false) { // FIXME: writing seems to crash RdoServD
+                    mFh.updateEFLinearFixed(EF_SMS,
+                            i, ba, null, obtainMessage(EVENT_MARK_SMS_READ_DONE, i));
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onRecordLoaded() {
+        // One record loaded successfully or failed, In either case
+        // we need to update the recordsToLoad count
+        mRecordsToLoad -= 1;
+        if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
+
+        if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+            onAllRecordsLoaded();
+        } else if (mRecordsToLoad < 0) {
+            loge("recordsToLoad <0, programmer error suspected");
+            mRecordsToLoad = 0;
+        }
+    }
+
+    private void setVoiceCallForwardingFlagFromSimRecords() {
+        if (validEfCfis(mEfCfis)) {
+            // Refer TS 51.011 Section 10.3.46 for the content description
+            mCallForwardingStatus = (mEfCfis[1] & 0x01);
+            log("EF_CFIS: callForwardingEnabled=" + mCallForwardingStatus);
+        } else if (mEfCff != null) {
+            mCallForwardingStatus =
+                    ((mEfCff[0] & CFF_LINE1_MASK) == CFF_UNCONDITIONAL_ACTIVE) ?
+                            CALL_FORWARDING_STATUS_ENABLED : CALL_FORWARDING_STATUS_DISABLED;
+            log("EF_CFF: callForwardingEnabled=" + mCallForwardingStatus);
+        } else {
+            mCallForwardingStatus = CALL_FORWARDING_STATUS_UNKNOWN;
+            log("EF_CFIS and EF_CFF not valid. callForwardingEnabled=" + mCallForwardingStatus);
+        }
+    }
+
+    @Override
+    protected void onAllRecordsLoaded() {
+        if (DBG) log("record load complete");
+
+        Resources resource = Resources.getSystem();
+        if (resource.getBoolean(com.android.internal.R.bool.config_use_sim_language_file)) {
+            setSimLanguage(mEfLi, mEfPl);
+        } else {
+            if (DBG) log ("Not using EF LI/EF PL");
+        }
+
+        setVoiceCallForwardingFlagFromSimRecords();
+
+        if (mParentApp.getState() == AppState.APPSTATE_PIN ||
+               mParentApp.getState() == AppState.APPSTATE_PUK) {
+            // reset recordsRequested, since sim is not loaded really
+            mRecordsRequested = false;
+            // lock state, only update language
+            return ;
+        }
+
+        // Some fields require more than one SIM record to set
+
+        String operator = getOperatorNumeric();
+        if (!TextUtils.isEmpty(operator)) {
+            log("onAllRecordsLoaded set 'gsm.sim.operator.numeric' to operator='" +
+                    operator + "'");
+            mTelephonyManager.setSimOperatorNumericForPhone(
+                    mParentApp.getPhoneId(), operator);
+        } else {
+            log("onAllRecordsLoaded empty 'gsm.sim.operator.numeric' skipping");
+        }
+
+        String imsi = getIMSI();
+
+        if (!TextUtils.isEmpty(imsi) && imsi.length() >= 3) {
+            log("onAllRecordsLoaded set mcc imsi" + (VDBG ? ("=" + imsi) : ""));
+            mTelephonyManager.setSimCountryIsoForPhone(
+                    mParentApp.getPhoneId(), MccTable.countryCodeForMcc(
+                    Integer.parseInt(imsi.substring(0, 3))));
+        } else {
+            log("onAllRecordsLoaded empty imsi skipping setting mcc");
+        }
+
+        setVoiceMailByCountry(operator);
+
+        mRecordsLoadedRegistrants.notifyRegistrants(
+            new AsyncResult(null, null, null));
+    }
+
+    //***** Private methods
+
+    private void handleCarrierNameOverride() {
+        CarrierConfigManager configLoader = (CarrierConfigManager)
+                mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configLoader != null && configLoader.getConfig().getBoolean(
+                CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL)) {
+            String carrierName = configLoader.getConfig().getString(
+                    CarrierConfigManager.KEY_CARRIER_NAME_STRING);
+            setServiceProviderName(carrierName);
+            mTelephonyManager.setSimOperatorNameForPhone(mParentApp.getPhoneId(),
+                    carrierName);
+        } else {
+            setSpnFromConfig(getOperatorNumeric());
+        }
+    }
+
+    private void setSpnFromConfig(String carrier) {
+        if (mSpnOverride.containsCarrier(carrier)) {
+            setServiceProviderName(mSpnOverride.getSpn(carrier));
+            mTelephonyManager.setSimOperatorNameForPhone(
+                    mParentApp.getPhoneId(), getServiceProviderName());
+        }
+    }
+
+
+    private void setVoiceMailByCountry (String spn) {
+        if (mVmConfig.containsCarrier(spn)) {
+            mIsVoiceMailFixed = true;
+            mVoiceMailNum = mVmConfig.getVoiceMailNumber(spn);
+            mVoiceMailTag = mVmConfig.getVoiceMailTag(spn);
+        }
+    }
+
+    /**
+     * String[] of forbidden PLMNs will be sent to the Message's handler
+     * in the result field of an AsyncResult in the response.obj.
+     */
+    public void getForbiddenPlmns(Message response) {
+        int key = storePendingResponseMessage(response);
+        mFh.loadEFTransparent(EF_FPLMN, obtainMessage(
+                    EVENT_GET_FPLMN_DONE, HANDLER_ACTION_SEND_RESPONSE, key));
+    }
+
+    @Override
+    public void onReady() {
+        fetchSimRecords();
+    }
+
+    private void onLocked() {
+        if (DBG) log("only fetch EF_LI and EF_PL in lock state");
+        loadEfLiAndEfPl();
+    }
+
+    private void loadEfLiAndEfPl() {
+        if (mParentApp.getType() == AppType.APPTYPE_USIM) {
+            mRecordsRequested = true;
+            mFh.loadEFTransparent(EF_LI,
+                    obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfUsimLiLoaded()));
+            mRecordsToLoad++;
+
+            mFh.loadEFTransparent(EF_PL,
+                    obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfPlLoaded()));
+            mRecordsToLoad++;
+        }
+    }
+
+    private void loadCallForwardingRecords() {
+        mRecordsRequested = true;
+        mFh.loadEFLinearFixed(EF_CFIS, 1, obtainMessage(EVENT_GET_CFIS_DONE));
+        mRecordsToLoad++;
+        mFh.loadEFTransparent(EF_CFF_CPHS, obtainMessage(EVENT_GET_CFF_DONE));
+        mRecordsToLoad++;
+    }
+
+    protected void fetchSimRecords() {
+        mRecordsRequested = true;
+
+        if (DBG) log("fetchSimRecords " + mRecordsToLoad);
+
+        mCi.getIMSIForApp(mParentApp.getAid(), obtainMessage(EVENT_GET_IMSI_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE));
+        mRecordsToLoad++;
+
+        // FIXME should examine EF[MSISDN]'s capability configuration
+        // to determine which is the voice/data/fax line
+        new AdnRecordLoader(mFh).loadFromEF(EF_MSISDN, getExtFromEf(EF_MSISDN), 1,
+                    obtainMessage(EVENT_GET_MSISDN_DONE));
+        mRecordsToLoad++;
+
+        // Record number is subscriber profile
+        mFh.loadEFLinearFixed(EF_MBI, 1, obtainMessage(EVENT_GET_MBI_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_AD, obtainMessage(EVENT_GET_AD_DONE));
+        mRecordsToLoad++;
+
+        // Record number is subscriber profile
+        mFh.loadEFLinearFixed(EF_MWIS, 1, obtainMessage(EVENT_GET_MWIS_DONE));
+        mRecordsToLoad++;
+
+
+        // Also load CPHS-style voice mail indicator, which stores
+        // the same info as EF[MWIS]. If both exist, both are updated
+        // but the EF[MWIS] data is preferred
+        // Please note this must be loaded after EF[MWIS]
+        mFh.loadEFTransparent(
+                EF_VOICE_MAIL_INDICATOR_CPHS,
+                obtainMessage(EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE));
+        mRecordsToLoad++;
+
+        // Same goes for Call Forward Status indicator: fetch both
+        // EF[CFIS] and CPHS-EF, with EF[CFIS] preferred.
+        loadCallForwardingRecords();
+
+        getSpnFsm(true, null);
+
+        mFh.loadEFTransparent(EF_SPDI, obtainMessage(EVENT_GET_SPDI_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFLinearFixed(EF_PNN, 1, obtainMessage(EVENT_GET_PNN_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_SST, obtainMessage(EVENT_GET_SST_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_INFO_CPHS, obtainMessage(EVENT_GET_INFO_CPHS_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_CSP_CPHS,obtainMessage(EVENT_GET_CSP_CPHS_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_GID1, obtainMessage(EVENT_GET_GID1_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_GID2, obtainMessage(EVENT_GET_GID2_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_PLMN_W_ACT, obtainMessage(EVENT_GET_PLMN_W_ACT_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_OPLMN_W_ACT, obtainMessage(EVENT_GET_OPLMN_W_ACT_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_HPLMN_W_ACT, obtainMessage(EVENT_GET_HPLMN_W_ACT_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_EHPLMN, obtainMessage(EVENT_GET_EHPLMN_DONE));
+        mRecordsToLoad++;
+
+        mFh.loadEFTransparent(EF_FPLMN, obtainMessage(
+                    EVENT_GET_FPLMN_DONE, HANDLER_ACTION_NONE, -1));
+        mRecordsToLoad++;
+
+        loadEfLiAndEfPl();
+
+        // XXX should seek instead of examining them all
+        if (false) { // XXX
+            mFh.loadEFLinearFixedAll(EF_SMS, obtainMessage(EVENT_GET_ALL_SMS_DONE));
+            mRecordsToLoad++;
+        }
+
+        if (CRASH_RIL) {
+            String sms = "0107912160130310f20404d0110041007030208054832b0120"
+                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+                         + "ffffffffffffffffffffffffffffff";
+            byte[] ba = IccUtils.hexStringToBytes(sms);
+
+            mFh.updateEFLinearFixed(EF_SMS, 1, ba, null,
+                            obtainMessage(EVENT_MARK_SMS_READ_DONE, 1));
+        }
+        if (DBG) log("fetchSimRecords " + mRecordsToLoad + " requested: " + mRecordsRequested);
+    }
+
+    /**
+     * Returns the SpnDisplayRule based on settings on the SIM and the
+     * specified plmn (currently-registered PLMN).  See TS 22.101 Annex A
+     * and TS 51.011 10.3.11 for details.
+     *
+     * If the SPN is not found on the SIM or is empty, the rule is
+     * always PLMN_ONLY.
+     */
+    @Override
+    public int getDisplayRule(String plmn) {
+        int rule;
+
+        if (mParentApp != null && mParentApp.getUiccCard() != null &&
+            mParentApp.getUiccCard().getOperatorBrandOverride() != null) {
+        // If the operator has been overridden, treat it as the SPN file on the SIM did not exist.
+            rule = SPN_RULE_SHOW_PLMN;
+        } else if (TextUtils.isEmpty(getServiceProviderName()) || mSpnDisplayCondition == -1) {
+            // No EF_SPN content was found on the SIM, or not yet loaded.  Just show ONS.
+            rule = SPN_RULE_SHOW_PLMN;
+        } else if (isOnMatchingPlmn(plmn)) {
+            rule = SPN_RULE_SHOW_SPN;
+            if ((mSpnDisplayCondition & 0x01) == 0x01) {
+                // ONS required when registered to HPLMN or PLMN in EF_SPDI
+                rule |= SPN_RULE_SHOW_PLMN;
+            }
+        } else {
+            rule = SPN_RULE_SHOW_PLMN;
+            if ((mSpnDisplayCondition & 0x02) == 0x00) {
+                // SPN required if not registered to HPLMN or PLMN in EF_SPDI
+                rule |= SPN_RULE_SHOW_SPN;
+            }
+        }
+        return rule;
+    }
+
+    /**
+     * Checks if plmn is HPLMN or on the spdiNetworks list.
+     */
+    private boolean isOnMatchingPlmn(String plmn) {
+        if (plmn == null) return false;
+
+        if (plmn.equals(getOperatorNumeric())) {
+            return true;
+        }
+
+        if (mSpdiNetworks != null) {
+            for (String spdiNet : mSpdiNetworks) {
+                if (plmn.equals(spdiNet)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * States of Get SPN Finite State Machine which only used by getSpnFsm()
+     */
+    private enum GetSpnFsmState {
+        IDLE,               // No initialized
+        INIT,               // Start FSM
+        READ_SPN_3GPP,      // Load EF_SPN firstly
+        READ_SPN_CPHS,      // Load EF_SPN_CPHS secondly
+        READ_SPN_SHORT_CPHS // Load EF_SPN_SHORT_CPHS last
+    }
+
+    /**
+     * Finite State Machine to load Service Provider Name , which can be stored
+     * in either EF_SPN (3GPP), EF_SPN_CPHS, or EF_SPN_SHORT_CPHS (CPHS4.2)
+     *
+     * After starting, FSM will search SPN EFs in order and stop after finding
+     * the first valid SPN
+     *
+     * If the FSM gets restart while waiting for one of
+     * SPN EFs results (i.e. a SIM refresh occurs after issuing
+     * read EF_CPHS_SPN), it will re-initialize only after
+     * receiving and discarding the unfinished SPN EF result.
+     *
+     * @param start set true only for initialize loading
+     * @param ar the AsyncResult from loadEFTransparent
+     *        ar.exception holds exception in error
+     *        ar.result is byte[] for data in success
+     */
+    private void getSpnFsm(boolean start, AsyncResult ar) {
+        byte[] data;
+
+        if (start) {
+            // Check previous state to see if there is outstanding
+            // SPN read
+            if (mSpnState == GetSpnFsmState.READ_SPN_3GPP
+                    || mSpnState == GetSpnFsmState.READ_SPN_CPHS
+                    || mSpnState == GetSpnFsmState.READ_SPN_SHORT_CPHS
+                    || mSpnState == GetSpnFsmState.INIT) {
+                // Set INIT then return so the INIT code
+                // will run when the outstanding read done.
+                mSpnState = GetSpnFsmState.INIT;
+                return;
+            } else {
+                mSpnState = GetSpnFsmState.INIT;
+            }
+        }
+
+        switch(mSpnState){
+            case INIT:
+                setServiceProviderName(null);
+
+                mFh.loadEFTransparent(EF_SPN,
+                        obtainMessage(EVENT_GET_SPN_DONE));
+                mRecordsToLoad++;
+
+                mSpnState = GetSpnFsmState.READ_SPN_3GPP;
+                break;
+            case READ_SPN_3GPP:
+                if (ar != null && ar.exception == null) {
+                    data = (byte[]) ar.result;
+                    mSpnDisplayCondition = 0xff & data[0];
+
+                    setServiceProviderName(IccUtils.adnStringFieldToString(
+                                data, 1, data.length - 1));
+                    // for card double-check and brand override
+                    // we have to do this:
+                    final String spn = getServiceProviderName();
+
+                    if (spn == null || spn.length() == 0) {
+                        mSpnState = GetSpnFsmState.READ_SPN_CPHS;
+                    } else {
+                        if (DBG) log("Load EF_SPN: " + spn
+                                + " spnDisplayCondition: " + mSpnDisplayCondition);
+                        mTelephonyManager.setSimOperatorNameForPhone(
+                                mParentApp.getPhoneId(), spn);
+
+                        mSpnState = GetSpnFsmState.IDLE;
+                    }
+                } else {
+                    mSpnState = GetSpnFsmState.READ_SPN_CPHS;
+                }
+
+                if (mSpnState == GetSpnFsmState.READ_SPN_CPHS) {
+                    mFh.loadEFTransparent( EF_SPN_CPHS,
+                            obtainMessage(EVENT_GET_SPN_DONE));
+                    mRecordsToLoad++;
+
+                    // See TS 51.011 10.3.11.  Basically, default to
+                    // show PLMN always, and SPN also if roaming.
+                    mSpnDisplayCondition = -1;
+                }
+                break;
+            case READ_SPN_CPHS:
+                if (ar != null && ar.exception == null) {
+                    data = (byte[]) ar.result;
+
+                    setServiceProviderName(IccUtils.adnStringFieldToString(
+                                data, 0, data.length));
+                    // for card double-check and brand override
+                    // we have to do this:
+                    final String spn = getServiceProviderName();
+
+                    if (spn == null || spn.length() == 0) {
+                        mSpnState = GetSpnFsmState.READ_SPN_SHORT_CPHS;
+                    } else {
+                        // Display CPHS Operator Name only when not roaming
+                        mSpnDisplayCondition = 2;
+
+                        if (DBG) log("Load EF_SPN_CPHS: " + spn);
+                        mTelephonyManager.setSimOperatorNameForPhone(
+                                mParentApp.getPhoneId(), spn);
+
+                        mSpnState = GetSpnFsmState.IDLE;
+                    }
+                } else {
+                    mSpnState = GetSpnFsmState.READ_SPN_SHORT_CPHS;
+                }
+
+                if (mSpnState == GetSpnFsmState.READ_SPN_SHORT_CPHS) {
+                    mFh.loadEFTransparent(
+                            EF_SPN_SHORT_CPHS, obtainMessage(EVENT_GET_SPN_DONE));
+                    mRecordsToLoad++;
+                }
+                break;
+            case READ_SPN_SHORT_CPHS:
+                if (ar != null && ar.exception == null) {
+                    data = (byte[]) ar.result;
+
+                    setServiceProviderName(IccUtils.adnStringFieldToString(
+                                data, 0, data.length));
+                    // for card double-check and brand override
+                    // we have to do this:
+                    final String spn = getServiceProviderName();
+
+                    if (spn == null || spn.length() == 0) {
+                        if (DBG) log("No SPN loaded in either CHPS or 3GPP");
+                    } else {
+                        // Display CPHS Operator Name only when not roaming
+                        mSpnDisplayCondition = 2;
+
+                        if (DBG) log("Load EF_SPN_SHORT_CPHS: " + spn);
+                        mTelephonyManager.setSimOperatorNameForPhone(
+                                mParentApp.getPhoneId(), spn);
+                    }
+                } else {
+                    setServiceProviderName(null);
+                    if (DBG) log("No SPN loaded in either CHPS or 3GPP");
+                }
+
+                mSpnState = GetSpnFsmState.IDLE;
+                break;
+            default:
+                mSpnState = GetSpnFsmState.IDLE;
+        }
+    }
+
+    /**
+     * Parse TS 51.011 EF[SPDI] record
+     * This record contains the list of numeric network IDs that
+     * are treated specially when determining SPN display
+     */
+    private void
+    parseEfSpdi(byte[] data) {
+        SimTlv tlv = new SimTlv(data, 0, data.length);
+
+        byte[] plmnEntries = null;
+
+        for ( ; tlv.isValidObject() ; tlv.nextObject()) {
+            // Skip SPDI tag, if existant
+            if (tlv.getTag() == TAG_SPDI) {
+              tlv = new SimTlv(tlv.getData(), 0, tlv.getData().length);
+            }
+            // There should only be one TAG_SPDI_PLMN_LIST
+            if (tlv.getTag() == TAG_SPDI_PLMN_LIST) {
+                plmnEntries = tlv.getData();
+                break;
+            }
+        }
+
+        if (plmnEntries == null) {
+            return;
+        }
+
+        mSpdiNetworks = new ArrayList<String>(plmnEntries.length / 3);
+
+        for (int i = 0 ; i + 2 < plmnEntries.length ; i += 3) {
+            String plmnCode;
+            plmnCode = IccUtils.bcdPlmnToString(plmnEntries, i);
+
+            // Valid operator codes are 5 or 6 digits
+            if (plmnCode != null && plmnCode.length() >= 5) {
+                log("EF_SPDI network: " + plmnCode);
+                mSpdiNetworks.add(plmnCode);
+            }
+        }
+    }
+
+    /**
+     * convert a byte array of packed plmns to an array of strings
+     */
+    private String[] parseBcdPlmnList(byte[] data, String description) {
+        final int packedBcdPlmnLenBytes = 3;
+        log("Received " + description + " PLMNs, raw=" + IccUtils.bytesToHexString(data));
+        if (data.length == 0 || (data.length % packedBcdPlmnLenBytes) != 0) {
+            loge("Received invalid " + description + " PLMN list");
+            return null;
+        }
+        int numPlmns = data.length / packedBcdPlmnLenBytes;
+        String[] ret = new String[numPlmns];
+        for (int i = 0; i < numPlmns; i++) {
+            ret[i] = IccUtils.bcdPlmnToString(data, i * packedBcdPlmnLenBytes);
+        }
+        if (VDBG) logv(description + " PLMNs: " + Arrays.toString(ret));
+        return ret;
+    }
+
+    /**
+     * check to see if Mailbox Number is allocated and activated in CPHS SST
+     */
+    private boolean isCphsMailboxEnabled() {
+        if (mCphsInfo == null)  return false;
+        return ((mCphsInfo[1] & CPHS_SST_MBN_MASK) == CPHS_SST_MBN_ENABLED );
+    }
+
+    @Override
+    protected void log(String s) {
+        Rlog.d(LOG_TAG, "[SIMRecords] " + s);
+    }
+
+    @Override
+    protected void loge(String s) {
+        Rlog.e(LOG_TAG, "[SIMRecords] " + s);
+    }
+
+    protected void logw(String s, Throwable tr) {
+        Rlog.w(LOG_TAG, "[SIMRecords] " + s, tr);
+    }
+
+    protected void logv(String s) {
+        Rlog.v(LOG_TAG, "[SIMRecords] " + s);
+    }
+
+    /**
+     * Return true if "Restriction of menu options for manual PLMN selection"
+     * bit is set or EF_CSP data is unavailable, return false otherwise.
+     */
+    @Override
+    public boolean isCspPlmnEnabled() {
+        return mCspPlmnEnabled;
+    }
+
+    /**
+     * Parse EF_CSP data and check if
+     * "Restriction of menu options for manual PLMN selection" is
+     * Enabled/Disabled
+     *
+     * @param data EF_CSP hex data.
+     */
+    private void handleEfCspData(byte[] data) {
+        // As per spec CPHS4_2.WW6, CPHS B.4.7.1, EF_CSP contains CPHS defined
+        // 18 bytes (i.e 9 service groups info) and additional data specific to
+        // operator. The valueAddedServicesGroup is not part of standard
+        // services. This is operator specific and can be programmed any where.
+        // Normally this is programmed as 10th service after the standard
+        // services.
+        int usedCspGroups = data.length / 2;
+        // This is the "Service Group Number" of "Value Added Services Group".
+        byte valueAddedServicesGroup = (byte)0xC0;
+
+        mCspPlmnEnabled = true;
+        for (int i = 0; i < usedCspGroups; i++) {
+             if (data[2 * i] == valueAddedServicesGroup) {
+                 log("[CSP] found ValueAddedServicesGroup, value " + data[(2 * i) + 1]);
+                 if ((data[(2 * i) + 1] & 0x80) == 0x80) {
+                     // Bit 8 is for
+                     // "Restriction of menu options for manual PLMN selection".
+                     // Operator Selection menu should be enabled.
+                     mCspPlmnEnabled = true;
+                 } else {
+                     mCspPlmnEnabled = false;
+                     // Operator Selection menu should be disabled.
+                     // Operator Selection Mode should be set to Automatic.
+                     log("[CSP] Set Automatic Network Selection");
+                     mNetworkSelectionModeAutomaticRegistrants.notifyRegistrants();
+                 }
+                 return;
+             }
+        }
+
+        log("[CSP] Value Added Service Group (0xC0), not found!");
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("SIMRecords: " + this);
+        pw.println(" extends:");
+        super.dump(fd, pw, args);
+        pw.println(" mVmConfig=" + mVmConfig);
+        pw.println(" mSpnOverride=" + mSpnOverride);
+        pw.println(" mCallForwardingStatus=" + mCallForwardingStatus);
+        pw.println(" mSpnState=" + mSpnState);
+        pw.println(" mCphsInfo=" + mCphsInfo);
+        pw.println(" mCspPlmnEnabled=" + mCspPlmnEnabled);
+        pw.println(" mEfMWIS[]=" + Arrays.toString(mEfMWIS));
+        pw.println(" mEfCPHS_MWI[]=" + Arrays.toString(mEfCPHS_MWI));
+        pw.println(" mEfCff[]=" + Arrays.toString(mEfCff));
+        pw.println(" mEfCfis[]=" + Arrays.toString(mEfCfis));
+        pw.println(" mSpnDisplayCondition=" + mSpnDisplayCondition);
+        pw.println(" mSpdiNetworks[]=" + mSpdiNetworks);
+        pw.println(" mPnnHomeName=" + mPnnHomeName);
+        pw.println(" mUsimServiceTable=" + mUsimServiceTable);
+        pw.println(" mGid1=" + mGid1);
+        if (mCarrierTestOverride.isInTestMode()) {
+            pw.println(" mFakeGid1=" + ((mFakeGid1 != null) ? mFakeGid1 : "null"));
+        }
+        pw.println(" mGid2=" + mGid2);
+        if (mCarrierTestOverride.isInTestMode()) {
+            pw.println(" mFakeGid2=" + ((mFakeGid2 != null) ? mFakeGid2 : "null"));
+        }
+        pw.println(" mPlmnActRecords[]=" + Arrays.toString(mPlmnActRecords));
+        pw.println(" mOplmnActRecords[]=" + Arrays.toString(mOplmnActRecords));
+        pw.println(" mHplmnActRecords[]=" + Arrays.toString(mHplmnActRecords));
+        pw.println(" mFplmns[]=" + Arrays.toString(mFplmns));
+        pw.println(" mEhplmns[]=" + Arrays.toString(mEhplmns));
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/SpnOverride.java b/com/android/internal/telephony/uicc/SpnOverride.java
new file mode 100644
index 0000000..3a01af6
--- /dev/null
+++ b/com/android/internal/telephony/uicc/SpnOverride.java
@@ -0,0 +1,114 @@
+/*
+ * 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.internal.telephony.uicc;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.Environment;
+import android.telephony.Rlog;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+
+public class SpnOverride {
+    private HashMap<String, String> mCarrierSpnMap;
+
+
+    static final String LOG_TAG = "SpnOverride";
+    static final String PARTNER_SPN_OVERRIDE_PATH ="etc/spn-conf.xml";
+    static final String OEM_SPN_OVERRIDE_PATH = "telephony/spn-conf.xml";
+
+    SpnOverride () {
+        mCarrierSpnMap = new HashMap<String, String>();
+        loadSpnOverrides();
+    }
+
+    boolean containsCarrier(String carrier) {
+        return mCarrierSpnMap.containsKey(carrier);
+    }
+
+    String getSpn(String carrier) {
+        return mCarrierSpnMap.get(carrier);
+    }
+
+    private void loadSpnOverrides() {
+        FileReader spnReader;
+
+        File spnFile = new File(Environment.getRootDirectory(),
+                PARTNER_SPN_OVERRIDE_PATH);
+        File oemSpnFile = new File(Environment.getOemDirectory(),
+                OEM_SPN_OVERRIDE_PATH);
+
+        if (oemSpnFile.exists()) {
+            // OEM image exist SPN xml, get the timestamp from OEM & System image for comparison.
+            long oemSpnTime = oemSpnFile.lastModified();
+            long sysSpnTime = spnFile.lastModified();
+            Rlog.d(LOG_TAG, "SPN Timestamp: oemTime = " + oemSpnTime + " sysTime = " + sysSpnTime);
+
+            // To get the newer version of SPN from OEM image
+            if (oemSpnTime > sysSpnTime) {
+                Rlog.d(LOG_TAG, "SPN in OEM image is newer than System image");
+                spnFile = oemSpnFile;
+            }
+        } else {
+            // No SPN in OEM image, so load it from system image.
+            Rlog.d(LOG_TAG, "No SPN in OEM image = " + oemSpnFile.getPath() +
+                " Load SPN from system image");
+        }
+
+        try {
+            spnReader = new FileReader(spnFile);
+        } catch (FileNotFoundException e) {
+            Rlog.w(LOG_TAG, "Can not open " + spnFile.getAbsolutePath());
+            return;
+        }
+
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(spnReader);
+
+            XmlUtils.beginDocument(parser, "spnOverrides");
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+
+                String name = parser.getName();
+                if (!"spnOverride".equals(name)) {
+                    break;
+                }
+
+                String numeric = parser.getAttributeValue(null, "numeric");
+                String data    = parser.getAttributeValue(null, "spn");
+
+                mCarrierSpnMap.put(numeric, data);
+            }
+            spnReader.close();
+        } catch (XmlPullParserException e) {
+            Rlog.w(LOG_TAG, "Exception in spn-conf parser " + e);
+        } catch (IOException e) {
+            Rlog.w(LOG_TAG, "Exception in spn-conf parser " + e);
+        }
+    }
+
+}
diff --git a/com/android/internal/telephony/uicc/UiccCard.java b/com/android/internal/telephony/uicc/UiccCard.java
new file mode 100644
index 0000000..baad60b
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccCard.java
@@ -0,0 +1,839 @@
+/*
+ * Copyright (C) 2006, 2012 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.internal.telephony.uicc;
+
+import android.app.AlertDialog;
+import android.app.usage.UsageStatsManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.CommandsInterface.RadioState;
+import com.android.internal.telephony.cat.CatService;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
+import com.android.internal.telephony.uicc.IccCardStatus.PinState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public class UiccCard {
+    protected static final String LOG_TAG = "UiccCard";
+    protected static final boolean DBG = true;
+
+    public static final String EXTRA_ICC_CARD_ADDED =
+            "com.android.internal.telephony.uicc.ICC_CARD_ADDED";
+
+    private static final String OPERATOR_BRAND_OVERRIDE_PREFIX = "operator_branding_";
+
+    private final Object mLock = new Object();
+    private CardState mCardState;
+    private PinState mUniversalPinState;
+    private int mGsmUmtsSubscriptionAppIndex;
+    private int mCdmaSubscriptionAppIndex;
+    private int mImsSubscriptionAppIndex;
+    private UiccCardApplication[] mUiccApplications =
+            new UiccCardApplication[IccCardStatus.CARD_MAX_APPS];
+    private Context mContext;
+    private CommandsInterface mCi;
+    private CatService mCatService;
+    private RadioState mLastRadioState =  RadioState.RADIO_UNAVAILABLE;
+    private UiccCarrierPrivilegeRules mCarrierPrivilegeRules;
+
+    private RegistrantList mAbsentRegistrants = new RegistrantList();
+    private RegistrantList mCarrierPrivilegeRegistrants = new RegistrantList();
+
+    private static final int EVENT_CARD_REMOVED = 13;
+    private static final int EVENT_CARD_ADDED = 14;
+    private static final int EVENT_OPEN_LOGICAL_CHANNEL_DONE = 15;
+    private static final int EVENT_CLOSE_LOGICAL_CHANNEL_DONE = 16;
+    private static final int EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE = 17;
+    private static final int EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE = 18;
+    private static final int EVENT_SIM_IO_DONE = 19;
+    private static final int EVENT_CARRIER_PRIVILEGES_LOADED = 20;
+
+    private static final LocalLog mLocalLog = new LocalLog(100);
+
+    private final int mPhoneId;
+
+    public UiccCard(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId) {
+        if (DBG) log("Creating");
+        mCardState = ics.mCardState;
+        mPhoneId = phoneId;
+        update(c, ci, ics);
+    }
+
+    public void dispose() {
+        synchronized (mLock) {
+            if (DBG) log("Disposing card");
+            if (mCatService != null) mCatService.dispose();
+            for (UiccCardApplication app : mUiccApplications) {
+                if (app != null) {
+                    app.dispose();
+                }
+            }
+            mCatService = null;
+            mUiccApplications = null;
+            mCarrierPrivilegeRules = null;
+        }
+    }
+
+    public void update(Context c, CommandsInterface ci, IccCardStatus ics) {
+        synchronized (mLock) {
+            CardState oldState = mCardState;
+            mCardState = ics.mCardState;
+            mUniversalPinState = ics.mUniversalPinState;
+            mGsmUmtsSubscriptionAppIndex = ics.mGsmUmtsSubscriptionAppIndex;
+            mCdmaSubscriptionAppIndex = ics.mCdmaSubscriptionAppIndex;
+            mImsSubscriptionAppIndex = ics.mImsSubscriptionAppIndex;
+            mContext = c;
+            mCi = ci;
+
+            //update applications
+            if (DBG) log(ics.mApplications.length + " applications");
+            for ( int i = 0; i < mUiccApplications.length; i++) {
+                if (mUiccApplications[i] == null) {
+                    //Create newly added Applications
+                    if (i < ics.mApplications.length) {
+                        mUiccApplications[i] = new UiccCardApplication(this,
+                                ics.mApplications[i], mContext, mCi);
+                    }
+                } else if (i >= ics.mApplications.length) {
+                    //Delete removed applications
+                    mUiccApplications[i].dispose();
+                    mUiccApplications[i] = null;
+                } else {
+                    //Update the rest
+                    mUiccApplications[i].update(ics.mApplications[i], mContext, mCi);
+                }
+            }
+
+            createAndUpdateCatServiceLocked();
+
+            // Reload the carrier privilege rules if necessary.
+            log("Before privilege rules: " + mCarrierPrivilegeRules + " : " + mCardState);
+            if (mCarrierPrivilegeRules == null && mCardState == CardState.CARDSTATE_PRESENT) {
+                mCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(this,
+                        mHandler.obtainMessage(EVENT_CARRIER_PRIVILEGES_LOADED));
+            } else if (mCarrierPrivilegeRules != null
+                    && mCardState != CardState.CARDSTATE_PRESENT) {
+                mCarrierPrivilegeRules = null;
+            }
+
+            sanitizeApplicationIndexesLocked();
+
+            RadioState radioState = mCi.getRadioState();
+            if (DBG) log("update: radioState=" + radioState + " mLastRadioState="
+                    + mLastRadioState);
+            // No notifications while radio is off or we just powering up
+            if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
+                if (oldState != CardState.CARDSTATE_ABSENT &&
+                        mCardState == CardState.CARDSTATE_ABSENT) {
+                    if (DBG) log("update: notify card removed");
+                    mAbsentRegistrants.notifyRegistrants();
+                    mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_REMOVED, null));
+                } else if (oldState == CardState.CARDSTATE_ABSENT &&
+                        mCardState != CardState.CARDSTATE_ABSENT) {
+                    if (DBG) log("update: notify card added");
+                    mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_ADDED, null));
+                }
+            }
+            mLastRadioState = radioState;
+        }
+    }
+
+    private void createAndUpdateCatServiceLocked() {
+        if (mUiccApplications.length > 0 && mUiccApplications[0] != null) {
+            // Initialize or Reinitialize CatService
+            if (mCatService == null) {
+                mCatService = CatService.getInstance(mCi, mContext, this, mPhoneId);
+            } else {
+                mCatService.update(mCi, mContext, this);
+            }
+        } else {
+            if (mCatService != null) {
+                mCatService.dispose();
+            }
+            mCatService = null;
+        }
+    }
+
+    @Override
+    protected void finalize() {
+        if (DBG) log("UiccCard finalized");
+    }
+
+    /**
+     * This function makes sure that application indexes are valid
+     * and resets invalid indexes. (This should never happen, but in case
+     * RIL misbehaves we need to manage situation gracefully)
+     */
+    private void sanitizeApplicationIndexesLocked() {
+        mGsmUmtsSubscriptionAppIndex =
+                checkIndexLocked(
+                        mGsmUmtsSubscriptionAppIndex, AppType.APPTYPE_SIM, AppType.APPTYPE_USIM);
+        mCdmaSubscriptionAppIndex =
+                checkIndexLocked(
+                        mCdmaSubscriptionAppIndex, AppType.APPTYPE_RUIM, AppType.APPTYPE_CSIM);
+        mImsSubscriptionAppIndex =
+                checkIndexLocked(mImsSubscriptionAppIndex, AppType.APPTYPE_ISIM, null);
+    }
+
+    private int checkIndexLocked(int index, AppType expectedAppType, AppType altExpectedAppType) {
+        if (mUiccApplications == null || index >= mUiccApplications.length) {
+            loge("App index " + index + " is invalid since there are no applications");
+            return -1;
+        }
+
+        if (index < 0) {
+            // This is normal. (i.e. no application of this type)
+            return -1;
+        }
+
+        if (mUiccApplications[index].getType() != expectedAppType &&
+            mUiccApplications[index].getType() != altExpectedAppType) {
+            loge("App index " + index + " is invalid since it's not " +
+                    expectedAppType + " and not " + altExpectedAppType);
+            return -1;
+        }
+
+        // Seems to be valid
+        return index;
+    }
+
+    /**
+     * Notifies handler of any transition into State.ABSENT
+     */
+    public void registerForAbsent(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+
+            mAbsentRegistrants.add(r);
+
+            if (mCardState == CardState.CARDSTATE_ABSENT) {
+                r.notifyRegistrant();
+            }
+        }
+    }
+
+    public void unregisterForAbsent(Handler h) {
+        synchronized (mLock) {
+            mAbsentRegistrants.remove(h);
+        }
+    }
+
+    /**
+     * Notifies handler when carrier privilege rules are loaded.
+     */
+    public void registerForCarrierPrivilegeRulesLoaded(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+
+            mCarrierPrivilegeRegistrants.add(r);
+
+            if (areCarrierPriviligeRulesLoaded()) {
+                r.notifyRegistrant();
+            }
+        }
+    }
+
+    public void unregisterForCarrierPrivilegeRulesLoaded(Handler h) {
+        synchronized (mLock) {
+            mCarrierPrivilegeRegistrants.remove(h);
+        }
+    }
+
+    private void onIccSwap(boolean isAdded) {
+
+        boolean isHotSwapSupported = mContext.getResources().getBoolean(
+                R.bool.config_hotswapCapable);
+
+        if (isHotSwapSupported) {
+            log("onIccSwap: isHotSwapSupported is true, don't prompt for rebooting");
+            return;
+        }
+        log("onIccSwap: isHotSwapSupported is false, prompt for rebooting");
+
+        promptForRestart(isAdded);
+    }
+
+    private void promptForRestart(boolean isAdded) {
+        synchronized (mLock) {
+            final Resources res = mContext.getResources();
+            final String dialogComponent = res.getString(
+                    R.string.config_iccHotswapPromptForRestartDialogComponent);
+            if (dialogComponent != null) {
+                Intent intent = new Intent().setComponent(ComponentName.unflattenFromString(
+                        dialogComponent)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                        .putExtra(EXTRA_ICC_CARD_ADDED, isAdded);
+                try {
+                    mContext.startActivity(intent);
+                    return;
+                } catch (ActivityNotFoundException e) {
+                    loge("Unable to find ICC hotswap prompt for restart activity: " + e);
+                }
+            }
+
+            // TODO: Here we assume the device can't handle SIM hot-swap
+            //      and has to reboot. We may want to add a property,
+            //      e.g. REBOOT_ON_SIM_SWAP, to indicate if modem support
+            //      hot-swap.
+            DialogInterface.OnClickListener listener = null;
+
+
+            // TODO: SimRecords is not reset while SIM ABSENT (only reset while
+            //       Radio_off_or_not_available). Have to reset in both both
+            //       added or removed situation.
+            listener = new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    synchronized (mLock) {
+                        if (which == DialogInterface.BUTTON_POSITIVE) {
+                            if (DBG) log("Reboot due to SIM swap");
+                            PowerManager pm = (PowerManager) mContext
+                                    .getSystemService(Context.POWER_SERVICE);
+                            pm.reboot("SIM is added.");
+                        }
+                    }
+                }
+
+            };
+
+            Resources r = Resources.getSystem();
+
+            String title = (isAdded) ? r.getString(R.string.sim_added_title) :
+                r.getString(R.string.sim_removed_title);
+            String message = (isAdded) ? r.getString(R.string.sim_added_message) :
+                r.getString(R.string.sim_removed_message);
+            String buttonTxt = r.getString(R.string.sim_restart_button);
+
+            AlertDialog dialog = new AlertDialog.Builder(mContext)
+            .setTitle(title)
+            .setMessage(message)
+            .setPositiveButton(buttonTxt, listener)
+            .create();
+            dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+            dialog.show();
+        }
+    }
+
+    protected Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg){
+            switch (msg.what) {
+                case EVENT_CARD_REMOVED:
+                    onIccSwap(false);
+                    break;
+                case EVENT_CARD_ADDED:
+                    onIccSwap(true);
+                    break;
+                case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
+                case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
+                case EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE:
+                case EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE:
+                case EVENT_SIM_IO_DONE:
+                    AsyncResult ar = (AsyncResult)msg.obj;
+                    if (ar.exception != null) {
+                        loglocal("Exception: " + ar.exception);
+                        log("Error in SIM access with exception" + ar.exception);
+                    }
+                    AsyncResult.forMessage((Message)ar.userObj, ar.result, ar.exception);
+                    ((Message)ar.userObj).sendToTarget();
+                    break;
+                case EVENT_CARRIER_PRIVILEGES_LOADED:
+                    onCarrierPriviligesLoadedMessage();
+                    break;
+                default:
+                    loge("Unknown Event " + msg.what);
+            }
+        }
+    };
+
+    private boolean isPackageInstalled(String pkgName) {
+        PackageManager pm = mContext.getPackageManager();
+        try {
+            pm.getPackageInfo(pkgName, PackageManager.GET_ACTIVITIES);
+            if (DBG) log(pkgName + " is installed.");
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            if (DBG) log(pkgName + " is not installed.");
+            return false;
+        }
+    }
+
+    private class ClickListener implements DialogInterface.OnClickListener {
+        String pkgName;
+        public ClickListener(String pkgName) {
+            this.pkgName = pkgName;
+        }
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            synchronized (mLock) {
+                if (which == DialogInterface.BUTTON_POSITIVE) {
+                    Intent market = new Intent(Intent.ACTION_VIEW);
+                    market.setData(Uri.parse("market://details?id=" + pkgName));
+                    market.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    mContext.startActivity(market);
+                } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                    if (DBG) log("Not now clicked for carrier app dialog.");
+                }
+            }
+        }
+    }
+
+    private void promptInstallCarrierApp(String pkgName) {
+        DialogInterface.OnClickListener listener = new ClickListener(pkgName);
+
+        Resources r = Resources.getSystem();
+        String message = r.getString(R.string.carrier_app_dialog_message);
+        String buttonTxt = r.getString(R.string.carrier_app_dialog_button);
+        String notNowTxt = r.getString(R.string.carrier_app_dialog_not_now);
+
+        AlertDialog dialog = new AlertDialog.Builder(mContext)
+        .setMessage(message)
+        .setNegativeButton(notNowTxt, listener)
+        .setPositiveButton(buttonTxt, listener)
+        .create();
+        dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+        dialog.show();
+    }
+
+    private void onCarrierPriviligesLoadedMessage() {
+        UsageStatsManager usm = (UsageStatsManager) mContext.getSystemService(
+                Context.USAGE_STATS_SERVICE);
+        if (usm != null) {
+            usm.onCarrierPrivilegedAppsChanged();
+        }
+        synchronized (mLock) {
+            mCarrierPrivilegeRegistrants.notifyRegistrants();
+            String whitelistSetting = Settings.Global.getString(mContext.getContentResolver(),
+                    Settings.Global.CARRIER_APP_WHITELIST);
+            if (TextUtils.isEmpty(whitelistSetting)) {
+                return;
+            }
+            HashSet<String> carrierAppSet = new HashSet<String>(
+                    Arrays.asList(whitelistSetting.split("\\s*;\\s*")));
+            if (carrierAppSet.isEmpty()) {
+                return;
+            }
+
+            List<String> pkgNames = mCarrierPrivilegeRules.getPackageNames();
+            for (String pkgName : pkgNames) {
+                if (!TextUtils.isEmpty(pkgName) && carrierAppSet.contains(pkgName)
+                        && !isPackageInstalled(pkgName)) {
+                    promptInstallCarrierApp(pkgName);
+                }
+            }
+        }
+    }
+
+    public boolean isApplicationOnIcc(IccCardApplicationStatus.AppType type) {
+        synchronized (mLock) {
+            for (int i = 0 ; i < mUiccApplications.length; i++) {
+                if (mUiccApplications[i] != null && mUiccApplications[i].getType() == type) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    public CardState getCardState() {
+        synchronized (mLock) {
+            return mCardState;
+        }
+    }
+
+    public PinState getUniversalPinState() {
+        synchronized (mLock) {
+            return mUniversalPinState;
+        }
+    }
+
+    public UiccCardApplication getApplication(int family) {
+        synchronized (mLock) {
+            int index = IccCardStatus.CARD_MAX_APPS;
+            switch (family) {
+                case UiccController.APP_FAM_3GPP:
+                    index = mGsmUmtsSubscriptionAppIndex;
+                    break;
+                case UiccController.APP_FAM_3GPP2:
+                    index = mCdmaSubscriptionAppIndex;
+                    break;
+                case UiccController.APP_FAM_IMS:
+                    index = mImsSubscriptionAppIndex;
+                    break;
+            }
+            if (index >= 0 && index < mUiccApplications.length) {
+                return mUiccApplications[index];
+            }
+            return null;
+        }
+    }
+
+    public UiccCardApplication getApplicationIndex(int index) {
+        synchronized (mLock) {
+            if (index >= 0 && index < mUiccApplications.length) {
+                return mUiccApplications[index];
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Returns the SIM application of the specified type.
+     *
+     * @param type ICC application type (@see com.android.internal.telephony.PhoneConstants#APPTYPE_xxx)
+     * @return application corresponding to type or a null if no match found
+     */
+    public UiccCardApplication getApplicationByType(int type) {
+        synchronized (mLock) {
+            for (int i = 0 ; i < mUiccApplications.length; i++) {
+                if (mUiccApplications[i] != null &&
+                        mUiccApplications[i].getType().ordinal() == type) {
+                    return mUiccApplications[i];
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Resets the application with the input AID. Returns true if any changes were made.
+     *
+     * A null aid implies a card level reset - all applications must be reset.
+     */
+    public boolean resetAppWithAid(String aid) {
+        synchronized (mLock) {
+            boolean changed = false;
+            for (int i = 0; i < mUiccApplications.length; i++) {
+                if (mUiccApplications[i] != null
+                        && (TextUtils.isEmpty(aid) || aid.equals(mUiccApplications[i].getAid()))) {
+                    // Delete removed applications
+                    mUiccApplications[i].dispose();
+                    mUiccApplications[i] = null;
+                    changed = true;
+                }
+            }
+            if (TextUtils.isEmpty(aid)) {
+                if (mCarrierPrivilegeRules != null) {
+                    mCarrierPrivilegeRules = null;
+                    changed = true;
+                }
+                if (mCatService != null) {
+                    mCatService.dispose();
+                    mCatService = null;
+                    changed = true;
+                }
+            }
+            return changed;
+        }
+    }
+
+    /**
+     * Exposes {@link CommandsInterface#iccOpenLogicalChannel}
+     */
+    public void iccOpenLogicalChannel(String AID, int p2, Message response) {
+        loglocal("Open Logical Channel: " + AID + " , " + p2 + " by pid:" + Binder.getCallingPid()
+                + " uid:" + Binder.getCallingUid());
+        mCi.iccOpenLogicalChannel(AID, p2,
+                mHandler.obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, response));
+    }
+
+    /**
+     * Exposes {@link CommandsInterface#iccCloseLogicalChannel}
+     */
+    public void iccCloseLogicalChannel(int channel, Message response) {
+        loglocal("Close Logical Channel: " + channel);
+        mCi.iccCloseLogicalChannel(channel,
+                mHandler.obtainMessage(EVENT_CLOSE_LOGICAL_CHANNEL_DONE, response));
+    }
+
+    /**
+     * Exposes {@link CommandsInterface#iccTransmitApduLogicalChannel}
+     */
+    public void iccTransmitApduLogicalChannel(int channel, int cla, int command,
+            int p1, int p2, int p3, String data, Message response) {
+        mCi.iccTransmitApduLogicalChannel(channel, cla, command, p1, p2, p3,
+                data, mHandler.obtainMessage(EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE, response));
+    }
+
+    /**
+     * Exposes {@link CommandsInterface#iccTransmitApduBasicChannel}
+     */
+    public void iccTransmitApduBasicChannel(int cla, int command,
+            int p1, int p2, int p3, String data, Message response) {
+        mCi.iccTransmitApduBasicChannel(cla, command, p1, p2, p3,
+                data, mHandler.obtainMessage(EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE, response));
+    }
+
+    /**
+     * Exposes {@link CommandsInterface#iccIO}
+     */
+    public void iccExchangeSimIO(int fileID, int command, int p1, int p2, int p3,
+            String pathID, Message response) {
+        mCi.iccIO(command, fileID, pathID, p1, p2, p3, null, null,
+                mHandler.obtainMessage(EVENT_SIM_IO_DONE, response));
+    }
+
+    /**
+     * Exposes {@link CommandsInterface#sendEnvelopeWithStatus}
+     */
+    public void sendEnvelopeWithStatus(String contents, Message response) {
+        mCi.sendEnvelopeWithStatus(contents, response);
+    }
+
+    /* Returns number of applications on this card */
+    public int getNumApplications() {
+        int count = 0;
+        for (UiccCardApplication a : mUiccApplications) {
+            if (a != null) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public int getPhoneId() {
+        return mPhoneId;
+    }
+
+    /**
+     * Returns true iff carrier privileges rules are null (dont need to be loaded) or loaded.
+     */
+    public boolean areCarrierPriviligeRulesLoaded() {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules == null
+                || carrierPrivilegeRules.areCarrierPriviligeRulesLoaded();
+    }
+
+    /**
+     * Returns true if there are some carrier privilege rules loaded and specified.
+     */
+    public boolean hasCarrierPrivilegeRules() {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules != null && carrierPrivilegeRules.hasCarrierPrivilegeRules();
+    }
+
+    /**
+     * Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatus}.
+     */
+    public int getCarrierPrivilegeStatus(Signature signature, String packageName) {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules == null
+                ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
+                carrierPrivilegeRules.getCarrierPrivilegeStatus(signature, packageName);
+    }
+
+    /**
+     * Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatus}.
+     */
+    public int getCarrierPrivilegeStatus(PackageManager packageManager, String packageName) {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules == null
+                ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
+                carrierPrivilegeRules.getCarrierPrivilegeStatus(packageManager, packageName);
+    }
+
+    /**
+     * Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatus}.
+     */
+    public int getCarrierPrivilegeStatus(PackageInfo packageInfo) {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules == null
+                ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
+                carrierPrivilegeRules.getCarrierPrivilegeStatus(packageInfo);
+    }
+
+    /**
+     * Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatusForCurrentTransaction}.
+     */
+    public int getCarrierPrivilegeStatusForCurrentTransaction(PackageManager packageManager) {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules == null
+                ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
+                carrierPrivilegeRules.getCarrierPrivilegeStatusForCurrentTransaction(
+                        packageManager);
+    }
+
+    /**
+     * Exposes {@link UiccCarrierPrivilegeRules#getCarrierPackageNamesForIntent}.
+     */
+    public List<String> getCarrierPackageNamesForIntent(
+            PackageManager packageManager, Intent intent) {
+        UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
+        return carrierPrivilegeRules == null ? null :
+                carrierPrivilegeRules.getCarrierPackageNamesForIntent(
+                        packageManager, intent);
+    }
+
+    /** Returns a reference to the current {@link UiccCarrierPrivilegeRules}. */
+    private UiccCarrierPrivilegeRules getCarrierPrivilegeRules() {
+        synchronized (mLock) {
+            return mCarrierPrivilegeRules;
+        }
+    }
+
+    public boolean setOperatorBrandOverride(String brand) {
+        log("setOperatorBrandOverride: " + brand);
+        log("current iccId: " + getIccId());
+
+        String iccId = getIccId();
+        if (TextUtils.isEmpty(iccId)) {
+            return false;
+        }
+
+        SharedPreferences.Editor spEditor =
+                PreferenceManager.getDefaultSharedPreferences(mContext).edit();
+        String key = OPERATOR_BRAND_OVERRIDE_PREFIX + iccId;
+        if (brand == null) {
+            spEditor.remove(key).commit();
+        } else {
+            spEditor.putString(key, brand).commit();
+        }
+        return true;
+    }
+
+    public String getOperatorBrandOverride() {
+        String iccId = getIccId();
+        if (TextUtils.isEmpty(iccId)) {
+            return null;
+        }
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+        return sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
+    }
+
+    public String getIccId() {
+        // ICCID should be same across all the apps.
+        for (UiccCardApplication app : mUiccApplications) {
+            if (app != null) {
+                IccRecords ir = app.getIccRecords();
+                if (ir != null && ir.getIccId() != null) {
+                    return ir.getIccId();
+                }
+            }
+        }
+        return null;
+    }
+
+    private void log(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    private void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+
+    private void loglocal(String msg) {
+        if (DBG) mLocalLog.log(msg);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("UiccCard:");
+        pw.println(" mCi=" + mCi);
+        pw.println(" mLastRadioState=" + mLastRadioState);
+        pw.println(" mCatService=" + mCatService);
+        pw.println(" mAbsentRegistrants: size=" + mAbsentRegistrants.size());
+        for (int i = 0; i < mAbsentRegistrants.size(); i++) {
+            pw.println("  mAbsentRegistrants[" + i + "]="
+                    + ((Registrant)mAbsentRegistrants.get(i)).getHandler());
+        }
+        for (int i = 0; i < mCarrierPrivilegeRegistrants.size(); i++) {
+            pw.println("  mCarrierPrivilegeRegistrants[" + i + "]="
+                    + ((Registrant)mCarrierPrivilegeRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mCardState=" + mCardState);
+        pw.println(" mUniversalPinState=" + mUniversalPinState);
+        pw.println(" mGsmUmtsSubscriptionAppIndex=" + mGsmUmtsSubscriptionAppIndex);
+        pw.println(" mCdmaSubscriptionAppIndex=" + mCdmaSubscriptionAppIndex);
+        pw.println(" mImsSubscriptionAppIndex=" + mImsSubscriptionAppIndex);
+        pw.println(" mImsSubscriptionAppIndex=" + mImsSubscriptionAppIndex);
+        pw.println(" mUiccApplications: length=" + mUiccApplications.length);
+        for (int i = 0; i < mUiccApplications.length; i++) {
+            if (mUiccApplications[i] == null) {
+                pw.println("  mUiccApplications[" + i + "]=" + null);
+            } else {
+                pw.println("  mUiccApplications[" + i + "]="
+                        + mUiccApplications[i].getType() + " " + mUiccApplications[i]);
+            }
+        }
+        pw.println();
+        // Print details of all applications
+        for (UiccCardApplication app : mUiccApplications) {
+            if (app != null) {
+                app.dump(fd, pw, args);
+                pw.println();
+            }
+        }
+        // Print details of all IccRecords
+        for (UiccCardApplication app : mUiccApplications) {
+            if (app != null) {
+                IccRecords ir = app.getIccRecords();
+                if (ir != null) {
+                    ir.dump(fd, pw, args);
+                    pw.println();
+                }
+            }
+        }
+        // Print UiccCarrierPrivilegeRules and registrants.
+        if (mCarrierPrivilegeRules == null) {
+            pw.println(" mCarrierPrivilegeRules: null");
+        } else {
+            pw.println(" mCarrierPrivilegeRules: " + mCarrierPrivilegeRules);
+            mCarrierPrivilegeRules.dump(fd, pw, args);
+        }
+        pw.println(" mCarrierPrivilegeRegistrants: size=" + mCarrierPrivilegeRegistrants.size());
+        for (int i = 0; i < mCarrierPrivilegeRegistrants.size(); i++) {
+            pw.println("  mCarrierPrivilegeRegistrants[" + i + "]="
+                    + ((Registrant)mCarrierPrivilegeRegistrants.get(i)).getHandler());
+        }
+        pw.flush();
+        pw.println("mLocalLog:");
+        mLocalLog.dump(fd, pw, args);
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UiccCardApplication.java b/com/android/internal/telephony/uicc/UiccCardApplication.java
new file mode 100644
index 0000000..e2904df
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccCardApplication.java
@@ -0,0 +1,911 @@
+/*
+ * Copyright (C) 2006, 2012 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.internal.telephony.uicc;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.PersoSubState;
+import com.android.internal.telephony.uicc.IccCardStatus.PinState;
+import com.android.internal.telephony.SubscriptionController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * {@hide}
+ */
+public class UiccCardApplication {
+    private static final String LOG_TAG = "UiccCardApplication";
+    private static final boolean DBG = true;
+
+    private static final int EVENT_PIN1_PUK1_DONE = 1;
+    private static final int EVENT_CHANGE_PIN1_DONE = 2;
+    private static final int EVENT_CHANGE_PIN2_DONE = 3;
+    private static final int EVENT_QUERY_FACILITY_FDN_DONE = 4;
+    private static final int EVENT_CHANGE_FACILITY_FDN_DONE = 5;
+    private static final int EVENT_QUERY_FACILITY_LOCK_DONE = 6;
+    private static final int EVENT_CHANGE_FACILITY_LOCK_DONE = 7;
+    private static final int EVENT_PIN2_PUK2_DONE = 8;
+    private static final int EVENT_RADIO_UNAVAILABLE = 9;
+
+    /**
+     * These values are for authContext (parameter P2) per 3GPP TS 31.102 (Section 7.1.2)
+     */
+    public static final int AUTH_CONTEXT_EAP_SIM = PhoneConstants.AUTH_CONTEXT_EAP_SIM;
+    public static final int AUTH_CONTEXT_EAP_AKA = PhoneConstants.AUTH_CONTEXT_EAP_AKA;
+    public static final int AUTH_CONTEXT_UNDEFINED = PhoneConstants.AUTH_CONTEXT_UNDEFINED;
+
+    private final Object  mLock = new Object();
+    private UiccCard      mUiccCard; //parent
+    private AppState      mAppState;
+    private AppType       mAppType;
+    private int           mAuthContext;
+    private PersoSubState mPersoSubState;
+    private String        mAid;
+    private String        mAppLabel;
+    private boolean       mPin1Replaced;
+    private PinState      mPin1State;
+    private PinState      mPin2State;
+    private boolean       mIccFdnEnabled;
+    private boolean       mDesiredFdnEnabled;
+    private boolean       mIccLockEnabled;
+    private boolean       mDesiredPinLocked;
+    private boolean       mIccFdnAvailable = true; // Default is enabled.
+
+    private CommandsInterface mCi;
+    private Context mContext;
+    private IccRecords mIccRecords;
+    private IccFileHandler mIccFh;
+
+    private boolean mDestroyed;//set to true once this App is commanded to be disposed of.
+
+    private RegistrantList mReadyRegistrants = new RegistrantList();
+    private RegistrantList mPinLockedRegistrants = new RegistrantList();
+    private RegistrantList mNetworkLockedRegistrants = new RegistrantList();
+
+    public UiccCardApplication(UiccCard uiccCard,
+                        IccCardApplicationStatus as,
+                        Context c,
+                        CommandsInterface ci) {
+        if (DBG) log("Creating UiccApp: " + as);
+        mUiccCard = uiccCard;
+        mAppState = as.app_state;
+        mAppType = as.app_type;
+        mAuthContext = getAuthContext(mAppType);
+        mPersoSubState = as.perso_substate;
+        mAid = as.aid;
+        mAppLabel = as.app_label;
+        mPin1Replaced = (as.pin1_replaced != 0);
+        mPin1State = as.pin1;
+        mPin2State = as.pin2;
+
+        mContext = c;
+        mCi = ci;
+
+        mIccFh = createIccFileHandler(as.app_type);
+        mIccRecords = createIccRecords(as.app_type, mContext, mCi);
+        if (mAppState == AppState.APPSTATE_READY) {
+            queryFdn();
+            queryPin1State();
+        }
+        mCi.registerForNotAvailable(mHandler, EVENT_RADIO_UNAVAILABLE, null);
+    }
+
+    public void update (IccCardApplicationStatus as, Context c, CommandsInterface ci) {
+        synchronized (mLock) {
+            if (mDestroyed) {
+                loge("Application updated after destroyed! Fix me!");
+                return;
+            }
+
+            if (DBG) log(mAppType + " update. New " + as);
+            mContext = c;
+            mCi = ci;
+            AppType oldAppType = mAppType;
+            AppState oldAppState = mAppState;
+            PersoSubState oldPersoSubState = mPersoSubState;
+            mAppType = as.app_type;
+            mAuthContext = getAuthContext(mAppType);
+            mAppState = as.app_state;
+            mPersoSubState = as.perso_substate;
+            mAid = as.aid;
+            mAppLabel = as.app_label;
+            mPin1Replaced = (as.pin1_replaced != 0);
+            mPin1State = as.pin1;
+            mPin2State = as.pin2;
+
+            if (mAppType != oldAppType) {
+                if (mIccFh != null) { mIccFh.dispose();}
+                if (mIccRecords != null) { mIccRecords.dispose();}
+                mIccFh = createIccFileHandler(as.app_type);
+                mIccRecords = createIccRecords(as.app_type, c, ci);
+            }
+
+            if (mPersoSubState != oldPersoSubState &&
+                    mPersoSubState == PersoSubState.PERSOSUBSTATE_SIM_NETWORK) {
+                notifyNetworkLockedRegistrantsIfNeeded(null);
+            }
+
+            if (mAppState != oldAppState) {
+                if (DBG) log(oldAppType + " changed state: " + oldAppState + " -> " + mAppState);
+                // If the app state turns to APPSTATE_READY, then query FDN status,
+                //as it might have failed in earlier attempt.
+                if (mAppState == AppState.APPSTATE_READY) {
+                    queryFdn();
+                    queryPin1State();
+                }
+                notifyPinLockedRegistrantsIfNeeded(null);
+                notifyReadyRegistrantsIfNeeded(null);
+            }
+        }
+    }
+
+    void dispose() {
+        synchronized (mLock) {
+            if (DBG) log(mAppType + " being Disposed");
+            mDestroyed = true;
+            if (mIccRecords != null) { mIccRecords.dispose();}
+            if (mIccFh != null) { mIccFh.dispose();}
+            mIccRecords = null;
+            mIccFh = null;
+            mCi.unregisterForNotAvailable(mHandler);
+        }
+    }
+
+    private IccRecords createIccRecords(AppType type, Context c, CommandsInterface ci) {
+        if (type == AppType.APPTYPE_USIM || type == AppType.APPTYPE_SIM) {
+            return new SIMRecords(this, c, ci);
+        } else if (type == AppType.APPTYPE_RUIM || type == AppType.APPTYPE_CSIM){
+            return new RuimRecords(this, c, ci);
+        } else if (type == AppType.APPTYPE_ISIM) {
+            return new IsimUiccRecords(this, c, ci);
+        } else {
+            // Unknown app type (maybe detection is still in progress)
+            return null;
+        }
+    }
+
+    private IccFileHandler createIccFileHandler(AppType type) {
+        switch (type) {
+            case APPTYPE_SIM:
+                return new SIMFileHandler(this, mAid, mCi);
+            case APPTYPE_RUIM:
+                return new RuimFileHandler(this, mAid, mCi);
+            case APPTYPE_USIM:
+                return new UsimFileHandler(this, mAid, mCi);
+            case APPTYPE_CSIM:
+                return new CsimFileHandler(this, mAid, mCi);
+            case APPTYPE_ISIM:
+                return new IsimFileHandler(this, mAid, mCi);
+            default:
+                return null;
+        }
+    }
+
+    /** Assumes mLock is held. */
+    public void queryFdn() {
+        //This shouldn't change run-time. So needs to be called only once.
+        int serviceClassX;
+
+        serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE +
+                        CommandsInterface.SERVICE_CLASS_DATA +
+                        CommandsInterface.SERVICE_CLASS_FAX;
+        mCi.queryFacilityLockForApp (
+                CommandsInterface.CB_FACILITY_BA_FD, "", serviceClassX,
+                mAid, mHandler.obtainMessage(EVENT_QUERY_FACILITY_FDN_DONE));
+    }
+    /**
+     * Interpret EVENT_QUERY_FACILITY_LOCK_DONE
+     * @param ar is asyncResult of Query_Facility_Locked
+     */
+    private void onQueryFdnEnabled(AsyncResult ar) {
+        synchronized (mLock) {
+            if (ar.exception != null) {
+                if (DBG) log("Error in querying facility lock:" + ar.exception);
+                return;
+            }
+
+            int[] result = (int[])ar.result;
+            if(result.length != 0) {
+                //0 - Available & Disabled, 1-Available & Enabled, 2-Unavailable.
+                if (result[0] == 2) {
+                    mIccFdnEnabled = false;
+                    mIccFdnAvailable = false;
+                } else {
+                    mIccFdnEnabled = (result[0] == 1) ? true : false;
+                    mIccFdnAvailable = true;
+                }
+                log("Query facility FDN : FDN service available: "+ mIccFdnAvailable
+                        +" enabled: "  + mIccFdnEnabled);
+            } else {
+                loge("Bogus facility lock response");
+            }
+        }
+    }
+
+    private void onChangeFdnDone(AsyncResult ar) {
+        synchronized (mLock) {
+            int attemptsRemaining = -1;
+
+            if (ar.exception == null) {
+                mIccFdnEnabled = mDesiredFdnEnabled;
+                if (DBG) log("EVENT_CHANGE_FACILITY_FDN_DONE: " +
+                        "mIccFdnEnabled=" + mIccFdnEnabled);
+            } else {
+                attemptsRemaining = parsePinPukErrorResult(ar);
+                loge("Error change facility fdn with exception " + ar.exception);
+            }
+            Message response = (Message)ar.userObj;
+            response.arg1 = attemptsRemaining;
+            AsyncResult.forMessage(response).exception = ar.exception;
+            response.sendToTarget();
+        }
+    }
+
+    /** REMOVE when mIccLockEnabled is not needed, assumes mLock is held */
+    private void queryPin1State() {
+        int serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE +
+                CommandsInterface.SERVICE_CLASS_DATA +
+                CommandsInterface.SERVICE_CLASS_FAX;
+        mCi.queryFacilityLockForApp (
+            CommandsInterface.CB_FACILITY_BA_SIM, "", serviceClassX,
+            mAid, mHandler.obtainMessage(EVENT_QUERY_FACILITY_LOCK_DONE));
+    }
+
+    /** REMOVE when mIccLockEnabled is not needed*/
+    private void onQueryFacilityLock(AsyncResult ar) {
+        synchronized (mLock) {
+            if(ar.exception != null) {
+                if (DBG) log("Error in querying facility lock:" + ar.exception);
+                return;
+            }
+
+            int[] ints = (int[])ar.result;
+            if(ints.length != 0) {
+                if (DBG) log("Query facility lock : "  + ints[0]);
+
+                mIccLockEnabled = (ints[0] != 0);
+
+                if (mIccLockEnabled) {
+                    mPinLockedRegistrants.notifyRegistrants();
+                }
+
+                // Sanity check: we expect mPin1State to match mIccLockEnabled.
+                // When mPin1State is DISABLED mIccLockEanbled should be false.
+                // When mPin1State is ENABLED mIccLockEnabled should be true.
+                //
+                // Here we validate these assumptions to assist in identifying which ril/radio's
+                // have not correctly implemented GET_SIM_STATUS
+                switch (mPin1State) {
+                    case PINSTATE_DISABLED:
+                        if (mIccLockEnabled) {
+                            loge("QUERY_FACILITY_LOCK:enabled GET_SIM_STATUS.Pin1:disabled."
+                                    + " Fixme");
+                        }
+                        break;
+                    case PINSTATE_ENABLED_NOT_VERIFIED:
+                    case PINSTATE_ENABLED_VERIFIED:
+                    case PINSTATE_ENABLED_BLOCKED:
+                    case PINSTATE_ENABLED_PERM_BLOCKED:
+                        if (!mIccLockEnabled) {
+                            loge("QUERY_FACILITY_LOCK:disabled GET_SIM_STATUS.Pin1:enabled."
+                                    + " Fixme");
+                        }
+                    case PINSTATE_UNKNOWN:
+                    default:
+                        if (DBG) log("Ignoring: pin1state=" + mPin1State);
+                        break;
+                }
+            } else {
+                loge("Bogus facility lock response");
+            }
+        }
+    }
+
+    /** REMOVE when mIccLockEnabled is not needed */
+    private void onChangeFacilityLock(AsyncResult ar) {
+        synchronized (mLock) {
+            int attemptsRemaining = -1;
+
+            if (ar.exception == null) {
+                mIccLockEnabled = mDesiredPinLocked;
+                if (DBG) log( "EVENT_CHANGE_FACILITY_LOCK_DONE: mIccLockEnabled= "
+                        + mIccLockEnabled);
+            } else {
+                attemptsRemaining = parsePinPukErrorResult(ar);
+                loge("Error change facility lock with exception " + ar.exception);
+            }
+            Message response = (Message)ar.userObj;
+            AsyncResult.forMessage(response).exception = ar.exception;
+            response.arg1 = attemptsRemaining;
+            response.sendToTarget();
+        }
+    }
+
+    /**
+     * Parse the error response to obtain number of attempts remaining
+     */
+    private int parsePinPukErrorResult(AsyncResult ar) {
+        int[] result = (int[]) ar.result;
+        if (result == null) {
+            return -1;
+        } else {
+            int length = result.length;
+            int attemptsRemaining = -1;
+            if (length > 0) {
+                attemptsRemaining = result[0];
+            }
+            log("parsePinPukErrorResult: attemptsRemaining=" + attemptsRemaining);
+            return attemptsRemaining;
+        }
+    }
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg){
+            AsyncResult ar;
+
+            if (mDestroyed) {
+                loge("Received message " + msg + "[" + msg.what
+                        + "] while being destroyed. Ignoring.");
+                return;
+            }
+
+            switch (msg.what) {
+                case EVENT_PIN1_PUK1_DONE:
+                case EVENT_PIN2_PUK2_DONE:
+                case EVENT_CHANGE_PIN1_DONE:
+                case EVENT_CHANGE_PIN2_DONE:
+                    // a PIN/PUK/PIN2/PUK2 complete
+                    // request has completed. ar.userObj is the response Message
+                    int attemptsRemaining = -1;
+                    ar = (AsyncResult)msg.obj;
+                    if ((ar.exception != null) && (ar.result != null)) {
+                        attemptsRemaining = parsePinPukErrorResult(ar);
+                    }
+                    Message response = (Message)ar.userObj;
+                    AsyncResult.forMessage(response).exception = ar.exception;
+                    response.arg1 = attemptsRemaining;
+                    response.sendToTarget();
+                    break;
+                case EVENT_QUERY_FACILITY_FDN_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    onQueryFdnEnabled(ar);
+                    break;
+                case EVENT_CHANGE_FACILITY_FDN_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    onChangeFdnDone(ar);
+                    break;
+                case EVENT_QUERY_FACILITY_LOCK_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    onQueryFacilityLock(ar);
+                    break;
+                case EVENT_CHANGE_FACILITY_LOCK_DONE:
+                    ar = (AsyncResult)msg.obj;
+                    onChangeFacilityLock(ar);
+                    break;
+                case EVENT_RADIO_UNAVAILABLE:
+                    if (DBG) log("handleMessage (EVENT_RADIO_UNAVAILABLE)");
+                    mAppState = AppState.APPSTATE_UNKNOWN;
+                    break;
+                default:
+                    loge("Unknown Event " + msg.what);
+            }
+        }
+    };
+
+    public void registerForReady(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+            mReadyRegistrants.add(r);
+            notifyReadyRegistrantsIfNeeded(r);
+        }
+    }
+
+    public void unregisterForReady(Handler h) {
+        synchronized (mLock) {
+            mReadyRegistrants.remove(h);
+        }
+    }
+
+    /**
+     * Notifies handler of any transition into State.isPinLocked()
+     */
+    public void registerForLocked(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+            mPinLockedRegistrants.add(r);
+            notifyPinLockedRegistrantsIfNeeded(r);
+        }
+    }
+
+    public void unregisterForLocked(Handler h) {
+        synchronized (mLock) {
+            mPinLockedRegistrants.remove(h);
+        }
+    }
+
+    /**
+     * Notifies handler of any transition into State.NETWORK_LOCKED
+     */
+    public void registerForNetworkLocked(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+            mNetworkLockedRegistrants.add(r);
+            notifyNetworkLockedRegistrantsIfNeeded(r);
+        }
+    }
+
+    public void unregisterForNetworkLocked(Handler h) {
+        synchronized (mLock) {
+            mNetworkLockedRegistrants.remove(h);
+        }
+    }
+
+    /**
+     * Notifies specified registrant, assume mLock is held.
+     *
+     * @param r Registrant to be notified. If null - all registrants will be notified
+     */
+    private void notifyReadyRegistrantsIfNeeded(Registrant r) {
+        if (mDestroyed) {
+            return;
+        }
+        if (mAppState == AppState.APPSTATE_READY) {
+            if (mPin1State == PinState.PINSTATE_ENABLED_NOT_VERIFIED ||
+                    mPin1State == PinState.PINSTATE_ENABLED_BLOCKED ||
+                    mPin1State == PinState.PINSTATE_ENABLED_PERM_BLOCKED) {
+                loge("Sanity check failed! APPSTATE is ready while PIN1 is not verified!!!");
+                // Don't notify if application is in insane state
+                return;
+            }
+            if (r == null) {
+                if (DBG) log("Notifying registrants: READY");
+                mReadyRegistrants.notifyRegistrants();
+            } else {
+                if (DBG) log("Notifying 1 registrant: READY");
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+
+    /**
+     * Notifies specified registrant, assume mLock is held.
+     *
+     * @param r Registrant to be notified. If null - all registrants will be notified
+     */
+    private void notifyPinLockedRegistrantsIfNeeded(Registrant r) {
+        if (mDestroyed) {
+            return;
+        }
+
+        if (mAppState == AppState.APPSTATE_PIN ||
+                mAppState == AppState.APPSTATE_PUK) {
+            if (mPin1State == PinState.PINSTATE_ENABLED_VERIFIED ||
+                    mPin1State == PinState.PINSTATE_DISABLED) {
+                loge("Sanity check failed! APPSTATE is locked while PIN1 is not!!!");
+                //Don't notify if application is in insane state
+                return;
+            }
+            if (r == null) {
+                if (DBG) log("Notifying registrants: LOCKED");
+                mPinLockedRegistrants.notifyRegistrants();
+            } else {
+                if (DBG) log("Notifying 1 registrant: LOCKED");
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+
+    /**
+     * Notifies specified registrant, assume mLock is held.
+     *
+     * @param r Registrant to be notified. If null - all registrants will be notified
+     */
+    private void notifyNetworkLockedRegistrantsIfNeeded(Registrant r) {
+        if (mDestroyed) {
+            return;
+        }
+
+        if (mAppState == AppState.APPSTATE_SUBSCRIPTION_PERSO &&
+                mPersoSubState == PersoSubState.PERSOSUBSTATE_SIM_NETWORK) {
+            if (r == null) {
+                if (DBG) log("Notifying registrants: NETWORK_LOCKED");
+                mNetworkLockedRegistrants.notifyRegistrants();
+            } else {
+                if (DBG) log("Notifying 1 registrant: NETWORK_LOCED");
+                r.notifyRegistrant(new AsyncResult(null, null, null));
+            }
+        }
+    }
+
+    public AppState getState() {
+        synchronized (mLock) {
+            return mAppState;
+        }
+    }
+
+    public AppType getType() {
+        synchronized (mLock) {
+            return mAppType;
+        }
+    }
+
+    public int getAuthContext() {
+        synchronized (mLock) {
+            return mAuthContext;
+        }
+    }
+
+    /**
+     * Returns the authContext based on the type of UiccCard.
+     *
+     * @param appType the app type
+     * @return authContext corresponding to the type or AUTH_CONTEXT_UNDEFINED if appType not
+     * supported
+     */
+    private static int getAuthContext(AppType appType) {
+        int authContext;
+
+        switch (appType) {
+            case APPTYPE_SIM:
+                authContext = AUTH_CONTEXT_EAP_SIM;
+                break;
+
+            case APPTYPE_USIM:
+                authContext = AUTH_CONTEXT_EAP_AKA;
+                break;
+
+            default:
+                authContext = AUTH_CONTEXT_UNDEFINED;
+                break;
+        }
+
+        return authContext;
+    }
+
+    public PersoSubState getPersoSubState() {
+        synchronized (mLock) {
+            return mPersoSubState;
+        }
+    }
+
+    public String getAid() {
+        synchronized (mLock) {
+            return mAid;
+        }
+    }
+
+    public String getAppLabel() {
+        return mAppLabel;
+    }
+
+    public PinState getPin1State() {
+        synchronized (mLock) {
+            if (mPin1Replaced) {
+                return mUiccCard.getUniversalPinState();
+            }
+            return mPin1State;
+        }
+    }
+
+    public IccFileHandler getIccFileHandler() {
+        synchronized (mLock) {
+            return mIccFh;
+        }
+    }
+
+    public IccRecords getIccRecords() {
+        synchronized (mLock) {
+            return mIccRecords;
+        }
+    }
+
+    /**
+     * Supply the ICC PIN to the ICC
+     *
+     * When the operation is complete, onComplete will be sent to its
+     * Handler.
+     *
+     * onComplete.obj will be an AsyncResult
+     * onComplete.arg1 = remaining attempts before puk locked or -1 if unknown
+     *
+     * ((AsyncResult)onComplete.obj).exception == null on success
+     * ((AsyncResult)onComplete.obj).exception != null on fail
+     *
+     * If the supplied PIN is incorrect:
+     * ((AsyncResult)onComplete.obj).exception != null
+     * && ((AsyncResult)onComplete.obj).exception
+     *       instanceof com.android.internal.telephony.gsm.CommandException)
+     * && ((CommandException)(((AsyncResult)onComplete.obj).exception))
+     *          .getCommandError() == CommandException.Error.PASSWORD_INCORRECT
+     */
+    public void supplyPin (String pin, Message onComplete) {
+        synchronized (mLock) {
+            mCi.supplyIccPinForApp(pin, mAid, mHandler.obtainMessage(EVENT_PIN1_PUK1_DONE,
+                    onComplete));
+        }
+    }
+
+    /**
+     * Supply the ICC PUK to the ICC
+     *
+     * When the operation is complete, onComplete will be sent to its
+     * Handler.
+     *
+     * onComplete.obj will be an AsyncResult
+     * onComplete.arg1 = remaining attempts before Icc will be permanently unusable
+     * or -1 if unknown
+     *
+     * ((AsyncResult)onComplete.obj).exception == null on success
+     * ((AsyncResult)onComplete.obj).exception != null on fail
+     *
+     * If the supplied PIN is incorrect:
+     * ((AsyncResult)onComplete.obj).exception != null
+     * && ((AsyncResult)onComplete.obj).exception
+     *       instanceof com.android.internal.telephony.gsm.CommandException)
+     * && ((CommandException)(((AsyncResult)onComplete.obj).exception))
+     *          .getCommandError() == CommandException.Error.PASSWORD_INCORRECT
+     *
+     *
+     */
+    public void supplyPuk (String puk, String newPin, Message onComplete) {
+        synchronized (mLock) {
+        mCi.supplyIccPukForApp(puk, newPin, mAid,
+                mHandler.obtainMessage(EVENT_PIN1_PUK1_DONE, onComplete));
+        }
+    }
+
+    public void supplyPin2 (String pin2, Message onComplete) {
+        synchronized (mLock) {
+            mCi.supplyIccPin2ForApp(pin2, mAid,
+                    mHandler.obtainMessage(EVENT_PIN2_PUK2_DONE, onComplete));
+        }
+    }
+
+    public void supplyPuk2 (String puk2, String newPin2, Message onComplete) {
+        synchronized (mLock) {
+            mCi.supplyIccPuk2ForApp(puk2, newPin2, mAid,
+                    mHandler.obtainMessage(EVENT_PIN2_PUK2_DONE, onComplete));
+        }
+    }
+
+    public void supplyNetworkDepersonalization (String pin, Message onComplete) {
+        synchronized (mLock) {
+            if (DBG) log("supplyNetworkDepersonalization");
+            mCi.supplyNetworkDepersonalization(pin, onComplete);
+        }
+    }
+
+    /**
+     * Check whether ICC pin lock is enabled
+     * This is a sync call which returns the cached pin enabled state
+     *
+     * @return true for ICC locked enabled
+     *         false for ICC locked disabled
+     */
+    public boolean getIccLockEnabled() {
+        return mIccLockEnabled;
+        /* STOPSHIP: Remove line above and all code associated with setting
+           mIccLockEanbled once all RIL correctly sends the pin1 state.
+        // Use getPin1State to take into account pin1Replaced flag
+        PinState pinState = getPin1State();
+        return pinState == PinState.PINSTATE_ENABLED_NOT_VERIFIED ||
+               pinState == PinState.PINSTATE_ENABLED_VERIFIED ||
+               pinState == PinState.PINSTATE_ENABLED_BLOCKED ||
+               pinState == PinState.PINSTATE_ENABLED_PERM_BLOCKED;*/
+     }
+
+    /**
+     * Check whether ICC fdn (fixed dialing number) is enabled
+     * This is a sync call which returns the cached pin enabled state
+     *
+     * @return true for ICC fdn enabled
+     *         false for ICC fdn disabled
+     */
+    public boolean getIccFdnEnabled() {
+        synchronized (mLock) {
+            return mIccFdnEnabled;
+        }
+    }
+
+    /**
+     * Check whether fdn (fixed dialing number) service is available.
+     * @return true if ICC fdn service available
+     *         false if ICC fdn service not available
+     */
+    public boolean getIccFdnAvailable() {
+        return mIccFdnAvailable;
+    }
+
+    /**
+     * Set the ICC pin lock enabled or disabled
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param enabled "true" for locked "false" for unlocked.
+     * @param password needed to change the ICC pin state, aka. Pin1
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    public void setIccLockEnabled (boolean enabled,
+            String password, Message onComplete) {
+        synchronized (mLock) {
+            int serviceClassX;
+            serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE +
+                    CommandsInterface.SERVICE_CLASS_DATA +
+                    CommandsInterface.SERVICE_CLASS_FAX;
+
+            mDesiredPinLocked = enabled;
+
+            mCi.setFacilityLockForApp(CommandsInterface.CB_FACILITY_BA_SIM,
+                    enabled, password, serviceClassX, mAid,
+                    mHandler.obtainMessage(EVENT_CHANGE_FACILITY_LOCK_DONE, onComplete));
+        }
+    }
+
+    /**
+     * Set the ICC fdn enabled or disabled
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param enabled "true" for locked "false" for unlocked.
+     * @param password needed to change the ICC fdn enable, aka Pin2
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    public void setIccFdnEnabled (boolean enabled,
+            String password, Message onComplete) {
+        synchronized (mLock) {
+            int serviceClassX;
+            serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE +
+                    CommandsInterface.SERVICE_CLASS_DATA +
+                    CommandsInterface.SERVICE_CLASS_FAX +
+                    CommandsInterface.SERVICE_CLASS_SMS;
+
+            mDesiredFdnEnabled = enabled;
+
+            mCi.setFacilityLockForApp(CommandsInterface.CB_FACILITY_BA_FD,
+                    enabled, password, serviceClassX, mAid,
+                    mHandler.obtainMessage(EVENT_CHANGE_FACILITY_FDN_DONE, onComplete));
+        }
+    }
+
+    /**
+     * Change the ICC password used in ICC pin lock
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param oldPassword is the old password
+     * @param newPassword is the new password
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        onComplete.arg1 = attempts remaining or -1 if unknown
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    public void changeIccLockPassword(String oldPassword, String newPassword,
+            Message onComplete) {
+        synchronized (mLock) {
+            if (DBG) log("changeIccLockPassword");
+            mCi.changeIccPinForApp(oldPassword, newPassword, mAid,
+                    mHandler.obtainMessage(EVENT_CHANGE_PIN1_DONE, onComplete));
+        }
+    }
+
+    /**
+     * Change the ICC password used in ICC fdn enable
+     * When the operation is complete, onComplete will be sent to its handler
+     *
+     * @param oldPassword is the old password
+     * @param newPassword is the new password
+     * @param onComplete
+     *        onComplete.obj will be an AsyncResult
+     *        ((AsyncResult)onComplete.obj).exception == null on success
+     *        ((AsyncResult)onComplete.obj).exception != null on fail
+     */
+    public void changeIccFdnPassword(String oldPassword, String newPassword,
+            Message onComplete) {
+        synchronized (mLock) {
+            if (DBG) log("changeIccFdnPassword");
+            mCi.changeIccPin2ForApp(oldPassword, newPassword, mAid,
+                    mHandler.obtainMessage(EVENT_CHANGE_PIN2_DONE, onComplete));
+        }
+    }
+
+    /**
+     * @return true if ICC card is PIN2 blocked
+     */
+    public boolean getIccPin2Blocked() {
+        synchronized (mLock) {
+            return mPin2State == PinState.PINSTATE_ENABLED_BLOCKED;
+        }
+    }
+
+    /**
+     * @return true if ICC card is PUK2 blocked
+     */
+    public boolean getIccPuk2Blocked() {
+        synchronized (mLock) {
+            return mPin2State == PinState.PINSTATE_ENABLED_PERM_BLOCKED;
+        }
+    }
+
+    public int getPhoneId() {
+        return mUiccCard.getPhoneId();
+    }
+
+    protected UiccCard getUiccCard() {
+        return mUiccCard;
+    }
+
+    private void log(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    private void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("UiccCardApplication: " + this);
+        pw.println(" mUiccCard=" + mUiccCard);
+        pw.println(" mAppState=" + mAppState);
+        pw.println(" mAppType=" + mAppType);
+        pw.println(" mPersoSubState=" + mPersoSubState);
+        pw.println(" mAid=" + mAid);
+        pw.println(" mAppLabel=" + mAppLabel);
+        pw.println(" mPin1Replaced=" + mPin1Replaced);
+        pw.println(" mPin1State=" + mPin1State);
+        pw.println(" mPin2State=" + mPin2State);
+        pw.println(" mIccFdnEnabled=" + mIccFdnEnabled);
+        pw.println(" mDesiredFdnEnabled=" + mDesiredFdnEnabled);
+        pw.println(" mIccLockEnabled=" + mIccLockEnabled);
+        pw.println(" mDesiredPinLocked=" + mDesiredPinLocked);
+        pw.println(" mCi=" + mCi);
+        pw.println(" mIccRecords=" + mIccRecords);
+        pw.println(" mIccFh=" + mIccFh);
+        pw.println(" mDestroyed=" + mDestroyed);
+        pw.println(" mReadyRegistrants: size=" + mReadyRegistrants.size());
+        for (int i = 0; i < mReadyRegistrants.size(); i++) {
+            pw.println("  mReadyRegistrants[" + i + "]="
+                    + ((Registrant)mReadyRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mPinLockedRegistrants: size=" + mPinLockedRegistrants.size());
+        for (int i = 0; i < mPinLockedRegistrants.size(); i++) {
+            pw.println("  mPinLockedRegistrants[" + i + "]="
+                    + ((Registrant)mPinLockedRegistrants.get(i)).getHandler());
+        }
+        pw.println(" mNetworkLockedRegistrants: size=" + mNetworkLockedRegistrants.size());
+        for (int i = 0; i < mNetworkLockedRegistrants.size(); i++) {
+            pw.println("  mNetworkLockedRegistrants[" + i + "]="
+                    + ((Registrant)mNetworkLockedRegistrants.get(i)).getHandler());
+        }
+        pw.flush();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java b/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java
new file mode 100644
index 0000000..e50f40c
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony.uicc;
+
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccAccessRule;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.CommandException;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Class that reads and stores the carrier privileged rules from the UICC.
+ *
+ * The rules are read when the class is created, hence it should only be created
+ * after the UICC can be read. And it should be deleted when a UICC is changed.
+ *
+ * Document: https://source.android.com/devices/tech/config/uicc.html
+ *
+ * {@hide}
+ */
+public class UiccCarrierPrivilegeRules extends Handler {
+    private static final String LOG_TAG = "UiccCarrierPrivilegeRules";
+    private static final boolean DBG = false;
+
+    private static final String AID = "A00000015141434C00";
+    private static final int CLA = 0x80;
+    private static final int COMMAND = 0xCA;
+    private static final int P1 = 0xFF;
+    private static final int P2 = 0x40;
+    private static final int P2_EXTENDED_DATA = 0x60;
+    private static final int P3 = 0x00;
+    private static final String DATA = "";
+
+    /*
+     * Rules format:
+     *   ALL_REF_AR_DO = TAG_ALL_REF_AR_DO + len + [REF_AR_DO]*n
+     *   REF_AR_DO = TAG_REF_AR_DO + len + REF-DO + AR-DO
+     *
+     *   REF_DO = TAG_REF_DO + len + DEVICE_APP_ID_REF_DO + (optional) PKG_REF_DO
+     *   AR_DO = TAG_AR_DO + len + PERM_AR_DO
+     *
+     *   DEVICE_APP_ID_REF_DO = TAG_DEVICE_APP_ID_REF_DO + len + sha256 hexstring of cert
+     *   PKG_REF_DO = TAG_PKG_REF_DO + len + package name
+     *   PERM_AR_DO = TAG_PERM_AR_DO + len + detailed permission (8 bytes)
+     *
+     * Data objects hierarchy by TAG:
+     * FF40
+     *   E2
+     *     E1
+     *       C1
+     *       CA
+     *     E3
+     *       DB
+     */
+    // Values from the data standard.
+    private static final String TAG_ALL_REF_AR_DO = "FF40";
+    private static final String TAG_REF_AR_DO = "E2";
+    private static final String TAG_REF_DO = "E1";
+    private static final String TAG_DEVICE_APP_ID_REF_DO = "C1";
+    private static final String TAG_PKG_REF_DO = "CA";
+    private static final String TAG_AR_DO = "E3";
+    private static final String TAG_PERM_AR_DO = "DB";
+    private static final String TAG_AID_REF_DO = "4F";
+    private static final String CARRIER_PRIVILEGE_AID = "FFFFFFFFFFFF";
+
+    private static final int EVENT_OPEN_LOGICAL_CHANNEL_DONE = 1;
+    private static final int EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE = 2;
+    private static final int EVENT_CLOSE_LOGICAL_CHANNEL_DONE = 3;
+    private static final int EVENT_PKCS15_READ_DONE = 4;
+
+    // State of the object.
+    private static final int STATE_LOADING  = 0;
+    private static final int STATE_LOADED   = 1;
+    private static final int STATE_ERROR    = 2;
+
+    // Max number of retries for open logical channel, interval is 10s.
+    private static final int MAX_RETRY = 1;
+    private static final int RETRY_INTERVAL_MS = 10000;
+
+    // Used for parsing the data from the UICC.
+    public static class TLV {
+        private static final int SINGLE_BYTE_MAX_LENGTH = 0x80;
+        private String tag;
+        // Length encoding is in GPC_Specification_2.2.1: 11.1.5 APDU Message and Data Length.
+        // Length field could be either 1 byte if length < 128, or multiple bytes with first byte
+        // specifying how many bytes are used for length, followed by length bytes.
+        // Bytes for the length field, in ASCII HEX string form.
+        private String lengthBytes;
+        // Decoded length as integer.
+        private Integer length;
+        private String value;
+
+        public TLV(String tag) {
+            this.tag = tag;
+        }
+
+        public String getValue() {
+            if (value == null) return "";
+            return value;
+        }
+
+        public String parseLength(String data) {
+            int offset = tag.length();
+            int firstByte = Integer.parseInt(data.substring(offset, offset + 2), 16);
+            if (firstByte < SINGLE_BYTE_MAX_LENGTH) {
+                length = firstByte * 2;
+                lengthBytes = data.substring(offset, offset + 2);
+            } else {
+                int numBytes = firstByte - SINGLE_BYTE_MAX_LENGTH;
+                length = Integer.parseInt(data.substring(offset + 2, offset + 2 + numBytes * 2), 16) * 2;
+                lengthBytes = data.substring(offset, offset + 2 + numBytes * 2);
+            }
+            log("TLV parseLength length=" + length + "lenghtBytes: " + lengthBytes);
+            return lengthBytes;
+        }
+
+        public String parse(String data, boolean shouldConsumeAll) {
+            log("Parse TLV: " + tag);
+            if (!data.startsWith(tag)) {
+                throw new IllegalArgumentException("Tags don't match.");
+            }
+            int index = tag.length();
+            if (index + 2 > data.length()) {
+                throw new IllegalArgumentException("No length.");
+            }
+
+            parseLength(data);
+            index += lengthBytes.length();
+
+            log("index="+index+" length="+length+"data.length="+data.length());
+            int remainingLength = data.length() - (index + length);
+            if (remainingLength < 0) {
+                throw new IllegalArgumentException("Not enough data.");
+            }
+            if (shouldConsumeAll && (remainingLength != 0)) {
+                throw new IllegalArgumentException("Did not consume all.");
+            }
+            value = data.substring(index, index + length);
+
+            log("Got TLV: " + tag + "," + length + "," + value);
+
+            return data.substring(index + length);
+        }
+    }
+
+    private UiccCard mUiccCard;  // Parent
+    private UiccPkcs15 mUiccPkcs15; // ARF fallback
+    private AtomicInteger mState;
+    private List<UiccAccessRule> mAccessRules;
+    private String mRules;
+    private Message mLoadedCallback;
+    private String mStatusMessage;  // Only used for debugging.
+    private int mChannelId; // Channel Id for communicating with UICC.
+    private int mRetryCount;  // Number of retries for open logical channel.
+    private final Runnable mRetryRunnable = new Runnable() {
+        @Override
+        public void run() {
+            openChannel();
+        }
+    };
+
+    private void openChannel() {
+        // Send open logical channel request.
+        int p2 = 0x00;
+        mUiccCard.iccOpenLogicalChannel(AID, p2, /* supported p2 value */
+            obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, null));
+    }
+
+    public UiccCarrierPrivilegeRules(UiccCard uiccCard, Message loadedCallback) {
+        log("Creating UiccCarrierPrivilegeRules");
+        mUiccCard = uiccCard;
+        mState = new AtomicInteger(STATE_LOADING);
+        mStatusMessage = "Not loaded.";
+        mLoadedCallback = loadedCallback;
+        mRules = "";
+        mAccessRules = new ArrayList<>();
+
+        openChannel();
+    }
+
+    /**
+     * Returns true if the carrier privilege rules have finished loading.
+     */
+    public boolean areCarrierPriviligeRulesLoaded() {
+        return mState.get() != STATE_LOADING;
+    }
+
+    /**
+     * Returns true if the carrier privilege rules have finished loading and some rules were
+     * specified.
+     */
+    public boolean hasCarrierPrivilegeRules() {
+        return mState.get() != STATE_LOADING && mAccessRules != null && mAccessRules.size() > 0;
+    }
+
+    /**
+     * Returns package names for privilege rules.
+     * Return empty list if no rules defined or package name is empty string.
+     */
+    public List<String> getPackageNames() {
+        List<String> pkgNames = new ArrayList<String>();
+        if (mAccessRules != null) {
+            for (UiccAccessRule ar : mAccessRules) {
+                if (!TextUtils.isEmpty(ar.getPackageName())) {
+                    pkgNames.add(ar.getPackageName());
+                }
+            }
+        }
+        return pkgNames;
+    }
+
+    /**
+     * Returns the status of the carrier privileges for the input certificate and package name.
+     *
+     * @param signature The signature of the certificate.
+     * @param packageName name of the package.
+     * @return Access status.
+     */
+    public int getCarrierPrivilegeStatus(Signature signature, String packageName) {
+        int state = mState.get();
+        if (state == STATE_LOADING) {
+            return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+        } else if (state == STATE_ERROR) {
+            return TelephonyManager.CARRIER_PRIVILEGE_STATUS_ERROR_LOADING_RULES;
+        }
+
+        for (UiccAccessRule ar : mAccessRules) {
+            int accessStatus = ar.getCarrierPrivilegeStatus(signature, packageName);
+            if (accessStatus != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) {
+                return accessStatus;
+            }
+        }
+
+        return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS;
+    }
+
+    /**
+     * Returns the status of the carrier privileges for the input package name.
+     *
+     * @param packageManager PackageManager for getting signatures.
+     * @param packageName name of the package.
+     * @return Access status.
+     */
+    public int getCarrierPrivilegeStatus(PackageManager packageManager, String packageName) {
+        try {
+            // Short-circuit if there are no rules to check against, so we don't need to fetch
+            // the package info with signatures.
+            if (!hasCarrierPrivilegeRules()) {
+                int state = mState.get();
+                if (state == STATE_LOADING) {
+                    return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+                } else if (state == STATE_ERROR) {
+                    return TelephonyManager.CARRIER_PRIVILEGE_STATUS_ERROR_LOADING_RULES;
+                }
+                return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS;
+            }
+            // Include DISABLED_UNTIL_USED components. This facilitates cases where a carrier app
+            // is disabled by default, and some other component wants to enable it when it has
+            // gained carrier privileges (as an indication that a matching SIM has been inserted).
+            PackageInfo pInfo = packageManager.getPackageInfo(packageName,
+                    PackageManager.GET_SIGNATURES
+                            | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);
+            return getCarrierPrivilegeStatus(pInfo);
+        } catch (PackageManager.NameNotFoundException ex) {
+            Rlog.e(LOG_TAG, "NameNotFoundException", ex);
+        }
+        return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS;
+    }
+
+    /**
+     * Returns the status of the carrier privileges for the input package info.
+     *
+     * @param packageInfo PackageInfo for the package, containing the package signatures.
+     * @return Access status.
+     */
+    public int getCarrierPrivilegeStatus(PackageInfo packageInfo) {
+        int state = mState.get();
+        if (state == STATE_LOADING) {
+            return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+        } else if (state == STATE_ERROR) {
+            return TelephonyManager.CARRIER_PRIVILEGE_STATUS_ERROR_LOADING_RULES;
+        }
+
+        for (UiccAccessRule ar : mAccessRules) {
+            int accessStatus = ar.getCarrierPrivilegeStatus(packageInfo);
+            if (accessStatus != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) {
+                return accessStatus;
+            }
+        }
+        return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS;
+    }
+
+    /**
+     * Returns the status of the carrier privileges for the caller of the current transaction.
+     *
+     * @param packageManager PackageManager for getting signatures and package names.
+     * @return Access status.
+     */
+    public int getCarrierPrivilegeStatusForCurrentTransaction(PackageManager packageManager) {
+        String[] packages = packageManager.getPackagesForUid(Binder.getCallingUid());
+
+        for (String pkg : packages) {
+            int accessStatus = getCarrierPrivilegeStatus(packageManager, pkg);
+            if (accessStatus != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) {
+                return accessStatus;
+            }
+        }
+        return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS;
+    }
+
+    /**
+     * Returns the package name of the carrier app that should handle the input intent.
+     *
+     * @param packageManager PackageManager for getting receivers.
+     * @param intent Intent that will be sent.
+     * @return list of carrier app package names that can handle the intent.
+     *         Returns null if there is an error and an empty list if there
+     *         are no matching packages.
+     */
+    public List<String> getCarrierPackageNamesForIntent(
+            PackageManager packageManager, Intent intent) {
+        List<String> packages = new ArrayList<String>();
+        List<ResolveInfo> receivers = new ArrayList<ResolveInfo>();
+        receivers.addAll(packageManager.queryBroadcastReceivers(intent, 0));
+        receivers.addAll(packageManager.queryIntentContentProviders(intent, 0));
+        receivers.addAll(packageManager.queryIntentActivities(intent, 0));
+        receivers.addAll(packageManager.queryIntentServices(intent, 0));
+
+        for (ResolveInfo resolveInfo : receivers) {
+            String packageName = getPackageName(resolveInfo);
+            if (packageName == null) {
+                continue;
+            }
+
+            int status = getCarrierPrivilegeStatus(packageManager, packageName);
+            if (status == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+                packages.add(packageName);
+            } else if (status != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) {
+                // Any status apart from HAS_ACCESS and NO_ACCESS is considered an error.
+                return null;
+            }
+        }
+
+        return packages;
+    }
+
+    @Nullable
+    private String getPackageName(ResolveInfo resolveInfo) {
+        if (resolveInfo.activityInfo != null) {
+            return resolveInfo.activityInfo.packageName;
+        } else if (resolveInfo.serviceInfo != null) {
+            return resolveInfo.serviceInfo.packageName;
+        } else if (resolveInfo.providerInfo != null) {
+            return resolveInfo.providerInfo.packageName;
+        }
+        return null;
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        AsyncResult ar;
+
+        switch (msg.what) {
+
+            case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
+                log("EVENT_OPEN_LOGICAL_CHANNEL_DONE");
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null && ar.result != null) {
+                    mChannelId = ((int[]) ar.result)[0];
+                    mUiccCard.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND, P1, P2, P3,
+                            DATA, obtainMessage(EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE,
+                                    mChannelId));
+                } else {
+                    // MISSING_RESOURCE could be due to logical channels temporarily unavailable,
+                    // so we retry up to MAX_RETRY times, with an interval of RETRY_INTERVAL_MS.
+                    if (ar.exception instanceof CommandException && mRetryCount < MAX_RETRY
+                            && ((CommandException) (ar.exception)).getCommandError()
+                                    == CommandException.Error.MISSING_RESOURCE) {
+                        mRetryCount++;
+                        removeCallbacks(mRetryRunnable);
+                        postDelayed(mRetryRunnable, RETRY_INTERVAL_MS);
+                    } else {
+                        // if rules cannot be read from ARA applet,
+                        // fallback to PKCS15-based ARF.
+                        log("No ARA, try ARF next.");
+                        mUiccPkcs15 = new UiccPkcs15(mUiccCard,
+                                obtainMessage(EVENT_PKCS15_READ_DONE));
+                    }
+                }
+                break;
+
+            case EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE:
+                log("EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE");
+                ar = (AsyncResult) msg.obj;
+                if (ar.exception == null && ar.result != null) {
+                    IccIoResult response = (IccIoResult) ar.result;
+                    if (response.sw1 == 0x90 && response.sw2 == 0x00
+                            && response.payload != null && response.payload.length > 0) {
+                        try {
+                            mRules += IccUtils.bytesToHexString(response.payload)
+                                    .toUpperCase(Locale.US);
+                            if (isDataComplete()) {
+                                mAccessRules = parseRules(mRules);
+                                updateState(STATE_LOADED, "Success!");
+                            } else {
+                                mUiccCard.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND,
+                                        P1, P2_EXTENDED_DATA, P3, DATA,
+                                        obtainMessage(EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE,
+                                                mChannelId));
+                                break;
+                            }
+                        } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
+                            updateState(STATE_ERROR, "Error parsing rules: " + ex);
+                        }
+                    } else {
+                        String errorMsg = "Invalid response: payload=" + response.payload
+                                + " sw1=" + response.sw1 + " sw2=" + response.sw2;
+                        updateState(STATE_ERROR, errorMsg);
+                    }
+                } else {
+                    updateState(STATE_ERROR, "Error reading value from SIM.");
+                }
+
+                mUiccCard.iccCloseLogicalChannel(mChannelId, obtainMessage(
+                        EVENT_CLOSE_LOGICAL_CHANNEL_DONE));
+                mChannelId = -1;
+                break;
+
+            case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
+                log("EVENT_CLOSE_LOGICAL_CHANNEL_DONE");
+                break;
+
+            case EVENT_PKCS15_READ_DONE:
+                log("EVENT_PKCS15_READ_DONE");
+                if (mUiccPkcs15 == null || mUiccPkcs15.getRules() == null) {
+                    updateState(STATE_ERROR, "No ARA or ARF.");
+                } else {
+                    for (String cert : mUiccPkcs15.getRules()) {
+                        UiccAccessRule accessRule = new UiccAccessRule(
+                                IccUtils.hexStringToBytes(cert), "", 0x00);
+                        mAccessRules.add(accessRule);
+                    }
+                    updateState(STATE_LOADED, "Success!");
+                }
+                break;
+
+            default:
+                Rlog.e(LOG_TAG, "Unknown event " + msg.what);
+        }
+    }
+
+    /*
+     * Check if all rule bytes have been read from UICC.
+     * For long payload, we need to fetch it repeatly before start parsing it.
+     */
+    private boolean isDataComplete() {
+        log("isDataComplete mRules:" + mRules);
+        if (mRules.startsWith(TAG_ALL_REF_AR_DO)) {
+            TLV allRules = new TLV(TAG_ALL_REF_AR_DO);
+            String lengthBytes = allRules.parseLength(mRules);
+            log("isDataComplete lengthBytes: " + lengthBytes);
+            if (mRules.length() == TAG_ALL_REF_AR_DO.length() + lengthBytes.length() +
+                                   allRules.length) {
+                log("isDataComplete yes");
+                return true;
+            } else {
+                log("isDataComplete no");
+                return false;
+            }
+        } else {
+            throw new IllegalArgumentException("Tags don't match.");
+        }
+    }
+
+    /*
+     * Parses the rules from the input string.
+     */
+    private static List<UiccAccessRule> parseRules(String rules) {
+        log("Got rules: " + rules);
+
+        TLV allRefArDo = new TLV(TAG_ALL_REF_AR_DO); //FF40
+        allRefArDo.parse(rules, true);
+
+        String arDos = allRefArDo.value;
+        List<UiccAccessRule> accessRules = new ArrayList<>();
+        while (!arDos.isEmpty()) {
+            TLV refArDo = new TLV(TAG_REF_AR_DO); //E2
+            arDos = refArDo.parse(arDos, false);
+            UiccAccessRule accessRule = parseRefArdo(refArDo.value);
+            if (accessRule != null) {
+                accessRules.add(accessRule);
+            } else {
+              Rlog.e(LOG_TAG, "Skip unrecognized rule." + refArDo.value);
+            }
+        }
+        return accessRules;
+    }
+
+    /*
+     * Parses a single rule.
+     */
+    private static UiccAccessRule parseRefArdo(String rule) {
+        log("Got rule: " + rule);
+
+        String certificateHash = null;
+        String packageName = null;
+        String tmp = null;
+        long accessType = 0;
+
+        while (!rule.isEmpty()) {
+            if (rule.startsWith(TAG_REF_DO)) {
+                TLV refDo = new TLV(TAG_REF_DO); //E1
+                rule = refDo.parse(rule, false);
+                // Allow 4F tag with a default value "FF FF FF FF FF FF" to be compatible with
+                // devices having GP access control enforcer:
+                //  - If no 4F tag is present, it's a CP rule.
+                //  - If 4F tag has value "FF FF FF FF FF FF", it's a CP rule.
+                //  - If 4F tag has other values, it's not a CP rule and Android should ignore it.
+                TLV deviceDo = new TLV(TAG_DEVICE_APP_ID_REF_DO); //C1
+                if (refDo.value.startsWith(TAG_AID_REF_DO)) {
+                    TLV cpDo = new TLV(TAG_AID_REF_DO); //4F
+                    String remain = cpDo.parse(refDo.value, false);
+                    if (!cpDo.lengthBytes.equals("06") || !cpDo.value.equals(CARRIER_PRIVILEGE_AID)
+                            || remain.isEmpty() || !remain.startsWith(TAG_DEVICE_APP_ID_REF_DO)) {
+                        return null;
+                    }
+                    tmp = deviceDo.parse(remain, false);
+                    certificateHash = deviceDo.value;
+                } else if (refDo.value.startsWith(TAG_DEVICE_APP_ID_REF_DO)) {
+                    tmp = deviceDo.parse(refDo.value, false);
+                    certificateHash = deviceDo.value;
+                } else {
+                    return null;
+                }
+                if (!tmp.isEmpty()) {
+                    if (!tmp.startsWith(TAG_PKG_REF_DO)) {
+                        return null;
+                    }
+                    TLV pkgDo = new TLV(TAG_PKG_REF_DO); //CA
+                    pkgDo.parse(tmp, true);
+                    packageName = new String(IccUtils.hexStringToBytes(pkgDo.value));
+                } else {
+                    packageName = null;
+                }
+            } else if (rule.startsWith(TAG_AR_DO)) {
+                TLV arDo = new TLV(TAG_AR_DO); //E3
+                rule = arDo.parse(rule, false);
+                // Skip all the irrelevant tags (All the optional tags here are two bytes
+                // according to the spec GlobalPlatform Secure Element Access Control).
+                String remain = arDo.value;
+                while (!remain.isEmpty() && !remain.startsWith(TAG_PERM_AR_DO)) {
+                    TLV tmpDo = new TLV(remain.substring(0, 2));
+                    remain = tmpDo.parse(remain, false);
+                }
+                if (remain.isEmpty()) {
+                    return null;
+                }
+                TLV permDo = new TLV(TAG_PERM_AR_DO); //DB
+                permDo.parse(remain, true);
+            } else  {
+                // Spec requires it must be either TAG_REF_DO or TAG_AR_DO.
+                throw new RuntimeException("Invalid Rule type");
+            }
+        }
+
+        UiccAccessRule accessRule = new UiccAccessRule(
+                IccUtils.hexStringToBytes(certificateHash), packageName, accessType);
+        return accessRule;
+    }
+
+    /*
+     * Updates the state and notifies the UiccCard that the rules have finished loading.
+     */
+    private void updateState(int newState, String statusMessage) {
+        mState.set(newState);
+        if (mLoadedCallback != null) {
+            mLoadedCallback.sendToTarget();
+        }
+
+        mStatusMessage = statusMessage;
+    }
+
+    private static void log(String msg) {
+        if (DBG) Rlog.d(LOG_TAG, msg);
+    }
+
+    /**
+     * Dumps info to Dumpsys - useful for debugging.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("UiccCarrierPrivilegeRules: " + this);
+        pw.println(" mState=" + getStateString(mState.get()));
+        pw.println(" mStatusMessage='" + mStatusMessage + "'");
+        if (mAccessRules != null) {
+            pw.println(" mAccessRules: ");
+            for (UiccAccessRule ar : mAccessRules) {
+                pw.println("  rule='" + ar + "'");
+            }
+        } else {
+            pw.println(" mAccessRules: null");
+        }
+        if (mUiccPkcs15 != null) {
+            pw.println(" mUiccPkcs15: " + mUiccPkcs15);
+            mUiccPkcs15.dump(fd, pw, args);
+        } else {
+            pw.println(" mUiccPkcs15: null");
+        }
+        pw.flush();
+    }
+
+    /*
+     * Converts state into human readable format.
+     */
+    private String getStateString(int state) {
+      switch (state) {
+        case STATE_LOADING:
+            return "STATE_LOADING";
+        case STATE_LOADED:
+            return "STATE_LOADED";
+        case STATE_ERROR:
+            return "STATE_ERROR";
+        default:
+            return "UNKNOWN";
+      }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UiccController.java b/com/android/internal/telephony/uicc/UiccController.java
new file mode 100644
index 0000000..a948b75
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccController.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2011-2012 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.internal.telephony.uicc;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.os.SystemProperties;
+import android.os.storage.StorageManager;
+import android.telephony.TelephonyManager;
+import android.telephony.Rlog;
+import android.text.format.Time;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.SubscriptionController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.LinkedList;
+
+/**
+ * This class is responsible for keeping all knowledge about
+ * Universal Integrated Circuit Card (UICC), also know as SIM's,
+ * in the system. It is also used as API to get appropriate
+ * applications to pass them to phone and service trackers.
+ *
+ * UiccController is created with the call to make() function.
+ * UiccController is a singleton and make() must only be called once
+ * and throws an exception if called multiple times.
+ *
+ * Once created UiccController registers with RIL for "on" and "unsol_sim_status_changed"
+ * notifications. When such notification arrives UiccController will call
+ * getIccCardStatus (GET_SIM_STATUS). Based on the response of GET_SIM_STATUS
+ * request appropriate tree of uicc objects will be created.
+ *
+ * Following is class diagram for uicc classes:
+ *
+ *                       UiccController
+ *                            #
+ *                            |
+ *                        UiccCard
+ *                          #   #
+ *                          |   ------------------
+ *                    UiccCardApplication    CatService
+ *                      #            #
+ *                      |            |
+ *                 IccRecords    IccFileHandler
+ *                 ^ ^ ^           ^ ^ ^ ^ ^
+ *    SIMRecords---- | |           | | | | ---SIMFileHandler
+ *    RuimRecords----- |           | | | ----RuimFileHandler
+ *    IsimUiccRecords---           | | -----UsimFileHandler
+ *                                 | ------CsimFileHandler
+ *                                 ----IsimFileHandler
+ *
+ * Legend: # stands for Composition
+ *         ^ stands for Generalization
+ *
+ * See also {@link com.android.internal.telephony.IccCard}
+ * and {@link com.android.internal.telephony.uicc.IccCardProxy}
+ */
+public class UiccController extends Handler {
+    private static final boolean DBG = true;
+    private static final String LOG_TAG = "UiccController";
+
+    public static final int APP_FAM_3GPP =  1;
+    public static final int APP_FAM_3GPP2 = 2;
+    public static final int APP_FAM_IMS   = 3;
+
+    private static final int EVENT_ICC_STATUS_CHANGED = 1;
+    private static final int EVENT_GET_ICC_STATUS_DONE = 2;
+    private static final int EVENT_RADIO_UNAVAILABLE = 3;
+    private static final int EVENT_SIM_REFRESH = 4;
+
+    private CommandsInterface[] mCis;
+    private UiccCard[] mUiccCards = new UiccCard[TelephonyManager.getDefault().getPhoneCount()];
+
+    private static final Object mLock = new Object();
+    private static UiccController mInstance;
+
+    private Context mContext;
+
+    protected RegistrantList mIccChangedRegistrants = new RegistrantList();
+
+    private UiccStateChangedLauncher mLauncher;
+
+    // Logging for dumpsys. Useful in cases when the cards run into errors.
+    private static final int MAX_PROACTIVE_COMMANDS_TO_LOG = 20;
+    private LinkedList<String> mCardLogs = new LinkedList<String>();
+
+    public static UiccController make(Context c, CommandsInterface[] ci) {
+        synchronized (mLock) {
+            if (mInstance != null) {
+                throw new RuntimeException("MSimUiccController.make() should only be called once");
+            }
+            mInstance = new UiccController(c, ci);
+            return (UiccController)mInstance;
+        }
+    }
+
+    private UiccController(Context c, CommandsInterface []ci) {
+        if (DBG) log("Creating UiccController");
+        mContext = c;
+        mCis = ci;
+        for (int i = 0; i < mCis.length; i++) {
+            Integer index = new Integer(i);
+            mCis[i].registerForIccStatusChanged(this, EVENT_ICC_STATUS_CHANGED, index);
+            // TODO remove this once modem correctly notifies the unsols
+            // If the device is unencrypted or has been decrypted or FBE is supported,
+            // i.e. not in cryptkeeper bounce, read SIM when radio state isavailable.
+            // Else wait for radio to be on. This is needed for the scenario when SIM is locked --
+            // to avoid overlap of CryptKeeper and SIM unlock screen.
+            if (!StorageManager.inCryptKeeperBounce()) {
+                mCis[i].registerForAvailable(this, EVENT_ICC_STATUS_CHANGED, index);
+            } else {
+                mCis[i].registerForOn(this, EVENT_ICC_STATUS_CHANGED, index);
+            }
+            mCis[i].registerForNotAvailable(this, EVENT_RADIO_UNAVAILABLE, index);
+            mCis[i].registerForIccRefresh(this, EVENT_SIM_REFRESH, index);
+        }
+
+        mLauncher = new UiccStateChangedLauncher(c, this);
+    }
+
+    public static UiccController getInstance() {
+        synchronized (mLock) {
+            if (mInstance == null) {
+                throw new RuntimeException(
+                        "UiccController.getInstance can't be called before make()");
+            }
+            return mInstance;
+        }
+    }
+
+    public UiccCard getUiccCard(int phoneId) {
+        synchronized (mLock) {
+            if (isValidCardIndex(phoneId)) {
+                return mUiccCards[phoneId];
+            }
+            return null;
+        }
+    }
+
+    public UiccCard[] getUiccCards() {
+        // Return cloned array since we don't want to give out reference
+        // to internal data structure.
+        synchronized (mLock) {
+            return mUiccCards.clone();
+        }
+    }
+
+    // Easy to use API
+    public IccRecords getIccRecords(int phoneId, int family) {
+        synchronized (mLock) {
+            UiccCardApplication app = getUiccCardApplication(phoneId, family);
+            if (app != null) {
+                return app.getIccRecords();
+            }
+            return null;
+        }
+    }
+
+    // Easy to use API
+    public IccFileHandler getIccFileHandler(int phoneId, int family) {
+        synchronized (mLock) {
+            UiccCardApplication app = getUiccCardApplication(phoneId, family);
+            if (app != null) {
+                return app.getIccFileHandler();
+            }
+            return null;
+        }
+    }
+
+
+    //Notifies when card status changes
+    public void registerForIccChanged(Handler h, int what, Object obj) {
+        synchronized (mLock) {
+            Registrant r = new Registrant (h, what, obj);
+            mIccChangedRegistrants.add(r);
+            //Notify registrant right after registering, so that it will get the latest ICC status,
+            //otherwise which may not happen until there is an actual change in ICC status.
+            r.notifyRegistrant();
+        }
+    }
+
+    public void unregisterForIccChanged(Handler h) {
+        synchronized (mLock) {
+            mIccChangedRegistrants.remove(h);
+        }
+    }
+
+    @Override
+    public void handleMessage (Message msg) {
+        synchronized (mLock) {
+            Integer index = getCiIndex(msg);
+
+            if (index < 0 || index >= mCis.length) {
+                Rlog.e(LOG_TAG, "Invalid index : " + index + " received with event " + msg.what);
+                return;
+            }
+
+            AsyncResult ar = (AsyncResult)msg.obj;
+            switch (msg.what) {
+                case EVENT_ICC_STATUS_CHANGED:
+                    if (DBG) log("Received EVENT_ICC_STATUS_CHANGED, calling getIccCardStatus");
+                    mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
+                    break;
+                case EVENT_GET_ICC_STATUS_DONE:
+                    if (DBG) log("Received EVENT_GET_ICC_STATUS_DONE");
+                    onGetIccCardStatusDone(ar, index);
+                    break;
+                case EVENT_RADIO_UNAVAILABLE:
+                    if (DBG) log("EVENT_RADIO_UNAVAILABLE, dispose card");
+                    if (mUiccCards[index] != null) {
+                        mUiccCards[index].dispose();
+                    }
+                    mUiccCards[index] = null;
+                    mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
+                    break;
+                case EVENT_SIM_REFRESH:
+                    if (DBG) log("Received EVENT_SIM_REFRESH");
+                    onSimRefresh(ar, index);
+                    break;
+                default:
+                    Rlog.e(LOG_TAG, " Unknown Event " + msg.what);
+            }
+        }
+    }
+
+    private Integer getCiIndex(Message msg) {
+        AsyncResult ar;
+        Integer index = new Integer(PhoneConstants.DEFAULT_CARD_INDEX);
+
+        /*
+         * The events can be come in two ways. By explicitly sending it using
+         * sendMessage, in this case the user object passed is msg.obj and from
+         * the CommandsInterface, in this case the user object is msg.obj.userObj
+         */
+        if (msg != null) {
+            if (msg.obj != null && msg.obj instanceof Integer) {
+                index = (Integer)msg.obj;
+            } else if(msg.obj != null && msg.obj instanceof AsyncResult) {
+                ar = (AsyncResult)msg.obj;
+                if (ar.userObj != null && ar.userObj instanceof Integer) {
+                    index = (Integer)ar.userObj;
+                }
+            }
+        }
+        return index;
+    }
+
+    // Easy to use API
+    public UiccCardApplication getUiccCardApplication(int phoneId, int family) {
+        synchronized (mLock) {
+            if (isValidCardIndex(phoneId)) {
+                UiccCard c = mUiccCards[phoneId];
+                if (c != null) {
+                    return mUiccCards[phoneId].getApplication(family);
+                }
+            }
+            return null;
+        }
+    }
+
+    private synchronized void onGetIccCardStatusDone(AsyncResult ar, Integer index) {
+        if (ar.exception != null) {
+            Rlog.e(LOG_TAG,"Error getting ICC status. "
+                    + "RIL_REQUEST_GET_ICC_STATUS should "
+                    + "never return an error", ar.exception);
+            return;
+        }
+        if (!isValidCardIndex(index)) {
+            Rlog.e(LOG_TAG,"onGetIccCardStatusDone: invalid index : " + index);
+            return;
+        }
+
+        IccCardStatus status = (IccCardStatus)ar.result;
+
+        if (mUiccCards[index] == null) {
+            //Create new card
+            mUiccCards[index] = new UiccCard(mContext, mCis[index], status, index);
+        } else {
+            //Update already existing card
+            mUiccCards[index].update(mContext, mCis[index] , status);
+        }
+
+        if (DBG) log("Notifying IccChangedRegistrants");
+        mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
+
+    }
+
+    private void onSimRefresh(AsyncResult ar, Integer index) {
+        if (ar.exception != null) {
+            Rlog.e(LOG_TAG, "Sim REFRESH with exception: " + ar.exception);
+            return;
+        }
+
+        if (!isValidCardIndex(index)) {
+            Rlog.e(LOG_TAG,"onSimRefresh: invalid index : " + index);
+            return;
+        }
+
+        IccRefreshResponse resp = (IccRefreshResponse) ar.result;
+        Rlog.d(LOG_TAG, "onSimRefresh: " + resp);
+
+        if (mUiccCards[index] == null) {
+            Rlog.e(LOG_TAG,"onSimRefresh: refresh on null card : " + index);
+            return;
+        }
+
+        if (resp.refreshResult != IccRefreshResponse.REFRESH_RESULT_RESET) {
+          Rlog.d(LOG_TAG, "Ignoring non reset refresh: " + resp);
+          return;
+        }
+
+        Rlog.d(LOG_TAG, "Handling refresh reset: " + resp);
+
+        boolean changed = mUiccCards[index].resetAppWithAid(resp.aid);
+        if (changed) {
+            boolean requirePowerOffOnSimRefreshReset = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_requireRadioPowerOffOnSimRefreshReset);
+            if (requirePowerOffOnSimRefreshReset) {
+                mCis[index].setRadioPower(false, null);
+            } else {
+                mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
+            }
+            mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
+        }
+    }
+
+    private boolean isValidCardIndex(int index) {
+        return (index >= 0 && index < mUiccCards.length);
+    }
+
+    private void log(String string) {
+        Rlog.d(LOG_TAG, string);
+    }
+
+    // TODO: This is hacky. We need a better way of saving the logs.
+    public void addCardLog(String data) {
+        Time t = new Time();
+        t.setToNow();
+        mCardLogs.addLast(t.format("%m-%d %H:%M:%S") + " " + data);
+        if (mCardLogs.size() > MAX_PROACTIVE_COMMANDS_TO_LOG) {
+            mCardLogs.removeFirst();
+        }
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("UiccController: " + this);
+        pw.println(" mContext=" + mContext);
+        pw.println(" mInstance=" + mInstance);
+        pw.println(" mIccChangedRegistrants: size=" + mIccChangedRegistrants.size());
+        for (int i = 0; i < mIccChangedRegistrants.size(); i++) {
+            pw.println("  mIccChangedRegistrants[" + i + "]="
+                    + ((Registrant)mIccChangedRegistrants.get(i)).getHandler());
+        }
+        pw.println();
+        pw.flush();
+        pw.println(" mUiccCards: size=" + mUiccCards.length);
+        for (int i = 0; i < mUiccCards.length; i++) {
+            if (mUiccCards[i] == null) {
+                pw.println("  mUiccCards[" + i + "]=null");
+            } else {
+                pw.println("  mUiccCards[" + i + "]=" + mUiccCards[i]);
+                mUiccCards[i].dump(fd, pw, args);
+            }
+        }
+        pw.println("mCardLogs: ");
+        for (int i = 0; i < mCardLogs.size(); ++i) {
+            pw.println("  " + mCardLogs.get(i));
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UiccPkcs15.java b/com/android/internal/telephony/uicc/UiccPkcs15.java
new file mode 100644
index 0000000..80ebcbf
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccPkcs15.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.uicc;
+
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccCarrierPrivilegeRules.TLV;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.IllegalArgumentException;
+import java.lang.IndexOutOfBoundsException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Class that reads PKCS15-based rules for carrier privileges.
+ *
+ * The spec for the rules:
+ *     GP Secure Element Access Control:
+ *     https://www.globalplatform.org/specificationsdevice.asp
+ *
+ * The UiccPkcs15 class handles overall flow of finding/selecting PKCS15 applet
+ * and reading/parsing each file. Because PKCS15 can be selected in 2 different ways:
+ * via logical channel or EF_DIR, PKCS15Selector is a handler to encapsulate the flow.
+ * Similarly, FileHandler is used for selecting/reading each file, so common codes are
+ * all in same place.
+ *
+ * {@hide}
+ */
+public class UiccPkcs15 extends Handler {
+    private static final String LOG_TAG = "UiccPkcs15";
+    private static final boolean DBG = true;
+
+    // File handler for PKCS15 files, select file and read binary,
+    // convert to String then send to callback message.
+    private class FileHandler extends Handler {
+        // EF path for PKCS15 root, eg. "3F007F50"
+        // null if logical channel is used for PKCS15 access.
+        private final String mPkcs15Path;
+        // Message to send when file has been parsed.
+        private Message mCallback;
+        // File id to read data from, eg. "5031"
+        private String mFileId;
+
+        // async events for the sequence of select and read
+        static protected final int EVENT_SELECT_FILE_DONE = 101;
+        static protected final int EVENT_READ_BINARY_DONE = 102;
+
+        // pkcs15Path is nullable when using logical channel
+        public FileHandler(String pkcs15Path) {
+            log("Creating FileHandler, pkcs15Path: " + pkcs15Path);
+            mPkcs15Path = pkcs15Path;
+        }
+
+        public boolean loadFile(String fileId, Message callBack) {
+            log("loadFile: " + fileId);
+            if (fileId == null || callBack == null) return false;
+            mFileId = fileId;
+            mCallback = callBack;
+            selectFile();
+            return true;
+        }
+
+        private void selectFile() {
+            if (mChannelId >= 0) {
+                mUiccCard.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xA4, 0x00, 0x04, 0x02,
+                        mFileId, obtainMessage(EVENT_SELECT_FILE_DONE));
+            } else {
+                log("EF based");
+            }
+        }
+
+        private void readBinary() {
+            if (mChannelId >=0 ) {
+                mUiccCard.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xB0, 0x00, 0x00, 0x00,
+                        "", obtainMessage(EVENT_READ_BINARY_DONE));
+            } else {
+                log("EF based");
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            log("handleMessage: " + msg.what);
+            AsyncResult ar = (AsyncResult) msg.obj;
+            if (ar.exception != null || ar.result == null) {
+                log("Error: " + ar.exception);
+                AsyncResult.forMessage(mCallback, null, ar.exception);
+                mCallback.sendToTarget();
+                return;
+            }
+
+            switch (msg.what) {
+                case EVENT_SELECT_FILE_DONE:
+                    readBinary();
+                    break;
+
+                case EVENT_READ_BINARY_DONE:
+                    IccIoResult response = (IccIoResult) ar.result;
+                    String result = IccUtils.bytesToHexString(response.payload)
+                            .toUpperCase(Locale.US);
+                    log("IccIoResult: " + response + " payload: " + result);
+                    AsyncResult.forMessage(mCallback, result, (result == null) ?
+                            new IccException("Error: null response for " + mFileId) : null);
+                    mCallback.sendToTarget();
+                    break;
+
+                default:
+                    log("Unknown event" + msg.what);
+            }
+        }
+    }
+
+    private class Pkcs15Selector extends Handler {
+        private static final String PKCS15_AID = "A000000063504B43532D3135";
+        private Message mCallback;
+        private static final int EVENT_OPEN_LOGICAL_CHANNEL_DONE = 201;
+
+        public Pkcs15Selector(Message callBack) {
+            mCallback = callBack;
+            // Specified in ISO 7816-4 clause 7.1.1 0x04 means that FCP template is requested.
+            int p2 = 0x04;
+            mUiccCard.iccOpenLogicalChannel(PKCS15_AID, p2, /* supported P2 value */
+                    obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE));
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            log("handleMessage: " + msg.what);
+            AsyncResult ar;
+
+            switch (msg.what) {
+              case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
+                  ar = (AsyncResult) msg.obj;
+                  if (ar.exception == null && ar.result != null) {
+                      mChannelId = ((int[]) ar.result)[0];
+                      log("mChannelId: " + mChannelId);
+                      AsyncResult.forMessage(mCallback, null, null);
+                  } else {
+                      log("error: " + ar.exception);
+                      AsyncResult.forMessage(mCallback, null, ar.exception);
+                      // TODO: don't sendToTarget and read EF_DIR to find PKCS15
+                  }
+                  mCallback.sendToTarget();
+                  break;
+
+              default:
+                  log("Unknown event" + msg.what);
+            }
+        }
+    }
+
+    private UiccCard mUiccCard;  // Parent
+    private Message mLoadedCallback;
+    private int mChannelId = -1; // Channel Id for communicating with UICC.
+    private List<String> mRules = new ArrayList<String>();
+    private Pkcs15Selector mPkcs15Selector;
+    private FileHandler mFh;
+
+    private static final int EVENT_SELECT_PKCS15_DONE = 1;
+    private static final int EVENT_LOAD_ODF_DONE = 2;
+    private static final int EVENT_LOAD_DODF_DONE = 3;
+    private static final int EVENT_LOAD_ACMF_DONE = 4;
+    private static final int EVENT_LOAD_ACRF_DONE = 5;
+    private static final int EVENT_LOAD_ACCF_DONE = 6;
+    private static final int EVENT_CLOSE_LOGICAL_CHANNEL_DONE = 7;
+
+    public UiccPkcs15(UiccCard uiccCard, Message loadedCallback) {
+        log("Creating UiccPkcs15");
+        mUiccCard = uiccCard;
+        mLoadedCallback = loadedCallback;
+        mPkcs15Selector = new Pkcs15Selector(obtainMessage(EVENT_SELECT_PKCS15_DONE));
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        log("handleMessage: " + msg.what);
+        AsyncResult ar = (AsyncResult) msg.obj;
+
+        switch (msg.what) {
+          case EVENT_SELECT_PKCS15_DONE:
+              if (ar.exception == null) {
+                  // ar.result is null if using logical channel,
+                  // or string for pkcs15 path if using file access.
+                  mFh = new FileHandler((String)ar.result);
+                  if (!mFh.loadFile(ID_ACRF, obtainMessage(EVENT_LOAD_ACRF_DONE))) {
+                      cleanUp();
+                  }
+              } else {
+                  log("select pkcs15 failed: " + ar.exception);
+                  // select PKCS15 failed, notify uiccCarrierPrivilegeRules
+                  mLoadedCallback.sendToTarget();
+              }
+              break;
+
+          case EVENT_LOAD_ACRF_DONE:
+              if (ar.exception == null && ar.result != null) {
+                  String idAccf = parseAcrf((String)ar.result);
+                  if (!mFh.loadFile(idAccf, obtainMessage(EVENT_LOAD_ACCF_DONE))) {
+                      cleanUp();
+                  }
+              } else {
+                  cleanUp();
+              }
+              break;
+
+          case EVENT_LOAD_ACCF_DONE:
+              if (ar.exception == null && ar.result != null) {
+                  parseAccf((String)ar.result);
+              }
+              // We are done here, no more file to read
+              cleanUp();
+              break;
+
+          case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
+              break;
+
+          default:
+              Rlog.e(LOG_TAG, "Unknown event " + msg.what);
+        }
+    }
+
+    private void cleanUp() {
+        log("cleanUp");
+        if (mChannelId >= 0) {
+            mUiccCard.iccCloseLogicalChannel(mChannelId, obtainMessage(
+                    EVENT_CLOSE_LOGICAL_CHANNEL_DONE));
+            mChannelId = -1;
+        }
+        mLoadedCallback.sendToTarget();
+    }
+
+    // Constants defined in specs, needed for parsing
+    private static final String CARRIER_RULE_AID = "FFFFFFFFFFFF"; // AID for carrier privilege rule
+    private static final String ID_ACRF = "4300";
+    private static final String TAG_ASN_SEQUENCE = "30";
+    private static final String TAG_ASN_OCTET_STRING = "04";
+    private static final String TAG_TARGET_AID = "A0";
+
+    // parse ACRF file to get file id for ACCF file
+    // data is hex string, return file id if parse success, null otherwise
+    private String parseAcrf(String data) {
+        String ret = null;
+
+        String acRules = data;
+        while (!acRules.isEmpty()) {
+            TLV tlvRule = new TLV(TAG_ASN_SEQUENCE);
+            try {
+                acRules = tlvRule.parse(acRules, false);
+                String ruleString = tlvRule.getValue();
+                if (ruleString.startsWith(TAG_TARGET_AID)) {
+                    // rule string consists of target AID + path, example:
+                    // [A0] 08 [04] 06 FF FF FF FF FF FF [30] 04 [04] 02 43 10
+                    // bytes in [] are tags for the data
+                    TLV tlvTarget = new TLV(TAG_TARGET_AID); // A0
+                    TLV tlvAid = new TLV(TAG_ASN_OCTET_STRING); // 04
+                    TLV tlvAsnPath = new TLV(TAG_ASN_SEQUENCE); // 30
+                    TLV tlvPath = new TLV(TAG_ASN_OCTET_STRING);  // 04
+
+                    // populate tlvTarget.value with aid data,
+                    // ruleString has remaining data for path
+                    ruleString = tlvTarget.parse(ruleString, false);
+                    // parse tlvTarget.value to get actual strings for AID.
+                    // no other tags expected so shouldConsumeAll is true.
+                    tlvAid.parse(tlvTarget.getValue(), true);
+
+                    if (CARRIER_RULE_AID.equals(tlvAid.getValue())) {
+                        tlvAsnPath.parse(ruleString, true);
+                        tlvPath.parse(tlvAsnPath.getValue(), true);
+                        ret = tlvPath.getValue();
+                    }
+                }
+                continue; // skip current rule as it doesn't have expected TAG
+            } catch (IllegalArgumentException|IndexOutOfBoundsException ex) {
+                log("Error: " + ex);
+                break; // Bad data, ignore all remaining ACRules
+            }
+        }
+        return ret;
+    }
+
+    // parse ACCF and add to mRules
+    private void parseAccf(String data) {
+        String acCondition = data;
+        while (!acCondition.isEmpty()) {
+            TLV tlvCondition = new TLV(TAG_ASN_SEQUENCE);
+            TLV tlvCert = new TLV(TAG_ASN_OCTET_STRING);
+            try {
+                acCondition = tlvCondition.parse(acCondition, false);
+                tlvCert.parse(tlvCondition.getValue(), true);
+                if (!tlvCert.getValue().isEmpty()) {
+                    mRules.add(tlvCert.getValue());
+                }
+            } catch (IllegalArgumentException|IndexOutOfBoundsException ex) {
+                log("Error: " + ex);
+                break; // Bad data, ignore all remaining acCondition data
+            }
+        }
+    }
+
+    public List<String> getRules() {
+        return mRules;
+    }
+
+    private static void log(String msg) {
+        if (DBG) Rlog.d(LOG_TAG, msg);
+    }
+
+    /**
+     * Dumps info to Dumpsys - useful for debugging.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mRules != null) {
+            pw.println(" mRules:");
+            for (String cert : mRules) {
+                pw.println("  " + cert);
+            }
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java b/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
new file mode 100644
index 0000000..a5ab60b
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 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.internal.telephony.uicc;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
+
+/**
+ * This class launches its logic on Uicc cards state changes to / from a
+ * {@link #CARDSTATE_RESTRICTED} to notify a device provisioning package {@link
+ * com.android.internal.R.string.config_deviceProvisioningPackage}, which manages user notifications
+ * that inserted SIM is not supported on the device.
+ *
+ * @see #CARDSTATE_RESTRICTED
+ *
+ * {@hide}
+ */
+public class UiccStateChangedLauncher extends Handler {
+    private static final String TAG = UiccStateChangedLauncher.class.getName();
+    private static final int EVENT_ICC_CHANGED = 1;
+
+    private static String sDeviceProvisioningPackage = null;
+    private Context mContext;
+    private UiccController mUiccController;
+    private boolean[] mIsRestricted = null;
+
+    public UiccStateChangedLauncher(Context context, UiccController controller) {
+        sDeviceProvisioningPackage = context.getResources().getString(
+                R.string.config_deviceProvisioningPackage);
+        if (sDeviceProvisioningPackage != null && !sDeviceProvisioningPackage.isEmpty()) {
+            mContext = context;
+            mUiccController = controller;
+            mUiccController.registerForIccChanged(this, EVENT_ICC_CHANGED, null);
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch(msg.what) {
+            case (EVENT_ICC_CHANGED):
+                boolean shouldNotify = false;
+                if (mIsRestricted == null) {
+                    mIsRestricted = new boolean[TelephonyManager.getDefault().getPhoneCount()];
+                    shouldNotify = true;
+                }
+                UiccCard[] cards = mUiccController.getUiccCards();
+                for (int i = 0; cards != null && i < cards.length; ++i) {
+                    // Update only if restricted state changes.
+                    if ((cards[i] == null
+                            || cards[i].getCardState() != CardState.CARDSTATE_RESTRICTED)
+                            != mIsRestricted[i]) {
+                        mIsRestricted[i] = !mIsRestricted[i];
+                        shouldNotify = true;
+                    }
+                }
+                if (shouldNotify) {
+                    notifyStateChanged();
+                }
+                break;
+            default:
+                throw new RuntimeException("unexpected event not handled");
+        }
+    }
+
+    /**
+     * Send an explicit intent to device provisioning package.
+     */
+    private void notifyStateChanged() {
+        Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+        intent.setPackage(sDeviceProvisioningPackage);
+        try {
+            mContext.sendBroadcast(intent);
+        } catch (Exception e) {
+            Log.e(TAG, e.toString());
+        }
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UsimFileHandler.java b/com/android/internal/telephony/uicc/UsimFileHandler.java
new file mode 100644
index 0000000..b120eb6
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UsimFileHandler.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2006, 2012 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.internal.telephony.uicc;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.UiccCardApplication;
+
+/**
+ * {@hide}
+ * This class should be used to access files in USIM ADF
+ */
+public final class UsimFileHandler extends IccFileHandler implements IccConstants {
+    static final String LOG_TAG = "UsimFH";
+
+    public UsimFileHandler(UiccCardApplication app, String aid, CommandsInterface ci) {
+        super(app, aid, ci);
+    }
+
+    @Override
+    protected String getEFPath(int efid) {
+        switch(efid) {
+        case EF_SMS:
+        case EF_EXT5:
+        case EF_EXT6:
+        case EF_MWIS:
+        case EF_MBI:
+        case EF_SPN:
+        case EF_AD:
+        case EF_MBDN:
+        case EF_PNN:
+        case EF_OPL:
+        case EF_SPDI:
+        case EF_SST:
+        case EF_CFIS:
+        case EF_MAILBOX_CPHS:
+        case EF_VOICE_MAIL_INDICATOR_CPHS:
+        case EF_CFF_CPHS:
+        case EF_SPN_CPHS:
+        case EF_SPN_SHORT_CPHS:
+        case EF_FDN:
+        case EF_SDN:
+        case EF_EXT3:
+        case EF_MSISDN:
+        case EF_EXT2:
+        case EF_INFO_CPHS:
+        case EF_CSP_CPHS:
+        case EF_GID1:
+        case EF_GID2:
+        case EF_LI:
+        case EF_PLMN_W_ACT:
+        case EF_OPLMN_W_ACT:
+        case EF_HPLMN_W_ACT:
+        case EF_EHPLMN:
+        case EF_FPLMN:
+        case EF_LRPLMNSI:
+        case EF_HPPLMN:
+            return MF_SIM + DF_ADF;
+
+        case EF_PBR:
+            // we only support global phonebook.
+            return MF_SIM + DF_TELECOM + DF_PHONEBOOK;
+        }
+        String path = getCommonIccEFPath(efid);
+        if (path == null) {
+            // The EFids in USIM phone book entries are decided by the card manufacturer.
+            // So if we don't match any of the cases above and if its a USIM return
+            // the phone book path.
+            return MF_SIM + DF_TELECOM + DF_PHONEBOOK;
+        }
+        return path;
+    }
+
+    @Override
+    protected void logd(String msg) {
+        Rlog.d(LOG_TAG, msg);
+    }
+
+    @Override
+    protected void loge(String msg) {
+        Rlog.e(LOG_TAG, msg);
+    }
+}
diff --git a/com/android/internal/telephony/uicc/UsimServiceTable.java b/com/android/internal/telephony/uicc/UsimServiceTable.java
new file mode 100644
index 0000000..d00475c
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UsimServiceTable.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2011 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.internal.telephony.uicc;
+
+
+/**
+ * Wrapper class for the USIM Service Table EF.
+ * See 3GPP TS 31.102 Release 10 section 4.2.8
+ */
+public final class UsimServiceTable extends IccServiceTable {
+    public enum UsimService {
+        PHONEBOOK,
+        FDN,                                // Fixed Dialing Numbers
+        FDN_EXTENSION,                      // FDN extension data in EF_EXT2
+        SDN,                                // Service Dialing Numbers
+        SDN_EXTENSION,                      // SDN extension data in EF_EXT3
+        BDN,                                // Barred Dialing Numbers
+        BDN_EXTENSION,                      // BDN extension data in EF_EXT4
+        OUTGOING_CALL_INFO,
+        INCOMING_CALL_INFO,
+        SM_STORAGE,
+        SM_STATUS_REPORTS,
+        SM_SERVICE_PARAMS,
+        ADVICE_OF_CHARGE,
+        CAP_CONFIG_PARAMS_2,
+        CB_MESSAGE_ID,
+        CB_MESSAGE_ID_RANGES,
+        GROUP_ID_LEVEL_1,
+        GROUP_ID_LEVEL_2,
+        SPN,                                // Service Provider Name
+        USER_PLMN_SELECT,
+        MSISDN,
+        IMAGE,
+        LOCALISED_SERVICE_AREAS,
+        EMLPP,                              // Enhanced Multi-Level Precedence and Preemption
+        EMLPP_AUTO_ANSWER,
+        RFU,
+        GSM_ACCESS,
+        DATA_DL_VIA_SMS_PP,
+        DATA_DL_VIA_SMS_CB,
+        CALL_CONTROL_BY_USIM,
+        MO_SMS_CONTROL_BY_USIM,
+        RUN_AT_COMMAND,
+        IGNORED_1,
+        ENABLED_SERVICES_TABLE,
+        APN_CONTROL_LIST,
+        DEPERSONALISATION_CONTROL_KEYS,
+        COOPERATIVE_NETWORK_LIST,
+        GSM_SECURITY_CONTEXT,
+        CPBCCH_INFO,
+        INVESTIGATION_SCAN,
+        MEXE,
+        OPERATOR_PLMN_SELECT,
+        HPLMN_SELECT,
+        EXTENSION_5,                        // Extension data for ICI, OCI, MSISDN in EF_EXT5
+        PLMN_NETWORK_NAME,
+        OPERATOR_PLMN_LIST,
+        MBDN,                               // Mailbox Dialing Numbers
+        MWI_STATUS,                         // Message Waiting Indication status
+        CFI_STATUS,                         // Call Forwarding Indication status
+        IGNORED_2,
+        SERVICE_PROVIDER_DISPLAY_INFO,
+        MMS_NOTIFICATION,
+        MMS_NOTIFICATION_EXTENSION,         // MMS Notification extension data in EF_EXT8
+        GPRS_CALL_CONTROL_BY_USIM,
+        MMS_CONNECTIVITY_PARAMS,
+        NETWORK_INDICATION_OF_ALERTING,
+        VGCS_GROUP_ID_LIST,
+        VBS_GROUP_ID_LIST,
+        PSEUDONYM,
+        IWLAN_USER_PLMN_SELECT,
+        IWLAN_OPERATOR_PLMN_SELECT,
+        USER_WSID_LIST,
+        OPERATOR_WSID_LIST,
+        VGCS_SECURITY,
+        VBS_SECURITY,
+        WLAN_REAUTH_IDENTITY,
+        MM_STORAGE,
+        GBA,                                // Generic Bootstrapping Architecture
+        MBMS_SECURITY,
+        DATA_DL_VIA_USSD,
+        EQUIVALENT_HPLMN,
+        TERMINAL_PROFILE_AFTER_UICC_ACTIVATION,
+        EQUIVALENT_HPLMN_PRESENTATION,
+        LAST_RPLMN_SELECTION_INDICATION,
+        OMA_BCAST_PROFILE,
+        GBA_LOCAL_KEY_ESTABLISHMENT,
+        TERMINAL_APPLICATIONS,
+        SPN_ICON,
+        PLMN_NETWORK_NAME_ICON,
+        USIM_IP_CONNECTION_PARAMS,
+        IWLAN_HOME_ID_LIST,
+        IWLAN_EQUIVALENT_HPLMN_PRESENTATION,
+        IWLAN_HPLMN_PRIORITY_INDICATION,
+        IWLAN_LAST_REGISTERED_PLMN,
+        EPS_MOBILITY_MANAGEMENT_INFO,
+        ALLOWED_CSG_LISTS_AND_INDICATIONS,
+        CALL_CONTROL_ON_EPS_PDN_CONNECTION_BY_USIM,
+        HPLMN_DIRECT_ACCESS,
+        ECALL_DATA,
+        OPERATOR_CSG_LISTS_AND_INDICATIONS,
+        SM_OVER_IP,
+        CSG_DISPLAY_CONTROL,
+        IMS_COMMUNICATION_CONTROL_BY_USIM,
+        EXTENDED_TERMINAL_APPLICATIONS,
+        UICC_ACCESS_TO_IMS,
+        NAS_CONFIG_BY_USIM
+    }
+
+    public UsimServiceTable(byte[] table) {
+        super(table);
+    }
+
+    public boolean isAvailable(UsimService service) {
+        return super.isAvailable(service.ordinal());
+    }
+
+    @Override
+    protected String getTag() {
+        return "UsimServiceTable";
+    }
+
+    @Override
+    protected Object[] getValues() {
+        return UsimService.values();
+    }
+}
diff --git a/com/android/internal/telephony/uicc/VoiceMailConstants.java b/com/android/internal/telephony/uicc/VoiceMailConstants.java
new file mode 100644
index 0000000..2c70648
--- /dev/null
+++ b/com/android/internal/telephony/uicc/VoiceMailConstants.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2006 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.internal.telephony.uicc;
+
+import android.os.Environment;
+import android.util.Xml;
+import android.telephony.Rlog;
+
+import java.util.HashMap;
+import java.io.FileReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import com.android.internal.util.XmlUtils;
+
+/**
+ * {@hide}
+ */
+class VoiceMailConstants {
+    private HashMap<String, String[]> CarrierVmMap;
+
+
+    static final String LOG_TAG = "VoiceMailConstants";
+    static final String PARTNER_VOICEMAIL_PATH ="etc/voicemail-conf.xml";
+
+    static final int NAME = 0;
+    static final int NUMBER = 1;
+    static final int TAG = 2;
+    static final int SIZE = 3;
+
+    VoiceMailConstants () {
+        CarrierVmMap = new HashMap<String, String[]>();
+        loadVoiceMail();
+    }
+
+    boolean containsCarrier(String carrier) {
+        return CarrierVmMap.containsKey(carrier);
+    }
+
+    String getCarrierName(String carrier) {
+        String[] data = CarrierVmMap.get(carrier);
+        return data[NAME];
+    }
+
+    String getVoiceMailNumber(String carrier) {
+        String[] data = CarrierVmMap.get(carrier);
+        return data[NUMBER];
+    }
+
+    String getVoiceMailTag(String carrier) {
+        String[] data = CarrierVmMap.get(carrier);
+        return data[TAG];
+    }
+
+    private void loadVoiceMail() {
+        FileReader vmReader;
+
+        final File vmFile = new File(Environment.getRootDirectory(),
+                PARTNER_VOICEMAIL_PATH);
+
+        try {
+            vmReader = new FileReader(vmFile);
+        } catch (FileNotFoundException e) {
+            Rlog.w(LOG_TAG, "Can't open " +
+                    Environment.getRootDirectory() + "/" + PARTNER_VOICEMAIL_PATH);
+            return;
+        }
+
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(vmReader);
+
+            XmlUtils.beginDocument(parser, "voicemail");
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+
+                String name = parser.getName();
+                if (!"voicemail".equals(name)) {
+                    break;
+                }
+
+                String[] data = new String[SIZE];
+                String numeric = parser.getAttributeValue(null, "numeric");
+                data[NAME]     = parser.getAttributeValue(null, "carrier");
+                data[NUMBER]   = parser.getAttributeValue(null, "vmnumber");
+                data[TAG]      = parser.getAttributeValue(null, "vmtag");
+
+                CarrierVmMap.put(numeric, data);
+            }
+        } catch (XmlPullParserException e) {
+            Rlog.w(LOG_TAG, "Exception in Voicemail parser " + e);
+        } catch (IOException e) {
+            Rlog.w(LOG_TAG, "Exception in Voicemail parser " + e);
+        } finally {
+            try {
+                if (vmReader != null) {
+                    vmReader.close();
+                }
+            } catch (IOException e) {}
+        }
+    }
+}
diff --git a/com/android/internal/telephony/util/NotificationChannelController.java b/com/android/internal/telephony/util/NotificationChannelController.java
new file mode 100644
index 0000000..54d7d1a
--- /dev/null
+++ b/com/android/internal/telephony/util/NotificationChannelController.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 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.internal.telephony.util;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+
+import com.android.internal.R;
+
+import java.util.Arrays;
+
+
+public class NotificationChannelController {
+
+    /**
+     * list of {@link android.app.NotificationChannel} for telephony service.
+     */
+    public static final String CHANNEL_ID_ALERT = "alert";
+    public static final String CHANNEL_ID_CALL_FORWARD = "callForward";
+    public static final String CHANNEL_ID_MOBILE_DATA_STATUS = "mobileDataAlertNew";
+    public static final String CHANNEL_ID_SMS = "sms";
+    public static final String CHANNEL_ID_VOICE_MAIL = "voiceMail";
+    public static final String CHANNEL_ID_WFC = "wfc";
+
+    /** deprecated channel, replaced with @see #CHANNEL_ID_MOBILE_DATA_STATUS */
+    private static final String CHANNEL_ID_MOBILE_DATA_ALERT_DEPRECATED = "mobileDataAlert";
+
+    /**
+     * Creates all notification channels and registers with NotificationManager. If a channel
+     * with the same ID is already registered, NotificationManager will ignore this call.
+     */
+    private static void createAll(Context context) {
+        final NotificationChannel alertChannel = new NotificationChannel(
+                CHANNEL_ID_ALERT,
+                context.getText(R.string.notification_channel_network_alert),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        alertChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
+
+        final NotificationChannel mobileDataStatusChannel = new NotificationChannel(
+                CHANNEL_ID_MOBILE_DATA_STATUS,
+                context.getText(R.string.notification_channel_mobile_data_status),
+                NotificationManager.IMPORTANCE_LOW);
+        // allow users to block notifications from system
+        mobileDataStatusChannel.setBlockableSystem(true);
+
+        context.getSystemService(NotificationManager.class)
+                .createNotificationChannels(Arrays.asList(
+                new NotificationChannel(CHANNEL_ID_CALL_FORWARD,
+                        context.getText(R.string.notification_channel_call_forward),
+                        NotificationManager.IMPORTANCE_LOW),
+                new NotificationChannel(CHANNEL_ID_SMS,
+                        context.getText(R.string.notification_channel_sms),
+                        NotificationManager.IMPORTANCE_HIGH),
+                new NotificationChannel(CHANNEL_ID_WFC,
+                        context.getText(R.string.notification_channel_wfc),
+                        NotificationManager.IMPORTANCE_LOW),
+                alertChannel,
+                mobileDataStatusChannel));
+        // only for update
+        if (getChannel(CHANNEL_ID_VOICE_MAIL, context) != null) {
+            migrateVoicemailNotificationSettings(context);
+        }
+        // after channel has been created there is no way to change the channel setting
+        // programmatically. delete the old channel and create a new one with a new ID.
+        if (getChannel(CHANNEL_ID_MOBILE_DATA_ALERT_DEPRECATED, context) != null) {
+            context.getSystemService(NotificationManager.class)
+                    .deleteNotificationChannel(CHANNEL_ID_MOBILE_DATA_ALERT_DEPRECATED);
+        }
+    }
+
+    public NotificationChannelController(Context context) {
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_LOCALE_CHANGED);
+        intentFilter.addAction(Intent.ACTION_SIM_STATE_CHANGED);
+        context.registerReceiver(mBroadcastReceiver, intentFilter);
+        createAll(context);
+    }
+
+    public static NotificationChannel getChannel(String channelId, Context context) {
+        return context.getSystemService(NotificationManager.class)
+                .getNotificationChannel(channelId);
+    }
+
+    /**
+     * migrate deprecated voicemail notification settings to initial notification channel settings
+     * {@link VoicemailNotificationSettingsUtil#getRingTonePreference(Context)}}
+     * {@link VoicemailNotificationSettingsUtil#getVibrationPreference(Context)}
+     * notification settings are based on subId, only migrate if sub id matches.
+     * otherwise fallback to predefined voicemail channel settings.
+     * @param context
+     */
+    private static void migrateVoicemailNotificationSettings(Context context) {
+        final NotificationChannel voiceMailChannel = new NotificationChannel(
+                CHANNEL_ID_VOICE_MAIL,
+                context.getText(R.string.notification_channel_voice_mail),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        voiceMailChannel.enableVibration(
+                VoicemailNotificationSettingsUtil.getVibrationPreference(context));
+        Uri sound = VoicemailNotificationSettingsUtil.getRingTonePreference(context);
+        voiceMailChannel.setSound(
+                (sound == null) ? Settings.System.DEFAULT_NOTIFICATION_URI : sound,
+                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
+        context.getSystemService(NotificationManager.class)
+                .createNotificationChannel(voiceMailChannel);
+    }
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
+                // rename all notification channels on locale change
+                createAll(context);
+            } else if (Intent.ACTION_SIM_STATE_CHANGED.equals(intent.getAction())) {
+                // migrate voicemail notification settings on sim load
+                if (SubscriptionManager.INVALID_SUBSCRIPTION_ID !=
+                        SubscriptionManager.getDefaultSubscriptionId()) {
+                    migrateVoicemailNotificationSettings(context);
+                }
+            }
+        }
+    };
+}
diff --git a/com/android/internal/telephony/util/VoicemailNotificationSettingsUtil.java b/com/android/internal/telephony/util/VoicemailNotificationSettingsUtil.java
new file mode 100644
index 0000000..d8988e3
--- /dev/null
+++ b/com/android/internal/telephony/util/VoicemailNotificationSettingsUtil.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2014 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.internal.telephony.util;
+
+import android.app.NotificationChannel;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+public class VoicemailNotificationSettingsUtil {
+    private static final String VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY_PREFIX =
+            "voicemail_notification_ringtone_";
+    private static final String VOICEMAIL_NOTIFICATION_VIBRATION_SHARED_PREFS_KEY_PREFIX =
+            "voicemail_notification_vibrate_";
+
+    // Old voicemail notification vibration string constants used for migration.
+    private static final String OLD_VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY =
+            "button_voicemail_notification_ringtone_key";
+    private static final String OLD_VOICEMAIL_NOTIFICATION_VIBRATION_SHARED_PREFS_KEY =
+            "button_voicemail_notification_vibrate_key";
+    private static final String OLD_VOICEMAIL_VIBRATE_WHEN_SHARED_PREFS_KEY =
+            "button_voicemail_notification_vibrate_when_key";
+    private static final String OLD_VOICEMAIL_RINGTONE_SHARED_PREFS_KEY =
+            "button_voicemail_notification_ringtone_key";
+    private static final String OLD_VOICEMAIL_VIBRATION_ALWAYS = "always";
+    private static final String OLD_VOICEMAIL_VIBRATION_NEVER = "never";
+
+    public static void setVibrationEnabled(Context context, boolean isEnabled) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        SharedPreferences.Editor editor = prefs.edit();
+        editor.putBoolean(getVoicemailVibrationSharedPrefsKey(), isEnabled);
+        editor.commit();
+    }
+
+    public static boolean isVibrationEnabled(Context context) {
+        final NotificationChannel channel = NotificationChannelController.getChannel(
+                NotificationChannelController.CHANNEL_ID_VOICE_MAIL, context);
+        return (channel != null) ? channel.shouldVibrate() : getVibrationPreference(context);
+    }
+
+    public static boolean getVibrationPreference(Context context) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        migrateVoicemailVibrationSettingsIfNeeded(context, prefs);
+        return prefs.getBoolean(getVoicemailVibrationSharedPrefsKey(), false /* defValue */);
+    }
+
+   public static void setRingtoneUri(Context context, Uri ringtoneUri) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        String ringtoneUriStr = ringtoneUri != null ? ringtoneUri.toString() : "";
+
+        SharedPreferences.Editor editor = prefs.edit();
+        editor.putString(getVoicemailRingtoneSharedPrefsKey(), ringtoneUriStr);
+        editor.commit();
+    }
+
+    public static Uri getRingtoneUri(Context context) {
+        final NotificationChannel channel = NotificationChannelController.getChannel(
+                NotificationChannelController.CHANNEL_ID_VOICE_MAIL, context);
+        return (channel != null) ? channel.getSound() : getRingTonePreference(context);
+    }
+
+    public static Uri getRingTonePreference(Context context) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        migrateVoicemailRingtoneSettingsIfNeeded(context, prefs);
+        String uriString = prefs.getString(
+                getVoicemailRingtoneSharedPrefsKey(),
+                Settings.System.DEFAULT_NOTIFICATION_URI.toString());
+        return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null;
+    }
+
+    /**
+     * Migrate voicemail settings from {@link #OLD_VIBRATE_WHEN_KEY} or
+     * {@link #OLD_VOICEMAIL_NOTIFICATION_VIBRATE_KEY}.
+     *
+     * TODO: Add helper which migrates settings from old version to new version.
+     */
+    private static void migrateVoicemailVibrationSettingsIfNeeded(
+            Context context, SharedPreferences prefs) {
+        String key = getVoicemailVibrationSharedPrefsKey();
+        TelephonyManager telephonyManager = TelephonyManager.from(context);
+
+        // Skip if a preference exists, or if phone is MSIM.
+        if (prefs.contains(key) || telephonyManager.getPhoneCount() != 1) {
+            return;
+        }
+
+        if (prefs.contains(OLD_VOICEMAIL_NOTIFICATION_VIBRATION_SHARED_PREFS_KEY)) {
+            boolean voicemailVibrate = prefs.getBoolean(
+                    OLD_VOICEMAIL_NOTIFICATION_VIBRATION_SHARED_PREFS_KEY, false /* defValue */);
+
+            SharedPreferences.Editor editor = prefs.edit();
+            editor.putBoolean(key, voicemailVibrate)
+                    .remove(OLD_VOICEMAIL_VIBRATE_WHEN_SHARED_PREFS_KEY)
+                    .commit();
+        }
+
+        if (prefs.contains(OLD_VOICEMAIL_VIBRATE_WHEN_SHARED_PREFS_KEY)) {
+            // If vibrateWhen is always, then voicemailVibrate should be true.
+            // If it is "only in silent mode", or "never", then voicemailVibrate should be false.
+            String vibrateWhen = prefs.getString(
+                    OLD_VOICEMAIL_VIBRATE_WHEN_SHARED_PREFS_KEY, OLD_VOICEMAIL_VIBRATION_NEVER);
+            boolean voicemailVibrate = vibrateWhen.equals(OLD_VOICEMAIL_VIBRATION_ALWAYS);
+
+            SharedPreferences.Editor editor = prefs.edit();
+            editor.putBoolean(key, voicemailVibrate)
+                    .remove(OLD_VOICEMAIL_NOTIFICATION_VIBRATION_SHARED_PREFS_KEY)
+                    .commit();
+        }
+    }
+
+    /**
+     * Migrate voicemail settings from OLD_VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY.
+     *
+     * TODO: Add helper which migrates settings from old version to new version.
+     */
+    private static void migrateVoicemailRingtoneSettingsIfNeeded(
+            Context context, SharedPreferences prefs) {
+        String key = getVoicemailRingtoneSharedPrefsKey();
+        TelephonyManager telephonyManager = TelephonyManager.from(context);
+
+        // Skip if a preference exists, or if phone is MSIM.
+        if (prefs.contains(key) || telephonyManager.getPhoneCount() != 1) {
+            return;
+        }
+
+        if (prefs.contains(OLD_VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY)) {
+            String uriString = prefs.getString(
+                    OLD_VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY, null /* defValue */);
+
+            SharedPreferences.Editor editor = prefs.edit();
+            editor.putString(key, uriString)
+                    .remove(OLD_VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY)
+                    .commit();
+        }
+    }
+
+    private static String getVoicemailVibrationSharedPrefsKey() {
+        return VOICEMAIL_NOTIFICATION_VIBRATION_SHARED_PREFS_KEY_PREFIX
+                + SubscriptionManager.getDefaultSubscriptionId();
+    }
+
+    private static String getVoicemailRingtoneSharedPrefsKey() {
+        return VOICEMAIL_NOTIFICATION_RINGTONE_SHARED_PREFS_KEY_PREFIX
+                + SubscriptionManager.getDefaultSubscriptionId();
+    }
+}
diff --git a/com/android/internal/transition/EpicenterTranslateClipReveal.java b/com/android/internal/transition/EpicenterTranslateClipReveal.java
new file mode 100644
index 0000000..2c10297
--- /dev/null
+++ b/com/android/internal/transition/EpicenterTranslateClipReveal.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2015 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.internal.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.TypeEvaluator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.transition.TransitionValues;
+import android.transition.Visibility;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.R;
+
+/**
+ * EpicenterTranslateClipReveal captures the clip bounds and translation values
+ * before and after the scene change and animates between those and the
+ * epicenter bounds during a visibility transition.
+ */
+public class EpicenterTranslateClipReveal extends Visibility {
+    private static final String PROPNAME_CLIP = "android:epicenterReveal:clip";
+    private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds";
+    private static final String PROPNAME_TRANSLATE_X = "android:epicenterReveal:translateX";
+    private static final String PROPNAME_TRANSLATE_Y = "android:epicenterReveal:translateY";
+    private static final String PROPNAME_TRANSLATE_Z = "android:epicenterReveal:translateZ";
+    private static final String PROPNAME_Z = "android:epicenterReveal:z";
+
+    private final TimeInterpolator mInterpolatorX;
+    private final TimeInterpolator mInterpolatorY;
+    private final TimeInterpolator mInterpolatorZ;
+
+    public EpicenterTranslateClipReveal() {
+        mInterpolatorX = null;
+        mInterpolatorY = null;
+        mInterpolatorZ = null;
+    }
+
+    public EpicenterTranslateClipReveal(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.EpicenterTranslateClipReveal, 0, 0);
+
+        final int interpolatorX = a.getResourceId(
+                R.styleable.EpicenterTranslateClipReveal_interpolatorX, 0);
+        if (interpolatorX != 0) {
+            mInterpolatorX = AnimationUtils.loadInterpolator(context, interpolatorX);
+        } else {
+            mInterpolatorX = TransitionConstants.LINEAR_OUT_SLOW_IN;
+        }
+
+        final int interpolatorY = a.getResourceId(
+                R.styleable.EpicenterTranslateClipReveal_interpolatorY, 0);
+        if (interpolatorY != 0) {
+            mInterpolatorY = AnimationUtils.loadInterpolator(context, interpolatorY);
+        } else {
+            mInterpolatorY = TransitionConstants.FAST_OUT_SLOW_IN;
+        }
+
+        final int interpolatorZ = a.getResourceId(
+                R.styleable.EpicenterTranslateClipReveal_interpolatorZ, 0);
+        if (interpolatorZ != 0) {
+            mInterpolatorZ = AnimationUtils.loadInterpolator(context, interpolatorZ);
+        } else {
+            mInterpolatorZ = TransitionConstants.FAST_OUT_SLOW_IN;
+        }
+
+        a.recycle();
+    }
+
+    @Override
+    public void captureStartValues(TransitionValues transitionValues) {
+        super.captureStartValues(transitionValues);
+        captureValues(transitionValues);
+    }
+
+    @Override
+    public void captureEndValues(TransitionValues transitionValues) {
+        super.captureEndValues(transitionValues);
+        captureValues(transitionValues);
+    }
+
+    private void captureValues(TransitionValues values) {
+        final View view = values.view;
+        if (view.getVisibility() == View.GONE) {
+            return;
+        }
+
+        final Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight());
+        values.values.put(PROPNAME_BOUNDS, bounds);
+        values.values.put(PROPNAME_TRANSLATE_X, view.getTranslationX());
+        values.values.put(PROPNAME_TRANSLATE_Y, view.getTranslationY());
+        values.values.put(PROPNAME_TRANSLATE_Z, view.getTranslationZ());
+        values.values.put(PROPNAME_Z, view.getZ());
+
+        final Rect clip = view.getClipBounds();
+        values.values.put(PROPNAME_CLIP, clip);
+    }
+
+    @Override
+    public Animator onAppear(ViewGroup sceneRoot, View view,
+            TransitionValues startValues, TransitionValues endValues) {
+        if (endValues == null) {
+            return null;
+        }
+
+        final Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+        final Rect startBounds = getEpicenterOrCenter(endBounds);
+        final float startX = startBounds.centerX() - endBounds.centerX();
+        final float startY = startBounds.centerY() - endBounds.centerY();
+        final float startZ = 0 - (float) endValues.values.get(PROPNAME_Z);
+
+        // Translate the view to be centered on the epicenter.
+        view.setTranslationX(startX);
+        view.setTranslationY(startY);
+        view.setTranslationZ(startZ);
+
+        final float endX = (float) endValues.values.get(PROPNAME_TRANSLATE_X);
+        final float endY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y);
+        final float endZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z);
+
+        final Rect endClip = getBestRect(endValues);
+        final Rect startClip = getEpicenterOrCenter(endClip);
+
+        // Prepare the view.
+        view.setClipBounds(startClip);
+
+        final State startStateX = new State(startClip.left, startClip.right, startX);
+        final State endStateX = new State(endClip.left, endClip.right, endX);
+        final State startStateY = new State(startClip.top, startClip.bottom, startY);
+        final State endStateY = new State(endClip.top, endClip.bottom, endY);
+
+        return createRectAnimator(view, startStateX, startStateY, startZ, endStateX, endStateY,
+                endZ, endValues, mInterpolatorX, mInterpolatorY, mInterpolatorZ);
+    }
+
+    @Override
+    public Animator onDisappear(ViewGroup sceneRoot, View view,
+            TransitionValues startValues, TransitionValues endValues) {
+        if (startValues == null) {
+            return null;
+        }
+
+        final Rect startBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+        final Rect endBounds = getEpicenterOrCenter(startBounds);
+        final float endX = endBounds.centerX() - startBounds.centerX();
+        final float endY = endBounds.centerY() - startBounds.centerY();
+        final float endZ = 0 - (float) startValues.values.get(PROPNAME_Z);
+
+        final float startX = (float) endValues.values.get(PROPNAME_TRANSLATE_X);
+        final float startY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y);
+        final float startZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z);
+
+        final Rect startClip = getBestRect(startValues);
+        final Rect endClip = getEpicenterOrCenter(startClip);
+
+        // Prepare the view.
+        view.setClipBounds(startClip);
+
+        final State startStateX = new State(startClip.left, startClip.right, startX);
+        final State endStateX = new State(endClip.left, endClip.right, endX);
+        final State startStateY = new State(startClip.top, startClip.bottom, startY);
+        final State endStateY = new State(endClip.top, endClip.bottom, endY);
+
+        return createRectAnimator(view, startStateX, startStateY, startZ, endStateX, endStateY,
+                endZ, endValues, mInterpolatorX, mInterpolatorY, mInterpolatorZ);
+    }
+
+    private Rect getEpicenterOrCenter(Rect bestRect) {
+        final Rect epicenter = getEpicenter();
+        if (epicenter != null) {
+            return epicenter;
+        }
+
+        final int centerX = bestRect.centerX();
+        final int centerY = bestRect.centerY();
+        return new Rect(centerX, centerY, centerX, centerY);
+    }
+
+    private Rect getBestRect(TransitionValues values) {
+        final Rect clipRect = (Rect) values.values.get(PROPNAME_CLIP);
+        if (clipRect == null) {
+            return (Rect) values.values.get(PROPNAME_BOUNDS);
+        }
+        return clipRect;
+    }
+
+    private static Animator createRectAnimator(final View view, State startX, State startY,
+            float startZ, State endX, State endY, float endZ, TransitionValues endValues,
+            TimeInterpolator interpolatorX, TimeInterpolator interpolatorY,
+            TimeInterpolator interpolatorZ) {
+        final StateEvaluator evaluator = new StateEvaluator();
+
+        final ObjectAnimator animZ = ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, startZ, endZ);
+        if (interpolatorZ != null) {
+            animZ.setInterpolator(interpolatorZ);
+        }
+
+        final StateProperty propX = new StateProperty(StateProperty.TARGET_X);
+        final ObjectAnimator animX = ObjectAnimator.ofObject(view, propX, evaluator, startX, endX);
+        if (interpolatorX != null) {
+            animX.setInterpolator(interpolatorX);
+        }
+
+        final StateProperty propY = new StateProperty(StateProperty.TARGET_Y);
+        final ObjectAnimator animY = ObjectAnimator.ofObject(view, propY, evaluator, startY, endY);
+        if (interpolatorY != null) {
+            animY.setInterpolator(interpolatorY);
+        }
+
+        final Rect terminalClip = (Rect) endValues.values.get(PROPNAME_CLIP);
+        final AnimatorListenerAdapter animatorListener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setClipBounds(terminalClip);
+            }
+        };
+
+        final AnimatorSet animSet = new AnimatorSet();
+        animSet.playTogether(animX, animY, animZ);
+        animSet.addListener(animatorListener);
+        return animSet;
+    }
+
+    private static class State {
+        int lower;
+        int upper;
+        float trans;
+
+        public State() {}
+
+        public State(int lower, int upper, float trans) {
+            this.lower = lower;
+            this.upper = upper;
+            this.trans = trans;
+        }
+    }
+
+    private static class StateEvaluator implements TypeEvaluator<State> {
+        private final State mTemp = new State();
+
+        @Override
+        public State evaluate(float fraction, State startValue, State endValue) {
+            mTemp.upper = startValue.upper + (int) ((endValue.upper - startValue.upper) * fraction);
+            mTemp.lower = startValue.lower + (int) ((endValue.lower - startValue.lower) * fraction);
+            mTemp.trans = startValue.trans + (int) ((endValue.trans - startValue.trans) * fraction);
+            return mTemp;
+        }
+    }
+
+    private static class StateProperty extends Property<View, State> {
+        public static final char TARGET_X = 'x';
+        public static final char TARGET_Y = 'y';
+
+        private final Rect mTempRect = new Rect();
+        private final State mTempState = new State();
+
+        private final int mTargetDimension;
+
+        public StateProperty(char targetDimension) {
+            super(State.class, "state_" + targetDimension);
+
+            mTargetDimension = targetDimension;
+        }
+
+        @Override
+        public State get(View object) {
+            final Rect tempRect = mTempRect;
+            if (!object.getClipBounds(tempRect)) {
+                tempRect.setEmpty();
+            }
+            final State tempState = mTempState;
+            if (mTargetDimension == TARGET_X) {
+                tempState.trans = object.getTranslationX();
+                tempState.lower = tempRect.left + (int) tempState.trans;
+                tempState.upper = tempRect.right + (int) tempState.trans;
+            } else {
+                tempState.trans = object.getTranslationY();
+                tempState.lower = tempRect.top + (int) tempState.trans;
+                tempState.upper = tempRect.bottom + (int) tempState.trans;
+            }
+            return tempState;
+        }
+
+        @Override
+        public void set(View object, State value) {
+            final Rect tempRect = mTempRect;
+            if (object.getClipBounds(tempRect)) {
+                if (mTargetDimension == TARGET_X) {
+                    tempRect.left = value.lower - (int) value.trans;
+                    tempRect.right = value.upper - (int) value.trans;
+                } else {
+                    tempRect.top = value.lower - (int) value.trans;
+                    tempRect.bottom = value.upper - (int) value.trans;
+                }
+                object.setClipBounds(tempRect);
+            }
+
+            if (mTargetDimension == TARGET_X) {
+                object.setTranslationX(value.trans);
+            } else {
+                object.setTranslationY(value.trans);
+            }
+        }
+    }
+}
diff --git a/com/android/internal/transition/TransitionConstants.java b/com/android/internal/transition/TransitionConstants.java
new file mode 100644
index 0000000..e9015e6
--- /dev/null
+++ b/com/android/internal/transition/TransitionConstants.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 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.internal.transition;
+
+import android.animation.TimeInterpolator;
+import android.view.animation.PathInterpolator;
+
+class TransitionConstants {
+    static final TimeInterpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0, 0, 0.2f, 1);
+    static final TimeInterpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0, 0.2f, 1);
+}
diff --git a/com/android/internal/util/ArrayUtils.java b/com/android/internal/util/ArrayUtils.java
new file mode 100644
index 0000000..91bc681
--- /dev/null
+++ b/com/android/internal/util/ArrayUtils.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2006 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.internal.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArraySet;
+
+import dalvik.system.VMRuntime;
+
+import libcore.util.EmptyArray;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * ArrayUtils contains some methods that you can call to find out
+ * the most efficient increments by which to grow arrays.
+ */
+public class ArrayUtils {
+    private static final int CACHE_SIZE = 73;
+    private static Object[] sCache = new Object[CACHE_SIZE];
+
+    private ArrayUtils() { /* cannot be instantiated */ }
+
+    public static byte[] newUnpaddedByteArray(int minLen) {
+        return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen);
+    }
+
+    public static char[] newUnpaddedCharArray(int minLen) {
+        return (char[])VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen);
+    }
+
+    public static int[] newUnpaddedIntArray(int minLen) {
+        return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen);
+    }
+
+    public static boolean[] newUnpaddedBooleanArray(int minLen) {
+        return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen);
+    }
+
+    public static long[] newUnpaddedLongArray(int minLen) {
+        return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen);
+    }
+
+    public static float[] newUnpaddedFloatArray(int minLen) {
+        return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen);
+    }
+
+    public static Object[] newUnpaddedObjectArray(int minLen) {
+        return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T[] newUnpaddedArray(Class<T> clazz, int minLen) {
+        return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen);
+    }
+
+    /**
+     * Checks if the beginnings of two byte arrays are equal.
+     *
+     * @param array1 the first byte array
+     * @param array2 the second byte array
+     * @param length the number of bytes to check
+     * @return true if they're equal, false otherwise
+     */
+    public static boolean equals(byte[] array1, byte[] array2, int length) {
+        if (length < 0) {
+            throw new IllegalArgumentException();
+        }
+
+        if (array1 == array2) {
+            return true;
+        }
+        if (array1 == null || array2 == null || array1.length < length || array2.length < length) {
+            return false;
+        }
+        for (int i = 0; i < length; i++) {
+            if (array1[i] != array2[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns an empty array of the specified type.  The intent is that
+     * it will return the same empty array every time to avoid reallocation,
+     * although this is not guaranteed.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T[] emptyArray(Class<T> kind) {
+        if (kind == Object.class) {
+            return (T[]) EmptyArray.OBJECT;
+        }
+
+        int bucket = (kind.hashCode() & 0x7FFFFFFF) % CACHE_SIZE;
+        Object cache = sCache[bucket];
+
+        if (cache == null || cache.getClass().getComponentType() != kind) {
+            cache = Array.newInstance(kind, 0);
+            sCache[bucket] = cache;
+
+            // Log.e("cache", "new empty " + kind.getName() + " at " + bucket);
+        }
+
+        return (T[]) cache;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable Collection<?> array) {
+        return array == null || array.isEmpty();
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static <T> boolean isEmpty(@Nullable T[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable int[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable long[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable byte[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable boolean[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Length of the given array or 0 if it's null.
+     */
+    public static int size(@Nullable Object[] array) {
+        return array == null ? 0 : array.length;
+    }
+
+    /**
+     * Checks that value is present as at least one of the elements of the array.
+     * @param array the array to check in
+     * @param value the value to check for
+     * @return true if the value is present in the array
+     */
+    public static <T> boolean contains(@Nullable T[] array, T value) {
+        return indexOf(array, value) != -1;
+    }
+
+    /**
+     * Return first index of {@code value} in {@code array}, or {@code -1} if
+     * not found.
+     */
+    public static <T> int indexOf(@Nullable T[] array, T value) {
+        if (array == null) return -1;
+        for (int i = 0; i < array.length; i++) {
+            if (Objects.equals(array[i], value)) return i;
+        }
+        return -1;
+    }
+
+    /**
+     * Test if all {@code check} items are contained in {@code array}.
+     */
+    public static <T> boolean containsAll(@Nullable T[] array, T[] check) {
+        if (check == null) return true;
+        for (T checkItem : check) {
+            if (!contains(array, checkItem)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Test if any {@code check} items are contained in {@code array}.
+     */
+    public static <T> boolean containsAny(@Nullable T[] array, T[] check) {
+        if (check == null) return false;
+        for (T checkItem : check) {
+            if (contains(array, checkItem)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean contains(@Nullable int[] array, int value) {
+        if (array == null) return false;
+        for (int element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean contains(@Nullable long[] array, long value) {
+        if (array == null) return false;
+        for (long element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean contains(@Nullable char[] array, char value) {
+        if (array == null) return false;
+        for (char element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Test if all {@code check} items are contained in {@code array}.
+     */
+    public static <T> boolean containsAll(@Nullable char[] array, char[] check) {
+        if (check == null) return true;
+        for (char checkItem : check) {
+            if (!contains(array, checkItem)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static long total(@Nullable long[] array) {
+        long total = 0;
+        if (array != null) {
+            for (long value : array) {
+                total += value;
+            }
+        }
+        return total;
+    }
+
+    public static int[] convertToIntArray(List<Integer> list) {
+        int[] array = new int[list.size()];
+        for (int i = 0; i < list.size(); i++) {
+            array[i] = list.get(i);
+        }
+        return array;
+    }
+
+    /**
+     * Adds value to given array if not already present, providing set-like
+     * behavior.
+     */
+    @SuppressWarnings("unchecked")
+    public static @NonNull <T> T[] appendElement(Class<T> kind, @Nullable T[] array, T element) {
+        return appendElement(kind, array, element, false);
+    }
+
+    /**
+     * Adds value to given array.
+     */
+    @SuppressWarnings("unchecked")
+    public static @NonNull <T> T[] appendElement(Class<T> kind, @Nullable T[] array, T element,
+            boolean allowDuplicates) {
+        final T[] result;
+        final int end;
+        if (array != null) {
+            if (!allowDuplicates && contains(array, element)) return array;
+            end = array.length;
+            result = (T[])Array.newInstance(kind, end + 1);
+            System.arraycopy(array, 0, result, 0, end);
+        } else {
+            end = 0;
+            result = (T[])Array.newInstance(kind, 1);
+        }
+        result[end] = element;
+        return result;
+    }
+
+    /**
+     * Removes value from given array if present, providing set-like behavior.
+     */
+    @SuppressWarnings("unchecked")
+    public static @Nullable <T> T[] removeElement(Class<T> kind, @Nullable T[] array, T element) {
+        if (array != null) {
+            if (!contains(array, element)) return array;
+            final int length = array.length;
+            for (int i = 0; i < length; i++) {
+                if (Objects.equals(array[i], element)) {
+                    if (length == 1) {
+                        return null;
+                    }
+                    T[] result = (T[])Array.newInstance(kind, length - 1);
+                    System.arraycopy(array, 0, result, 0, i);
+                    System.arraycopy(array, i + 1, result, i, length - i - 1);
+                    return result;
+                }
+            }
+        }
+        return array;
+    }
+
+    /**
+     * Adds value to given array.
+     */
+    public static @NonNull int[] appendInt(@Nullable int[] cur, int val,
+            boolean allowDuplicates) {
+        if (cur == null) {
+            return new int[] { val };
+        }
+        final int N = cur.length;
+        if (!allowDuplicates) {
+            for (int i = 0; i < N; i++) {
+                if (cur[i] == val) {
+                    return cur;
+                }
+            }
+        }
+        int[] ret = new int[N + 1];
+        System.arraycopy(cur, 0, ret, 0, N);
+        ret[N] = val;
+        return ret;
+    }
+
+    /**
+     * Adds value to given array if not already present, providing set-like
+     * behavior.
+     */
+    public static @NonNull int[] appendInt(@Nullable int[] cur, int val) {
+        return appendInt(cur, val, false);
+    }
+
+    /**
+     * Removes value from given array if present, providing set-like behavior.
+     */
+    public static @Nullable int[] removeInt(@Nullable int[] cur, int val) {
+        if (cur == null) {
+            return null;
+        }
+        final int N = cur.length;
+        for (int i = 0; i < N; i++) {
+            if (cur[i] == val) {
+                int[] ret = new int[N - 1];
+                if (i > 0) {
+                    System.arraycopy(cur, 0, ret, 0, i);
+                }
+                if (i < (N - 1)) {
+                    System.arraycopy(cur, i + 1, ret, i, N - i - 1);
+                }
+                return ret;
+            }
+        }
+        return cur;
+    }
+
+    /**
+     * Removes value from given array if present, providing set-like behavior.
+     */
+    public static @Nullable String[] removeString(@Nullable String[] cur, String val) {
+        if (cur == null) {
+            return null;
+        }
+        final int N = cur.length;
+        for (int i = 0; i < N; i++) {
+            if (Objects.equals(cur[i], val)) {
+                String[] ret = new String[N - 1];
+                if (i > 0) {
+                    System.arraycopy(cur, 0, ret, 0, i);
+                }
+                if (i < (N - 1)) {
+                    System.arraycopy(cur, i + 1, ret, i, N - i - 1);
+                }
+                return ret;
+            }
+        }
+        return cur;
+    }
+
+    /**
+     * Adds value to given array if not already present, providing set-like
+     * behavior.
+     */
+    public static @NonNull long[] appendLong(@Nullable long[] cur, long val) {
+        if (cur == null) {
+            return new long[] { val };
+        }
+        final int N = cur.length;
+        for (int i = 0; i < N; i++) {
+            if (cur[i] == val) {
+                return cur;
+            }
+        }
+        long[] ret = new long[N + 1];
+        System.arraycopy(cur, 0, ret, 0, N);
+        ret[N] = val;
+        return ret;
+    }
+
+    /**
+     * Removes value from given array if present, providing set-like behavior.
+     */
+    public static @Nullable long[] removeLong(@Nullable long[] cur, long val) {
+        if (cur == null) {
+            return null;
+        }
+        final int N = cur.length;
+        for (int i = 0; i < N; i++) {
+            if (cur[i] == val) {
+                long[] ret = new long[N - 1];
+                if (i > 0) {
+                    System.arraycopy(cur, 0, ret, 0, i);
+                }
+                if (i < (N - 1)) {
+                    System.arraycopy(cur, i + 1, ret, i, N - i - 1);
+                }
+                return ret;
+            }
+        }
+        return cur;
+    }
+
+    public static @Nullable long[] cloneOrNull(@Nullable long[] array) {
+        return (array != null) ? array.clone() : null;
+    }
+
+    public static @Nullable <T> ArraySet<T> cloneOrNull(@Nullable ArraySet<T> array) {
+        return (array != null) ? new ArraySet<T>(array) : null;
+    }
+
+    public static @NonNull <T> ArraySet<T> add(@Nullable ArraySet<T> cur, T val) {
+        if (cur == null) {
+            cur = new ArraySet<>();
+        }
+        cur.add(val);
+        return cur;
+    }
+
+    public static @Nullable <T> ArraySet<T> remove(@Nullable ArraySet<T> cur, T val) {
+        if (cur == null) {
+            return null;
+        }
+        cur.remove(val);
+        if (cur.isEmpty()) {
+            return null;
+        } else {
+            return cur;
+        }
+    }
+
+    public static @NonNull <T> ArrayList<T> add(@Nullable ArrayList<T> cur, T val) {
+        if (cur == null) {
+            cur = new ArrayList<>();
+        }
+        cur.add(val);
+        return cur;
+    }
+
+    public static @Nullable <T> ArrayList<T> remove(@Nullable ArrayList<T> cur, T val) {
+        if (cur == null) {
+            return null;
+        }
+        cur.remove(val);
+        if (cur.isEmpty()) {
+            return null;
+        } else {
+            return cur;
+        }
+    }
+
+    public static <T> boolean contains(@Nullable Collection<T> cur, T val) {
+        return (cur != null) ? cur.contains(val) : false;
+    }
+
+    public static @Nullable <T> T[] trimToSize(@Nullable T[] array, int size) {
+        if (array == null || size == 0) {
+            return null;
+        } else if (array.length == size) {
+            return array;
+        } else {
+            return Arrays.copyOf(array, size);
+        }
+    }
+
+    /**
+     * Returns true if the two ArrayLists are equal with respect to the objects they contain.
+     * The objects must be in the same order and be reference equal (== not .equals()).
+     */
+    public static <T> boolean referenceEquals(ArrayList<T> a, ArrayList<T> b) {
+        if (a == b) {
+            return true;
+        }
+
+        final int sizeA = a.size();
+        final int sizeB = b.size();
+        if (a == null || b == null || sizeA != sizeB) {
+            return false;
+        }
+
+        boolean diff = false;
+        for (int i = 0; i < sizeA && !diff; i++) {
+            diff |= a.get(i) != b.get(i);
+        }
+        return !diff;
+    }
+
+    /**
+     * Removes elements that match the predicate in an efficient way that alters the order of
+     * elements in the collection. This should only be used if order is not important.
+     * @param collection The ArrayList from which to remove elements.
+     * @param predicate The predicate that each element is tested against.
+     * @return the number of elements removed.
+     */
+    public static <T> int unstableRemoveIf(@Nullable ArrayList<T> collection,
+                                           @NonNull java.util.function.Predicate<T> predicate) {
+        if (collection == null) {
+            return 0;
+        }
+
+        final int size = collection.size();
+        int leftIdx = 0;
+        int rightIdx = size - 1;
+        while (leftIdx <= rightIdx) {
+            // Find the next element to remove moving left to right.
+            while (leftIdx < size && !predicate.test(collection.get(leftIdx))) {
+                leftIdx++;
+            }
+
+            // Find the next element to keep moving right to left.
+            while (rightIdx > leftIdx && predicate.test(collection.get(rightIdx))) {
+                rightIdx--;
+            }
+
+            if (leftIdx >= rightIdx) {
+                // Done.
+                break;
+            }
+
+            Collections.swap(collection, leftIdx, rightIdx);
+            leftIdx++;
+            rightIdx--;
+        }
+
+        // leftIdx is now at the end.
+        for (int i = size - 1; i >= leftIdx; i--) {
+            collection.remove(i);
+        }
+        return size - leftIdx;
+    }
+
+    public static @NonNull String[] defeatNullable(@Nullable String[] val) {
+        return (val != null) ? val : EmptyArray.STRING;
+    }
+}
diff --git a/com/android/internal/util/AsyncChannel.java b/com/android/internal/util/AsyncChannel.java
new file mode 100644
index 0000000..e760f25
--- /dev/null
+++ b/com/android/internal/util/AsyncChannel.java
@@ -0,0 +1,932 @@
+/**
+ * Copyright (C) 2010 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.internal.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.util.Stack;
+
+/**
+ * <p>An asynchronous channel between two handlers.</p>
+ *
+ * <p>The handlers maybe in the same process or in another process. There
+ * are two protocol styles that can be used with an AysncChannel. The
+ * first is a simple request/reply protocol where the server does
+ * not need to know which client is issuing the request.</p>
+ *
+ * <p>In a simple request/reply protocol the client/source sends requests to the
+ * server/destination. And the server uses the replyToMessage methods.
+ * In this usage model there is no need for the destination to
+ * use the connect methods. The typical sequence of operations is:</p>
+ *<ol>
+ *   <li>Client calls AsyncChannel#connectSync or Asynchronously:</li>
+ *      <ol>For an asynchronous half connection client calls AsyncChannel#connect.</ol>
+ *          <li>Client receives CMD_CHANNEL_HALF_CONNECTED from AsyncChannel</li>
+ *      </ol>
+ *   <li><code>comm-loop:</code></li>
+ *   <li>Client calls AsyncChannel#sendMessage</li>
+ *   <li>Server processes messages and optionally replies using AsyncChannel#replyToMessage
+ *   <li>Loop to <code>comm-loop</code> until done</li>
+ *   <li>When done Client calls {@link AsyncChannel#disconnect}</li>
+ *   <li>Client/Server receives CMD_CHANNEL_DISCONNECTED from AsyncChannel</li>
+ *</ol>
+ *<br/>
+ * <p>A second usage model is where the server/destination needs to know
+ * which client it's connected too. For example the server needs to
+ * send unsolicited messages back to the client. Or the server keeps
+ * different state for each client. In this model the server will also
+ * use the connect methods. The typical sequence of operation is:</p>
+ *<ol>
+ *   <li>Client calls AsyncChannel#fullyConnectSync or Asynchronously:<li>
+ *      <ol>For an asynchronous full connection it calls AsyncChannel#connect</li>
+ *          <li>Client receives CMD_CHANNEL_HALF_CONNECTED from AsyncChannel</li>
+ *          <li>Client calls AsyncChannel#sendMessage(CMD_CHANNEL_FULL_CONNECTION)</li>
+ *      </ol>
+ *   <li>Server receives CMD_CHANNEL_FULL_CONNECTION</li>
+ *   <li>Server calls AsyncChannel#connected</li>
+ *   <li>Server sends AsyncChannel#sendMessage(CMD_CHANNEL_FULLY_CONNECTED)</li>
+ *   <li>Client receives CMD_CHANNEL_FULLY_CONNECTED</li>
+ *   <li><code>comm-loop:</code></li>
+ *   <li>Client/Server uses AsyncChannel#sendMessage/replyToMessage
+ *       to communicate and perform work</li>
+ *   <li>Loop to <code>comm-loop</code> until done</li>
+ *   <li>When done Client/Server calls {@link AsyncChannel#disconnect}</li>
+ *   <li>Client/Server receives CMD_CHANNEL_DISCONNECTED from AsyncChannel</li>
+ *</ol>
+ *
+ * TODO: Consider simplifying where we have connect and fullyConnect with only one response
+ * message RSP_CHANNEL_CONNECT instead of two, CMD_CHANNEL_HALF_CONNECTED and
+ * CMD_CHANNEL_FULLY_CONNECTED. We'd also change CMD_CHANNEL_FULL_CONNECTION to REQ_CHANNEL_CONNECT.
+ */
+public class AsyncChannel {
+    /** Log tag */
+    private static final String TAG = "AsyncChannel";
+
+    /** Enable to turn on debugging */
+    private static final boolean DBG = false;
+
+    private static final int BASE = Protocol.BASE_SYSTEM_ASYNC_CHANNEL;
+
+    /**
+     * Command sent when the channel is half connected. Half connected
+     * means that the channel can be used to send commends to the destination
+     * but the destination is unaware that the channel exists. The first
+     * command sent to the destination is typically CMD_CHANNEL_FULL_CONNECTION if
+     * it is desired to establish a long term connection, but any command maybe
+     * sent.
+     *
+     * msg.arg1 == 0 : STATUS_SUCCESSFUL
+     *             1 : STATUS_BINDING_UNSUCCESSFUL
+     * msg.obj  == the AsyncChannel
+     * msg.replyTo == dstMessenger if successful
+     */
+    public static final int CMD_CHANNEL_HALF_CONNECTED = BASE + 0;
+
+    /**
+     * Command typically sent when after receiving the CMD_CHANNEL_HALF_CONNECTED.
+     * This is used to initiate a long term connection with the destination and
+     * typically the destination will reply with CMD_CHANNEL_FULLY_CONNECTED.
+     *
+     * msg.replyTo = srcMessenger.
+     */
+    public static final int CMD_CHANNEL_FULL_CONNECTION = BASE + 1;
+
+    /**
+     * Command typically sent after the destination receives a CMD_CHANNEL_FULL_CONNECTION.
+     * This signifies the acceptance or rejection of the channel by the sender.
+     *
+     * msg.arg1 == 0 : Accept connection
+     *               : All other values signify the destination rejected the connection
+     *                 and {@link AsyncChannel#disconnect} would typically be called.
+     */
+    public static final int CMD_CHANNEL_FULLY_CONNECTED = BASE + 2;
+
+    /**
+     * Command sent when one side or the other wishes to disconnect. The sender
+     * may or may not be able to receive a reply depending upon the protocol and
+     * the state of the connection. The receiver should call {@link AsyncChannel#disconnect}
+     * to close its side of the channel and it will receive a CMD_CHANNEL_DISCONNECTED
+     * when the channel is closed.
+     *
+     * msg.replyTo = messenger that is disconnecting
+     */
+    public static final int CMD_CHANNEL_DISCONNECT = BASE + 3;
+
+    /**
+     * Command sent when the channel becomes disconnected. This is sent when the
+     * channel is forcibly disconnected by the system or as a reply to CMD_CHANNEL_DISCONNECT.
+     *
+     * msg.arg1 == 0 : STATUS_SUCCESSFUL
+     *             1 : STATUS_BINDING_UNSUCCESSFUL
+     *             2 : STATUS_SEND_UNSUCCESSFUL
+     *               : All other values signify failure and the channel state is indeterminate
+     * msg.obj  == the AsyncChannel
+     * msg.replyTo = messenger disconnecting or null if it was never connected.
+     */
+    public static final int CMD_CHANNEL_DISCONNECTED = BASE + 4;
+
+    private static final int CMD_TO_STRING_COUNT = CMD_CHANNEL_DISCONNECTED - BASE + 1;
+    private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT];
+    static {
+        sCmdToString[CMD_CHANNEL_HALF_CONNECTED - BASE] = "CMD_CHANNEL_HALF_CONNECTED";
+        sCmdToString[CMD_CHANNEL_FULL_CONNECTION - BASE] = "CMD_CHANNEL_FULL_CONNECTION";
+        sCmdToString[CMD_CHANNEL_FULLY_CONNECTED - BASE] = "CMD_CHANNEL_FULLY_CONNECTED";
+        sCmdToString[CMD_CHANNEL_DISCONNECT - BASE] = "CMD_CHANNEL_DISCONNECT";
+        sCmdToString[CMD_CHANNEL_DISCONNECTED - BASE] = "CMD_CHANNEL_DISCONNECTED";
+    }
+    protected static String cmdToString(int cmd) {
+        cmd -= BASE;
+        if ((cmd >= 0) && (cmd < sCmdToString.length)) {
+            return sCmdToString[cmd];
+        } else {
+            return null;
+        }
+    }
+
+    /** Successful status always 0, !0 is an unsuccessful status */
+    public static final int STATUS_SUCCESSFUL = 0;
+
+    /** Error attempting to bind on a connect */
+    public static final int STATUS_BINDING_UNSUCCESSFUL = 1;
+
+    /** Error attempting to send a message */
+    public static final int STATUS_SEND_UNSUCCESSFUL = 2;
+
+    /** CMD_FULLY_CONNECTED refused because a connection already exists*/
+    public static final int STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED = 3;
+
+    /** Error indicating abnormal termination of destination messenger */
+    public static final int STATUS_REMOTE_DISCONNECTION = 4;
+
+    /** Service connection */
+    private AsyncChannelConnection mConnection;
+
+    /** Context for source */
+    private Context mSrcContext;
+
+    /** Handler for source */
+    private Handler mSrcHandler;
+
+    /** Messenger for source */
+    private Messenger mSrcMessenger;
+
+    /** Messenger for destination */
+    private Messenger mDstMessenger;
+
+    /** Death Monitor for destination messenger */
+    private DeathMonitor mDeathMonitor;
+
+    /**
+     * AsyncChannel constructor
+     */
+    public AsyncChannel() {
+    }
+
+    /**
+     * Connect handler to named package/class synchronously.
+     *
+     * @param srcContext is the context of the source
+     * @param srcHandler is the hander to receive CONNECTED & DISCONNECTED
+     *            messages
+     * @param dstPackageName is the destination package name
+     * @param dstClassName is the fully qualified class name (i.e. contains
+     *            package name)
+     *
+     * @return STATUS_SUCCESSFUL on success any other value is an error.
+     */
+    public int connectSrcHandlerToPackageSync(
+            Context srcContext, Handler srcHandler, String dstPackageName, String dstClassName) {
+        if (DBG) log("connect srcHandler to dst Package & class E");
+
+        mConnection = new AsyncChannelConnection();
+
+        /* Initialize the source information */
+        mSrcContext = srcContext;
+        mSrcHandler = srcHandler;
+        mSrcMessenger = new Messenger(srcHandler);
+
+        /*
+         * Initialize destination information to null they will
+         * be initialized when the AsyncChannelConnection#onServiceConnected
+         * is called
+         */
+        mDstMessenger = null;
+
+        /* Send intent to create the connection */
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClassName(dstPackageName, dstClassName);
+        boolean result = srcContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+        if (DBG) log("connect srcHandler to dst Package & class X result=" + result);
+        return result ? STATUS_SUCCESSFUL : STATUS_BINDING_UNSUCCESSFUL;
+    }
+
+    /**
+     * Connect a handler to Messenger synchronously.
+     *
+     * @param srcContext is the context of the source
+     * @param srcHandler is the hander to receive CONNECTED & DISCONNECTED
+     *            messages
+     * @param dstMessenger is the hander to send messages to.
+     *
+     * @return STATUS_SUCCESSFUL on success any other value is an error.
+     */
+    public int connectSync(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
+        if (DBG) log("halfConnectSync srcHandler to the dstMessenger  E");
+
+        // We are connected
+        connected(srcContext, srcHandler, dstMessenger);
+
+        if (DBG) log("halfConnectSync srcHandler to the dstMessenger X");
+        return STATUS_SUCCESSFUL;
+    }
+
+    /**
+     * connect two local Handlers synchronously.
+     *
+     * @param srcContext is the context of the source
+     * @param srcHandler is the hander to receive CONNECTED & DISCONNECTED
+     *            messages
+     * @param dstHandler is the hander to send messages to.
+     *
+     * @return STATUS_SUCCESSFUL on success any other value is an error.
+     */
+    public int connectSync(Context srcContext, Handler srcHandler, Handler dstHandler) {
+        return connectSync(srcContext, srcHandler, new Messenger(dstHandler));
+    }
+
+    /**
+     * Fully connect two local Handlers synchronously.
+     *
+     * @param srcContext is the context of the source
+     * @param srcHandler is the hander to receive CONNECTED & DISCONNECTED
+     *            messages
+     * @param dstHandler is the hander to send messages to.
+     *
+     * @return STATUS_SUCCESSFUL on success any other value is an error.
+     */
+    public int fullyConnectSync(Context srcContext, Handler srcHandler, Handler dstHandler) {
+        int status = connectSync(srcContext, srcHandler, dstHandler);
+        if (status == STATUS_SUCCESSFUL) {
+            Message response = sendMessageSynchronously(CMD_CHANNEL_FULL_CONNECTION);
+            status = response.arg1;
+        }
+        return status;
+    }
+
+    /**
+     * Connect handler to named package/class.
+     *
+     * Sends a CMD_CHANNEL_HALF_CONNECTED message to srcHandler when complete.
+     *      msg.arg1 = status
+     *      msg.obj = the AsyncChannel
+     *
+     * @param srcContext is the context of the source
+     * @param srcHandler is the hander to receive CONNECTED & DISCONNECTED
+     *            messages
+     * @param dstPackageName is the destination package name
+     * @param dstClassName is the fully qualified class name (i.e. contains
+     *            package name)
+     */
+    public void connect(Context srcContext, Handler srcHandler, String dstPackageName,
+            String dstClassName) {
+        if (DBG) log("connect srcHandler to dst Package & class E");
+
+        final class ConnectAsync implements Runnable {
+            Context mSrcCtx;
+            Handler mSrcHdlr;
+            String mDstPackageName;
+            String mDstClassName;
+
+            ConnectAsync(Context srcContext, Handler srcHandler, String dstPackageName,
+                    String dstClassName) {
+                mSrcCtx = srcContext;
+                mSrcHdlr = srcHandler;
+                mDstPackageName = dstPackageName;
+                mDstClassName = dstClassName;
+            }
+
+            @Override
+            public void run() {
+                int result = connectSrcHandlerToPackageSync(mSrcCtx, mSrcHdlr, mDstPackageName,
+                        mDstClassName);
+                replyHalfConnected(result);
+            }
+        }
+
+        ConnectAsync ca = new ConnectAsync(srcContext, srcHandler, dstPackageName, dstClassName);
+        new Thread(ca).start();
+
+        if (DBG) log("connect srcHandler to dst Package & class X");
+    }
+
+    /**
+     * Connect handler to a class
+     *
+     * Sends a CMD_CHANNEL_HALF_CONNECTED message to srcHandler when complete.
+     *      msg.arg1 = status
+     *      msg.obj = the AsyncChannel
+     *
+     * @param srcContext
+     * @param srcHandler
+     * @param klass is the class to send messages to.
+     */
+    public void connect(Context srcContext, Handler srcHandler, Class<?> klass) {
+        connect(srcContext, srcHandler, klass.getPackage().getName(), klass.getName());
+    }
+
+    /**
+     * Connect handler and messenger.
+     *
+     * Sends a CMD_CHANNEL_HALF_CONNECTED message to srcHandler when complete.
+     *      msg.arg1 = status
+     *      msg.obj = the AsyncChannel
+     *
+     * @param srcContext
+     * @param srcHandler
+     * @param dstMessenger
+     */
+    public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
+        if (DBG) log("connect srcHandler to the dstMessenger  E");
+
+        // We are connected
+        connected(srcContext, srcHandler, dstMessenger);
+
+        // Tell source we are half connected
+        replyHalfConnected(STATUS_SUCCESSFUL);
+
+        if (DBG) log("connect srcHandler to the dstMessenger X");
+    }
+
+    /**
+     * Connect handler to messenger. This method is typically called
+     * when a server receives a CMD_CHANNEL_FULL_CONNECTION request
+     * and initializes the internal instance variables to allow communication
+     * with the dstMessenger.
+     *
+     * @param srcContext
+     * @param srcHandler
+     * @param dstMessenger
+     */
+    public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
+        if (DBG) log("connected srcHandler to the dstMessenger  E");
+
+        // Initialize source fields
+        mSrcContext = srcContext;
+        mSrcHandler = srcHandler;
+        mSrcMessenger = new Messenger(mSrcHandler);
+
+        // Initialize destination fields
+        mDstMessenger = dstMessenger;
+        if (DBG) log("connected srcHandler to the dstMessenger X");
+    }
+
+    /**
+     * Connect two local Handlers.
+     *
+     * @param srcContext is the context of the source
+     * @param srcHandler is the hander to receive CONNECTED & DISCONNECTED
+     *            messages
+     * @param dstHandler is the hander to send messages to.
+     */
+    public void connect(Context srcContext, Handler srcHandler, Handler dstHandler) {
+        connect(srcContext, srcHandler, new Messenger(dstHandler));
+    }
+
+    /**
+     * Connect service and messenger.
+     *
+     * Sends a CMD_CHANNEL_HALF_CONNECTED message to srcAsyncService when complete.
+     *      msg.arg1 = status
+     *      msg.obj = the AsyncChannel
+     *
+     * @param srcAsyncService
+     * @param dstMessenger
+     */
+    public void connect(AsyncService srcAsyncService, Messenger dstMessenger) {
+        connect(srcAsyncService, srcAsyncService.getHandler(), dstMessenger);
+    }
+
+    /**
+     * To close the connection call when handler receives CMD_CHANNEL_DISCONNECTED
+     */
+    public void disconnected() {
+        mSrcContext = null;
+        mSrcHandler = null;
+        mSrcMessenger = null;
+        mDstMessenger = null;
+        mDeathMonitor = null;
+        mConnection = null;
+    }
+
+    /**
+     * Disconnect
+     */
+    public void disconnect() {
+        if ((mConnection != null) && (mSrcContext != null)) {
+            mSrcContext.unbindService(mConnection);
+            mConnection = null;
+        }
+        try {
+            // Send the DISCONNECTED, although it may not be received
+            // but its the best we can do.
+            Message msg = Message.obtain();
+            msg.what = CMD_CHANNEL_DISCONNECTED;
+            msg.replyTo = mSrcMessenger;
+            mDstMessenger.send(msg);
+        } catch(Exception e) {
+        }
+        // Tell source we're disconnected.
+        replyDisconnected(STATUS_SUCCESSFUL);
+        mSrcHandler = null;
+        // Unlink only when bindService isn't used
+        if (mConnection == null && mDstMessenger != null && mDeathMonitor!= null) {
+            mDstMessenger.getBinder().unlinkToDeath(mDeathMonitor, 0);
+            mDeathMonitor = null;
+        }
+    }
+
+    /**
+     * Send a message to the destination handler.
+     *
+     * @param msg
+     */
+    public void sendMessage(Message msg) {
+        msg.replyTo = mSrcMessenger;
+        try {
+            mDstMessenger.send(msg);
+        } catch (RemoteException e) {
+            replyDisconnected(STATUS_SEND_UNSUCCESSFUL);
+        }
+    }
+
+    /**
+     * Send a message to the destination handler
+     *
+     * @param what
+     */
+    public void sendMessage(int what) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        sendMessage(msg);
+    }
+
+    /**
+     * Send a message to the destination handler
+     *
+     * @param what
+     * @param arg1
+     */
+    public void sendMessage(int what, int arg1) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        sendMessage(msg);
+    }
+
+    /**
+     * Send a message to the destination handler
+     *
+     * @param what
+     * @param arg1
+     * @param arg2
+     */
+    public void sendMessage(int what, int arg1, int arg2) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        sendMessage(msg);
+    }
+
+    /**
+     * Send a message to the destination handler
+     *
+     * @param what
+     * @param arg1
+     * @param arg2
+     * @param obj
+     */
+    public void sendMessage(int what, int arg1, int arg2, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        msg.obj = obj;
+        sendMessage(msg);
+    }
+
+    /**
+     * Send a message to the destination handler
+     *
+     * @param what
+     * @param obj
+     */
+    public void sendMessage(int what, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.obj = obj;
+        sendMessage(msg);
+    }
+
+    /**
+     * Reply to srcMsg sending dstMsg
+     *
+     * @param srcMsg
+     * @param dstMsg
+     */
+    public void replyToMessage(Message srcMsg, Message dstMsg) {
+        try {
+            dstMsg.replyTo = mSrcMessenger;
+            srcMsg.replyTo.send(dstMsg);
+        } catch (RemoteException e) {
+            log("TODO: handle replyToMessage RemoteException" + e);
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Reply to srcMsg
+     *
+     * @param srcMsg
+     * @param what
+     */
+    public void replyToMessage(Message srcMsg, int what) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        replyToMessage(srcMsg, msg);
+    }
+
+    /**
+     * Reply to srcMsg
+     *
+     * @param srcMsg
+     * @param what
+     * @param arg1
+     */
+    public void replyToMessage(Message srcMsg, int what, int arg1) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        replyToMessage(srcMsg, msg);
+    }
+
+    /**
+     * Reply to srcMsg
+     *
+     * @param srcMsg
+     * @param what
+     * @param arg1
+     * @param arg2
+     */
+    public void replyToMessage(Message srcMsg, int what, int arg1, int arg2) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        replyToMessage(srcMsg, msg);
+    }
+
+    /**
+     * Reply to srcMsg
+     *
+     * @param srcMsg
+     * @param what
+     * @param arg1
+     * @param arg2
+     * @param obj
+     */
+    public void replyToMessage(Message srcMsg, int what, int arg1, int arg2, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        msg.obj = obj;
+        replyToMessage(srcMsg, msg);
+    }
+
+    /**
+     * Reply to srcMsg
+     *
+     * @param srcMsg
+     * @param what
+     * @param obj
+     */
+    public void replyToMessage(Message srcMsg, int what, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.obj = obj;
+        replyToMessage(srcMsg, msg);
+    }
+
+    /**
+     * Send the Message synchronously.
+     *
+     * @param msg to send
+     * @return reply message or null if an error.
+     */
+    public Message sendMessageSynchronously(Message msg) {
+        Message resultMsg = SyncMessenger.sendMessageSynchronously(mDstMessenger, msg);
+        return resultMsg;
+    }
+
+    /**
+     * Send the Message synchronously.
+     *
+     * @param what
+     * @return reply message or null if an error.
+     */
+    public Message sendMessageSynchronously(int what) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        Message resultMsg = sendMessageSynchronously(msg);
+        return resultMsg;
+    }
+
+    /**
+     * Send the Message synchronously.
+     *
+     * @param what
+     * @param arg1
+     * @return reply message or null if an error.
+     */
+    public Message sendMessageSynchronously(int what, int arg1) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        Message resultMsg = sendMessageSynchronously(msg);
+        return resultMsg;
+    }
+
+    /**
+     * Send the Message synchronously.
+     *
+     * @param what
+     * @param arg1
+     * @param arg2
+     * @return reply message or null if an error.
+     */
+    public Message sendMessageSynchronously(int what, int arg1, int arg2) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        Message resultMsg = sendMessageSynchronously(msg);
+        return resultMsg;
+    }
+
+    /**
+     * Send the Message synchronously.
+     *
+     * @param what
+     * @param arg1
+     * @param arg2
+     * @param obj
+     * @return reply message or null if an error.
+     */
+    public Message sendMessageSynchronously(int what, int arg1, int arg2, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        msg.obj = obj;
+        Message resultMsg = sendMessageSynchronously(msg);
+        return resultMsg;
+    }
+
+    /**
+     * Send the Message synchronously.
+     *
+     * @param what
+     * @param obj
+     * @return reply message or null if an error.
+     */
+    public Message sendMessageSynchronously(int what, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.obj = obj;
+        Message resultMsg = sendMessageSynchronously(msg);
+        return resultMsg;
+    }
+
+    /**
+     * Helper class to send messages synchronously
+     */
+    private static class SyncMessenger {
+        /** A stack of SyncMessengers */
+        private static Stack<SyncMessenger> sStack = new Stack<SyncMessenger>();
+        /** A number of SyncMessengers created */
+        private static int sCount = 0;
+        /** The handler thread */
+        private HandlerThread mHandlerThread;
+        /** The handler that will receive the result */
+        private SyncHandler mHandler;
+        /** The messenger used to send the message */
+        private Messenger mMessenger;
+
+        /** private constructor */
+        private SyncMessenger() {
+        }
+
+        /** Synchronous Handler class */
+        private class SyncHandler extends Handler {
+            /** The object used to wait/notify */
+            private Object mLockObject = new Object();
+            /** The resulting message */
+            private Message mResultMsg;
+
+            /** Constructor */
+            private SyncHandler(Looper looper) {
+                super(looper);
+            }
+
+            /** Handle of the reply message */
+            @Override
+            public void handleMessage(Message msg) {
+                Message msgCopy = Message.obtain();
+                msgCopy.copyFrom(msg);
+                synchronized(mLockObject) {
+                    mResultMsg = msgCopy;
+                    mLockObject.notify();
+                }
+            }
+        }
+
+        /**
+         * @return the SyncMessenger
+         */
+        private static SyncMessenger obtain() {
+            SyncMessenger sm;
+            synchronized (sStack) {
+                if (sStack.isEmpty()) {
+                    sm = new SyncMessenger();
+                    sm.mHandlerThread = new HandlerThread("SyncHandler-" + sCount++);
+                    sm.mHandlerThread.start();
+                    sm.mHandler = sm.new SyncHandler(sm.mHandlerThread.getLooper());
+                    sm.mMessenger = new Messenger(sm.mHandler);
+                } else {
+                    sm = sStack.pop();
+                }
+            }
+            return sm;
+        }
+
+        /**
+         * Recycle this object
+         */
+        private void recycle() {
+            synchronized (sStack) {
+                sStack.push(this);
+            }
+        }
+
+        /**
+         * Send a message synchronously.
+         *
+         * @param msg to send
+         * @return result message or null if an error occurs
+         */
+        private static Message sendMessageSynchronously(Messenger dstMessenger, Message msg) {
+            SyncMessenger sm = SyncMessenger.obtain();
+            Message resultMsg = null;
+            try {
+                if (dstMessenger != null && msg != null) {
+                    msg.replyTo = sm.mMessenger;
+                    synchronized (sm.mHandler.mLockObject) {
+                        if (sm.mHandler.mResultMsg != null) {
+                            Slog.wtf(TAG, "mResultMsg should be null here");
+                            sm.mHandler.mResultMsg = null;
+                        }
+                        dstMessenger.send(msg);
+                        sm.mHandler.mLockObject.wait();
+                        resultMsg = sm.mHandler.mResultMsg;
+                        sm.mHandler.mResultMsg = null;
+                    }
+                }
+            } catch (InterruptedException e) {
+                Slog.e(TAG, "error in sendMessageSynchronously", e);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "error in sendMessageSynchronously", e);
+            }
+            sm.recycle();
+            return resultMsg;
+        }
+    }
+
+    /**
+     * Reply to the src handler that we're half connected.
+     * see: CMD_CHANNEL_HALF_CONNECTED for message contents
+     *
+     * @param status to be stored in msg.arg1
+     */
+    private void replyHalfConnected(int status) {
+        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);
+        msg.arg1 = status;
+        msg.obj = this;
+        msg.replyTo = mDstMessenger;
+        if (!linkToDeathMonitor()) {
+            // Override status to indicate failure
+            msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;
+        }
+
+        mSrcHandler.sendMessage(msg);
+    }
+
+    /**
+     * Link to death monitor for destination messenger. Returns true if successfully binded to
+     * destination messenger; false otherwise.
+     */
+    private boolean linkToDeathMonitor() {
+        // Link to death only when bindService isn't used and not already linked.
+        if (mConnection == null && mDeathMonitor == null) {
+            mDeathMonitor = new DeathMonitor();
+            try {
+                mDstMessenger.getBinder().linkToDeath(mDeathMonitor, 0);
+            } catch (RemoteException e) {
+                mDeathMonitor = null;
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Reply to the src handler that we are disconnected
+     * see: CMD_CHANNEL_DISCONNECTED for message contents
+     *
+     * @param status to be stored in msg.arg1
+     */
+    private void replyDisconnected(int status) {
+        // Can't reply if already disconnected. Avoid NullPointerException.
+        if (mSrcHandler == null) return;
+        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_DISCONNECTED);
+        msg.arg1 = status;
+        msg.obj = this;
+        msg.replyTo = mDstMessenger;
+        mSrcHandler.sendMessage(msg);
+    }
+
+
+    /**
+     * ServiceConnection to receive call backs.
+     */
+    class AsyncChannelConnection implements ServiceConnection {
+        AsyncChannelConnection() {
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mDstMessenger = new Messenger(service);
+            replyHalfConnected(STATUS_SUCCESSFUL);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName className) {
+            replyDisconnected(STATUS_SUCCESSFUL);
+        }
+    }
+
+    /**
+     * Log the string.
+     *
+     * @param s
+     */
+    private static void log(String s) {
+        Slog.d(TAG, s);
+    }
+
+    private final class DeathMonitor implements IBinder.DeathRecipient {
+
+        DeathMonitor() {
+        }
+
+        public void binderDied() {
+            replyDisconnected(STATUS_REMOTE_DISCONNECTION);
+        }
+
+    }
+}
diff --git a/com/android/internal/util/AsyncService.java b/com/android/internal/util/AsyncService.java
new file mode 100644
index 0000000..e39a2bf
--- /dev/null
+++ b/com/android/internal/util/AsyncService.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright (C) 2010 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.internal.util;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Slog;
+
+/**
+ * A service that receives Intents and IBinder transactions
+ * as messages via an AsyncChannel.
+ * <p>
+ * The Start Intent arrives as CMD_ASYNC_SERVICE_ON_START_INTENT with msg.arg1 = flags,
+ * msg.arg2 = startId, and msg.obj = intent.
+ * <p>
+ */
+abstract public class AsyncService extends Service {
+    private static final String TAG = "AsyncService";
+
+    protected static final boolean DBG = true;
+
+    /** The command sent when a onStartCommand is invoked */
+    public static final int CMD_ASYNC_SERVICE_ON_START_INTENT = IBinder.LAST_CALL_TRANSACTION;
+
+    /** The command sent when a onDestroy is invoked */
+    public static final int CMD_ASYNC_SERVICE_DESTROY = IBinder.LAST_CALL_TRANSACTION + 1;
+
+    /** Messenger transport */
+    protected Messenger mMessenger;
+
+    /** Message Handler that will receive messages */
+    Handler mHandler;
+
+    public static final class AsyncServiceInfo {
+        /** Message Handler that will receive messages */
+        public Handler mHandler;
+
+        /**
+         * The flags returned by onStartCommand on how to restart.
+         * For instance @see android.app.Service#START_STICKY
+         */
+        public int mRestartFlags;
+    }
+
+    AsyncServiceInfo mAsyncServiceInfo;
+
+    /**
+     * Create the service's handler returning AsyncServiceInfo.
+     *
+     * @return AsyncServiceInfo
+     */
+    abstract public AsyncServiceInfo createHandler();
+
+    /**
+     * Get the handler
+     */
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * onCreate
+     */
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mAsyncServiceInfo = createHandler();
+        mHandler = mAsyncServiceInfo.mHandler;
+        mMessenger = new Messenger(mHandler);
+    }
+
+    /**
+     * Sends the CMD_ASYNC_SERVICE_ON_START_INTENT message.
+     */
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (DBG) Slog.d(TAG, "onStartCommand");
+
+        Message msg = mHandler.obtainMessage();
+        msg.what = CMD_ASYNC_SERVICE_ON_START_INTENT;
+        msg.arg1 = flags;
+        msg.arg2 = startId;
+        msg.obj = intent;
+        mHandler.sendMessage(msg);
+
+        return mAsyncServiceInfo.mRestartFlags;
+    }
+
+    /**
+     * Called when service is destroyed. After returning the
+     * service is dead and no more processing should be expected
+     * to occur.
+     */
+    @Override
+    public void onDestroy() {
+        if (DBG) Slog.d(TAG, "onDestroy");
+
+        Message msg = mHandler.obtainMessage();
+        msg.what = CMD_ASYNC_SERVICE_DESTROY;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * Returns the Messenger's binder.
+     */
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mMessenger.getBinder();
+    }
+}
diff --git a/com/android/internal/util/BitUtils.java b/com/android/internal/util/BitUtils.java
new file mode 100644
index 0000000..28f12eb
--- /dev/null
+++ b/com/android/internal/util/BitUtils.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import libcore.util.Objects;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.function.IntFunction;
+
+/**
+ * A utility class for handling unsigned integers and unsigned arithmetics, as well as syntactic
+ * sugar methods for ByteBuffer. Useful for networking and packet manipulations.
+ * {@hide}
+ */
+public final class BitUtils {
+    private BitUtils() {}
+
+    public static boolean maskedEquals(long a, long b, long mask) {
+        return (a & mask) == (b & mask);
+    }
+
+    public static boolean maskedEquals(byte a, byte b, byte mask) {
+        return (a & mask) == (b & mask);
+    }
+
+    public static boolean maskedEquals(byte[] a, byte[] b, @Nullable byte[] mask) {
+        if (a == null || b == null) return a == b;
+        Preconditions.checkArgument(a.length == b.length, "Inputs must be of same size");
+        if (mask == null) return Arrays.equals(a, b);
+        Preconditions.checkArgument(a.length == mask.length, "Mask must be of same size as inputs");
+        for (int i = 0; i < mask.length; i++) {
+            if (!maskedEquals(a[i], b[i], mask[i])) return false;
+        }
+        return true;
+    }
+
+    public static boolean maskedEquals(UUID a, UUID b, @Nullable UUID mask) {
+        if (mask == null) {
+            return Objects.equal(a, b);
+        }
+        return maskedEquals(a.getLeastSignificantBits(), b.getLeastSignificantBits(),
+                    mask.getLeastSignificantBits())
+                && maskedEquals(a.getMostSignificantBits(), b.getMostSignificantBits(),
+                    mask.getMostSignificantBits());
+    }
+
+    public static int[] unpackBits(long val) {
+        int size = Long.bitCount(val);
+        int[] result = new int[size];
+        int index = 0;
+        int bitPos = 0;
+        while (val > 0) {
+            if ((val & 1) == 1) result[index++] = bitPos;
+            val = val >> 1;
+            bitPos++;
+        }
+        return result;
+    }
+
+    public static long packBits(int[] bits) {
+        long packed = 0;
+        for (int b : bits) {
+            packed |= (1 << b);
+        }
+        return packed;
+    }
+
+    public static int uint8(byte b) {
+        return b & 0xff;
+    }
+
+    public static int uint16(short s) {
+        return s & 0xffff;
+    }
+
+    public static long uint32(int i) {
+        return i & 0xffffffffL;
+    }
+
+    public static int bytesToBEInt(byte[] bytes) {
+        return (uint8(bytes[0]) << 24)
+                + (uint8(bytes[1]) << 16)
+                + (uint8(bytes[2]) << 8)
+                + (uint8(bytes[3]));
+    }
+
+    public static int bytesToLEInt(byte[] bytes) {
+        return Integer.reverseBytes(bytesToBEInt(bytes));
+    }
+
+    public static int getUint8(ByteBuffer buffer, int position) {
+        return uint8(buffer.get(position));
+    }
+
+    public static int getUint16(ByteBuffer buffer, int position) {
+        return uint16(buffer.getShort(position));
+    }
+
+    public static long getUint32(ByteBuffer buffer, int position) {
+        return uint32(buffer.getInt(position));
+    }
+
+    public static void put(ByteBuffer buffer, int position, byte[] bytes) {
+        final int original = buffer.position();
+        buffer.position(position);
+        buffer.put(bytes);
+        buffer.position(original);
+    }
+
+    public static boolean isBitSet(long flags, int bitIndex) {
+        return (flags & bitAt(bitIndex)) != 0;
+    }
+
+    public static long bitAt(int bitIndex) {
+        return 1L << bitIndex;
+    }
+
+    public static String flagsToString(int flags, IntFunction<String> getFlagName) {
+        StringBuilder builder = new StringBuilder();
+        int count = 0;
+        while (flags != 0) {
+            final int flag = 1 << Integer.numberOfTrailingZeros(flags);
+            flags &= ~flag;
+            if (count > 0) builder.append(", ");
+            builder.append(getFlagName.apply(flag));
+            count++;
+        }
+        TextUtils.wrap(builder, "[", "]");
+        return builder.toString();
+    }
+}
diff --git a/com/android/internal/util/BitwiseInputStream.java b/com/android/internal/util/BitwiseInputStream.java
new file mode 100644
index 0000000..86f74f3
--- /dev/null
+++ b/com/android/internal/util/BitwiseInputStream.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2008 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.internal.util;
+
+/**
+ * An object that provides bitwise incremental read access to a byte array.
+ *
+ * This is useful, for example, when accessing a series of fields that
+ * may not be aligned on byte boundaries.
+ *
+ * NOTE -- This class is not threadsafe.
+ */
+public class BitwiseInputStream {
+
+    // The byte array being read from.
+    private byte[] mBuf;
+
+    // The current position offset, in bits, from the msb in byte 0.
+    private int mPos;
+
+    // The last valid bit offset.
+    private int mEnd;
+
+    /**
+     * An exception to report access problems.
+     */
+    public static class AccessException extends Exception {
+        public AccessException(String s) {
+            super("BitwiseInputStream access failed: " + s);
+        }
+    }
+
+    /**
+     * Create object from byte array.
+     *
+     * @param buf a byte array containing data
+     */
+    public BitwiseInputStream(byte buf[]) {
+        mBuf = buf;
+        mEnd = buf.length << 3;
+        mPos = 0;
+    }
+
+    /**
+     * Return the number of bit still available for reading.
+     */
+    public int available() {
+        return mEnd - mPos;
+    }
+
+    /**
+     * Read some data and increment the current position.
+     *
+     * The 8-bit limit on access to bitwise streams is intentional to
+     * avoid endianness issues.
+     *
+     * @param bits the amount of data to read (gte 0, lte 8)
+     * @return byte of read data (possibly partially filled, from lsb)
+     */
+    public int read(int bits) throws AccessException {
+        int index = mPos >>> 3;
+        int offset = 16 - (mPos & 0x07) - bits;  // &7==%8
+        if ((bits < 0) || (bits > 8) || ((mPos + bits) > mEnd)) {
+            throw new AccessException("illegal read " +
+                "(pos " + mPos + ", end " + mEnd + ", bits " + bits + ")");
+        }
+        int data = (mBuf[index] & 0xFF) << 8;
+        if (offset < 8) data |= mBuf[index + 1] & 0xFF;
+        data >>>= offset;
+        data &= (-1 >>> (32 - bits));
+        mPos += bits;
+        return data;
+    }
+
+    /**
+     * Read data in bulk into a byte array and increment the current position.
+     *
+     * @param bits the amount of data to read
+     * @return newly allocated byte array of read data
+     */
+    public byte[] readByteArray(int bits) throws AccessException {
+        int bytes = (bits >>> 3) + ((bits & 0x07) > 0 ? 1 : 0);  // &7==%8
+        byte[] arr = new byte[bytes];
+        for (int i = 0; i < bytes; i++) {
+            int increment = Math.min(8, bits - (i << 3));
+            arr[i] = (byte)(read(increment) << (8 - increment));
+        }
+        return arr;
+    }
+
+    /**
+     * Increment the current position and ignore contained data.
+     *
+     * @param bits the amount by which to increment the position
+     */
+    public void skip(int bits) throws AccessException {
+        if ((mPos + bits) > mEnd) {
+            throw new AccessException("illegal skip " +
+                "(pos " + mPos + ", end " + mEnd + ", bits " + bits + ")");
+        }
+        mPos += bits;
+    }
+}
diff --git a/com/android/internal/util/BitwiseOutputStream.java b/com/android/internal/util/BitwiseOutputStream.java
new file mode 100644
index 0000000..ddecbed
--- /dev/null
+++ b/com/android/internal/util/BitwiseOutputStream.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2008 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.internal.util;
+
+/**
+ * An object that provides bitwise incremental write access to a byte array.
+ *
+ * This is useful, for example, when writing a series of fields that
+ * may not be aligned on byte boundaries.
+ *
+ * NOTE -- This class is not threadsafe.
+ */
+public class BitwiseOutputStream {
+
+    // The byte array being written to, which will be grown as needed.
+    private byte[] mBuf;
+
+    // The current position offset, in bits, from the msb in byte 0.
+    private int mPos;
+
+    // The last bit offset, given the current buf length.
+    private int mEnd;
+
+    /**
+     * An exception to report access problems.
+     */
+    public static class AccessException extends Exception {
+        public AccessException(String s) {
+            super("BitwiseOutputStream access failed: " + s);
+        }
+    }
+
+    /**
+     * Create object from hint at desired size.
+     *
+     * @param startingLength initial internal byte array length in bytes
+     */
+    public BitwiseOutputStream(int startingLength) {
+        mBuf = new byte[startingLength];
+        mEnd = startingLength << 3;
+        mPos = 0;
+    }
+
+    /**
+     * Return byte array containing accumulated data, sized to just fit.
+     *
+     * @return newly allocated byte array
+     */
+    public byte[] toByteArray() {
+        int len = (mPos >>> 3) + ((mPos & 0x07) > 0 ? 1 : 0);  // &7==%8
+        byte[] newBuf = new byte[len];
+        System.arraycopy(mBuf, 0, newBuf, 0, len);
+        return newBuf;
+    }
+
+    /**
+     * Allocate a new internal buffer, if needed.
+     *
+     * @param bits additional bits to be accommodated
+     */
+    private void possExpand(int bits) {
+        if ((mPos + bits) < mEnd) return;
+        byte[] newBuf = new byte[(mPos + bits) >>> 2];
+        System.arraycopy(mBuf, 0, newBuf, 0, mEnd >>> 3);
+        mBuf = newBuf;
+        mEnd = newBuf.length << 3;
+    }
+
+    /**
+     * Write some data and increment the current position.
+     *
+     * The 8-bit limit on access to bitwise streams is intentional to
+     * avoid endianness issues.
+     *
+     * @param bits the amount of data to write (gte 0, lte 8)
+     * @param data to write, will be masked to expose only bits param from lsb
+     */
+    public void write(int bits, int data) throws AccessException {
+        if ((bits < 0) || (bits > 8)) {
+            throw new AccessException("illegal write (" + bits + " bits)");
+        }
+        possExpand(bits);
+        data &= (-1 >>> (32 - bits));
+        int index = mPos >>> 3;
+        int offset = 16 - (mPos & 0x07) - bits;  // &7==%8
+        data <<= offset;
+        mPos += bits;
+        mBuf[index] |= data >>> 8;
+        if (offset < 8) mBuf[index + 1] |= data & 0xFF;
+    }
+
+    /**
+     * Write data in bulk from a byte array and increment the current position.
+     *
+     * @param bits the amount of data to write
+     * @param arr the byte array containing data to be written
+     */
+    public void writeByteArray(int bits, byte[] arr) throws AccessException {
+        for (int i = 0; i < arr.length; i++) {
+            int increment = Math.min(8, bits - (i << 3));
+            if (increment > 0) {
+                write(increment, (byte)(arr[i] >>> (8 - increment)));
+            }
+        }
+    }
+
+    /**
+     * Increment the current position, implicitly writing zeros.
+     *
+     * @param bits the amount by which to increment the position
+     */
+    public void skip(int bits) {
+        possExpand(bits);
+        mPos += bits;
+    }
+}
diff --git a/com/android/internal/util/CallbackRegistry.java b/com/android/internal/util/CallbackRegistry.java
new file mode 100644
index 0000000..0f228d4
--- /dev/null
+++ b/com/android/internal/util/CallbackRegistry.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2015 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.internal.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tracks callbacks for the event. This class supports reentrant modification
+ * of the callbacks during notification without adversely disrupting notifications.
+ * A common pattern for callbacks is to receive a notification and then remove
+ * themselves. This class handles this behavior with constant memory under
+ * most circumstances.
+ *
+ * <p>A subclass of {@link CallbackRegistry.NotifierCallback} must be passed to
+ * the constructor to define how notifications should be called. That implementation
+ * does the actual notification on the listener.</p>
+ *
+ * <p>This class supports only callbacks with at most two parameters.
+ * Typically, these are the notification originator and a parameter, but these may
+ * be used as required. If more than two parameters are required or primitive types
+ * must be used, <code>A</code> should be some kind of containing structure that
+ * the subclass may reuse between notifications.</p>
+ *
+ * @param <C> The callback type.
+ * @param <T> The notification sender type. Typically this is the containing class.
+ * @param <A> Opaque argument used to pass additional data beyond an int.
+ */
+public class CallbackRegistry<C, T, A> implements Cloneable {
+    private static final String TAG = "CallbackRegistry";
+
+    /** An ordered collection of listeners waiting to be notified. */
+    private List<C> mCallbacks = new ArrayList<C>();
+
+    /**
+     * A bit flag for the first 64 listeners that are removed during notification.
+     * The lowest significant bit corresponds to the 0th index into mCallbacks.
+     * For a small number of callbacks, no additional array of objects needs to
+     * be allocated.
+     */
+    private long mFirst64Removed = 0x0;
+
+    /**
+     * Bit flags for the remaining callbacks that are removed during notification.
+     * When there are more than 64 callbacks and one is marked for removal, a dynamic
+     * array of bits are allocated for the callbacks.
+     */
+    private long[] mRemainderRemoved;
+
+    /**
+     * The reentrancy level of the notification. When we notify a callback, it may cause
+     * further notifications. The reentrancy level must be tracked to let us clean up
+     * the callback state when all notifications have been processed.
+     */
+    private int mNotificationLevel;
+
+    /** The notification mechanism for notifying an event. */
+    private final NotifierCallback<C, T, A> mNotifier;
+
+    /**
+     * Creates an EventRegistry that notifies the event with notifier.
+     * @param notifier The class to use to notify events.
+     */
+    public CallbackRegistry(NotifierCallback<C, T, A> notifier) {
+        mNotifier = notifier;
+    }
+
+    /**
+     * Notify all callbacks.
+     *
+     * @param sender The originator. This is an opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg2 An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     */
+    public synchronized void notifyCallbacks(T sender, int arg, A arg2) {
+        mNotificationLevel++;
+        notifyRecurseLocked(sender, arg, arg2);
+        mNotificationLevel--;
+        if (mNotificationLevel == 0) {
+            if (mRemainderRemoved != null) {
+                for (int i = mRemainderRemoved.length - 1; i >= 0; i--) {
+                    final long removedBits = mRemainderRemoved[i];
+                    if (removedBits != 0) {
+                        removeRemovedCallbacks((i + 1) * Long.SIZE, removedBits);
+                        mRemainderRemoved[i] = 0;
+                    }
+                }
+            }
+            if (mFirst64Removed != 0) {
+                removeRemovedCallbacks(0, mFirst64Removed);
+                mFirst64Removed = 0;
+            }
+        }
+    }
+
+    /**
+     * Notify up to the first Long.SIZE callbacks that don't have a bit set in <code>removed</code>.
+     *
+     * @param sender The originator. This is an opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg2 An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     */
+    private void notifyFirst64Locked(T sender, int arg, A arg2) {
+        final int maxNotified = Math.min(Long.SIZE, mCallbacks.size());
+        notifyCallbacksLocked(sender, arg, arg2, 0, maxNotified, mFirst64Removed);
+    }
+
+    /**
+     * Notify all callbacks using a recursive algorithm to avoid allocating on the heap.
+     * This part captures the callbacks beyond Long.SIZE that have no bits allocated for
+     * removal before it recurses into {@link #notifyRemainderLocked(Object, int, A, int)}.
+     * <p>
+     * Recursion is used to avoid allocating temporary state on the heap. Each stack has one
+     * long (64 callbacks) worth of information of which has been removed.
+     *
+     * @param sender The originator. This is an opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg2 An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     */
+    private void notifyRecurseLocked(T sender, int arg, A arg2) {
+        final int callbackCount = mCallbacks.size();
+        final int remainderIndex = mRemainderRemoved == null ? -1 : mRemainderRemoved.length - 1;
+
+        // Now we've got all callbacks that have no mRemainderRemoved value, so notify the
+        // others.
+        notifyRemainderLocked(sender, arg, arg2, remainderIndex);
+
+        // notifyRemainderLocked notifies all at maxIndex, so we'd normally start at maxIndex + 1
+        // However, we must also keep track of those in mFirst64Removed, so we add 2 instead:
+        final int startCallbackIndex = (remainderIndex + 2) * Long.SIZE;
+
+        // The remaining have no bit set
+        notifyCallbacksLocked(sender, arg, arg2, startCallbackIndex, callbackCount, 0);
+    }
+
+    /**
+     * Notify callbacks that have mRemainderRemoved bits set for remainderIndex. If
+     * remainderIndex is -1, the first 64 will be notified instead.
+     *
+     * @param sender The originator. This is an opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg2 An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param remainderIndex The index into mRemainderRemoved that should be notified.
+     */
+    private void notifyRemainderLocked(T sender, int arg, A arg2, int remainderIndex) {
+        if (remainderIndex < 0) {
+            notifyFirst64Locked(sender, arg, arg2);
+        } else {
+            final long bits = mRemainderRemoved[remainderIndex];
+            final int startIndex = (remainderIndex + 1) * Long.SIZE;
+            final int endIndex = Math.min(mCallbacks.size(), startIndex + Long.SIZE);
+            notifyRemainderLocked(sender, arg, arg2, remainderIndex - 1);
+            notifyCallbacksLocked(sender, arg, arg2, startIndex, endIndex, bits);
+        }
+    }
+
+    /**
+     * Notify callbacks from startIndex to endIndex, using bits as the bit status
+     * for whether they have been removed or not. bits should be from mRemainderRemoved or
+     * mFirst64Removed. bits set to 0 indicates that all callbacks from startIndex to
+     * endIndex should be notified.
+     *
+     * @param sender The originator. This is an opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param arg2 An opaque parameter passed to
+     *      {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)}
+     * @param startIndex The index into the mCallbacks to start notifying.
+     * @param endIndex One past the last index into mCallbacks to notify.
+     * @param bits A bit field indicating which callbacks have been removed and shouldn't
+     *             be notified.
+     */
+    private void notifyCallbacksLocked(T sender, int arg, A arg2, final int startIndex,
+            final int endIndex, final long bits) {
+        long bitMask = 1;
+        for (int i = startIndex; i < endIndex; i++) {
+            if ((bits & bitMask) == 0) {
+                mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2);
+            }
+            bitMask <<= 1;
+        }
+    }
+
+    /**
+     * Add a callback to be notified. If the callback is already in the list, another won't
+     * be added. This does not affect current notifications.
+     * @param callback The callback to add.
+     */
+    public synchronized void add(C callback) {
+        int index = mCallbacks.lastIndexOf(callback);
+        if (index < 0 || isRemovedLocked(index)) {
+            mCallbacks.add(callback);
+        }
+    }
+
+    /**
+     * Returns true if the callback at index has been marked for removal.
+     *
+     * @param index The index into mCallbacks to check.
+     * @return true if the callback at index has been marked for removal.
+     */
+    private boolean isRemovedLocked(int index) {
+        if (index < Long.SIZE) {
+            // It is in the first 64 callbacks, just check the bit.
+            final long bitMask = 1L << index;
+            return (mFirst64Removed & bitMask) != 0;
+        } else if (mRemainderRemoved == null) {
+            // It is after the first 64 callbacks, but nothing else was marked for removal.
+            return false;
+        } else {
+            final int maskIndex = (index / Long.SIZE) - 1;
+            if (maskIndex >= mRemainderRemoved.length) {
+                // There are some items in mRemainderRemoved, but nothing at the given index.
+                return false;
+            } else {
+                // There is something marked for removal, so we have to check the bit.
+                final long bits = mRemainderRemoved[maskIndex];
+                final long bitMask = 1L << (index % Long.SIZE);
+                return (bits & bitMask) != 0;
+            }
+        }
+    }
+
+    /**
+     * Removes callbacks from startIndex to startIndex + Long.SIZE, based
+     * on the bits set in removed.
+     * @param startIndex The index into the mCallbacks to start removing callbacks.
+     * @param removed The bits indicating removal, where each bit is set for one callback
+     *                to be removed.
+     */
+    private void removeRemovedCallbacks(int startIndex, long removed) {
+        // The naive approach should be fine. There may be a better bit-twiddling approach.
+        final int endIndex = startIndex + Long.SIZE;
+
+        long bitMask = 1L << (Long.SIZE - 1);
+        for (int i = endIndex - 1; i >= startIndex; i--) {
+            if ((removed & bitMask) != 0) {
+                mCallbacks.remove(i);
+            }
+            bitMask >>>= 1;
+        }
+    }
+
+    /**
+     * Remove a callback. This callback won't be notified after this call completes.
+     * @param callback The callback to remove.
+     */
+    public synchronized void remove(C callback) {
+        if (mNotificationLevel == 0) {
+            mCallbacks.remove(callback);
+        } else {
+            int index = mCallbacks.lastIndexOf(callback);
+            if (index >= 0) {
+                setRemovalBitLocked(index);
+            }
+        }
+    }
+
+    private void setRemovalBitLocked(int index) {
+        if (index < Long.SIZE) {
+            // It is in the first 64 callbacks, just check the bit.
+            final long bitMask = 1L << index;
+            mFirst64Removed |= bitMask;
+        } else {
+            final int remainderIndex = (index / Long.SIZE) - 1;
+            if (mRemainderRemoved == null) {
+                mRemainderRemoved = new long[mCallbacks.size() / Long.SIZE];
+            } else if (mRemainderRemoved.length < remainderIndex) {
+                // need to make it bigger
+                long[] newRemainders = new long[mCallbacks.size() / Long.SIZE];
+                System.arraycopy(mRemainderRemoved, 0, newRemainders, 0, mRemainderRemoved.length);
+                mRemainderRemoved = newRemainders;
+            }
+            final long bitMask = 1L << (index % Long.SIZE);
+            mRemainderRemoved[remainderIndex] |= bitMask;
+        }
+    }
+
+    /**
+     * Makes a copy of the registered callbacks and returns it.
+     *
+     * @return a copy of the registered callbacks.
+     */
+    public synchronized ArrayList<C> copyListeners() {
+        ArrayList<C> callbacks = new ArrayList<C>(mCallbacks.size());
+        int numListeners = mCallbacks.size();
+        for (int i = 0; i < numListeners; i++) {
+            if (!isRemovedLocked(i)) {
+                callbacks.add(mCallbacks.get(i));
+            }
+        }
+        return callbacks;
+    }
+
+    /**
+     * Returns true if there are no registered callbacks or false otherwise.
+     *
+     * @return true if there are no registered callbacks or false otherwise.
+     */
+    public synchronized boolean isEmpty() {
+        if (mCallbacks.isEmpty()) {
+            return true;
+        } else if (mNotificationLevel == 0) {
+            return false;
+        } else {
+            int numListeners = mCallbacks.size();
+            for (int i = 0; i < numListeners; i++) {
+                if (!isRemovedLocked(i)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Removes all callbacks from the list.
+     */
+    public synchronized void clear() {
+        if (mNotificationLevel == 0) {
+            mCallbacks.clear();
+        } else if (!mCallbacks.isEmpty()) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                setRemovalBitLocked(i);
+            }
+        }
+    }
+
+    public synchronized CallbackRegistry<C, T, A> clone() {
+        CallbackRegistry<C, T, A> clone = null;
+        try {
+            clone = (CallbackRegistry<C, T, A>) super.clone();
+            clone.mFirst64Removed = 0;
+            clone.mRemainderRemoved = null;
+            clone.mNotificationLevel = 0;
+            clone.mCallbacks = new ArrayList<C>();
+            final int numListeners = mCallbacks.size();
+            for (int i = 0; i < numListeners; i++) {
+                if (!isRemovedLocked(i)) {
+                    clone.mCallbacks.add(mCallbacks.get(i));
+                }
+            }
+        } catch (CloneNotSupportedException e) {
+            e.printStackTrace();
+        }
+        return clone;
+    }
+
+    /**
+     * Class used to notify events from CallbackRegistry.
+     *
+     * @param <C> The callback type.
+     * @param <T> The notification sender type. Typically this is the containing class.
+     * @param <A> An opaque argument to pass to the notifier
+     */
+    public abstract static class NotifierCallback<C, T, A> {
+        /**
+         * Used to notify the callback.
+         *
+         * @param callback The callback to notify.
+         * @param sender The opaque sender object.
+         * @param arg The opaque notification parameter.
+         * @param arg2 An opaque argument passed in
+         *        {@link CallbackRegistry#notifyCallbacks}
+         * @see CallbackRegistry#CallbackRegistry(CallbackRegistry.NotifierCallback)
+         */
+        public abstract void onNotifyCallback(C callback, T sender, int arg, A arg2);
+    }
+}
diff --git a/com/android/internal/util/CharSequences.java b/com/android/internal/util/CharSequences.java
new file mode 100644
index 0000000..fdaa4bc
--- /dev/null
+++ b/com/android/internal/util/CharSequences.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2007 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.internal.util;
+
+/**
+ * {@link CharSequence} utility methods.
+ */
+public class CharSequences {
+
+    /**
+     * Adapts {@link CharSequence} to an array of ASCII (7-bits per character)
+     * bytes.
+     * 
+     * @param bytes ASCII bytes
+     */
+    public static CharSequence forAsciiBytes(final byte[] bytes) {
+        return new CharSequence() {
+            public char charAt(int index) {
+                return (char) bytes[index];
+            }
+
+            public int length() {
+                return bytes.length;
+            }
+
+            public CharSequence subSequence(int start, int end) {
+                return forAsciiBytes(bytes, start, end);
+            }
+
+            public String toString() {
+                return new String(bytes);
+            }
+        };
+    }
+
+    /**
+     * Adapts {@link CharSequence} to an array of ASCII (7-bits per character)
+     * bytes.
+     *
+     * @param bytes ASCII bytes
+     * @param start index, inclusive
+     * @param end index, exclusive
+     *
+     * @throws IndexOutOfBoundsException if start or end are negative, if end
+     *  is greater than length(), or if start is greater than end
+     */
+    public static CharSequence forAsciiBytes(final byte[] bytes,
+            final int start, final int end) {
+        validate(start, end, bytes.length);
+        return new CharSequence() {
+            public char charAt(int index) {
+                return (char) bytes[index + start];
+            }
+
+            public int length() {
+                return end - start;
+            }
+
+            public CharSequence subSequence(int newStart, int newEnd) {
+                newStart -= start;
+                newEnd -= start;
+                validate(newStart, newEnd, length());
+                return forAsciiBytes(bytes, newStart, newEnd);
+            }
+
+            public String toString() {
+                return new String(bytes, start, length());
+            }
+        };
+    }
+
+    static void validate(int start, int end, int length) {
+        if (start < 0) throw new IndexOutOfBoundsException();
+        if (end < 0) throw new IndexOutOfBoundsException();
+        if (end > length) throw new IndexOutOfBoundsException();
+        if (start > end) throw new IndexOutOfBoundsException();
+    }
+
+    /**
+     * Compares two character sequences for equality.
+     */
+    public static boolean equals(CharSequence a, CharSequence b) {
+        if (a.length() != b.length()) {
+            return false;
+        }
+
+        int length = a.length();
+        for (int i = 0; i < length; i++) {
+            if (a.charAt(i) != b.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * Compares two character sequences with API like {@link Comparable#compareTo}.
+     * 
+     * @param me The CharSequence that receives the compareTo call.
+     * @param another The other CharSequence.
+     * @return See {@link Comparable#compareTo}.
+     */
+    public static int compareToIgnoreCase(CharSequence me, CharSequence another) {
+        // Code adapted from String#compareTo
+        int myLen = me.length(), anotherLen = another.length();
+        int myPos = 0, anotherPos = 0, result;
+        int end = (myLen < anotherLen) ? myLen : anotherLen;
+
+        while (myPos < end) {
+            if ((result = Character.toLowerCase(me.charAt(myPos++))
+                    - Character.toLowerCase(another.charAt(anotherPos++))) != 0) {
+                return result;
+            }
+        }
+        return myLen - anotherLen;
+    }
+}
diff --git a/com/android/internal/util/CollectionUtils.java b/com/android/internal/util/CollectionUtils.java
new file mode 100644
index 0000000..f0b47de
--- /dev/null
+++ b/com/android/internal/util/CollectionUtils.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import static com.android.internal.util.ArrayUtils.isEmpty;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArraySet;
+import android.util.ExceptionUtils;
+
+import com.android.internal.util.FunctionalUtils.ThrowingConsumer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.*;
+import java.util.stream.Stream;
+
+/**
+ * Utility methods for dealing with (typically {@code Nullable}) {@link Collection}s
+ *
+ * Unless a method specifies otherwise, a null value for a collection is treated as an empty
+ * collection of that type.
+ */
+public class CollectionUtils {
+    private CollectionUtils() { /* cannot be instantiated */ }
+
+    /**
+     * Returns a list of items from the provided list that match the given condition.
+     *
+     * This is similar to {@link Stream#filter} but without the overhead of creating an intermediate
+     * {@link Stream} instance
+     */
+    public static @NonNull <T> List<T> filter(@Nullable List<T> list,
+            java.util.function.Predicate<? super T> predicate) {
+        ArrayList<T> result = null;
+        for (int i = 0; i < size(list); i++) {
+            final T item = list.get(i);
+            if (predicate.test(item)) {
+                result = ArrayUtils.add(result, item);
+            }
+        }
+        return emptyIfNull(result);
+    }
+
+    /**
+     * @see #filter(List, java.util.function.Predicate)
+     */
+    public static @NonNull <T> Set<T> filter(@Nullable Set<T> set,
+            java.util.function.Predicate<? super T> predicate) {
+        if (set == null || set.size() == 0) return Collections.emptySet();
+        ArraySet<T> result = null;
+        if (set instanceof ArraySet) {
+            ArraySet<T> arraySet = (ArraySet<T>) set;
+            int size = arraySet.size();
+            for (int i = 0; i < size; i++) {
+                final T item = arraySet.valueAt(i);
+                if (predicate.test(item)) {
+                    result = ArrayUtils.add(result, item);
+                }
+            }
+        } else {
+            for (T item : set) {
+                if (predicate.test(item)) {
+                    result = ArrayUtils.add(result, item);
+                }
+            }
+        }
+        return emptyIfNull(result);
+    }
+
+    /**
+     * Returns a list of items resulting from applying the given function to each element of the
+     * provided list.
+     *
+     * The resulting list will have the same {@link #size} as the input one.
+     *
+     * This is similar to {@link Stream#map} but without the overhead of creating an intermediate
+     * {@link Stream} instance
+     */
+    public static @NonNull <I, O> List<O> map(@Nullable List<I> cur,
+            Function<? super I, ? extends O> f) {
+        if (isEmpty(cur)) return Collections.emptyList();
+        final ArrayList<O> result = new ArrayList<>();
+        for (int i = 0; i < cur.size(); i++) {
+            result.add(f.apply(cur.get(i)));
+        }
+        return result;
+    }
+
+    /**
+     * @see #map(List, Function)
+     */
+    public static @NonNull <I, O> Set<O> map(@Nullable Set<I> cur,
+            Function<? super I, ? extends O> f) {
+        if (isEmpty(cur)) return Collections.emptySet();
+        ArraySet<O> result = new ArraySet<>();
+        if (cur instanceof ArraySet) {
+            ArraySet<I> arraySet = (ArraySet<I>) cur;
+            int size = arraySet.size();
+            for (int i = 0; i < size; i++) {
+                result.add(f.apply(arraySet.valueAt(i)));
+            }
+        } else {
+            for (I item : cur) {
+                result.add(f.apply(item));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * {@link #map(List, Function)} + {@link #filter(List, java.util.function.Predicate)}
+     *
+     * Calling this is equivalent (but more memory efficient) to:
+     *
+     * {@code
+     *      filter(
+     *          map(cur, f),
+     *          i -> { i != null })
+     * }
+     */
+    public static @NonNull <I, O> List<O> mapNotNull(@Nullable List<I> cur,
+            Function<? super I, ? extends O> f) {
+        if (isEmpty(cur)) return Collections.emptyList();
+        final ArrayList<O> result = new ArrayList<>();
+        for (int i = 0; i < cur.size(); i++) {
+            O transformed = f.apply(cur.get(i));
+            if (transformed != null) {
+                result.add(transformed);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns the given list, or an immutable empty list if the provided list is null
+     *
+     * This can be used to guarantee null-safety without paying the price of extra allocations
+     *
+     * @see Collections#emptyList
+     */
+    public static @NonNull <T> List<T> emptyIfNull(@Nullable List<T> cur) {
+        return cur == null ? Collections.emptyList() : cur;
+    }
+
+    /**
+     * Returns the given set, or an immutable empty set if the provided set is null
+     *
+     * This can be used to guarantee null-safety without paying the price of extra allocations
+     *
+     * @see Collections#emptySet
+     */
+    public static @NonNull <T> Set<T> emptyIfNull(@Nullable Set<T> cur) {
+        return cur == null ? Collections.emptySet() : cur;
+    }
+
+    /**
+     * Returns the size of the given list, or 0 if the list is null
+     */
+    public static int size(@Nullable Collection<?> cur) {
+        return cur != null ? cur.size() : 0;
+    }
+
+    /**
+     * Returns the elements of the given list that are of type {@code c}
+     */
+    public static @NonNull <T> List<T> filter(@Nullable List<?> list, Class<T> c) {
+        if (isEmpty(list)) return Collections.emptyList();
+        ArrayList<T> result = null;
+        for (int i = 0; i < list.size(); i++) {
+            final Object item = list.get(i);
+            if (c.isInstance(item)) {
+                result = ArrayUtils.add(result, (T) item);
+            }
+        }
+        return emptyIfNull(result);
+    }
+
+    /**
+     * Returns whether there exists at least one element in the list for which
+     * condition {@code predicate} is true
+     */
+    public static <T> boolean any(@Nullable List<T> items,
+            java.util.function.Predicate<T> predicate) {
+        return find(items, predicate) != null;
+    }
+
+    /**
+     * Returns the first element from the list for which
+     * condition {@code predicate} is true, or null if there is no such element
+     */
+    public static @Nullable <T> T find(@Nullable List<T> items,
+            java.util.function.Predicate<T> predicate) {
+        if (isEmpty(items)) return null;
+        for (int i = 0; i < items.size(); i++) {
+            final T item = items.get(i);
+            if (predicate.test(item)) return item;
+        }
+        return null;
+    }
+
+    /**
+     * Similar to {@link List#add}, but with support for list values of {@code null} and
+     * {@link Collections#emptyList}
+     */
+    public static @NonNull <T> List<T> add(@Nullable List<T> cur, T val) {
+        if (cur == null || cur == Collections.emptyList()) {
+            cur = new ArrayList<>();
+        }
+        cur.add(val);
+        return cur;
+    }
+
+    /**
+     * @see #add(List, Object)
+     */
+    public static @NonNull <T> Set<T> add(@Nullable Set<T> cur, T val) {
+        if (cur == null || cur == Collections.emptySet()) {
+            cur = new ArraySet<>();
+        }
+        cur.add(val);
+        return cur;
+    }
+
+    /**
+     * Similar to {@link List#remove}, but with support for list values of {@code null} and
+     * {@link Collections#emptyList}
+     */
+    public static @NonNull <T> List<T> remove(@Nullable List<T> cur, T val) {
+        if (isEmpty(cur)) {
+            return emptyIfNull(cur);
+        }
+        cur.remove(val);
+        return cur;
+    }
+
+    /**
+     * @see #remove(List, Object)
+     */
+    public static @NonNull <T> Set<T> remove(@Nullable Set<T> cur, T val) {
+        if (isEmpty(cur)) {
+            return emptyIfNull(cur);
+        }
+        cur.remove(val);
+        return cur;
+    }
+
+    /**
+     * @return a list that will not be affected by mutations to the given original list.
+     */
+    public static @NonNull <T> List<T> copyOf(@Nullable List<T> cur) {
+        return isEmpty(cur) ? Collections.emptyList() : new ArrayList<>(cur);
+    }
+
+    /**
+     * @return a list that will not be affected by mutations to the given original list.
+     */
+    public static @NonNull <T> Set<T> copyOf(@Nullable Set<T> cur) {
+        return isEmpty(cur) ? Collections.emptySet() : new ArraySet<>(cur);
+    }
+
+    /**
+     * Applies {@code action} to each element in {@code cur}
+     *
+     * This avoids creating an iterator if the given set is an {@link ArraySet}
+     */
+    public static <T> void forEach(@Nullable Set<T> cur, @Nullable ThrowingConsumer<T> action) {
+        if (cur == null || action == null) return;
+        int size = cur.size();
+        if (size == 0) return;
+        try {
+            if (cur instanceof ArraySet) {
+                ArraySet<T> arraySet = (ArraySet<T>) cur;
+                for (int i = 0; i < size; i++) {
+                    action.accept(arraySet.valueAt(i));
+                }
+            } else {
+                for (T t : cur) {
+                    action.accept(t);
+                }
+            }
+        } catch (Exception e) {
+            throw ExceptionUtils.propagate(e);
+        }
+    }
+}
diff --git a/com/android/internal/util/ConcurrentUtils.java b/com/android/internal/util/ConcurrentUtils.java
new file mode 100644
index 0000000..e35f9f4
--- /dev/null
+++ b/com/android/internal/util/ConcurrentUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import android.os.Process;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Utility methods for common functionality using java.util.concurrent package
+ *
+ * @hide
+ */
+public class ConcurrentUtils {
+
+    private ConcurrentUtils() {
+    }
+
+    /**
+     * Creates a thread pool using
+     * {@link java.util.concurrent.Executors#newFixedThreadPool(int, ThreadFactory)}
+     *
+     * @param nThreads the number of threads in the pool
+     * @param poolName base name of the threads in the pool
+     * @param linuxThreadPriority a Linux priority level. see {@link Process#setThreadPriority(int)}
+     * @return the newly created thread pool
+     */
+    public static ExecutorService newFixedThreadPool(int nThreads, String poolName,
+            int linuxThreadPriority) {
+        return Executors.newFixedThreadPool(nThreads,
+                new ThreadFactory() {
+                    private final AtomicInteger threadNum = new AtomicInteger(0);
+
+                    @Override
+                    public Thread newThread(final Runnable r) {
+                        return new Thread(poolName + threadNum.incrementAndGet()) {
+                            @Override
+                            public void run() {
+                                Process.setThreadPriority(linuxThreadPriority);
+                                r.run();
+                            }
+                        };
+                    }
+                });
+    }
+
+    /**
+     * Waits if necessary for the computation to complete, and then retrieves its result.
+     * <p>If {@code InterruptedException} occurs, this method will interrupt the current thread
+     * and throw {@code IllegalStateException}</p>
+     *
+     * @param future future to wait for result
+     * @param description short description of the operation
+     * @return the computed result
+     * @throws IllegalStateException if interrupted during wait
+     * @throws RuntimeException if an error occurs while waiting for {@link Future#get()}
+     * @see Future#get()
+     */
+    public static <T> T waitForFutureNoInterrupt(Future<T> future, String description) {
+        try {
+            return future.get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException(description + " interrupted");
+        } catch (ExecutionException e) {
+            throw new RuntimeException(description + " failed", e);
+        }
+    }
+
+}
diff --git a/com/android/internal/util/DumpUtils.java b/com/android/internal/util/DumpUtils.java
new file mode 100644
index 0000000..66b777e
--- /dev/null
+++ b/com/android/internal/util/DumpUtils.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2012 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.internal.util;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Helper functions for dumping the state of system services.
+ */
+public final class DumpUtils {
+    private static final String TAG = "DumpUtils";
+    private static final boolean DEBUG = false;
+
+    private DumpUtils() {
+    }
+
+    /**
+     * Helper for dumping state owned by a handler thread.
+     *
+     * Because the caller might be holding an important lock that the handler is
+     * trying to acquire, we use a short timeout to avoid deadlocks.  The process
+     * is inelegant but this function is only used for debugging purposes.
+     */
+    public static void dumpAsync(Handler handler, final Dump dump, PrintWriter pw,
+            final String prefix, long timeout) {
+        final StringWriter sw = new StringWriter();
+        if (handler.runWithScissors(new Runnable() {
+            @Override
+            public void run() {
+                PrintWriter lpw = new FastPrintWriter(sw);
+                dump.dump(lpw, prefix);
+                lpw.close();
+            }
+        }, timeout)) {
+            pw.print(sw.toString());
+        } else {
+            pw.println("... timed out");
+        }
+    }
+
+    public interface Dump {
+        void dump(PrintWriter pw, String prefix);
+    }
+
+    private static void logMessage(PrintWriter pw, String msg) {
+        if (DEBUG) Slog.v(TAG, msg);
+        pw.println(msg);
+    }
+
+    /**
+     * Verify that caller holds {@link android.Manifest.permission#DUMP}.
+     *
+     * @return true if access should be granted.
+     * @hide
+     */
+    public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+        if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            logMessage(pw, "Permission Denial: can't dump " + tag + " from from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " due to missing android.permission.DUMP permission");
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Verify that caller holds
+     * {@link android.Manifest.permission#PACKAGE_USAGE_STATS} and that they
+     * have {@link AppOpsManager#OP_GET_USAGE_STATS} access.
+     *
+     * @return true if access should be granted.
+     * @hide
+     */
+    public static boolean checkUsageStatsPermission(Context context, String tag, PrintWriter pw) {
+        // System internals always get access
+        final int uid = Binder.getCallingUid();
+        switch (uid) {
+            case android.os.Process.ROOT_UID:
+            case android.os.Process.SYSTEM_UID:
+            case android.os.Process.SHELL_UID:
+                return true;
+        }
+
+        // Caller always needs to hold permission
+        if (context.checkCallingOrSelfPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
+                != PackageManager.PERMISSION_GRANTED) {
+            logMessage(pw, "Permission Denial: can't dump " + tag + " from from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " due to missing android.permission.PACKAGE_USAGE_STATS permission");
+            return false;
+        }
+
+        // And finally, caller needs to have appops access; this is totally
+        // hacky, but it's the easiest way to wire this up without retrofitting
+        // Binder.dump() to pass through package names.
+        final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+        final String[] pkgs = context.getPackageManager().getPackagesForUid(uid);
+        if (pkgs != null) {
+            for (String pkg : pkgs) {
+                switch (appOps.checkOpNoThrow(AppOpsManager.OP_GET_USAGE_STATS, uid, pkg)) {
+                    case AppOpsManager.MODE_ALLOWED:
+                        if (DEBUG) Slog.v(TAG, "Found package " + pkg + " with "
+                                + "android:get_usage_stats allowed");
+                        return true;
+                    case AppOpsManager.MODE_DEFAULT:
+                        if (DEBUG) Slog.v(TAG, "Found package " + pkg + " with "
+                                + "android:get_usage_stats default");
+                        return true;
+                }
+            }
+        }
+
+        logMessage(pw, "Permission Denial: can't dump " + tag + " from from pid="
+                + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                + " due to android:get_usage_stats app-op not allowed");
+        return false;
+    }
+
+    /**
+     * Verify that caller holds both {@link android.Manifest.permission#DUMP}
+     * and {@link android.Manifest.permission#PACKAGE_USAGE_STATS}, and that
+     * they have {@link AppOpsManager#OP_GET_USAGE_STATS} access.
+     *
+     * @return true if access should be granted.
+     * @hide
+     */
+    public static boolean checkDumpAndUsageStatsPermission(Context context, String tag,
+            PrintWriter pw) {
+        return checkDumpPermission(context, tag, pw) && checkUsageStatsPermission(context, tag, pw);
+    }
+}
diff --git a/com/android/internal/util/EmergencyAffordanceManager.java b/com/android/internal/util/EmergencyAffordanceManager.java
new file mode 100644
index 0000000..ba95bfc
--- /dev/null
+++ b/com/android/internal/util/EmergencyAffordanceManager.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+/**
+ * A class that manages emergency affordances and enables immediate calling to emergency services
+ */
+public class EmergencyAffordanceManager {
+
+    public static final boolean ENABLED = true;
+
+    /**
+     * Global setting override with the number to call with the emergency affordance.
+     * @hide
+     */
+    private static final String EMERGENCY_CALL_NUMBER_SETTING = "emergency_affordance_number";
+
+    /**
+     * Global setting, whether the emergency affordance should be shown regardless of device state.
+     * The value is a boolean (1 or 0).
+     * @hide
+     */
+    private static final String FORCE_EMERGENCY_AFFORDANCE_SETTING = "force_emergency_affordance";
+
+    private final Context mContext;
+
+    public EmergencyAffordanceManager(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * perform an emergency call.
+     */
+    public final void performEmergencyCall() {
+        performEmergencyCall(mContext);
+    }
+
+    private static Uri getPhoneUri(Context context) {
+        String number = context.getResources().getString(
+                com.android.internal.R.string.config_emergency_call_number);
+        if (Build.IS_DEBUGGABLE) {
+            String override = Settings.Global.getString(
+                    context.getContentResolver(), EMERGENCY_CALL_NUMBER_SETTING);
+            if (override != null) {
+                number = override;
+            }
+        }
+        return Uri.fromParts("tel", number, null);
+    }
+
+    private static void performEmergencyCall(Context context) {
+        Intent intent = new Intent(Intent.ACTION_CALL_EMERGENCY);
+        intent.setData(getPhoneUri(context));
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivityAsUser(intent, UserHandle.CURRENT);
+    }
+
+    /**
+     * @return whether emergency affordance should be active.
+     */
+    public boolean needsEmergencyAffordance() {
+        if (!ENABLED) {
+            return false;
+        }
+        if (forceShowing()) {
+            return true;
+        }
+        return isEmergencyAffordanceNeeded();
+    }
+
+    private boolean isEmergencyAffordanceNeeded() {
+        return Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.EMERGENCY_AFFORDANCE_NEEDED, 0) != 0;
+    }
+
+
+    private boolean forceShowing() {
+        return Settings.Global.getInt(mContext.getContentResolver(),
+                FORCE_EMERGENCY_AFFORDANCE_SETTING, 0) != 0;
+    }
+}
diff --git a/com/android/internal/util/ExponentiallyBucketedHistogram.java b/com/android/internal/util/ExponentiallyBucketedHistogram.java
new file mode 100644
index 0000000..dc9f3f4
--- /dev/null
+++ b/com/android/internal/util/ExponentiallyBucketedHistogram.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * A histogram for positive integers where each bucket is twice the size of the previous one.
+ */
+public class ExponentiallyBucketedHistogram {
+    @NonNull
+    private final int[] mData;
+
+    /**
+     * Create a new histogram.
+     *
+     * @param numBuckets The number of buckets. The highest bucket is for all value >=
+     *                   2<sup>numBuckets - 1</sup>
+     */
+    public ExponentiallyBucketedHistogram(@IntRange(from = 1, to = 31) int numBuckets) {
+        numBuckets = Preconditions.checkArgumentInRange(numBuckets, 1, 31, "numBuckets");
+
+        mData = new int[numBuckets];
+    }
+
+    /**
+     * Add a new value to the histogram.
+     *
+     * All values <= 0 are in the first bucket. The last bucket contains all values >=
+     * 2<sup>numBuckets - 1</sup>
+     *
+     * @param value The value to add
+     */
+    public void add(int value) {
+        if (value <= 0) {
+            mData[0]++;
+        } else {
+            mData[Math.min(mData.length - 1, 32 - Integer.numberOfLeadingZeros(value))]++;
+        }
+    }
+
+    /**
+     * Clear all data from the histogram
+     */
+    public void reset() {
+        Arrays.fill(mData, 0);
+    }
+
+    /**
+     * Write the histogram to the log.
+     *
+     * @param tag    The tag to use when logging
+     * @param prefix A custom prefix that is printed in front of the histogram
+     */
+    public void log(@NonNull String tag, @Nullable CharSequence prefix) {
+        StringBuilder builder = new StringBuilder(prefix);
+        builder.append('[');
+
+        for (int i = 0; i < mData.length; i++) {
+            if (i != 0) {
+                builder.append(", ");
+            }
+
+            if (i < mData.length - 1) {
+                builder.append("<");
+                builder.append(1 << i);
+            } else {
+                builder.append(">=");
+                builder.append(1 << (i - 1));
+            }
+
+            builder.append(": ");
+            builder.append(mData[i]);
+        }
+        builder.append("]");
+
+        Log.d(tag, builder.toString());
+    }
+}
diff --git a/com/android/internal/util/FastMath.java b/com/android/internal/util/FastMath.java
new file mode 100644
index 0000000..88a17e6
--- /dev/null
+++ b/com/android/internal/util/FastMath.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 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.internal.util;
+
+/**
+ * Fast and loose math routines.
+ */
+public class FastMath {
+
+    /**
+     * Fast round from float to int. This is faster than Math.round()
+     * thought it may return slightly different results. It does not try to
+     * handle (in any meaningful way) NaN or infinities.
+     */
+    public static int round(float value) {
+        long lx = (long) (value * (65536 * 256f));
+        return (int) ((lx + 0x800000) >> 24);
+    }
+}
diff --git a/com/android/internal/util/FastPrintWriter.java b/com/android/internal/util/FastPrintWriter.java
new file mode 100644
index 0000000..cc2c4cf
--- /dev/null
+++ b/com/android/internal/util/FastPrintWriter.java
@@ -0,0 +1,702 @@
+/*
+ * 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.internal.util;
+
+import android.util.Log;
+import android.util.Printer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+public class FastPrintWriter extends PrintWriter {
+    private static class DummyWriter extends Writer {
+        @Override
+        public void close() throws IOException {
+            UnsupportedOperationException ex
+                    = new UnsupportedOperationException("Shouldn't be here");
+            throw ex;
+        }
+
+        @Override
+        public void flush() throws IOException {
+            close();
+        }
+
+        @Override
+        public void write(char[] buf, int offset, int count) throws IOException {
+            close();
+        }
+    };
+
+    private final int mBufferLen;
+    private final char[] mText;
+    private int mPos;
+
+    final private OutputStream mOutputStream;
+    final private boolean mAutoFlush;
+    final private String mSeparator;
+
+    final private Writer mWriter;
+    final private Printer mPrinter;
+
+    private CharsetEncoder mCharset;
+    final private ByteBuffer mBytes;
+
+    private boolean mIoError;
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code out} as its target
+     * stream. By default, the new print writer does not automatically flush its
+     * contents to the target stream when a newline is encountered.
+     *
+     * @param out
+     *            the target output stream.
+     * @throws NullPointerException
+     *             if {@code out} is {@code null}.
+     */
+    public FastPrintWriter(OutputStream out) {
+        this(out, false, 8192);
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code out} as its target
+     * stream. The parameter {@code autoFlush} determines if the print writer
+     * automatically flushes its contents to the target stream when a newline is
+     * encountered.
+     *
+     * @param out
+     *            the target output stream.
+     * @param autoFlush
+     *            indicates whether contents are flushed upon encountering a
+     *            newline sequence.
+     * @throws NullPointerException
+     *             if {@code out} is {@code null}.
+     */
+    public FastPrintWriter(OutputStream out, boolean autoFlush) {
+        this(out, autoFlush, 8192);
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code out} as its target
+     * stream and a custom buffer size. The parameter {@code autoFlush} determines
+     * if the print writer automatically flushes its contents to the target stream
+     * when a newline is encountered.
+     *
+     * @param out
+     *            the target output stream.
+     * @param autoFlush
+     *            indicates whether contents are flushed upon encountering a
+     *            newline sequence.
+     * @param bufferLen
+     *            specifies the size of the FastPrintWriter's internal buffer; the
+     *            default is 8192.
+     * @throws NullPointerException
+     *             if {@code out} is {@code null}.
+     */
+    public FastPrintWriter(OutputStream out, boolean autoFlush, int bufferLen) {
+        super(new DummyWriter(), autoFlush);
+        if (out == null) {
+            throw new NullPointerException("out is null");
+        }
+        mBufferLen = bufferLen;
+        mText = new char[bufferLen];
+        mBytes = ByteBuffer.allocate(mBufferLen);
+        mOutputStream = out;
+        mWriter = null;
+        mPrinter = null;
+        mAutoFlush = autoFlush;
+        mSeparator = System.lineSeparator();
+        initDefaultEncoder();
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code wr} as its target
+     * writer. By default, the new print writer does not automatically flush its
+     * contents to the target writer when a newline is encountered.
+     *
+     * <p>NOTE: Unlike PrintWriter, this version will still do buffering inside of
+     * FastPrintWriter before sending data to the Writer.  This means you must call
+     * flush() before retrieving any data from the Writer.</p>
+     *
+     * @param wr
+     *            the target writer.
+     * @throws NullPointerException
+     *             if {@code wr} is {@code null}.
+     */
+    public FastPrintWriter(Writer wr) {
+        this(wr, false, 8192);
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code wr} as its target
+     * writer. The parameter {@code autoFlush} determines if the print writer
+     * automatically flushes its contents to the target writer when a newline is
+     * encountered.
+     *
+     * @param wr
+     *            the target writer.
+     * @param autoFlush
+     *            indicates whether to flush contents upon encountering a
+     *            newline sequence.
+     * @throws NullPointerException
+     *             if {@code out} is {@code null}.
+     */
+    public FastPrintWriter(Writer wr, boolean autoFlush) {
+        this(wr, autoFlush, 8192);
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code wr} as its target
+     * writer and a custom buffer size. The parameter {@code autoFlush} determines
+     * if the print writer automatically flushes its contents to the target writer
+     * when a newline is encountered.
+     *
+     * @param wr
+     *            the target writer.
+     * @param autoFlush
+     *            indicates whether to flush contents upon encountering a
+     *            newline sequence.
+     * @param bufferLen
+     *            specifies the size of the FastPrintWriter's internal buffer; the
+     *            default is 8192.
+     * @throws NullPointerException
+     *             if {@code wr} is {@code null}.
+     */
+    public FastPrintWriter(Writer wr, boolean autoFlush, int bufferLen) {
+        super(new DummyWriter(), autoFlush);
+        if (wr == null) {
+            throw new NullPointerException("wr is null");
+        }
+        mBufferLen = bufferLen;
+        mText = new char[bufferLen];
+        mBytes = null;
+        mOutputStream = null;
+        mWriter = wr;
+        mPrinter = null;
+        mAutoFlush = autoFlush;
+        mSeparator = System.lineSeparator();
+        initDefaultEncoder();
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code pr} as its target
+     * printer and the default buffer size.  Because a {@link Printer} is line-base,
+     * autoflush is always enabled.
+     *
+     * @param pr
+     *            the target writer.
+     * @throws NullPointerException
+     *             if {@code pr} is {@code null}.
+     */
+    public FastPrintWriter(Printer pr) {
+        this(pr, 512);
+    }
+
+    /**
+     * Constructs a new {@code PrintWriter} with {@code pr} as its target
+     * printer and a custom buffer size.  Because a {@link Printer} is line-base,
+     * autoflush is always enabled.
+     *
+     * @param pr
+     *            the target writer.
+     * @param bufferLen
+     *            specifies the size of the FastPrintWriter's internal buffer; the
+     *            default is 512.
+     * @throws NullPointerException
+     *             if {@code pr} is {@code null}.
+     */
+    public FastPrintWriter(Printer pr, int bufferLen) {
+        super(new DummyWriter(), true);
+        if (pr == null) {
+            throw new NullPointerException("pr is null");
+        }
+        mBufferLen = bufferLen;
+        mText = new char[bufferLen];
+        mBytes = null;
+        mOutputStream = null;
+        mWriter = null;
+        mPrinter = pr;
+        mAutoFlush = true;
+        mSeparator = System.lineSeparator();
+        initDefaultEncoder();
+    }
+
+    private final void initEncoder(String csn) throws UnsupportedEncodingException {
+        try {
+            mCharset = Charset.forName(csn).newEncoder();
+        } catch (Exception e) {
+            throw new UnsupportedEncodingException(csn);
+        }
+        mCharset.onMalformedInput(CodingErrorAction.REPLACE);
+        mCharset.onUnmappableCharacter(CodingErrorAction.REPLACE);
+    }
+
+    /**
+     * Flushes this writer and returns the value of the error flag.
+     *
+     * @return {@code true} if either an {@code IOException} has been thrown
+     *         previously or if {@code setError()} has been called;
+     *         {@code false} otherwise.
+     * @see #setError()
+     */
+    public boolean checkError() {
+        flush();
+        synchronized (lock) {
+            return mIoError;
+        }
+    }
+
+    /**
+     * Sets the error state of the stream to false.
+     * @since 1.6
+     */
+    protected void clearError() {
+        synchronized (lock) {
+            mIoError = false;
+        }
+    }
+
+    /**
+     * Sets the error flag of this writer to true.
+     */
+    protected void setError() {
+        synchronized (lock) {
+            mIoError = true;
+        }
+    }
+
+    private final void initDefaultEncoder() {
+        mCharset = Charset.defaultCharset().newEncoder();
+        mCharset.onMalformedInput(CodingErrorAction.REPLACE);
+        mCharset.onUnmappableCharacter(CodingErrorAction.REPLACE);
+    }
+
+    private void appendLocked(char c) throws IOException {
+        int pos = mPos;
+        if (pos >= (mBufferLen-1)) {
+            flushLocked();
+            pos = mPos;
+        }
+        mText[pos] = c;
+        mPos = pos+1;
+    }
+
+    private void appendLocked(String str, int i, final int length) throws IOException {
+        final int BUFFER_LEN = mBufferLen;
+        if (length > BUFFER_LEN) {
+            final int end = i + length;
+            while (i < end) {
+                int next = i + BUFFER_LEN;
+                appendLocked(str, i, next < end ? BUFFER_LEN : (end - i));
+                i = next;
+            }
+            return;
+        }
+        int pos = mPos;
+        if ((pos+length) > BUFFER_LEN) {
+            flushLocked();
+            pos = mPos;
+        }
+        str.getChars(i, i + length, mText, pos);
+        mPos = pos + length;
+    }
+
+    private void appendLocked(char[] buf, int i, final int length) throws IOException {
+        final int BUFFER_LEN = mBufferLen;
+        if (length > BUFFER_LEN) {
+            final int end = i + length;
+            while (i < end) {
+                int next = i + BUFFER_LEN;
+                appendLocked(buf, i, next < end ? BUFFER_LEN : (end - i));
+                i = next;
+            }
+            return;
+        }
+        int pos = mPos;
+        if ((pos+length) > BUFFER_LEN) {
+            flushLocked();
+            pos = mPos;
+        }
+        System.arraycopy(buf, i, mText, pos, length);
+        mPos = pos + length;
+    }
+
+    private void flushBytesLocked() throws IOException {
+        if (!mIoError) {
+            int position;
+            if ((position = mBytes.position()) > 0) {
+                mBytes.flip();
+                mOutputStream.write(mBytes.array(), 0, position);
+                mBytes.clear();
+            }
+        }
+    }
+
+    private void flushLocked() throws IOException {
+        //Log.i("PackageManager", "flush mPos=" + mPos);
+        if (mPos > 0) {
+            if (mOutputStream != null) {
+                CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos);
+                CoderResult result = mCharset.encode(charBuffer, mBytes, true);
+                while (!mIoError) {
+                    if (result.isError()) {
+                        throw new IOException(result.toString());
+                    } else if (result.isOverflow()) {
+                        flushBytesLocked();
+                        result = mCharset.encode(charBuffer, mBytes, true);
+                        continue;
+                    }
+                    break;
+                }
+                if (!mIoError) {
+                    flushBytesLocked();
+                    mOutputStream.flush();
+                }
+            } else if (mWriter != null) {
+                if (!mIoError) {
+                    mWriter.write(mText, 0, mPos);
+                    mWriter.flush();
+                }
+            } else {
+                int nonEolOff = 0;
+                final int sepLen = mSeparator.length();
+                final int len = sepLen < mPos ? sepLen : mPos;
+                while (nonEolOff < len && mText[mPos-1-nonEolOff]
+                        == mSeparator.charAt(mSeparator.length()-1-nonEolOff)) {
+                    nonEolOff++;
+                }
+                if (nonEolOff >= mPos) {
+                    mPrinter.println("");
+                } else {
+                    mPrinter.println(new String(mText, 0, mPos-nonEolOff));
+                }
+            }
+            mPos = 0;
+        }
+    }
+
+    /**
+     * Ensures that all pending data is sent out to the target. It also
+     * flushes the target. If an I/O error occurs, this writer's error
+     * state is set to {@code true}.
+     */
+    @Override
+    public void flush() {
+        synchronized (lock) {
+            try {
+                flushLocked();
+                if (!mIoError) {
+                    if (mOutputStream != null) {
+                        mOutputStream.flush();
+                    } else if (mWriter != null) {
+                        mWriter.flush();
+                    }
+                }
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (lock) {
+            try {
+                flushLocked();
+                if (mOutputStream != null) {
+                    mOutputStream.close();
+                } else if (mWriter != null) {
+                    mWriter.close();
+                }
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Prints the string representation of the specified character array
+     * to the target.
+     *
+     * @param charArray
+     *            the character array to print to the target.
+     * @see #print(String)
+     */
+    public void print(char[] charArray) {
+        synchronized (lock) {
+            try {
+                appendLocked(charArray, 0, charArray.length);
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Prints the string representation of the specified character to the
+     * target.
+     *
+     * @param ch
+     *            the character to print to the target.
+     * @see #print(String)
+     */
+    public void print(char ch) {
+        synchronized (lock) {
+            try {
+                appendLocked(ch);
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Prints a string to the target. The string is converted to an array of
+     * bytes using the encoding chosen during the construction of this writer.
+     * The bytes are then written to the target with {@code write(int)}.
+     * <p>
+     * If an I/O error occurs, this writer's error flag is set to {@code true}.
+     *
+     * @param str
+     *            the string to print to the target.
+     * @see #write(int)
+     */
+    public void print(String str) {
+        if (str == null) {
+            str = String.valueOf((Object) null);
+        }
+        synchronized (lock) {
+            try {
+                appendLocked(str, 0, str.length());
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+
+    @Override
+    public void print(int inum) {
+        if (inum == 0) {
+            print("0");
+        } else {
+            super.print(inum);
+        }
+    }
+
+    @Override
+    public void print(long lnum) {
+        if (lnum == 0) {
+            print("0");
+        } else {
+            super.print(lnum);
+        }
+    }
+
+    /**
+     * Prints a newline. Flushes this writer if the autoFlush flag is set to {@code true}.
+     */
+    public void println() {
+        synchronized (lock) {
+            try {
+                appendLocked(mSeparator, 0, mSeparator.length());
+                if (mAutoFlush) {
+                    flushLocked();
+                }
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    @Override
+    public void println(int inum) {
+        if (inum == 0) {
+            println("0");
+        } else {
+            super.println(inum);
+        }
+    }
+
+    @Override
+    public void println(long lnum) {
+        if (lnum == 0) {
+            println("0");
+        } else {
+            super.println(lnum);
+        }
+    }
+
+    /**
+     * Prints the string representation of the character array {@code chars} followed by a newline.
+     * Flushes this writer if the autoFlush flag is set to {@code true}.
+     */
+    public void println(char[] chars) {
+        print(chars);
+        println();
+    }
+
+    /**
+     * Prints the string representation of the char {@code c} followed by a newline.
+     * Flushes this writer if the autoFlush flag is set to {@code true}.
+     */
+    public void println(char c) {
+        print(c);
+        println();
+    }
+
+    /**
+     * Writes {@code count} characters from {@code buffer} starting at {@code
+     * offset} to the target.
+     * <p>
+     * This writer's error flag is set to {@code true} if this writer is closed
+     * or an I/O error occurs.
+     *
+     * @param buf
+     *            the buffer to write to the target.
+     * @param offset
+     *            the index of the first character in {@code buffer} to write.
+     * @param count
+     *            the number of characters in {@code buffer} to write.
+     * @throws IndexOutOfBoundsException
+     *             if {@code offset < 0} or {@code count < 0}, or if {@code
+     *             offset + count} is greater than the length of {@code buf}.
+     */
+    @Override
+    public void write(char[] buf, int offset, int count) {
+        synchronized (lock) {
+            try {
+                appendLocked(buf, offset, count);
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Writes one character to the target. Only the two least significant bytes
+     * of the integer {@code oneChar} are written.
+     * <p>
+     * This writer's error flag is set to {@code true} if this writer is closed
+     * or an I/O error occurs.
+     *
+     * @param oneChar
+     *            the character to write to the target.
+     */
+    @Override
+    public void write(int oneChar) {
+        synchronized (lock) {
+            try {
+                appendLocked((char) oneChar);
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Writes the characters from the specified string to the target.
+     *
+     * @param str
+     *            the non-null string containing the characters to write.
+     */
+    @Override
+    public void write(String str) {
+        synchronized (lock) {
+            try {
+                appendLocked(str, 0, str.length());
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Writes {@code count} characters from {@code str} starting at {@code
+     * offset} to the target.
+     *
+     * @param str
+     *            the non-null string containing the characters to write.
+     * @param offset
+     *            the index of the first character in {@code str} to write.
+     * @param count
+     *            the number of characters from {@code str} to write.
+     * @throws IndexOutOfBoundsException
+     *             if {@code offset < 0} or {@code count < 0}, or if {@code
+     *             offset + count} is greater than the length of {@code str}.
+     */
+    @Override
+    public void write(String str, int offset, int count) {
+        synchronized (lock) {
+            try {
+                appendLocked(str, offset, count);
+            } catch (IOException e) {
+                Log.w("FastPrintWriter", "Write failure", e);
+                setError();
+            }
+        }
+    }
+
+    /**
+     * Appends a subsequence of the character sequence {@code csq} to the
+     * target. This method works the same way as {@code
+     * PrintWriter.print(csq.subsequence(start, end).toString())}. If {@code
+     * csq} is {@code null}, then the specified subsequence of the string "null"
+     * will be written to the target.
+     *
+     * @param csq
+     *            the character sequence appended to the target.
+     * @param start
+     *            the index of the first char in the character sequence appended
+     *            to the target.
+     * @param end
+     *            the index of the character following the last character of the
+     *            subsequence appended to the target.
+     * @return this writer.
+     * @throws StringIndexOutOfBoundsException
+     *             if {@code start > end}, {@code start < 0}, {@code end < 0} or
+     *             either {@code start} or {@code end} are greater or equal than
+     *             the length of {@code csq}.
+     */
+    @Override
+    public PrintWriter append(CharSequence csq, int start, int end) {
+        if (csq == null) {
+            csq = "null";
+        }
+        String output = csq.subSequence(start, end).toString();
+        write(output, 0, output.length());
+        return this;
+    }
+}
diff --git a/com/android/internal/util/FastXmlSerializer.java b/com/android/internal/util/FastXmlSerializer.java
new file mode 100644
index 0000000..b85b84f
--- /dev/null
+++ b/com/android/internal/util/FastXmlSerializer.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2006 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.internal.util;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+
+/**
+ * This is a quick and dirty implementation of XmlSerializer that isn't horribly
+ * painfully slow like the normal one.  It only does what is needed for the
+ * specific XML files being written with it.
+ */
+public class FastXmlSerializer implements XmlSerializer {
+    private static final String ESCAPE_TABLE[] = new String[] {
+        "&#0;",   "&#1;",   "&#2;",   "&#3;",  "&#4;",    "&#5;",   "&#6;",  "&#7;",  // 0-7
+        "&#8;",   "&#9;",   "&#10;",  "&#11;", "&#12;",   "&#13;",  "&#14;", "&#15;", // 8-15
+        "&#16;",  "&#17;",  "&#18;",  "&#19;", "&#20;",   "&#21;",  "&#22;", "&#23;", // 16-23
+        "&#24;",  "&#25;",  "&#26;",  "&#27;", "&#28;",   "&#29;",  "&#30;", "&#31;", // 24-31
+        null,     null,     "&quot;", null,     null,     null,     "&amp;",  null,   // 32-39
+        null,     null,     null,     null,     null,     null,     null,     null,   // 40-47
+        null,     null,     null,     null,     null,     null,     null,     null,   // 48-55
+        null,     null,     null,     null,     "&lt;",   null,     "&gt;",   null,   // 56-63
+    };
+
+    private static final int DEFAULT_BUFFER_LEN = 32*1024;
+
+    private static String sSpace = "                                                              ";
+
+    private final int mBufferLen;
+    private final char[] mText;
+    private int mPos;
+
+    private Writer mWriter;
+
+    private OutputStream mOutputStream;
+    private CharsetEncoder mCharset;
+    private ByteBuffer mBytes;
+
+    private boolean mIndent = false;
+    private boolean mInTag;
+
+    private int mNesting = 0;
+    private boolean mLineStart = true;
+
+    public FastXmlSerializer() {
+        this(DEFAULT_BUFFER_LEN);
+    }
+
+    /**
+     * Allocate a FastXmlSerializer with the given internal output buffer size.  If the
+     * size is zero or negative, then the default buffer size will be used.
+     *
+     * @param bufferSize Size in bytes of the in-memory output buffer that the writer will use.
+     */
+    public FastXmlSerializer(int bufferSize) {
+        mBufferLen = (bufferSize > 0) ? bufferSize : DEFAULT_BUFFER_LEN;
+        mText = new char[mBufferLen];
+        mBytes = ByteBuffer.allocate(mBufferLen);
+    }
+
+    private void append(char c) throws IOException {
+        int pos = mPos;
+        if (pos >= (mBufferLen-1)) {
+            flush();
+            pos = mPos;
+        }
+        mText[pos] = c;
+        mPos = pos+1;
+    }
+
+    private void append(String str, int i, final int length) throws IOException {
+        if (length > mBufferLen) {
+            final int end = i + length;
+            while (i < end) {
+                int next = i + mBufferLen;
+                append(str, i, next<end ? mBufferLen : (end-i));
+                i = next;
+            }
+            return;
+        }
+        int pos = mPos;
+        if ((pos+length) > mBufferLen) {
+            flush();
+            pos = mPos;
+        }
+        str.getChars(i, i+length, mText, pos);
+        mPos = pos + length;
+    }
+
+    private void append(char[] buf, int i, final int length) throws IOException {
+        if (length > mBufferLen) {
+            final int end = i + length;
+            while (i < end) {
+                int next = i + mBufferLen;
+                append(buf, i, next<end ? mBufferLen : (end-i));
+                i = next;
+            }
+            return;
+        }
+        int pos = mPos;
+        if ((pos+length) > mBufferLen) {
+            flush();
+            pos = mPos;
+        }
+        System.arraycopy(buf, i, mText, pos, length);
+        mPos = pos + length;
+    }
+
+    private void append(String str) throws IOException {
+        append(str, 0, str.length());
+    }
+
+    private void appendIndent(int indent) throws IOException {
+        indent *= 4;
+        if (indent > sSpace.length()) {
+            indent = sSpace.length();
+        }
+        append(sSpace, 0, indent);
+    }
+
+    private void escapeAndAppendString(final String string) throws IOException {
+        final int N = string.length();
+        final char NE = (char)ESCAPE_TABLE.length;
+        final String[] escapes = ESCAPE_TABLE;
+        int lastPos = 0;
+        int pos;
+        for (pos=0; pos<N; pos++) {
+            char c = string.charAt(pos);
+            if (c >= NE) continue;
+            String escape = escapes[c];
+            if (escape == null) continue;
+            if (lastPos < pos) append(string, lastPos, pos-lastPos);
+            lastPos = pos + 1;
+            append(escape);
+        }
+        if (lastPos < pos) append(string, lastPos, pos-lastPos);
+    }
+
+    private void escapeAndAppendString(char[] buf, int start, int len) throws IOException {
+        final char NE = (char)ESCAPE_TABLE.length;
+        final String[] escapes = ESCAPE_TABLE;
+        int end = start+len;
+        int lastPos = start;
+        int pos;
+        for (pos=start; pos<end; pos++) {
+            char c = buf[pos];
+            if (c >= NE) continue;
+            String escape = escapes[c];
+            if (escape == null) continue;
+            if (lastPos < pos) append(buf, lastPos, pos-lastPos);
+            lastPos = pos + 1;
+            append(escape);
+        }
+        if (lastPos < pos) append(buf, lastPos, pos-lastPos);
+    }
+
+    public XmlSerializer attribute(String namespace, String name, String value) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        append(' ');
+        if (namespace != null) {
+            append(namespace);
+            append(':');
+        }
+        append(name);
+        append("=\"");
+
+        escapeAndAppendString(value);
+        append('"');
+        mLineStart = false;
+        return this;
+    }
+
+    public void cdsect(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void comment(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void docdecl(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void endDocument() throws IOException, IllegalArgumentException, IllegalStateException {
+        flush();
+    }
+
+    public XmlSerializer endTag(String namespace, String name) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        mNesting--;
+        if (mInTag) {
+            append(" />\n");
+        } else {
+            if (mIndent && mLineStart) {
+                appendIndent(mNesting);
+            }
+            append("</");
+            if (namespace != null) {
+                append(namespace);
+                append(':');
+            }
+            append(name);
+            append(">\n");
+        }
+        mLineStart = true;
+        mInTag = false;
+        return this;
+    }
+
+    public void entityRef(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void flushBytes() throws IOException {
+        int position;
+        if ((position = mBytes.position()) > 0) {
+            mBytes.flip();
+            mOutputStream.write(mBytes.array(), 0, position);
+            mBytes.clear();
+        }
+    }
+
+    public void flush() throws IOException {
+        //Log.i("PackageManager", "flush mPos=" + mPos);
+        if (mPos > 0) {
+            if (mOutputStream != null) {
+                CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos);
+                CoderResult result = mCharset.encode(charBuffer, mBytes, true);
+                while (true) {
+                    if (result.isError()) {
+                        throw new IOException(result.toString());
+                    } else if (result.isOverflow()) {
+                        flushBytes();
+                        result = mCharset.encode(charBuffer, mBytes, true);
+                        continue;
+                    }
+                    break;
+                }
+                flushBytes();
+                mOutputStream.flush();
+            } else {
+                mWriter.write(mText, 0, mPos);
+                mWriter.flush();
+            }
+            mPos = 0;
+        }
+    }
+
+    public int getDepth() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean getFeature(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getName() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getNamespace() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getPrefix(String namespace, boolean generatePrefix)
+            throws IllegalArgumentException {
+        throw new UnsupportedOperationException();
+    }
+
+    public Object getProperty(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void ignorableWhitespace(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void processingInstruction(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setFeature(String name, boolean state) throws IllegalArgumentException,
+            IllegalStateException {
+        if (name.equals("http://xmlpull.org/v1/doc/features.html#indent-output")) {
+            mIndent = true;
+            return;
+        }
+        throw new UnsupportedOperationException();
+    }
+
+    public void setOutput(OutputStream os, String encoding) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        if (os == null)
+            throw new IllegalArgumentException();
+        if (true) {
+            try {
+                mCharset = Charset.forName(encoding).newEncoder()
+                        .onMalformedInput(CodingErrorAction.REPLACE)
+                        .onUnmappableCharacter(CodingErrorAction.REPLACE);
+            } catch (IllegalCharsetNameException e) {
+                throw (UnsupportedEncodingException) (new UnsupportedEncodingException(
+                        encoding).initCause(e));
+            } catch (UnsupportedCharsetException e) {
+                throw (UnsupportedEncodingException) (new UnsupportedEncodingException(
+                        encoding).initCause(e));
+            }
+            mOutputStream = os;
+        } else {
+            setOutput(
+                encoding == null
+                    ? new OutputStreamWriter(os)
+                    : new OutputStreamWriter(os, encoding));
+        }
+    }
+
+    public void setOutput(Writer writer) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        mWriter = writer;
+    }
+
+    public void setPrefix(String prefix, String namespace) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setProperty(String name, Object value) throws IllegalArgumentException,
+            IllegalStateException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void startDocument(String encoding, Boolean standalone) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        append("<?xml version='1.0' encoding='utf-8' standalone='"
+                + (standalone ? "yes" : "no") + "' ?>\n");
+        mLineStart = true;
+    }
+
+    public XmlSerializer startTag(String namespace, String name) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        if (mInTag) {
+            append(">\n");
+        }
+        if (mIndent) {
+            appendIndent(mNesting);
+        }
+        mNesting++;
+        append('<');
+        if (namespace != null) {
+            append(namespace);
+            append(':');
+        }
+        append(name);
+        mInTag = true;
+        mLineStart = false;
+        return this;
+    }
+
+    public XmlSerializer text(char[] buf, int start, int len) throws IOException,
+            IllegalArgumentException, IllegalStateException {
+        if (mInTag) {
+            append(">");
+            mInTag = false;
+        }
+        escapeAndAppendString(buf, start, len);
+        if (mIndent) {
+            mLineStart = buf[start+len-1] == '\n';
+        }
+        return this;
+    }
+
+    public XmlSerializer text(String text) throws IOException, IllegalArgumentException,
+            IllegalStateException {
+        if (mInTag) {
+            append(">");
+            mInTag = false;
+        }
+        escapeAndAppendString(text);
+        if (mIndent) {
+            mLineStart = text.length() > 0 && (text.charAt(text.length()-1) == '\n');
+        }
+        return this;
+    }
+
+}
diff --git a/com/android/internal/util/FileRotator.java b/com/android/internal/util/FileRotator.java
new file mode 100644
index 0000000..71550be
--- /dev/null
+++ b/com/android/internal/util/FileRotator.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2012 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.internal.util;
+
+import android.os.FileUtils;
+import android.util.Slog;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+/**
+ * Utility that rotates files over time, similar to {@code logrotate}. There is
+ * a single "active" file, which is periodically rotated into historical files,
+ * and eventually deleted entirely. Files are stored under a specific directory
+ * with a well-known prefix.
+ * <p>
+ * Instead of manipulating files directly, users implement interfaces that
+ * perform operations on {@link InputStream} and {@link OutputStream}. This
+ * enables atomic rewriting of file contents in
+ * {@link #rewriteActive(Rewriter, long)}.
+ * <p>
+ * Users must periodically call {@link #maybeRotate(long)} to perform actual
+ * rotation. Not inherently thread safe.
+ */
+public class FileRotator {
+    private static final String TAG = "FileRotator";
+    private static final boolean LOGD = false;
+
+    private final File mBasePath;
+    private final String mPrefix;
+    private final long mRotateAgeMillis;
+    private final long mDeleteAgeMillis;
+
+    private static final String SUFFIX_BACKUP = ".backup";
+    private static final String SUFFIX_NO_BACKUP = ".no_backup";
+
+    // TODO: provide method to append to active file
+
+    /**
+     * External class that reads data from a given {@link InputStream}. May be
+     * called multiple times when reading rotated data.
+     */
+    public interface Reader {
+        public void read(InputStream in) throws IOException;
+    }
+
+    /**
+     * External class that writes data to a given {@link OutputStream}.
+     */
+    public interface Writer {
+        public void write(OutputStream out) throws IOException;
+    }
+
+    /**
+     * External class that reads existing data from given {@link InputStream},
+     * then writes any modified data to {@link OutputStream}.
+     */
+    public interface Rewriter extends Reader, Writer {
+        public void reset();
+        public boolean shouldWrite();
+    }
+
+    /**
+     * Create a file rotator.
+     *
+     * @param basePath Directory under which all files will be placed.
+     * @param prefix Filename prefix used to identify this rotator.
+     * @param rotateAgeMillis Age in milliseconds beyond which an active file
+     *            may be rotated into a historical file.
+     * @param deleteAgeMillis Age in milliseconds beyond which a rotated file
+     *            may be deleted.
+     */
+    public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) {
+        mBasePath = Preconditions.checkNotNull(basePath);
+        mPrefix = Preconditions.checkNotNull(prefix);
+        mRotateAgeMillis = rotateAgeMillis;
+        mDeleteAgeMillis = deleteAgeMillis;
+
+        // ensure that base path exists
+        mBasePath.mkdirs();
+
+        // recover any backup files
+        for (String name : mBasePath.list()) {
+            if (!name.startsWith(mPrefix)) continue;
+
+            if (name.endsWith(SUFFIX_BACKUP)) {
+                if (LOGD) Slog.d(TAG, "recovering " + name);
+
+                final File backupFile = new File(mBasePath, name);
+                final File file = new File(
+                        mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
+
+                // write failed with backup; recover last file
+                backupFile.renameTo(file);
+
+            } else if (name.endsWith(SUFFIX_NO_BACKUP)) {
+                if (LOGD) Slog.d(TAG, "recovering " + name);
+
+                final File noBackupFile = new File(mBasePath, name);
+                final File file = new File(
+                        mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
+
+                // write failed without backup; delete both
+                noBackupFile.delete();
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Delete all files managed by this rotator.
+     */
+    public void deleteAll() {
+        final FileInfo info = new FileInfo(mPrefix);
+        for (String name : mBasePath.list()) {
+            if (info.parse(name)) {
+                // delete each file that matches parser
+                new File(mBasePath, name).delete();
+            }
+        }
+    }
+
+    /**
+     * Dump all files managed by this rotator for debugging purposes.
+     */
+    public void dumpAll(OutputStream os) throws IOException {
+        final ZipOutputStream zos = new ZipOutputStream(os);
+        try {
+            final FileInfo info = new FileInfo(mPrefix);
+            for (String name : mBasePath.list()) {
+                if (info.parse(name)) {
+                    final ZipEntry entry = new ZipEntry(name);
+                    zos.putNextEntry(entry);
+
+                    final File file = new File(mBasePath, name);
+                    final FileInputStream is = new FileInputStream(file);
+                    try {
+                        Streams.copy(is, zos);
+                    } finally {
+                        IoUtils.closeQuietly(is);
+                    }
+
+                    zos.closeEntry();
+                }
+            }
+        } finally {
+            IoUtils.closeQuietly(zos);
+        }
+    }
+
+    /**
+     * Process currently active file, first reading any existing data, then
+     * writing modified data. Maintains a backup during write, which is restored
+     * if the write fails.
+     */
+    public void rewriteActive(Rewriter rewriter, long currentTimeMillis)
+            throws IOException {
+        final String activeName = getActiveName(currentTimeMillis);
+        rewriteSingle(rewriter, activeName);
+    }
+
+    @Deprecated
+    public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis)
+            throws IOException {
+        rewriteActive(new Rewriter() {
+            @Override
+            public void reset() {
+                // ignored
+            }
+
+            @Override
+            public void read(InputStream in) throws IOException {
+                reader.read(in);
+            }
+
+            @Override
+            public boolean shouldWrite() {
+                return true;
+            }
+
+            @Override
+            public void write(OutputStream out) throws IOException {
+                writer.write(out);
+            }
+        }, currentTimeMillis);
+    }
+
+    /**
+     * Process all files managed by this rotator, usually to rewrite historical
+     * data. Each file is processed atomically.
+     */
+    public void rewriteAll(Rewriter rewriter) throws IOException {
+        final FileInfo info = new FileInfo(mPrefix);
+        for (String name : mBasePath.list()) {
+            if (!info.parse(name)) continue;
+
+            // process each file that matches parser
+            rewriteSingle(rewriter, name);
+        }
+    }
+
+    /**
+     * Process a single file atomically, first reading any existing data, then
+     * writing modified data. Maintains a backup during write, which is restored
+     * if the write fails.
+     */
+    private void rewriteSingle(Rewriter rewriter, String name) throws IOException {
+        if (LOGD) Slog.d(TAG, "rewriting " + name);
+
+        final File file = new File(mBasePath, name);
+        final File backupFile;
+
+        rewriter.reset();
+
+        if (file.exists()) {
+            // read existing data
+            readFile(file, rewriter);
+
+            // skip when rewriter has nothing to write
+            if (!rewriter.shouldWrite()) return;
+
+            // backup existing data during write
+            backupFile = new File(mBasePath, name + SUFFIX_BACKUP);
+            file.renameTo(backupFile);
+
+            try {
+                writeFile(file, rewriter);
+
+                // write success, delete backup
+                backupFile.delete();
+            } catch (Throwable t) {
+                // write failed, delete file and restore backup
+                file.delete();
+                backupFile.renameTo(file);
+                throw rethrowAsIoException(t);
+            }
+
+        } else {
+            // create empty backup during write
+            backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP);
+            backupFile.createNewFile();
+
+            try {
+                writeFile(file, rewriter);
+
+                // write success, delete empty backup
+                backupFile.delete();
+            } catch (Throwable t) {
+                // write failed, delete file and empty backup
+                file.delete();
+                backupFile.delete();
+                throw rethrowAsIoException(t);
+            }
+        }
+    }
+
+    /**
+     * Read any rotated data that overlap the requested time range.
+     */
+    public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis)
+            throws IOException {
+        final FileInfo info = new FileInfo(mPrefix);
+        for (String name : mBasePath.list()) {
+            if (!info.parse(name)) continue;
+
+            // read file when it overlaps
+            if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
+                if (LOGD) Slog.d(TAG, "reading matching " + name);
+
+                final File file = new File(mBasePath, name);
+                readFile(file, reader);
+            }
+        }
+    }
+
+    /**
+     * Return the currently active file, which may not exist yet.
+     */
+    private String getActiveName(long currentTimeMillis) {
+        String oldestActiveName = null;
+        long oldestActiveStart = Long.MAX_VALUE;
+
+        final FileInfo info = new FileInfo(mPrefix);
+        for (String name : mBasePath.list()) {
+            if (!info.parse(name)) continue;
+
+            // pick the oldest active file which covers current time
+            if (info.isActive() && info.startMillis < currentTimeMillis
+                    && info.startMillis < oldestActiveStart) {
+                oldestActiveName = name;
+                oldestActiveStart = info.startMillis;
+            }
+        }
+
+        if (oldestActiveName != null) {
+            return oldestActiveName;
+        } else {
+            // no active file found above; create one starting now
+            info.startMillis = currentTimeMillis;
+            info.endMillis = Long.MAX_VALUE;
+            return info.build();
+        }
+    }
+
+    /**
+     * Examine all files managed by this rotator, renaming or deleting if their
+     * age matches the configured thresholds.
+     */
+    public void maybeRotate(long currentTimeMillis) {
+        final long rotateBefore = currentTimeMillis - mRotateAgeMillis;
+        final long deleteBefore = currentTimeMillis - mDeleteAgeMillis;
+
+        final FileInfo info = new FileInfo(mPrefix);
+        String[] baseFiles = mBasePath.list();
+        if (baseFiles == null) {
+            return;
+        }
+
+        for (String name : baseFiles) {
+            if (!info.parse(name)) continue;
+
+            if (info.isActive()) {
+                if (info.startMillis <= rotateBefore) {
+                    // found active file; rotate if old enough
+                    if (LOGD) Slog.d(TAG, "rotating " + name);
+
+                    info.endMillis = currentTimeMillis;
+
+                    final File file = new File(mBasePath, name);
+                    final File destFile = new File(mBasePath, info.build());
+                    file.renameTo(destFile);
+                }
+            } else if (info.endMillis <= deleteBefore) {
+                // found rotated file; delete if old enough
+                if (LOGD) Slog.d(TAG, "deleting " + name);
+
+                final File file = new File(mBasePath, name);
+                file.delete();
+            }
+        }
+    }
+
+    private static void readFile(File file, Reader reader) throws IOException {
+        final FileInputStream fis = new FileInputStream(file);
+        final BufferedInputStream bis = new BufferedInputStream(fis);
+        try {
+            reader.read(bis);
+        } finally {
+            IoUtils.closeQuietly(bis);
+        }
+    }
+
+    private static void writeFile(File file, Writer writer) throws IOException {
+        final FileOutputStream fos = new FileOutputStream(file);
+        final BufferedOutputStream bos = new BufferedOutputStream(fos);
+        try {
+            writer.write(bos);
+            bos.flush();
+        } finally {
+            FileUtils.sync(fos);
+            IoUtils.closeQuietly(bos);
+        }
+    }
+
+    private static IOException rethrowAsIoException(Throwable t) throws IOException {
+        if (t instanceof IOException) {
+            throw (IOException) t;
+        } else {
+            throw new IOException(t.getMessage(), t);
+        }
+    }
+
+    /**
+     * Details for a rotated file, either parsed from an existing filename, or
+     * ready to be built into a new filename.
+     */
+    private static class FileInfo {
+        public final String prefix;
+
+        public long startMillis;
+        public long endMillis;
+
+        public FileInfo(String prefix) {
+            this.prefix = Preconditions.checkNotNull(prefix);
+        }
+
+        /**
+         * Attempt parsing the given filename.
+         *
+         * @return Whether parsing was successful.
+         */
+        public boolean parse(String name) {
+            startMillis = endMillis = -1;
+
+            final int dotIndex = name.lastIndexOf('.');
+            final int dashIndex = name.lastIndexOf('-');
+
+            // skip when missing time section
+            if (dotIndex == -1 || dashIndex == -1) return false;
+
+            // skip when prefix doesn't match
+            if (!prefix.equals(name.substring(0, dotIndex))) return false;
+
+            try {
+                startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex));
+
+                if (name.length() - dashIndex == 1) {
+                    endMillis = Long.MAX_VALUE;
+                } else {
+                    endMillis = Long.parseLong(name.substring(dashIndex + 1));
+                }
+
+                return true;
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+
+        /**
+         * Build current state into filename.
+         */
+        public String build() {
+            final StringBuilder name = new StringBuilder();
+            name.append(prefix).append('.').append(startMillis).append('-');
+            if (endMillis != Long.MAX_VALUE) {
+                name.append(endMillis);
+            }
+            return name.toString();
+        }
+
+        /**
+         * Test if current file is active (no end timestamp).
+         */
+        public boolean isActive() {
+            return endMillis == Long.MAX_VALUE;
+        }
+    }
+}
diff --git a/com/android/internal/util/FunctionalUtils.java b/com/android/internal/util/FunctionalUtils.java
new file mode 100644
index 0000000..cdef97e
--- /dev/null
+++ b/com/android/internal/util/FunctionalUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import java.util.function.Supplier;
+
+/**
+ * Utilities specific to functional programming
+ */
+public class FunctionalUtils {
+    private FunctionalUtils() {}
+
+    /**
+     * An equivalent of {@link Runnable} that allows throwing checked exceptions
+     *
+     * This can be used to specify a lambda argument without forcing all the checked exceptions
+     * to be handled within it
+     */
+    @FunctionalInterface
+    public interface ThrowingRunnable {
+        void run() throws Exception;
+    }
+
+    /**
+     * An equivalent of {@link Supplier} that allows throwing checked exceptions
+     *
+     * This can be used to specify a lambda argument without forcing all the checked exceptions
+     * to be handled within it
+     */
+    @FunctionalInterface
+    public interface ThrowingSupplier<T> {
+        T get() throws Exception;
+    }
+
+    /**
+     * An equivalent of {@link java.util.function.Consumer} that allows throwing checked exceptions
+     *
+     * This can be used to specify a lambda argument without forcing all the checked exceptions
+     * to be handled within it
+     */
+    @FunctionalInterface
+    public interface ThrowingConsumer<T> {
+        void accept(T t) throws Exception;
+    }
+}
diff --git a/com/android/internal/util/GrowingArrayUtils.java b/com/android/internal/util/GrowingArrayUtils.java
new file mode 100644
index 0000000..968d920
--- /dev/null
+++ b/com/android/internal/util/GrowingArrayUtils.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2014 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.internal.util;
+
+/**
+ * A helper class that aims to provide comparable growth performance to ArrayList, but on primitive
+ * arrays. Common array operations are implemented for efficient use in dynamic containers.
+ *
+ * All methods in this class assume that the length of an array is equivalent to its capacity and
+ * NOT the number of elements in the array. The current size of the array is always passed in as a
+ * parameter.
+ *
+ * @hide
+ */
+public final class GrowingArrayUtils {
+
+    /**
+     * Appends an element to the end of the array, growing the array if there is no more room.
+     * @param array The array to which to append the element. This must NOT be null.
+     * @param currentSize The number of elements in the array. Must be less than or equal to
+     *                    array.length.
+     * @param element The element to append.
+     * @return the array to which the element was appended. This may be different than the given
+     *         array.
+     */
+    public static <T> T[] append(T[] array, int currentSize, T element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 > array.length) {
+            @SuppressWarnings("unchecked")
+            T[] newArray = ArrayUtils.newUnpaddedArray(
+                    (Class<T>) array.getClass().getComponentType(), growSize(currentSize));
+            System.arraycopy(array, 0, newArray, 0, currentSize);
+            array = newArray;
+        }
+        array[currentSize] = element;
+        return array;
+    }
+
+    /**
+     * Primitive int version of {@link #append(Object[], int, Object)}.
+     */
+    public static int[] append(int[] array, int currentSize, int element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 > array.length) {
+            int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
+            System.arraycopy(array, 0, newArray, 0, currentSize);
+            array = newArray;
+        }
+        array[currentSize] = element;
+        return array;
+    }
+
+    /**
+     * Primitive long version of {@link #append(Object[], int, Object)}.
+     */
+    public static long[] append(long[] array, int currentSize, long element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 > array.length) {
+            long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
+            System.arraycopy(array, 0, newArray, 0, currentSize);
+            array = newArray;
+        }
+        array[currentSize] = element;
+        return array;
+    }
+
+    /**
+     * Primitive boolean version of {@link #append(Object[], int, Object)}.
+     */
+    public static boolean[] append(boolean[] array, int currentSize, boolean element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 > array.length) {
+            boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
+            System.arraycopy(array, 0, newArray, 0, currentSize);
+            array = newArray;
+        }
+        array[currentSize] = element;
+        return array;
+    }
+
+    /**
+     * Primitive float version of {@link #append(Object[], int, Object)}.
+     */
+    public static float[] append(float[] array, int currentSize, float element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 > array.length) {
+            float[] newArray = ArrayUtils.newUnpaddedFloatArray(growSize(currentSize));
+            System.arraycopy(array, 0, newArray, 0, currentSize);
+            array = newArray;
+        }
+        array[currentSize] = element;
+        return array;
+    }
+
+    /**
+     * Inserts an element into the array at the specified index, growing the array if there is no
+     * more room.
+     *
+     * @param array The array to which to append the element. Must NOT be null.
+     * @param currentSize The number of elements in the array. Must be less than or equal to
+     *                    array.length.
+     * @param element The element to insert.
+     * @return the array to which the element was appended. This may be different than the given
+     *         array.
+     */
+    public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 <= array.length) {
+            System.arraycopy(array, index, array, index + 1, currentSize - index);
+            array[index] = element;
+            return array;
+        }
+
+        @SuppressWarnings("unchecked")
+        T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
+                growSize(currentSize));
+        System.arraycopy(array, 0, newArray, 0, index);
+        newArray[index] = element;
+        System.arraycopy(array, index, newArray, index + 1, array.length - index);
+        return newArray;
+    }
+
+    /**
+     * Primitive int version of {@link #insert(Object[], int, int, Object)}.
+     */
+    public static int[] insert(int[] array, int currentSize, int index, int element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 <= array.length) {
+            System.arraycopy(array, index, array, index + 1, currentSize - index);
+            array[index] = element;
+            return array;
+        }
+
+        int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
+        System.arraycopy(array, 0, newArray, 0, index);
+        newArray[index] = element;
+        System.arraycopy(array, index, newArray, index + 1, array.length - index);
+        return newArray;
+    }
+
+    /**
+     * Primitive long version of {@link #insert(Object[], int, int, Object)}.
+     */
+    public static long[] insert(long[] array, int currentSize, int index, long element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 <= array.length) {
+            System.arraycopy(array, index, array, index + 1, currentSize - index);
+            array[index] = element;
+            return array;
+        }
+
+        long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
+        System.arraycopy(array, 0, newArray, 0, index);
+        newArray[index] = element;
+        System.arraycopy(array, index, newArray, index + 1, array.length - index);
+        return newArray;
+    }
+
+    /**
+     * Primitive boolean version of {@link #insert(Object[], int, int, Object)}.
+     */
+    public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) {
+        assert currentSize <= array.length;
+
+        if (currentSize + 1 <= array.length) {
+            System.arraycopy(array, index, array, index + 1, currentSize - index);
+            array[index] = element;
+            return array;
+        }
+
+        boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
+        System.arraycopy(array, 0, newArray, 0, index);
+        newArray[index] = element;
+        System.arraycopy(array, index, newArray, index + 1, array.length - index);
+        return newArray;
+    }
+
+    /**
+     * Given the current size of an array, returns an ideal size to which the array should grow.
+     * This is typically double the given size, but should not be relied upon to do so in the
+     * future.
+     */
+    public static int growSize(int currentSize) {
+        return currentSize <= 4 ? 8 : currentSize * 2;
+    }
+
+    // Uninstantiable
+    private GrowingArrayUtils() {}
+}
diff --git a/com/android/internal/util/HexDump.java b/com/android/internal/util/HexDump.java
new file mode 100644
index 0000000..7be95d8
--- /dev/null
+++ b/com/android/internal/util/HexDump.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2006 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.internal.util;
+
+public class HexDump
+{
+    private final static char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+    private final static char[] HEX_LOWER_CASE_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+    public static String dumpHexString(byte[] array)
+    {
+        return dumpHexString(array, 0, array.length);
+    }
+
+    public static String dumpHexString(byte[] array, int offset, int length)
+    {
+        StringBuilder result = new StringBuilder();
+
+        byte[] line = new byte[16];
+        int lineIndex = 0;
+
+        result.append("\n0x");
+        result.append(toHexString(offset));
+
+        for (int i = offset ; i < offset + length ; i++)
+        {
+            if (lineIndex == 16)
+            {
+                result.append(" ");
+
+                for (int j = 0 ; j < 16 ; j++)
+                {
+                    if (line[j] > ' ' && line[j] < '~')
+                    {
+                        result.append(new String(line, j, 1));
+                    }
+                    else
+                    {
+                        result.append(".");
+                    }
+                }
+
+                result.append("\n0x");
+                result.append(toHexString(i));
+                lineIndex = 0;
+            }
+
+            byte b = array[i];
+            result.append(" ");
+            result.append(HEX_DIGITS[(b >>> 4) & 0x0F]);
+            result.append(HEX_DIGITS[b & 0x0F]);
+
+            line[lineIndex++] = b;
+        }
+
+        if (lineIndex != 16)
+        {
+            int count = (16 - lineIndex) * 3;
+            count++;
+            for (int i = 0 ; i < count ; i++)
+            {
+                result.append(" ");
+            }
+
+            for (int i = 0 ; i < lineIndex ; i++)
+            {
+                if (line[i] > ' ' && line[i] < '~')
+                {
+                    result.append(new String(line, i, 1));
+                }
+                else
+                {
+                    result.append(".");
+                }
+            }
+        }
+
+        return result.toString();
+    }
+
+    public static String toHexString(byte b)
+    {
+        return toHexString(toByteArray(b));
+    }
+
+    public static String toHexString(byte[] array)
+    {
+        return toHexString(array, 0, array.length, true);
+    }
+
+    public static String toHexString(byte[] array, boolean upperCase)
+    {
+        return toHexString(array, 0, array.length, upperCase);
+    }
+
+    public static String toHexString(byte[] array, int offset, int length)
+    {
+        return toHexString(array, offset, length, true);
+    }
+
+    public static String toHexString(byte[] array, int offset, int length, boolean upperCase)
+    {
+        char[] digits = upperCase ? HEX_DIGITS : HEX_LOWER_CASE_DIGITS;
+        char[] buf = new char[length * 2];
+
+        int bufIndex = 0;
+        for (int i = offset ; i < offset + length; i++)
+        {
+            byte b = array[i];
+            buf[bufIndex++] = digits[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = digits[b & 0x0F];
+        }
+
+        return new String(buf);
+    }
+
+    public static String toHexString(int i)
+    {
+        return toHexString(toByteArray(i));
+    }
+
+    public static byte[] toByteArray(byte b)
+    {
+        byte[] array = new byte[1];
+        array[0] = b;
+        return array;
+    }
+
+    public static byte[] toByteArray(int i)
+    {
+        byte[] array = new byte[4];
+
+        array[3] = (byte)(i & 0xFF);
+        array[2] = (byte)((i >> 8) & 0xFF);
+        array[1] = (byte)((i >> 16) & 0xFF);
+        array[0] = (byte)((i >> 24) & 0xFF);
+
+        return array;
+    }
+
+    private static int toByte(char c)
+    {
+        if (c >= '0' && c <= '9') return (c - '0');
+        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
+        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+
+        throw new RuntimeException ("Invalid hex char '" + c + "'");
+    }
+
+    public static byte[] hexStringToByteArray(String hexString)
+    {
+        int length = hexString.length();
+        byte[] buffer = new byte[length / 2];
+
+        for (int i = 0 ; i < length ; i += 2)
+        {
+            buffer[i / 2] = (byte)((toByte(hexString.charAt(i)) << 4) | toByte(hexString.charAt(i+1)));
+        }
+
+        return buffer;
+    }
+
+    public static StringBuilder appendByteAsHex(StringBuilder sb, byte b, boolean upperCase) {
+        char[] digits = upperCase ? HEX_DIGITS : HEX_LOWER_CASE_DIGITS;
+        sb.append(digits[(b >> 4) & 0xf]);
+        sb.append(digits[b & 0xf]);
+        return sb;
+    }
+
+}
diff --git a/com/android/internal/util/IState.java b/com/android/internal/util/IState.java
new file mode 100644
index 0000000..056f8e9
--- /dev/null
+++ b/com/android/internal/util/IState.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright (C) 2011 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.internal.util;
+
+import android.os.Message;
+
+/**
+ * {@hide}
+ *
+ * The interface for implementing states in a {@link StateMachine}
+ */
+public interface IState {
+
+    /**
+     * Returned by processMessage to indicate the the message was processed.
+     */
+    static final boolean HANDLED = true;
+
+    /**
+     * Returned by processMessage to indicate the the message was NOT processed.
+     */
+    static final boolean NOT_HANDLED = false;
+
+    /**
+     * Called when a state is entered.
+     */
+    void enter();
+
+    /**
+     * Called when a state is exited.
+     */
+    void exit();
+
+    /**
+     * Called when a message is to be processed by the
+     * state machine.
+     *
+     * This routine is never reentered thus no synchronization
+     * is needed as only one processMessage method will ever be
+     * executing within a state machine at any given time. This
+     * does mean that processing by this routine must be completed
+     * as expeditiously as possible as no subsequent messages will
+     * be processed until this routine returns.
+     *
+     * @param msg to process
+     * @return HANDLED if processing has completed and NOT_HANDLED
+     *         if the message wasn't processed.
+     */
+    boolean processMessage(Message msg);
+
+    /**
+     * Name of State for debugging purposes.
+     *
+     * @return name of state.
+     */
+    String getName();
+}
diff --git a/com/android/internal/util/ImageUtils.java b/com/android/internal/util/ImageUtils.java
new file mode 100644
index 0000000..7d56e9e
--- /dev/null
+++ b/com/android/internal/util/ImageUtils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2014 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.internal.util;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Utility class for image analysis and processing.
+ *
+ * @hide
+ */
+public class ImageUtils {
+
+    // Amount (max is 255) that two channels can differ before the color is no longer "gray".
+    private static final int TOLERANCE = 20;
+
+    // Alpha amount for which values below are considered transparent.
+    private static final int ALPHA_TOLERANCE = 50;
+
+    // Size of the smaller bitmap we're actually going to scan.
+    private static final int COMPACT_BITMAP_SIZE = 64; // pixels
+
+    private int[] mTempBuffer;
+    private Bitmap mTempCompactBitmap;
+    private Canvas mTempCompactBitmapCanvas;
+    private Paint mTempCompactBitmapPaint;
+    private final Matrix mTempMatrix = new Matrix();
+
+    /**
+     * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect
+     * gray".
+     *
+     * Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than
+     * COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements
+     * will survive the squeezing process, contaminating the result with color.
+     */
+    public boolean isGrayscale(Bitmap bitmap) {
+        int height = bitmap.getHeight();
+        int width = bitmap.getWidth();
+
+        // shrink to a more manageable (yet hopefully no more or less colorful) size
+        if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) {
+            if (mTempCompactBitmap == null) {
+                mTempCompactBitmap = Bitmap.createBitmap(
+                        COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Bitmap.Config.ARGB_8888
+                );
+                mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap);
+                mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+                mTempCompactBitmapPaint.setFilterBitmap(true);
+            }
+            mTempMatrix.reset();
+            mTempMatrix.setScale(
+                    (float) COMPACT_BITMAP_SIZE / width,
+                    (float) COMPACT_BITMAP_SIZE / height,
+                    0, 0);
+            mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase
+            mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint);
+            bitmap = mTempCompactBitmap;
+            width = height = COMPACT_BITMAP_SIZE;
+        }
+
+        final int size = height*width;
+        ensureBufferSize(size);
+        bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height);
+        for (int i = 0; i < size; i++) {
+            if (!isGrayscale(mTempBuffer[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Makes sure that {@code mTempBuffer} has at least length {@code size}.
+     */
+    private void ensureBufferSize(int size) {
+        if (mTempBuffer == null || mTempBuffer.length < size) {
+            mTempBuffer = new int[size];
+        }
+    }
+
+    /**
+     * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect
+     * gray"; if all three channels are approximately equal, this will return true.
+     *
+     * Note that really transparent colors are always grayscale.
+     */
+    public static boolean isGrayscale(int color) {
+        int alpha = 0xFF & (color >> 24);
+        if (alpha < ALPHA_TOLERANCE) {
+            return true;
+        }
+
+        int r = 0xFF & (color >> 16);
+        int g = 0xFF & (color >> 8);
+        int b = 0xFF & color;
+
+        return Math.abs(r - g) < TOLERANCE
+                && Math.abs(r - b) < TOLERANCE
+                && Math.abs(g - b) < TOLERANCE;
+    }
+
+    /**
+     * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight.
+     */
+    public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth,
+            int maxHeight) {
+        if (drawable == null) {
+            return null;
+        }
+        int originalWidth = drawable.getIntrinsicWidth();
+        int originalHeight = drawable.getIntrinsicHeight();
+
+        if ((originalWidth <= maxWidth) && (originalHeight <= maxHeight) &&
+                (drawable instanceof BitmapDrawable)) {
+            return ((BitmapDrawable) drawable).getBitmap();
+        }
+        if (originalHeight <= 0 || originalWidth <= 0) {
+            return null;
+        }
+
+        // create a new bitmap, scaling down to fit the max dimensions of
+        // a large notification icon if necessary
+        float ratio = Math.min((float) maxWidth / (float) originalWidth,
+                (float) maxHeight / (float) originalHeight);
+        ratio = Math.min(1.0f, ratio);
+        int scaledWidth = (int) (ratio * originalWidth);
+        int scaledHeight = (int) (ratio * originalHeight);
+        Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888);
+
+        // and paint our app bitmap on it
+        Canvas canvas = new Canvas(result);
+        drawable.setBounds(0, 0, scaledWidth, scaledHeight);
+        drawable.draw(canvas);
+
+        return result;
+    }
+}
diff --git a/com/android/internal/util/IndentingPrintWriter.java b/com/android/internal/util/IndentingPrintWriter.java
new file mode 100644
index 0000000..696667c
--- /dev/null
+++ b/com/android/internal/util/IndentingPrintWriter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2012 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.internal.util;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents
+ * newlines based on internal state. It also automatically wraps long lines
+ * based on given line length.
+ * <p>
+ * Delays writing indent until first actual write on a newline, enabling indent
+ * modification after newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+    private final String mSingleIndent;
+    private final int mWrapLength;
+
+    /** Mutable version of current indent */
+    private StringBuilder mIndentBuilder = new StringBuilder();
+    /** Cache of current {@link #mIndentBuilder} value */
+    private char[] mCurrentIndent;
+    /** Length of current line being built, excluding any indent */
+    private int mCurrentLength;
+
+    /**
+     * Flag indicating if we're currently sitting on an empty line, and that
+     * next write should be prefixed with the current indent.
+     */
+    private boolean mEmptyLine = true;
+
+    private char[] mSingleChar = new char[1];
+
+    public IndentingPrintWriter(Writer writer, String singleIndent) {
+        this(writer, singleIndent, -1);
+    }
+
+    public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
+        super(writer);
+        mSingleIndent = singleIndent;
+        mWrapLength = wrapLength;
+    }
+
+    public void increaseIndent() {
+        mIndentBuilder.append(mSingleIndent);
+        mCurrentIndent = null;
+    }
+
+    public void decreaseIndent() {
+        mIndentBuilder.delete(0, mSingleIndent.length());
+        mCurrentIndent = null;
+    }
+
+    public void printPair(String key, Object value) {
+        print(key + "=" + String.valueOf(value) + " ");
+    }
+
+    public void printPair(String key, Object[] value) {
+        print(key + "=" + Arrays.toString(value) + " ");
+    }
+
+    public void printHexPair(String key, int value) {
+        print(key + "=0x" + Integer.toHexString(value) + " ");
+    }
+
+    @Override
+    public void println() {
+        write('\n');
+    }
+
+    @Override
+    public void write(int c) {
+        mSingleChar[0] = (char) c;
+        write(mSingleChar, 0, 1);
+    }
+
+    @Override
+    public void write(String s, int off, int len) {
+        final char[] buf = new char[len];
+        s.getChars(off, len - off, buf, 0);
+        write(buf, 0, len);
+    }
+
+    @Override
+    public void write(char[] buf, int offset, int count) {
+        final int indentLength = mIndentBuilder.length();
+        final int bufferEnd = offset + count;
+        int lineStart = offset;
+        int lineEnd = offset;
+
+        // March through incoming buffer looking for newlines
+        while (lineEnd < bufferEnd) {
+            char ch = buf[lineEnd++];
+            mCurrentLength++;
+            if (ch == '\n') {
+                maybeWriteIndent();
+                super.write(buf, lineStart, lineEnd - lineStart);
+                lineStart = lineEnd;
+                mEmptyLine = true;
+                mCurrentLength = 0;
+            }
+
+            // Wrap if we've pushed beyond line length
+            if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+                if (!mEmptyLine) {
+                    // Give ourselves a fresh line to work with
+                    super.write('\n');
+                    mEmptyLine = true;
+                    mCurrentLength = lineEnd - lineStart;
+                } else {
+                    // We need more than a dedicated line, slice it hard
+                    maybeWriteIndent();
+                    super.write(buf, lineStart, lineEnd - lineStart);
+                    super.write('\n');
+                    mEmptyLine = true;
+                    lineStart = lineEnd;
+                    mCurrentLength = 0;
+                }
+            }
+        }
+
+        if (lineStart != lineEnd) {
+            maybeWriteIndent();
+            super.write(buf, lineStart, lineEnd - lineStart);
+        }
+    }
+
+    private void maybeWriteIndent() {
+        if (mEmptyLine) {
+            mEmptyLine = false;
+            if (mIndentBuilder.length() != 0) {
+                if (mCurrentIndent == null) {
+                    mCurrentIndent = mIndentBuilder.toString().toCharArray();
+                }
+                super.write(mCurrentIndent, 0, mCurrentIndent.length);
+            }
+        }
+    }
+}
diff --git a/com/android/internal/util/IntPair.java b/com/android/internal/util/IntPair.java
new file mode 100644
index 0000000..7992507
--- /dev/null
+++ b/com/android/internal/util/IntPair.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+/**
+ * Utilities for treating a {@code long} as a pair of {@code int}s
+ *
+ * @hide
+ */
+public class IntPair {
+    private IntPair() {}
+
+    public static long of(int first, int second) {
+        return (((long)first) << 32) | ((long)second & 0xffffffffL);
+    }
+
+    public static int first(long intPair) {
+        return (int)(intPair >> 32);
+    }
+
+    public static int second(long intPair) {
+        return (int)intPair;
+    }
+}
diff --git a/com/android/internal/util/JournaledFile.java b/com/android/internal/util/JournaledFile.java
new file mode 100644
index 0000000..5372fc0
--- /dev/null
+++ b/com/android/internal/util/JournaledFile.java
@@ -0,0 +1,116 @@
+/*
+ * 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.internal.util;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @deprecated Use {@code AtomicFile} instead.  It would
+ * be nice to update all existing uses of this to switch to AtomicFile, but since
+ * their on-file semantics are slightly different that would run the risk of losing
+ * data if at the point of the platform upgrade to the new code it would need to
+ * roll back to the backup file.  This can be solved...  but is it worth it and
+ * all of the testing needed to make sure it is correct?
+ */
+@Deprecated
+public class JournaledFile {
+    File mReal;
+    File mTemp;
+    boolean mWriting;
+
+    public JournaledFile(File real, File temp) {
+        mReal = real;
+        mTemp = temp;
+    }
+
+    /** Returns the file for you to read.
+     * @more
+     * Prefers the real file.  If it doesn't exist, uses the temp one, and then copies
+     * it to the real one.  If there is both a real file and a temp one, assumes that the
+     * temp one isn't fully written and deletes it.
+     */
+    public File chooseForRead() {
+        File result;
+        if (mReal.exists()) {
+            result = mReal;
+            if (mTemp.exists()) {
+                mTemp.delete();
+            }
+        } else if (mTemp.exists()) {
+            result = mTemp;
+            mTemp.renameTo(mReal);
+        } else {
+            return mReal;
+        }
+        return result;
+    }
+
+    /**
+     * Returns a file for you to write.
+     * @more
+     * If a write is already happening, throws.  In other words, you must provide your
+     * own locking.
+     * <p>
+     * Call {@link #commit} to commit the changes, or {@link #rollback} to forget the changes.
+     */
+    public File chooseForWrite() {
+        if (mWriting) {
+            throw new IllegalStateException("uncommitted write already in progress");
+        }
+        if (!mReal.exists()) {
+            // If the real one doesn't exist, it's either because this is the first time
+            // or because something went wrong while copying them.  In this case, we can't
+            // trust anything that's in temp.  In order to have the chooseForRead code not
+            // use the temporary one until it's fully written, create an empty file
+            // for real, which will we'll shortly delete.
+            try {
+                mReal.createNewFile();
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+
+        if (mTemp.exists()) {
+            mTemp.delete();
+        }
+        mWriting = true;
+        return mTemp;
+    }
+
+    /**
+     * Commit changes.
+     */
+    public void commit() {
+        if (!mWriting) {
+            throw new IllegalStateException("no file to commit");
+        }
+        mWriting = false;
+        mTemp.renameTo(mReal);
+    }
+
+    /**
+     * Roll back changes.
+     */
+    public void rollback() {
+        if (!mWriting) {
+            throw new IllegalStateException("no file to roll back");
+        }
+        mWriting = false;
+        mTemp.delete();
+    }
+}
diff --git a/com/android/internal/util/LineBreakBufferedWriter.java b/com/android/internal/util/LineBreakBufferedWriter.java
new file mode 100644
index 0000000..552a93f
--- /dev/null
+++ b/com/android/internal/util/LineBreakBufferedWriter.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2015 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.internal.util;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * A writer that breaks up its output into chunks before writing to its out writer,
+ * and which is linebreak aware, i.e., chunks will created along line breaks, if
+ * possible.
+ *
+ * Note: this class is not thread-safe.
+ */
+public class LineBreakBufferedWriter extends PrintWriter {
+
+    /**
+     * A buffer to collect data until the buffer size is reached.
+     *
+     * Note: we manage a char[] ourselves to avoid an allocation when printing to the
+     *       out writer. Otherwise a StringBuilder would have been simpler to use.
+     */
+    private char[] buffer;
+
+    /**
+     * The index of the first free element in the buffer.
+     */
+    private int bufferIndex;
+
+    /**
+     * The chunk size (=maximum buffer size) to use for this writer.
+     */
+    private final int bufferSize;
+
+
+    /**
+     * Index of the last newline character discovered in the buffer. The writer will try
+     * to split there.
+     */
+    private int lastNewline = -1;
+
+    /**
+     * The line separator for println().
+     */
+    private final String lineSeparator;
+
+    /**
+     * Create a new linebreak-aware buffered writer with the given output and buffer
+     * size. The initial capacity will be a default value.
+     * @param out The writer to write to.
+     * @param bufferSize The maximum buffer size.
+     */
+    public LineBreakBufferedWriter(Writer out, int bufferSize) {
+        this(out, bufferSize, 16);  // 16 is the default size of a StringBuilder buffer.
+    }
+
+    /**
+     * Create a new linebreak-aware buffered writer with the given output, buffer
+     * size and initial capacity.
+     * @param out The writer to write to.
+     * @param bufferSize The maximum buffer size.
+     * @param initialCapacity The initial capacity of the internal buffer.
+     */
+    public LineBreakBufferedWriter(Writer out, int bufferSize, int initialCapacity) {
+        super(out);
+        this.buffer = new char[Math.min(initialCapacity, bufferSize)];
+        this.bufferIndex = 0;
+        this.bufferSize = bufferSize;
+        this.lineSeparator = System.getProperty("line.separator");
+    }
+
+    /**
+     * Flush the current buffer. This will ignore line breaks.
+     */
+    @Override
+    public void flush() {
+        writeBuffer(bufferIndex);
+        bufferIndex = 0;
+        super.flush();
+    }
+
+    @Override
+    public void write(int c) {
+        if (bufferIndex < buffer.length) {
+            buffer[bufferIndex] = (char)c;
+            bufferIndex++;
+            if ((char)c == '\n') {
+                lastNewline = bufferIndex;
+            }
+        } else {
+            // This should be an uncommon case, we mostly expect char[] and String. So
+            // let the chunking be handled by the char[] case.
+            write(new char[] { (char)c }, 0 ,1);
+        }
+    }
+
+    @Override
+    public void println() {
+        write(lineSeparator);
+    }
+
+    @Override
+    public void write(char[] buf, int off, int len) {
+        while (bufferIndex + len > bufferSize) {
+            // Find the next newline in the buffer, see if that's below the limit.
+            // Repeat.
+            int nextNewLine = -1;
+            int maxLength = bufferSize - bufferIndex;
+            for (int i = 0; i < maxLength; i++) {
+                if (buf[off + i] == '\n') {
+                    if (bufferIndex + i < bufferSize) {
+                        nextNewLine = i;
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            if (nextNewLine != -1) {
+                // We can add some more data.
+                appendToBuffer(buf, off, nextNewLine);
+                writeBuffer(bufferIndex);
+                bufferIndex = 0;
+                lastNewline = -1;
+                off += nextNewLine + 1;
+                len -= nextNewLine + 1;
+            } else if (lastNewline != -1) {
+                // Use the last newline.
+                writeBuffer(lastNewline);
+                removeFromBuffer(lastNewline + 1);
+                lastNewline = -1;
+            } else {
+                // OK, there was no newline, break at a full buffer.
+                int rest = bufferSize - bufferIndex;
+                appendToBuffer(buf, off, rest);
+                writeBuffer(bufferIndex);
+                bufferIndex = 0;
+                off += rest;
+                len -= rest;
+            }
+        }
+
+        // Add to the buffer, this will fit.
+        if (len > 0) {
+            // Add the chars, find the last newline.
+            appendToBuffer(buf, off, len);
+            for (int i = len - 1; i >= 0; i--) {
+                if (buf[off + i] == '\n') {
+                    lastNewline = bufferIndex - len + i;
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void write(String s, int off, int len) {
+        while (bufferIndex + len > bufferSize) {
+            // Find the next newline in the buffer, see if that's below the limit.
+            // Repeat.
+            int nextNewLine = -1;
+            int maxLength = bufferSize - bufferIndex;
+            for (int i = 0; i < maxLength; i++) {
+                if (s.charAt(off + i) == '\n') {
+                    if (bufferIndex + i < bufferSize) {
+                        nextNewLine = i;
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            if (nextNewLine != -1) {
+                // We can add some more data.
+                appendToBuffer(s, off, nextNewLine);
+                writeBuffer(bufferIndex);
+                bufferIndex = 0;
+                lastNewline = -1;
+                off += nextNewLine + 1;
+                len -= nextNewLine + 1;
+            } else if (lastNewline != -1) {
+                // Use the last newline.
+                writeBuffer(lastNewline);
+                removeFromBuffer(lastNewline + 1);
+                lastNewline = -1;
+            } else {
+                // OK, there was no newline, break at a full buffer.
+                int rest = bufferSize - bufferIndex;
+                appendToBuffer(s, off, rest);
+                writeBuffer(bufferIndex);
+                bufferIndex = 0;
+                off += rest;
+                len -= rest;
+            }
+        }
+
+        // Add to the buffer, this will fit.
+        if (len > 0) {
+            // Add the chars, find the last newline.
+            appendToBuffer(s, off, len);
+            for (int i = len - 1; i >= 0; i--) {
+                if (s.charAt(off + i) == '\n') {
+                    lastNewline = bufferIndex - len + i;
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Append the characters to the buffer. This will potentially resize the buffer,
+     * and move the index along.
+     * @param buf The char[] containing the data.
+     * @param off The start index to copy from.
+     * @param len The number of characters to copy.
+     */
+    private void appendToBuffer(char[] buf, int off, int len) {
+        if (bufferIndex + len > buffer.length) {
+            ensureCapacity(bufferIndex + len);
+        }
+        System.arraycopy(buf, off, buffer, bufferIndex, len);
+        bufferIndex += len;
+    }
+
+    /**
+     * Append the characters from the given string to the buffer. This will potentially
+     * resize the buffer, and move the index along.
+     * @param s The string supplying the characters.
+     * @param off The start index to copy from.
+     * @param len The number of characters to copy.
+     */
+    private void appendToBuffer(String s, int off, int len) {
+        if (bufferIndex + len > buffer.length) {
+            ensureCapacity(bufferIndex + len);
+        }
+        s.getChars(off, off + len, buffer, bufferIndex);
+        bufferIndex += len;
+    }
+
+    /**
+     * Resize the buffer. We use the usual double-the-size plus constant scheme for
+     * amortized O(1) insert. Note: we expect small buffers, so this won't check for
+     * overflow.
+     * @param capacity The size to be ensured.
+     */
+    private void ensureCapacity(int capacity) {
+        int newSize = buffer.length * 2 + 2;
+        if (newSize < capacity) {
+            newSize = capacity;
+        }
+        buffer = Arrays.copyOf(buffer, newSize);
+    }
+
+    /**
+     * Remove the characters up to (and excluding) index i from the buffer. This will
+     * not resize the buffer, but will update bufferIndex.
+     * @param i The number of characters to remove from the front.
+     */
+    private void removeFromBuffer(int i) {
+        int rest = bufferIndex - i;
+        if (rest > 0) {
+            System.arraycopy(buffer, bufferIndex - rest, buffer, 0, rest);
+            bufferIndex = rest;
+        } else {
+            bufferIndex = 0;
+        }
+    }
+
+    /**
+     * Helper method, write the given part of the buffer, [start,length), to the output.
+     * @param length The number of characters to flush.
+     */
+    private void writeBuffer(int length) {
+        if (length > 0) {
+            super.write(buffer, 0, length);
+        }
+    }
+}
diff --git a/com/android/internal/util/LocalLog.java b/com/android/internal/util/LocalLog.java
new file mode 100644
index 0000000..f0e6171
--- /dev/null
+++ b/com/android/internal/util/LocalLog.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 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.internal.util;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+import android.util.Slog;
+
+/**
+ * Helper class for logging serious issues, which also keeps a small
+ * snapshot of the logged events that can be printed later, such as part
+ * of a system service's dumpsys output.
+ * @hide
+ */
+public class LocalLog {
+    private final String mTag;
+    private final int mMaxLines = 20;
+    private final ArrayList<String> mLines = new ArrayList<String>(mMaxLines);
+
+    public LocalLog(String tag) {
+        mTag = tag;
+    }
+
+    public void w(String msg) {
+        synchronized (mLines) {
+            Slog.w(mTag, msg);
+            if (mLines.size() >= mMaxLines) {
+                mLines.remove(0);
+            }
+            mLines.add(msg);
+        }
+    }
+
+    public boolean dump(PrintWriter pw, String header, String prefix) {
+        synchronized (mLines) {
+            if (mLines.size() <= 0) {
+                return false;
+            }
+            if (header != null) {
+                pw.println(header);
+            }
+            for (int i=0; i<mLines.size(); i++) {
+                if (prefix != null) {
+                    pw.print(prefix);
+                }
+                pw.println(mLines.get(i));
+            }
+            return true;
+        }
+    }
+}
diff --git a/com/android/internal/util/MemInfoReader.java b/com/android/internal/util/MemInfoReader.java
new file mode 100644
index 0000000..b71fa06
--- /dev/null
+++ b/com/android/internal/util/MemInfoReader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2011 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.internal.util;
+
+import android.os.Debug;
+import android.os.StrictMode;
+
+public final class MemInfoReader {
+    final long[] mInfos = new long[Debug.MEMINFO_COUNT];
+
+    public void readMemInfo() {
+        // Permit disk reads here, as /proc/meminfo isn't really "on
+        // disk" and should be fast.  TODO: make BlockGuard ignore
+        // /proc/ and /sys/ files perhaps?
+        StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+        try {
+            Debug.getMemInfo(mInfos);
+        } finally {
+            StrictMode.setThreadPolicy(savedPolicy);
+        }
+    }
+
+    /**
+     * Total amount of RAM available to the kernel.
+     */
+    public long getTotalSize() {
+        return mInfos[Debug.MEMINFO_TOTAL] * 1024;
+    }
+
+    /**
+     * Amount of RAM that is not being used for anything.
+     */
+    public long getFreeSize() {
+        return mInfos[Debug.MEMINFO_FREE] * 1024;
+    }
+
+    /**
+     * Amount of RAM that the kernel is being used for caches, not counting caches
+     * that are mapped in to processes.
+     */
+    public long getCachedSize() {
+        return getCachedSizeKb() * 1024;
+    }
+
+    /**
+     * Amount of RAM that is in use by the kernel for actual allocations.
+     */
+    public long getKernelUsedSize() {
+        return getKernelUsedSizeKb() * 1024;
+    }
+
+    /**
+     * Total amount of RAM available to the kernel.
+     */
+    public long getTotalSizeKb() {
+        return mInfos[Debug.MEMINFO_TOTAL];
+    }
+
+    /**
+     * Amount of RAM that is not being used for anything.
+     */
+    public long getFreeSizeKb() {
+        return mInfos[Debug.MEMINFO_FREE];
+    }
+
+    /**
+     * Amount of RAM that the kernel is being used for caches, not counting caches
+     * that are mapped in to processes.
+     */
+    public long getCachedSizeKb() {
+        return mInfos[Debug.MEMINFO_BUFFERS]
+                + mInfos[Debug.MEMINFO_CACHED] - mInfos[Debug.MEMINFO_MAPPED];
+    }
+
+    /**
+     * Amount of RAM that is in use by the kernel for actual allocations.
+     */
+    public long getKernelUsedSizeKb() {
+        return mInfos[Debug.MEMINFO_SHMEM] + mInfos[Debug.MEMINFO_SLAB]
+                + mInfos[Debug.MEMINFO_VM_ALLOC_USED] + mInfos[Debug.MEMINFO_PAGE_TABLES]
+                + mInfos[Debug.MEMINFO_KERNEL_STACK];
+    }
+
+    public long getSwapTotalSizeKb() {
+        return mInfos[Debug.MEMINFO_SWAP_TOTAL];
+    }
+
+    public long getSwapFreeSizeKb() {
+        return mInfos[Debug.MEMINFO_SWAP_FREE];
+    }
+
+    public long getZramTotalSizeKb() {
+        return mInfos[Debug.MEMINFO_ZRAM_TOTAL];
+    }
+
+    public long[] getRawInfo() {
+        return mInfos;
+    }
+}
diff --git a/com/android/internal/util/MessageUtils.java b/com/android/internal/util/MessageUtils.java
new file mode 100644
index 0000000..e733c30
--- /dev/null
+++ b/com/android/internal/util/MessageUtils.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import android.os.Message;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+/**
+ * Static utility class for dealing with {@link Message} objects.
+ */
+public class MessageUtils {
+
+    private static final String TAG = MessageUtils.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    /** Thrown when two different constants have the same value. */
+    public static class DuplicateConstantError extends Error {
+        private DuplicateConstantError() {}
+        public DuplicateConstantError(String name1, String name2, int value) {
+            super(String.format("Duplicate constant value: both %s and %s = %d",
+                name1, name2, value));
+        }
+    }
+
+    /**
+     * Finds the names of integer constants. Searches the specified {@code classes}, looking for
+     * accessible static integer fields whose names begin with one of the specified
+     * {@code prefixes}.
+     *
+     * @param classes the classes to examine.
+     * @param prefixes only consider fields names starting with one of these prefixes.
+     * @return a {@link SparseArray} mapping integer constants to their names.
+     */
+    public static SparseArray<String> findMessageNames(Class[] classes, String[] prefixes) {
+        SparseArray<String> messageNames = new SparseArray<>();
+        for (Class c : classes) {
+            String className = c.getName();
+            if (DBG) Log.d(TAG, "Examining class " + className);
+
+            Field[] fields;
+            try {
+                fields = c.getDeclaredFields();
+            } catch (SecurityException e) {
+                Log.e(TAG, "Can't list fields of class " + className);
+                continue;
+            }
+
+            for (Field field : fields) {
+                int modifiers = field.getModifiers();
+                if (!Modifier.isStatic(modifiers) | !Modifier.isFinal(modifiers)) {
+                    continue;
+                }
+
+                String name = field.getName();
+                for (String prefix : prefixes) {
+                    // Does this look like a constant?
+                    if (!name.startsWith(prefix)) {
+                        continue;
+                    }
+
+                    try {
+                        // TODO: can we have the caller try to access the field instead, so we don't
+                        // expose constants it does not have access to?
+                        field.setAccessible(true);
+
+                        // Fetch the constant's value.
+                        int value;
+                        try {
+                            value = field.getInt(null);
+                        } catch (IllegalArgumentException | ExceptionInInitializerError e) {
+                            // The field is not an integer (or short or byte), or c's static
+                            // initializer failed and we have no idea what its value is.
+                            // Either way, give up on this field.
+                            break;
+                        }
+
+                        // Check for duplicate values.
+                        String previousName = messageNames.get(value);
+                        if (previousName != null && !previousName.equals(name)) {
+                            throw new DuplicateConstantError(name, previousName, value);
+                        }
+
+                        messageNames.put(value, name);
+                        if (DBG) {
+                            Log.d(TAG, String.format("Found constant: %s.%s = %d",
+                                    className, name, value));
+                        }
+                    } catch (SecurityException | IllegalAccessException e) {
+                        // Not allowed to make the field accessible, or no access. Ignore.
+                        continue;
+                    }
+                }
+            }
+        }
+        return messageNames;
+    }
+
+    /**
+     * Default prefixes for constants.
+     */
+    public static final String[] DEFAULT_PREFIXES = {"CMD_", "EVENT_"};
+
+    /**
+     * Finds the names of integer constants. Searches the specified {@code classes}, looking for
+     * accessible static integer values whose names begin with {@link #DEFAULT_PREFIXES}.
+     *
+     * @param classNames the classes to examine.
+     * @return a {@link SparseArray} mapping integer constants to their names.
+     */
+    public static SparseArray<String> findMessageNames(Class[] classNames) {
+        return findMessageNames(classNames, DEFAULT_PREFIXES);
+    }
+}
+
diff --git a/com/android/internal/util/MimeIconUtils.java b/com/android/internal/util/MimeIconUtils.java
new file mode 100644
index 0000000..841ec7c
--- /dev/null
+++ b/com/android/internal/util/MimeIconUtils.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.provider.DocumentsContract;
+
+import com.android.internal.R;
+
+import java.util.HashMap;
+
+public class MimeIconUtils {
+
+    private static HashMap<String, Integer> sMimeIcons = new HashMap<>();
+
+    private static void add(String mimeType, int resId) {
+        if (sMimeIcons.put(mimeType, resId) != null) {
+            throw new RuntimeException(mimeType + " already registered!");
+        }
+    }
+
+    static {
+        int icon;
+
+        // Package
+        icon = R.drawable.ic_doc_apk;
+        add("application/vnd.android.package-archive", icon);
+
+        // Audio
+        icon = R.drawable.ic_doc_audio;
+        add("application/ogg", icon);
+        add("application/x-flac", icon);
+
+        // Certificate
+        icon = R.drawable.ic_doc_certificate;
+        add("application/pgp-keys", icon);
+        add("application/pgp-signature", icon);
+        add("application/x-pkcs12", icon);
+        add("application/x-pkcs7-certreqresp", icon);
+        add("application/x-pkcs7-crl", icon);
+        add("application/x-x509-ca-cert", icon);
+        add("application/x-x509-user-cert", icon);
+        add("application/x-pkcs7-certificates", icon);
+        add("application/x-pkcs7-mime", icon);
+        add("application/x-pkcs7-signature", icon);
+
+        // Source code
+        icon = R.drawable.ic_doc_codes;
+        add("application/rdf+xml", icon);
+        add("application/rss+xml", icon);
+        add("application/x-object", icon);
+        add("application/xhtml+xml", icon);
+        add("text/css", icon);
+        add("text/html", icon);
+        add("text/xml", icon);
+        add("text/x-c++hdr", icon);
+        add("text/x-c++src", icon);
+        add("text/x-chdr", icon);
+        add("text/x-csrc", icon);
+        add("text/x-dsrc", icon);
+        add("text/x-csh", icon);
+        add("text/x-haskell", icon);
+        add("text/x-java", icon);
+        add("text/x-literate-haskell", icon);
+        add("text/x-pascal", icon);
+        add("text/x-tcl", icon);
+        add("text/x-tex", icon);
+        add("application/x-latex", icon);
+        add("application/x-texinfo", icon);
+        add("application/atom+xml", icon);
+        add("application/ecmascript", icon);
+        add("application/json", icon);
+        add("application/javascript", icon);
+        add("application/xml", icon);
+        add("text/javascript", icon);
+        add("application/x-javascript", icon);
+
+        // Compressed
+        icon = R.drawable.ic_doc_compressed;
+        add("application/mac-binhex40", icon);
+        add("application/rar", icon);
+        add("application/zip", icon);
+        add("application/x-apple-diskimage", icon);
+        add("application/x-debian-package", icon);
+        add("application/x-gtar", icon);
+        add("application/x-iso9660-image", icon);
+        add("application/x-lha", icon);
+        add("application/x-lzh", icon);
+        add("application/x-lzx", icon);
+        add("application/x-stuffit", icon);
+        add("application/x-tar", icon);
+        add("application/x-webarchive", icon);
+        add("application/x-webarchive-xml", icon);
+        add("application/gzip", icon);
+        add("application/x-7z-compressed", icon);
+        add("application/x-deb", icon);
+        add("application/x-rar-compressed", icon);
+
+        // Contact
+        icon = R.drawable.ic_doc_contact;
+        add("text/x-vcard", icon);
+        add("text/vcard", icon);
+
+        // Event
+        icon = R.drawable.ic_doc_event;
+        add("text/calendar", icon);
+        add("text/x-vcalendar", icon);
+
+        // Font
+        icon = R.drawable.ic_doc_font;
+        add("application/x-font", icon);
+        add("application/font-woff", icon);
+        add("application/x-font-woff", icon);
+        add("application/x-font-ttf", icon);
+
+        // Image
+        icon = R.drawable.ic_doc_image;
+        add("application/vnd.oasis.opendocument.graphics", icon);
+        add("application/vnd.oasis.opendocument.graphics-template", icon);
+        add("application/vnd.oasis.opendocument.image", icon);
+        add("application/vnd.stardivision.draw", icon);
+        add("application/vnd.sun.xml.draw", icon);
+        add("application/vnd.sun.xml.draw.template", icon);
+
+        // PDF
+        icon = R.drawable.ic_doc_pdf;
+        add("application/pdf", icon);
+
+        // Presentation
+        icon = R.drawable.ic_doc_presentation;
+        add("application/vnd.stardivision.impress", icon);
+        add("application/vnd.sun.xml.impress", icon);
+        add("application/vnd.sun.xml.impress.template", icon);
+        add("application/x-kpresenter", icon);
+        add("application/vnd.oasis.opendocument.presentation", icon);
+
+        // Spreadsheet
+        icon = R.drawable.ic_doc_spreadsheet;
+        add("application/vnd.oasis.opendocument.spreadsheet", icon);
+        add("application/vnd.oasis.opendocument.spreadsheet-template", icon);
+        add("application/vnd.stardivision.calc", icon);
+        add("application/vnd.sun.xml.calc", icon);
+        add("application/vnd.sun.xml.calc.template", icon);
+        add("application/x-kspread", icon);
+
+        // Document
+        icon = R.drawable.ic_doc_document;
+        add("application/vnd.oasis.opendocument.text", icon);
+        add("application/vnd.oasis.opendocument.text-master", icon);
+        add("application/vnd.oasis.opendocument.text-template", icon);
+        add("application/vnd.oasis.opendocument.text-web", icon);
+        add("application/vnd.stardivision.writer", icon);
+        add("application/vnd.stardivision.writer-global", icon);
+        add("application/vnd.sun.xml.writer", icon);
+        add("application/vnd.sun.xml.writer.global", icon);
+        add("application/vnd.sun.xml.writer.template", icon);
+        add("application/x-abiword", icon);
+        add("application/x-kword", icon);
+
+        // Video
+        icon = R.drawable.ic_doc_video;
+        add("application/x-quicktimeplayer", icon);
+        add("application/x-shockwave-flash", icon);
+
+        // Word
+        icon = R.drawable.ic_doc_word;
+        add("application/msword", icon);
+        add("application/vnd.openxmlformats-officedocument.wordprocessingml.document", icon);
+        add("application/vnd.openxmlformats-officedocument.wordprocessingml.template", icon);
+
+        // Excel
+        icon = R.drawable.ic_doc_excel;
+        add("application/vnd.ms-excel", icon);
+        add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", icon);
+        add("application/vnd.openxmlformats-officedocument.spreadsheetml.template", icon);
+
+        // Powerpoint
+        icon = R.drawable.ic_doc_powerpoint;
+        add("application/vnd.ms-powerpoint", icon);
+        add("application/vnd.openxmlformats-officedocument.presentationml.presentation", icon);
+        add("application/vnd.openxmlformats-officedocument.presentationml.template", icon);
+        add("application/vnd.openxmlformats-officedocument.presentationml.slideshow", icon);
+    }
+
+    public static Drawable loadMimeIcon(Context context, String mimeType) {
+        if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
+            return context.getDrawable(R.drawable.ic_doc_folder);
+        }
+
+        // Look for exact match first
+        Integer resId = sMimeIcons.get(mimeType);
+        if (resId != null) {
+            return context.getDrawable(resId);
+        }
+
+        if (mimeType == null) {
+            // TODO: generic icon?
+            return null;
+        }
+
+        // Otherwise look for partial match
+        final String typeOnly = mimeType.split("/")[0];
+        if ("audio".equals(typeOnly)) {
+            return context.getDrawable(R.drawable.ic_doc_audio);
+        } else if ("image".equals(typeOnly)) {
+            return context.getDrawable(R.drawable.ic_doc_image);
+        } else if ("text".equals(typeOnly)) {
+            return context.getDrawable(R.drawable.ic_doc_text);
+        } else if ("video".equals(typeOnly)) {
+            return context.getDrawable(R.drawable.ic_doc_video);
+        } else {
+            return context.getDrawable(R.drawable.ic_doc_generic);
+        }
+    }
+}
diff --git a/com/android/internal/util/NotificationColorUtil.java b/com/android/internal/util/NotificationColorUtil.java
new file mode 100644
index 0000000..933cc7a
--- /dev/null
+++ b/com/android/internal/util/NotificationColorUtil.java
@@ -0,0 +1,1056 @@
+/*
+ * Copyright (C) 2014 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.internal.util;
+
+import android.annotation.ColorInt;
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.VectorDrawable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.TextAppearanceSpan;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.Arrays;
+import java.util.WeakHashMap;
+
+/**
+ * Helper class to process legacy (Holo) notifications to make them look like material notifications.
+ *
+ * @hide
+ */
+public class NotificationColorUtil {
+
+    private static final String TAG = "NotificationColorUtil";
+    private static final boolean DEBUG = false;
+
+    private static final Object sLock = new Object();
+    private static NotificationColorUtil sInstance;
+
+    private final ImageUtils mImageUtils = new ImageUtils();
+    private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
+            new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
+
+    private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
+
+    public static NotificationColorUtil getInstance(Context context) {
+        synchronized (sLock) {
+            if (sInstance == null) {
+                sInstance = new NotificationColorUtil(context);
+            }
+            return sInstance;
+        }
+    }
+
+    private NotificationColorUtil(Context context) {
+        mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_large_icon_width);
+    }
+
+    /**
+     * Checks whether a Bitmap is a small grayscale icon.
+     * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
+     *
+     * @param bitmap The bitmap to test.
+     * @return True if the bitmap is grayscale; false if it is color or too large to examine.
+     */
+    public boolean isGrayscaleIcon(Bitmap bitmap) {
+        // quick test: reject large bitmaps
+        if (bitmap.getWidth() > mGrayscaleIconMaxSize
+                || bitmap.getHeight() > mGrayscaleIconMaxSize) {
+            return false;
+        }
+
+        synchronized (sLock) {
+            Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
+            if (cached != null) {
+                if (cached.second == bitmap.getGenerationId()) {
+                    return cached.first;
+                }
+            }
+        }
+        boolean result;
+        int generationId;
+        synchronized (mImageUtils) {
+            result = mImageUtils.isGrayscale(bitmap);
+
+            // generationId and the check whether the Bitmap is grayscale can't be read atomically
+            // here. However, since the thread is in the process of posting the notification, we can
+            // assume that it doesn't modify the bitmap while we are checking the pixels.
+            generationId = bitmap.getGenerationId();
+        }
+        synchronized (sLock) {
+            mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
+        }
+        return result;
+    }
+
+    /**
+     * Checks whether a Drawable is a small grayscale icon.
+     * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
+     *
+     * @param d The drawable to test.
+     * @return True if the bitmap is grayscale; false if it is color or too large to examine.
+     */
+    public boolean isGrayscaleIcon(Drawable d) {
+        if (d == null) {
+            return false;
+        } else if (d instanceof BitmapDrawable) {
+            BitmapDrawable bd = (BitmapDrawable) d;
+            return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
+        } else if (d instanceof AnimationDrawable) {
+            AnimationDrawable ad = (AnimationDrawable) d;
+            int count = ad.getNumberOfFrames();
+            return count > 0 && isGrayscaleIcon(ad.getFrame(0));
+        } else if (d instanceof VectorDrawable) {
+            // We just assume you're doing the right thing if using vectors
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public boolean isGrayscaleIcon(Context context, Icon icon) {
+        if (icon == null) {
+            return false;
+        }
+        switch (icon.getType()) {
+            case Icon.TYPE_BITMAP:
+                return isGrayscaleIcon(icon.getBitmap());
+            case Icon.TYPE_RESOURCE:
+                return isGrayscaleIcon(context, icon.getResId());
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks whether a drawable with a resoure id is a small grayscale icon.
+     * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
+     *
+     * @param context The context to load the drawable from.
+     * @return True if the bitmap is grayscale; false if it is color or too large to examine.
+     */
+    public boolean isGrayscaleIcon(Context context, int drawableResId) {
+        if (drawableResId != 0) {
+            try {
+                return isGrayscaleIcon(context.getDrawable(drawableResId));
+            } catch (Resources.NotFoundException ex) {
+                Log.e(TAG, "Drawable not found: " + drawableResId);
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
+     * the text.
+     *
+     * @param charSequence The text to process.
+     * @return The color inverted text.
+     */
+    public CharSequence invertCharSequenceColors(CharSequence charSequence) {
+        if (charSequence instanceof Spanned) {
+            Spanned ss = (Spanned) charSequence;
+            Object[] spans = ss.getSpans(0, ss.length(), Object.class);
+            SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
+            for (Object span : spans) {
+                Object resultSpan = span;
+                if (resultSpan instanceof CharacterStyle) {
+                    resultSpan = ((CharacterStyle) span).getUnderlying();
+                }
+                if (resultSpan instanceof TextAppearanceSpan) {
+                    TextAppearanceSpan processedSpan = processTextAppearanceSpan(
+                            (TextAppearanceSpan) span);
+                    if (processedSpan != resultSpan) {
+                        resultSpan = processedSpan;
+                    } else {
+                        // we need to still take the orgininal for wrapped spans
+                        resultSpan = span;
+                    }
+                } else if (resultSpan instanceof ForegroundColorSpan) {
+                    ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
+                    int foregroundColor = originalSpan.getForegroundColor();
+                    resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
+                } else {
+                    resultSpan = span;
+                }
+                builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
+                        ss.getSpanFlags(span));
+            }
+            return builder;
+        }
+        return charSequence;
+    }
+
+    private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
+        ColorStateList colorStateList = span.getTextColor();
+        if (colorStateList != null) {
+            int[] colors = colorStateList.getColors();
+            boolean changed = false;
+            for (int i = 0; i < colors.length; i++) {
+                if (ImageUtils.isGrayscale(colors[i])) {
+
+                    // Allocate a new array so we don't change the colors in the old color state
+                    // list.
+                    if (!changed) {
+                        colors = Arrays.copyOf(colors, colors.length);
+                    }
+                    colors[i] = processColor(colors[i]);
+                    changed = true;
+                }
+            }
+            if (changed) {
+                return new TextAppearanceSpan(
+                        span.getFamily(), span.getTextStyle(), span.getTextSize(),
+                        new ColorStateList(colorStateList.getStates(), colors),
+                        span.getLinkTextColor());
+            }
+        }
+        return span;
+    }
+
+    /**
+     * Clears all color spans of a text
+     * @param charSequence the input text
+     * @return the same text but without color spans
+     */
+    public static CharSequence clearColorSpans(CharSequence charSequence) {
+        if (charSequence instanceof Spanned) {
+            Spanned ss = (Spanned) charSequence;
+            Object[] spans = ss.getSpans(0, ss.length(), Object.class);
+            SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
+            for (Object span : spans) {
+                Object resultSpan = span;
+                if (resultSpan instanceof CharacterStyle) {
+                    resultSpan = ((CharacterStyle) span).getUnderlying();
+                }
+                if (resultSpan instanceof TextAppearanceSpan) {
+                    TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
+                    if (originalSpan.getTextColor() != null) {
+                        resultSpan = new TextAppearanceSpan(
+                                originalSpan.getFamily(),
+                                originalSpan.getTextStyle(),
+                                originalSpan.getTextSize(),
+                                null,
+                                originalSpan.getLinkTextColor());
+                    }
+                } else if (resultSpan instanceof ForegroundColorSpan
+                        || (resultSpan instanceof BackgroundColorSpan)) {
+                    continue;
+                } else {
+                    resultSpan = span;
+                }
+                builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
+                        ss.getSpanFlags(span));
+            }
+            return builder;
+        }
+        return charSequence;
+    }
+
+    private int processColor(int color) {
+        return Color.argb(Color.alpha(color),
+                255 - Color.red(color),
+                255 - Color.green(color),
+                255 - Color.blue(color));
+    }
+
+    /**
+     * Finds a suitable color such that there's enough contrast.
+     *
+     * @param color the color to start searching from.
+     * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
+     * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
+     * @param minRatio the minimum contrast ratio required.
+     * @return a color with the same hue as {@param color}, potentially darkened to meet the
+     *          contrast ratio.
+     */
+    public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
+        int fg = findFg ? color : other;
+        int bg = findFg ? other : color;
+        if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
+            return color;
+        }
+
+        double[] lab = new double[3];
+        ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
+
+        double low = 0, high = lab[0];
+        final double a = lab[1], b = lab[2];
+        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
+            final double l = (low + high) / 2;
+            if (findFg) {
+                fg = ColorUtilsFromCompat.LABToColor(l, a, b);
+            } else {
+                bg = ColorUtilsFromCompat.LABToColor(l, a, b);
+            }
+            if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
+                low = l;
+            } else {
+                high = l;
+            }
+        }
+        return ColorUtilsFromCompat.LABToColor(low, a, b);
+    }
+
+    /**
+     * Finds a suitable alpha such that there's enough contrast.
+     *
+     * @param color the color to start searching from.
+     * @param backgroundColor the color to ensure contrast against.
+     * @param minRatio the minimum contrast ratio required.
+     * @return the same color as {@param color} with potentially modified alpha to meet contrast
+     */
+    public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
+        int fg = color;
+        int bg = backgroundColor;
+        if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
+            return color;
+        }
+        int startAlpha = Color.alpha(color);
+        int r = Color.red(color);
+        int g = Color.green(color);
+        int b = Color.blue(color);
+
+        int low = startAlpha, high = 255;
+        for (int i = 0; i < 15 && high - low > 0; i++) {
+            final int alpha = (low + high) / 2;
+            fg = Color.argb(alpha, r, g, b);
+            if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
+                high = alpha;
+            } else {
+                low = alpha;
+            }
+        }
+        return Color.argb(high, r, g, b);
+    }
+
+    /**
+     * Finds a suitable color such that there's enough contrast.
+     *
+     * @param color the color to start searching from.
+     * @param other the color to ensure contrast against. Assumed to be darker than {@param color}
+     * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
+     * @param minRatio the minimum contrast ratio required.
+     * @return a color with the same hue as {@param color}, potentially darkened to meet the
+     *          contrast ratio.
+     */
+    public static int findContrastColorAgainstDark(int color, int other, boolean findFg,
+            double minRatio) {
+        int fg = findFg ? color : other;
+        int bg = findFg ? other : color;
+        if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
+            return color;
+        }
+
+        float[] hsl = new float[3];
+        ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
+
+        float low = hsl[2], high = 1;
+        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
+            final float l = (low + high) / 2;
+            hsl[2] = l;
+            if (findFg) {
+                fg = ColorUtilsFromCompat.HSLToColor(hsl);
+            } else {
+                bg = ColorUtilsFromCompat.HSLToColor(hsl);
+            }
+            if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
+                high = l;
+            } else {
+                low = l;
+            }
+        }
+        return findFg ? fg : bg;
+    }
+
+    public static int ensureTextContrastOnBlack(int color) {
+        return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
+    }
+
+     /**
+     * Finds a large text color with sufficient contrast over bg that has the same or darker hue as
+     * the original color, depending on the value of {@code isBgDarker}.
+     *
+     * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
+     */
+    public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) {
+        return isBgDarker
+                ? findContrastColorAgainstDark(color, bg, true, 3)
+                : findContrastColor(color, bg, true, 3);
+    }
+
+    /**
+     * Finds a text color with sufficient contrast over bg that has the same or darker hue as the
+     * original color, depending on the value of {@code isBgDarker}.
+     *
+     * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
+     */
+    private static int ensureTextContrast(int color, int bg, boolean isBgDarker) {
+        return isBgDarker
+                ? findContrastColorAgainstDark(color, bg, true, 4.5)
+                : findContrastColor(color, bg, true, 4.5);
+    }
+
+    /** Finds a background color for a text view with given text color and hint text color, that
+     * has the same hue as the original color.
+     */
+    public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
+        color = findContrastColor(color, hintColor, false, 3.0);
+        return findContrastColor(color, textColor, false, 4.5);
+    }
+
+    private static String contrastChange(int colorOld, int colorNew, int bg) {
+        return String.format("from %.2f:1 to %.2f:1",
+                ColorUtilsFromCompat.calculateContrast(colorOld, bg),
+                ColorUtilsFromCompat.calculateContrast(colorNew, bg));
+    }
+
+    /**
+     * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
+     */
+    public static int resolveColor(Context context, int color) {
+        if (color == Notification.COLOR_DEFAULT) {
+            return context.getColor(com.android.internal.R.color.notification_icon_default_color);
+        }
+        return color;
+    }
+
+    /**
+     * Resolves a Notification's color such that it has enough contrast to be used as the
+     * color for the Notification's action and header text on a background that is lighter than
+     * {@code notificationColor}.
+     *
+     * @see {@link #resolveContrastColor(Context, int, boolean)}
+     */
+    public static int resolveContrastColor(Context context, int notificationColor,
+            int backgroundColor) {
+        return NotificationColorUtil.resolveContrastColor(context, notificationColor,
+                backgroundColor, false /* isDark */);
+    }
+
+    /**
+     * Resolves a Notification's color such that it has enough contrast to be used as the
+     * color for the Notification's action and header text.
+     *
+     * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
+     * @param backgroundColor the background color to ensure the contrast against.
+     * @param isDark whether or not the {@code notificationColor} will be placed on a background
+     *               that is darker than the color itself
+     * @return a color of the same hue with enough contrast against the backgrounds.
+     */
+    public static int resolveContrastColor(Context context, int notificationColor,
+            int backgroundColor, boolean isDark) {
+        final int resolvedColor = resolveColor(context, notificationColor);
+
+        final int actionBg = context.getColor(
+                com.android.internal.R.color.notification_action_list);
+
+        int color = resolvedColor;
+        color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark);
+        color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark);
+
+        if (color != resolvedColor) {
+            if (DEBUG){
+                Log.w(TAG, String.format(
+                        "Enhanced contrast of notification for %s %s (over action)"
+                                + " and %s (over background) by changing #%s to %s",
+                        context.getPackageName(),
+                        NotificationColorUtil.contrastChange(resolvedColor, color, actionBg),
+                        NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor),
+                        Integer.toHexString(resolvedColor), Integer.toHexString(color)));
+            }
+        }
+        return color;
+    }
+
+    /**
+     * Change a color by a specified value
+     * @param baseColor the base color to lighten
+     * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
+     *               increase in the LAB color space. A negative value will darken the color and
+     *               a positive will lighten it.
+     * @return the changed color
+     */
+    public static int changeColorLightness(int baseColor, int amount) {
+        final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
+        ColorUtilsFromCompat.colorToLAB(baseColor, result);
+        result[0] = Math.max(Math.min(100, result[0] + amount), 0);
+        return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
+    }
+
+    public static int resolveAmbientColor(Context context, int notificationColor) {
+        final int resolvedColor = resolveColor(context, notificationColor);
+
+        int color = resolvedColor;
+        color = NotificationColorUtil.ensureTextContrastOnBlack(color);
+
+        if (color != resolvedColor) {
+            if (DEBUG){
+                Log.w(TAG, String.format(
+                        "Ambient contrast of notification for %s is %s (over black)"
+                                + " by changing #%s to #%s",
+                        context.getPackageName(),
+                        NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
+                        Integer.toHexString(resolvedColor), Integer.toHexString(color)));
+            }
+        }
+        return color;
+    }
+
+    public static int resolvePrimaryColor(Context context, int backgroundColor) {
+        boolean useDark = shouldUseDark(backgroundColor);
+        if (useDark) {
+            return context.getColor(
+                    com.android.internal.R.color.notification_primary_text_color_light);
+        } else {
+            return context.getColor(
+                    com.android.internal.R.color.notification_primary_text_color_dark);
+        }
+    }
+
+    public static int resolveSecondaryColor(Context context, int backgroundColor) {
+        boolean useDark = shouldUseDark(backgroundColor);
+        if (useDark) {
+            return context.getColor(
+                    com.android.internal.R.color.notification_secondary_text_color_light);
+        } else {
+            return context.getColor(
+                    com.android.internal.R.color.notification_secondary_text_color_dark);
+        }
+    }
+
+    public static int resolveActionBarColor(Context context, int backgroundColor) {
+        if (backgroundColor == Notification.COLOR_DEFAULT) {
+            return context.getColor(com.android.internal.R.color.notification_action_list);
+        }
+        return getShiftedColor(backgroundColor, 7);
+    }
+
+    /**
+     * Get a color that stays in the same tint, but darkens or lightens it by a certain
+     * amount.
+     * This also looks at the lightness of the provided color and shifts it appropriately.
+     *
+     * @param color the base color to use
+     * @param amount the amount from 1 to 100 how much to modify the color
+     * @return the now color that was modified
+     */
+    public static int getShiftedColor(int color, int amount) {
+        final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
+        ColorUtilsFromCompat.colorToLAB(color, result);
+        if (result[0] >= 4) {
+            result[0] = Math.max(0, result[0] - amount);
+        } else {
+            result[0] = Math.min(100, result[0] + amount);
+        }
+        return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
+    }
+
+    private static boolean shouldUseDark(int backgroundColor) {
+        boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
+        if (!useDark) {
+            useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
+        }
+        return useDark;
+    }
+
+    public static double calculateLuminance(int backgroundColor) {
+        return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
+    }
+
+
+    public static double calculateContrast(int foregroundColor, int backgroundColor) {
+        return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
+    }
+
+    public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
+        return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
+    }
+
+    /**
+     * Composite two potentially translucent colors over each other and returns the result.
+     */
+    public static int compositeColors(int foreground, int background) {
+        return ColorUtilsFromCompat.compositeColors(foreground, background);
+    }
+
+    public static boolean isColorLight(int backgroundColor) {
+        return calculateLuminance(backgroundColor) > 0.5f;
+    }
+
+    /**
+     * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
+     */
+    private static class ColorUtilsFromCompat {
+        private static final double XYZ_WHITE_REFERENCE_X = 95.047;
+        private static final double XYZ_WHITE_REFERENCE_Y = 100;
+        private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
+        private static final double XYZ_EPSILON = 0.008856;
+        private static final double XYZ_KAPPA = 903.3;
+
+        private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
+        private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
+
+        private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
+
+        private ColorUtilsFromCompat() {}
+
+        /**
+         * Composite two potentially translucent colors over each other and returns the result.
+         */
+        public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
+            int bgAlpha = Color.alpha(background);
+            int fgAlpha = Color.alpha(foreground);
+            int a = compositeAlpha(fgAlpha, bgAlpha);
+
+            int r = compositeComponent(Color.red(foreground), fgAlpha,
+                    Color.red(background), bgAlpha, a);
+            int g = compositeComponent(Color.green(foreground), fgAlpha,
+                    Color.green(background), bgAlpha, a);
+            int b = compositeComponent(Color.blue(foreground), fgAlpha,
+                    Color.blue(background), bgAlpha, a);
+
+            return Color.argb(a, r, g, b);
+        }
+
+        private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
+            return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
+        }
+
+        private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
+            if (a == 0) return 0;
+            return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
+        }
+
+        /**
+         * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
+         * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
+         */
+        @FloatRange(from = 0.0, to = 1.0)
+        public static double calculateLuminance(@ColorInt int color) {
+            final double[] result = getTempDouble3Array();
+            colorToXYZ(color, result);
+            // Luminance is the Y component
+            return result[1] / 100;
+        }
+
+        /**
+         * Returns the contrast ratio between {@code foreground} and {@code background}.
+         * {@code background} must be opaque.
+         * <p>
+         * Formula defined
+         * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
+         */
+        public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
+            if (Color.alpha(background) != 255) {
+                Log.wtf(TAG, "background can not be translucent: #"
+                        + Integer.toHexString(background));
+            }
+            if (Color.alpha(foreground) < 255) {
+                // If the foreground is translucent, composite the foreground over the background
+                foreground = compositeColors(foreground, background);
+            }
+
+            final double luminance1 = calculateLuminance(foreground) + 0.05;
+            final double luminance2 = calculateLuminance(background) + 0.05;
+
+            // Now return the lighter luminance divided by the darker luminance
+            return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
+        }
+
+        /**
+         * Convert the ARGB color to its CIE Lab representative components.
+         *
+         * @param color  the ARGB color to convert. The alpha component is ignored
+         * @param outLab 3-element array which holds the resulting LAB components
+         */
+        public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
+            RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
+        }
+
+        /**
+         * Convert RGB components to its CIE Lab representative components.
+         *
+         * <ul>
+         * <li>outLab[0] is L [0 ...100)</li>
+         * <li>outLab[1] is a [-128...127)</li>
+         * <li>outLab[2] is b [-128...127)</li>
+         * </ul>
+         *
+         * @param r      red component value [0..255]
+         * @param g      green component value [0..255]
+         * @param b      blue component value [0..255]
+         * @param outLab 3-element array which holds the resulting LAB components
+         */
+        public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
+                @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+                @NonNull double[] outLab) {
+            // First we convert RGB to XYZ
+            RGBToXYZ(r, g, b, outLab);
+            // outLab now contains XYZ
+            XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
+            // outLab now contains LAB representation
+        }
+
+        /**
+         * Convert the ARGB color to it's CIE XYZ representative components.
+         *
+         * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+         * 2° Standard Observer (1931).</p>
+         *
+         * <ul>
+         * <li>outXyz[0] is X [0 ...95.047)</li>
+         * <li>outXyz[1] is Y [0...100)</li>
+         * <li>outXyz[2] is Z [0...108.883)</li>
+         * </ul>
+         *
+         * @param color  the ARGB color to convert. The alpha component is ignored
+         * @param outXyz 3-element array which holds the resulting LAB components
+         */
+        public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
+            RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
+        }
+
+        /**
+         * Convert RGB components to it's CIE XYZ representative components.
+         *
+         * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+         * 2° Standard Observer (1931).</p>
+         *
+         * <ul>
+         * <li>outXyz[0] is X [0 ...95.047)</li>
+         * <li>outXyz[1] is Y [0...100)</li>
+         * <li>outXyz[2] is Z [0...108.883)</li>
+         * </ul>
+         *
+         * @param r      red component value [0..255]
+         * @param g      green component value [0..255]
+         * @param b      blue component value [0..255]
+         * @param outXyz 3-element array which holds the resulting XYZ components
+         */
+        public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
+                @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+                @NonNull double[] outXyz) {
+            if (outXyz.length != 3) {
+                throw new IllegalArgumentException("outXyz must have a length of 3.");
+            }
+
+            double sr = r / 255.0;
+            sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
+            double sg = g / 255.0;
+            sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
+            double sb = b / 255.0;
+            sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
+
+            outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
+            outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
+            outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
+        }
+
+        /**
+         * Converts a color from CIE XYZ to CIE Lab representation.
+         *
+         * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
+         * 2° Standard Observer (1931).</p>
+         *
+         * <ul>
+         * <li>outLab[0] is L [0 ...100)</li>
+         * <li>outLab[1] is a [-128...127)</li>
+         * <li>outLab[2] is b [-128...127)</li>
+         * </ul>
+         *
+         * @param x      X component value [0...95.047)
+         * @param y      Y component value [0...100)
+         * @param z      Z component value [0...108.883)
+         * @param outLab 3-element array which holds the resulting Lab components
+         */
+        public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
+                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
+                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
+                @NonNull double[] outLab) {
+            if (outLab.length != 3) {
+                throw new IllegalArgumentException("outLab must have a length of 3.");
+            }
+            x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
+            y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
+            z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
+            outLab[0] = Math.max(0, 116 * y - 16);
+            outLab[1] = 500 * (x - y);
+            outLab[2] = 200 * (y - z);
+        }
+
+        /**
+         * Converts a color from CIE Lab to CIE XYZ representation.
+         *
+         * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+         * 2° Standard Observer (1931).</p>
+         *
+         * <ul>
+         * <li>outXyz[0] is X [0 ...95.047)</li>
+         * <li>outXyz[1] is Y [0...100)</li>
+         * <li>outXyz[2] is Z [0...108.883)</li>
+         * </ul>
+         *
+         * @param l      L component value [0...100)
+         * @param a      A component value [-128...127)
+         * @param b      B component value [-128...127)
+         * @param outXyz 3-element array which holds the resulting XYZ components
+         */
+        public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
+                @FloatRange(from = -128, to = 127) final double a,
+                @FloatRange(from = -128, to = 127) final double b,
+                @NonNull double[] outXyz) {
+            final double fy = (l + 16) / 116;
+            final double fx = a / 500 + fy;
+            final double fz = fy - b / 200;
+
+            double tmp = Math.pow(fx, 3);
+            final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
+            final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
+
+            tmp = Math.pow(fz, 3);
+            final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
+
+            outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
+            outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
+            outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
+        }
+
+        /**
+         * Converts a color from CIE XYZ to its RGB representation.
+         *
+         * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
+         * 2° Standard Observer (1931).</p>
+         *
+         * @param x X component value [0...95.047)
+         * @param y Y component value [0...100)
+         * @param z Z component value [0...108.883)
+         * @return int containing the RGB representation
+         */
+        @ColorInt
+        public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
+                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
+                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
+            double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
+            double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
+            double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
+
+            r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
+            g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
+            b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
+
+            return Color.rgb(
+                    constrain((int) Math.round(r * 255), 0, 255),
+                    constrain((int) Math.round(g * 255), 0, 255),
+                    constrain((int) Math.round(b * 255), 0, 255));
+        }
+
+        /**
+         * Converts a color from CIE Lab to its RGB representation.
+         *
+         * @param l L component value [0...100]
+         * @param a A component value [-128...127]
+         * @param b B component value [-128...127]
+         * @return int containing the RGB representation
+         */
+        @ColorInt
+        public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
+                @FloatRange(from = -128, to = 127) final double a,
+                @FloatRange(from = -128, to = 127) final double b) {
+            final double[] result = getTempDouble3Array();
+            LABToXYZ(l, a, b, result);
+            return XYZToColor(result[0], result[1], result[2]);
+        }
+
+        private static int constrain(int amount, int low, int high) {
+            return amount < low ? low : (amount > high ? high : amount);
+        }
+
+        private static float constrain(float amount, float low, float high) {
+            return amount < low ? low : (amount > high ? high : amount);
+        }
+
+        private static double pivotXyzComponent(double component) {
+            return component > XYZ_EPSILON
+                    ? Math.pow(component, 1 / 3.0)
+                    : (XYZ_KAPPA * component + 16) / 116;
+        }
+
+        public static double[] getTempDouble3Array() {
+            double[] result = TEMP_ARRAY.get();
+            if (result == null) {
+                result = new double[3];
+                TEMP_ARRAY.set(result);
+            }
+            return result;
+        }
+
+        /**
+         * Convert HSL (hue-saturation-lightness) components to a RGB color.
+         * <ul>
+         * <li>hsl[0] is Hue [0 .. 360)</li>
+         * <li>hsl[1] is Saturation [0...1]</li>
+         * <li>hsl[2] is Lightness [0...1]</li>
+         * </ul>
+         * If hsv values are out of range, they are pinned.
+         *
+         * @param hsl 3-element array which holds the input HSL components
+         * @return the resulting RGB color
+         */
+        @ColorInt
+        public static int HSLToColor(@NonNull float[] hsl) {
+            final float h = hsl[0];
+            final float s = hsl[1];
+            final float l = hsl[2];
+
+            final float c = (1f - Math.abs(2 * l - 1f)) * s;
+            final float m = l - 0.5f * c;
+            final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
+
+            final int hueSegment = (int) h / 60;
+
+            int r = 0, g = 0, b = 0;
+
+            switch (hueSegment) {
+                case 0:
+                    r = Math.round(255 * (c + m));
+                    g = Math.round(255 * (x + m));
+                    b = Math.round(255 * m);
+                    break;
+                case 1:
+                    r = Math.round(255 * (x + m));
+                    g = Math.round(255 * (c + m));
+                    b = Math.round(255 * m);
+                    break;
+                case 2:
+                    r = Math.round(255 * m);
+                    g = Math.round(255 * (c + m));
+                    b = Math.round(255 * (x + m));
+                    break;
+                case 3:
+                    r = Math.round(255 * m);
+                    g = Math.round(255 * (x + m));
+                    b = Math.round(255 * (c + m));
+                    break;
+                case 4:
+                    r = Math.round(255 * (x + m));
+                    g = Math.round(255 * m);
+                    b = Math.round(255 * (c + m));
+                    break;
+                case 5:
+                case 6:
+                    r = Math.round(255 * (c + m));
+                    g = Math.round(255 * m);
+                    b = Math.round(255 * (x + m));
+                    break;
+            }
+
+            r = constrain(r, 0, 255);
+            g = constrain(g, 0, 255);
+            b = constrain(b, 0, 255);
+
+            return Color.rgb(r, g, b);
+        }
+
+        /**
+         * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
+         * <ul>
+         * <li>outHsl[0] is Hue [0 .. 360)</li>
+         * <li>outHsl[1] is Saturation [0...1]</li>
+         * <li>outHsl[2] is Lightness [0...1]</li>
+         * </ul>
+         *
+         * @param color  the ARGB color to convert. The alpha component is ignored
+         * @param outHsl 3-element array which holds the resulting HSL components
+         */
+        public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
+            RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
+        }
+
+        /**
+         * Convert RGB components to HSL (hue-saturation-lightness).
+         * <ul>
+         * <li>outHsl[0] is Hue [0 .. 360)</li>
+         * <li>outHsl[1] is Saturation [0...1]</li>
+         * <li>outHsl[2] is Lightness [0...1]</li>
+         * </ul>
+         *
+         * @param r      red component value [0..255]
+         * @param g      green component value [0..255]
+         * @param b      blue component value [0..255]
+         * @param outHsl 3-element array which holds the resulting HSL components
+         */
+        public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
+                @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+                @NonNull float[] outHsl) {
+            final float rf = r / 255f;
+            final float gf = g / 255f;
+            final float bf = b / 255f;
+
+            final float max = Math.max(rf, Math.max(gf, bf));
+            final float min = Math.min(rf, Math.min(gf, bf));
+            final float deltaMaxMin = max - min;
+
+            float h, s;
+            float l = (max + min) / 2f;
+
+            if (max == min) {
+                // Monochromatic
+                h = s = 0f;
+            } else {
+                if (max == rf) {
+                    h = ((gf - bf) / deltaMaxMin) % 6f;
+                } else if (max == gf) {
+                    h = ((bf - rf) / deltaMaxMin) + 2f;
+                } else {
+                    h = ((rf - gf) / deltaMaxMin) + 4f;
+                }
+
+                s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
+            }
+
+            h = (h * 60f) % 360f;
+            if (h < 0) {
+                h += 360f;
+            }
+
+            outHsl[0] = constrain(h, 0f, 360f);
+            outHsl[1] = constrain(s, 0f, 1f);
+            outHsl[2] = constrain(l, 0f, 1f);
+        }
+
+    }
+}
diff --git a/com/android/internal/util/NotificationMessagingUtil.java b/com/android/internal/util/NotificationMessagingUtil.java
new file mode 100644
index 0000000..518cf41
--- /dev/null
+++ b/com/android/internal/util/NotificationMessagingUtil.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import java.util.Objects;
+
+/**
+ * A util to look up messaging related functions for notifications. This is used for both the
+ * ranking and the actual layout.
+ */
+public class NotificationMessagingUtil {
+
+    private static final String DEFAULT_SMS_APP_SETTING = Settings.Secure.SMS_DEFAULT_APPLICATION;
+    private final Context mContext;
+    private ArrayMap<Integer, String> mDefaultSmsApp = new ArrayMap<>();
+
+    public NotificationMessagingUtil(Context context) {
+        mContext = context;
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(DEFAULT_SMS_APP_SETTING), false, mSmsContentObserver);
+    }
+
+    @SuppressWarnings("deprecation")
+    private boolean isDefaultMessagingApp(StatusBarNotification sbn) {
+        final int userId = sbn.getUserId();
+        if (userId == UserHandle.USER_NULL || userId == UserHandle.USER_ALL) return false;
+        if (mDefaultSmsApp.get(userId) == null) {
+            cacheDefaultSmsApp(userId);
+        }
+        return Objects.equals(mDefaultSmsApp.get(userId), sbn.getPackageName());
+    }
+
+    private void cacheDefaultSmsApp(int userId) {
+        mDefaultSmsApp.put(userId, Settings.Secure.getStringForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.SMS_DEFAULT_APPLICATION, userId));
+    }
+
+    private final ContentObserver mSmsContentObserver = new ContentObserver(
+            new Handler(Looper.getMainLooper())) {
+        @Override
+        public void onChange(boolean selfChange, Uri uri, int userId) {
+            if (Settings.Secure.getUriFor(DEFAULT_SMS_APP_SETTING).equals(uri)) {
+                cacheDefaultSmsApp(userId);
+            }
+        }
+    };
+
+    public boolean isImportantMessaging(StatusBarNotification sbn, int importance) {
+        if (importance < NotificationManager.IMPORTANCE_LOW) {
+            return false;
+        }
+
+        Class<? extends Notification.Style> style = sbn.getNotification().getNotificationStyle();
+        if (Notification.MessagingStyle.class.equals(style)) {
+            return true;
+        }
+
+        if (Notification.CATEGORY_MESSAGE.equals(sbn.getNotification().category)
+                && isDefaultMessagingApp(sbn)) {
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/com/android/internal/util/ObjectUtils.java b/com/android/internal/util/ObjectUtils.java
new file mode 100644
index 0000000..59e5a64
--- /dev/null
+++ b/com/android/internal/util/ObjectUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 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.internal.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** @hide */
+public class ObjectUtils {
+    private ObjectUtils() {}
+
+    @NonNull
+    public static <T> T firstNotNull(@Nullable T a, @NonNull T b) {
+        return a != null ? a : Preconditions.checkNotNull(b);
+    }
+
+    public static <T extends Comparable> int compare(@Nullable T a, @Nullable T b) {
+        if (a != null) {
+            return (b != null) ? a.compareTo(b) : 1;
+        } else {
+            return (b != null) ? -1 : 0;
+        }
+    }
+}
diff --git a/com/android/internal/util/Preconditions.java b/com/android/internal/util/Preconditions.java
new file mode 100644
index 0000000..e5d5716
--- /dev/null
+++ b/com/android/internal/util/Preconditions.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2011 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.internal.util;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.text.TextUtils;
+
+import java.util.Collection;
+
+/**
+ * Simple static methods to be called at the start of your own methods to verify
+ * correct arguments and state.
+ */
+public class Preconditions {
+
+    public static void checkArgument(boolean expression) {
+        if (!expression) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Ensures that an expression checking an argument is true.
+     *
+     * @param expression the expression to check
+     * @param errorMessage the exception message to use if the check fails; will
+     *     be converted to a string using {@link String#valueOf(Object)}
+     * @throws IllegalArgumentException if {@code expression} is false
+     */
+    public static void checkArgument(boolean expression, final Object errorMessage) {
+        if (!expression) {
+            throw new IllegalArgumentException(String.valueOf(errorMessage));
+        }
+    }
+
+    /**
+     * Ensures that an expression checking an argument is true.
+     *
+     * @param expression the expression to check
+     * @param messageTemplate a printf-style message template to use if the check fails; will
+     *     be converted to a string using {@link String#format(String, Object...)}
+     * @param messageArgs arguments for {@code messageTemplate}
+     * @throws IllegalArgumentException if {@code expression} is false
+     */
+    public static void checkArgument(boolean expression,
+            final String messageTemplate,
+            final Object... messageArgs) {
+        if (!expression) {
+            throw new IllegalArgumentException(String.format(messageTemplate, messageArgs));
+        }
+    }
+
+    /**
+     * Ensures that an string reference passed as a parameter to the calling
+     * method is not empty.
+     *
+     * @param string an string reference
+     * @return the string reference that was validated
+     * @throws IllegalArgumentException if {@code string} is empty
+     */
+    public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string) {
+        if (TextUtils.isEmpty(string)) {
+            throw new IllegalArgumentException();
+        }
+        return string;
+    }
+
+    /**
+     * Ensures that an string reference passed as a parameter to the calling
+     * method is not empty.
+     *
+     * @param string an string reference
+     * @param errorMessage the exception message to use if the check fails; will
+     *     be converted to a string using {@link String#valueOf(Object)}
+     * @return the string reference that was validated
+     * @throws IllegalArgumentException if {@code string} is empty
+     */
+    public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string,
+            final Object errorMessage) {
+        if (TextUtils.isEmpty(string)) {
+            throw new IllegalArgumentException(String.valueOf(errorMessage));
+        }
+        return string;
+    }
+
+    /**
+     * Ensures that an object reference passed as a parameter to the calling
+     * method is not null.
+     *
+     * @param reference an object reference
+     * @return the non-null reference that was validated
+     * @throws NullPointerException if {@code reference} is null
+     */
+    public static @NonNull <T> T checkNotNull(final T reference) {
+        if (reference == null) {
+            throw new NullPointerException();
+        }
+        return reference;
+    }
+
+    /**
+     * Ensures that an object reference passed as a parameter to the calling
+     * method is not null.
+     *
+     * @param reference an object reference
+     * @param errorMessage the exception message to use if the check fails; will
+     *     be converted to a string using {@link String#valueOf(Object)}
+     * @return the non-null reference that was validated
+     * @throws NullPointerException if {@code reference} is null
+     */
+    public static @NonNull <T> T checkNotNull(final T reference, final Object errorMessage) {
+        if (reference == null) {
+            throw new NullPointerException(String.valueOf(errorMessage));
+        }
+        return reference;
+    }
+
+    /**
+     * Ensures that an object reference passed as a parameter to the calling
+     * method is not null.
+     *
+     * @param reference an object reference
+     * @param messageTemplate a printf-style message template to use if the check fails; will
+     *     be converted to a string using {@link String#format(String, Object...)}
+     * @param messageArgs arguments for {@code messageTemplate}
+     * @return the non-null reference that was validated
+     * @throws NullPointerException if {@code reference} is null
+     */
+    public static @NonNull <T> T checkNotNull(final T reference,
+            final String messageTemplate,
+            final Object... messageArgs) {
+        if (reference == null) {
+            throw new NullPointerException(String.format(messageTemplate, messageArgs));
+        }
+        return reference;
+    }
+
+    /**
+     * Ensures the truth of an expression involving the state of the calling
+     * instance, but not involving any parameters to the calling method.
+     *
+     * @param expression a boolean expression
+     * @param message exception message
+     * @throws IllegalStateException if {@code expression} is false
+     */
+    public static void checkState(final boolean expression, String message) {
+        if (!expression) {
+            throw new IllegalStateException(message);
+        }
+    }
+
+    /**
+     * Ensures the truth of an expression involving the state of the calling
+     * instance, but not involving any parameters to the calling method.
+     *
+     * @param expression a boolean expression
+     * @throws IllegalStateException if {@code expression} is false
+     */
+    public static void checkState(final boolean expression) {
+        checkState(expression, null);
+    }
+
+    /**
+     * Check the requested flags, throwing if any requested flags are outside
+     * the allowed set.
+     *
+     * @return the validated requested flags.
+     */
+    public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
+        if ((requestedFlags & allowedFlags) != requestedFlags) {
+            throw new IllegalArgumentException("Requested flags 0x"
+                    + Integer.toHexString(requestedFlags) + ", but only 0x"
+                    + Integer.toHexString(allowedFlags) + " are allowed");
+        }
+
+        return requestedFlags;
+    }
+
+    /**
+     * Ensures that that the argument numeric value is non-negative.
+     *
+     * @param value a numeric int value
+     * @param errorMessage the exception message to use if the check fails
+     * @return the validated numeric value
+     * @throws IllegalArgumentException if {@code value} was negative
+     */
+    public static @IntRange(from = 0) int checkArgumentNonnegative(final int value,
+            final String errorMessage) {
+        if (value < 0) {
+            throw new IllegalArgumentException(errorMessage);
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that that the argument numeric value is non-negative.
+     *
+     * @param value a numeric int value
+     *
+     * @return the validated numeric value
+     * @throws IllegalArgumentException if {@code value} was negative
+     */
+    public static @IntRange(from = 0) int checkArgumentNonnegative(final int value) {
+        if (value < 0) {
+            throw new IllegalArgumentException();
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that that the argument numeric value is non-negative.
+     *
+     * @param value a numeric long value
+     * @return the validated numeric value
+     * @throws IllegalArgumentException if {@code value} was negative
+     */
+    public static long checkArgumentNonnegative(final long value) {
+        if (value < 0) {
+            throw new IllegalArgumentException();
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that that the argument numeric value is non-negative.
+     *
+     * @param value a numeric long value
+     * @param errorMessage the exception message to use if the check fails
+     * @return the validated numeric value
+     * @throws IllegalArgumentException if {@code value} was negative
+     */
+    public static long checkArgumentNonnegative(final long value, final String errorMessage) {
+        if (value < 0) {
+            throw new IllegalArgumentException(errorMessage);
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that that the argument numeric value is positive.
+     *
+     * @param value a numeric int value
+     * @param errorMessage the exception message to use if the check fails
+     * @return the validated numeric value
+     * @throws IllegalArgumentException if {@code value} was not positive
+     */
+    public static int checkArgumentPositive(final int value, final String errorMessage) {
+        if (value <= 0) {
+            throw new IllegalArgumentException(errorMessage);
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument floating point value is a finite number.
+     *
+     * <p>A finite number is defined to be both representable (that is, not NaN) and
+     * not infinite (that is neither positive or negative infinity).</p>
+     *
+     * @param value a floating point value
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated floating point value
+     *
+     * @throws IllegalArgumentException if {@code value} was not finite
+     */
+    public static float checkArgumentFinite(final float value, final String valueName) {
+        if (Float.isNaN(value)) {
+            throw new IllegalArgumentException(valueName + " must not be NaN");
+        } else if (Float.isInfinite(value)) {
+            throw new IllegalArgumentException(valueName + " must not be infinite");
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument floating point value is within the inclusive range.
+     *
+     * <p>While this can be used to range check against +/- infinity, note that all NaN numbers
+     * will always be out of range.</p>
+     *
+     * @param value a floating point value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated floating point value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static float checkArgumentInRange(float value, float lower, float upper,
+            String valueName) {
+        if (Float.isNaN(value)) {
+            throw new IllegalArgumentException(valueName + " must not be NaN");
+        } else if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%f, %f] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%f, %f] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument int value is within the inclusive range.
+     *
+     * @param value a int value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated int value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static int checkArgumentInRange(int value, int lower, int upper,
+            String valueName) {
+        if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument long value is within the inclusive range.
+     *
+     * @param value a long value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated long value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static long checkArgumentInRange(long value, long lower, long upper,
+            String valueName) {
+        if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the array is not {@code null}, and none of its elements are {@code null}.
+     *
+     * @param value an array of boxed objects
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated array
+     *
+     * @throws NullPointerException if the {@code value} or any of its elements were {@code null}
+     */
+    public static <T> T[] checkArrayElementsNotNull(final T[] value, final String valueName) {
+        if (value == null) {
+            throw new NullPointerException(valueName + " must not be null");
+        }
+
+        for (int i = 0; i < value.length; ++i) {
+            if (value[i] == null) {
+                throw new NullPointerException(
+                        String.format("%s[%d] must not be null", valueName, i));
+            }
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the {@link Collection} is not {@code null}, and none of its elements are
+     * {@code null}.
+     *
+     * @param value a {@link Collection} of boxed objects
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated {@link Collection}
+     *
+     * @throws NullPointerException if the {@code value} or any of its elements were {@code null}
+     */
+    public static @NonNull <C extends Collection<T>, T> C checkCollectionElementsNotNull(
+            final C value, final String valueName) {
+        if (value == null) {
+            throw new NullPointerException(valueName + " must not be null");
+        }
+
+        long ctr = 0;
+        for (T elem : value) {
+            if (elem == null) {
+                throw new NullPointerException(
+                        String.format("%s[%d] must not be null", valueName, ctr));
+            }
+            ++ctr;
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the {@link Collection} is not {@code null}, and contains at least one element.
+     *
+     * @param value a {@link Collection} of boxed elements.
+     * @param valueName the name of the argument to use if the check fails.
+
+     * @return the validated {@link Collection}
+     *
+     * @throws NullPointerException if the {@code value} was {@code null}
+     * @throws IllegalArgumentException if the {@code value} was empty
+     */
+    public static <T> Collection<T> checkCollectionNotEmpty(final Collection<T> value,
+            final String valueName) {
+        if (value == null) {
+            throw new NullPointerException(valueName + " must not be null");
+        }
+        if (value.isEmpty()) {
+            throw new IllegalArgumentException(valueName + " is empty");
+        }
+        return value;
+    }
+
+    /**
+     * Ensures that all elements in the argument floating point array are within the inclusive range
+     *
+     * <p>While this can be used to range check against +/- infinity, note that all NaN numbers
+     * will always be out of range.</p>
+     *
+     * @param value a floating point array of values
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated floating point value
+     *
+     * @throws IllegalArgumentException if any of the elements in {@code value} were out of range
+     * @throws NullPointerException if the {@code value} was {@code null}
+     */
+    public static float[] checkArrayElementsInRange(float[] value, float lower, float upper,
+            String valueName) {
+        checkNotNull(value, valueName + " must not be null");
+
+        for (int i = 0; i < value.length; ++i) {
+            float v = value[i];
+
+            if (Float.isNaN(v)) {
+                throw new IllegalArgumentException(valueName + "[" + i + "] must not be NaN");
+            } else if (v < lower) {
+                throw new IllegalArgumentException(
+                        String.format("%s[%d] is out of range of [%f, %f] (too low)",
+                                valueName, i, lower, upper));
+            } else if (v > upper) {
+                throw new IllegalArgumentException(
+                        String.format("%s[%d] is out of range of [%f, %f] (too high)",
+                                valueName, i, lower, upper));
+            }
+        }
+
+        return value;
+    }
+}
diff --git a/com/android/internal/util/Predicate.java b/com/android/internal/util/Predicate.java
new file mode 100644
index 0000000..1b5eaff
--- /dev/null
+++ b/com/android/internal/util/Predicate.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2008 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.internal.util;
+
+/**
+ * A Predicate can determine a true or false value for any input of its
+ * parameterized type. For example, a {@code RegexPredicate} might implement
+ * {@code Predicate<String>}, and return true for any String that matches its
+ * given regular expression.
+ * <p/>
+ * <p/>
+ * Implementors of Predicate which may cause side effects upon evaluation are
+ * strongly encouraged to state this fact clearly in their API documentation.
+ *
+ * @deprecated Use {@code java.util.function.Predicate} instead.
+ */
+@Deprecated
+public interface Predicate<T> {
+
+    boolean apply(T t);
+}
diff --git a/com/android/internal/util/ProcFileReader.java b/com/android/internal/util/ProcFileReader.java
new file mode 100644
index 0000000..ead58c7
--- /dev/null
+++ b/com/android/internal/util/ProcFileReader.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2011 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.internal.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ProtocolException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Reader that specializes in parsing {@code /proc/} files quickly. Walks
+ * through the stream using a single space {@code ' '} as token separator, and
+ * requires each line boundary to be explicitly acknowledged using
+ * {@link #finishLine()}. Assumes {@link StandardCharsets#US_ASCII} encoding.
+ * <p>
+ * Currently doesn't support formats based on {@code \0}, tabs, or repeated
+ * delimiters.
+ */
+public class ProcFileReader implements Closeable {
+    private final InputStream mStream;
+    private final byte[] mBuffer;
+
+    /** Write pointer in {@link #mBuffer}. */
+    private int mTail;
+    /** Flag when last read token finished current line. */
+    private boolean mLineFinished;
+
+    public ProcFileReader(InputStream stream) throws IOException {
+        this(stream, 4096);
+    }
+
+    public ProcFileReader(InputStream stream, int bufferSize) throws IOException {
+        mStream = stream;
+        mBuffer = new byte[bufferSize];
+
+        // read enough to answer hasMoreData
+        fillBuf();
+    }
+
+    /**
+     * Read more data from {@link #mStream} into internal buffer.
+     */
+    private int fillBuf() throws IOException {
+        final int length = mBuffer.length - mTail;
+        if (length == 0) {
+            throw new IOException("attempting to fill already-full buffer");
+        }
+
+        final int read = mStream.read(mBuffer, mTail, length);
+        if (read != -1) {
+            mTail += read;
+        }
+        return read;
+    }
+
+    /**
+     * Consume number of bytes from beginning of internal buffer. If consuming
+     * all remaining bytes, will attempt to {@link #fillBuf()}.
+     */
+    private void consumeBuf(int count) throws IOException {
+        // TODO: consider moving to read pointer, but for now traceview says
+        // these copies aren't a bottleneck.
+        System.arraycopy(mBuffer, count, mBuffer, 0, mTail - count);
+        mTail -= count;
+        if (mTail == 0) {
+            fillBuf();
+        }
+    }
+
+    /**
+     * Find buffer index of next token delimiter, usually space or newline.
+     * Fills buffer as needed.
+     *
+     * @return Index of next delimeter, otherwise -1 if no tokens remain on
+     *         current line.
+     */
+    private int nextTokenIndex() throws IOException {
+        if (mLineFinished) {
+            return -1;
+        }
+
+        int i = 0;
+        do {
+            // scan forward for token boundary
+            for (; i < mTail; i++) {
+                final byte b = mBuffer[i];
+                if (b == '\n') {
+                    mLineFinished = true;
+                    return i;
+                }
+                if (b == ' ') {
+                    return i;
+                }
+            }
+        } while (fillBuf() > 0);
+
+        throw new ProtocolException("End of stream while looking for token boundary");
+    }
+
+    /**
+     * Check if stream has more data to be parsed.
+     */
+    public boolean hasMoreData() {
+        return mTail > 0;
+    }
+
+    /**
+     * Finish current line, skipping any remaining data.
+     */
+    public void finishLine() throws IOException {
+        // last token already finished line; reset silently
+        if (mLineFinished) {
+            mLineFinished = false;
+            return;
+        }
+
+        int i = 0;
+        do {
+            // scan forward for line boundary and consume
+            for (; i < mTail; i++) {
+                if (mBuffer[i] == '\n') {
+                    consumeBuf(i + 1);
+                    return;
+                }
+            }
+        } while (fillBuf() > 0);
+
+        throw new ProtocolException("End of stream while looking for line boundary");
+    }
+
+    /**
+     * Parse and return next token as {@link String}.
+     */
+    public String nextString() throws IOException {
+        final int tokenIndex = nextTokenIndex();
+        if (tokenIndex == -1) {
+            throw new ProtocolException("Missing required string");
+        } else {
+            return parseAndConsumeString(tokenIndex);
+        }
+    }
+
+    /**
+     * Parse and return next token as base-10 encoded {@code long}.
+     */
+    public long nextLong() throws IOException {
+        final int tokenIndex = nextTokenIndex();
+        if (tokenIndex == -1) {
+            throw new ProtocolException("Missing required long");
+        } else {
+            return parseAndConsumeLong(tokenIndex);
+        }
+    }
+
+    /**
+     * Parse and return next token as base-10 encoded {@code long}, or return
+     * the given default value if no remaining tokens on current line.
+     */
+    public long nextOptionalLong(long def) throws IOException {
+        final int tokenIndex = nextTokenIndex();
+        if (tokenIndex == -1) {
+            return def;
+        } else {
+            return parseAndConsumeLong(tokenIndex);
+        }
+    }
+
+    private String parseAndConsumeString(int tokenIndex) throws IOException {
+        final String s = new String(mBuffer, 0, tokenIndex, StandardCharsets.US_ASCII);
+        consumeBuf(tokenIndex + 1);
+        return s;
+    }
+
+    private long parseAndConsumeLong(int tokenIndex) throws IOException {
+        final boolean negative = mBuffer[0] == '-';
+
+        // TODO: refactor into something like IntegralToString
+        long result = 0;
+        for (int i = negative ? 1 : 0; i < tokenIndex; i++) {
+            final int digit = mBuffer[i] - '0';
+            if (digit < 0 || digit > 9) {
+                throw invalidLong(tokenIndex);
+            }
+
+            // always parse as negative number and apply sign later; this
+            // correctly handles MIN_VALUE which is "larger" than MAX_VALUE.
+            final long next = result * 10 - digit;
+            if (next > result) {
+                throw invalidLong(tokenIndex);
+            }
+            result = next;
+        }
+
+        consumeBuf(tokenIndex + 1);
+        return negative ? result : -result;
+    }
+
+    private NumberFormatException invalidLong(int tokenIndex) {
+        return new NumberFormatException(
+                "invalid long: " + new String(mBuffer, 0, tokenIndex, StandardCharsets.US_ASCII));
+    }
+
+    /**
+     * Parse and return next token as base-10 encoded {@code int}.
+     */
+    public int nextInt() throws IOException {
+        final long value = nextLong();
+        if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
+            throw new NumberFormatException("parsed value larger than integer");
+        }
+        return (int) value;
+    }
+
+    @Override
+    public void close() throws IOException {
+        mStream.close();
+    }
+}
diff --git a/com/android/internal/util/ProgressReporter.java b/com/android/internal/util/ProgressReporter.java
new file mode 100644
index 0000000..7a8efba
--- /dev/null
+++ b/com/android/internal/util/ProgressReporter.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IProgressListener;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.MathUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Tracks and reports progress of a single task to a {@link IProgressListener}.
+ * The reported progress of a task ranges from 0-100, but the task can be
+ * segmented into smaller pieces using {@link #startSegment(int)} and
+ * {@link #endSegment(int[])}, and segments can be nested.
+ * <p>
+ * Here's an example in action; when finished the overall task progress will be
+ * at 60.
+ *
+ * <pre>
+ * prog.setProgress(20);
+ * {
+ *     final int restore = prog.startSegment(40);
+ *     for (int i = 0; i < N; i++) {
+ *         prog.setProgress(i, N);
+ *         ...
+ *     }
+ *     prog.endSegment(restore);
+ * }
+ * </pre>
+ *
+ * @hide
+ */
+public class ProgressReporter {
+    private static final int STATE_INIT = 0;
+    private static final int STATE_STARTED = 1;
+    private static final int STATE_FINISHED = 2;
+
+    private final int mId;
+
+    @GuardedBy("this")
+    private final RemoteCallbackList<IProgressListener> mListeners = new RemoteCallbackList<>();
+
+    @GuardedBy("this")
+    private int mState = STATE_INIT;
+    @GuardedBy("this")
+    private int mProgress = 0;
+    @GuardedBy("this")
+    private Bundle mExtras = new Bundle();
+
+    /**
+     * Current segment range: first element is starting progress of this
+     * segment, second element is length of segment.
+     */
+    @GuardedBy("this")
+    private int[] mSegmentRange = new int[] { 0, 100 };
+
+    /**
+     * Create a new task with the given identifier whose progress will be
+     * reported to the given listener.
+     */
+    public ProgressReporter(int id) {
+        mId = id;
+    }
+
+    /**
+     * Add given listener to watch for progress events. The current state will
+     * be immediately dispatched to the given listener.
+     */
+    public void addListener(@Nullable IProgressListener listener) {
+        if (listener == null) return;
+        synchronized (this) {
+            mListeners.register(listener);
+            switch (mState) {
+                case STATE_INIT:
+                    // Nothing has happened yet
+                    break;
+                case STATE_STARTED:
+                    try {
+                        listener.onStarted(mId, null);
+                        listener.onProgress(mId, mProgress, mExtras);
+                    } catch (RemoteException ignored) {
+                    }
+                    break;
+                case STATE_FINISHED:
+                    try {
+                        listener.onFinished(mId, null);
+                    } catch (RemoteException ignored) {
+                    }
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Set the progress of the currently active segment.
+     *
+     * @param progress Segment progress between 0-100.
+     */
+    public void setProgress(int progress) {
+        setProgress(progress, 100, null);
+    }
+
+    /**
+     * Set the progress of the currently active segment.
+     *
+     * @param progress Segment progress between 0-100.
+     */
+    public void setProgress(int progress, @Nullable CharSequence title) {
+        setProgress(progress, 100, title);
+    }
+
+    /**
+     * Set the fractional progress of the currently active segment.
+     */
+    public void setProgress(int n, int m) {
+        setProgress(n, m, null);
+    }
+
+    /**
+     * Set the fractional progress of the currently active segment.
+     */
+    public void setProgress(int n, int m, @Nullable CharSequence title) {
+        synchronized (this) {
+            if (mState != STATE_STARTED) {
+                throw new IllegalStateException("Must be started to change progress");
+            }
+            mProgress = mSegmentRange[0]
+                    + MathUtils.constrain((n * mSegmentRange[1]) / m, 0, mSegmentRange[1]);
+            if (title != null) {
+                mExtras.putCharSequence(Intent.EXTRA_TITLE, title);
+            }
+            notifyProgress(mId, mProgress, mExtras);
+        }
+    }
+
+    /**
+     * Start a new inner segment that will contribute the given range towards
+     * the currently active segment. You must pass the returned value to
+     * {@link #endSegment(int[])} when finished.
+     */
+    public int[] startSegment(int size) {
+        synchronized (this) {
+            final int[] lastRange = mSegmentRange;
+            mSegmentRange = new int[] { mProgress, (size * mSegmentRange[1] / 100) };
+            return lastRange;
+        }
+    }
+
+    /**
+     * End the current segment.
+     */
+    public void endSegment(int[] lastRange) {
+        synchronized (this) {
+            mProgress = mSegmentRange[0] + mSegmentRange[1];
+            mSegmentRange = lastRange;
+        }
+    }
+
+    int getProgress() {
+        return mProgress;
+    }
+
+    int[] getSegmentRange() {
+        return mSegmentRange;
+    }
+
+    /**
+     * Report this entire task as being started.
+     */
+    public void start() {
+        synchronized (this) {
+            mState = STATE_STARTED;
+            notifyStarted(mId, null);
+            notifyProgress(mId, mProgress, mExtras);
+        }
+    }
+
+    /**
+     * Report this entire task as being finished.
+     */
+    public void finish() {
+        synchronized (this) {
+            mState = STATE_FINISHED;
+            notifyFinished(mId, null);
+            mListeners.kill();
+        }
+    }
+
+    private void notifyStarted(int id, Bundle extras) {
+        for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) {
+            try {
+                mListeners.getBroadcastItem(i).onStarted(id, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    private void notifyProgress(int id, int progress, Bundle extras) {
+        for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) {
+            try {
+                mListeners.getBroadcastItem(i).onProgress(id, progress, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+
+    private void notifyFinished(int id, Bundle extras) {
+        for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) {
+            try {
+                mListeners.getBroadcastItem(i).onFinished(id, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+        mListeners.finishBroadcast();
+    }
+}
diff --git a/com/android/internal/util/Protocol.java b/com/android/internal/util/Protocol.java
new file mode 100644
index 0000000..1aa32cc
--- /dev/null
+++ b/com/android/internal/util/Protocol.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2011 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.internal.util;
+
+/**
+ * This class defines Message.what base addresses for various protocols that are recognized
+ * to be unique by any {@link com.android.internal.util.Statemachine} implementation. This
+ * allows for interaction between different StateMachine implementations without a conflict
+ * of message codes.
+ *
+ * As an example, all messages in {@link android.net.wifi.WifiStateMachine} will have message
+ * codes with Message.what starting at Protocol.WIFI + 1 and less than or equal to Protocol.WIFI +
+ * Protocol.MAX_MESSAGE
+ *
+ * NOTE: After a value is created and source released a value shouldn't be changed to
+ * maintain backwards compatibility.
+ *
+ * {@hide}
+ */
+public class Protocol {
+    public static final int MAX_MESSAGE                                             = 0x0000FFFF;
+
+    /** Base reserved for system */
+    public static final int BASE_SYSTEM_RESERVED                                    = 0x00010000;
+    public static final int BASE_SYSTEM_ASYNC_CHANNEL                               = 0x00011000;
+
+    /** Non system protocols */
+    public static final int BASE_WIFI                                               = 0x00020000;
+    public static final int BASE_WIFI_WATCHDOG                                      = 0x00021000;
+    public static final int BASE_WIFI_P2P_MANAGER                                   = 0x00022000;
+    public static final int BASE_WIFI_P2P_SERVICE                                   = 0x00023000;
+    public static final int BASE_WIFI_MONITOR                                       = 0x00024000;
+    public static final int BASE_WIFI_MANAGER                                       = 0x00025000;
+    public static final int BASE_WIFI_CONTROLLER                                    = 0x00026000;
+    public static final int BASE_WIFI_SCANNER                                       = 0x00027000;
+    public static final int BASE_WIFI_SCANNER_SERVICE                               = 0x00027100;
+    public static final int BASE_WIFI_RTT_MANAGER                                   = 0x00027200;
+    public static final int BASE_WIFI_RTT_SERVICE                                   = 0x00027300;
+    public static final int BASE_WIFI_PASSPOINT_MANAGER                             = 0x00028000;
+    public static final int BASE_WIFI_PASSPOINT_SERVICE                             = 0x00028100;
+    public static final int BASE_WIFI_LOGGER                                        = 0x00028300;
+    public static final int BASE_DHCP                                               = 0x00030000;
+    public static final int BASE_DATA_CONNECTION                                    = 0x00040000;
+    public static final int BASE_DATA_CONNECTION_AC                                 = 0x00041000;
+    public static final int BASE_DATA_CONNECTION_TRACKER                            = 0x00042000;
+    public static final int BASE_TETHERING                                          = 0x00050000;
+    public static final int BASE_NSD_MANAGER                                        = 0x00060000;
+    public static final int BASE_NETWORK_STATE_TRACKER                              = 0x00070000;
+    public static final int BASE_CONNECTIVITY_MANAGER                               = 0x00080000;
+    public static final int BASE_NETWORK_AGENT                                      = 0x00081000;
+    public static final int BASE_NETWORK_MONITOR                                    = 0x00082000;
+    public static final int BASE_NETWORK_FACTORY                                    = 0x00083000;
+    public static final int BASE_ETHERNET                                           = 0x00084000;
+    public static final int BASE_LOWPAN                                             = 0x00085000;
+    //TODO: define all used protocols
+}
diff --git a/com/android/internal/util/RingBufferIndices.java b/com/android/internal/util/RingBufferIndices.java
new file mode 100644
index 0000000..fe751f4
--- /dev/null
+++ b/com/android/internal/util/RingBufferIndices.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+/**
+ * Helper class for implementing a ring buffer.  This supplies the indices, you supply
+ * the array(s).
+ */
+public class RingBufferIndices {
+    private final int mCapacity;
+
+    // The first valid element and the next open slot.
+    private int mStart;
+    private int mSize;
+
+    /**
+     * Create ring buffer of the given capacity.
+     */
+    public RingBufferIndices(int capacity) {
+        mCapacity = capacity;
+    }
+
+    /**
+     * Add a new item to the ring buffer.  If the ring buffer is full, this
+     * replaces the oldest item.
+     * @return Returns the index at which the new item appears, for placing in your array.
+     */
+    public int add() {
+        if (mSize < mCapacity) {
+            final int pos = mSize;
+            mSize++;
+            return pos;
+        }
+        int pos = mStart;
+        mStart++;
+        if (mStart == mCapacity) {
+            mStart = 0;
+        }
+        return pos;
+    }
+
+    /**
+     * Clear the ring buffer.
+     */
+    public void clear() {
+        mStart = 0;
+        mSize = 0;
+    }
+
+    /**
+     * Return the current size of the ring buffer.
+     */
+    public int size() {
+        return mSize;
+    }
+
+    /**
+     * Convert a position in the ring buffer that is [0..size()] to an offset
+     * in the array(s) containing the ring buffer items.
+     */
+    public int indexOf(int pos) {
+        int index = mStart + pos;
+        if (index >= mCapacity) {
+            index -= mCapacity;
+        }
+        return index;
+    }
+}
diff --git a/com/android/internal/util/ScreenShapeHelper.java b/com/android/internal/util/ScreenShapeHelper.java
new file mode 100644
index 0000000..5f390be
--- /dev/null
+++ b/com/android/internal/util/ScreenShapeHelper.java
@@ -0,0 +1,23 @@
+package com.android.internal.util;
+
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.view.ViewRootImpl;
+
+/**
+ * @hide
+ */
+public class ScreenShapeHelper {
+    /**
+     * Return the bottom pixel window outset of a window given its style attributes.
+     * @return An outset dimension in pixels or 0 if no outset should be applied.
+     */
+    public static int getWindowOutsetBottomPx(Resources resources) {
+        if (Build.IS_EMULATOR) {
+            return SystemProperties.getInt(ViewRootImpl.PROPERTY_EMULATOR_WIN_OUTSET_BOTTOM_PX, 0);
+        } else {
+            return resources.getInteger(com.android.internal.R.integer.config_windowOutsetBottom);
+        }
+    }
+}
diff --git a/com/android/internal/util/SizedInputStream.java b/com/android/internal/util/SizedInputStream.java
new file mode 100644
index 0000000..00a729d
--- /dev/null
+++ b/com/android/internal/util/SizedInputStream.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 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.internal.util;
+
+import libcore.io.Streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Reads exact number of bytes from wrapped stream, returning EOF once those
+ * bytes have been read.
+ */
+public class SizedInputStream extends InputStream {
+    private final InputStream mWrapped;
+    private long mLength;
+
+    public SizedInputStream(InputStream wrapped, long length) {
+        mWrapped = wrapped;
+        mLength = length;
+    }
+
+    @Override
+    public void close() throws IOException {
+        super.close();
+        mWrapped.close();
+    }
+
+    @Override
+    public int read() throws IOException {
+        return Streams.readSingleByte(this);
+    }
+
+    @Override
+    public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
+        if (mLength <= 0) {
+            return -1;
+        } else if (byteCount > mLength) {
+            byteCount = (int) mLength;
+        }
+
+        final int n = mWrapped.read(buffer, byteOffset, byteCount);
+        if (n == -1) {
+            if (mLength > 0) {
+                throw new IOException("Unexpected EOF; expected " + mLength + " more bytes");
+            }
+        } else {
+            mLength -= n;
+        }
+        return n;
+    }
+}
diff --git a/com/android/internal/util/State.java b/com/android/internal/util/State.java
new file mode 100644
index 0000000..3eadff5
--- /dev/null
+++ b/com/android/internal/util/State.java
@@ -0,0 +1,74 @@
+/**
+ * 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.internal.util;
+
+import android.os.Message;
+
+/**
+ * {@hide}
+ *
+ * The class for implementing states in a StateMachine
+ */
+public class State implements IState {
+
+    /**
+     * Constructor
+     */
+    protected State() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.util.IState#enter()
+     */
+    @Override
+    public void enter() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.util.IState#exit()
+     */
+    @Override
+    public void exit() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.util.IState#processMessage(android.os.Message)
+     */
+    @Override
+    public boolean processMessage(Message msg) {
+        return false;
+    }
+
+    /**
+     * Name of State for debugging purposes.
+     *
+     * This default implementation returns the class name, returning
+     * the instance name would better in cases where a State class
+     * is used for multiple states. But normally there is one class per
+     * state and the class name is sufficient and easy to get. You may
+     * want to provide a setName or some other mechanism for setting
+     * another name if the class name is not appropriate.
+     *
+     * @see com.android.internal.util.IState#processMessage(android.os.Message)
+     */
+    @Override
+    public String getName() {
+        String name = getClass().getName();
+        int lastDollar = name.lastIndexOf('$');
+        return name.substring(lastDollar + 1);
+    }
+}
diff --git a/com/android/internal/util/StateMachine.java b/com/android/internal/util/StateMachine.java
new file mode 100644
index 0000000..8d9630f
--- /dev/null
+++ b/com/android/internal/util/StateMachine.java
@@ -0,0 +1,2168 @@
+/**
+ * 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.internal.util;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Vector;
+
+/**
+ * {@hide}
+ *
+ * <p>The state machine defined here is a hierarchical state machine which processes messages
+ * and can have states arranged hierarchically.</p>
+ *
+ * <p>A state is a <code>State</code> object and must implement
+ * <code>processMessage</code> and optionally <code>enter/exit/getName</code>.
+ * The enter/exit methods are equivalent to the construction and destruction
+ * in Object Oriented programming and are used to perform initialization and
+ * cleanup of the state respectively. The <code>getName</code> method returns the
+ * name of the state; the default implementation returns the class name. It may be
+ * desirable to have <code>getName</code> return the the state instance name instead,
+ * in particular if a particular state class has multiple instances.</p>
+ *
+ * <p>When a state machine is created, <code>addState</code> is used to build the
+ * hierarchy and <code>setInitialState</code> is used to identify which of these
+ * is the initial state. After construction the programmer calls <code>start</code>
+ * which initializes and starts the state machine. The first action the StateMachine
+ * is to the invoke <code>enter</code> for all of the initial state's hierarchy,
+ * starting at its eldest parent. The calls to enter will be done in the context
+ * of the StateMachine's Handler, not in the context of the call to start, and they
+ * will be invoked before any messages are processed. For example, given the simple
+ * state machine below, mP1.enter will be invoked and then mS1.enter. Finally,
+ * messages sent to the state machine will be processed by the current state;
+ * in our simple state machine below that would initially be mS1.processMessage.</p>
+<pre>
+        mP1
+       /   \
+      mS2   mS1 ----&gt; initial state
+</pre>
+ * <p>After the state machine is created and started, messages are sent to a state
+ * machine using <code>sendMessage</code> and the messages are created using
+ * <code>obtainMessage</code>. When the state machine receives a message the
+ * current state's <code>processMessage</code> is invoked. In the above example
+ * mS1.processMessage will be invoked first. The state may use <code>transitionTo</code>
+ * to change the current state to a new state.</p>
+ *
+ * <p>Each state in the state machine may have a zero or one parent states. If
+ * a child state is unable to handle a message it may have the message processed
+ * by its parent by returning false or NOT_HANDLED. If a message is not handled by
+ * a child state or any of its ancestors, <code>unhandledMessage</code> will be invoked
+ * to give one last chance for the state machine to process the message.</p>
+ *
+ * <p>When all processing is completed a state machine may choose to call
+ * <code>transitionToHaltingState</code>. When the current <code>processingMessage</code>
+ * returns the state machine will transfer to an internal <code>HaltingState</code>
+ * and invoke <code>halting</code>. Any message subsequently received by the state
+ * machine will cause <code>haltedProcessMessage</code> to be invoked.</p>
+ *
+ * <p>If it is desirable to completely stop the state machine call <code>quit</code> or
+ * <code>quitNow</code>. These will call <code>exit</code> of the current state and its parents,
+ * call <code>onQuitting</code> and then exit Thread/Loopers.</p>
+ *
+ * <p>In addition to <code>processMessage</code> each <code>State</code> has
+ * an <code>enter</code> method and <code>exit</code> method which may be overridden.</p>
+ *
+ * <p>Since the states are arranged in a hierarchy transitioning to a new state
+ * causes current states to be exited and new states to be entered. To determine
+ * the list of states to be entered/exited the common parent closest to
+ * the current state is found. We then exit from the current state and its
+ * parent's up to but not including the common parent state and then enter all
+ * of the new states below the common parent down to the destination state.
+ * If there is no common parent all states are exited and then the new states
+ * are entered.</p>
+ *
+ * <p>Two other methods that states can use are <code>deferMessage</code> and
+ * <code>sendMessageAtFrontOfQueue</code>. The <code>sendMessageAtFrontOfQueue</code> sends
+ * a message but places it on the front of the queue rather than the back. The
+ * <code>deferMessage</code> causes the message to be saved on a list until a
+ * transition is made to a new state. At which time all of the deferred messages
+ * will be put on the front of the state machine queue with the oldest message
+ * at the front. These will then be processed by the new current state before
+ * any other messages that are on the queue or might be added later. Both of
+ * these are protected and may only be invoked from within a state machine.</p>
+ *
+ * <p>To illustrate some of these properties we'll use state machine with an 8
+ * state hierarchy:</p>
+<pre>
+          mP0
+         /   \
+        mP1   mS0
+       /   \
+      mS2   mS1
+     /  \    \
+    mS3  mS4  mS5  ---&gt; initial state
+</pre>
+ * <p>After starting mS5 the list of active states is mP0, mP1, mS1 and mS5.
+ * So the order of calling processMessage when a message is received is mS5,
+ * mS1, mP1, mP0 assuming each processMessage indicates it can't handle this
+ * message by returning false or NOT_HANDLED.</p>
+ *
+ * <p>Now assume mS5.processMessage receives a message it can handle, and during
+ * the handling determines the machine should change states. It could call
+ * transitionTo(mS4) and return true or HANDLED. Immediately after returning from
+ * processMessage the state machine runtime will find the common parent,
+ * which is mP1. It will then call mS5.exit, mS1.exit, mS2.enter and then
+ * mS4.enter. The new list of active states is mP0, mP1, mS2 and mS4. So
+ * when the next message is received mS4.processMessage will be invoked.</p>
+ *
+ * <p>Now for some concrete examples, here is the canonical HelloWorld as a state machine.
+ * It responds with "Hello World" being printed to the log for every message.</p>
+<pre>
+class HelloWorld extends StateMachine {
+    HelloWorld(String name) {
+        super(name);
+        addState(mState1);
+        setInitialState(mState1);
+    }
+
+    public static HelloWorld makeHelloWorld() {
+        HelloWorld hw = new HelloWorld("hw");
+        hw.start();
+        return hw;
+    }
+
+    class State1 extends State {
+        &#64;Override public boolean processMessage(Message message) {
+            log("Hello World");
+            return HANDLED;
+        }
+    }
+    State1 mState1 = new State1();
+}
+
+void testHelloWorld() {
+    HelloWorld hw = makeHelloWorld();
+    hw.sendMessage(hw.obtainMessage());
+}
+</pre>
+ * <p>A more interesting state machine is one with four states
+ * with two independent parent states.</p>
+<pre>
+        mP1      mP2
+       /   \
+      mS2   mS1
+</pre>
+ * <p>Here is a description of this state machine using pseudo code.</p>
+ <pre>
+state mP1 {
+     enter { log("mP1.enter"); }
+     exit { log("mP1.exit");  }
+     on msg {
+         CMD_2 {
+             send(CMD_3);
+             defer(msg);
+             transitionTo(mS2);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+
+INITIAL
+state mS1 parent mP1 {
+     enter { log("mS1.enter"); }
+     exit  { log("mS1.exit");  }
+     on msg {
+         CMD_1 {
+             transitionTo(mS1);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+
+state mS2 parent mP1 {
+     enter { log("mS2.enter"); }
+     exit  { log("mS2.exit");  }
+     on msg {
+         CMD_2 {
+             send(CMD_4);
+             return HANDLED;
+         }
+         CMD_3 {
+             defer(msg);
+             transitionTo(mP2);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+
+state mP2 {
+     enter {
+         log("mP2.enter");
+         send(CMD_5);
+     }
+     exit { log("mP2.exit"); }
+     on msg {
+         CMD_3, CMD_4 { return HANDLED; }
+         CMD_5 {
+             transitionTo(HaltingState);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+</pre>
+ * <p>The implementation is below and also in StateMachineTest:</p>
+<pre>
+class Hsm1 extends StateMachine {
+    public static final int CMD_1 = 1;
+    public static final int CMD_2 = 2;
+    public static final int CMD_3 = 3;
+    public static final int CMD_4 = 4;
+    public static final int CMD_5 = 5;
+
+    public static Hsm1 makeHsm1() {
+        log("makeHsm1 E");
+        Hsm1 sm = new Hsm1("hsm1");
+        sm.start();
+        log("makeHsm1 X");
+        return sm;
+    }
+
+    Hsm1(String name) {
+        super(name);
+        log("ctor E");
+
+        // Add states, use indentation to show hierarchy
+        addState(mP1);
+            addState(mS1, mP1);
+            addState(mS2, mP1);
+        addState(mP2);
+
+        // Set the initial state
+        setInitialState(mS1);
+        log("ctor X");
+    }
+
+    class P1 extends State {
+        &#64;Override public void enter() {
+            log("mP1.enter");
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            boolean retVal;
+            log("mP1.processMessage what=" + message.what);
+            switch(message.what) {
+            case CMD_2:
+                // CMD_2 will arrive in mS2 before CMD_3
+                sendMessage(obtainMessage(CMD_3));
+                deferMessage(message);
+                transitionTo(mS2);
+                retVal = HANDLED;
+                break;
+            default:
+                // Any message we don't understand in this state invokes unhandledMessage
+                retVal = NOT_HANDLED;
+                break;
+            }
+            return retVal;
+        }
+        &#64;Override public void exit() {
+            log("mP1.exit");
+        }
+    }
+
+    class S1 extends State {
+        &#64;Override public void enter() {
+            log("mS1.enter");
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            log("S1.processMessage what=" + message.what);
+            if (message.what == CMD_1) {
+                // Transition to ourself to show that enter/exit is called
+                transitionTo(mS1);
+                return HANDLED;
+            } else {
+                // Let parent process all other messages
+                return NOT_HANDLED;
+            }
+        }
+        &#64;Override public void exit() {
+            log("mS1.exit");
+        }
+    }
+
+    class S2 extends State {
+        &#64;Override public void enter() {
+            log("mS2.enter");
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            boolean retVal;
+            log("mS2.processMessage what=" + message.what);
+            switch(message.what) {
+            case(CMD_2):
+                sendMessage(obtainMessage(CMD_4));
+                retVal = HANDLED;
+                break;
+            case(CMD_3):
+                deferMessage(message);
+                transitionTo(mP2);
+                retVal = HANDLED;
+                break;
+            default:
+                retVal = NOT_HANDLED;
+                break;
+            }
+            return retVal;
+        }
+        &#64;Override public void exit() {
+            log("mS2.exit");
+        }
+    }
+
+    class P2 extends State {
+        &#64;Override public void enter() {
+            log("mP2.enter");
+            sendMessage(obtainMessage(CMD_5));
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            log("P2.processMessage what=" + message.what);
+            switch(message.what) {
+            case(CMD_3):
+                break;
+            case(CMD_4):
+                break;
+            case(CMD_5):
+                transitionToHaltingState();
+                break;
+            }
+            return HANDLED;
+        }
+        &#64;Override public void exit() {
+            log("mP2.exit");
+        }
+    }
+
+    &#64;Override
+    void onHalting() {
+        log("halting");
+        synchronized (this) {
+            this.notifyAll();
+        }
+    }
+
+    P1 mP1 = new P1();
+    S1 mS1 = new S1();
+    S2 mS2 = new S2();
+    P2 mP2 = new P2();
+}
+</pre>
+ * <p>If this is executed by sending two messages CMD_1 and CMD_2
+ * (Note the synchronize is only needed because we use hsm.wait())</p>
+<pre>
+Hsm1 hsm = makeHsm1();
+synchronize(hsm) {
+     hsm.sendMessage(obtainMessage(hsm.CMD_1));
+     hsm.sendMessage(obtainMessage(hsm.CMD_2));
+     try {
+          // wait for the messages to be handled
+          hsm.wait();
+     } catch (InterruptedException e) {
+          loge("exception while waiting " + e.getMessage());
+     }
+}
+</pre>
+ * <p>The output is:</p>
+<pre>
+D/hsm1    ( 1999): makeHsm1 E
+D/hsm1    ( 1999): ctor E
+D/hsm1    ( 1999): ctor X
+D/hsm1    ( 1999): mP1.enter
+D/hsm1    ( 1999): mS1.enter
+D/hsm1    ( 1999): makeHsm1 X
+D/hsm1    ( 1999): mS1.processMessage what=1
+D/hsm1    ( 1999): mS1.exit
+D/hsm1    ( 1999): mS1.enter
+D/hsm1    ( 1999): mS1.processMessage what=2
+D/hsm1    ( 1999): mP1.processMessage what=2
+D/hsm1    ( 1999): mS1.exit
+D/hsm1    ( 1999): mS2.enter
+D/hsm1    ( 1999): mS2.processMessage what=2
+D/hsm1    ( 1999): mS2.processMessage what=3
+D/hsm1    ( 1999): mS2.exit
+D/hsm1    ( 1999): mP1.exit
+D/hsm1    ( 1999): mP2.enter
+D/hsm1    ( 1999): mP2.processMessage what=3
+D/hsm1    ( 1999): mP2.processMessage what=4
+D/hsm1    ( 1999): mP2.processMessage what=5
+D/hsm1    ( 1999): mP2.exit
+D/hsm1    ( 1999): halting
+</pre>
+ */
+public class StateMachine {
+    // Name of the state machine and used as logging tag
+    private String mName;
+
+    /** Message.what value when quitting */
+    private static final int SM_QUIT_CMD = -1;
+
+    /** Message.what value when initializing */
+    private static final int SM_INIT_CMD = -2;
+
+    /**
+     * Convenience constant that maybe returned by processMessage
+     * to indicate the the message was processed and is not to be
+     * processed by parent states
+     */
+    public static final boolean HANDLED = true;
+
+    /**
+     * Convenience constant that maybe returned by processMessage
+     * to indicate the the message was NOT processed and is to be
+     * processed by parent states
+     */
+    public static final boolean NOT_HANDLED = false;
+
+    /**
+     * StateMachine logging record.
+     * {@hide}
+     */
+    public static class LogRec {
+        private StateMachine mSm;
+        private long mTime;
+        private int mWhat;
+        private String mInfo;
+        private IState mState;
+        private IState mOrgState;
+        private IState mDstState;
+
+        /**
+         * Constructor
+         *
+         * @param msg
+         * @param state the state which handled the message
+         * @param orgState is the first state the received the message but
+         * did not processes the message.
+         * @param transToState is the state that was transitioned to after the message was
+         * processed.
+         */
+        LogRec(StateMachine sm, Message msg, String info, IState state, IState orgState,
+                IState transToState) {
+            update(sm, msg, info, state, orgState, transToState);
+        }
+
+        /**
+         * Update the information in the record.
+         * @param state that handled the message
+         * @param orgState is the first state the received the message
+         * @param dstState is the state that was the transition target when logging
+         */
+        public void update(StateMachine sm, Message msg, String info, IState state, IState orgState,
+                IState dstState) {
+            mSm = sm;
+            mTime = System.currentTimeMillis();
+            mWhat = (msg != null) ? msg.what : 0;
+            mInfo = info;
+            mState = state;
+            mOrgState = orgState;
+            mDstState = dstState;
+        }
+
+        /**
+         * @return time stamp
+         */
+        public long getTime() {
+            return mTime;
+        }
+
+        /**
+         * @return msg.what
+         */
+        public long getWhat() {
+            return mWhat;
+        }
+
+        /**
+         * @return the command that was executing
+         */
+        public String getInfo() {
+            return mInfo;
+        }
+
+        /**
+         * @return the state that handled this message
+         */
+        public IState getState() {
+            return mState;
+        }
+
+        /**
+         * @return the state destination state if a transition is occurring or null if none.
+         */
+        public IState getDestState() {
+            return mDstState;
+        }
+
+        /**
+         * @return the original state that received the message.
+         */
+        public IState getOriginalState() {
+            return mOrgState;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("time=");
+            Calendar c = Calendar.getInstance();
+            c.setTimeInMillis(mTime);
+            sb.append(String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
+            sb.append(" processed=");
+            sb.append(mState == null ? "<null>" : mState.getName());
+            sb.append(" org=");
+            sb.append(mOrgState == null ? "<null>" : mOrgState.getName());
+            sb.append(" dest=");
+            sb.append(mDstState == null ? "<null>" : mDstState.getName());
+            sb.append(" what=");
+            String what = mSm != null ? mSm.getWhatToString(mWhat) : "";
+            if (TextUtils.isEmpty(what)) {
+                sb.append(mWhat);
+                sb.append("(0x");
+                sb.append(Integer.toHexString(mWhat));
+                sb.append(")");
+            } else {
+                sb.append(what);
+            }
+            if (!TextUtils.isEmpty(mInfo)) {
+                sb.append(" ");
+                sb.append(mInfo);
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
+     * A list of log records including messages recently processed by the state machine.
+     *
+     * The class maintains a list of log records including messages
+     * recently processed. The list is finite and may be set in the
+     * constructor or by calling setSize. The public interface also
+     * includes size which returns the number of recent records,
+     * count which is the number of records processed since the
+     * the last setSize, get which returns a record and
+     * add which adds a record.
+     */
+    private static class LogRecords {
+
+        private static final int DEFAULT_SIZE = 20;
+
+        private Vector<LogRec> mLogRecVector = new Vector<LogRec>();
+        private int mMaxSize = DEFAULT_SIZE;
+        private int mOldestIndex = 0;
+        private int mCount = 0;
+        private boolean mLogOnlyTransitions = false;
+
+        /**
+         * private constructor use add
+         */
+        private LogRecords() {
+        }
+
+        /**
+         * Set size of messages to maintain and clears all current records.
+         *
+         * @param maxSize number of records to maintain at anyone time.
+        */
+        synchronized void setSize(int maxSize) {
+            // TODO: once b/28217358 is fixed, add unit tests  to verify that these variables are
+            // cleared after calling this method, and that subsequent calls to get() function as
+            // expected.
+            mMaxSize = maxSize;
+            mOldestIndex = 0;
+            mCount = 0;
+            mLogRecVector.clear();
+        }
+
+        synchronized void setLogOnlyTransitions(boolean enable) {
+            mLogOnlyTransitions = enable;
+        }
+
+        synchronized boolean logOnlyTransitions() {
+            return mLogOnlyTransitions;
+        }
+
+        /**
+         * @return the number of recent records.
+         */
+        synchronized int size() {
+            return mLogRecVector.size();
+        }
+
+        /**
+         * @return the total number of records processed since size was set.
+         */
+        synchronized int count() {
+            return mCount;
+        }
+
+        /**
+         * Clear the list of records.
+         */
+        synchronized void cleanup() {
+            mLogRecVector.clear();
+        }
+
+        /**
+         * @return the information on a particular record. 0 is the oldest
+         * record and size()-1 is the newest record. If the index is to
+         * large null is returned.
+         */
+        synchronized LogRec get(int index) {
+            int nextIndex = mOldestIndex + index;
+            if (nextIndex >= mMaxSize) {
+                nextIndex -= mMaxSize;
+            }
+            if (nextIndex >= size()) {
+                return null;
+            } else {
+                return mLogRecVector.get(nextIndex);
+            }
+        }
+
+        /**
+         * Add a processed message.
+         *
+         * @param msg
+         * @param messageInfo to be stored
+         * @param state that handled the message
+         * @param orgState is the first state the received the message but
+         * did not processes the message.
+         * @param transToState is the state that was transitioned to after the message was
+         * processed.
+         *
+         */
+        synchronized void add(StateMachine sm, Message msg, String messageInfo, IState state,
+                IState orgState, IState transToState) {
+            mCount += 1;
+            if (mLogRecVector.size() < mMaxSize) {
+                mLogRecVector.add(new LogRec(sm, msg, messageInfo, state, orgState, transToState));
+            } else {
+                LogRec pmi = mLogRecVector.get(mOldestIndex);
+                mOldestIndex += 1;
+                if (mOldestIndex >= mMaxSize) {
+                    mOldestIndex = 0;
+                }
+                pmi.update(sm, msg, messageInfo, state, orgState, transToState);
+            }
+        }
+    }
+
+    private static class SmHandler extends Handler {
+
+        /** true if StateMachine has quit */
+        private boolean mHasQuit = false;
+
+        /** The debug flag */
+        private boolean mDbg = false;
+
+        /** The SmHandler object, identifies that message is internal */
+        private static final Object mSmHandlerObj = new Object();
+
+        /** The current message */
+        private Message mMsg;
+
+        /** A list of log records including messages this state machine has processed */
+        private LogRecords mLogRecords = new LogRecords();
+
+        /** true if construction of the state machine has not been completed */
+        private boolean mIsConstructionCompleted;
+
+        /** Stack used to manage the current hierarchy of states */
+        private StateInfo mStateStack[];
+
+        /** Top of mStateStack */
+        private int mStateStackTopIndex = -1;
+
+        /** A temporary stack used to manage the state stack */
+        private StateInfo mTempStateStack[];
+
+        /** The top of the mTempStateStack */
+        private int mTempStateStackCount;
+
+        /** State used when state machine is halted */
+        private HaltingState mHaltingState = new HaltingState();
+
+        /** State used when state machine is quitting */
+        private QuittingState mQuittingState = new QuittingState();
+
+        /** Reference to the StateMachine */
+        private StateMachine mSm;
+
+        /**
+         * Information about a state.
+         * Used to maintain the hierarchy.
+         */
+        private class StateInfo {
+            /** The state */
+            State state;
+
+            /** The parent of this state, null if there is no parent */
+            StateInfo parentStateInfo;
+
+            /** True when the state has been entered and on the stack */
+            boolean active;
+
+            /**
+             * Convert StateInfo to string
+             */
+            @Override
+            public String toString() {
+                return "state=" + state.getName() + ",active=" + active + ",parent="
+                        + ((parentStateInfo == null) ? "null" : parentStateInfo.state.getName());
+            }
+        }
+
+        /** The map of all of the states in the state machine */
+        private HashMap<State, StateInfo> mStateInfo = new HashMap<State, StateInfo>();
+
+        /** The initial state that will process the first message */
+        private State mInitialState;
+
+        /** The destination state when transitionTo has been invoked */
+        private State mDestState;
+
+        /**
+         * Indicates if a transition is in progress
+         *
+         * This will be true for all calls of State.exit and all calls of State.enter except for the
+         * last enter call for the current destination state.
+         */
+        private boolean mTransitionInProgress = false;
+
+        /** The list of deferred messages */
+        private ArrayList<Message> mDeferredMessages = new ArrayList<Message>();
+
+        /**
+         * State entered when transitionToHaltingState is called.
+         */
+        private class HaltingState extends State {
+            @Override
+            public boolean processMessage(Message msg) {
+                mSm.haltedProcessMessage(msg);
+                return true;
+            }
+        }
+
+        /**
+         * State entered when a valid quit message is handled.
+         */
+        private class QuittingState extends State {
+            @Override
+            public boolean processMessage(Message msg) {
+                return NOT_HANDLED;
+            }
+        }
+
+        /**
+         * Handle messages sent to the state machine by calling
+         * the current state's processMessage. It also handles
+         * the enter/exit calls and placing any deferred messages
+         * back onto the queue when transitioning to a new state.
+         */
+        @Override
+        public final void handleMessage(Message msg) {
+            if (!mHasQuit) {
+                if (mSm != null && msg.what != SM_INIT_CMD && msg.what != SM_QUIT_CMD) {
+                    mSm.onPreHandleMessage(msg);
+                }
+
+                if (mDbg) mSm.log("handleMessage: E msg.what=" + msg.what);
+
+                /** Save the current message */
+                mMsg = msg;
+
+                /** State that processed the message */
+                State msgProcessedState = null;
+                if (mIsConstructionCompleted) {
+                    /** Normal path */
+                    msgProcessedState = processMsg(msg);
+                } else if (!mIsConstructionCompleted && (mMsg.what == SM_INIT_CMD)
+                        && (mMsg.obj == mSmHandlerObj)) {
+                    /** Initial one time path. */
+                    mIsConstructionCompleted = true;
+                    invokeEnterMethods(0);
+                } else {
+                    throw new RuntimeException("StateMachine.handleMessage: "
+                            + "The start method not called, received msg: " + msg);
+                }
+                performTransitions(msgProcessedState, msg);
+
+                // We need to check if mSm == null here as we could be quitting.
+                if (mDbg && mSm != null) mSm.log("handleMessage: X");
+
+                if (mSm != null && msg.what != SM_INIT_CMD && msg.what != SM_QUIT_CMD) {
+                    mSm.onPostHandleMessage(msg);
+                }
+            }
+        }
+
+        /**
+         * Do any transitions
+         * @param msgProcessedState is the state that processed the message
+         */
+        private void performTransitions(State msgProcessedState, Message msg) {
+            /**
+             * If transitionTo has been called, exit and then enter
+             * the appropriate states. We loop on this to allow
+             * enter and exit methods to use transitionTo.
+             */
+            State orgState = mStateStack[mStateStackTopIndex].state;
+
+            /**
+             * Record whether message needs to be logged before we transition and
+             * and we won't log special messages SM_INIT_CMD or SM_QUIT_CMD which
+             * always set msg.obj to the handler.
+             */
+            boolean recordLogMsg = mSm.recordLogRec(mMsg) && (msg.obj != mSmHandlerObj);
+
+            if (mLogRecords.logOnlyTransitions()) {
+                /** Record only if there is a transition */
+                if (mDestState != null) {
+                    mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState,
+                            orgState, mDestState);
+                }
+            } else if (recordLogMsg) {
+                /** Record message */
+                mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState, orgState,
+                        mDestState);
+            }
+
+            State destState = mDestState;
+            if (destState != null) {
+                /**
+                 * Process the transitions including transitions in the enter/exit methods
+                 */
+                while (true) {
+                    if (mDbg) mSm.log("handleMessage: new destination call exit/enter");
+
+                    /**
+                     * Determine the states to exit and enter and return the
+                     * common ancestor state of the enter/exit states. Then
+                     * invoke the exit methods then the enter methods.
+                     */
+                    StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
+                    // flag is cleared in invokeEnterMethods before entering the target state
+                    mTransitionInProgress = true;
+                    invokeExitMethods(commonStateInfo);
+                    int stateStackEnteringIndex = moveTempStateStackToStateStack();
+                    invokeEnterMethods(stateStackEnteringIndex);
+
+                    /**
+                     * Since we have transitioned to a new state we need to have
+                     * any deferred messages moved to the front of the message queue
+                     * so they will be processed before any other messages in the
+                     * message queue.
+                     */
+                    moveDeferredMessageAtFrontOfQueue();
+
+                    if (destState != mDestState) {
+                        // A new mDestState so continue looping
+                        destState = mDestState;
+                    } else {
+                        // No change in mDestState so we're done
+                        break;
+                    }
+                }
+                mDestState = null;
+            }
+
+            /**
+             * After processing all transitions check and
+             * see if the last transition was to quit or halt.
+             */
+            if (destState != null) {
+                if (destState == mQuittingState) {
+                    /**
+                     * Call onQuitting to let subclasses cleanup.
+                     */
+                    mSm.onQuitting();
+                    cleanupAfterQuitting();
+                } else if (destState == mHaltingState) {
+                    /**
+                     * Call onHalting() if we've transitioned to the halting
+                     * state. All subsequent messages will be processed in
+                     * in the halting state which invokes haltedProcessMessage(msg);
+                     */
+                    mSm.onHalting();
+                }
+            }
+        }
+
+        /**
+         * Cleanup all the static variables and the looper after the SM has been quit.
+         */
+        private final void cleanupAfterQuitting() {
+            if (mSm.mSmThread != null) {
+                // If we made the thread then quit looper which stops the thread.
+                getLooper().quit();
+                mSm.mSmThread = null;
+            }
+
+            mSm.mSmHandler = null;
+            mSm = null;
+            mMsg = null;
+            mLogRecords.cleanup();
+            mStateStack = null;
+            mTempStateStack = null;
+            mStateInfo.clear();
+            mInitialState = null;
+            mDestState = null;
+            mDeferredMessages.clear();
+            mHasQuit = true;
+        }
+
+        /**
+         * Complete the construction of the state machine.
+         */
+        private final void completeConstruction() {
+            if (mDbg) mSm.log("completeConstruction: E");
+
+            /**
+             * Determine the maximum depth of the state hierarchy
+             * so we can allocate the state stacks.
+             */
+            int maxDepth = 0;
+            for (StateInfo si : mStateInfo.values()) {
+                int depth = 0;
+                for (StateInfo i = si; i != null; depth++) {
+                    i = i.parentStateInfo;
+                }
+                if (maxDepth < depth) {
+                    maxDepth = depth;
+                }
+            }
+            if (mDbg) mSm.log("completeConstruction: maxDepth=" + maxDepth);
+
+            mStateStack = new StateInfo[maxDepth];
+            mTempStateStack = new StateInfo[maxDepth];
+            setupInitialStateStack();
+
+            /** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
+            sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
+
+            if (mDbg) mSm.log("completeConstruction: X");
+        }
+
+        /**
+         * Process the message. If the current state doesn't handle
+         * it, call the states parent and so on. If it is never handled then
+         * call the state machines unhandledMessage method.
+         * @return the state that processed the message
+         */
+        private final State processMsg(Message msg) {
+            StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
+            if (mDbg) {
+                mSm.log("processMsg: " + curStateInfo.state.getName());
+            }
+
+            if (isQuit(msg)) {
+                transitionTo(mQuittingState);
+            } else {
+                while (!curStateInfo.state.processMessage(msg)) {
+                    /**
+                     * Not processed
+                     */
+                    curStateInfo = curStateInfo.parentStateInfo;
+                    if (curStateInfo == null) {
+                        /**
+                         * No parents left so it's not handled
+                         */
+                        mSm.unhandledMessage(msg);
+                        break;
+                    }
+                    if (mDbg) {
+                        mSm.log("processMsg: " + curStateInfo.state.getName());
+                    }
+                }
+            }
+            return (curStateInfo != null) ? curStateInfo.state : null;
+        }
+
+        /**
+         * Call the exit method for each state from the top of stack
+         * up to the common ancestor state.
+         */
+        private final void invokeExitMethods(StateInfo commonStateInfo) {
+            while ((mStateStackTopIndex >= 0)
+                    && (mStateStack[mStateStackTopIndex] != commonStateInfo)) {
+                State curState = mStateStack[mStateStackTopIndex].state;
+                if (mDbg) mSm.log("invokeExitMethods: " + curState.getName());
+                curState.exit();
+                mStateStack[mStateStackTopIndex].active = false;
+                mStateStackTopIndex -= 1;
+            }
+        }
+
+        /**
+         * Invoke the enter method starting at the entering index to top of state stack
+         */
+        private final void invokeEnterMethods(int stateStackEnteringIndex) {
+            for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
+                if (stateStackEnteringIndex == mStateStackTopIndex) {
+                    // Last enter state for transition
+                    mTransitionInProgress = false;
+                }
+                if (mDbg) mSm.log("invokeEnterMethods: " + mStateStack[i].state.getName());
+                mStateStack[i].state.enter();
+                mStateStack[i].active = true;
+            }
+            mTransitionInProgress = false; // ensure flag set to false if no methods called
+        }
+
+        /**
+         * Move the deferred message to the front of the message queue.
+         */
+        private final void moveDeferredMessageAtFrontOfQueue() {
+            /**
+             * The oldest messages on the deferred list must be at
+             * the front of the queue so start at the back, which
+             * as the most resent message and end with the oldest
+             * messages at the front of the queue.
+             */
+            for (int i = mDeferredMessages.size() - 1; i >= 0; i--) {
+                Message curMsg = mDeferredMessages.get(i);
+                if (mDbg) mSm.log("moveDeferredMessageAtFrontOfQueue; what=" + curMsg.what);
+                sendMessageAtFrontOfQueue(curMsg);
+            }
+            mDeferredMessages.clear();
+        }
+
+        /**
+         * Move the contents of the temporary stack to the state stack
+         * reversing the order of the items on the temporary stack as
+         * they are moved.
+         *
+         * @return index into mStateStack where entering needs to start
+         */
+        private final int moveTempStateStackToStateStack() {
+            int startingIndex = mStateStackTopIndex + 1;
+            int i = mTempStateStackCount - 1;
+            int j = startingIndex;
+            while (i >= 0) {
+                if (mDbg) mSm.log("moveTempStackToStateStack: i=" + i + ",j=" + j);
+                mStateStack[j] = mTempStateStack[i];
+                j += 1;
+                i -= 1;
+            }
+
+            mStateStackTopIndex = j - 1;
+            if (mDbg) {
+                mSm.log("moveTempStackToStateStack: X mStateStackTop=" + mStateStackTopIndex
+                        + ",startingIndex=" + startingIndex + ",Top="
+                        + mStateStack[mStateStackTopIndex].state.getName());
+            }
+            return startingIndex;
+        }
+
+        /**
+         * Setup the mTempStateStack with the states we are going to enter.
+         *
+         * This is found by searching up the destState's ancestors for a
+         * state that is already active i.e. StateInfo.active == true.
+         * The destStae and all of its inactive parents will be on the
+         * TempStateStack as the list of states to enter.
+         *
+         * @return StateInfo of the common ancestor for the destState and
+         * current state or null if there is no common parent.
+         */
+        private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
+            /**
+             * Search up the parent list of the destination state for an active
+             * state. Use a do while() loop as the destState must always be entered
+             * even if it is active. This can happen if we are exiting/entering
+             * the current state.
+             */
+            mTempStateStackCount = 0;
+            StateInfo curStateInfo = mStateInfo.get(destState);
+            do {
+                mTempStateStack[mTempStateStackCount++] = curStateInfo;
+                curStateInfo = curStateInfo.parentStateInfo;
+            } while ((curStateInfo != null) && !curStateInfo.active);
+
+            if (mDbg) {
+                mSm.log("setupTempStateStackWithStatesToEnter: X mTempStateStackCount="
+                        + mTempStateStackCount + ",curStateInfo: " + curStateInfo);
+            }
+            return curStateInfo;
+        }
+
+        /**
+         * Initialize StateStack to mInitialState.
+         */
+        private final void setupInitialStateStack() {
+            if (mDbg) {
+                mSm.log("setupInitialStateStack: E mInitialState=" + mInitialState.getName());
+            }
+
+            StateInfo curStateInfo = mStateInfo.get(mInitialState);
+            for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
+                mTempStateStack[mTempStateStackCount] = curStateInfo;
+                curStateInfo = curStateInfo.parentStateInfo;
+            }
+
+            // Empty the StateStack
+            mStateStackTopIndex = -1;
+
+            moveTempStateStackToStateStack();
+        }
+
+        /**
+         * @return current message
+         */
+        private final Message getCurrentMessage() {
+            return mMsg;
+        }
+
+        /**
+         * @return current state
+         */
+        private final IState getCurrentState() {
+            return mStateStack[mStateStackTopIndex].state;
+        }
+
+        /**
+         * Add a new state to the state machine. Bottom up addition
+         * of states is allowed but the same state may only exist
+         * in one hierarchy.
+         *
+         * @param state the state to add
+         * @param parent the parent of state
+         * @return stateInfo for this state
+         */
+        private final StateInfo addState(State state, State parent) {
+            if (mDbg) {
+                mSm.log("addStateInternal: E state=" + state.getName() + ",parent="
+                        + ((parent == null) ? "" : parent.getName()));
+            }
+            StateInfo parentStateInfo = null;
+            if (parent != null) {
+                parentStateInfo = mStateInfo.get(parent);
+                if (parentStateInfo == null) {
+                    // Recursively add our parent as it's not been added yet.
+                    parentStateInfo = addState(parent, null);
+                }
+            }
+            StateInfo stateInfo = mStateInfo.get(state);
+            if (stateInfo == null) {
+                stateInfo = new StateInfo();
+                mStateInfo.put(state, stateInfo);
+            }
+
+            // Validate that we aren't adding the same state in two different hierarchies.
+            if ((stateInfo.parentStateInfo != null)
+                    && (stateInfo.parentStateInfo != parentStateInfo)) {
+                throw new RuntimeException("state already added");
+            }
+            stateInfo.state = state;
+            stateInfo.parentStateInfo = parentStateInfo;
+            stateInfo.active = false;
+            if (mDbg) mSm.log("addStateInternal: X stateInfo: " + stateInfo);
+            return stateInfo;
+        }
+
+        /**
+         * Remove a state from the state machine. Will not remove the state if it is currently
+         * active or if it has any children in the hierarchy.
+         * @param state the state to remove
+         */
+        private void removeState(State state) {
+            StateInfo stateInfo = mStateInfo.get(state);
+            if (stateInfo == null || stateInfo.active) {
+                return;
+            }
+            boolean isParent = mStateInfo.values().stream()
+                    .filter(si -> si.parentStateInfo == stateInfo)
+                    .findAny()
+                    .isPresent();
+            if (isParent) {
+                return;
+            }
+            mStateInfo.remove(state);
+        }
+
+        /**
+         * Constructor
+         *
+         * @param looper for dispatching messages
+         * @param sm the hierarchical state machine
+         */
+        private SmHandler(Looper looper, StateMachine sm) {
+            super(looper);
+            mSm = sm;
+
+            addState(mHaltingState, null);
+            addState(mQuittingState, null);
+        }
+
+        /** @see StateMachine#setInitialState(State) */
+        private final void setInitialState(State initialState) {
+            if (mDbg) mSm.log("setInitialState: initialState=" + initialState.getName());
+            mInitialState = initialState;
+        }
+
+        /** @see StateMachine#transitionTo(IState) */
+        private final void transitionTo(IState destState) {
+            if (mTransitionInProgress) {
+                Log.wtf(mSm.mName, "transitionTo called while transition already in progress to " +
+                        mDestState + ", new target state=" + destState);
+            }
+            mDestState = (State) destState;
+            if (mDbg) mSm.log("transitionTo: destState=" + mDestState.getName());
+        }
+
+        /** @see StateMachine#deferMessage(Message) */
+        private final void deferMessage(Message msg) {
+            if (mDbg) mSm.log("deferMessage: msg=" + msg.what);
+
+            /* Copy the "msg" to "newMsg" as "msg" will be recycled */
+            Message newMsg = obtainMessage();
+            newMsg.copyFrom(msg);
+
+            mDeferredMessages.add(newMsg);
+        }
+
+        /** @see StateMachine#quit() */
+        private final void quit() {
+            if (mDbg) mSm.log("quit:");
+            sendMessage(obtainMessage(SM_QUIT_CMD, mSmHandlerObj));
+        }
+
+        /** @see StateMachine#quitNow() */
+        private final void quitNow() {
+            if (mDbg) mSm.log("quitNow:");
+            sendMessageAtFrontOfQueue(obtainMessage(SM_QUIT_CMD, mSmHandlerObj));
+        }
+
+        /** Validate that the message was sent by quit or quitNow. */
+        private final boolean isQuit(Message msg) {
+            return (msg.what == SM_QUIT_CMD) && (msg.obj == mSmHandlerObj);
+        }
+
+        /** @see StateMachine#isDbg() */
+        private final boolean isDbg() {
+            return mDbg;
+        }
+
+        /** @see StateMachine#setDbg(boolean) */
+        private final void setDbg(boolean dbg) {
+            mDbg = dbg;
+        }
+
+    }
+
+    private SmHandler mSmHandler;
+    private HandlerThread mSmThread;
+
+    /**
+     * Initialize.
+     *
+     * @param looper for this state machine
+     * @param name of the state machine
+     */
+    private void initStateMachine(String name, Looper looper) {
+        mName = name;
+        mSmHandler = new SmHandler(looper, this);
+    }
+
+    /**
+     * Constructor creates a StateMachine with its own thread.
+     *
+     * @param name of the state machine
+     */
+    protected StateMachine(String name) {
+        mSmThread = new HandlerThread(name);
+        mSmThread.start();
+        Looper looper = mSmThread.getLooper();
+
+        initStateMachine(name, looper);
+    }
+
+    /**
+     * Constructor creates a StateMachine using the looper.
+     *
+     * @param name of the state machine
+     */
+    protected StateMachine(String name, Looper looper) {
+        initStateMachine(name, looper);
+    }
+
+    /**
+     * Constructor creates a StateMachine using the handler.
+     *
+     * @param name of the state machine
+     */
+    protected StateMachine(String name, Handler handler) {
+        initStateMachine(name, handler.getLooper());
+    }
+
+    /**
+     * Notifies subclass that the StateMachine handler is about to process the Message msg
+     * @param msg The message that is being handled
+     */
+    protected void onPreHandleMessage(Message msg) {
+    }
+
+    /**
+     * Notifies subclass that the StateMachine handler has finished processing the Message msg and
+     * has possibly transitioned to a new state.
+     * @param msg The message that is being handled
+     */
+    protected void onPostHandleMessage(Message msg) {
+    }
+
+    /**
+     * Add a new state to the state machine
+     * @param state the state to add
+     * @param parent the parent of state
+     */
+    public final void addState(State state, State parent) {
+        mSmHandler.addState(state, parent);
+    }
+
+    /**
+     * Add a new state to the state machine, parent will be null
+     * @param state to add
+     */
+    public final void addState(State state) {
+        mSmHandler.addState(state, null);
+    }
+
+    /**
+     * Removes a state from the state machine, unless it is currently active or if it has children.
+     * @param state state to remove
+     */
+    public final void removeState(State state) {
+        mSmHandler.removeState(state);
+    }
+
+    /**
+     * Set the initial state. This must be invoked before
+     * and messages are sent to the state machine.
+     *
+     * @param initialState is the state which will receive the first message.
+     */
+    public final void setInitialState(State initialState) {
+        mSmHandler.setInitialState(initialState);
+    }
+
+    /**
+     * @return current message
+     */
+    public final Message getCurrentMessage() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return null;
+        return smh.getCurrentMessage();
+    }
+
+    /**
+     * @return current state
+     */
+    public final IState getCurrentState() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return null;
+        return smh.getCurrentState();
+    }
+
+    /**
+     * transition to destination state. Upon returning
+     * from processMessage the current state's exit will
+     * be executed and upon the next message arriving
+     * destState.enter will be invoked.
+     *
+     * this function can also be called inside the enter function of the
+     * previous transition target, but the behavior is undefined when it is
+     * called mid-way through a previous transition (for example, calling this
+     * in the enter() routine of a intermediate node when the current transition
+     * target is one of the nodes descendants).
+     *
+     * @param destState will be the state that receives the next message.
+     */
+    public final void transitionTo(IState destState) {
+        mSmHandler.transitionTo(destState);
+    }
+
+    /**
+     * transition to halt state. Upon returning
+     * from processMessage we will exit all current
+     * states, execute the onHalting() method and then
+     * for all subsequent messages haltedProcessMessage
+     * will be called.
+     */
+    public final void transitionToHaltingState() {
+        mSmHandler.transitionTo(mSmHandler.mHaltingState);
+    }
+
+    /**
+     * Defer this message until next state transition.
+     * Upon transitioning all deferred messages will be
+     * placed on the queue and reprocessed in the original
+     * order. (i.e. The next state the oldest messages will
+     * be processed first)
+     *
+     * @param msg is deferred until the next transition.
+     */
+    public final void deferMessage(Message msg) {
+        mSmHandler.deferMessage(msg);
+    }
+
+    /**
+     * Called when message wasn't handled
+     *
+     * @param msg that couldn't be handled.
+     */
+    protected void unhandledMessage(Message msg) {
+        if (mSmHandler.mDbg) loge(" - unhandledMessage: msg.what=" + msg.what);
+    }
+
+    /**
+     * Called for any message that is received after
+     * transitionToHalting is called.
+     */
+    protected void haltedProcessMessage(Message msg) {
+    }
+
+    /**
+     * This will be called once after handling a message that called
+     * transitionToHalting. All subsequent messages will invoke
+     * {@link StateMachine#haltedProcessMessage(Message)}
+     */
+    protected void onHalting() {
+    }
+
+    /**
+     * This will be called once after a quit message that was NOT handled by
+     * the derived StateMachine. The StateMachine will stop and any subsequent messages will be
+     * ignored. In addition, if this StateMachine created the thread, the thread will
+     * be stopped after this method returns.
+     */
+    protected void onQuitting() {
+    }
+
+    /**
+     * @return the name
+     */
+    public final String getName() {
+        return mName;
+    }
+
+    /**
+     * Set number of log records to maintain and clears all current records.
+     *
+     * @param maxSize number of messages to maintain at anyone time.
+     */
+    public final void setLogRecSize(int maxSize) {
+        mSmHandler.mLogRecords.setSize(maxSize);
+    }
+
+    /**
+     * Set to log only messages that cause a state transition
+     *
+     * @param enable {@code true} to enable, {@code false} to disable
+     */
+    public final void setLogOnlyTransitions(boolean enable) {
+        mSmHandler.mLogRecords.setLogOnlyTransitions(enable);
+    }
+
+    /**
+     * @return the number of log records currently readable
+     */
+    public final int getLogRecSize() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return 0;
+        return smh.mLogRecords.size();
+    }
+
+    /**
+     * @return the number of log records we can store
+     */
+    @VisibleForTesting
+    public final int getLogRecMaxSize() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return 0;
+        return smh.mLogRecords.mMaxSize;
+    }
+
+    /**
+     * @return the total number of records processed
+     */
+    public final int getLogRecCount() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return 0;
+        return smh.mLogRecords.count();
+    }
+
+    /**
+     * @return a log record, or null if index is out of range
+     */
+    public final LogRec getLogRec(int index) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return null;
+        return smh.mLogRecords.get(index);
+    }
+
+    /**
+     * @return a copy of LogRecs as a collection
+     */
+    public final Collection<LogRec> copyLogRecs() {
+        Vector<LogRec> vlr = new Vector<LogRec>();
+        SmHandler smh = mSmHandler;
+        if (smh != null) {
+            for (LogRec lr : smh.mLogRecords.mLogRecVector) {
+                vlr.add(lr);
+            }
+        }
+        return vlr;
+    }
+
+    /**
+     * Add the string to LogRecords.
+     *
+     * @param string
+     */
+    public void addLogRec(String string) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+        smh.mLogRecords.add(this, smh.getCurrentMessage(), string, smh.getCurrentState(),
+                smh.mStateStack[smh.mStateStackTopIndex].state, smh.mDestState);
+    }
+
+    /**
+     * @return true if msg should be saved in the log, default is true.
+     */
+    protected boolean recordLogRec(Message msg) {
+        return true;
+    }
+
+    /**
+     * Return a string to be logged by LogRec, default
+     * is an empty string. Override if additional information is desired.
+     *
+     * @param msg that was processed
+     * @return information to be logged as a String
+     */
+    protected String getLogRecString(Message msg) {
+        return "";
+    }
+
+    /**
+     * @return the string for msg.what
+     */
+    protected String getWhatToString(int what) {
+        return null;
+    }
+
+    /**
+     * @return Handler, maybe null if state machine has quit.
+     */
+    public final Handler getHandler() {
+        return mSmHandler;
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler.
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage() {
+        return Message.obtain(mSmHandler);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler, what.
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what is the assigned to Message.what.
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what) {
+        return Message.obtain(mSmHandler, what);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what and obj.
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what is the assigned to Message.what.
+     * @param obj is assigned to Message.obj.
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what, Object obj) {
+        return Message.obtain(mSmHandler, what, obj);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what, arg1 and arg2
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what  is assigned to Message.what
+     * @param arg1  is assigned to Message.arg1
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what, int arg1) {
+        // use this obtain so we don't match the obtain(h, what, Object) method
+        return Message.obtain(mSmHandler, what, arg1, 0);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what, arg1 and arg2
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what  is assigned to Message.what
+     * @param arg1  is assigned to Message.arg1
+     * @param arg2  is assigned to Message.arg2
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what, int arg1, int arg2) {
+        return Message.obtain(mSmHandler, what, arg1, arg2);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what, arg1, arg2 and obj
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what  is assigned to Message.what
+     * @param arg1  is assigned to Message.arg1
+     * @param arg2  is assigned to Message.arg2
+     * @param obj is assigned to Message.obj
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what, int arg1, int arg2, Object obj) {
+        return Message.obtain(mSmHandler, what, arg1, arg2, obj);
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(int what) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(int what, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, obj));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(int what, int arg1) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, arg1));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(int what, int arg1, int arg2) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, arg1, arg2));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(int what, int arg1, int arg2, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, arg1, arg2, obj));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(Message msg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(msg);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, Object obj, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, obj), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, int arg1, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, arg1), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, int arg1, int arg2, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, arg1, arg2), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, int arg1, int arg2, Object obj,
+            long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, arg1, arg2, obj), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(Message msg, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(msg, delayMillis);
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, obj));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, int arg1) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1));
+    }
+
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, int arg1, int arg2) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1, arg2));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, int arg1, int arg2, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1, arg2, obj));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(Message msg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(msg);
+    }
+
+    /**
+     * Removes a message from the message queue.
+     * Protected, may only be called by instances of StateMachine.
+     */
+    protected final void removeMessages(int what) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.removeMessages(what);
+    }
+
+    /**
+     * Removes a message from the deferred messages queue.
+     */
+    protected final void removeDeferredMessages(int what) {
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        Iterator<Message> iterator = smh.mDeferredMessages.iterator();
+        while (iterator.hasNext()) {
+            Message msg = iterator.next();
+            if (msg.what == what) iterator.remove();
+        }
+    }
+
+    /**
+     * Check if there are any pending messages with code 'what' in deferred messages queue.
+     */
+    protected final boolean hasDeferredMessages(int what) {
+        SmHandler smh = mSmHandler;
+        if (smh == null) return false;
+
+        Iterator<Message> iterator = smh.mDeferredMessages.iterator();
+        while (iterator.hasNext()) {
+            Message msg = iterator.next();
+            if (msg.what == what) return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if there are any pending posts of messages with code 'what' in
+     * the message queue. This does NOT check messages in deferred message queue.
+     */
+    protected final boolean hasMessages(int what) {
+        SmHandler smh = mSmHandler;
+        if (smh == null) return false;
+
+        return smh.hasMessages(what);
+    }
+
+    /**
+     * Validate that the message was sent by
+     * {@link StateMachine#quit} or {@link StateMachine#quitNow}.
+     * */
+    protected final boolean isQuit(Message msg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return msg.what == SM_QUIT_CMD;
+
+        return smh.isQuit(msg);
+    }
+
+    /**
+     * Quit the state machine after all currently queued up messages are processed.
+     */
+    public final void quit() {
+        // mSmHandler can be null if the state machine is already stopped.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.quit();
+    }
+
+    /**
+     * Quit the state machine immediately all currently queued messages will be discarded.
+     */
+    public final void quitNow() {
+        // mSmHandler can be null if the state machine is already stopped.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.quitNow();
+    }
+
+    /**
+     * @return if debugging is enabled
+     */
+    public boolean isDbg() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return false;
+
+        return smh.isDbg();
+    }
+
+    /**
+     * Set debug enable/disabled.
+     *
+     * @param dbg is true to enable debugging.
+     */
+    public void setDbg(boolean dbg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.setDbg(dbg);
+    }
+
+    /**
+     * Start the state machine.
+     */
+    public void start() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        /** Send the complete construction message */
+        smh.completeConstruction();
+    }
+
+    /**
+     * Dump the current state.
+     *
+     * @param fd
+     * @param pw
+     * @param args
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(getName() + ":");
+        pw.println(" total records=" + getLogRecCount());
+        for (int i = 0; i < getLogRecSize(); i++) {
+            pw.println(" rec[" + i + "]: " + getLogRec(i).toString());
+            pw.flush();
+        }
+        pw.println("curState=" + getCurrentState().getName());
+    }
+
+    @Override
+    public String toString() {
+        String name = "(null)";
+        String state = "(null)";
+        try {
+            name = mName.toString();
+            state = mSmHandler.getCurrentState().getName().toString();
+        } catch (NullPointerException npe) {
+            // Will use default(s) initialized above.
+        }
+        return "name=" + name + " state=" + state;
+    }
+
+    /**
+     * Log with debug and add to the LogRecords.
+     *
+     * @param s is string log
+     */
+    protected void logAndAddLogRec(String s) {
+        addLogRec(s);
+        log(s);
+    }
+
+    /**
+     * Log with debug
+     *
+     * @param s is string log
+     */
+    protected void log(String s) {
+        Log.d(mName, s);
+    }
+
+    /**
+     * Log with debug attribute
+     *
+     * @param s is string log
+     */
+    protected void logd(String s) {
+        Log.d(mName, s);
+    }
+
+    /**
+     * Log with verbose attribute
+     *
+     * @param s is string log
+     */
+    protected void logv(String s) {
+        Log.v(mName, s);
+    }
+
+    /**
+     * Log with info attribute
+     *
+     * @param s is string log
+     */
+    protected void logi(String s) {
+        Log.i(mName, s);
+    }
+
+    /**
+     * Log with warning attribute
+     *
+     * @param s is string log
+     */
+    protected void logw(String s) {
+        Log.w(mName, s);
+    }
+
+    /**
+     * Log with error attribute
+     *
+     * @param s is string log
+     */
+    protected void loge(String s) {
+        Log.e(mName, s);
+    }
+
+    /**
+     * Log with error attribute
+     *
+     * @param s is string log
+     * @param e is a Throwable which logs additional information.
+     */
+    protected void loge(String s, Throwable e) {
+        Log.e(mName, s, e);
+    }
+}
diff --git a/com/android/internal/util/ToBooleanFunction.java b/com/android/internal/util/ToBooleanFunction.java
new file mode 100644
index 0000000..83866c2
--- /dev/null
+++ b/com/android/internal/util/ToBooleanFunction.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import java.util.function.Function;
+
+/**
+ * Represents a function that produces an boolean-valued result.  This is the
+ * {@code boolean}-producing primitive specialization for {@link Function}.
+ *
+ * <p>This is a <a href="package-summary.html">functional interface</a>
+ * whose functional method is {@link #apply(Object)}.
+ *
+ * @param <T> the type of the input to the function
+ *
+ * @see Function
+ * @since 1.8
+ */
+@FunctionalInterface
+public interface ToBooleanFunction<T> {
+
+    /**
+     * Applies this function to the given argument.
+     *
+     * @param value the function argument
+     * @return the function result
+     */
+    boolean apply(T value);
+}
diff --git a/com/android/internal/util/TokenBucket.java b/com/android/internal/util/TokenBucket.java
new file mode 100644
index 0000000..a163ceb
--- /dev/null
+++ b/com/android/internal/util/TokenBucket.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import android.os.SystemClock;
+
+import static com.android.internal.util.Preconditions.checkArgumentNonnegative;
+import static com.android.internal.util.Preconditions.checkArgumentPositive;
+
+/**
+ * A class useful for rate-limiting or throttling that stores and distributes tokens.
+ *
+ * A TokenBucket starts with a fixed capacity of tokens, an initial amount of tokens, and
+ * a fixed filling period (in milliseconds).
+ *
+ * For every filling period, the bucket gains one token, up to its maximum capacity from
+ * which point tokens simply overflow and are lost. Tokens can be obtained one by one or n by n.
+ *
+ * The available amount of tokens is computed lazily when the bucket state is inspected.
+ * Therefore it is purely synchronous and does not involve any asynchronous activity.
+ * It is not synchronized in any way and not a thread-safe object.
+ *
+ * {@hide}
+ */
+public class TokenBucket {
+
+    private final int mFillDelta; // Time in ms it takes to generate one token.
+    private final int mCapacity;  // Maximum number of tokens that can be stored.
+    private long mLastFill;       // Last time in ms the bucket generated tokens.
+    private int mAvailable;       // Current number of available tokens.
+
+    /**
+     * Create a new TokenBucket.
+     * @param deltaMs the time in milliseconds it takes to generate a new token.
+     * Must be strictly positive.
+     * @param capacity the maximum token capacity. Must be strictly positive.
+     * @param tokens the starting amount of token. Must be positive or zero.
+     */
+    public TokenBucket(int deltaMs, int capacity, int tokens) {
+        mFillDelta = checkArgumentPositive(deltaMs, "deltaMs must be strictly positive");
+        mCapacity = checkArgumentPositive(capacity, "capacity must be strictly positive");
+        mAvailable = Math.min(checkArgumentNonnegative(tokens), mCapacity);
+        mLastFill = scaledTime();
+    }
+
+    /**
+     * Create a new TokenBucket that starts completely filled.
+     * @param deltaMs the time in milliseconds it takes to generate a new token.
+     * Must be strictly positive.
+     * @param capacity the maximum token capacity. Must be strictly positive.
+     */
+    public TokenBucket(int deltaMs, int capacity) {
+        this(deltaMs, capacity, capacity);
+    }
+
+    /** Reset this TokenBucket and set its number of available tokens. */
+    public void reset(int tokens) {
+        checkArgumentNonnegative(tokens);
+        mAvailable = Math.min(tokens, mCapacity);
+        mLastFill = scaledTime();
+    }
+
+    /** Returns this TokenBucket maximum token capacity. */
+    public int capacity() {
+        return mCapacity;
+    }
+
+    /** Returns this TokenBucket currently number of available tokens. */
+    public int available() {
+        fill();
+        return mAvailable;
+    }
+
+    /** Returns true if this TokenBucket as one or more tokens available. */
+    public boolean has() {
+        fill();
+        return mAvailable > 0;
+    }
+
+    /** Consumes a token from this TokenBucket and returns true if a token is available. */
+    public boolean get() {
+        return (get(1) == 1);
+    }
+
+    /**
+     * Try to consume many tokens from this TokenBucket.
+     * @param n the number of tokens to consume.
+     * @return the number of tokens that were actually consumed.
+     */
+    public int get(int n) {
+        fill();
+        if (n <= 0) {
+            return 0;
+        }
+        if (n > mAvailable) {
+            int got = mAvailable;
+            mAvailable = 0;
+            return got;
+        }
+        mAvailable -= n;
+        return n;
+    }
+
+    private void fill() {
+        final long now = scaledTime();
+        final int diff = (int) (now - mLastFill);
+        mAvailable = Math.min(mCapacity, mAvailable + diff);
+        mLastFill = now;
+    }
+
+    private long scaledTime() {
+        return SystemClock.elapsedRealtime() / mFillDelta;
+    }
+}
diff --git a/com/android/internal/util/TypedProperties.java b/com/android/internal/util/TypedProperties.java
new file mode 100644
index 0000000..5613999
--- /dev/null
+++ b/com/android/internal/util/TypedProperties.java
@@ -0,0 +1,714 @@
+/*
+ * 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.internal.util;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * A {@code Map} that publishes a set of typed properties, defined by
+ * zero or more {@code Reader}s containing textual definitions and assignments.
+ */
+public class TypedProperties extends HashMap<String, Object> {
+    /**
+     * Instantiates a {@link java.io.StreamTokenizer} and sets its syntax tables
+     * appropriately for the {@code TypedProperties} file format.
+     *
+     * @param r The {@code Reader} that the {@code StreamTokenizer} will read from
+     * @return a newly-created and initialized {@code StreamTokenizer}
+     */
+    static StreamTokenizer initTokenizer(Reader r) {
+        StreamTokenizer st = new StreamTokenizer(r);
+
+        // Treat everything we don't specify as "ordinary".
+        st.resetSyntax();
+
+        /* The only non-quoted-string words we'll be reading are:
+         * - property names: [._$a-zA-Z0-9]
+         * - type names: [a-zS]
+         * - number literals: [-0-9.eExXA-Za-z]  ('x' for 0xNNN hex literals. "NaN", "Infinity")
+         * - "true" or "false" (case insensitive): [a-zA-Z]
+         */
+        st.wordChars('0', '9');
+        st.wordChars('A', 'Z');
+        st.wordChars('a', 'z');
+        st.wordChars('_', '_');
+        st.wordChars('$', '$');
+        st.wordChars('.', '.');
+        st.wordChars('-', '-');
+        st.wordChars('+', '+');
+
+        // Single-character tokens
+        st.ordinaryChar('=');
+
+        // Other special characters
+        st.whitespaceChars(' ', ' ');
+        st.whitespaceChars('\t', '\t');
+        st.whitespaceChars('\n', '\n');
+        st.whitespaceChars('\r', '\r');
+        st.quoteChar('"');
+
+        // Java-style comments
+        st.slashStarComments(true);
+        st.slashSlashComments(true);
+
+        return st;
+    }
+
+
+    /**
+     * An unchecked exception that is thrown when encountering a syntax
+     * or semantic error in the input.
+     */
+    public static class ParseException extends IllegalArgumentException {
+        ParseException(StreamTokenizer state, String expected) {
+            super("expected " + expected + ", saw " + state.toString());
+        }
+    }
+
+    // A sentinel instance used to indicate a null string.
+    static final String NULL_STRING = new String("<TypedProperties:NULL_STRING>");
+
+    // Constants used to represent the supported types.
+    static final int TYPE_UNSET = 'x';
+    static final int TYPE_BOOLEAN = 'Z';
+    static final int TYPE_BYTE = 'I' | 1 << 8;
+    // TYPE_CHAR: character literal syntax not supported; use short.
+    static final int TYPE_SHORT = 'I' | 2 << 8;
+    static final int TYPE_INT = 'I' | 4 << 8;
+    static final int TYPE_LONG = 'I' | 8 << 8;
+    static final int TYPE_FLOAT = 'F' | 4 << 8;
+    static final int TYPE_DOUBLE = 'F' | 8 << 8;
+    static final int TYPE_STRING = 'L' | 's' << 8;
+    static final int TYPE_ERROR = -1;
+
+    /**
+     * Converts a string to an internal type constant.
+     *
+     * @param typeName the type name to convert
+     * @return the type constant that corresponds to {@code typeName},
+     *         or {@code TYPE_ERROR} if the type is unknown
+     */
+    static int interpretType(String typeName) {
+        if ("unset".equals(typeName)) {
+            return TYPE_UNSET;
+        } else if ("boolean".equals(typeName)) {
+            return TYPE_BOOLEAN;
+        } else if ("byte".equals(typeName)) {
+            return TYPE_BYTE;
+        } else if ("short".equals(typeName)) {
+            return TYPE_SHORT;
+        } else if ("int".equals(typeName)) {
+            return TYPE_INT;
+        } else if ("long".equals(typeName)) {
+            return TYPE_LONG;
+        } else if ("float".equals(typeName)) {
+            return TYPE_FLOAT;
+        } else if ("double".equals(typeName)) {
+            return TYPE_DOUBLE;
+        } else if ("String".equals(typeName)) {
+            return TYPE_STRING;
+        }
+        return TYPE_ERROR;
+    }
+
+    /**
+     * Parses the data in the reader.
+     *
+     * @param r The {@code Reader} containing input data to parse
+     * @param map The {@code Map} to insert parameter values into
+     * @throws ParseException if the input data is malformed
+     * @throws IOException if there is a problem reading from the {@code Reader}
+     */
+    static void parse(Reader r, Map<String, Object> map) throws ParseException, IOException {
+        final StreamTokenizer st = initTokenizer(r);
+
+        /* A property name must be a valid fully-qualified class + package name.
+         * We don't support Unicode, though.
+         */
+        final String identifierPattern = "[a-zA-Z_$][0-9a-zA-Z_$]*";
+        final Pattern propertyNamePattern =
+            Pattern.compile("(" + identifierPattern + "\\.)*" + identifierPattern);
+
+
+        while (true) {
+            int token;
+
+            // Read the next token, which is either the type or EOF.
+            token = st.nextToken();
+            if (token == StreamTokenizer.TT_EOF) {
+                break;
+            }
+            if (token != StreamTokenizer.TT_WORD) {
+                throw new ParseException(st, "type name");
+            }
+            final int type = interpretType(st.sval);
+            if (type == TYPE_ERROR) {
+                throw new ParseException(st, "valid type name");
+            }
+            st.sval = null;
+
+            if (type == TYPE_UNSET) {
+                // Expect '('.
+                token = st.nextToken();
+                if (token != '(') {
+                    throw new ParseException(st, "'('");
+                }
+            }
+
+            // Read the property name.
+            token = st.nextToken();
+            if (token != StreamTokenizer.TT_WORD) {
+                throw new ParseException(st, "property name");
+            }
+            final String propertyName = st.sval;
+            if (!propertyNamePattern.matcher(propertyName).matches()) {
+                throw new ParseException(st, "valid property name");
+            }
+            st.sval = null;
+
+            if (type == TYPE_UNSET) {
+                // Expect ')'.
+                token = st.nextToken();
+                if (token != ')') {
+                    throw new ParseException(st, "')'");
+                }
+                map.remove(propertyName);
+            } else {
+                // Expect '='.
+                token = st.nextToken();
+                if (token != '=') {
+                    throw new ParseException(st, "'='");
+                }
+
+                // Read a value of the appropriate type, and insert into the map.
+                final Object value = parseValue(st, type);
+                final Object oldValue = map.remove(propertyName);
+                if (oldValue != null) {
+                    // TODO: catch the case where a string is set to null and then
+                    //       the same property is defined with a different type.
+                    if (value.getClass() != oldValue.getClass()) {
+                        throw new ParseException(st,
+                            "(property previously declared as a different type)");
+                    }
+                }
+                map.put(propertyName, value);
+            }
+
+            // Expect ';'.
+            token = st.nextToken();
+            if (token != ';') {
+                throw new ParseException(st, "';'");
+            }
+        }
+    }
+
+    /**
+     * Parses the next token in the StreamTokenizer as the specified type.
+     *
+     * @param st The token source
+     * @param type The type to interpret next token as
+     * @return a Boolean, Number subclass, or String representing the value.
+     *         Null strings are represented by the String instance NULL_STRING
+     * @throws IOException if there is a problem reading from the {@code StreamTokenizer}
+     */
+    static Object parseValue(StreamTokenizer st, final int type) throws IOException {
+        final int token = st.nextToken();
+
+        if (type == TYPE_BOOLEAN) {
+            if (token != StreamTokenizer.TT_WORD) {
+                throw new ParseException(st, "boolean constant");
+            }
+
+            if ("true".equals(st.sval)) {
+                return Boolean.TRUE;
+            } else if ("false".equals(st.sval)) {
+                return Boolean.FALSE;
+            }
+
+            throw new ParseException(st, "boolean constant");
+        } else if ((type & 0xff) == 'I') {
+            if (token != StreamTokenizer.TT_WORD) {
+                throw new ParseException(st, "integer constant");
+            }
+
+            /* Parse the string.  Long.decode() handles C-style integer constants
+             * ("0x" -> hex, "0" -> octal).  It also treats numbers with a prefix of "#" as
+             * hex, but our syntax intentionally does not list '#' as a word character.
+             */
+            long value;
+            try {
+                value = Long.decode(st.sval);
+            } catch (NumberFormatException ex) {
+                throw new ParseException(st, "integer constant");
+            }
+
+            // Ensure that the type can hold this value, and return.
+            int width = (type >> 8) & 0xff;
+            switch (width) {
+            case 1:
+                if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
+                    throw new ParseException(st, "8-bit integer constant");
+                }
+                return new Byte((byte)value);
+            case 2:
+                if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) {
+                    throw new ParseException(st, "16-bit integer constant");
+                }
+                return new Short((short)value);
+            case 4:
+                if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
+                    throw new ParseException(st, "32-bit integer constant");
+                }
+                return new Integer((int)value);
+            case 8:
+                if (value < Long.MIN_VALUE || value > Long.MAX_VALUE) {
+                    throw new ParseException(st, "64-bit integer constant");
+                }
+                return new Long(value);
+            default:
+                throw new IllegalStateException(
+                    "Internal error; unexpected integer type width " + width);
+            }
+        } else if ((type & 0xff) == 'F') {
+            if (token != StreamTokenizer.TT_WORD) {
+                throw new ParseException(st, "float constant");
+            }
+
+            // Parse the string.
+            /* TODO: Maybe just parse as float or double, losing precision if necessary.
+             *       Parsing as double and converting to float can change the value
+             *       compared to just parsing as float.
+             */
+            double value;
+            try {
+                /* TODO: detect if the string representation loses precision
+                 *       when being converted to a double.
+                 */
+                value = Double.parseDouble(st.sval);
+            } catch (NumberFormatException ex) {
+                throw new ParseException(st, "float constant");
+            }
+
+            // Ensure that the type can hold this value, and return.
+            if (((type >> 8) & 0xff) == 4) {
+                // This property is a float; make sure the value fits.
+                double absValue = Math.abs(value);
+                if (absValue != 0.0 && !Double.isInfinite(value) && !Double.isNaN(value)) {
+                    if (absValue < Float.MIN_VALUE || absValue > Float.MAX_VALUE) {
+                        throw new ParseException(st, "32-bit float constant");
+                    }
+                }
+                return new Float((float)value);
+            } else {
+                // This property is a double; no need to truncate.
+                return new Double(value);
+            }
+        } else if (type == TYPE_STRING) {
+            // Expect a quoted string or the word "null".
+            if (token == '"') {
+                return st.sval;
+            } else if (token == StreamTokenizer.TT_WORD && "null".equals(st.sval)) {
+                return NULL_STRING;
+            }
+            throw new ParseException(st, "double-quoted string or 'null'");
+        }
+
+        throw new IllegalStateException("Internal error; unknown type " + type);
+    }
+
+
+    /**
+     * Creates an empty TypedProperties instance.
+     */
+    public TypedProperties() {
+        super();
+    }
+
+    /**
+     * Loads zero or more properties from the specified Reader.
+     * Properties that have already been loaded are preserved unless
+     * the new Reader overrides or unsets earlier values for the
+     * same properties.
+     * <p>
+     * File syntax:
+     * <blockquote>
+     *     <tt>
+     *     &lt;type&gt; &lt;property-name&gt; = &lt;value&gt; ;
+     *     <br />
+     *     unset ( &lt;property-name&gt; ) ;
+     *     </tt>
+     *     <p>
+     *     "//" comments everything until the end of the line.
+     *     "/&#2a;" comments everything until the next appearance of "&#2a;/".
+     *     <p>
+     *     Blank lines are ignored.
+     *     <p>
+     *     The only required whitespace is between the type and
+     *     the property name.
+     *     <p>
+     *     &lt;type&gt; is one of {boolean, byte, short, int, long,
+     *     float, double, String}, and is case-sensitive.
+     *     <p>
+     *     &lt;property-name&gt; is a valid fully-qualified class name
+     *     (one or more valid identifiers separated by dot characters).
+     *     <p>
+     *     &lt;value&gt; depends on the type:
+     *     <ul>
+     *     <li> boolean: one of {true, false} (case-sensitive)
+     *     <li> byte, short, int, long: a valid Java integer constant
+     *          (including non-base-10 constants like 0xabc and 074)
+     *          whose value does not overflow the type.  NOTE: these are
+     *          interpreted as Java integer values, so they are all signed.
+     *     <li> float, double: a valid Java floating-point constant.
+     *          If the type is float, the value must fit in 32 bits.
+     *     <li> String: a double-quoted string value, or the word {@code null}.
+     *          NOTE: the contents of the string must be 7-bit clean ASCII;
+     *          C-style octal escapes are recognized, but Unicode escapes are not.
+     *     </ul>
+     *     <p>
+     *     Passing a property-name to {@code unset()} will unset the property,
+     *     removing its value and type information, as if it had never been
+     *     defined.
+     * </blockquote>
+     *
+     * @param r The Reader to load properties from
+     * @throws IOException if an error occurs when reading the data
+     * @throws IllegalArgumentException if the data is malformed
+     */
+    public void load(Reader r) throws IOException {
+        parse(r, this);
+    }
+
+    @Override
+    public Object get(Object key) {
+        Object value = super.get(key);
+        if (value == NULL_STRING) {
+            return null;
+        }
+        return value;
+    }
+
+    /*
+     * Getters with explicit defaults
+     */
+
+    /**
+     * An unchecked exception that is thrown if a {@code get<TYPE>()} method
+     * is used to retrieve a parameter whose type does not match the method name.
+     */
+    public static class TypeException extends IllegalArgumentException {
+        TypeException(String property, Object value, String requestedType) {
+            super(property + " has type " + value.getClass().getName() +
+                ", not " + requestedType);
+        }
+    }
+
+    /**
+     * Returns the value of a boolean property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a boolean
+     */
+    public boolean getBoolean(String property, boolean def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Boolean) {
+            return ((Boolean)value).booleanValue();
+        }
+        throw new TypeException(property, value, "boolean");
+    }
+
+    /**
+     * Returns the value of a byte property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a byte
+     */
+    public byte getByte(String property, byte def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Byte) {
+            return ((Byte)value).byteValue();
+        }
+        throw new TypeException(property, value, "byte");
+    }
+
+    /**
+     * Returns the value of a short property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a short
+     */
+    public short getShort(String property, short def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Short) {
+            return ((Short)value).shortValue();
+        }
+        throw new TypeException(property, value, "short");
+    }
+
+    /**
+     * Returns the value of an integer property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not an integer
+     */
+    public int getInt(String property, int def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Integer) {
+            return ((Integer)value).intValue();
+        }
+        throw new TypeException(property, value, "int");
+    }
+
+    /**
+     * Returns the value of a long property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a long
+     */
+    public long getLong(String property, long def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Long) {
+            return ((Long)value).longValue();
+        }
+        throw new TypeException(property, value, "long");
+    }
+
+    /**
+     * Returns the value of a float property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a float
+     */
+    public float getFloat(String property, float def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Float) {
+            return ((Float)value).floatValue();
+        }
+        throw new TypeException(property, value, "float");
+    }
+
+    /**
+     * Returns the value of a double property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a double
+     */
+    public double getDouble(String property, double def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value instanceof Double) {
+            return ((Double)value).doubleValue();
+        }
+        throw new TypeException(property, value, "double");
+    }
+
+    /**
+     * Returns the value of a string property, or the default if the property
+     * has not been defined.
+     *
+     * @param property The name of the property to return
+     * @param def The default value to return if the property is not set
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a string
+     */
+    public String getString(String property, String def) {
+        Object value = super.get(property);
+        if (value == null) {
+            return def;
+        }
+        if (value == NULL_STRING) {
+            return null;
+        } else if (value instanceof String) {
+            return (String)value;
+        }
+        throw new TypeException(property, value, "string");
+    }
+
+    /*
+     * Getters with implicit defaults
+     */
+
+    /**
+     * Returns the value of a boolean property, or false
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a boolean
+     */
+    public boolean getBoolean(String property) {
+        return getBoolean(property, false);
+    }
+
+    /**
+     * Returns the value of a byte property, or 0
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a byte
+     */
+    public byte getByte(String property) {
+        return getByte(property, (byte)0);
+    }
+
+    /**
+     * Returns the value of a short property, or 0
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a short
+     */
+    public short getShort(String property) {
+        return getShort(property, (short)0);
+    }
+
+    /**
+     * Returns the value of an integer property, or 0
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not an integer
+     */
+    public int getInt(String property) {
+        return getInt(property, 0);
+    }
+
+    /**
+     * Returns the value of a long property, or 0
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a long
+     */
+    public long getLong(String property) {
+        return getLong(property, 0L);
+    }
+
+    /**
+     * Returns the value of a float property, or 0.0
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a float
+     */
+    public float getFloat(String property) {
+        return getFloat(property, 0.0f);
+    }
+
+    /**
+     * Returns the value of a double property, or 0.0
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a double
+     */
+    public double getDouble(String property) {
+        return getDouble(property, 0.0);
+    }
+
+    /**
+     * Returns the value of a String property, or ""
+     * if the property has not been defined.
+     *
+     * @param property The name of the property to return
+     * @return the value of the property
+     * @throws TypeException if the property is set and is not a string
+     */
+    public String getString(String property) {
+        return getString(property, "");
+    }
+
+    // Values returned by getStringInfo()
+    public static final int STRING_TYPE_MISMATCH = -2;
+    public static final int STRING_NOT_SET = -1;
+    public static final int STRING_NULL = 0;
+    public static final int STRING_SET = 1;
+
+    /**
+     * Provides string type information about a property.
+     *
+     * @param property the property to check
+     * @return STRING_SET if the property is a string and is non-null.
+     *         STRING_NULL if the property is a string and is null.
+     *         STRING_NOT_SET if the property is not set (no type or value).
+     *         STRING_TYPE_MISMATCH if the property is set but is not a string.
+     */
+    public int getStringInfo(String property) {
+        Object value = super.get(property);
+        if (value == null) {
+            return STRING_NOT_SET;
+        }
+        if (value == NULL_STRING) {
+            return STRING_NULL;
+        } else if (value instanceof String) {
+            return STRING_SET;
+        }
+        return STRING_TYPE_MISMATCH;
+    }
+}
diff --git a/com/android/internal/util/UserIcons.java b/com/android/internal/util/UserIcons.java
new file mode 100644
index 0000000..daf745f
--- /dev/null
+++ b/com/android/internal/util/UserIcons.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2014 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.internal.util;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import com.android.internal.R;
+
+/**
+ * Helper class that generates default user icons.
+ */
+public class UserIcons {
+
+    private static final int[] USER_ICON_COLORS = {
+        R.color.user_icon_1,
+        R.color.user_icon_2,
+        R.color.user_icon_3,
+        R.color.user_icon_4,
+        R.color.user_icon_5,
+        R.color.user_icon_6,
+        R.color.user_icon_7,
+        R.color.user_icon_8
+    };
+
+    /**
+     * Converts a given drawable to a bitmap.
+     */
+    public static Bitmap convertToBitmap(Drawable icon) {
+        if (icon == null) {
+            return null;
+        }
+        final int width = icon.getIntrinsicWidth();
+        final int height = icon.getIntrinsicHeight();
+        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        icon.setBounds(0, 0, width, height);
+        icon.draw(canvas);
+        return bitmap;
+    }
+
+    /**
+     * Returns a default user icon for the given user.
+     *
+     * Note that for guest users, you should pass in {@code UserHandle.USER_NULL}.
+     * @param userId the user id or {@code UserHandle.USER_NULL} for a non-user specific icon
+     * @param light whether we want a light icon (suitable for a dark background)
+     */
+    public static Drawable getDefaultUserIcon(int userId, boolean light) {
+        int colorResId = light ? R.color.user_icon_default_white : R.color.user_icon_default_gray;
+        if (userId != UserHandle.USER_NULL) {
+            // Return colored icon instead
+            colorResId = USER_ICON_COLORS[userId % USER_ICON_COLORS.length];
+        }
+        Drawable icon = Resources.getSystem().getDrawable(R.drawable.ic_account_circle, null).mutate();
+        icon.setColorFilter(Resources.getSystem().getColor(colorResId, null), Mode.SRC_IN);
+        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+        return icon;
+    }
+}
diff --git a/com/android/internal/util/VirtualRefBasePtr.java b/com/android/internal/util/VirtualRefBasePtr.java
new file mode 100644
index 0000000..52306f1
--- /dev/null
+++ b/com/android/internal/util/VirtualRefBasePtr.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 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.internal.util;
+
+/**
+ * Helper class that contains a strong reference to a VirtualRefBase native
+ * object. This will incStrong in the ctor, and decStrong in the finalizer
+ */
+public final class VirtualRefBasePtr {
+    private long mNativePtr;
+
+    public VirtualRefBasePtr(long ptr) {
+        mNativePtr = ptr;
+        nIncStrong(mNativePtr);
+    }
+
+    public long get() {
+        return mNativePtr;
+    }
+
+    public void release() {
+        if (mNativePtr != 0) {
+            nDecStrong(mNativePtr);
+            mNativePtr = 0;
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            release();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private static native void nIncStrong(long ptr);
+    private static native void nDecStrong(long ptr);
+}
diff --git a/com/android/internal/util/VirtualRefBasePtr_Delegate.java b/com/android/internal/util/VirtualRefBasePtr_Delegate.java
new file mode 100644
index 0000000..01fe45d
--- /dev/null
+++ b/com/android/internal/util/VirtualRefBasePtr_Delegate.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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.internal.util;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.util.LongSparseLongArray;
+
+/**
+ * Delegate used to provide new implementation the native methods of {@link VirtualRefBasePtr}
+ *
+ * Through the layoutlib_create tool, the original native  methods of VirtualRefBasePtr have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ */
+@SuppressWarnings("unused")
+public class VirtualRefBasePtr_Delegate {
+    private static final DelegateManager<Object> sManager = new DelegateManager<>(Object.class);
+    private static final LongSparseLongArray sRefCount = new LongSparseLongArray();
+
+    @LayoutlibDelegate
+    /*package*/ static synchronized void nIncStrong(long ptr) {
+        long counter = sRefCount.get(ptr);
+        sRefCount.put(ptr, ++counter);
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static synchronized void nDecStrong(long ptr) {
+        long counter = sRefCount.get(ptr);
+
+        if (counter > 1) {
+            sRefCount.put(ptr, --counter);
+        } else {
+            sRefCount.delete(ptr);
+            sManager.removeJavaReferenceFor(ptr);
+        }
+    }
+}
diff --git a/com/android/internal/util/WakeupMessage.java b/com/android/internal/util/WakeupMessage.java
new file mode 100644
index 0000000..46098c5
--- /dev/null
+++ b/com/android/internal/util/WakeupMessage.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 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.internal.util;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+ /**
+ * An AlarmListener that sends the specified message to a Handler and keeps the system awake until
+ * the message is processed.
+ *
+ * This is useful when using the AlarmManager direct callback interface to wake up the system and
+ * request that an object whose API consists of messages (such as a StateMachine) perform some
+ * action.
+ *
+ * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send
+ * the message, but does not guarantee that the system will be awake until the target object has
+ * processed it. This is because as soon as the onAlarmListener sends the message and returns, the
+ * AlarmManager releases its wakelock and the system is free to go to sleep again.
+ */
+public class WakeupMessage implements AlarmManager.OnAlarmListener {
+    private final AlarmManager mAlarmManager;
+
+    @VisibleForTesting
+    protected final Handler mHandler;
+    @VisibleForTesting
+    protected final String mCmdName;
+    @VisibleForTesting
+    protected final int mCmd, mArg1, mArg2;
+    @VisibleForTesting
+    protected final Object mObj;
+    private boolean mScheduled;
+
+    public WakeupMessage(Context context, Handler handler,
+            String cmdName, int cmd, int arg1, int arg2, Object obj) {
+        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        mHandler = handler;
+        mCmdName = cmdName;
+        mCmd = cmd;
+        mArg1 = arg1;
+        mArg2 = arg2;
+        mObj = obj;
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) {
+        this(context, handler, cmdName, cmd, arg1, 0, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler,
+            String cmdName, int cmd, int arg1, int arg2) {
+        this(context, handler, cmdName, cmd, arg1, arg2, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
+        this(context, handler, cmdName, cmd, 0, 0, null);
+    }
+
+    /**
+     * Schedule the message to be delivered at the time in milliseconds of the
+     * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup
+     * the device when it goes off. If schedule is called multiple times without the message being
+     * dispatched then the alarm is rescheduled to the new time.
+     */
+    public synchronized void schedule(long when) {
+        mAlarmManager.setExact(
+                AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler);
+        mScheduled = true;
+    }
+
+    /**
+     * Cancel all pending messages. This includes alarms that may have been fired, but have not been
+     * run on the handler yet.
+     */
+    public synchronized void cancel() {
+        if (mScheduled) {
+            mAlarmManager.cancel(this);
+            mScheduled = false;
+        }
+    }
+
+    @Override
+    public void onAlarm() {
+        // Once this method is called the alarm has already been fired and removed from
+        // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now
+        // be marked as unscheduled so that it can be rescheduled in the message handler.
+        final boolean stillScheduled;
+        synchronized (this) {
+            stillScheduled = mScheduled;
+            mScheduled = false;
+        }
+        if (stillScheduled) {
+            Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+            mHandler.dispatchMessage(msg);
+            msg.recycle();
+        }
+    }
+}
diff --git a/com/android/internal/util/XmlUtils.java b/com/android/internal/util/XmlUtils.java
new file mode 100644
index 0000000..3188d30
--- /dev/null
+++ b/com/android/internal/util/XmlUtils.java
@@ -0,0 +1,1773 @@
+/*
+ * Copyright (C) 2006 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.internal.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Bitmap.CompressFormat;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Base64;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ProtocolException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** {@hide} */
+public class XmlUtils {
+
+    private static final String STRING_ARRAY_SEPARATOR = ":";
+
+    public static void skipCurrentTag(XmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+               && (type != XmlPullParser.END_TAG
+                       || parser.getDepth() > outerDepth)) {
+        }
+    }
+
+    public static final int
+    convertValueToList(CharSequence value, String[] options, int defaultValue)
+    {
+        if (null != value) {
+            for (int i = 0; i < options.length; i++) {
+                if (value.equals(options[i]))
+                    return i;
+            }
+        }
+
+        return defaultValue;
+    }
+
+    public static final boolean
+    convertValueToBoolean(CharSequence value, boolean defaultValue)
+    {
+        boolean result = false;
+
+        if (null == value)
+            return defaultValue;
+
+        if (value.equals("1")
+        ||  value.equals("true")
+        ||  value.equals("TRUE"))
+            result = true;
+
+        return result;
+    }
+
+    public static final int
+    convertValueToInt(CharSequence charSeq, int defaultValue)
+    {
+        if (null == charSeq)
+            return defaultValue;
+
+        String nm = charSeq.toString();
+
+        // XXX This code is copied from Integer.decode() so we don't
+        // have to instantiate an Integer!
+
+        int value;
+        int sign = 1;
+        int index = 0;
+        int len = nm.length();
+        int base = 10;
+
+        if ('-' == nm.charAt(0)) {
+            sign = -1;
+            index++;
+        }
+
+        if ('0' == nm.charAt(index)) {
+            //  Quick check for a zero by itself
+            if (index == (len - 1))
+                return 0;
+
+            char    c = nm.charAt(index + 1);
+
+            if ('x' == c || 'X' == c) {
+                index += 2;
+                base = 16;
+            } else {
+                index++;
+                base = 8;
+            }
+        }
+        else if ('#' == nm.charAt(index))
+        {
+            index++;
+            base = 16;
+        }
+
+        return Integer.parseInt(nm.substring(index), base) * sign;
+    }
+
+    public static int convertValueToUnsignedInt(String value, int defaultValue) {
+        if (null == value) {
+            return defaultValue;
+        }
+
+        return parseUnsignedIntAttribute(value);
+    }
+
+    public static int parseUnsignedIntAttribute(CharSequence charSeq) {
+        String  value = charSeq.toString();
+
+        long    bits;
+        int     index = 0;
+        int     len = value.length();
+        int     base = 10;
+
+        if ('0' == value.charAt(index)) {
+            //  Quick check for zero by itself
+            if (index == (len - 1))
+                return 0;
+
+            char    c = value.charAt(index + 1);
+
+            if ('x' == c || 'X' == c) {     //  check for hex
+                index += 2;
+                base = 16;
+            } else {                        //  check for octal
+                index++;
+                base = 8;
+            }
+        } else if ('#' == value.charAt(index)) {
+            index++;
+            base = 16;
+        }
+
+        return (int) Long.parseLong(value.substring(index), base);
+    }
+
+    /**
+     * Flatten a Map into an output stream as XML.  The map can later be
+     * read back with readMapXml().
+     *
+     * @param val The map to be flattened.
+     * @param out Where to write the XML data.
+     *
+     * @see #writeMapXml(Map, String, XmlSerializer)
+     * @see #writeListXml
+     * @see #writeValueXml
+     * @see #readMapXml
+     */
+    public static final void writeMapXml(Map val, OutputStream out)
+            throws XmlPullParserException, java.io.IOException {
+        XmlSerializer serializer = new FastXmlSerializer();
+        serializer.setOutput(out, StandardCharsets.UTF_8.name());
+        serializer.startDocument(null, true);
+        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+        writeMapXml(val, null, serializer);
+        serializer.endDocument();
+    }
+
+    /**
+     * Flatten a List into an output stream as XML.  The list can later be
+     * read back with readListXml().
+     *
+     * @param val The list to be flattened.
+     * @param out Where to write the XML data.
+     *
+     * @see #writeListXml(List, String, XmlSerializer)
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readListXml
+     */
+    public static final void writeListXml(List val, OutputStream out)
+    throws XmlPullParserException, java.io.IOException
+    {
+        XmlSerializer serializer = Xml.newSerializer();
+        serializer.setOutput(out, StandardCharsets.UTF_8.name());
+        serializer.startDocument(null, true);
+        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+        writeListXml(val, null, serializer);
+        serializer.endDocument();
+    }
+
+    /**
+     * Flatten a Map into an XmlSerializer.  The map can later be read back
+     * with readThisMapXml().
+     *
+     * @param val The map to be flattened.
+     * @param name Name attribute to include with this list's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the map into.
+     *
+     * @see #writeMapXml(Map, OutputStream)
+     * @see #writeListXml
+     * @see #writeValueXml
+     * @see #readMapXml
+     */
+    public static final void writeMapXml(Map val, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+        writeMapXml(val, name, out, null);
+    }
+
+    /**
+     * Flatten a Map into an XmlSerializer.  The map can later be read back
+     * with readThisMapXml().
+     *
+     * @param val The map to be flattened.
+     * @param name Name attribute to include with this list's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the map into.
+     * @param callback Method to call when an Object type is not recognized.
+     *
+     * @see #writeMapXml(Map, OutputStream)
+     * @see #writeListXml
+     * @see #writeValueXml
+     * @see #readMapXml
+     *
+     * @hide
+     */
+    public static final void writeMapXml(Map val, String name, XmlSerializer out,
+            WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "map");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        writeMapXml(val, out, callback);
+
+        out.endTag(null, "map");
+    }
+
+    /**
+     * Flatten a Map into an XmlSerializer.  The map can later be read back
+     * with readThisMapXml(). This method presumes that the start tag and
+     * name attribute have already been written and does not write an end tag.
+     *
+     * @param val The map to be flattened.
+     * @param out XmlSerializer to write the map into.
+     *
+     * @see #writeMapXml(Map, OutputStream)
+     * @see #writeListXml
+     * @see #writeValueXml
+     * @see #readMapXml
+     *
+     * @hide
+     */
+    public static final void writeMapXml(Map val, XmlSerializer out,
+            WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {
+        if (val == null) {
+            return;
+        }
+
+        Set s = val.entrySet();
+        Iterator i = s.iterator();
+
+        while (i.hasNext()) {
+            Map.Entry e = (Map.Entry)i.next();
+            writeValueXml(e.getValue(), (String)e.getKey(), out, callback);
+        }
+    }
+
+    /**
+     * Flatten a List into an XmlSerializer.  The list can later be read back
+     * with readThisListXml().
+     *
+     * @param val The list to be flattened.
+     * @param name Name attribute to include with this list's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the list into.
+     *
+     * @see #writeListXml(List, OutputStream)
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readListXml
+     */
+    public static final void writeListXml(List val, String name, XmlSerializer out)
+    throws XmlPullParserException, java.io.IOException
+    {
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "list");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        int N = val.size();
+        int i=0;
+        while (i < N) {
+            writeValueXml(val.get(i), null, out);
+            i++;
+        }
+
+        out.endTag(null, "list");
+    }
+    
+    public static final void writeSetXml(Set val, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+        
+        out.startTag(null, "set");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+        
+        for (Object v : val) {
+            writeValueXml(v, null, out);
+        }
+        
+        out.endTag(null, "set");
+    }
+
+    /**
+     * Flatten a byte[] into an XmlSerializer.  The list can later be read back
+     * with readThisByteArrayXml().
+     *
+     * @param val The byte array to be flattened.
+     * @param name Name attribute to include with this array's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the array into.
+     *
+     * @see #writeMapXml
+     * @see #writeValueXml
+     */
+    public static final void writeByteArrayXml(byte[] val, String name,
+            XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "byte-array");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        final int N = val.length;
+        out.attribute(null, "num", Integer.toString(N));
+
+        StringBuilder sb = new StringBuilder(val.length*2);
+        for (int i=0; i<N; i++) {
+            int b = val[i];
+            int h = (b >> 4) & 0x0f;
+            sb.append((char)(h >= 10 ? ('a'+h-10) : ('0'+h)));
+            h = b & 0x0f;
+            sb.append((char)(h >= 10 ? ('a'+h-10) : ('0'+h)));
+        }
+
+        out.text(sb.toString());
+
+        out.endTag(null, "byte-array");
+    }
+
+    /**
+     * Flatten an int[] into an XmlSerializer.  The list can later be read back
+     * with readThisIntArrayXml().
+     *
+     * @param val The int array to be flattened.
+     * @param name Name attribute to include with this array's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the array into.
+     *
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readThisIntArrayXml
+     */
+    public static final void writeIntArrayXml(int[] val, String name,
+            XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "int-array");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        final int N = val.length;
+        out.attribute(null, "num", Integer.toString(N));
+
+        for (int i=0; i<N; i++) {
+            out.startTag(null, "item");
+            out.attribute(null, "value", Integer.toString(val[i]));
+            out.endTag(null, "item");
+        }
+
+        out.endTag(null, "int-array");
+    }
+
+    /**
+     * Flatten a long[] into an XmlSerializer.  The list can later be read back
+     * with readThisLongArrayXml().
+     *
+     * @param val The long array to be flattened.
+     * @param name Name attribute to include with this array's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the array into.
+     *
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readThisIntArrayXml
+     */
+    public static final void writeLongArrayXml(long[] val, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "long-array");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        final int N = val.length;
+        out.attribute(null, "num", Integer.toString(N));
+
+        for (int i=0; i<N; i++) {
+            out.startTag(null, "item");
+            out.attribute(null, "value", Long.toString(val[i]));
+            out.endTag(null, "item");
+        }
+
+        out.endTag(null, "long-array");
+    }
+
+    /**
+     * Flatten a double[] into an XmlSerializer.  The list can later be read back
+     * with readThisDoubleArrayXml().
+     *
+     * @param val The double array to be flattened.
+     * @param name Name attribute to include with this array's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the array into.
+     *
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readThisIntArrayXml
+     */
+    public static final void writeDoubleArrayXml(double[] val, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "double-array");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        final int N = val.length;
+        out.attribute(null, "num", Integer.toString(N));
+
+        for (int i=0; i<N; i++) {
+            out.startTag(null, "item");
+            out.attribute(null, "value", Double.toString(val[i]));
+            out.endTag(null, "item");
+        }
+
+        out.endTag(null, "double-array");
+    }
+
+    /**
+     * Flatten a String[] into an XmlSerializer.  The list can later be read back
+     * with readThisStringArrayXml().
+     *
+     * @param val The String array to be flattened.
+     * @param name Name attribute to include with this array's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the array into.
+     *
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readThisIntArrayXml
+     */
+    public static final void writeStringArrayXml(String[] val, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "string-array");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        final int N = val.length;
+        out.attribute(null, "num", Integer.toString(N));
+
+        for (int i=0; i<N; i++) {
+            out.startTag(null, "item");
+            out.attribute(null, "value", val[i]);
+            out.endTag(null, "item");
+        }
+
+        out.endTag(null, "string-array");
+    }
+
+    /**
+     * Flatten a boolean[] into an XmlSerializer.  The list can later be read back
+     * with readThisBooleanArrayXml().
+     *
+     * @param val The boolean array to be flattened.
+     * @param name Name attribute to include with this array's tag, or null for
+     *             none.
+     * @param out XmlSerializer to write the array into.
+     *
+     * @see #writeMapXml
+     * @see #writeValueXml
+     * @see #readThisIntArrayXml
+     */
+    public static final void writeBooleanArrayXml(boolean[] val, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+
+        if (val == null) {
+            out.startTag(null, "null");
+            out.endTag(null, "null");
+            return;
+        }
+
+        out.startTag(null, "boolean-array");
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+
+        final int N = val.length;
+        out.attribute(null, "num", Integer.toString(N));
+
+        for (int i=0; i<N; i++) {
+            out.startTag(null, "item");
+            out.attribute(null, "value", Boolean.toString(val[i]));
+            out.endTag(null, "item");
+        }
+
+        out.endTag(null, "boolean-array");
+    }
+
+    /**
+     * Flatten an object's value into an XmlSerializer.  The value can later
+     * be read back with readThisValueXml().
+     *
+     * Currently supported value types are: null, String, Integer, Long,
+     * Float, Double Boolean, Map, List.
+     *
+     * @param v The object to be flattened.
+     * @param name Name attribute to include with this value's tag, or null
+     *             for none.
+     * @param out XmlSerializer to write the object into.
+     *
+     * @see #writeMapXml
+     * @see #writeListXml
+     * @see #readValueXml
+     */
+    public static final void writeValueXml(Object v, String name, XmlSerializer out)
+            throws XmlPullParserException, java.io.IOException {
+        writeValueXml(v, name, out, null);
+    }
+
+    /**
+     * Flatten an object's value into an XmlSerializer.  The value can later
+     * be read back with readThisValueXml().
+     *
+     * Currently supported value types are: null, String, Integer, Long,
+     * Float, Double Boolean, Map, List.
+     *
+     * @param v The object to be flattened.
+     * @param name Name attribute to include with this value's tag, or null
+     *             for none.
+     * @param out XmlSerializer to write the object into.
+     * @param callback Handler for Object types not recognized.
+     *
+     * @see #writeMapXml
+     * @see #writeListXml
+     * @see #readValueXml
+     */
+    private static final void writeValueXml(Object v, String name, XmlSerializer out,
+            WriteMapCallback callback)  throws XmlPullParserException, java.io.IOException {
+        String typeStr;
+        if (v == null) {
+            out.startTag(null, "null");
+            if (name != null) {
+                out.attribute(null, "name", name);
+            }
+            out.endTag(null, "null");
+            return;
+        } else if (v instanceof String) {
+            out.startTag(null, "string");
+            if (name != null) {
+                out.attribute(null, "name", name);
+            }
+            out.text(v.toString());
+            out.endTag(null, "string");
+            return;
+        } else if (v instanceof Integer) {
+            typeStr = "int";
+        } else if (v instanceof Long) {
+            typeStr = "long";
+        } else if (v instanceof Float) {
+            typeStr = "float";
+        } else if (v instanceof Double) {
+            typeStr = "double";
+        } else if (v instanceof Boolean) {
+            typeStr = "boolean";
+        } else if (v instanceof byte[]) {
+            writeByteArrayXml((byte[])v, name, out);
+            return;
+        } else if (v instanceof int[]) {
+            writeIntArrayXml((int[])v, name, out);
+            return;
+        } else if (v instanceof long[]) {
+            writeLongArrayXml((long[])v, name, out);
+            return;
+        } else if (v instanceof double[]) {
+            writeDoubleArrayXml((double[])v, name, out);
+            return;
+        } else if (v instanceof String[]) {
+            writeStringArrayXml((String[])v, name, out);
+            return;
+        } else if (v instanceof boolean[]) {
+            writeBooleanArrayXml((boolean[])v, name, out);
+            return;
+        } else if (v instanceof Map) {
+            writeMapXml((Map)v, name, out);
+            return;
+        } else if (v instanceof List) {
+            writeListXml((List) v, name, out);
+            return;
+        } else if (v instanceof Set) {
+            writeSetXml((Set) v, name, out);
+            return;
+        } else if (v instanceof CharSequence) {
+            // XXX This is to allow us to at least write something if
+            // we encounter styled text...  but it means we will drop all
+            // of the styling information. :(
+            out.startTag(null, "string");
+            if (name != null) {
+                out.attribute(null, "name", name);
+            }
+            out.text(v.toString());
+            out.endTag(null, "string");
+            return;
+        } else if (callback != null) {
+            callback.writeUnknownObject(v, name, out);
+            return;
+        } else {
+            throw new RuntimeException("writeValueXml: unable to write value " + v);
+        }
+
+        out.startTag(null, typeStr);
+        if (name != null) {
+            out.attribute(null, "name", name);
+        }
+        out.attribute(null, "value", v.toString());
+        out.endTag(null, typeStr);
+    }
+
+    /**
+     * Read a HashMap from an InputStream containing XML.  The stream can
+     * previously have been written by writeMapXml().
+     *
+     * @param in The InputStream from which to read.
+     *
+     * @return HashMap The resulting map.
+     *
+     * @see #readListXml
+     * @see #readValueXml
+     * @see #readThisMapXml
+     * #see #writeMapXml
+     */
+    @SuppressWarnings("unchecked")
+    public static final HashMap<String, ?> readMapXml(InputStream in)
+    throws XmlPullParserException, java.io.IOException
+    {
+        XmlPullParser   parser = Xml.newPullParser();
+        parser.setInput(in, StandardCharsets.UTF_8.name());
+        return (HashMap<String, ?>) readValueXml(parser, new String[1]);
+    }
+
+    /**
+     * Read an ArrayList from an InputStream containing XML.  The stream can
+     * previously have been written by writeListXml().
+     *
+     * @param in The InputStream from which to read.
+     *
+     * @return ArrayList The resulting list.
+     *
+     * @see #readMapXml
+     * @see #readValueXml
+     * @see #readThisListXml
+     * @see #writeListXml
+     */
+    public static final ArrayList readListXml(InputStream in)
+    throws XmlPullParserException, java.io.IOException
+    {
+        XmlPullParser   parser = Xml.newPullParser();
+        parser.setInput(in, StandardCharsets.UTF_8.name());
+        return (ArrayList)readValueXml(parser, new String[1]);
+    }
+    
+    
+    /**
+     * Read a HashSet from an InputStream containing XML. The stream can
+     * previously have been written by writeSetXml().
+     * 
+     * @param in The InputStream from which to read.
+     * 
+     * @return HashSet The resulting set.
+     * 
+     * @throws XmlPullParserException
+     * @throws java.io.IOException
+     * 
+     * @see #readValueXml
+     * @see #readThisSetXml
+     * @see #writeSetXml
+     */
+    public static final HashSet readSetXml(InputStream in)
+            throws XmlPullParserException, java.io.IOException {
+        XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(in, null);
+        return (HashSet) readValueXml(parser, new String[1]);
+    }
+
+    /**
+     * Read a HashMap object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeMapXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the map.
+     *
+     * @param parser The XmlPullParser from which to read the map data.
+     * @param endTag Name of the tag that will end the map, usually "map".
+     * @param name An array of one string, used to return the name attribute
+     *             of the map's tag.
+     *
+     * @return HashMap The newly generated map.
+     *
+     * @see #readMapXml
+     */
+    public static final HashMap<String, ?> readThisMapXml(XmlPullParser parser, String endTag,
+            String[] name) throws XmlPullParserException, java.io.IOException {
+        return readThisMapXml(parser, endTag, name, null);
+    }
+
+    /**
+     * Read a HashMap object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeMapXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the map.
+     *
+     * @param parser The XmlPullParser from which to read the map data.
+     * @param endTag Name of the tag that will end the map, usually "map".
+     * @param name An array of one string, used to return the name attribute
+     *             of the map's tag.
+     *
+     * @return HashMap The newly generated map.
+     *
+     * @see #readMapXml
+     * @hide
+     */
+    public static final HashMap<String, ?> readThisMapXml(XmlPullParser parser, String endTag,
+            String[] name, ReadMapCallback callback)
+            throws XmlPullParserException, java.io.IOException
+    {
+        HashMap<String, Object> map = new HashMap<String, Object>();
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                Object val = readThisValueXml(parser, name, callback, false);
+                map.put(name[0], val);
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return map;
+                }
+                throw new XmlPullParserException(
+                    "Expected " + endTag + " end tag at: " + parser.getName());
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+            "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Like {@link #readThisMapXml}, but returns an ArrayMap instead of HashMap.
+     * @hide
+     */
+    public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag,
+            String[] name, ReadMapCallback callback)
+            throws XmlPullParserException, java.io.IOException
+    {
+        ArrayMap<String, Object> map = new ArrayMap<>();
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                Object val = readThisValueXml(parser, name, callback, true);
+                map.put(name[0], val);
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return map;
+                }
+                throw new XmlPullParserException(
+                    "Expected " + endTag + " end tag at: " + parser.getName());
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+            "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read an ArrayList object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeListXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "list".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return HashMap The newly generated list.
+     *
+     * @see #readListXml
+     */
+    public static final ArrayList readThisListXml(XmlPullParser parser, String endTag,
+            String[] name) throws XmlPullParserException, java.io.IOException {
+        return readThisListXml(parser, endTag, name, null, false);
+    }
+
+    /**
+     * Read an ArrayList object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeListXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "list".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return HashMap The newly generated list.
+     *
+     * @see #readListXml
+     */
+    private static final ArrayList readThisListXml(XmlPullParser parser, String endTag,
+            String[] name, ReadMapCallback callback, boolean arrayMap)
+            throws XmlPullParserException, java.io.IOException {
+        ArrayList list = new ArrayList();
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                Object val = readThisValueXml(parser, name, callback, arrayMap);
+                list.add(val);
+                //System.out.println("Adding to list: " + val);
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return list;
+                }
+                throw new XmlPullParserException(
+                    "Expected " + endTag + " end tag at: " + parser.getName());
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+            "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a HashSet object from an XmlPullParser. The XML data could previously
+     * have been generated by writeSetXml(). The XmlPullParser must be positioned
+     * <em>after</em> the tag that begins the set.
+     *
+     * @param parser The XmlPullParser from which to read the set data.
+     * @param endTag Name of the tag that will end the set, usually "set".
+     * @param name An array of one string, used to return the name attribute
+     *             of the set's tag.
+     *
+     * @return HashSet The newly generated set.
+     *
+     * @throws XmlPullParserException
+     * @throws java.io.IOException
+     *
+     * @see #readSetXml
+     */
+    public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name)
+            throws XmlPullParserException, java.io.IOException {
+        return readThisSetXml(parser, endTag, name, null, false);
+    }
+
+    /**
+     * Read a HashSet object from an XmlPullParser. The XML data could previously
+     * have been generated by writeSetXml(). The XmlPullParser must be positioned
+     * <em>after</em> the tag that begins the set.
+     * 
+     * @param parser The XmlPullParser from which to read the set data.
+     * @param endTag Name of the tag that will end the set, usually "set".
+     * @param name An array of one string, used to return the name attribute
+     *             of the set's tag.
+     *
+     * @return HashSet The newly generated set.
+     * 
+     * @throws XmlPullParserException
+     * @throws java.io.IOException
+     * 
+     * @see #readSetXml
+     * @hide
+     */
+    private static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name,
+            ReadMapCallback callback, boolean arrayMap)
+            throws XmlPullParserException, java.io.IOException {
+        HashSet set = new HashSet();
+        
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                Object val = readThisValueXml(parser, name, callback, arrayMap);
+                set.add(val);
+                //System.out.println("Adding to set: " + val);
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return set;
+                }
+                throw new XmlPullParserException(
+                        "Expected " + endTag + " end tag at: " + parser.getName());
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+        
+        throw new XmlPullParserException(
+                "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a byte[] object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeByteArrayXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "list".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return Returns a newly generated byte[].
+     *
+     * @see #writeByteArrayXml
+     */
+    public static final byte[] readThisByteArrayXml(XmlPullParser parser,
+            String endTag, String[] name)
+            throws XmlPullParserException, java.io.IOException {
+
+        int num;
+        try {
+            num = Integer.parseInt(parser.getAttributeValue(null, "num"));
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException(
+                    "Need num attribute in byte-array");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException(
+                    "Not a number in num attribute in byte-array");
+        }
+
+        byte[] array = new byte[num];
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.TEXT) {
+                if (num > 0) {
+                    String values = parser.getText();
+                    if (values == null || values.length() != num * 2) {
+                        throw new XmlPullParserException(
+                                "Invalid value found in byte-array: " + values);
+                    }
+                    // This is ugly, but keeping it to mirror the logic in #writeByteArrayXml.
+                    for (int i = 0; i < num; i ++) {
+                        char nibbleHighChar = values.charAt(2 * i);
+                        char nibbleLowChar = values.charAt(2 * i + 1);
+                        int nibbleHigh = nibbleHighChar > 'a' ? (nibbleHighChar - 'a' + 10)
+                                : (nibbleHighChar - '0');
+                        int nibbleLow = nibbleLowChar > 'a' ? (nibbleLowChar - 'a' + 10)
+                                : (nibbleLowChar - '0');
+                        array[i] = (byte) ((nibbleHigh & 0x0F) << 4 | (nibbleLow & 0x0F));
+                    }
+                }
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array;
+                } else {
+                    throw new XmlPullParserException(
+                            "Expected " + endTag + " end tag at: "
+                                    + parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+                "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read an int[] object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeIntArrayXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "list".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return Returns a newly generated int[].
+     *
+     * @see #readListXml
+     */
+    public static final int[] readThisIntArrayXml(XmlPullParser parser,
+            String endTag, String[] name)
+            throws XmlPullParserException, java.io.IOException {
+
+        int num;
+        try {
+            num = Integer.parseInt(parser.getAttributeValue(null, "num"));
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException(
+                    "Need num attribute in int-array");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException(
+                    "Not a number in num attribute in int-array");
+        }
+        parser.next();
+
+        int[] array = new int[num];
+        int i = 0;
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    try {
+                        array[i] = Integer.parseInt(
+                                parser.getAttributeValue(null, "value"));
+                    } catch (NullPointerException e) {
+                        throw new XmlPullParserException(
+                                "Need value attribute in item");
+                    } catch (NumberFormatException e) {
+                        throw new XmlPullParserException(
+                                "Not a number in value attribute in item");
+                    }
+                } else {
+                    throw new XmlPullParserException(
+                            "Expected item tag at: " + parser.getName());
+                }
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array;
+                } else if (parser.getName().equals("item")) {
+                    i++;
+                } else {
+                    throw new XmlPullParserException(
+                        "Expected " + endTag + " end tag at: "
+                        + parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+            "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a long[] object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeLongArrayXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "list".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return Returns a newly generated long[].
+     *
+     * @see #readListXml
+     */
+    public static final long[] readThisLongArrayXml(XmlPullParser parser,
+            String endTag, String[] name)
+            throws XmlPullParserException, java.io.IOException {
+
+        int num;
+        try {
+            num = Integer.parseInt(parser.getAttributeValue(null, "num"));
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need num attribute in long-array");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException("Not a number in num attribute in long-array");
+        }
+        parser.next();
+
+        long[] array = new long[num];
+        int i = 0;
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    try {
+                        array[i] = Long.parseLong(parser.getAttributeValue(null, "value"));
+                    } catch (NullPointerException e) {
+                        throw new XmlPullParserException("Need value attribute in item");
+                    } catch (NumberFormatException e) {
+                        throw new XmlPullParserException("Not a number in value attribute in item");
+                    }
+                } else {
+                    throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+                }
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array;
+                } else if (parser.getName().equals("item")) {
+                    i++;
+                } else {
+                    throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
+                            parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a double[] object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeDoubleArrayXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "double-array".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return Returns a newly generated double[].
+     *
+     * @see #readListXml
+     */
+    public static final double[] readThisDoubleArrayXml(XmlPullParser parser, String endTag,
+            String[] name) throws XmlPullParserException, java.io.IOException {
+
+        int num;
+        try {
+            num = Integer.parseInt(parser.getAttributeValue(null, "num"));
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need num attribute in double-array");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException("Not a number in num attribute in double-array");
+        }
+        parser.next();
+
+        double[] array = new double[num];
+        int i = 0;
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    try {
+                        array[i] = Double.parseDouble(parser.getAttributeValue(null, "value"));
+                    } catch (NullPointerException e) {
+                        throw new XmlPullParserException("Need value attribute in item");
+                    } catch (NumberFormatException e) {
+                        throw new XmlPullParserException("Not a number in value attribute in item");
+                    }
+                } else {
+                    throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+                }
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array;
+                } else if (parser.getName().equals("item")) {
+                    i++;
+                } else {
+                    throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
+                            parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a String[] object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeStringArrayXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "string-array".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return Returns a newly generated String[].
+     *
+     * @see #readListXml
+     */
+    public static final String[] readThisStringArrayXml(XmlPullParser parser, String endTag,
+            String[] name) throws XmlPullParserException, java.io.IOException {
+
+        int num;
+        try {
+            num = Integer.parseInt(parser.getAttributeValue(null, "num"));
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need num attribute in string-array");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException("Not a number in num attribute in string-array");
+        }
+        parser.next();
+
+        String[] array = new String[num];
+        int i = 0;
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    try {
+                        array[i] = parser.getAttributeValue(null, "value");
+                    } catch (NullPointerException e) {
+                        throw new XmlPullParserException("Need value attribute in item");
+                    } catch (NumberFormatException e) {
+                        throw new XmlPullParserException("Not a number in value attribute in item");
+                    }
+                } else {
+                    throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+                }
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array;
+                } else if (parser.getName().equals("item")) {
+                    i++;
+                } else {
+                    throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
+                            parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a boolean[] object from an XmlPullParser.  The XML data could
+     * previously have been generated by writeBooleanArrayXml().  The XmlPullParser
+     * must be positioned <em>after</em> the tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "string-array".
+     * @param name An array of one string, used to return the name attribute
+     *             of the list's tag.
+     *
+     * @return Returns a newly generated boolean[].
+     *
+     * @see #readListXml
+     */
+    public static final boolean[] readThisBooleanArrayXml(XmlPullParser parser, String endTag,
+            String[] name) throws XmlPullParserException, java.io.IOException {
+
+        int num;
+        try {
+            num = Integer.parseInt(parser.getAttributeValue(null, "num"));
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need num attribute in string-array");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException("Not a number in num attribute in string-array");
+        }
+        parser.next();
+
+        boolean[] array = new boolean[num];
+        int i = 0;
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    try {
+                        array[i] = Boolean.parseBoolean(parser.getAttributeValue(null, "value"));
+                    } catch (NullPointerException e) {
+                        throw new XmlPullParserException("Need value attribute in item");
+                    } catch (NumberFormatException e) {
+                        throw new XmlPullParserException("Not a number in value attribute in item");
+                    }
+                } else {
+                    throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+                }
+            } else if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array;
+                } else if (parser.getName().equals("item")) {
+                    i++;
+                } else {
+                    throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
+                            parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a flattened object from an XmlPullParser.  The XML data could
+     * previously have been written with writeMapXml(), writeListXml(), or
+     * writeValueXml().  The XmlPullParser must be positioned <em>at</em> the
+     * tag that defines the value.
+     *
+     * @param parser The XmlPullParser from which to read the object.
+     * @param name An array of one string, used to return the name attribute
+     *             of the value's tag.
+     *
+     * @return Object The newly generated value object.
+     *
+     * @see #readMapXml
+     * @see #readListXml
+     * @see #writeValueXml
+     */
+    public static final Object readValueXml(XmlPullParser parser, String[] name)
+    throws XmlPullParserException, java.io.IOException
+    {
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == parser.START_TAG) {
+                return readThisValueXml(parser, name, null, false);
+            } else if (eventType == parser.END_TAG) {
+                throw new XmlPullParserException(
+                    "Unexpected end tag at: " + parser.getName());
+            } else if (eventType == parser.TEXT) {
+                throw new XmlPullParserException(
+                    "Unexpected text: " + parser.getText());
+            }
+            eventType = parser.next();
+        } while (eventType != parser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+            "Unexpected end of document");
+    }
+
+    private static final Object readThisValueXml(XmlPullParser parser, String[] name,
+            ReadMapCallback callback, boolean arrayMap)
+            throws XmlPullParserException, java.io.IOException {
+        final String valueName = parser.getAttributeValue(null, "name");
+        final String tagName = parser.getName();
+
+        //System.out.println("Reading this value tag: " + tagName + ", name=" + valueName);
+
+        Object res;
+
+        if (tagName.equals("null")) {
+            res = null;
+        } else if (tagName.equals("string")) {
+            String value = "";
+            int eventType;
+            while ((eventType = parser.next()) != parser.END_DOCUMENT) {
+                if (eventType == parser.END_TAG) {
+                    if (parser.getName().equals("string")) {
+                        name[0] = valueName;
+                        //System.out.println("Returning value for " + valueName + ": " + value);
+                        return value;
+                    }
+                    throw new XmlPullParserException(
+                        "Unexpected end tag in <string>: " + parser.getName());
+                } else if (eventType == parser.TEXT) {
+                    value += parser.getText();
+                } else if (eventType == parser.START_TAG) {
+                    throw new XmlPullParserException(
+                        "Unexpected start tag in <string>: " + parser.getName());
+                }
+            }
+            throw new XmlPullParserException(
+                "Unexpected end of document in <string>");
+        } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
+            // all work already done by readThisPrimitiveValueXml
+        } else if (tagName.equals("byte-array")) {
+            res = readThisByteArrayXml(parser, "byte-array", name);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("int-array")) {
+            res = readThisIntArrayXml(parser, "int-array", name);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("long-array")) {
+            res = readThisLongArrayXml(parser, "long-array", name);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("double-array")) {
+            res = readThisDoubleArrayXml(parser, "double-array", name);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("string-array")) {
+            res = readThisStringArrayXml(parser, "string-array", name);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("boolean-array")) {
+            res = readThisBooleanArrayXml(parser, "boolean-array", name);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("map")) {
+            parser.next();
+            res = arrayMap
+                    ? readThisArrayMapXml(parser, "map", name, callback)
+                    : readThisMapXml(parser, "map", name, callback);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("list")) {
+            parser.next();
+            res = readThisListXml(parser, "list", name, callback, arrayMap);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (tagName.equals("set")) {
+            parser.next();
+            res = readThisSetXml(parser, "set", name, callback, arrayMap);
+            name[0] = valueName;
+            //System.out.println("Returning value for " + valueName + ": " + res);
+            return res;
+        } else if (callback != null) {
+            res = callback.readThisUnknownObjectXml(parser, tagName);
+            name[0] = valueName;
+            return res;
+        } else {
+            throw new XmlPullParserException("Unknown tag: " + tagName);
+        }
+
+        // Skip through to end tag.
+        int eventType;
+        while ((eventType = parser.next()) != parser.END_DOCUMENT) {
+            if (eventType == parser.END_TAG) {
+                if (parser.getName().equals(tagName)) {
+                    name[0] = valueName;
+                    //System.out.println("Returning value for " + valueName + ": " + res);
+                    return res;
+                }
+                throw new XmlPullParserException(
+                    "Unexpected end tag in <" + tagName + ">: " + parser.getName());
+            } else if (eventType == parser.TEXT) {
+                throw new XmlPullParserException(
+                "Unexpected text in <" + tagName + ">: " + parser.getName());
+            } else if (eventType == parser.START_TAG) {
+                throw new XmlPullParserException(
+                    "Unexpected start tag in <" + tagName + ">: " + parser.getName());
+            }
+        }
+        throw new XmlPullParserException(
+            "Unexpected end of document in <" + tagName + ">");
+    }
+
+    private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
+    throws XmlPullParserException, java.io.IOException
+    {
+        try {
+            if (tagName.equals("int")) {
+                return Integer.parseInt(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("long")) {
+                return Long.valueOf(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("float")) {
+                return new Float(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("double")) {
+                return new Double(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("boolean")) {
+                return Boolean.valueOf(parser.getAttributeValue(null, "value"));
+            } else {
+                return null;
+            }
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException(
+                    "Not a number in value attribute in <" + tagName + ">");
+        }
+    }
+
+    public static final void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException
+    {
+        int type;
+        while ((type=parser.next()) != parser.START_TAG
+                   && type != parser.END_DOCUMENT) {
+            ;
+        }
+
+        if (type != parser.START_TAG) {
+            throw new XmlPullParserException("No start tag found");
+        }
+
+        if (!parser.getName().equals(firstElementName)) {
+            throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
+                    ", expected " + firstElementName);
+        }
+    }
+
+    public static final void nextElement(XmlPullParser parser) throws XmlPullParserException, IOException
+    {
+        int type;
+        while ((type=parser.next()) != parser.START_TAG
+                   && type != parser.END_DOCUMENT) {
+            ;
+        }
+    }
+
+    public static boolean nextElementWithin(XmlPullParser parser, int outerDepth)
+            throws IOException, XmlPullParserException {
+        for (;;) {
+            int type = parser.next();
+            if (type == XmlPullParser.END_DOCUMENT
+                    || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) {
+                return false;
+            }
+            if (type == XmlPullParser.START_TAG
+                    && parser.getDepth() == outerDepth + 1) {
+                return true;
+            }
+        }
+    }
+
+    public static int readIntAttribute(XmlPullParser in, String name, int defaultValue) {
+        final String value = in.getAttributeValue(null, name);
+        if (TextUtils.isEmpty(value)) {
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    public static int readIntAttribute(XmlPullParser in, String name) throws IOException {
+        final String value = in.getAttributeValue(null, name);
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            throw new ProtocolException("problem parsing " + name + "=" + value + " as int");
+        }
+    }
+
+    public static void writeIntAttribute(XmlSerializer out, String name, int value)
+            throws IOException {
+        out.attribute(null, name, Integer.toString(value));
+    }
+
+    public static long readLongAttribute(XmlPullParser in, String name, long defaultValue) {
+        final String value = in.getAttributeValue(null, name);
+        if (TextUtils.isEmpty(value)) {
+            return defaultValue;
+        }
+        try {
+            return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    public static long readLongAttribute(XmlPullParser in, String name) throws IOException {
+        final String value = in.getAttributeValue(null, name);
+        try {
+            return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+            throw new ProtocolException("problem parsing " + name + "=" + value + " as long");
+        }
+    }
+
+    public static void writeLongAttribute(XmlSerializer out, String name, long value)
+            throws IOException {
+        out.attribute(null, name, Long.toString(value));
+    }
+
+    public static float readFloatAttribute(XmlPullParser in, String name) throws IOException {
+        final String value = in.getAttributeValue(null, name);
+        try {
+            return Float.parseFloat(value);
+        } catch (NumberFormatException e) {
+            throw new ProtocolException("problem parsing " + name + "=" + value + " as long");
+        }
+    }
+
+    public static void writeFloatAttribute(XmlSerializer out, String name, float value)
+            throws IOException {
+        out.attribute(null, name, Float.toString(value));
+    }
+
+    public static boolean readBooleanAttribute(XmlPullParser in, String name) {
+        final String value = in.getAttributeValue(null, name);
+        return Boolean.parseBoolean(value);
+    }
+
+    public static boolean readBooleanAttribute(XmlPullParser in, String name,
+            boolean defaultValue) {
+        final String value = in.getAttributeValue(null, name);
+        if (value == null) {
+            return defaultValue;
+        } else {
+            return Boolean.parseBoolean(value);
+        }
+    }
+
+    public static void writeBooleanAttribute(XmlSerializer out, String name, boolean value)
+            throws IOException {
+        out.attribute(null, name, Boolean.toString(value));
+    }
+
+    public static Uri readUriAttribute(XmlPullParser in, String name) {
+        final String value = in.getAttributeValue(null, name);
+        return (value != null) ? Uri.parse(value) : null;
+    }
+
+    public static void writeUriAttribute(XmlSerializer out, String name, Uri value)
+            throws IOException {
+        if (value != null) {
+            out.attribute(null, name, value.toString());
+        }
+    }
+
+    public static String readStringAttribute(XmlPullParser in, String name) {
+        return in.getAttributeValue(null, name);
+    }
+
+    public static void writeStringAttribute(XmlSerializer out, String name, CharSequence value)
+            throws IOException {
+        if (value != null) {
+            out.attribute(null, name, value.toString());
+        }
+    }
+
+    public static byte[] readByteArrayAttribute(XmlPullParser in, String name) {
+        final String value = in.getAttributeValue(null, name);
+        if (value != null) {
+            return Base64.decode(value, Base64.DEFAULT);
+        } else {
+            return null;
+        }
+    }
+
+    public static void writeByteArrayAttribute(XmlSerializer out, String name, byte[] value)
+            throws IOException {
+        if (value != null) {
+            out.attribute(null, name, Base64.encodeToString(value, Base64.DEFAULT));
+        }
+    }
+
+    public static Bitmap readBitmapAttribute(XmlPullParser in, String name) {
+        final byte[] value = readByteArrayAttribute(in, name);
+        if (value != null) {
+            return BitmapFactory.decodeByteArray(value, 0, value.length);
+        } else {
+            return null;
+        }
+    }
+
+    @Deprecated
+    public static void writeBitmapAttribute(XmlSerializer out, String name, Bitmap value)
+            throws IOException {
+        if (value != null) {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            value.compress(CompressFormat.PNG, 90, os);
+            writeByteArrayAttribute(out, name, os.toByteArray());
+        }
+    }
+
+    /** @hide */
+    public interface WriteMapCallback {
+        /**
+         * Called from writeMapXml when an Object type is not recognized. The implementer
+         * must write out the entire element including start and end tags.
+         *
+         * @param v The object to be written out
+         * @param name The mapping key for v. Must be written into the "name" attribute of the
+         *             start tag.
+         * @param out The XML output stream.
+         * @throws XmlPullParserException on unrecognized Object type.
+         * @throws IOException on XmlSerializer serialization errors.
+         * @hide
+         */
+         public void writeUnknownObject(Object v, String name, XmlSerializer out)
+                 throws XmlPullParserException, IOException;
+    }
+
+    /** @hide */
+    public interface ReadMapCallback {
+        /**
+         * Called from readThisMapXml when a START_TAG is not recognized. The input stream
+         * is positioned within the start tag so that attributes can be read using in.getAttribute.
+         *
+         * @param in the XML input stream
+         * @param tag the START_TAG that was not recognized.
+         * @return the Object parsed from the stream which will be put into the map.
+         * @throws XmlPullParserException if the START_TAG is not recognized.
+         * @throws IOException on XmlPullParser serialization errors.
+         * @hide
+         */
+        public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+                throws XmlPullParserException, IOException;
+    }
+}
diff --git a/com/android/internal/util/XmlUtils_Delegate.java b/com/android/internal/util/XmlUtils_Delegate.java
new file mode 100644
index 0000000..bf998b8
--- /dev/null
+++ b/com/android/internal/util/XmlUtils_Delegate.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 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.internal.util;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link XmlUtils}
+ *
+ * Through the layoutlib_create tool, the original  methods of XmlUtils have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class XmlUtils_Delegate {
+
+    @LayoutlibDelegate
+    /*package*/ static final int convertValueToInt(CharSequence charSeq, int defaultValue) {
+        if (null == charSeq)
+            return defaultValue;
+
+        String nm = charSeq.toString();
+
+        // This code is copied from the original implementation. The issue is that
+        // The Dalvik libraries are able to handle Integer.parse("XXXXXXXX", 16) where XXXXXXX
+        // is > 80000000 but the Java VM cannot.
+
+        int sign = 1;
+        int index = 0;
+        int len = nm.length();
+        int base = 10;
+
+        if ('-' == nm.charAt(0)) {
+            sign = -1;
+            index++;
+        }
+
+        if ('0' == nm.charAt(index)) {
+            //  Quick check for a zero by itself
+            if (index == (len - 1))
+                return 0;
+
+            char c = nm.charAt(index + 1);
+
+            if ('x' == c || 'X' == c) {
+                index += 2;
+                base = 16;
+            } else {
+                index++;
+                base = 8;
+            }
+        }
+        else if ('#' == nm.charAt(index)) {
+            index++;
+            base = 16;
+        }
+
+        return ((int)Long.parseLong(nm.substring(index), base)) * sign;
+    }
+}
diff --git a/com/android/internal/view/ActionBarPolicy.java b/com/android/internal/view/ActionBarPolicy.java
new file mode 100644
index 0000000..755faf3
--- /dev/null
+++ b/com/android/internal/view/ActionBarPolicy.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2012 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.internal.view;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+
+/**
+ * Allows components to query for various configuration policy decisions
+ * about how the action bar should lay out and behave on the current device.
+ */
+public class ActionBarPolicy {
+    private Context mContext;
+
+    public static ActionBarPolicy get(Context context) {
+        return new ActionBarPolicy(context);
+    }
+
+    private ActionBarPolicy(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Returns the maximum number of action buttons that should be permitted within an action
+     * bar/action mode. This will be used to determine how many showAsAction="ifRoom" items can fit.
+     * "always" items can override this.
+     */
+    public int getMaxActionButtons() {
+        final Configuration config = mContext.getResources().getConfiguration();
+        final int width = config.screenWidthDp;
+        final int height = config.screenHeightDp;
+        final int smallest = config.smallestScreenWidthDp;
+        if (smallest > 600 || (width > 960 && height > 720) || (width > 720 && height > 960)) {
+            // For values-w600dp, values-sw600dp and values-xlarge.
+            return 5;
+        } else if (width >= 500 || (width > 640 && height > 480) || (width > 480 && height > 640)) {
+            // For values-w500dp and values-large.
+            return 4;
+        } else if (width >= 360) {
+            // For values-w360dp.
+            return 3;
+        } else {
+            return 2;
+        }
+    }
+    public boolean showsOverflowMenuButton() {
+        return true;
+    }
+
+    public int getEmbeddedMenuWidthLimit() {
+        return mContext.getResources().getDisplayMetrics().widthPixels / 2;
+    }
+
+    public boolean hasEmbeddedTabs() {
+        final int targetSdk = mContext.getApplicationInfo().targetSdkVersion;
+        if (targetSdk >= Build.VERSION_CODES.JELLY_BEAN) {
+            return mContext.getResources().getBoolean(R.bool.action_bar_embed_tabs);
+        }
+
+        // The embedded tabs policy changed in Jellybean; give older apps the old policy
+        // so they get what they expect.
+        final Configuration configuration = mContext.getResources().getConfiguration();
+        final int width = configuration.screenWidthDp;
+        final int height = configuration.screenHeightDp;
+        return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE ||
+                width >= 480 || (width >= 640 && height >= 480);
+    }
+
+    public int getTabContainerHeight() {
+        TypedArray a = mContext.obtainStyledAttributes(null, R.styleable.ActionBar,
+                com.android.internal.R.attr.actionBarStyle, 0);
+        int height = a.getLayoutDimension(R.styleable.ActionBar_height, 0);
+        Resources r = mContext.getResources();
+        if (!hasEmbeddedTabs()) {
+            // Stacked tabs; limit the height
+            height = Math.min(height,
+                    r.getDimensionPixelSize(R.dimen.action_bar_stacked_max_height));
+        }
+        a.recycle();
+        return height;
+    }
+
+    public boolean enableHomeButtonByDefault() {
+        // Older apps get the home button interaction enabled by default.
+        // Newer apps need to enable it explicitly.
+        return mContext.getApplicationInfo().targetSdkVersion <
+                Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+    }
+
+    public int getStackedTabMaxWidth() {
+        return mContext.getResources().getDimensionPixelSize(
+                R.dimen.action_bar_stacked_tab_max_width);
+    }
+}
diff --git a/com/android/internal/view/BaseIWindow.java b/com/android/internal/view/BaseIWindow.java
new file mode 100644
index 0000000..361fd3d
--- /dev/null
+++ b/com/android/internal/view/BaseIWindow.java
@@ -0,0 +1,130 @@
+/*
+ * 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.internal.view;
+
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.MergedConfiguration;
+import android.view.DragEvent;
+import android.view.IWindow;
+import android.view.IWindowSession;
+import android.view.PointerIcon;
+
+import com.android.internal.os.IResultReceiver;
+
+public class BaseIWindow extends IWindow.Stub {
+    private IWindowSession mSession;
+    public int mSeq;
+
+    public void setSession(IWindowSession session) {
+        mSession = session;
+    }
+
+    @Override
+    public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets,
+            Rect stableInsets, Rect outsets, boolean reportDraw,
+            MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout,
+            boolean alwaysConsumeNavBar, int displayId) {
+        if (reportDraw) {
+            try {
+                mSession.finishDrawing(this);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    @Override
+    public void moved(int newX, int newY) {
+    }
+
+    @Override
+    public void dispatchAppVisibility(boolean visible) {
+    }
+
+    @Override
+    public void dispatchGetNewSurface() {
+    }
+
+    @Override
+    public void windowFocusChanged(boolean hasFocus, boolean touchEnabled) {
+    }
+
+    @Override
+    public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {
+    }
+
+    @Override
+    public void closeSystemDialogs(String reason) {
+    }
+
+    @Override
+    public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, boolean sync) {
+        if (sync) {
+            try {
+                mSession.wallpaperOffsetsComplete(asBinder());
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    @Override
+    public void dispatchDragEvent(DragEvent event) {
+        if (event.getAction() == DragEvent.ACTION_DROP) {
+            try {
+                mSession.reportDropResult(this, false);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    @Override
+    public void updatePointerIcon(float x, float y) {
+        InputManager.getInstance().setPointerIconType(PointerIcon.TYPE_NOT_SPECIFIED);
+    }
+
+    @Override
+    public void dispatchSystemUiVisibilityChanged(int seq, int globalUi,
+            int localValue, int localChanges) {
+        mSeq = seq;
+    }
+
+    @Override
+    public void dispatchWallpaperCommand(String action, int x, int y,
+            int z, Bundle extras, boolean sync) {
+        if (sync) {
+            try {
+                mSession.wallpaperCommandComplete(asBinder(), null);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    @Override
+    public void dispatchWindowShown() {
+    }
+
+    @Override
+    public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
+    }
+
+    @Override
+    public void dispatchPointerCaptureChanged(boolean hasCapture) {
+    }
+}
diff --git a/com/android/internal/view/BaseSurfaceHolder.java b/com/android/internal/view/BaseSurfaceHolder.java
new file mode 100644
index 0000000..32ce0fe
--- /dev/null
+++ b/com/android/internal/view/BaseSurfaceHolder.java
@@ -0,0 +1,244 @@
+/*
+ * 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.internal.view;
+
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import java.util.ArrayList;
+import java.util.concurrent.locks.ReentrantLock;
+
+public abstract class BaseSurfaceHolder implements SurfaceHolder {
+    private static final String TAG = "BaseSurfaceHolder";
+    static final boolean DEBUG = false;
+
+    public final ArrayList<SurfaceHolder.Callback> mCallbacks
+            = new ArrayList<SurfaceHolder.Callback>();
+    SurfaceHolder.Callback[] mGottenCallbacks;
+    boolean mHaveGottenCallbacks;
+    
+    public final ReentrantLock mSurfaceLock = new ReentrantLock();
+    public Surface mSurface = new Surface();
+
+    int mRequestedWidth = -1;
+    int mRequestedHeight = -1;
+    /** @hide */
+    protected int mRequestedFormat = PixelFormat.OPAQUE;
+    int mRequestedType = -1;
+
+    long mLastLockTime = 0;
+    
+    int mType = -1;
+    final Rect mSurfaceFrame = new Rect();
+    Rect mTmpDirty;
+    
+    public abstract void onUpdateSurface();
+    public abstract void onRelayoutContainer();
+    public abstract boolean onAllowLockCanvas();
+    
+    public int getRequestedWidth() {
+        return mRequestedWidth;
+    }
+    
+    public int getRequestedHeight() {
+        return mRequestedHeight;
+    }
+    
+    public int getRequestedFormat() {
+        return mRequestedFormat;
+    }
+    
+    public int getRequestedType() {
+        return mRequestedType;
+    }
+    
+    public void addCallback(Callback callback) {
+        synchronized (mCallbacks) {
+            // This is a linear search, but in practice we'll 
+            // have only a couple callbacks, so it doesn't matter.
+            if (mCallbacks.contains(callback) == false) {      
+                mCallbacks.add(callback);
+            }
+        }
+    }
+
+    public void removeCallback(Callback callback) {
+        synchronized (mCallbacks) {
+            mCallbacks.remove(callback);
+        }
+    }
+
+    public SurfaceHolder.Callback[] getCallbacks() {
+        if (mHaveGottenCallbacks) {
+            return mGottenCallbacks;
+        }
+        
+        synchronized (mCallbacks) {
+            final int N = mCallbacks.size();
+            if (N > 0) {
+                if (mGottenCallbacks == null || mGottenCallbacks.length != N) {
+                    mGottenCallbacks = new SurfaceHolder.Callback[N];
+                }
+                mCallbacks.toArray(mGottenCallbacks);
+            } else {
+                mGottenCallbacks = null;
+            }
+            mHaveGottenCallbacks = true;
+        }
+        
+        return mGottenCallbacks;
+    }
+    
+    public void ungetCallbacks() {
+        mHaveGottenCallbacks = false;
+    }
+    
+    public void setFixedSize(int width, int height) {
+        if (mRequestedWidth != width || mRequestedHeight != height) {
+            mRequestedWidth = width;
+            mRequestedHeight = height;
+            onRelayoutContainer();
+        }
+    }
+
+    public void setSizeFromLayout() {
+        if (mRequestedWidth != -1 || mRequestedHeight != -1) {
+            mRequestedWidth = mRequestedHeight = -1;
+            onRelayoutContainer();
+        }
+    }
+
+    public void setFormat(int format) {
+        if (mRequestedFormat != format) {
+            mRequestedFormat = format;
+            onUpdateSurface();
+        }
+    }
+
+    public void setType(int type) {
+        switch (type) {
+        case SURFACE_TYPE_HARDWARE:
+        case SURFACE_TYPE_GPU:
+            // these are deprecated, treat as "NORMAL"
+            type = SURFACE_TYPE_NORMAL;
+            break;
+        }
+        switch (type) {
+        case SURFACE_TYPE_NORMAL:
+        case SURFACE_TYPE_PUSH_BUFFERS:
+            if (mRequestedType != type) {
+                mRequestedType = type;
+                onUpdateSurface();
+            }
+            break;
+        }
+    }
+
+    @Override
+    public Canvas lockCanvas() {
+        return internalLockCanvas(null, false);
+    }
+
+    @Override
+    public Canvas lockCanvas(Rect dirty) {
+        return internalLockCanvas(dirty, false);
+    }
+
+    @Override
+    public Canvas lockHardwareCanvas() {
+        return internalLockCanvas(null, true);
+    }
+
+    private final Canvas internalLockCanvas(Rect dirty, boolean hardware) {
+        if (mType == SURFACE_TYPE_PUSH_BUFFERS) {
+            throw new BadSurfaceTypeException(
+                    "Surface type is SURFACE_TYPE_PUSH_BUFFERS");
+        }
+        mSurfaceLock.lock();
+
+        if (DEBUG) Log.i(TAG, "Locking canvas..,");
+
+        Canvas c = null;
+        if (onAllowLockCanvas()) {
+            if (dirty == null) {
+                if (mTmpDirty == null) {
+                    mTmpDirty = new Rect();
+                }
+                mTmpDirty.set(mSurfaceFrame);
+                dirty = mTmpDirty;
+            }
+
+            try {
+                if (hardware) {
+                    c = mSurface.lockHardwareCanvas();
+                } else {
+                    c = mSurface.lockCanvas(dirty);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Exception locking surface", e);
+            }
+        }
+
+        if (DEBUG) Log.i(TAG, "Returned canvas: " + c);
+        if (c != null) {
+            mLastLockTime = SystemClock.uptimeMillis();
+            return c;
+        }
+        
+        // If the Surface is not ready to be drawn, then return null,
+        // but throttle calls to this function so it isn't called more
+        // than every 100ms.
+        long now = SystemClock.uptimeMillis();
+        long nextTime = mLastLockTime + 100;
+        if (nextTime > now) {
+            try {
+                Thread.sleep(nextTime-now);
+            } catch (InterruptedException e) {
+            }
+            now = SystemClock.uptimeMillis();
+        }
+        mLastLockTime = now;
+        mSurfaceLock.unlock();
+        
+        return null;
+    }
+
+    public void unlockCanvasAndPost(Canvas canvas) {
+        mSurface.unlockCanvasAndPost(canvas);
+        mSurfaceLock.unlock();
+    }
+
+    public Surface getSurface() {
+        return mSurface;
+    }
+
+    public Rect getSurfaceFrame() {
+        return mSurfaceFrame;
+    }
+
+    public void setSurfaceFrameSize(int width, int height) {
+        mSurfaceFrame.top = 0;
+        mSurfaceFrame.left = 0;
+        mSurfaceFrame.right = width;
+        mSurfaceFrame.bottom = height;
+    }
+};
diff --git a/com/android/internal/view/FloatingActionMode.java b/com/android/internal/view/FloatingActionMode.java
new file mode 100644
index 0000000..497e7b0
--- /dev/null
+++ b/com/android/internal/view/FloatingActionMode.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2015 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.internal.view;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.widget.FloatingToolbar;
+
+import java.util.Arrays;
+
+public final class FloatingActionMode extends ActionMode {
+
+    private static final int MAX_HIDE_DURATION = 3000;
+    private static final int MOVING_HIDE_DELAY = 50;
+
+    @NonNull private final Context mContext;
+    @NonNull private final ActionMode.Callback2 mCallback;
+    @NonNull private final MenuBuilder mMenu;
+    @NonNull private final Rect mContentRect;
+    @NonNull private final Rect mContentRectOnScreen;
+    @NonNull private final Rect mPreviousContentRectOnScreen;
+    @NonNull private final int[] mViewPositionOnScreen;
+    @NonNull private final int[] mPreviousViewPositionOnScreen;
+    @NonNull private final int[] mRootViewPositionOnScreen;
+    @NonNull private final Rect mViewRectOnScreen;
+    @NonNull private final Rect mPreviousViewRectOnScreen;
+    @NonNull private final Rect mScreenRect;
+    @NonNull private final View mOriginatingView;
+    @NonNull private final Point mDisplaySize;
+    private final int mBottomAllowance;
+
+    private final Runnable mMovingOff = new Runnable() {
+        public void run() {
+            if (isViewStillActive()) {
+                mFloatingToolbarVisibilityHelper.setMoving(false);
+                mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
+            }
+        }
+    };
+
+    private final Runnable mHideOff = new Runnable() {
+        public void run() {
+            if (isViewStillActive()) {
+                mFloatingToolbarVisibilityHelper.setHideRequested(false);
+                mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
+            }
+        }
+    };
+
+    @NonNull private FloatingToolbar mFloatingToolbar;
+    @NonNull private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper;
+
+    public FloatingActionMode(
+            Context context, ActionMode.Callback2 callback,
+            View originatingView, FloatingToolbar floatingToolbar) {
+        mContext = Preconditions.checkNotNull(context);
+        mCallback = Preconditions.checkNotNull(callback);
+        mMenu = new MenuBuilder(context).setDefaultShowAsAction(
+                MenuItem.SHOW_AS_ACTION_IF_ROOM);
+        setType(ActionMode.TYPE_FLOATING);
+        mMenu.setCallback(new MenuBuilder.Callback() {
+            @Override
+            public void onMenuModeChange(MenuBuilder menu) {}
+
+            @Override
+            public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+                return mCallback.onActionItemClicked(FloatingActionMode.this, item);
+            }
+        });
+        mContentRect = new Rect();
+        mContentRectOnScreen = new Rect();
+        mPreviousContentRectOnScreen = new Rect();
+        mViewPositionOnScreen = new int[2];
+        mPreviousViewPositionOnScreen = new int[2];
+        mRootViewPositionOnScreen = new int[2];
+        mViewRectOnScreen = new Rect();
+        mPreviousViewRectOnScreen = new Rect();
+        mScreenRect = new Rect();
+        mOriginatingView = Preconditions.checkNotNull(originatingView);
+        mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
+        // Allow the content rect to overshoot a little bit beyond the
+        // bottom view bound if necessary.
+        mBottomAllowance = context.getResources()
+                .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance);
+        mDisplaySize = new Point();
+        setFloatingToolbar(Preconditions.checkNotNull(floatingToolbar));
+    }
+
+    private void setFloatingToolbar(FloatingToolbar floatingToolbar) {
+        mFloatingToolbar = floatingToolbar
+                .setMenu(mMenu)
+                .setOnMenuItemClickListener(item -> mMenu.performItemAction(item, 0));
+        mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar);
+        mFloatingToolbarVisibilityHelper.activate();
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {}
+
+    @Override
+    public void setTitle(int resId) {}
+
+    @Override
+    public void setSubtitle(CharSequence subtitle) {}
+
+    @Override
+    public void setSubtitle(int resId) {}
+
+    @Override
+    public void setCustomView(View view) {}
+
+    @Override
+    public void invalidate() {
+        mCallback.onPrepareActionMode(this, mMenu);
+        invalidateContentRect();  // Will re-layout and show the toolbar if necessary.
+    }
+
+    @Override
+    public void invalidateContentRect() {
+        mCallback.onGetContentRect(this, mOriginatingView, mContentRect);
+        repositionToolbar();
+    }
+
+    public void updateViewLocationInWindow() {
+        mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
+        mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen);
+        mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen);
+        mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
+
+        if (!Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen)
+                || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) {
+            repositionToolbar();
+            mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0];
+            mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1];
+            mPreviousViewRectOnScreen.set(mViewRectOnScreen);
+        }
+    }
+
+    private void repositionToolbar() {
+        mContentRectOnScreen.set(mContentRect);
+
+        // Offset the content rect into screen coordinates, taking into account any transformations
+        // that may be applied to the originating view or its ancestors.
+        final ViewParent parent = mOriginatingView.getParent();
+        if (parent instanceof ViewGroup) {
+            ((ViewGroup) parent).getChildVisibleRect(
+                    mOriginatingView, mContentRectOnScreen,
+                    null /* offset */, true /* forceParentCheck */);
+            mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
+        } else {
+            mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]);
+        }
+
+        if (isContentRectWithinBounds()) {
+            mFloatingToolbarVisibilityHelper.setOutOfBounds(false);
+            // Make sure that content rect is not out of the view's visible bounds.
+            mContentRectOnScreen.set(
+                    Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left),
+                    Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top),
+                    Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right),
+                    Math.min(mContentRectOnScreen.bottom,
+                            mViewRectOnScreen.bottom + mBottomAllowance));
+
+            if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) {
+                // Content rect is moving.
+                mOriginatingView.removeCallbacks(mMovingOff);
+                mFloatingToolbarVisibilityHelper.setMoving(true);
+                mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY);
+
+                mFloatingToolbar.setContentRect(mContentRectOnScreen);
+                mFloatingToolbar.updateLayout();
+            }
+        } else {
+            mFloatingToolbarVisibilityHelper.setOutOfBounds(true);
+            mContentRectOnScreen.setEmpty();
+        }
+        mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
+
+        mPreviousContentRectOnScreen.set(mContentRectOnScreen);
+    }
+
+    private boolean isContentRectWithinBounds() {
+        mContext.getSystemService(WindowManager.class)
+            .getDefaultDisplay().getRealSize(mDisplaySize);
+        mScreenRect.set(0, 0, mDisplaySize.x, mDisplaySize.y);
+
+        return intersectsClosed(mContentRectOnScreen, mScreenRect)
+            && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
+    }
+
+    /*
+     * Same as Rect.intersects, but includes cases where the rectangles touch.
+    */
+    private static boolean intersectsClosed(Rect a, Rect b) {
+         return a.left <= b.right && b.left <= a.right
+                 && a.top <= b.bottom && b.top <= a.bottom;
+    }
+
+    @Override
+    public void hide(long duration) {
+        if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
+            duration = ViewConfiguration.getDefaultActionModeHideDuration();
+        }
+        duration = Math.min(MAX_HIDE_DURATION, duration);
+        mOriginatingView.removeCallbacks(mHideOff);
+        if (duration <= 0) {
+            mHideOff.run();
+        } else {
+            mFloatingToolbarVisibilityHelper.setHideRequested(true);
+            mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
+            mOriginatingView.postDelayed(mHideOff, duration);
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
+        mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
+    }
+
+    @Override
+    public void finish() {
+        reset();
+        mCallback.onDestroyActionMode(this);
+    }
+
+    @Override
+    public Menu getMenu() {
+        return mMenu;
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        return null;
+    }
+
+    @Override
+    public CharSequence getSubtitle() {
+        return null;
+    }
+
+    @Override
+    public View getCustomView() {
+        return null;
+    }
+
+    @Override
+    public MenuInflater getMenuInflater() {
+        return new MenuInflater(mContext);
+    }
+
+    private void reset() {
+        mFloatingToolbar.dismiss();
+        mFloatingToolbarVisibilityHelper.deactivate();
+        mOriginatingView.removeCallbacks(mMovingOff);
+        mOriginatingView.removeCallbacks(mHideOff);
+    }
+
+    private boolean isViewStillActive() {
+        return mOriginatingView.getWindowVisibility() == View.VISIBLE
+                && mOriginatingView.isShown();
+    }
+
+    /**
+     * A helper for showing/hiding the floating toolbar depending on certain states.
+     */
+    private static final class FloatingToolbarVisibilityHelper {
+
+        private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500;
+
+        private final FloatingToolbar mToolbar;
+
+        private boolean mHideRequested;
+        private boolean mMoving;
+        private boolean mOutOfBounds;
+        private boolean mWindowFocused = true;
+
+        private boolean mActive;
+
+        private long mLastShowTime;
+
+        public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
+            mToolbar = Preconditions.checkNotNull(toolbar);
+        }
+
+        public void activate() {
+            mHideRequested = false;
+            mMoving = false;
+            mOutOfBounds = false;
+            mWindowFocused = true;
+
+            mActive = true;
+        }
+
+        public void deactivate() {
+            mActive = false;
+            mToolbar.dismiss();
+        }
+
+        public void setHideRequested(boolean hide) {
+            mHideRequested = hide;
+        }
+
+        public void setMoving(boolean moving) {
+            // Avoid unintended flickering by allowing the toolbar to show long enough before
+            // triggering the 'moving' flag - which signals a hide.
+            final boolean showingLongEnough =
+                System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE;
+            if (!moving || showingLongEnough) {
+                mMoving = moving;
+            }
+        }
+
+        public void setOutOfBounds(boolean outOfBounds) {
+            mOutOfBounds = outOfBounds;
+        }
+
+        public void setWindowFocused(boolean windowFocused) {
+            mWindowFocused = windowFocused;
+        }
+
+        public void updateToolbarVisibility() {
+            if (!mActive) {
+                return;
+            }
+
+            if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
+                mToolbar.hide();
+            } else {
+                mToolbar.show();
+                mLastShowTime = System.currentTimeMillis();
+            }
+        }
+    }
+}
diff --git a/com/android/internal/view/IInputConnectionWrapper.java b/com/android/internal/view/IInputConnectionWrapper.java
new file mode 100644
index 0000000..28291ae
--- /dev/null
+++ b/com/android/internal/view/IInputConnectionWrapper.java
@@ -0,0 +1,640 @@
+/*
+ * Copyright (C) 2008 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.internal.view;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.SomeArgs;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionInspector;
+import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
+import android.view.inputmethod.InputContentInfo;
+
+public abstract class IInputConnectionWrapper extends IInputContext.Stub {
+    private static final String TAG = "IInputConnectionWrapper";
+    private static final boolean DEBUG = false;
+
+    private static final int DO_GET_TEXT_AFTER_CURSOR = 10;
+    private static final int DO_GET_TEXT_BEFORE_CURSOR = 20;
+    private static final int DO_GET_SELECTED_TEXT = 25;
+    private static final int DO_GET_CURSOR_CAPS_MODE = 30;
+    private static final int DO_GET_EXTRACTED_TEXT = 40;
+    private static final int DO_COMMIT_TEXT = 50;
+    private static final int DO_COMMIT_COMPLETION = 55;
+    private static final int DO_COMMIT_CORRECTION = 56;
+    private static final int DO_SET_SELECTION = 57;
+    private static final int DO_PERFORM_EDITOR_ACTION = 58;
+    private static final int DO_PERFORM_CONTEXT_MENU_ACTION = 59;
+    private static final int DO_SET_COMPOSING_TEXT = 60;
+    private static final int DO_SET_COMPOSING_REGION = 63;
+    private static final int DO_FINISH_COMPOSING_TEXT = 65;
+    private static final int DO_SEND_KEY_EVENT = 70;
+    private static final int DO_DELETE_SURROUNDING_TEXT = 80;
+    private static final int DO_DELETE_SURROUNDING_TEXT_IN_CODE_POINTS = 81;
+    private static final int DO_BEGIN_BATCH_EDIT = 90;
+    private static final int DO_END_BATCH_EDIT = 95;
+    private static final int DO_PERFORM_PRIVATE_COMMAND = 120;
+    private static final int DO_CLEAR_META_KEY_STATES = 130;
+    private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140;
+    private static final int DO_CLOSE_CONNECTION = 150;
+    private static final int DO_COMMIT_CONTENT = 160;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private InputConnection mInputConnection;
+
+    private Looper mMainLooper;
+    private Handler mH;
+    private Object mLock = new Object();
+    @GuardedBy("mLock")
+    private boolean mFinished = false;
+
+    class MyHandler extends Handler {
+        MyHandler(Looper looper) {
+            super(looper);
+        }
+        
+        @Override
+        public void handleMessage(Message msg) {
+            executeMessage(msg);
+        }
+    }
+    
+    public IInputConnectionWrapper(Looper mainLooper, @NonNull InputConnection inputConnection) {
+        mInputConnection = inputConnection;
+        mMainLooper = mainLooper;
+        mH = new MyHandler(mMainLooper);
+    }
+
+    @Nullable
+    public InputConnection getInputConnection() {
+        synchronized (mLock) {
+            return mInputConnection;
+        }
+    }
+
+    protected boolean isFinished() {
+        synchronized (mLock) {
+            return mFinished;
+        }
+    }
+
+    abstract protected boolean isActive();
+
+    /**
+     * Called when the user took some actions that should be taken into consideration to update the
+     * LRU list for input method rotation.
+     */
+    abstract protected void onUserAction();
+
+    public void getTextAfterCursor(int length, int flags, int seq, IInputContextCallback callback) {
+        dispatchMessage(obtainMessageIISC(DO_GET_TEXT_AFTER_CURSOR, length, flags, seq, callback));
+    }
+    
+    public void getTextBeforeCursor(int length, int flags, int seq, IInputContextCallback callback) {
+        dispatchMessage(obtainMessageIISC(DO_GET_TEXT_BEFORE_CURSOR, length, flags, seq, callback));
+    }
+
+    public void getSelectedText(int flags, int seq, IInputContextCallback callback) {
+        dispatchMessage(obtainMessageISC(DO_GET_SELECTED_TEXT, flags, seq, callback));
+    }
+
+    public void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback) {
+        dispatchMessage(obtainMessageISC(DO_GET_CURSOR_CAPS_MODE, reqModes, seq, callback));
+    }
+
+    public void getExtractedText(ExtractedTextRequest request,
+            int flags, int seq, IInputContextCallback callback) {
+        dispatchMessage(obtainMessageIOSC(DO_GET_EXTRACTED_TEXT, flags,
+                request, seq, callback));
+    }
+    
+    public void commitText(CharSequence text, int newCursorPosition) {
+        dispatchMessage(obtainMessageIO(DO_COMMIT_TEXT, newCursorPosition, text));
+    }
+
+    public void commitCompletion(CompletionInfo text) {
+        dispatchMessage(obtainMessageO(DO_COMMIT_COMPLETION, text));
+    }
+
+    public void commitCorrection(CorrectionInfo info) {
+        dispatchMessage(obtainMessageO(DO_COMMIT_CORRECTION, info));
+    }
+
+    public void setSelection(int start, int end) {
+        dispatchMessage(obtainMessageII(DO_SET_SELECTION, start, end));
+    }
+
+    public void performEditorAction(int id) {
+        dispatchMessage(obtainMessageII(DO_PERFORM_EDITOR_ACTION, id, 0));
+    }
+    
+    public void performContextMenuAction(int id) {
+        dispatchMessage(obtainMessageII(DO_PERFORM_CONTEXT_MENU_ACTION, id, 0));
+    }
+    
+    public void setComposingRegion(int start, int end) {
+        dispatchMessage(obtainMessageII(DO_SET_COMPOSING_REGION, start, end));
+    }
+
+    public void setComposingText(CharSequence text, int newCursorPosition) {
+        dispatchMessage(obtainMessageIO(DO_SET_COMPOSING_TEXT, newCursorPosition, text));
+    }
+
+    public void finishComposingText() {
+        dispatchMessage(obtainMessage(DO_FINISH_COMPOSING_TEXT));
+    }
+
+    public void sendKeyEvent(KeyEvent event) {
+        dispatchMessage(obtainMessageO(DO_SEND_KEY_EVENT, event));
+    }
+
+    public void clearMetaKeyStates(int states) {
+        dispatchMessage(obtainMessageII(DO_CLEAR_META_KEY_STATES, states, 0));
+    }
+
+    public void deleteSurroundingText(int beforeLength, int afterLength) {
+        dispatchMessage(obtainMessageII(DO_DELETE_SURROUNDING_TEXT,
+                beforeLength, afterLength));
+    }
+
+    public void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
+        dispatchMessage(obtainMessageII(DO_DELETE_SURROUNDING_TEXT_IN_CODE_POINTS,
+                beforeLength, afterLength));
+    }
+
+    public void beginBatchEdit() {
+        dispatchMessage(obtainMessage(DO_BEGIN_BATCH_EDIT));
+    }
+
+    public void endBatchEdit() {
+        dispatchMessage(obtainMessage(DO_END_BATCH_EDIT));
+    }
+
+    public void performPrivateCommand(String action, Bundle data) {
+        dispatchMessage(obtainMessageOO(DO_PERFORM_PRIVATE_COMMAND, action, data));
+    }
+
+    public void requestUpdateCursorAnchorInfo(int cursorUpdateMode, int seq,
+            IInputContextCallback callback) {
+        dispatchMessage(obtainMessageISC(DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO, cursorUpdateMode,
+                seq, callback));
+    }
+
+    public void closeConnection() {
+        dispatchMessage(obtainMessage(DO_CLOSE_CONNECTION));
+    }
+
+    public void commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts,
+            int seq, IInputContextCallback callback) {
+        dispatchMessage(obtainMessageIOOSC(DO_COMMIT_CONTENT, flags, inputContentInfo, opts, seq,
+                callback));
+    }
+
+    void dispatchMessage(Message msg) {
+        // If we are calling this from the main thread, then we can call
+        // right through.  Otherwise, we need to send the message to the
+        // main thread.
+        if (Looper.myLooper() == mMainLooper) {
+            executeMessage(msg);
+            msg.recycle();
+            return;
+        }
+        
+        mH.sendMessage(msg);
+    }
+
+    void executeMessage(Message msg) {
+        switch (msg.what) {
+            case DO_GET_TEXT_AFTER_CURSOR: {
+                SomeArgs args = (SomeArgs)msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "getTextAfterCursor on inactive InputConnection");
+                        callback.setTextAfterCursor(null, callbackSeq);
+                        return;
+                    }
+                    callback.setTextAfterCursor(ic.getTextAfterCursor(
+                            msg.arg1, msg.arg2), callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling setTextAfterCursor", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_GET_TEXT_BEFORE_CURSOR: {
+                SomeArgs args = (SomeArgs)msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "getTextBeforeCursor on inactive InputConnection");
+                        callback.setTextBeforeCursor(null, callbackSeq);
+                        return;
+                    }
+                    callback.setTextBeforeCursor(ic.getTextBeforeCursor(
+                            msg.arg1, msg.arg2), callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling setTextBeforeCursor", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_GET_SELECTED_TEXT: {
+                SomeArgs args = (SomeArgs)msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "getSelectedText on inactive InputConnection");
+                        callback.setSelectedText(null, callbackSeq);
+                        return;
+                    }
+                    callback.setSelectedText(ic.getSelectedText(
+                            msg.arg1), callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling setSelectedText", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_GET_CURSOR_CAPS_MODE: {
+                SomeArgs args = (SomeArgs)msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "getCursorCapsMode on inactive InputConnection");
+                        callback.setCursorCapsMode(0, callbackSeq);
+                        return;
+                    }
+                    callback.setCursorCapsMode(ic.getCursorCapsMode(msg.arg1),
+                            callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling setCursorCapsMode", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_GET_EXTRACTED_TEXT: {
+                SomeArgs args = (SomeArgs)msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "getExtractedText on inactive InputConnection");
+                        callback.setExtractedText(null, callbackSeq);
+                        return;
+                    }
+                    callback.setExtractedText(ic.getExtractedText(
+                            (ExtractedTextRequest)args.arg1, msg.arg1), callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling setExtractedText", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_COMMIT_TEXT: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "commitText on inactive InputConnection");
+                    return;
+                }
+                ic.commitText((CharSequence)msg.obj, msg.arg1);
+                onUserAction();
+                return;
+            }
+            case DO_SET_SELECTION: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "setSelection on inactive InputConnection");
+                    return;
+                }
+                ic.setSelection(msg.arg1, msg.arg2);
+                return;
+            }
+            case DO_PERFORM_EDITOR_ACTION: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "performEditorAction on inactive InputConnection");
+                    return;
+                }
+                ic.performEditorAction(msg.arg1);
+                return;
+            }
+            case DO_PERFORM_CONTEXT_MENU_ACTION: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "performContextMenuAction on inactive InputConnection");
+                    return;
+                }
+                ic.performContextMenuAction(msg.arg1);
+                return;
+            }
+            case DO_COMMIT_COMPLETION: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "commitCompletion on inactive InputConnection");
+                    return;
+                }
+                ic.commitCompletion((CompletionInfo)msg.obj);
+                return;
+            }
+            case DO_COMMIT_CORRECTION: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "commitCorrection on inactive InputConnection");
+                    return;
+                }
+                ic.commitCorrection((CorrectionInfo)msg.obj);
+                return;
+            }
+            case DO_SET_COMPOSING_TEXT: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "setComposingText on inactive InputConnection");
+                    return;
+                }
+                ic.setComposingText((CharSequence)msg.obj, msg.arg1);
+                onUserAction();
+                return;
+            }
+            case DO_SET_COMPOSING_REGION: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "setComposingRegion on inactive InputConnection");
+                    return;
+                }
+                ic.setComposingRegion(msg.arg1, msg.arg2);
+                return;
+            }
+            case DO_FINISH_COMPOSING_TEXT: {
+                if (isFinished()) {
+                    // In this case, #finishComposingText() is guaranteed to be called already.
+                    // There should be no negative impact if we ignore this call silently.
+                    if (DEBUG) {
+                        Log.w(TAG, "Bug 35301295: Redundant finishComposingText.");
+                    }
+                    return;
+                }
+                InputConnection ic = getInputConnection();
+                // Note we do NOT check isActive() here, because this is safe
+                // for an IME to call at any time, and we need to allow it
+                // through to clean up our state after the IME has switched to
+                // another client.
+                if (ic == null) {
+                    Log.w(TAG, "finishComposingText on inactive InputConnection");
+                    return;
+                }
+                ic.finishComposingText();
+                return;
+            }
+            case DO_SEND_KEY_EVENT: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "sendKeyEvent on inactive InputConnection");
+                    return;
+                }
+                ic.sendKeyEvent((KeyEvent)msg.obj);
+                onUserAction();
+                return;
+            }
+            case DO_CLEAR_META_KEY_STATES: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "clearMetaKeyStates on inactive InputConnection");
+                    return;
+                }
+                ic.clearMetaKeyStates(msg.arg1);
+                return;
+            }
+            case DO_DELETE_SURROUNDING_TEXT: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "deleteSurroundingText on inactive InputConnection");
+                    return;
+                }
+                ic.deleteSurroundingText(msg.arg1, msg.arg2);
+                return;
+            }
+            case DO_DELETE_SURROUNDING_TEXT_IN_CODE_POINTS: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "deleteSurroundingTextInCodePoints on inactive InputConnection");
+                    return;
+                }
+                ic.deleteSurroundingTextInCodePoints(msg.arg1, msg.arg2);
+                return;
+            }
+            case DO_BEGIN_BATCH_EDIT: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "beginBatchEdit on inactive InputConnection");
+                    return;
+                }
+                ic.beginBatchEdit();
+                return;
+            }
+            case DO_END_BATCH_EDIT: {
+                InputConnection ic = getInputConnection();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "endBatchEdit on inactive InputConnection");
+                    return;
+                }
+                ic.endBatchEdit();
+                return;
+            }
+            case DO_PERFORM_PRIVATE_COMMAND: {
+                final SomeArgs args = (SomeArgs) msg.obj;
+                try {
+                    final String action = (String) args.arg1;
+                    final Bundle data = (Bundle) args.arg2;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "performPrivateCommand on inactive InputConnection");
+                        return;
+                    }
+                    ic.performPrivateCommand(action, data);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO: {
+                SomeArgs args = (SomeArgs)msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "requestCursorAnchorInfo on inactive InputConnection");
+                        callback.setRequestUpdateCursorAnchorInfoResult(false, callbackSeq);
+                        return;
+                    }
+                    callback.setRequestUpdateCursorAnchorInfoResult(
+                            ic.requestCursorUpdates(msg.arg1), callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling requestCursorAnchorInfo", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+            case DO_CLOSE_CONNECTION: {
+                // Note that we do not need to worry about race condition here, because 1) mFinished
+                // is updated only inside this block, and 2) the code here is running on a Handler
+                // hence we assume multiple DO_CLOSE_CONNECTION messages will not be handled at the
+                // same time.
+                if (isFinished()) {
+                    return;
+                }
+                try {
+                    InputConnection ic = getInputConnection();
+                    // Note we do NOT check isActive() here, because this is safe
+                    // for an IME to call at any time, and we need to allow it
+                    // through to clean up our state after the IME has switched to
+                    // another client.
+                    if (ic == null) {
+                        return;
+                    }
+                    @MissingMethodFlags
+                    final int missingMethods = InputConnectionInspector.getMissingMethodFlags(ic);
+                    if ((missingMethods & MissingMethodFlags.CLOSE_CONNECTION) == 0) {
+                        ic.closeConnection();
+                    }
+                } finally {
+                    synchronized (mLock) {
+                        mInputConnection = null;
+                        mFinished = true;
+                    }
+                }
+                return;
+            }
+            case DO_COMMIT_CONTENT: {
+                final int flags = msg.arg1;
+                SomeArgs args = (SomeArgs) msg.obj;
+                try {
+                    final IInputContextCallback callback = (IInputContextCallback) args.arg6;
+                    final int callbackSeq = args.argi6;
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "commitContent on inactive InputConnection");
+                        callback.setCommitContentResult(false, callbackSeq);
+                        return;
+                    }
+                    final InputContentInfo inputContentInfo = (InputContentInfo) args.arg1;
+                    if (inputContentInfo == null || !inputContentInfo.validate()) {
+                        Log.w(TAG, "commitContent with invalid inputContentInfo="
+                                + inputContentInfo);
+                        callback.setCommitContentResult(false, callbackSeq);
+                        return;
+                    }
+                    final boolean result =
+                            ic.commitContent(inputContentInfo, flags, (Bundle) args.arg2);
+                    callback.setCommitContentResult(result, callbackSeq);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Got RemoteException calling commitContent", e);
+                } finally {
+                    args.recycle();
+                }
+                return;
+            }
+        }
+        Log.w(TAG, "Unhandled message code: " + msg.what);
+    }
+
+    Message obtainMessage(int what) {
+        return mH.obtainMessage(what);
+    }
+
+    Message obtainMessageII(int what, int arg1, int arg2) {
+        return mH.obtainMessage(what, arg1, arg2);
+    }
+
+    Message obtainMessageO(int what, Object arg1) {
+        return mH.obtainMessage(what, 0, 0, arg1);
+    }
+
+    Message obtainMessageISC(int what, int arg1, int callbackSeq, IInputContextCallback callback) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.arg6 = callback;
+        args.argi6 = callbackSeq;
+        return mH.obtainMessage(what, arg1, 0, args);
+    }
+
+    Message obtainMessageIISC(int what, int arg1, int arg2, int callbackSeq,
+            IInputContextCallback callback) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.arg6 = callback;
+        args.argi6 = callbackSeq;
+        return mH.obtainMessage(what, arg1, arg2, args);
+    }
+
+    Message obtainMessageIOOSC(int what, int arg1, Object objArg1, Object objArg2, int callbackSeq,
+            IInputContextCallback callback) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.arg1 = objArg1;
+        args.arg2 = objArg2;
+        args.arg6 = callback;
+        args.argi6 = callbackSeq;
+        return mH.obtainMessage(what, arg1, 0, args);
+    }
+
+    Message obtainMessageIOSC(int what, int arg1, Object arg2, int callbackSeq,
+            IInputContextCallback callback) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg2;
+        args.arg6 = callback;
+        args.argi6 = callbackSeq;
+        return mH.obtainMessage(what, arg1, 0, args);
+    }
+
+    Message obtainMessageIO(int what, int arg1, Object arg2) {
+        return mH.obtainMessage(what, arg1, 0, arg2);
+    }
+
+    Message obtainMessageOO(int what, Object arg1, Object arg2) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        return mH.obtainMessage(what, 0, 0, args);
+    }
+}
diff --git a/com/android/internal/view/InputBindResult.java b/com/android/internal/view/InputBindResult.java
new file mode 100644
index 0000000..3a3e56d
--- /dev/null
+++ b/com/android/internal/view/InputBindResult.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2007-2008 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.internal.view;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.InputChannel;
+
+/**
+ * Bundle of information returned by input method manager about a successful
+ * binding to an input method.
+ */
+public final class InputBindResult implements Parcelable {
+    static final String TAG = "InputBindResult";
+    
+    /**
+     * The input method service.
+     */
+    public final IInputMethodSession method;
+
+    /**
+     * The input channel used to send input events to this IME.
+     */
+    public final InputChannel channel;
+
+    /**
+     * The ID for this input method, as found in InputMethodInfo; null if
+     * no input method will be bound.
+     */
+    public final String id;
+    
+    /**
+     * Sequence number of this binding.
+     */
+    public final int sequence;
+
+    /**
+     * Sequence number of user action notification.
+     */
+    public final int userActionNotificationSequenceNumber;
+
+    public InputBindResult(IInputMethodSession _method, InputChannel _channel,
+            String _id, int _sequence, int _userActionNotificationSequenceNumber) {
+        method = _method;
+        channel = _channel;
+        id = _id;
+        sequence = _sequence;
+        userActionNotificationSequenceNumber = _userActionNotificationSequenceNumber;
+    }
+    
+    InputBindResult(Parcel source) {
+        method = IInputMethodSession.Stub.asInterface(source.readStrongBinder());
+        if (source.readInt() != 0) {
+            channel = InputChannel.CREATOR.createFromParcel(source);
+        } else {
+            channel = null;
+        }
+        id = source.readString();
+        sequence = source.readInt();
+        userActionNotificationSequenceNumber = source.readInt();
+    }
+
+    @Override
+    public String toString() {
+        return "InputBindResult{" + method + " " + id
+                + " sequence:" + sequence
+                + " userActionNotificationSequenceNumber:" + userActionNotificationSequenceNumber
+                + "}";
+    }
+
+    /**
+     * Used to package this object into a {@link Parcel}.
+     *
+     * @param dest The {@link Parcel} to be written.
+     * @param flags The flags used for parceling.
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeStrongInterface(method);
+        if (channel != null) {
+            dest.writeInt(1);
+            channel.writeToParcel(dest, flags);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeString(id);
+        dest.writeInt(sequence);
+        dest.writeInt(userActionNotificationSequenceNumber);
+    }
+
+    /**
+     * Used to make this class parcelable.
+     */
+    public static final Parcelable.Creator<InputBindResult> CREATOR =
+            new Parcelable.Creator<InputBindResult>() {
+        @Override
+        public InputBindResult createFromParcel(Parcel source) {
+            return new InputBindResult(source);
+        }
+
+        @Override
+        public InputBindResult[] newArray(int size) {
+            return new InputBindResult[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return channel != null ? channel.describeContents() : 0;
+    }
+}
diff --git a/com/android/internal/view/InputConnectionWrapper.java b/com/android/internal/view/InputConnectionWrapper.java
new file mode 100644
index 0000000..cc0ef75
--- /dev/null
+++ b/com/android/internal/view/InputConnectionWrapper.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2008 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.internal.view;
+
+import android.annotation.NonNull;
+import android.inputmethodservice.AbstractInputMethodService;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionInspector;
+import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
+import android.view.inputmethod.InputContentInfo;
+
+import java.lang.ref.WeakReference;
+
+public class InputConnectionWrapper implements InputConnection {
+    private static final int MAX_WAIT_TIME_MILLIS = 2000;
+    private final IInputContext mIInputContext;
+    @NonNull
+    private final WeakReference<AbstractInputMethodService> mInputMethodService;
+
+    @MissingMethodFlags
+    private final int mMissingMethods;
+
+    static class InputContextCallback extends IInputContextCallback.Stub {
+        private static final String TAG = "InputConnectionWrapper.ICC";
+        public int mSeq;
+        public boolean mHaveValue;
+        public CharSequence mTextBeforeCursor;
+        public CharSequence mTextAfterCursor;
+        public CharSequence mSelectedText;
+        public ExtractedText mExtractedText;
+        public int mCursorCapsMode;
+        public boolean mRequestUpdateCursorAnchorInfoResult;
+        public boolean mCommitContentResult;
+
+        // A 'pool' of one InputContextCallback.  Each ICW request will attempt to gain
+        // exclusive access to this object.
+        private static InputContextCallback sInstance = new InputContextCallback();
+        private static int sSequenceNumber = 1;
+        
+        /**
+         * Returns an InputContextCallback object that is guaranteed not to be in use by
+         * any other thread.  The returned object's 'have value' flag is cleared and its expected
+         * sequence number is set to a new integer.  We use a sequence number so that replies that
+         * occur after a timeout has expired are not interpreted as replies to a later request.
+         */
+        private static InputContextCallback getInstance() {
+            synchronized (InputContextCallback.class) {
+                // Return sInstance if it's non-null, otherwise construct a new callback
+                InputContextCallback callback;
+                if (sInstance != null) {
+                    callback = sInstance;
+                    sInstance = null;
+                    
+                    // Reset the callback
+                    callback.mHaveValue = false;
+                } else {
+                    callback = new InputContextCallback();
+                }
+                
+                // Set the sequence number
+                callback.mSeq = sSequenceNumber++;
+                return callback;
+            }
+        }
+        
+        /**
+         * Makes the given InputContextCallback available for use in the future.
+         */
+        private void dispose() {
+            synchronized (InputContextCallback.class) {
+                // If sInstance is non-null, just let this object be garbage-collected
+                if (sInstance == null) {
+                    // Allow any objects being held to be gc'ed
+                    mTextAfterCursor = null;
+                    mTextBeforeCursor = null;
+                    mExtractedText = null;
+                    sInstance = this;
+                }
+            }
+        }
+        
+        public void setTextBeforeCursor(CharSequence textBeforeCursor, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mTextBeforeCursor = textBeforeCursor;
+                    mHaveValue = true;
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setTextBeforeCursor, ignoring.");
+                }
+            }
+        }
+
+        public void setTextAfterCursor(CharSequence textAfterCursor, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mTextAfterCursor = textAfterCursor;
+                    mHaveValue = true;
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setTextAfterCursor, ignoring.");
+                }
+            }
+        }
+
+        public void setSelectedText(CharSequence selectedText, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mSelectedText = selectedText;
+                    mHaveValue = true;
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setSelectedText, ignoring.");
+                }
+            }
+        }
+
+        public void setCursorCapsMode(int capsMode, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mCursorCapsMode = capsMode; 
+                    mHaveValue = true;  
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setCursorCapsMode, ignoring.");
+                }
+            }
+        }
+
+        public void setExtractedText(ExtractedText extractedText, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mExtractedText = extractedText;
+                    mHaveValue = true;
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setExtractedText, ignoring.");
+                }
+            }
+        }
+
+        public void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mRequestUpdateCursorAnchorInfoResult = result;
+                    mHaveValue = true;
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setCursorAnchorInfoRequestResult, ignoring.");
+                }
+            }
+        }
+
+        public void setCommitContentResult(boolean result, int seq) {
+            synchronized (this) {
+                if (seq == mSeq) {
+                    mCommitContentResult = result;
+                    mHaveValue = true;
+                    notifyAll();
+                } else {
+                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+                            + ") in setCommitContentResult, ignoring.");
+                }
+            }
+        }
+
+        /**
+         * Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds.
+         * 
+         * <p>The caller must be synchronized on this callback object.
+         */
+        void waitForResultLocked() {
+            long startTime = SystemClock.uptimeMillis();
+            long endTime = startTime + MAX_WAIT_TIME_MILLIS;
+
+            while (!mHaveValue) {
+                long remainingTime = endTime - SystemClock.uptimeMillis();
+                if (remainingTime <= 0) {
+                    Log.w(TAG, "Timed out waiting on IInputContextCallback");
+                    return;
+                }
+                try {
+                    wait(remainingTime);
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+    public InputConnectionWrapper(
+            @NonNull WeakReference<AbstractInputMethodService> inputMethodService,
+            IInputContext inputContext, @MissingMethodFlags final int missingMethods) {
+        mInputMethodService = inputMethodService;
+        mIInputContext = inputContext;
+        mMissingMethods = missingMethods;
+    }
+
+    public CharSequence getTextAfterCursor(int length, int flags) {
+        CharSequence value = null;
+        try {
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.getTextAfterCursor(length, flags, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    value = callback.mTextAfterCursor;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return null;
+        }
+        return value;
+    }
+    
+    public CharSequence getTextBeforeCursor(int length, int flags) {
+        CharSequence value = null;
+        try {
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.getTextBeforeCursor(length, flags, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    value = callback.mTextBeforeCursor;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return null;
+        }
+        return value;
+    }
+
+    public CharSequence getSelectedText(int flags) {
+        if (isMethodMissing(MissingMethodFlags.GET_SELECTED_TEXT)) {
+            // This method is not implemented.
+            return null;
+        }
+        CharSequence value = null;
+        try {
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.getSelectedText(flags, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    value = callback.mSelectedText;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return null;
+        }
+        return value;
+    }
+
+    public int getCursorCapsMode(int reqModes) {
+        int value = 0;
+        try {
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    value = callback.mCursorCapsMode;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return 0;
+        }
+        return value;
+    }
+
+    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+        ExtractedText value = null;
+        try {
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.getExtractedText(request, flags, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    value = callback.mExtractedText;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return null;
+        }
+        return value;
+    }
+    
+    public boolean commitText(CharSequence text, int newCursorPosition) {
+        try {
+            mIInputContext.commitText(text, newCursorPosition);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean commitCompletion(CompletionInfo text) {
+        if (isMethodMissing(MissingMethodFlags.COMMIT_CORRECTION)) {
+            // This method is not implemented.
+            return false;
+        }
+        try {
+            mIInputContext.commitCompletion(text);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean commitCorrection(CorrectionInfo correctionInfo) {
+        try {
+            mIInputContext.commitCorrection(correctionInfo);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean setSelection(int start, int end) {
+        try {
+            mIInputContext.setSelection(start, end);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+    
+    public boolean performEditorAction(int actionCode) {
+        try {
+            mIInputContext.performEditorAction(actionCode);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+    
+    public boolean performContextMenuAction(int id) {
+        try {
+            mIInputContext.performContextMenuAction(id);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean setComposingRegion(int start, int end) {
+        if (isMethodMissing(MissingMethodFlags.SET_COMPOSING_REGION)) {
+            // This method is not implemented.
+            return false;
+        }
+        try {
+            mIInputContext.setComposingRegion(start, end);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean setComposingText(CharSequence text, int newCursorPosition) {
+        try {
+            mIInputContext.setComposingText(text, newCursorPosition);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean finishComposingText() {
+        try {
+            mIInputContext.finishComposingText();
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean beginBatchEdit() {
+        try {
+            mIInputContext.beginBatchEdit();
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+    
+    public boolean endBatchEdit() {
+        try {
+            mIInputContext.endBatchEdit();
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+    
+    public boolean sendKeyEvent(KeyEvent event) {
+        try {
+            mIInputContext.sendKeyEvent(event);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean clearMetaKeyStates(int states) {
+        try {
+            mIInputContext.clearMetaKeyStates(states);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+        try {
+            mIInputContext.deleteSurroundingText(beforeLength, afterLength);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
+        if (isMethodMissing(MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS)) {
+            // This method is not implemented.
+            return false;
+        }
+        try {
+            mIInputContext.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean reportFullscreenMode(boolean enabled) {
+        // Nothing should happen when called from input method.
+        return false;
+    }
+
+    public boolean performPrivateCommand(String action, Bundle data) {
+        try {
+            mIInputContext.performPrivateCommand(action, data);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public boolean requestCursorUpdates(int cursorUpdateMode) {
+        boolean result = false;
+        if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) {
+            // This method is not implemented.
+            return false;
+        }
+        try {
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    result = callback.mRequestUpdateCursorAnchorInfoResult;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return false;
+        }
+        return result;
+    }
+
+    public Handler getHandler() {
+        // Nothing should happen when called from input method.
+        return null;
+    }
+
+    public void closeConnection() {
+        // Nothing should happen when called from input method.
+    }
+
+    public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
+        boolean result = false;
+        if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) {
+            // This method is not implemented.
+            return false;
+        }
+        try {
+            if ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+                final AbstractInputMethodService inputMethodService = mInputMethodService.get();
+                if (inputMethodService == null) {
+                    // This basically should not happen, because it's the the caller of this method.
+                    return false;
+                }
+                inputMethodService.exposeContent(inputContentInfo, this);
+            }
+
+            InputContextCallback callback = InputContextCallback.getInstance();
+            mIInputContext.commitContent(inputContentInfo, flags, opts, callback.mSeq, callback);
+            synchronized (callback) {
+                callback.waitForResultLocked();
+                if (callback.mHaveValue) {
+                    result = callback.mCommitContentResult;
+                }
+            }
+            callback.dispose();
+        } catch (RemoteException e) {
+            return false;
+        }
+        return result;
+    }
+
+    private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
+        return (mMissingMethods & methodFlag) == methodFlag;
+    }
+
+    @Override
+    public String toString() {
+        return "InputConnectionWrapper{idHash=#"
+                + Integer.toHexString(System.identityHashCode(this))
+                + " mMissingMethods="
+                + InputConnectionInspector.getMissingMethodFlagsAsString(mMissingMethods) + "}";
+    }
+}
diff --git a/com/android/internal/view/InputMethodClient.java b/com/android/internal/view/InputMethodClient.java
new file mode 100644
index 0000000..bbd33a2
--- /dev/null
+++ b/com/android/internal/view/InputMethodClient.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2015 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.internal.view;
+
+import android.annotation.IntDef;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+public final class InputMethodClient {
+    public static final int START_INPUT_REASON_UNSPECIFIED = 0;
+    public static final int START_INPUT_REASON_WINDOW_FOCUS_GAIN = 1;
+    public static final int START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY = 2;
+    public static final int START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API = 3;
+    public static final int START_INPUT_REASON_CHECK_FOCUS = 4;
+    public static final int START_INPUT_REASON_BOUND_TO_IMMS = 5;
+    public static final int START_INPUT_REASON_UNBOUND_FROM_IMMS = 6;
+    public static final int START_INPUT_REASON_ACTIVATED_BY_IMMS = 7;
+    public static final int START_INPUT_REASON_DEACTIVATED_BY_IMMS = 8;
+    public static final int START_INPUT_REASON_SESSION_CREATED_BY_IME = 9;
+
+    @Retention(SOURCE)
+    @IntDef({START_INPUT_REASON_UNSPECIFIED, START_INPUT_REASON_WINDOW_FOCUS_GAIN,
+            START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY,
+            START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API, START_INPUT_REASON_CHECK_FOCUS,
+            START_INPUT_REASON_BOUND_TO_IMMS, START_INPUT_REASON_ACTIVATED_BY_IMMS,
+            START_INPUT_REASON_DEACTIVATED_BY_IMMS, START_INPUT_REASON_SESSION_CREATED_BY_IME})
+    public @interface StartInputReason {}
+
+    public static String getStartInputReason(@StartInputReason final int reason) {
+        switch (reason) {
+            case START_INPUT_REASON_UNSPECIFIED:
+                return "UNSPECIFIED";
+            case START_INPUT_REASON_WINDOW_FOCUS_GAIN:
+                return "WINDOW_FOCUS_GAIN";
+            case START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY:
+                return "WINDOW_FOCUS_GAIN_REPORT_ONLY";
+            case START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API:
+                return "APP_CALLED_RESTART_INPUT_API";
+            case START_INPUT_REASON_CHECK_FOCUS:
+                return "CHECK_FOCUS";
+            case START_INPUT_REASON_BOUND_TO_IMMS:
+                return "BOUND_TO_IMMS";
+            case START_INPUT_REASON_UNBOUND_FROM_IMMS:
+                return "UNBOUND_FROM_IMMS";
+            case START_INPUT_REASON_ACTIVATED_BY_IMMS:
+                return "ACTIVATED_BY_IMMS";
+            case START_INPUT_REASON_DEACTIVATED_BY_IMMS:
+                return "DEACTIVATED_BY_IMMS";
+            case START_INPUT_REASON_SESSION_CREATED_BY_IME:
+                return "SESSION_CREATED_BY_IME";
+            default:
+                return "Unknown=" + reason;
+        }
+    }
+
+    public static final int UNBIND_REASON_UNSPECIFIED = 0;
+    public static final int UNBIND_REASON_SWITCH_CLIENT = 1;
+    public static final int UNBIND_REASON_SWITCH_IME = 2;
+    public static final int UNBIND_REASON_DISCONNECT_IME = 3;
+    public static final int UNBIND_REASON_NO_IME = 4;
+    public static final int UNBIND_REASON_SWITCH_IME_FAILED = 5;
+    public static final int UNBIND_REASON_SWITCH_USER = 6;
+
+    @Retention(SOURCE)
+    @IntDef({UNBIND_REASON_UNSPECIFIED, UNBIND_REASON_SWITCH_CLIENT, UNBIND_REASON_SWITCH_IME,
+            UNBIND_REASON_DISCONNECT_IME, UNBIND_REASON_NO_IME, UNBIND_REASON_SWITCH_IME_FAILED,
+            UNBIND_REASON_SWITCH_USER})
+    public @interface UnbindReason {}
+
+    public static String getUnbindReason(@UnbindReason final int reason) {
+        switch (reason) {
+            case UNBIND_REASON_UNSPECIFIED:
+                return "UNSPECIFIED";
+            case UNBIND_REASON_SWITCH_CLIENT:
+                return "SWITCH_CLIENT";
+            case UNBIND_REASON_SWITCH_IME:
+                return "SWITCH_IME";
+            case UNBIND_REASON_DISCONNECT_IME:
+                return "DISCONNECT_IME";
+            case UNBIND_REASON_NO_IME:
+                return "NO_IME";
+            case UNBIND_REASON_SWITCH_IME_FAILED:
+                return "SWITCH_IME_FAILED";
+            case UNBIND_REASON_SWITCH_USER:
+                return "SWITCH_USER";
+            default:
+                return "Unknown=" + reason;
+        }
+    }
+
+    public static String softInputModeToString(@SoftInputModeFlags final int softInputMode) {
+        final StringBuilder sb = new StringBuilder();
+        final int state = softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE;
+        final int adjust = softInputMode & LayoutParams.SOFT_INPUT_MASK_ADJUST;
+        final boolean isForwardNav =
+                (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
+
+        switch (state) {
+            case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
+                sb.append("STATE_UNSPECIFIED");
+                break;
+            case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+                sb.append("STATE_UNCHANGED");
+                break;
+            case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+                sb.append("STATE_HIDDEN");
+                break;
+            case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+                sb.append("STATE_ALWAYS_HIDDEN");
+                break;
+            case LayoutParams.SOFT_INPUT_STATE_VISIBLE:
+                sb.append("STATE_VISIBLE");
+                break;
+            case LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+                sb.append("STATE_ALWAYS_VISIBLE");
+                break;
+            default:
+                sb.append("STATE_UNKNOWN(");
+                sb.append(state);
+                sb.append(")");
+                break;
+        }
+
+        switch (adjust) {
+            case LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED:
+                sb.append("|ADJUST_UNSPECIFIED");
+                break;
+            case LayoutParams.SOFT_INPUT_ADJUST_RESIZE:
+                sb.append("|ADJUST_RESIZE");
+                break;
+            case LayoutParams.SOFT_INPUT_ADJUST_PAN:
+                sb.append("|ADJUST_PAN");
+                break;
+            case LayoutParams.SOFT_INPUT_ADJUST_NOTHING:
+                sb.append("|ADJUST_NOTHING");
+                break;
+            default:
+                sb.append("|ADJUST_UNKNOWN(");
+                sb.append(adjust);
+                sb.append(")");
+                break;
+        }
+
+        if (isForwardNav) {
+            // This is a special bit that is set by the system only during the window navigation.
+            sb.append("|IS_FORWARD_NAVIGATION");
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/com/android/internal/view/OneShotPreDrawListener.java b/com/android/internal/view/OneShotPreDrawListener.java
new file mode 100644
index 0000000..42d104a
--- /dev/null
+++ b/com/android/internal/view/OneShotPreDrawListener.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 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.internal.view;
+
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+/**
+ * An OnPreDrawListener that will remove itself after one OnPreDraw call. Typical
+ * usage is:
+ * <pre><code>
+ *     OneShotPreDrawListener.add(view, () -> { view.doSomething(); })
+ * </code></pre>
+ * <p>
+ * The listener will also remove itself from the ViewTreeObserver when the view
+ * is detached from the view hierarchy. In that case, the Runnable will never be
+ * executed.
+ */
+public class OneShotPreDrawListener implements ViewTreeObserver.OnPreDrawListener,
+        View.OnAttachStateChangeListener {
+    private final View mView;
+    private ViewTreeObserver mViewTreeObserver;
+    private final Runnable mRunnable;
+    private final boolean mReturnValue;
+
+    private OneShotPreDrawListener(View view, boolean returnValue, Runnable runnable) {
+        mView = view;
+        mViewTreeObserver = view.getViewTreeObserver();
+        mRunnable = runnable;
+        mReturnValue = returnValue;
+    }
+
+    /**
+     * Creates a OneShotPreDrawListener and adds it to view's ViewTreeObserver. The
+     * return value from the OnPreDrawListener is {@code true}.
+     *
+     * @param view The view whose ViewTreeObserver the OnPreDrawListener should listen.
+     * @param runnable The Runnable to execute in the OnPreDraw (once)
+     * @return The added OneShotPreDrawListener. It can be removed prior to
+     * the onPreDraw by calling {@link #removeListener()}.
+     */
+    public static OneShotPreDrawListener add(View view, Runnable runnable) {
+        return add(view, true, runnable);
+    }
+
+    /**
+     * Creates a OneShotPreDrawListener and adds it to view's ViewTreeObserver.
+     *
+     * @param view The view whose ViewTreeObserver the OnPreDrawListener should listen.
+     * @param returnValue The value to be returned from the OnPreDrawListener.
+     * @param runnable The Runnable to execute in the OnPreDraw (once)
+     * @return The added OneShotPreDrawListener. It can be removed prior to
+     * the onPreDraw by calling {@link #removeListener()}.
+     */
+    public static OneShotPreDrawListener add(View view, boolean returnValue, Runnable runnable) {
+        OneShotPreDrawListener listener = new OneShotPreDrawListener(view, returnValue, runnable);
+        view.getViewTreeObserver().addOnPreDrawListener(listener);
+        view.addOnAttachStateChangeListener(listener);
+        return listener;
+    }
+
+    @Override
+    public boolean onPreDraw() {
+        removeListener();
+        mRunnable.run();
+        return mReturnValue;
+    }
+
+    /**
+     * Removes the listener from the ViewTreeObserver. This is useful to call if the
+     * callback should be removed prior to {@link #onPreDraw()}.
+     */
+    public void removeListener() {
+        if (mViewTreeObserver.isAlive()) {
+            mViewTreeObserver.removeOnPreDrawListener(this);
+        } else {
+            mView.getViewTreeObserver().removeOnPreDrawListener(this);
+        }
+        mView.removeOnAttachStateChangeListener(this);
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View v) {
+        mViewTreeObserver = v.getViewTreeObserver();
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View v) {
+        removeListener();
+    }
+}
diff --git a/com/android/internal/view/RootViewSurfaceTaker.java b/com/android/internal/view/RootViewSurfaceTaker.java
new file mode 100644
index 0000000..433ec73
--- /dev/null
+++ b/com/android/internal/view/RootViewSurfaceTaker.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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.internal.view;
+
+import android.view.InputQueue;
+import android.view.SurfaceHolder;
+
+/** hahahah */
+public interface RootViewSurfaceTaker {
+    SurfaceHolder.Callback2 willYouTakeTheSurface();
+    void setSurfaceType(int type);
+    void setSurfaceFormat(int format);
+    void setSurfaceKeepScreenOn(boolean keepOn);
+    InputQueue.Callback willYouTakeTheInputQueue();
+    void onRootViewScrollYChanged(int scrollY);
+}
diff --git a/com/android/internal/view/RotationPolicy.java b/com/android/internal/view/RotationPolicy.java
new file mode 100644
index 0000000..b479cb1
--- /dev/null
+++ b/com/android/internal/view/RotationPolicy.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2012 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.internal.view;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.database.ContentObserver;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Display;
+import android.view.IWindowManager;
+import android.view.Surface;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.R;
+
+/**
+ * Provides helper functions for configuring the display rotation policy.
+ */
+public final class RotationPolicy {
+    private static final String TAG = "RotationPolicy";
+    private static final int CURRENT_ROTATION = -1;
+    private static final int NATURAL_ROTATION = Surface.ROTATION_0;
+
+    private RotationPolicy() {
+    }
+
+    /**
+     * Gets whether the device supports rotation. In general such a
+     * device has an accelerometer and has the portrait and landscape
+     * features.
+     *
+     * @param context Context for accessing system resources.
+     * @return Whether the device supports rotation.
+     */
+    public static boolean isRotationSupported(Context context) {
+        PackageManager pm = context.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER)
+                && pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
+                && pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE)
+                && context.getResources().getBoolean(
+                        com.android.internal.R.bool.config_supportAutoRotation);
+    }
+
+    /**
+     * Returns the orientation that will be used when locking the orientation from system UI
+     * with {@link #setRotationLock}.
+     *
+     * If the device only supports locking to its natural orientation, this will be either
+     * Configuration.ORIENTATION_PORTRAIT or Configuration.ORIENTATION_LANDSCAPE,
+     * otherwise Configuration.ORIENTATION_UNDEFINED if any orientation is lockable.
+     */
+    public static int getRotationLockOrientation(Context context) {
+        if (!areAllRotationsAllowed(context)) {
+            final Point size = new Point();
+            final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+            try {
+                wm.getInitialDisplaySize(Display.DEFAULT_DISPLAY, size);
+                return size.x < size.y ?
+                        Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Unable to get the display size");
+            }
+        }
+        return Configuration.ORIENTATION_UNDEFINED;
+    }
+
+    /**
+     * Returns true if the rotation-lock toggle should be shown in system UI.
+     */
+    public static boolean isRotationLockToggleVisible(Context context) {
+        return isRotationSupported(context) &&
+                Settings.System.getIntForUser(context.getContentResolver(),
+                        Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0,
+                        UserHandle.USER_CURRENT) == 0;
+    }
+
+    /**
+     * Returns true if rotation lock is enabled.
+     */
+    public static boolean isRotationLocked(Context context) {
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.ACCELEROMETER_ROTATION, 0, UserHandle.USER_CURRENT) == 0;
+    }
+
+    /**
+     * Enables or disables rotation lock from the system UI toggle.
+     */
+    public static void setRotationLock(Context context, final boolean enabled) {
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0,
+                UserHandle.USER_CURRENT);
+
+        final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION;
+        setRotationLock(enabled, rotation);
+    }
+
+    /**
+     * Enables or disables natural rotation lock from Accessibility settings.
+     *
+     * If rotation is locked for accessibility, the system UI toggle is hidden to avoid confusion.
+     */
+    public static void setRotationLockForAccessibility(Context context, final boolean enabled) {
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, enabled ? 1 : 0,
+                        UserHandle.USER_CURRENT);
+
+        setRotationLock(enabled, NATURAL_ROTATION);
+    }
+
+    private static boolean areAllRotationsAllowed(Context context) {
+        return context.getResources().getBoolean(R.bool.config_allowAllRotations);
+    }
+
+    private static void setRotationLock(final boolean enabled, final int rotation) {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    if (enabled) {
+                        wm.freezeRotation(rotation);
+                    } else {
+                        wm.thawRotation();
+                    }
+                } catch (RemoteException exc) {
+                    Log.w(TAG, "Unable to save auto-rotate setting");
+                }
+            }
+        });
+    }
+
+    /**
+     * Registers a listener for rotation policy changes affecting the caller's user
+     */
+    public static void registerRotationPolicyListener(Context context,
+            RotationPolicyListener listener) {
+        registerRotationPolicyListener(context, listener, UserHandle.getCallingUserId());
+    }
+
+    /**
+     * Registers a listener for rotation policy changes affecting a specific user,
+     * or USER_ALL for all users.
+     */
+    public static void registerRotationPolicyListener(Context context,
+            RotationPolicyListener listener, int userHandle) {
+        context.getContentResolver().registerContentObserver(Settings.System.getUriFor(
+                Settings.System.ACCELEROMETER_ROTATION),
+                false, listener.mObserver, userHandle);
+        context.getContentResolver().registerContentObserver(Settings.System.getUriFor(
+                Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY),
+                false, listener.mObserver, userHandle);
+    }
+
+    /**
+     * Unregisters a listener for rotation policy changes.
+     */
+    public static void unregisterRotationPolicyListener(Context context,
+            RotationPolicyListener listener) {
+        context.getContentResolver().unregisterContentObserver(listener.mObserver);
+    }
+
+    /**
+     * Listener that is invoked whenever a change occurs that might affect the rotation policy.
+     */
+    public static abstract class RotationPolicyListener {
+        final ContentObserver mObserver = new ContentObserver(new Handler()) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                RotationPolicyListener.this.onChange();
+            }
+        };
+
+        public abstract void onChange();
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/view/StandaloneActionMode.java b/com/android/internal/view/StandaloneActionMode.java
new file mode 100644
index 0000000..e6d911c
--- /dev/null
+++ b/com/android/internal/view/StandaloneActionMode.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2010 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.internal.view;
+
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.SubMenuBuilder;
+import com.android.internal.widget.ActionBarContextView;
+
+import android.content.Context;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.lang.ref.WeakReference;
+
+public class StandaloneActionMode extends ActionMode implements MenuBuilder.Callback {
+    private Context mContext;
+    private ActionBarContextView mContextView;
+    private ActionMode.Callback mCallback;
+    private WeakReference<View> mCustomView;
+    private boolean mFinished;
+    private boolean mFocusable;
+
+    private MenuBuilder mMenu;
+
+    public StandaloneActionMode(Context context, ActionBarContextView view,
+            ActionMode.Callback callback, boolean isFocusable) {
+        mContext = context;
+        mContextView = view;
+        mCallback = callback;
+
+        mMenu = new MenuBuilder(view.getContext()).setDefaultShowAsAction(
+                        MenuItem.SHOW_AS_ACTION_IF_ROOM);
+        mMenu.setCallback(this);
+        mFocusable = isFocusable;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mContextView.setTitle(title);
+    }
+
+    @Override
+    public void setSubtitle(CharSequence subtitle) {
+        mContextView.setSubtitle(subtitle);
+    }
+
+    @Override
+    public void setTitle(int resId) {
+        setTitle(resId != 0 ? mContext.getString(resId) : null);
+    }
+
+    @Override
+    public void setSubtitle(int resId) {
+        setSubtitle(resId != 0 ? mContext.getString(resId) : null);
+    }
+
+    @Override
+    public void setTitleOptionalHint(boolean titleOptional) {
+        super.setTitleOptionalHint(titleOptional);
+        mContextView.setTitleOptional(titleOptional);
+    }
+
+    @Override
+    public boolean isTitleOptional() {
+        return mContextView.isTitleOptional();
+    }
+
+    @Override
+    public void setCustomView(View view) {
+        mContextView.setCustomView(view);
+        mCustomView = view != null ? new WeakReference<View>(view) : null;
+    }
+
+    @Override
+    public void invalidate() {
+        mCallback.onPrepareActionMode(this, mMenu);
+    }
+
+    @Override
+    public void finish() {
+        if (mFinished) {
+            return;
+        }
+        mFinished = true;
+
+        mContextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        mCallback.onDestroyActionMode(this);
+    }
+
+    @Override
+    public Menu getMenu() {
+        return mMenu;
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        return mContextView.getTitle();
+    }
+
+    @Override
+    public CharSequence getSubtitle() {
+        return mContextView.getSubtitle();
+    }
+
+    @Override
+    public View getCustomView() {
+        return mCustomView != null ? mCustomView.get() : null;
+    }
+
+    @Override
+    public MenuInflater getMenuInflater() {
+        return new MenuInflater(mContextView.getContext());
+    }
+
+    public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+        return mCallback.onActionItemClicked(this, item);
+    }
+
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+    }
+
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+        if (!subMenu.hasVisibleItems()) {
+            return true;
+        }
+
+        new MenuPopupHelper(mContextView.getContext(), subMenu).show();
+        return true;
+    }
+
+    public void onCloseSubMenu(SubMenuBuilder menu) {
+    }
+
+    public void onMenuModeChange(MenuBuilder menu) {
+        invalidate();
+        mContextView.showOverflowMenu();
+    }
+
+    public boolean isUiFocusable() {
+        return mFocusable;
+    }
+}
diff --git a/com/android/internal/view/SurfaceCallbackHelper.java b/com/android/internal/view/SurfaceCallbackHelper.java
new file mode 100644
index 0000000..507b673
--- /dev/null
+++ b/com/android/internal/view/SurfaceCallbackHelper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 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.internal.view;
+
+import android.os.RemoteException;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+public class SurfaceCallbackHelper {
+    Runnable mRunnable;
+
+    int mFinishDrawingCollected = 0;
+    int mFinishDrawingExpected = 0;
+
+    private Runnable mFinishDrawingRunnable = new Runnable() {
+            @Override
+            public void run() {
+                synchronized (SurfaceCallbackHelper.this) {
+                    mFinishDrawingCollected++;
+                    if (mFinishDrawingCollected < mFinishDrawingExpected) {
+                        return;
+                    }
+                    mRunnable.run();
+                }
+            }
+    };
+
+    public SurfaceCallbackHelper(Runnable callbacksCollected) {
+        mRunnable = callbacksCollected;
+    }
+
+    public void dispatchSurfaceRedrawNeededAsync(SurfaceHolder holder, SurfaceHolder.Callback callbacks[]) {
+        if (callbacks == null || callbacks.length == 0) {
+            mRunnable.run();
+            return;
+        }
+
+        synchronized (this) {
+            mFinishDrawingExpected = callbacks.length;
+            mFinishDrawingCollected = 0;
+        }
+
+        for (SurfaceHolder.Callback c : callbacks) {
+            if (c instanceof SurfaceHolder.Callback2) {
+                ((SurfaceHolder.Callback2) c).surfaceRedrawNeededAsync(
+                        holder, mFinishDrawingRunnable);
+            } else {
+                mFinishDrawingRunnable.run();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/view/SurfaceFlingerVsyncChoreographer.java b/com/android/internal/view/SurfaceFlingerVsyncChoreographer.java
new file mode 100644
index 0000000..924b3f7
--- /dev/null
+++ b/com/android/internal/view/SurfaceFlingerVsyncChoreographer.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 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.internal.view;
+
+import android.os.Handler;
+import android.os.Message;
+import android.view.Choreographer;
+import android.view.Display;
+
+/**
+ * Utility class to schedule things at vsync-sf instead of vsync-app
+ * @hide
+ */
+public class SurfaceFlingerVsyncChoreographer {
+
+    private static final long ONE_MS_IN_NS = 1000000;
+    private static final long ONE_S_IN_NS = ONE_MS_IN_NS * 1000;
+
+    private final Handler mHandler;
+    private final Choreographer mChoreographer;
+
+    /**
+     * The offset between vsync-app and vsync-surfaceflinger. See
+     * {@link #calculateAppSurfaceFlingerVsyncOffsetMs} why this is necessary.
+     */
+    private long mSurfaceFlingerOffsetMs;
+
+    public SurfaceFlingerVsyncChoreographer(Handler handler, Display display,
+            Choreographer choreographer) {
+        mHandler = handler;
+        mChoreographer = choreographer;
+        mSurfaceFlingerOffsetMs = calculateAppSurfaceFlingerVsyncOffsetMs(display);
+    }
+
+    public long getSurfaceFlingerOffsetMs() {
+        return mSurfaceFlingerOffsetMs;
+    }
+
+    /**
+     * This method calculates the offset between vsync-surfaceflinger and vsync-app. If vsync-app
+     * is a couple of milliseconds before vsync-sf, a touch or animation event that causes a surface
+     * flinger transaction are sometimes processed before the vsync-sf tick, and sometimes after,
+     * which leads to jank. Figure out this difference here and then post all the touch/animation
+     * events to start being processed at vsync-sf.
+     *
+     * @return The offset between vsync-app and vsync-sf, or 0 if vsync app happens after vsync-sf.
+     */
+    private long calculateAppSurfaceFlingerVsyncOffsetMs(Display display) {
+
+        // Calculate vsync offset from SurfaceFlinger.
+        // See frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp:getDisplayConfigs
+        long vsyncPeriod = (long) (ONE_S_IN_NS / display.getRefreshRate());
+        long sfVsyncOffset = vsyncPeriod - (display.getPresentationDeadlineNanos() - ONE_MS_IN_NS);
+        return Math.max(0, (sfVsyncOffset - display.getAppVsyncOffsetNanos()) / ONE_MS_IN_NS);
+    }
+
+    public void scheduleAtSfVsync(Runnable r) {
+        final long delay = calculateDelay();
+        if (delay <= 0) {
+            r.run();
+        } else {
+            mHandler.postDelayed(r, delay);
+        }
+    }
+
+    public void scheduleAtSfVsync(Handler h, Message m) {
+        final long delay = calculateDelay();
+        if (delay <= 0) {
+            h.handleMessage(m);
+        } else {
+            m.setAsynchronous(true);
+            h.sendMessageDelayed(m, delay);
+        }
+    }
+
+    private long calculateDelay() {
+        final long sinceFrameStart = System.nanoTime() - mChoreographer.getLastFrameTimeNanos();
+        return mSurfaceFlingerOffsetMs - sinceFrameStart / 1000000;
+    }
+}
diff --git a/com/android/internal/view/TooltipPopup.java b/com/android/internal/view/TooltipPopup.java
new file mode 100644
index 0000000..d38ea2c
--- /dev/null
+++ b/com/android/internal/view/TooltipPopup.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 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.internal.view;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.widget.TextView;
+
+public class TooltipPopup {
+    private static final String TAG = "TooltipPopup";
+
+    private final Context mContext;
+
+    private final View mContentView;
+    private final TextView mMessageView;
+
+    private final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
+    private final Rect mTmpDisplayFrame = new Rect();
+    private final int[] mTmpAnchorPos = new int[2];
+    private final int[] mTmpAppPos = new int[2];
+
+    public TooltipPopup(Context context) {
+        mContext = context;
+
+        mContentView = LayoutInflater.from(mContext).inflate(
+                com.android.internal.R.layout.tooltip, null);
+        mMessageView = (TextView) mContentView.findViewById(
+                com.android.internal.R.id.message);
+
+        mLayoutParams.setTitle(
+                mContext.getString(com.android.internal.R.string.tooltip_popup_title));
+        mLayoutParams.packageName = mContext.getOpPackageName();
+        mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
+        mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+        mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+        mLayoutParams.format = PixelFormat.TRANSLUCENT;
+        mLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Tooltip;
+        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+    }
+
+    public void show(View anchorView, int anchorX, int anchorY, boolean fromTouch,
+            CharSequence tooltipText) {
+        if (isShowing()) {
+            hide();
+        }
+
+        mMessageView.setText(tooltipText);
+
+        computePosition(anchorView, anchorX, anchorY, fromTouch, mLayoutParams);
+
+        WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+        wm.addView(mContentView, mLayoutParams);
+    }
+
+    public void hide() {
+        if (!isShowing()) {
+            return;
+        }
+
+        WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+        wm.removeView(mContentView);
+    }
+
+    public View getContentView() {
+        return mContentView;
+    }
+
+    public boolean isShowing() {
+        return mContentView.getParent() != null;
+    }
+
+    private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch,
+            WindowManager.LayoutParams outParams) {
+        outParams.token = anchorView.getApplicationWindowToken();
+
+        final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
+                com.android.internal.R.dimen.tooltip_precise_anchor_threshold);
+
+        final int offsetX;
+        if (anchorView.getWidth() >= tooltipPreciseAnchorThreshold) {
+            // Wide view. Align the tooltip horizontally to the precise X position.
+            offsetX = anchorX;
+        } else {
+            // Otherwise anchor the tooltip to the view center.
+            offsetX = anchorView.getWidth() / 2;  // Center on the view horizontally.
+        }
+
+        final int offsetBelow;
+        final int offsetAbove;
+        if (anchorView.getHeight() >= tooltipPreciseAnchorThreshold) {
+            // Tall view. Align the tooltip vertically to the precise Y position.
+            final int offsetExtra = mContext.getResources().getDimensionPixelOffset(
+                    com.android.internal.R.dimen.tooltip_precise_anchor_extra_offset);
+            offsetBelow = anchorY + offsetExtra;
+            offsetAbove = anchorY - offsetExtra;
+        } else {
+            // Otherwise anchor the tooltip to the view center.
+            offsetBelow = anchorView.getHeight();  // Place below the view in most cases.
+            offsetAbove = 0;  // Place above the view if the tooltip does not fit below.
+        }
+
+        outParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+
+        final int tooltipOffset = mContext.getResources().getDimensionPixelOffset(
+                fromTouch ? com.android.internal.R.dimen.tooltip_y_offset_touch
+                        : com.android.internal.R.dimen.tooltip_y_offset_non_touch);
+
+        // Find the main app window. The popup window will be positioned relative to it.
+        final View appView = WindowManagerGlobal.getInstance().getWindowView(
+                anchorView.getApplicationWindowToken());
+        if (appView == null) {
+            Slog.e(TAG, "Cannot find app view");
+            return;
+        }
+        appView.getWindowVisibleDisplayFrame(mTmpDisplayFrame);
+        appView.getLocationOnScreen(mTmpAppPos);
+
+        anchorView.getLocationOnScreen(mTmpAnchorPos);
+        mTmpAnchorPos[0] -= mTmpAppPos[0];
+        mTmpAnchorPos[1] -= mTmpAppPos[1];
+        // mTmpAnchorPos is now relative to the main app window.
+
+        outParams.x = mTmpAnchorPos[0] + offsetX - mTmpDisplayFrame.width() / 2;
+
+        final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        mContentView.measure(spec, spec);
+        final int tooltipHeight = mContentView.getMeasuredHeight();
+
+        final int yAbove = mTmpAnchorPos[1] + offsetAbove - tooltipOffset - tooltipHeight;
+        final int yBelow = mTmpAnchorPos[1] + offsetBelow + tooltipOffset;
+        if (fromTouch) {
+            if (yAbove >= 0) {
+                outParams.y = yAbove;
+            } else {
+                outParams.y = yBelow;
+            }
+        } else {
+            if (yBelow + tooltipHeight <= mTmpDisplayFrame.height()) {
+                outParams.y = yBelow;
+            } else {
+                outParams.y = yAbove;
+            }
+        }
+    }
+}
diff --git a/com/android/internal/view/WindowManagerPolicyThread.java b/com/android/internal/view/WindowManagerPolicyThread.java
new file mode 100644
index 0000000..c8c38bb
--- /dev/null
+++ b/com/android/internal/view/WindowManagerPolicyThread.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.internal.view;
+
+import android.os.Looper;
+
+/**
+ * Static storage of the thread running the window manager policy, to
+ * share with others.
+ */
+public class WindowManagerPolicyThread {
+    static Thread mThread;
+    static Looper mLooper;
+
+    public static void set(Thread thread, Looper looper) {
+        mThread = thread;
+        mLooper = looper;
+    }
+
+    public static Thread getThread() {
+        return mThread;
+    }
+
+    public static Looper getLooper() {
+        return mLooper;
+    }
+}
diff --git a/com/android/internal/view/animation/FallbackLUTInterpolator.java b/com/android/internal/view/animation/FallbackLUTInterpolator.java
new file mode 100644
index 0000000..d28ab07
--- /dev/null
+++ b/com/android/internal/view/animation/FallbackLUTInterpolator.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 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.internal.view.animation;
+
+import android.animation.TimeInterpolator;
+import android.util.TimeUtils;
+import android.view.Choreographer;
+
+/**
+ * Interpolator that builds a lookup table to use. This is a fallback for
+ * building a native interpolator from a TimeInterpolator that is not marked
+ * with {@link HasNativeInterpolator}
+ *
+ * This implements TimeInterpolator to allow for easier interop with Animators
+ */
+@HasNativeInterpolator
+public class FallbackLUTInterpolator implements NativeInterpolatorFactory, TimeInterpolator {
+
+    // If the duration of an animation is more than 300 frames, we cap the sample size to 300.
+    private static final int MAX_SAMPLE_POINTS = 300;
+    private TimeInterpolator mSourceInterpolator;
+    private final float mLut[];
+
+    /**
+     * Used to cache the float[] LUT for use across multiple native
+     * interpolator creation
+     */
+    public FallbackLUTInterpolator(TimeInterpolator interpolator, long duration) {
+        mSourceInterpolator = interpolator;
+        mLut = createLUT(interpolator, duration);
+    }
+
+    private static float[] createLUT(TimeInterpolator interpolator, long duration) {
+        long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
+        int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
+        // We need 2 frame values as the minimal.
+        int numAnimFrames = Math.max(2, (int) Math.ceil(((double) duration) / animIntervalMs));
+        numAnimFrames = Math.min(numAnimFrames, MAX_SAMPLE_POINTS);
+        float values[] = new float[numAnimFrames];
+        float lastFrame = numAnimFrames - 1;
+        for (int i = 0; i < numAnimFrames; i++) {
+            float inValue = i / lastFrame;
+            values[i] = interpolator.getInterpolation(inValue);
+        }
+        return values;
+    }
+
+    @Override
+    public long createNativeInterpolator() {
+        return NativeInterpolatorFactoryHelper.createLutInterpolator(mLut);
+    }
+
+    /**
+     * Used to create a one-shot float[] LUT & native interpolator
+     */
+    public static long createNativeInterpolator(TimeInterpolator interpolator, long duration) {
+        float[] lut = createLUT(interpolator, duration);
+        return NativeInterpolatorFactoryHelper.createLutInterpolator(lut);
+    }
+
+    @Override
+    public float getInterpolation(float input) {
+        return mSourceInterpolator.getInterpolation(input);
+    }
+}
diff --git a/com/android/internal/view/animation/HasNativeInterpolator.java b/com/android/internal/view/animation/HasNativeInterpolator.java
new file mode 100644
index 0000000..48ea4da
--- /dev/null
+++ b/com/android/internal/view/animation/HasNativeInterpolator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 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.internal.view.animation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This is a class annotation that signals that it is safe to create
+ * a native interpolator counterpart via {@link NativeInterpolatorFactory}
+ *
+ * The idea here is to prevent subclasses of interpolators from being treated as a
+ * NativeInterpolatorFactory, and instead have them fall back to the LUT & LERP
+ * method like a custom interpolator.
+ *
+ * @hide
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface HasNativeInterpolator {
+}
diff --git a/com/android/internal/view/animation/NativeInterpolatorFactory.java b/com/android/internal/view/animation/NativeInterpolatorFactory.java
new file mode 100644
index 0000000..fcacd52
--- /dev/null
+++ b/com/android/internal/view/animation/NativeInterpolatorFactory.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2014 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.internal.view.animation;
+
+public interface NativeInterpolatorFactory {
+    long createNativeInterpolator();
+}
diff --git a/com/android/internal/view/animation/NativeInterpolatorFactoryHelper.java b/com/android/internal/view/animation/NativeInterpolatorFactoryHelper.java
new file mode 100644
index 0000000..ebeec40
--- /dev/null
+++ b/com/android/internal/view/animation/NativeInterpolatorFactoryHelper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 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.internal.view.animation;
+
+/**
+ * Static utility class for constructing native interpolators to keep the
+ * JNI simpler
+ */
+public final class NativeInterpolatorFactoryHelper {
+    private NativeInterpolatorFactoryHelper() {}
+
+    public static native long createAccelerateDecelerateInterpolator();
+    public static native long createAccelerateInterpolator(float factor);
+    public static native long createAnticipateInterpolator(float tension);
+    public static native long createAnticipateOvershootInterpolator(float tension);
+    public static native long createBounceInterpolator();
+    public static native long createCycleInterpolator(float cycles);
+    public static native long createDecelerateInterpolator(float factor);
+    public static native long createLinearInterpolator();
+    public static native long createOvershootInterpolator(float tension);
+    public static native long createPathInterpolator(float[] x, float[] y);
+    public static native long createLutInterpolator(float[] values);
+}
diff --git a/com/android/internal/view/animation/NativeInterpolatorFactoryHelper_Delegate.java b/com/android/internal/view/animation/NativeInterpolatorFactoryHelper_Delegate.java
new file mode 100644
index 0000000..da1ab27
--- /dev/null
+++ b/com/android/internal/view/animation/NativeInterpolatorFactoryHelper_Delegate.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 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.internal.view.animation;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.graphics.Path;
+import android.util.MathUtils;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AnticipateInterpolator;
+import android.view.animation.AnticipateOvershootInterpolator;
+import android.view.animation.BaseInterpolator;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.CycleInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link
+ * NativeInterpolatorFactoryHelper}
+ * <p>
+ * Through the layoutlib_create tool, the original  methods of NativeInterpolatorFactoryHelper have
+ * been replaced by calls to methods of the same name in this delegate class.
+ */
+@SuppressWarnings("unused")
+public class NativeInterpolatorFactoryHelper_Delegate {
+    private static final DelegateManager<Interpolator> sManager = new DelegateManager<>
+            (Interpolator.class);
+
+    public static Interpolator getDelegate(long nativePtr) {
+        return sManager.getDelegate(nativePtr);
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createAccelerateDecelerateInterpolator() {
+        return sManager.addNewDelegate(new AccelerateDecelerateInterpolator());
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createAccelerateInterpolator(float factor) {
+        return sManager.addNewDelegate(new AccelerateInterpolator(factor));
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createAnticipateInterpolator(float tension) {
+        return sManager.addNewDelegate(new AnticipateInterpolator(tension));
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createAnticipateOvershootInterpolator(float tension) {
+        return sManager.addNewDelegate(new AnticipateOvershootInterpolator(tension));
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createBounceInterpolator() {
+        return sManager.addNewDelegate(new BounceInterpolator());
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createCycleInterpolator(float cycles) {
+        return sManager.addNewDelegate(new CycleInterpolator(cycles));
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createDecelerateInterpolator(float factor) {
+        return sManager.addNewDelegate(new DecelerateInterpolator(factor));
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createLinearInterpolator() {
+        return sManager.addNewDelegate(new LinearInterpolator());
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createOvershootInterpolator(float tension) {
+        return sManager.addNewDelegate(new OvershootInterpolator(tension));
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createPathInterpolator(float[] x, float[] y) {
+        Path path = new Path();
+        path.moveTo(x[0], y[0]);
+        for (int i = 1; i < x.length; i++) {
+            path.lineTo(x[i], y[i]);
+        }
+        return sManager.addNewDelegate(new PathInterpolator(path));
+    }
+
+    private static class LutInterpolator extends BaseInterpolator {
+        private final float[] mValues;
+        private final int mSize;
+
+        private LutInterpolator(float[] values) {
+            mValues = values;
+            mSize = mValues.length;
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            float lutpos = input * (mSize - 1);
+            if (lutpos >= (mSize - 1)) {
+                return mValues[mSize - 1];
+            }
+
+            int ipart = (int) lutpos;
+            float weight = lutpos - ipart;
+
+            int i1 = ipart;
+            int i2 = Math.min(i1 + 1, mSize - 1);
+
+            assert i1 >= 0 && i2 >= 0 : "Negatives in the interpolation";
+
+            return MathUtils.lerp(mValues[i1], mValues[i2], weight);
+        }
+    }
+
+    @LayoutlibDelegate
+    /*package*/ static long createLutInterpolator(float[] values) {
+        return sManager.addNewDelegate(new LutInterpolator(values));
+    }
+}
diff --git a/com/android/internal/view/menu/ActionMenu.java b/com/android/internal/view/menu/ActionMenu.java
new file mode 100644
index 0000000..c657b87
--- /dev/null
+++ b/com/android/internal/view/menu/ActionMenu.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2010 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.internal.view.menu;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+
+/**
+ * @hide
+ */
+public class ActionMenu implements Menu {
+    private Context mContext;
+    
+    private boolean mIsQwerty;
+    
+    private ArrayList<ActionMenuItem> mItems;
+
+    public ActionMenu(Context context) {
+        mContext = context;
+        mItems = new ArrayList<ActionMenuItem>();
+    }
+    
+    public Context getContext() {
+        return mContext;
+    }
+
+    public MenuItem add(CharSequence title) {
+        return add(0, 0, 0, title);
+    }
+
+    public MenuItem add(int titleRes) {
+        return add(0, 0, 0, titleRes);
+    }
+
+    public MenuItem add(int groupId, int itemId, int order, int titleRes) {
+        return add(groupId, itemId, order, mContext.getResources().getString(titleRes));
+    }
+    
+    public MenuItem add(int groupId, int itemId, int order, CharSequence title) {
+        ActionMenuItem item = new ActionMenuItem(getContext(),
+                groupId, itemId, 0, order, title);
+        mItems.add(order, item);
+        return item;
+    }
+
+    public int addIntentOptions(int groupId, int itemId, int order,
+            ComponentName caller, Intent[] specifics, Intent intent, int flags,
+            MenuItem[] outSpecificItems) {
+        PackageManager pm = mContext.getPackageManager();
+        final List<ResolveInfo> lri =
+                pm.queryIntentActivityOptions(caller, specifics, intent, 0);
+        final int N = lri != null ? lri.size() : 0;
+
+        if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
+            removeGroup(groupId);
+        }
+
+        for (int i=0; i<N; i++) {
+            final ResolveInfo ri = lri.get(i);
+            Intent rintent = new Intent(
+                ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
+            rintent.setComponent(new ComponentName(
+                    ri.activityInfo.applicationInfo.packageName,
+                    ri.activityInfo.name));
+            final MenuItem item = add(groupId, itemId, order, ri.loadLabel(pm))
+                    .setIcon(ri.loadIcon(pm))
+                    .setIntent(rintent);
+            if (outSpecificItems != null && ri.specificIndex >= 0) {
+                outSpecificItems[ri.specificIndex] = item;
+            }
+        }
+
+        return N;
+    }
+
+    public SubMenu addSubMenu(CharSequence title) {
+        // TODO Implement submenus
+        return null;
+    }
+
+    public SubMenu addSubMenu(int titleRes) {
+        // TODO Implement submenus
+        return null;
+    }
+
+    public SubMenu addSubMenu(int groupId, int itemId, int order,
+            CharSequence title) {
+        // TODO Implement submenus
+        return null;
+    }
+
+    public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) {
+        // TODO Implement submenus
+        return null;
+    }
+
+    public void clear() {
+        mItems.clear();
+    }
+
+    public void close() {
+    }
+    
+    private int findItemIndex(int id) {
+        final ArrayList<ActionMenuItem> items = mItems;
+        final int itemCount = items.size();
+        for (int i = 0; i < itemCount; i++) {
+            if (items.get(i).getItemId() == id) {
+                return i;
+            }
+        }
+        
+        return -1;
+    }
+
+    public MenuItem findItem(int id) {
+        return mItems.get(findItemIndex(id));
+    }
+
+    public MenuItem getItem(int index) {
+        return mItems.get(index);
+    }
+
+    public boolean hasVisibleItems() {
+        final ArrayList<ActionMenuItem> items = mItems;
+        final int itemCount = items.size();
+        
+        for (int i = 0; i < itemCount; i++) {
+            if (items.get(i).isVisible()) {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+
+    private ActionMenuItem findItemWithShortcut(int keyCode, KeyEvent event) {
+        // TODO Make this smarter.
+        final boolean qwerty = mIsQwerty;
+        final ArrayList<ActionMenuItem> items = mItems;
+        final int itemCount = items.size();
+        final int modifierState = event.getModifiers();
+        for (int i = 0; i < itemCount; i++) {
+            ActionMenuItem item = items.get(i);
+            final char shortcut = qwerty ? item.getAlphabeticShortcut() :
+                    item.getNumericShortcut();
+            final int shortcutModifiers =
+                    qwerty ? item.getAlphabeticModifiers() : item.getNumericModifiers();
+            final boolean is_modifiers_exact_match = (modifierState & SUPPORTED_MODIFIERS_MASK)
+                    == (shortcutModifiers & SUPPORTED_MODIFIERS_MASK);
+            if ((keyCode == shortcut) && is_modifiers_exact_match) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public boolean isShortcutKey(int keyCode, KeyEvent event) {
+        return findItemWithShortcut(keyCode, event) != null;
+    }
+
+    public boolean performIdentifierAction(int id, int flags) {
+        final int index = findItemIndex(id);
+        if (index < 0) {
+            return false;
+        }
+
+        return mItems.get(index).invoke();
+    }
+
+    public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
+        ActionMenuItem item = findItemWithShortcut(keyCode, event);
+        if (item == null) {
+            return false;
+        }
+        
+        return item.invoke();
+    }
+
+    public void removeGroup(int groupId) {
+        final ArrayList<ActionMenuItem> items = mItems;
+        int itemCount = items.size();
+        int i = 0;
+        while (i < itemCount) {
+            if (items.get(i).getGroupId() == groupId) {
+                items.remove(i);
+                itemCount--;
+            } else {
+                i++;
+            }
+        }
+    }
+
+    public void removeItem(int id) {
+        mItems.remove(findItemIndex(id));
+    }
+
+    public void setGroupCheckable(int group, boolean checkable,
+            boolean exclusive) {
+        final ArrayList<ActionMenuItem> items = mItems;
+        final int itemCount = items.size();
+        
+        for (int i = 0; i < itemCount; i++) {
+            ActionMenuItem item = items.get(i);
+            if (item.getGroupId() == group) {
+                item.setCheckable(checkable);
+                item.setExclusiveCheckable(exclusive);
+            }
+        }
+    }
+
+    public void setGroupEnabled(int group, boolean enabled) {
+        final ArrayList<ActionMenuItem> items = mItems;
+        final int itemCount = items.size();
+        
+        for (int i = 0; i < itemCount; i++) {
+            ActionMenuItem item = items.get(i);
+            if (item.getGroupId() == group) {
+                item.setEnabled(enabled);
+            }
+        }
+    }
+
+    public void setGroupVisible(int group, boolean visible) {
+        final ArrayList<ActionMenuItem> items = mItems;
+        final int itemCount = items.size();
+        
+        for (int i = 0; i < itemCount; i++) {
+            ActionMenuItem item = items.get(i);
+            if (item.getGroupId() == group) {
+                item.setVisible(visible);
+            }
+        }
+    }
+
+    public void setQwertyMode(boolean isQwerty) {
+        mIsQwerty = isQwerty;
+    }
+
+    public int size() {
+        return mItems.size();
+    }
+}
diff --git a/com/android/internal/view/menu/ActionMenuItem.java b/com/android/internal/view/menu/ActionMenuItem.java
new file mode 100644
index 0000000..b807a42
--- /dev/null
+++ b/com/android/internal/view/menu/ActionMenuItem.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2010 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.internal.view.menu;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.view.ActionProvider;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+
+/**
+ * @hide
+ */
+public class ActionMenuItem implements MenuItem {
+    private final int mId;
+    private final int mGroup;
+    private final int mCategoryOrder;
+    private final int mOrdering;
+
+    private CharSequence mTitle;
+    private CharSequence mTitleCondensed;
+    private Intent mIntent;
+    private char mShortcutNumericChar;
+    private int mShortcutNumericModifiers = KeyEvent.META_CTRL_ON;
+    private char mShortcutAlphabeticChar;
+    private int mShortcutAlphabeticModifiers = KeyEvent.META_CTRL_ON;
+
+    private Drawable mIconDrawable;
+    private int mIconResId = NO_ICON;
+    private ColorStateList mIconTintList = null;
+    private PorterDuff.Mode mIconTintMode = null;
+    private boolean mHasIconTint = false;
+    private boolean mHasIconTintMode = false;
+
+    private Context mContext;
+
+    private MenuItem.OnMenuItemClickListener mClickListener;
+
+    private CharSequence mContentDescription;
+    private CharSequence mTooltipText;
+
+    private static final int NO_ICON = 0;
+
+    private int mFlags = ENABLED;
+    private static final int CHECKABLE      = 0x00000001;
+    private static final int CHECKED        = 0x00000002;
+    private static final int EXCLUSIVE      = 0x00000004;
+    private static final int HIDDEN         = 0x00000008;
+    private static final int ENABLED        = 0x00000010;
+
+    public ActionMenuItem(Context context, int group, int id, int categoryOrder, int ordering,
+            CharSequence title) {
+        mContext = context;
+        mId = id;
+        mGroup = group;
+        mCategoryOrder = categoryOrder;
+        mOrdering = ordering;
+        mTitle = title;
+    }
+
+    public char getAlphabeticShortcut() {
+        return mShortcutAlphabeticChar;
+    }
+
+    public int getAlphabeticModifiers() {
+        return mShortcutAlphabeticModifiers;
+    }
+
+    public int getGroupId() {
+        return mGroup;
+    }
+
+    public Drawable getIcon() {
+        return mIconDrawable;
+    }
+
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    public int getItemId() {
+        return mId;
+    }
+
+    public ContextMenuInfo getMenuInfo() {
+        return null;
+    }
+
+    public char getNumericShortcut() {
+        return mShortcutNumericChar;
+    }
+
+    public int getNumericModifiers() {
+        return mShortcutNumericModifiers;
+    }
+
+    public int getOrder() {
+        return mOrdering;
+    }
+
+    public SubMenu getSubMenu() {
+        return null;
+    }
+
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    public CharSequence getTitleCondensed() {
+        return mTitleCondensed != null ? mTitleCondensed : mTitle;
+    }
+
+    public boolean hasSubMenu() {
+        return false;
+    }
+
+    public boolean isCheckable() {
+        return (mFlags & CHECKABLE) != 0; 
+    }
+
+    public boolean isChecked() {
+        return (mFlags & CHECKED) != 0;
+    }
+
+    public boolean isEnabled() {
+        return (mFlags & ENABLED) != 0;
+    }
+
+    public boolean isVisible() {
+        return (mFlags & HIDDEN) == 0;
+    }
+
+    public MenuItem setAlphabeticShortcut(char alphaChar) {
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+        return this;
+    }
+
+    public MenuItem setAlphabeticShortcut(char alphachar, int alphaModifiers) {
+        mShortcutAlphabeticChar = Character.toLowerCase(alphachar);
+        mShortcutAlphabeticModifiers = KeyEvent.normalizeMetaState(alphaModifiers);
+        return this;
+    }
+
+    public MenuItem setCheckable(boolean checkable) {
+        mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
+        return this;
+    }
+    
+    public ActionMenuItem setExclusiveCheckable(boolean exclusive) {
+        mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
+        return this;
+    }
+
+    public MenuItem setChecked(boolean checked) {
+        mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
+        return this;
+    }
+
+    public MenuItem setEnabled(boolean enabled) {
+        mFlags = (mFlags & ~ENABLED) | (enabled ? ENABLED : 0);
+        return this;
+    }
+
+    public MenuItem setIcon(Drawable icon) {
+        mIconDrawable = icon;
+        mIconResId = NO_ICON;
+
+        applyIconTint();
+        return this;
+    }
+
+    public MenuItem setIcon(int iconRes) {
+        mIconResId = iconRes;
+        mIconDrawable = mContext.getDrawable(iconRes);
+
+        applyIconTint();
+        return this;
+    }
+
+    @Override
+    public MenuItem setIconTintList(@Nullable ColorStateList iconTintList) {
+        mIconTintList = iconTintList;
+        mHasIconTint = true;
+
+        applyIconTint();
+
+        return this;
+    }
+
+    @Nullable
+    @Override
+    public ColorStateList getIconTintList() {
+        return mIconTintList;
+    }
+
+    @Override
+    public MenuItem setIconTintMode(PorterDuff.Mode iconTintMode) {
+        mIconTintMode = iconTintMode;
+        mHasIconTintMode = true;
+
+        applyIconTint();
+
+        return this;
+    }
+
+    @Nullable
+    @Override
+    public PorterDuff.Mode getIconTintMode() {
+        return mIconTintMode;
+    }
+
+    private void applyIconTint() {
+        if (mIconDrawable != null && (mHasIconTint || mHasIconTintMode)) {
+            mIconDrawable = mIconDrawable.mutate();
+
+            if (mHasIconTint) {
+                mIconDrawable.setTintList(mIconTintList);
+            }
+
+            if (mHasIconTintMode) {
+                mIconDrawable.setTintMode(mIconTintMode);
+            }
+        }
+    }
+
+    public MenuItem setIntent(Intent intent) {
+        mIntent = intent;
+        return this;
+    }
+
+    public MenuItem setNumericShortcut(char numericChar) {
+        mShortcutNumericChar = numericChar;
+        return this;
+    }
+
+    public MenuItem setNumericShortcut(char numericChar, int numericModifiers) {
+        mShortcutNumericChar = numericChar;
+        mShortcutNumericModifiers = KeyEvent.normalizeMetaState(numericModifiers);
+        return this;
+    }
+
+    public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) {
+        mClickListener = menuItemClickListener;
+        return this;
+    }
+
+    public MenuItem setShortcut(char numericChar, char alphaChar) {
+        mShortcutNumericChar = numericChar;
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+        return this;
+    }
+
+    public MenuItem setShortcut(char numericChar, char alphaChar, int numericModifiers,
+            int alphaModifiers) {
+        mShortcutNumericChar = numericChar;
+        mShortcutNumericModifiers = KeyEvent.normalizeMetaState(numericModifiers);
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+        mShortcutAlphabeticModifiers = KeyEvent.normalizeMetaState(alphaModifiers);
+        return this;
+    }
+
+    public MenuItem setTitle(CharSequence title) {
+        mTitle = title;
+        return this;
+    }
+
+    public MenuItem setTitle(int title) {
+        mTitle = mContext.getResources().getString(title);
+        return this;
+    }
+
+    public MenuItem setTitleCondensed(CharSequence title) {
+        mTitleCondensed = title;
+        return this;
+    }
+
+    public MenuItem setVisible(boolean visible) {
+        mFlags = (mFlags & HIDDEN) | (visible ? 0 : HIDDEN);
+        return this;
+    }
+
+    public boolean invoke() {
+        if (mClickListener != null && mClickListener.onMenuItemClick(this)) {
+            return true;
+        }
+        
+        if (mIntent != null) {
+            mContext.startActivity(mIntent);
+            return true;
+        }
+        
+        return false;
+    }
+    
+    public void setShowAsAction(int show) {
+        // Do nothing. ActionMenuItems always show as action buttons.
+    }
+
+    public MenuItem setActionView(View actionView) {
+        throw new UnsupportedOperationException();
+    }
+
+    public View getActionView() {
+        return null;
+    }
+
+    @Override
+    public MenuItem setActionView(int resId) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ActionProvider getActionProvider() {
+        return null;
+    }
+
+    @Override
+    public MenuItem setActionProvider(ActionProvider actionProvider) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public MenuItem setShowAsActionFlags(int actionEnum) {
+        setShowAsAction(actionEnum);
+        return this;
+    }
+
+    @Override
+    public boolean expandActionView() {
+        return false;
+    }
+
+    @Override
+    public boolean collapseActionView() {
+        return false;
+    }
+
+    @Override
+    public boolean isActionViewExpanded() {
+        return false;
+    }
+
+    @Override
+    public MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
+        // No need to save the listener; ActionMenuItem does not support collapsing items.
+        return this;
+    }
+
+    @Override
+    public MenuItem setContentDescription(CharSequence contentDescription) {
+        mContentDescription = contentDescription;
+        return this;
+    }
+
+    @Override
+    public CharSequence getContentDescription() {
+        return mContentDescription;
+    }
+
+    @Override
+    public MenuItem setTooltipText(CharSequence tooltipText) {
+        mTooltipText = tooltipText;
+        return this;
+    }
+
+    @Override
+    public CharSequence getTooltipText() {
+        return mTooltipText;
+    }
+}
diff --git a/com/android/internal/view/menu/ActionMenuItemView.java b/com/android/internal/view/menu/ActionMenuItemView.java
new file mode 100644
index 0000000..92e1d80
--- /dev/null
+++ b/com/android/internal/view/menu/ActionMenuItemView.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2010 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.internal.view.menu;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ActionMenuView;
+import android.widget.ForwardingListener;
+import android.widget.TextView;
+
+/**
+ * @hide
+ */
+public class ActionMenuItemView extends TextView
+        implements MenuView.ItemView, View.OnClickListener, ActionMenuView.ActionMenuChildView {
+    private static final String TAG = "ActionMenuItemView";
+
+    private MenuItemImpl mItemData;
+    private CharSequence mTitle;
+    private Drawable mIcon;
+    private MenuBuilder.ItemInvoker mItemInvoker;
+    private ForwardingListener mForwardingListener;
+    private PopupCallback mPopupCallback;
+
+    private boolean mAllowTextWithIcon;
+    private boolean mExpandedFormat;
+    private int mMinWidth;
+    private int mSavedPaddingLeft;
+
+    private static final int MAX_ICON_SIZE = 32; // dp
+    private int mMaxIconSize;
+
+    public ActionMenuItemView(Context context) {
+        this(context, null);
+    }
+
+    public ActionMenuItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        final Resources res = context.getResources();
+        mAllowTextWithIcon = shouldAllowTextWithIcon();
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.ActionMenuItemView, defStyleAttr, defStyleRes);
+        mMinWidth = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.ActionMenuItemView_minWidth, 0);
+        a.recycle();
+
+        final float density = res.getDisplayMetrics().density;
+        mMaxIconSize = (int) (MAX_ICON_SIZE * density + 0.5f);
+
+        setOnClickListener(this);
+
+        mSavedPaddingLeft = -1;
+        setSaveEnabled(false);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        mAllowTextWithIcon = shouldAllowTextWithIcon();
+        updateTextButtonVisibility();
+    }
+
+    /**
+     * Whether action menu items should obey the "withText" showAsAction flag. This may be set to
+     * false for situations where space is extremely limited. -->
+     */
+    private boolean shouldAllowTextWithIcon() {
+        final Configuration configuration = getContext().getResources().getConfiguration();
+        final int width = configuration.screenWidthDp;
+        final int height = configuration.screenHeightDp;
+        return  width >= 480 || (width >= 640 && height >= 480)
+                || configuration.orientation == Configuration.ORIENTATION_LANDSCAPE;
+    }
+
+    @Override
+    public void setPadding(int l, int t, int r, int b) {
+        mSavedPaddingLeft = l;
+        super.setPadding(l, t, r, b);
+    }
+
+    public MenuItemImpl getItemData() {
+        return mItemData;
+    }
+
+    @Override
+    public void initialize(MenuItemImpl itemData, int menuType) {
+        mItemData = itemData;
+
+        setIcon(itemData.getIcon());
+        setTitle(itemData.getTitleForItemView(this)); // Title is only displayed if there is no icon
+        setId(itemData.getItemId());
+
+        setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
+        setEnabled(itemData.isEnabled());
+
+        if (itemData.hasSubMenu()) {
+            if (mForwardingListener == null) {
+                mForwardingListener = new ActionMenuItemForwardingListener();
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent e) {
+        if (mItemData.hasSubMenu() && mForwardingListener != null
+                && mForwardingListener.onTouch(this, e)) {
+            return true;
+        }
+        return super.onTouchEvent(e);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mItemInvoker != null) {
+            mItemInvoker.invokeItem(mItemData);
+        }
+    }
+
+    public void setItemInvoker(MenuBuilder.ItemInvoker invoker) {
+        mItemInvoker = invoker;
+    }
+
+    public void setPopupCallback(PopupCallback popupCallback) {
+        mPopupCallback = popupCallback;
+    }
+
+    public boolean prefersCondensedTitle() {
+        return true;
+    }
+
+    public void setCheckable(boolean checkable) {
+        // TODO Support checkable action items
+    }
+
+    public void setChecked(boolean checked) {
+        // TODO Support checkable action items
+    }
+
+    public void setExpandedFormat(boolean expandedFormat) {
+        if (mExpandedFormat != expandedFormat) {
+            mExpandedFormat = expandedFormat;
+            if (mItemData != null) {
+                mItemData.actionFormatChanged();
+            }
+        }
+    }
+
+    private void updateTextButtonVisibility() {
+        boolean visible = !TextUtils.isEmpty(mTitle);
+        visible &= mIcon == null ||
+                (mItemData.showsTextAsAction() && (mAllowTextWithIcon || mExpandedFormat));
+
+        setText(visible ? mTitle : null);
+
+        final CharSequence contentDescription = mItemData.getContentDescription();
+        if (TextUtils.isEmpty(contentDescription)) {
+            // Use the uncondensed title for content description, but only if the title is not
+            // shown already.
+            setContentDescription(visible ? null : mItemData.getTitle());
+        } else {
+            setContentDescription(contentDescription);
+        }
+
+        final CharSequence tooltipText = mItemData.getTooltipText();
+        if (TextUtils.isEmpty(tooltipText)) {
+            // Use the uncondensed title for tooltip, but only if the title is not shown already.
+            setTooltipText(visible ? null : mItemData.getTitle());
+        } else {
+            setTooltipText(tooltipText);
+        }
+    }
+
+    public void setIcon(Drawable icon) {
+        mIcon = icon;
+        if (icon != null) {
+            int width = icon.getIntrinsicWidth();
+            int height = icon.getIntrinsicHeight();
+            if (width > mMaxIconSize) {
+                final float scale = (float) mMaxIconSize / width;
+                width = mMaxIconSize;
+                height *= scale;
+            }
+            if (height > mMaxIconSize) {
+                final float scale = (float) mMaxIconSize / height;
+                height = mMaxIconSize;
+                width *= scale;
+            }
+            icon.setBounds(0, 0, width, height);
+        }
+        setCompoundDrawables(icon, null, null, null);
+
+        updateTextButtonVisibility();
+    }
+
+    public boolean hasText() {
+        return !TextUtils.isEmpty(getText());
+    }
+
+    public void setShortcut(boolean showShortcut, char shortcutKey) {
+        // Action buttons don't show text for shortcut keys.
+    }
+
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+
+        updateTextButtonVisibility();
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        onPopulateAccessibilityEvent(event);
+        return true;
+    }
+
+    @Override
+    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onPopulateAccessibilityEventInternal(event);
+        final CharSequence cdesc = getContentDescription();
+        if (!TextUtils.isEmpty(cdesc)) {
+            event.getText().add(cdesc);
+        }
+    }
+
+    @Override
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // Don't allow children to hover; we want this to be treated as a single component.
+        return onHoverEvent(event);
+    }
+
+    public boolean showsIcon() {
+        return true;
+    }
+
+    public boolean needsDividerBefore() {
+        return hasText() && mItemData.getIcon() == null;
+    }
+
+    public boolean needsDividerAfter() {
+        return hasText();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final boolean textVisible = hasText();
+        if (textVisible && mSavedPaddingLeft >= 0) {
+            super.setPadding(mSavedPaddingLeft, getPaddingTop(),
+                    getPaddingRight(), getPaddingBottom());
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        final int oldMeasuredWidth = getMeasuredWidth();
+        final int targetWidth = widthMode == MeasureSpec.AT_MOST ? Math.min(widthSize, mMinWidth)
+                : mMinWidth;
+
+        if (widthMode != MeasureSpec.EXACTLY && mMinWidth > 0 && oldMeasuredWidth < targetWidth) {
+            // Remeasure at exactly the minimum width.
+            super.onMeasure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
+                    heightMeasureSpec);
+        }
+
+        if (!textVisible && mIcon != null) {
+            // TextView won't center compound drawables in both dimensions without
+            // a little coercion. Pad in to center the icon after we've measured.
+            final int w = getMeasuredWidth();
+            final int dw = mIcon.getBounds().width();
+            super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom());
+        }
+    }
+
+    private class ActionMenuItemForwardingListener extends ForwardingListener {
+        public ActionMenuItemForwardingListener() {
+            super(ActionMenuItemView.this);
+        }
+
+        @Override
+        public ShowableListMenu getPopup() {
+            if (mPopupCallback != null) {
+                return mPopupCallback.getPopup();
+            }
+            return null;
+        }
+
+        @Override
+        protected boolean onForwardingStarted() {
+            // Call the invoker, then check if the expected popup is showing.
+            if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) {
+                final ShowableListMenu popup = getPopup();
+                return popup != null && popup.isShowing();
+            }
+            return false;
+        }
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        // This might get called with the state of ActionView since it shares the same ID with
+        // ActionMenuItemView. Do not restore this state as ActionMenuItemView never saved it.
+        super.onRestoreInstanceState(null);
+    }
+
+    public static abstract class PopupCallback {
+        public abstract ShowableListMenu getPopup();
+    }
+}
diff --git a/com/android/internal/view/menu/BaseMenuPresenter.java b/com/android/internal/view/menu/BaseMenuPresenter.java
new file mode 100644
index 0000000..7ac0ac3
--- /dev/null
+++ b/com/android/internal/view/menu/BaseMenuPresenter.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2011 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.internal.view.menu;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Base class for MenuPresenters that have a consistent container view and item
+ * views. Behaves similarly to an AdapterView in that existing item views will
+ * be reused if possible when items change.
+ */
+public abstract class BaseMenuPresenter implements MenuPresenter {
+    protected Context mSystemContext;
+    protected Context mContext;
+    protected MenuBuilder mMenu;
+    protected LayoutInflater mSystemInflater;
+    protected LayoutInflater mInflater;
+    private Callback mCallback;
+
+    private int mMenuLayoutRes;
+    private int mItemLayoutRes;
+
+    protected MenuView mMenuView;
+
+    private int mId;
+
+    /**
+     * Construct a new BaseMenuPresenter.
+     *
+     * @param context Context for generating system-supplied views
+     * @param menuLayoutRes Layout resource ID for the menu container view
+     * @param itemLayoutRes Layout resource ID for a single item view
+     */
+    public BaseMenuPresenter(Context context, int menuLayoutRes, int itemLayoutRes) {
+        mSystemContext = context;
+        mSystemInflater = LayoutInflater.from(context);
+        mMenuLayoutRes = menuLayoutRes;
+        mItemLayoutRes = itemLayoutRes;
+    }
+
+    @Override
+    public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+        mContext = context;
+        mInflater = LayoutInflater.from(mContext);
+        mMenu = menu;
+    }
+
+    @Override
+    public MenuView getMenuView(ViewGroup root) {
+        if (mMenuView == null) {
+            mMenuView = (MenuView) mSystemInflater.inflate(mMenuLayoutRes, root, false);
+            mMenuView.initialize(mMenu);
+            updateMenuView(true);
+        }
+
+        return mMenuView;
+    }
+
+    /**
+     * Reuses item views when it can
+     */
+    public void updateMenuView(boolean cleared) {
+        final ViewGroup parent = (ViewGroup) mMenuView;
+        if (parent == null) return;
+
+        int childIndex = 0;
+        if (mMenu != null) {
+            mMenu.flagActionItems();
+            ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
+            final int itemCount = visibleItems.size();
+            for (int i = 0; i < itemCount; i++) {
+                MenuItemImpl item = visibleItems.get(i);
+                if (shouldIncludeItem(childIndex, item)) {
+                    final View convertView = parent.getChildAt(childIndex);
+                    final MenuItemImpl oldItem = convertView instanceof MenuView.ItemView ?
+                            ((MenuView.ItemView) convertView).getItemData() : null;
+                    final View itemView = getItemView(item, convertView, parent);
+                    if (item != oldItem) {
+                        // Don't let old states linger with new data.
+                        itemView.setPressed(false);
+                        itemView.jumpDrawablesToCurrentState();
+                    }
+                    if (itemView != convertView) {
+                        addItemView(itemView, childIndex);
+                    }
+                    childIndex++;
+                }
+            }
+        }
+
+        // Remove leftover views.
+        while (childIndex < parent.getChildCount()) {
+            if (!filterLeftoverView(parent, childIndex)) {
+                childIndex++;
+            }
+        }
+    }
+
+    /**
+     * Add an item view at the given index.
+     *
+     * @param itemView View to add
+     * @param childIndex Index within the parent to insert at
+     */
+    protected void addItemView(View itemView, int childIndex) {
+        final ViewGroup currentParent = (ViewGroup) itemView.getParent();
+        if (currentParent != null) {
+            currentParent.removeView(itemView);
+        }
+        ((ViewGroup) mMenuView).addView(itemView, childIndex);
+    }
+
+    /**
+     * Filter the child view at index and remove it if appropriate.
+     * @param parent Parent to filter from
+     * @param childIndex Index to filter
+     * @return true if the child view at index was removed
+     */
+    protected boolean filterLeftoverView(ViewGroup parent, int childIndex) {
+        parent.removeViewAt(childIndex);
+        return true;
+    }
+
+    public void setCallback(Callback cb) {
+        mCallback = cb;
+    }
+
+    public Callback getCallback() {
+        return mCallback;
+    }
+
+    /**
+     * Create a new item view that can be re-bound to other item data later.
+     *
+     * @return The new item view
+     */
+    public MenuView.ItemView createItemView(ViewGroup parent) {
+        return (MenuView.ItemView) mSystemInflater.inflate(mItemLayoutRes, parent, false);
+    }
+
+    /**
+     * Prepare an item view for use. See AdapterView for the basic idea at work here.
+     * This may require creating a new item view, but well-behaved implementations will
+     * re-use the view passed as convertView if present. The returned view will be populated
+     * with data from the item parameter.
+     *
+     * @param item Item to present
+     * @param convertView Existing view to reuse
+     * @param parent Intended parent view - use for inflation.
+     * @return View that presents the requested menu item
+     */
+    public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) {
+        MenuView.ItemView itemView;
+        if (convertView instanceof MenuView.ItemView) {
+            itemView = (MenuView.ItemView) convertView;
+        } else {
+            itemView = createItemView(parent);
+        }
+        bindItemView(item, itemView);
+        return (View) itemView;
+    }
+
+    /**
+     * Bind item data to an existing item view.
+     *
+     * @param item Item to bind
+     * @param itemView View to populate with item data
+     */
+    public abstract void bindItemView(MenuItemImpl item, MenuView.ItemView itemView);
+
+    /**
+     * Filter item by child index and item data.
+     *
+     * @param childIndex Indended presentation index of this item
+     * @param item Item to present
+     * @return true if this item should be included in this menu presentation; false otherwise
+     */
+    public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
+        return true;
+    }
+
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        if (mCallback != null) {
+            mCallback.onCloseMenu(menu, allMenusAreClosing);
+        }
+    }
+
+    public boolean onSubMenuSelected(SubMenuBuilder menu) {
+        if (mCallback != null) {
+            return mCallback.onOpenSubMenu(menu);
+        }
+        return false;
+    }
+
+    public boolean flagActionItems() {
+        return false;
+    }
+
+    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+        return false;
+    }
+
+    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+        return false;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public void setId(int id) {
+        mId = id;
+    }
+}
diff --git a/com/android/internal/view/menu/BridgeMenuItemImpl.java b/com/android/internal/view/menu/BridgeMenuItemImpl.java
new file mode 100644
index 0000000..bb95c4e
--- /dev/null
+++ b/com/android/internal/view/menu/BridgeMenuItemImpl.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 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.internal.view.menu;
+
+import com.android.layoutlib.bridge.android.BridgeContext;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * An extension of the {@link MenuItemImpl} to store the view cookie also.
+ */
+public class BridgeMenuItemImpl extends MenuItemImpl {
+
+    /**
+     * An object returned by the IDE that helps mapping each View to the corresponding XML tag in
+     * the layout. For Menus, we store this cookie here and attach it to the corresponding view
+     * at the time of rendering.
+     */
+    private Object viewCookie;
+    private BridgeContext mContext;
+
+    /**
+     * Instantiates this menu item.
+     */
+    BridgeMenuItemImpl(MenuBuilder menu, int group, int id, int categoryOrder, int ordering,
+            CharSequence title, int showAsAction) {
+        super(menu, group, id, categoryOrder, ordering, title, showAsAction);
+        Context context = menu.getContext();
+        context = BridgeContext.getBaseContext(context);
+        if (context instanceof BridgeContext) {
+            mContext = ((BridgeContext) context);
+        }
+    }
+
+    public Object getViewCookie() {
+        return viewCookie;
+    }
+
+    public void setViewCookie(Object viewCookie) {
+        // If the menu item has an associated action provider view,
+        // directly set the cookie in the view to cookie map stored in BridgeContext.
+        View actionView = getActionView();
+        if (actionView != null && mContext != null) {
+            mContext.addViewKey(actionView, viewCookie);
+            // We don't need to add the view cookie to the this item now. But there's no harm in
+            // storing it, in case we need it in the future.
+        }
+        this.viewCookie = viewCookie;
+    }
+}
diff --git a/com/android/internal/view/menu/CascadingMenuPopup.java b/com/android/internal/view/menu/CascadingMenuPopup.java
new file mode 100644
index 0000000..6dff8b4
--- /dev/null
+++ b/com/android/internal/view/menu/CascadingMenuPopup.java
@@ -0,0 +1,746 @@
+package com.android.internal.view.menu;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.annotation.AttrRes;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.AbsListView;
+import android.widget.FrameLayout;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ListAdapter;
+import android.widget.MenuItemHoverListener;
+import android.widget.ListView;
+import android.widget.MenuPopupWindow;
+import android.widget.PopupWindow;
+import android.widget.PopupWindow.OnDismissListener;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
+/**
+ * A popup for a menu which will allow multiple submenus to appear in a cascading fashion, side by
+ * side.
+ * @hide
+ */
+final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKeyListener,
+        PopupWindow.OnDismissListener {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({HORIZ_POSITION_LEFT, HORIZ_POSITION_RIGHT})
+    public @interface HorizPosition {}
+
+    private static final int HORIZ_POSITION_LEFT = 0;
+    private static final int HORIZ_POSITION_RIGHT = 1;
+
+    /**
+     * Delay between hovering over a menu item with a mouse and receiving
+     * side-effects (ex. opening a sub-menu or closing unrelated menus).
+     */
+    private static final int SUBMENU_TIMEOUT_MS = 200;
+
+    private final Context mContext;
+    private final int mMenuMaxWidth;
+    private final int mPopupStyleAttr;
+    private final int mPopupStyleRes;
+    private final boolean mOverflowOnly;
+    private final Handler mSubMenuHoverHandler;
+
+    /** List of menus that were added before this popup was shown. */
+    private final List<MenuBuilder> mPendingMenus = new LinkedList<>();
+
+    /**
+     * List of open menus. The first item is the root menu and each
+     * subsequent item is a direct submenu of the previous item.
+     */
+    private final List<CascadingMenuInfo> mShowingMenus = new ArrayList<>();
+
+    private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() {
+        @Override
+        public void onGlobalLayout() {
+            // Only move the popup if it's showing and non-modal. We don't want
+            // to be moving around the only interactive window, since there's a
+            // good chance the user is interacting with it.
+            if (isShowing() && mShowingMenus.size() > 0
+                    && !mShowingMenus.get(0).window.isModal()) {
+                final View anchor = mShownAnchorView;
+                if (anchor == null || !anchor.isShown()) {
+                    dismiss();
+                } else {
+                    // Recompute window sizes and positions.
+                    for (CascadingMenuInfo info : mShowingMenus) {
+                        info.window.show();
+                    }
+                }
+            }
+        }
+    };
+
+    private final OnAttachStateChangeListener mAttachStateChangeListener =
+            new OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    if (mTreeObserver != null) {
+                        if (!mTreeObserver.isAlive()) {
+                            mTreeObserver = v.getViewTreeObserver();
+                        }
+                        mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
+                    }
+                    v.removeOnAttachStateChangeListener(this);
+                }
+            };
+
+    private final MenuItemHoverListener mMenuItemHoverListener = new MenuItemHoverListener() {
+        @Override
+        public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
+            // If the mouse moves between two windows, hover enter/exit pairs
+            // may be received out of order. So, instead of canceling all
+            // pending runnables, only cancel runnables for the host menu.
+            mSubMenuHoverHandler.removeCallbacksAndMessages(menu);
+        }
+
+        @Override
+        public void onItemHoverEnter(
+                @NonNull final MenuBuilder menu, @NonNull final MenuItem item) {
+            // Something new was hovered, cancel all scheduled runnables.
+            mSubMenuHoverHandler.removeCallbacksAndMessages(null);
+
+            // Find the position of the hovered menu within the added menus.
+            int menuIndex = -1;
+            for (int i = 0, count = mShowingMenus.size(); i < count; i++) {
+                if (menu == mShowingMenus.get(i).menu) {
+                    menuIndex = i;
+                    break;
+                }
+            }
+
+            if (menuIndex == -1) {
+                return;
+            }
+
+            final CascadingMenuInfo nextInfo;
+            final int nextIndex = menuIndex + 1;
+            if (nextIndex < mShowingMenus.size()) {
+                nextInfo = mShowingMenus.get(nextIndex);
+            } else {
+                nextInfo = null;
+            }
+
+            final Runnable runnable = new Runnable() {
+                @Override
+                public void run() {
+                    // Close any other submenus that might be open at the
+                    // current or a deeper level.
+                    if (nextInfo != null) {
+                        // Disable exit animations to prevent overlapping
+                        // fading out submenus.
+                        mShouldCloseImmediately = true;
+                        nextInfo.menu.close(false /* closeAllMenus */);
+                        mShouldCloseImmediately = false;
+                    }
+
+                    // Then open the selected submenu, if there is one.
+                    if (item.isEnabled() && item.hasSubMenu()) {
+                        menu.performItemAction(item, 0);
+                    }
+                }
+            };
+            final long uptimeMillis = SystemClock.uptimeMillis() + SUBMENU_TIMEOUT_MS;
+            mSubMenuHoverHandler.postAtTime(runnable, menu, uptimeMillis);
+        }
+    };
+
+    private int mRawDropDownGravity = Gravity.NO_GRAVITY;
+    private int mDropDownGravity = Gravity.NO_GRAVITY;
+    private View mAnchorView;
+    private View mShownAnchorView;
+    private int mLastPosition;
+    private boolean mHasXOffset;
+    private boolean mHasYOffset;
+    private int mXOffset;
+    private int mYOffset;
+    private boolean mForceShowIcon;
+    private boolean mShowTitle;
+    private Callback mPresenterCallback;
+    private ViewTreeObserver mTreeObserver;
+    private PopupWindow.OnDismissListener mOnDismissListener;
+
+    /** Whether popup menus should disable exit animations when closing. */
+    private boolean mShouldCloseImmediately;
+
+    /**
+     * Initializes a new cascading-capable menu popup.
+     *
+     * @param anchor A parent view to get the {@link android.view.View#getWindowToken()} token from.
+     */
+    public CascadingMenuPopup(@NonNull Context context, @NonNull View anchor,
+            @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes, boolean overflowOnly) {
+        mContext = Preconditions.checkNotNull(context);
+        mAnchorView = Preconditions.checkNotNull(anchor);
+        mPopupStyleAttr = popupStyleAttr;
+        mPopupStyleRes = popupStyleRes;
+        mOverflowOnly = overflowOnly;
+
+        mForceShowIcon = false;
+        mLastPosition = getInitialMenuPosition();
+
+        final Resources res = context.getResources();
+        mMenuMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
+                res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
+
+        mSubMenuHoverHandler = new Handler();
+    }
+
+    @Override
+    public void setForceShowIcon(boolean forceShow) {
+        mForceShowIcon = forceShow;
+    }
+
+    private MenuPopupWindow createPopupWindow() {
+        MenuPopupWindow popupWindow = new MenuPopupWindow(
+                mContext, null, mPopupStyleAttr, mPopupStyleRes);
+        popupWindow.setHoverListener(mMenuItemHoverListener);
+        popupWindow.setOnItemClickListener(this);
+        popupWindow.setOnDismissListener(this);
+        popupWindow.setAnchorView(mAnchorView);
+        popupWindow.setDropDownGravity(mDropDownGravity);
+        popupWindow.setModal(true);
+        popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+        return popupWindow;
+    }
+
+    @Override
+    public void show() {
+        if (isShowing()) {
+            return;
+        }
+
+        // Display all pending menus.
+        for (MenuBuilder menu : mPendingMenus) {
+            showMenu(menu);
+        }
+        mPendingMenus.clear();
+
+        mShownAnchorView = mAnchorView;
+
+        if (mShownAnchorView != null) {
+            final boolean addGlobalListener = mTreeObserver == null;
+            mTreeObserver = mShownAnchorView.getViewTreeObserver(); // Refresh to latest
+            if (addGlobalListener) {
+                mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
+            }
+            mShownAnchorView.addOnAttachStateChangeListener(mAttachStateChangeListener);
+        }
+    }
+
+    @Override
+    public void dismiss() {
+        // Need to make another list to avoid a concurrent modification
+        // exception, as #onDismiss may clear mPopupWindows while we are
+        // iterating. Remove from the last added menu so that the callbacks
+        // are received in order from foreground to background.
+        final int length = mShowingMenus.size();
+        if (length > 0) {
+            final CascadingMenuInfo[] addedMenus =
+                    mShowingMenus.toArray(new CascadingMenuInfo[length]);
+            for (int i = length - 1; i >= 0; i--) {
+                final CascadingMenuInfo info = addedMenus[i];
+                if (info.window.isShowing()) {
+                    info.window.dismiss();
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
+            dismiss();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Determines the proper initial menu position for the current LTR/RTL configuration.
+     * @return The initial position.
+     */
+    @HorizPosition
+    private int getInitialMenuPosition() {
+        final int layoutDirection = mAnchorView.getLayoutDirection();
+        return layoutDirection == View.LAYOUT_DIRECTION_RTL ? HORIZ_POSITION_LEFT :
+                HORIZ_POSITION_RIGHT;
+    }
+
+    /**
+     * Determines whether the next submenu (of the given width) should display on the right or on
+     * the left of the most recent menu.
+     *
+     * @param nextMenuWidth Width of the next submenu to display.
+     * @return The position to display it.
+     */
+    @HorizPosition
+    private int getNextMenuPosition(int nextMenuWidth) {
+        ListView lastListView = mShowingMenus.get(mShowingMenus.size() - 1).getListView();
+
+        final int[] screenLocation = new int[2];
+        lastListView.getLocationOnScreen(screenLocation);
+
+        final Rect displayFrame = new Rect();
+        mShownAnchorView.getWindowVisibleDisplayFrame(displayFrame);
+
+        if (mLastPosition == HORIZ_POSITION_RIGHT) {
+            final int right = screenLocation[0] + lastListView.getWidth() + nextMenuWidth;
+            if (right > displayFrame.right) {
+                return HORIZ_POSITION_LEFT;
+            }
+            return HORIZ_POSITION_RIGHT;
+        } else { // LEFT
+            final int left = screenLocation[0] - nextMenuWidth;
+            if (left < 0) {
+                return HORIZ_POSITION_RIGHT;
+            }
+            return HORIZ_POSITION_LEFT;
+        }
+    }
+
+    @Override
+    public void addMenu(MenuBuilder menu) {
+        menu.addMenuPresenter(this, mContext);
+
+        if (isShowing()) {
+            showMenu(menu);
+        } else {
+            mPendingMenus.add(menu);
+        }
+    }
+
+    /**
+     * Prepares and shows the specified menu immediately.
+     *
+     * @param menu the menu to show
+     */
+    private void showMenu(@NonNull MenuBuilder menu) {
+        final LayoutInflater inflater = LayoutInflater.from(mContext);
+        final MenuAdapter adapter = new MenuAdapter(menu, inflater, mOverflowOnly);
+
+        // Apply "force show icon" setting. There are 3 cases:
+        // (1) This is the top level menu and icon spacing is forced. Add spacing.
+        // (2) This is a submenu. Add spacing if any of the visible menu items has an icon.
+        // (3) This is the top level menu and icon spacing isn't forced. Do not add spacing.
+        if (!isShowing() && mForceShowIcon) {
+          // Case 1
+          adapter.setForceShowIcon(true);
+        } else if (isShowing()) {
+          // Case 2
+          adapter.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(menu));
+        }
+        // Case 3: Else, don't allow spacing for icons (default behavior; do nothing).
+
+        final int menuWidth = measureIndividualMenuWidth(adapter, null, mContext, mMenuMaxWidth);
+        final MenuPopupWindow popupWindow = createPopupWindow();
+        popupWindow.setAdapter(adapter);
+        popupWindow.setContentWidth(menuWidth);
+        popupWindow.setDropDownGravity(mDropDownGravity);
+
+        final CascadingMenuInfo parentInfo;
+        final View parentView;
+        if (mShowingMenus.size() > 0) {
+            parentInfo = mShowingMenus.get(mShowingMenus.size() - 1);
+            parentView = findParentViewForSubmenu(parentInfo, menu);
+        } else {
+            parentInfo = null;
+            parentView = null;
+        }
+
+        if (parentView != null) {
+            // This menu is a cascading submenu anchored to a parent view.
+            popupWindow.setAnchorView(parentView);
+            popupWindow.setTouchModal(false);
+            popupWindow.setEnterTransition(null);
+
+            final @HorizPosition int nextMenuPosition = getNextMenuPosition(menuWidth);
+            final boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT;
+            mLastPosition = nextMenuPosition;
+
+            // Compute the horizontal offset to display the submenu to the right or to the left
+            // of the parent item.
+            // By now, mDropDownGravity is the resolved absolute gravity, so
+            // this should work in both LTR and RTL.
+            final int x;
+            if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) {
+                if (showOnRight) {
+                    x = menuWidth;
+                } else {
+                    x = -parentView.getWidth();
+                }
+            } else {
+                if (showOnRight) {
+                    x = parentView.getWidth();
+                } else {
+                    x = -menuWidth;
+                }
+            }
+            popupWindow.setHorizontalOffset(x);
+
+            // Align with the top edge of the parent view (or the bottom edge when the submenu is
+            // flipped vertically).
+            popupWindow.setOverlapAnchor(true);
+            popupWindow.setVerticalOffset(0);
+        } else {
+            if (mHasXOffset) {
+                popupWindow.setHorizontalOffset(mXOffset);
+            }
+            if (mHasYOffset) {
+                popupWindow.setVerticalOffset(mYOffset);
+            }
+            final Rect epicenterBounds = getEpicenterBounds();
+            popupWindow.setEpicenterBounds(epicenterBounds);
+        }
+
+
+        final CascadingMenuInfo menuInfo = new CascadingMenuInfo(popupWindow, menu, mLastPosition);
+        mShowingMenus.add(menuInfo);
+
+        popupWindow.show();
+
+        final ListView listView = popupWindow.getListView();
+        listView.setOnKeyListener(this);
+
+        // If this is the root menu, show the title if requested.
+        if (parentInfo == null && mShowTitle && menu.getHeaderTitle() != null) {
+            final FrameLayout titleItemView = (FrameLayout) inflater.inflate(
+                    R.layout.popup_menu_header_item_layout, listView, false);
+            final TextView titleView = (TextView) titleItemView.findViewById(R.id.title);
+            titleItemView.setEnabled(false);
+            titleView.setText(menu.getHeaderTitle());
+            listView.addHeaderView(titleItemView, null, false);
+
+            // Show again to update the title.
+            popupWindow.show();
+        }
+    }
+
+    /**
+     * Returns the menu item within the specified parent menu that owns
+     * specified submenu.
+     *
+     * @param parent the parent menu
+     * @param submenu the submenu for which the index should be returned
+     * @return the menu item that owns the submenu, or {@code null} if not
+     *         present
+     */
+    private MenuItem findMenuItemForSubmenu(
+            @NonNull MenuBuilder parent, @NonNull MenuBuilder submenu) {
+        for (int i = 0, count = parent.size(); i < count; i++) {
+            final MenuItem item = parent.getItem(i);
+            if (item.hasSubMenu() && submenu == item.getSubMenu()) {
+                return item;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Attempts to find the view for the menu item that owns the specified
+     * submenu.
+     *
+     * @param parentInfo info for the parent menu
+     * @param submenu the submenu whose parent view should be obtained
+     * @return the parent view, or {@code null} if one could not be found
+     */
+    @Nullable
+    private View findParentViewForSubmenu(
+            @NonNull CascadingMenuInfo parentInfo, @NonNull MenuBuilder submenu) {
+        final MenuItem owner = findMenuItemForSubmenu(parentInfo.menu, submenu);
+        if (owner == null) {
+            // Couldn't find the submenu owner.
+            return null;
+        }
+
+        // The adapter may be wrapped. Adjust the index if necessary.
+        final int headersCount;
+        final MenuAdapter menuAdapter;
+        final ListView listView = parentInfo.getListView();
+        final ListAdapter listAdapter = listView.getAdapter();
+        if (listAdapter instanceof HeaderViewListAdapter) {
+            final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) listAdapter;
+            headersCount = headerAdapter.getHeadersCount();
+            menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter();
+        } else {
+            headersCount = 0;
+            menuAdapter = (MenuAdapter) listAdapter;
+        }
+
+        // Find the index within the menu adapter's data set of the menu item.
+        int ownerPosition = AbsListView.INVALID_POSITION;
+        for (int i = 0, count = menuAdapter.getCount(); i < count; i++) {
+            if (owner == menuAdapter.getItem(i)) {
+                ownerPosition = i;
+                break;
+            }
+        }
+        if (ownerPosition == AbsListView.INVALID_POSITION) {
+            // Couldn't find the owner within the menu adapter.
+            return null;
+        }
+
+        // Adjust the index for the adapter used to display views.
+        ownerPosition += headersCount;
+
+        // Adjust the index for the visible views.
+        final int ownerViewPosition = ownerPosition - listView.getFirstVisiblePosition();
+        if (ownerViewPosition < 0 || ownerViewPosition >= listView.getChildCount()) {
+            // Not visible on screen.
+            return null;
+        }
+
+        return listView.getChildAt(ownerViewPosition);
+    }
+
+    /**
+     * @return {@code true} if the popup is currently showing, {@code false} otherwise.
+     */
+    @Override
+    public boolean isShowing() {
+        return mShowingMenus.size() > 0 && mShowingMenus.get(0).window.isShowing();
+    }
+
+    /**
+     * Called when one or more of the popup windows was dismissed.
+     */
+    @Override
+    public void onDismiss() {
+        // The dismiss listener doesn't pass the calling window, so walk
+        // through the stack to figure out which one was just dismissed.
+        CascadingMenuInfo dismissedInfo = null;
+        for (int i = 0, count = mShowingMenus.size(); i < count; i++) {
+            final CascadingMenuInfo info = mShowingMenus.get(i);
+            if (!info.window.isShowing()) {
+                dismissedInfo = info;
+                break;
+            }
+        }
+
+        // Close all menus starting from the dismissed menu, passing false
+        // since we are manually closing only a subset of windows.
+        if (dismissedInfo != null) {
+            dismissedInfo.menu.close(false);
+        }
+    }
+
+    @Override
+    public void updateMenuView(boolean cleared) {
+        for (CascadingMenuInfo info : mShowingMenus) {
+            toMenuAdapter(info.getListView().getAdapter()).notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    public void setCallback(Callback cb) {
+        mPresenterCallback = cb;
+    }
+
+    @Override
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+        // Don't allow double-opening of the same submenu.
+        for (CascadingMenuInfo info : mShowingMenus) {
+            if (subMenu == info.menu) {
+                // Just re-focus that one.
+                info.getListView().requestFocus();
+                return true;
+            }
+        }
+
+        if (subMenu.hasVisibleItems()) {
+            addMenu(subMenu);
+
+            if (mPresenterCallback != null) {
+                mPresenterCallback.onOpenSubMenu(subMenu);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Finds the index of the specified menu within the list of added menus.
+     *
+     * @param menu the menu to find
+     * @return the index of the menu, or {@code -1} if not present
+     */
+    private int findIndexOfAddedMenu(@NonNull MenuBuilder menu) {
+        for (int i = 0, count = mShowingMenus.size(); i < count; i++) {
+            final CascadingMenuInfo info  = mShowingMenus.get(i);
+            if (menu == info.menu) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    @Override
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        final int menuIndex = findIndexOfAddedMenu(menu);
+        if (menuIndex < 0) {
+            return;
+        }
+
+        // Recursively close descendant menus.
+        final int nextMenuIndex = menuIndex + 1;
+        if (nextMenuIndex < mShowingMenus.size()) {
+            final CascadingMenuInfo childInfo = mShowingMenus.get(nextMenuIndex);
+            childInfo.menu.close(false /* closeAllMenus */);
+        }
+
+        // Close the target menu.
+        final CascadingMenuInfo info = mShowingMenus.remove(menuIndex);
+        info.menu.removeMenuPresenter(this);
+        if (mShouldCloseImmediately) {
+            // Disable all exit animations.
+            info.window.setExitTransition(null);
+            info.window.setAnimationStyle(0);
+        }
+        info.window.dismiss();
+
+        final int count = mShowingMenus.size();
+        if (count > 0) {
+            mLastPosition = mShowingMenus.get(count - 1).position;
+        } else {
+            mLastPosition = getInitialMenuPosition();
+        }
+
+        if (count == 0) {
+            // This was the last window. Clean up.
+            dismiss();
+
+            if (mPresenterCallback != null) {
+                mPresenterCallback.onCloseMenu(menu, true);
+            }
+
+            if (mTreeObserver != null) {
+                if (mTreeObserver.isAlive()) {
+                    mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
+                }
+                mTreeObserver = null;
+            }
+            mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
+
+            // If every [sub]menu was dismissed, that means the whole thing was
+            // dismissed, so notify the owner.
+            mOnDismissListener.onDismiss();
+        } else if (allMenusAreClosing) {
+            // Close all menus starting from the root. This will recursively
+            // close any remaining menus, so we don't need to propagate the
+            // "closeAllMenus" flag. The last window will clean up.
+            final CascadingMenuInfo rootInfo = mShowingMenus.get(0);
+            rootInfo.menu.close(false /* closeAllMenus */);
+        }
+    }
+
+    @Override
+    public boolean flagActionItems() {
+        return false;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        return null;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+    }
+
+    @Override
+    public void setGravity(int dropDownGravity) {
+        if (mRawDropDownGravity != dropDownGravity) {
+            mRawDropDownGravity = dropDownGravity;
+            mDropDownGravity = Gravity.getAbsoluteGravity(
+                    dropDownGravity, mAnchorView.getLayoutDirection());
+        }
+    }
+
+    @Override
+    public void setAnchorView(@NonNull View anchor) {
+        if (mAnchorView != anchor) {
+            mAnchorView = anchor;
+
+            // Gravity resolution may have changed, update from raw gravity.
+            mDropDownGravity = Gravity.getAbsoluteGravity(
+                    mRawDropDownGravity, mAnchorView.getLayoutDirection());
+        }
+    }
+
+    @Override
+    public void setOnDismissListener(OnDismissListener listener) {
+        mOnDismissListener = listener;
+    }
+
+    @Override
+    public ListView getListView() {
+        return mShowingMenus.isEmpty() ? null : mShowingMenus.get(mShowingMenus.size() - 1).getListView();
+    }
+
+    @Override
+    public void setHorizontalOffset(int x) {
+        mHasXOffset = true;
+        mXOffset = x;
+    }
+
+    @Override
+    public void setVerticalOffset(int y) {
+        mHasYOffset = true;
+        mYOffset = y;
+    }
+
+    @Override
+    public void setShowTitle(boolean showTitle) {
+        mShowTitle = showTitle;
+    }
+
+    private static class CascadingMenuInfo {
+        public final MenuPopupWindow window;
+        public final MenuBuilder menu;
+        public final int position;
+
+        public CascadingMenuInfo(@NonNull MenuPopupWindow window, @NonNull MenuBuilder menu,
+                int position) {
+            this.window = window;
+            this.menu = menu;
+            this.position = position;
+        }
+
+        public ListView getListView() {
+            return window.getListView();
+        }
+    }
+}
diff --git a/com/android/internal/view/menu/ContextMenuBuilder.java b/com/android/internal/view/menu/ContextMenuBuilder.java
new file mode 100644
index 0000000..82f061c
--- /dev/null
+++ b/com/android/internal/view/menu/ContextMenuBuilder.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.IBinder;
+import android.util.EventLog;
+import android.view.ContextMenu;
+import android.view.View;
+
+/**
+ * Implementation of the {@link android.view.ContextMenu} interface.
+ * <p>
+ * Most clients of the menu framework will never need to touch this
+ * class.  However, if the client has a window that
+ * is not a content view of a Dialog or Activity (for example, the
+ * view was added directly to the window manager) and needs to show
+ * context menus, it will use this class.
+ * <p>
+ * To use this class, instantiate it via {@link #ContextMenuBuilder(Context)},
+ * and optionally populate it with any of your custom items.  Finally,
+ * call {@link #showDialog(View, IBinder)} which will populate the menu
+ * with a view's context menu items and show the context menu.
+ */
+public class ContextMenuBuilder extends MenuBuilder implements ContextMenu {
+    
+    public ContextMenuBuilder(Context context) {
+        super(context);
+    }
+
+    public ContextMenu setHeaderIcon(Drawable icon) {
+        return (ContextMenu) super.setHeaderIconInt(icon);
+    }
+
+    public ContextMenu setHeaderIcon(int iconRes) {
+        return (ContextMenu) super.setHeaderIconInt(iconRes);
+    }
+
+    public ContextMenu setHeaderTitle(CharSequence title) {
+        return (ContextMenu) super.setHeaderTitleInt(title);
+    }
+
+    public ContextMenu setHeaderTitle(int titleRes) {
+        return (ContextMenu) super.setHeaderTitleInt(titleRes);
+    }
+
+    public ContextMenu setHeaderView(View view) {
+        return (ContextMenu) super.setHeaderViewInt(view);
+    }
+
+    /**
+     * Shows this context menu, allowing the optional original view (and its
+     * ancestors) to add items.
+     * 
+     * @param originalView Optional, the original view that triggered the
+     *        context menu.
+     * @param token Optional, the window token that should be set on the context
+     *        menu's window.
+     * @return If the context menu was shown, the {@link MenuDialogHelper} for
+     *         dismissing it. Otherwise, null.
+     */
+    public MenuDialogHelper showDialog(View originalView, IBinder token) {
+        if (originalView != null) {
+            // Let relevant views and their populate context listeners populate
+            // the context menu
+            originalView.createContextMenu(this);
+        }
+
+        if (getVisibleItems().size() > 0) {
+            EventLog.writeEvent(50001, 1);
+            
+            MenuDialogHelper helper = new MenuDialogHelper(this); 
+            helper.show(token);
+            
+            return helper;
+        }
+        
+        return null;
+    }
+    
+    public MenuPopupHelper showPopup(Context context, View originalView, float x, float y) {
+        if (originalView != null) {
+            // Let relevant views and their populate context listeners populate
+            // the context menu
+            originalView.createContextMenu(this);
+        }
+
+        if (getVisibleItems().size() > 0) {
+            EventLog.writeEvent(50001, 1);
+
+            int location[] = new int[2];
+            originalView.getLocationOnScreen(location);
+
+            final MenuPopupHelper helper = new MenuPopupHelper(
+                    context,
+                    this,
+                    originalView,
+                    false /* overflowOnly */,
+                    com.android.internal.R.attr.contextPopupMenuStyle);
+            helper.show(Math.round(x), Math.round(y));
+            return helper;
+        }
+
+        return null;
+    }
+}
diff --git a/com/android/internal/view/menu/ExpandedMenuView.java b/com/android/internal/view/menu/ExpandedMenuView.java
new file mode 100644
index 0000000..47058ad
--- /dev/null
+++ b/com/android/internal/view/menu/ExpandedMenuView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+
+import com.android.internal.view.menu.MenuBuilder.ItemInvoker;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+/**
+ * The expanded menu view is a list-like menu with all of the available menu items.  It is opened
+ * by the user clicking no the 'More' button on the icon menu view.
+ */
+public final class ExpandedMenuView extends ListView implements ItemInvoker, MenuView, OnItemClickListener {
+    private MenuBuilder mMenu;
+
+    /** Default animations for this menu */
+    private int mAnimations;
+    
+    /**
+     * Instantiates the ExpandedMenuView that is linked with the provided MenuBuilder.
+     * @param menu The model for the menu which this MenuView will display
+     */
+    public ExpandedMenuView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        
+        TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuView, 0, 0);
+        mAnimations = a.getResourceId(com.android.internal.R.styleable.MenuView_windowAnimationStyle, 0);
+        a.recycle();
+
+        setOnItemClickListener(this);
+    }
+
+    public void initialize(MenuBuilder menu) {
+        mMenu = menu;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        
+        // Clear the cached bitmaps of children
+        setChildrenDrawingCacheEnabled(false);
+    }
+
+    public boolean invokeItem(MenuItemImpl item) {
+        return mMenu.performItemAction(item, 0);
+    }
+
+    public void onItemClick(AdapterView parent, View v, int position, long id) {
+        invokeItem((MenuItemImpl) getAdapter().getItem(position));
+    }
+
+    public int getWindowAnimations() {
+        return mAnimations;
+    }
+    
+}
diff --git a/com/android/internal/view/menu/IconMenuItemView.java b/com/android/internal/view/menu/IconMenuItemView.java
new file mode 100644
index 0000000..6c8f330
--- /dev/null
+++ b/com/android/internal/view/menu/IconMenuItemView.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import com.android.internal.view.menu.MenuBuilder.ItemInvoker;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.widget.TextView;
+import android.text.Layout;
+
+/**
+ * The item view for each item in the {@link IconMenuView}.
+ */
+public final class IconMenuItemView extends TextView implements MenuView.ItemView {
+    
+    private static final int NO_ALPHA = 0xFF;
+    
+    private IconMenuView mIconMenuView;
+    
+    private ItemInvoker mItemInvoker;
+    private MenuItemImpl mItemData; 
+    
+    private Drawable mIcon;
+    
+    private int mTextAppearance;
+    private Context mTextAppearanceContext;
+    
+    private float mDisabledAlpha;
+
+    private Rect mPositionIconAvailable = new Rect();
+    private Rect mPositionIconOutput = new Rect();
+    
+    private boolean mShortcutCaptionMode;
+    private String mShortcutCaption;
+    
+    private static String sPrependShortcutLabel;
+
+    public IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        if (sPrependShortcutLabel == null) {
+            /*
+             * Views should only be constructed from the UI thread, so no
+             * synchronization needed
+             */
+            sPrependShortcutLabel = getResources().getString(
+                    com.android.internal.R.string.prepend_shortcut_label);
+        }
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.MenuView, defStyleAttr, defStyleRes);
+
+        mDisabledAlpha = a.getFloat(
+                com.android.internal.R.styleable.MenuView_itemIconDisabledAlpha, 0.8f);
+        mTextAppearance = a.getResourceId(com.android.internal.R.styleable.
+                                          MenuView_itemTextAppearance, -1);
+        mTextAppearanceContext = context;
+        
+        a.recycle();
+    }
+
+    public IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public IconMenuItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Initializes with the provided title and icon
+     * @param title The title of this item
+     * @param icon The icon of this item
+     */
+    void initialize(CharSequence title, Drawable icon) {
+        setClickable(true);
+        setFocusable(true);
+
+        if (mTextAppearance != -1) {
+            setTextAppearance(mTextAppearanceContext, mTextAppearance);
+        }
+
+        setTitle(title);
+        setIcon(icon);
+
+        if (mItemData != null) {
+            final CharSequence contentDescription = mItemData.getContentDescription();
+            if (TextUtils.isEmpty(contentDescription)) {
+                setContentDescription(title);
+            } else {
+                setContentDescription(contentDescription);
+            }
+            setTooltipText(mItemData.getTooltipText());
+        }
+    }
+
+    public void initialize(MenuItemImpl itemData, int menuType) {
+        mItemData = itemData;
+
+        initialize(itemData.getTitleForItemView(this), itemData.getIcon());
+
+        setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
+        setEnabled(itemData.isEnabled());
+    }
+
+    public void setItemData(MenuItemImpl data) {
+        mItemData = data;
+    }
+
+    @Override
+    public boolean performClick() {
+        // Let the view's click listener have top priority (the More button relies on this)
+        if (super.performClick()) {
+            return true;
+        }
+        
+        if ((mItemInvoker != null) && (mItemInvoker.invokeItem(mItemData))) {
+            playSoundEffect(SoundEffectConstants.CLICK);
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
+    public void setTitle(CharSequence title) {
+        
+        if (mShortcutCaptionMode) {
+            /*
+             * Don't set the title directly since it will replace the
+             * shortcut+title being shown. Instead, re-set the shortcut caption
+             * mode so the new title is shown.
+             */
+            setCaptionMode(true);
+            
+        } else if (title != null) {
+            setText(title);
+        }
+    }
+    
+    void setCaptionMode(boolean shortcut) {
+        /*
+         * If there is no item model, don't do any of the below (for example,
+         * the 'More' item doesn't have a model)
+         */
+        if (mItemData == null) {
+            return;
+        }
+        
+        mShortcutCaptionMode = shortcut && (mItemData.shouldShowShortcut());
+        
+        CharSequence text = mItemData.getTitleForItemView(this);
+        
+        if (mShortcutCaptionMode) {
+            
+            if (mShortcutCaption == null) {
+                mShortcutCaption = mItemData.getShortcutLabel();
+            }
+
+            text = mShortcutCaption;
+        }
+        
+        setText(text);
+    }
+    
+    public void setIcon(Drawable icon) {
+        mIcon = icon;
+        
+        if (icon != null) {
+            
+            /* Set the bounds of the icon since setCompoundDrawables needs it. */
+            icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+            
+            // Set the compound drawables
+            setCompoundDrawables(null, icon, null, null);
+            
+            // When there is an icon, make sure the text is at the bottom
+            setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+
+            /*
+             * Request a layout to reposition the icon. The positioning of icon
+             * depends on this TextView's line bounds, which is only available
+             * after a layout.
+             */  
+            requestLayout();
+        } else {
+            setCompoundDrawables(null, null, null, null);
+            
+            // When there is no icon, make sure the text is centered vertically
+            setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
+        }
+    }
+
+    public void setItemInvoker(ItemInvoker itemInvoker) {
+        mItemInvoker = itemInvoker;
+    }
+    
+    @ViewDebug.CapturedViewProperty(retrieveReturn = true)
+    public MenuItemImpl getItemData() {
+        return mItemData;
+    }
+
+    @Override
+    public void setVisibility(int v) {
+        super.setVisibility(v);
+        
+        if (mIconMenuView != null) {
+            // On visibility change, mark the IconMenuView to refresh itself eventually
+            mIconMenuView.markStaleChildren();
+        }
+    }
+    
+    void setIconMenuView(IconMenuView iconMenuView) {
+        mIconMenuView = iconMenuView;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        if (mItemData != null && mIcon != null) {
+            // When disabled, the not-focused state and the pressed state should
+            // drop alpha on the icon
+            final boolean isInAlphaState = !mItemData.isEnabled() && (isPressed() || !isFocused());
+            mIcon.setAlpha(isInAlphaState ? (int) (mDisabledAlpha * NO_ALPHA) : NO_ALPHA);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        
+        positionIcon();
+    }
+
+    @Override
+    protected void onTextChanged(CharSequence text, int start, int before, int after) {
+        super.onTextChanged(text, start, before, after);
+
+        // our layout params depend on the length of the text
+        setLayoutParams(getTextAppropriateLayoutParams());
+    }
+
+    /**
+     * @return layout params appropriate for this view.  If layout params already exist, it will
+     *         augment them to be appropriate to the current text size.
+     */
+    IconMenuView.LayoutParams getTextAppropriateLayoutParams() {
+        IconMenuView.LayoutParams lp = (IconMenuView.LayoutParams) getLayoutParams();
+        if (lp == null) {
+            // Default layout parameters
+            lp = new IconMenuView.LayoutParams(
+                    IconMenuView.LayoutParams.MATCH_PARENT, IconMenuView.LayoutParams.MATCH_PARENT);
+        }
+
+        // Set the desired width of item
+        lp.desiredWidth = (int) Layout.getDesiredWidth(getText(), 0, getText().length(),
+                getPaint(), getTextDirectionHeuristic());
+
+        return lp;
+    }
+
+    /**
+     * Positions the icon vertically (horizontal centering is taken care of by
+     * the TextView's gravity).
+     */
+    private void positionIcon() {
+        
+        if (mIcon == null) {
+            return;
+        }
+        
+        // We reuse the output rectangle as a temp rect
+        Rect tmpRect = mPositionIconOutput;
+        getLineBounds(0, tmpRect);
+        mPositionIconAvailable.set(0, 0, getWidth(), tmpRect.top);
+        final int layoutDirection = getLayoutDirection();
+        Gravity.apply(Gravity.CENTER_VERTICAL | Gravity.START, mIcon.getIntrinsicWidth(), mIcon
+                .getIntrinsicHeight(), mPositionIconAvailable, mPositionIconOutput,
+                layoutDirection);
+        mIcon.setBounds(mPositionIconOutput);
+    }
+
+    public void setCheckable(boolean checkable) {
+    }
+
+    public void setChecked(boolean checked) {
+    }
+
+    public void setShortcut(boolean showShortcut, char shortcutKey) {
+        
+        if (mShortcutCaptionMode) {
+            /*
+             * Shortcut has changed and we're showing it right now, need to
+             * update (clear the old one first).
+             */
+            mShortcutCaption = null;
+            setCaptionMode(true);
+        }
+    }
+
+    public boolean prefersCondensedTitle() {
+        return true;
+    }
+
+    public boolean showsIcon() {
+        return true;
+    }
+
+}
diff --git a/com/android/internal/view/menu/IconMenuPresenter.java b/com/android/internal/view/menu/IconMenuPresenter.java
new file mode 100644
index 0000000..5223a7b
--- /dev/null
+++ b/com/android/internal/view/menu/IconMenuPresenter.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2011 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.internal.view.menu;
+
+import com.android.internal.view.menu.MenuView.ItemView;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * MenuPresenter for the classic "six-pack" icon menu.
+ */
+public class IconMenuPresenter extends BaseMenuPresenter {
+    private IconMenuItemView mMoreView;
+    private int mMaxItems = -1;
+
+    int mOpenSubMenuId;
+    SubMenuPresenterCallback mSubMenuPresenterCallback = new SubMenuPresenterCallback();
+    MenuDialogHelper mOpenSubMenu;
+
+    private static final String VIEWS_TAG = "android:menu:icon";
+    private static final String OPEN_SUBMENU_KEY = "android:menu:icon:submenu";
+
+    public IconMenuPresenter(Context context) {
+        super(new ContextThemeWrapper(context, com.android.internal.R.style.Theme_IconMenu),
+                com.android.internal.R.layout.icon_menu_layout,
+                com.android.internal.R.layout.icon_menu_item_layout);
+    }
+
+    @Override
+    public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+        super.initForMenu(context, menu);
+        mMaxItems = -1;
+    }
+
+    @Override
+    public void bindItemView(MenuItemImpl item, ItemView itemView) {
+        final IconMenuItemView view = (IconMenuItemView) itemView;
+        view.setItemData(item);
+
+        view.initialize(item.getTitleForItemView(view), item.getIcon());
+
+        view.setVisibility(item.isVisible() ? View.VISIBLE : View.GONE);
+        view.setEnabled(view.isEnabled());
+        view.setLayoutParams(view.getTextAppropriateLayoutParams());
+    }
+
+    @Override
+    public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
+        final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems();
+        boolean fits = (itemsToShow.size() == mMaxItems && childIndex < mMaxItems) ||
+                childIndex < mMaxItems - 1;
+        return fits && !item.isActionButton();
+    }
+
+    @Override
+    protected void addItemView(View itemView, int childIndex) {
+        final IconMenuItemView v = (IconMenuItemView) itemView;
+        final IconMenuView parent = (IconMenuView) mMenuView;
+
+        v.setIconMenuView(parent);
+        v.setItemInvoker(parent);
+        v.setBackgroundDrawable(parent.getItemBackgroundDrawable());
+        super.addItemView(itemView, childIndex);
+    }
+
+    @Override
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+        if (!subMenu.hasVisibleItems()) return false;
+
+        // The window manager will give us a token.
+        MenuDialogHelper helper = new MenuDialogHelper(subMenu);
+        helper.setPresenterCallback(mSubMenuPresenterCallback);
+        helper.show(null);
+        mOpenSubMenu = helper;
+        mOpenSubMenuId = subMenu.getItem().getItemId();
+        super.onSubMenuSelected(subMenu);
+        return true;
+    }
+
+    @Override
+    public void updateMenuView(boolean cleared) {
+        final IconMenuView menuView = (IconMenuView) mMenuView;
+        if (mMaxItems < 0) mMaxItems = menuView.getMaxItems();
+        final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems();
+        final boolean needsMore = itemsToShow.size() > mMaxItems;
+        super.updateMenuView(cleared);
+
+        if (needsMore && (mMoreView == null || mMoreView.getParent() != menuView)) {
+            if (mMoreView == null) {
+                mMoreView = menuView.createMoreItemView();
+                mMoreView.setBackgroundDrawable(menuView.getItemBackgroundDrawable());
+            }
+            menuView.addView(mMoreView);
+        } else if (!needsMore && mMoreView != null) {
+            menuView.removeView(mMoreView);
+        }
+
+        menuView.setNumActualItemsShown(needsMore ? mMaxItems - 1 : itemsToShow.size());
+    }
+
+    @Override
+    protected boolean filterLeftoverView(ViewGroup parent, int childIndex) {
+        if (parent.getChildAt(childIndex) != mMoreView) {
+            return super.filterLeftoverView(parent, childIndex);
+        }
+        return false;
+    }
+
+    public int getNumActualItemsShown() {
+        return ((IconMenuView) mMenuView).getNumActualItemsShown();
+    }
+
+    public void saveHierarchyState(Bundle outState) {
+        SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>();
+        if (mMenuView != null) {
+            ((View) mMenuView).saveHierarchyState(viewStates);
+        }
+        outState.putSparseParcelableArray(VIEWS_TAG, viewStates);
+    }
+
+    public void restoreHierarchyState(Bundle inState) {
+        SparseArray<Parcelable> viewStates = inState.getSparseParcelableArray(VIEWS_TAG);
+        if (viewStates != null) {
+            ((View) mMenuView).restoreHierarchyState(viewStates);
+        }
+        int subMenuId = inState.getInt(OPEN_SUBMENU_KEY, 0);
+        if (subMenuId > 0 && mMenu != null) {
+            MenuItem item = mMenu.findItem(subMenuId);
+            if (item != null) {
+                onSubMenuSelected((SubMenuBuilder) item.getSubMenu());
+            }
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        if (mMenuView == null) {
+            return null;
+        }
+
+        Bundle state = new Bundle();
+        saveHierarchyState(state);
+        if (mOpenSubMenuId > 0) {
+            state.putInt(OPEN_SUBMENU_KEY, mOpenSubMenuId);
+        }
+        return state;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        restoreHierarchyState((Bundle) state);
+    }
+
+    class SubMenuPresenterCallback implements MenuPresenter.Callback {
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+            mOpenSubMenuId = 0;
+            if (mOpenSubMenu != null) {
+                mOpenSubMenu.dismiss();
+                mOpenSubMenu = null;
+            }
+        }
+
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            if (subMenu != null) {
+                mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
+            }
+            return false;
+        }
+
+    }
+}
diff --git a/com/android/internal/view/menu/IconMenuView.java b/com/android/internal/view/menu/IconMenuView.java
new file mode 100644
index 0000000..dab43eb
--- /dev/null
+++ b/com/android/internal/view/menu/IconMenuView.java
@@ -0,0 +1,761 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import com.android.internal.view.menu.MenuBuilder.ItemInvoker;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+
+import java.util.ArrayList;
+
+/**
+ * The icon menu view is an icon-based menu usually with a subset of all the menu items.
+ * It is opened as the default menu, and shows either the first five or all six of the menu items
+ * with text and icon.  In the situation of there being more than six items, the first five items
+ * will be accompanied with a 'More' button that opens an {@link ExpandedMenuView} which lists
+ * all the menu items. 
+ * 
+ * @attr ref android.R.styleable#IconMenuView_rowHeight
+ * @attr ref android.R.styleable#IconMenuView_maxRows
+ * @attr ref android.R.styleable#IconMenuView_maxItemsPerRow
+ * 
+ * @hide
+ */
+public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuView, Runnable {
+    private static final int ITEM_CAPTION_CYCLE_DELAY = 1000;
+
+    private MenuBuilder mMenu;
+    
+    /** Height of each row */
+    private int mRowHeight;
+    /** Maximum number of rows to be shown */ 
+    private int mMaxRows;
+    /** Maximum number of items to show in the icon menu. */
+    private int mMaxItems;
+    /** Maximum number of items per row */
+    private int mMaxItemsPerRow;
+    /** Actual number of items (the 'More' view does not count as an item) shown */
+    private int mNumActualItemsShown;
+    
+    /** Divider that is drawn between all rows */
+    private Drawable mHorizontalDivider;
+    /** Height of the horizontal divider */
+    private int mHorizontalDividerHeight;
+    /** Set of horizontal divider positions where the horizontal divider will be drawn */
+    private ArrayList<Rect> mHorizontalDividerRects;
+
+    /** Divider that is drawn between all columns */
+    private Drawable mVerticalDivider;
+    /** Width of the vertical divider */
+    private int mVerticalDividerWidth;
+    /** Set of vertical divider positions where the vertical divider will be drawn */
+    private ArrayList<Rect> mVerticalDividerRects;
+    
+    /** Icon for the 'More' button */
+    private Drawable mMoreIcon;
+
+    /** Background of each item (should contain the selected and focused states) */
+    private Drawable mItemBackground;
+
+    /** Default animations for this menu */
+    private int mAnimations;
+    
+    /**
+     * Whether this IconMenuView has stale children and needs to update them.
+     * Set true by {@link #markStaleChildren()} and reset to false by
+     * {@link #onMeasure(int, int)}
+     */
+    private boolean mHasStaleChildren;
+
+    /**
+     * Longpress on MENU (while this is shown) switches to shortcut caption
+     * mode. When the user releases the longpress, we do not want to pass the
+     * key-up event up since that will dismiss the menu.
+     */
+    private boolean mMenuBeingLongpressed = false;
+
+    /**
+     * While {@link #mMenuBeingLongpressed}, we toggle the children's caption
+     * mode between each's title and its shortcut. This is the last caption mode
+     * we broadcasted to children.
+     */
+    private boolean mLastChildrenCaptionMode;
+
+    /**
+     * The layout to use for menu items. Each index is the row number (0 is the
+     * top-most). Each value contains the number of items in that row.
+     * <p>
+     * The length of this array should not be used to get the number of rows in
+     * the current layout, instead use {@link #mLayoutNumRows}.
+     */
+    private int[] mLayout;
+
+    /**
+     * The number of rows in the current layout. 
+     */
+    private int mLayoutNumRows;
+    
+    /**
+     * Instantiates the IconMenuView that is linked with the provided MenuBuilder.
+     */
+    public IconMenuView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = 
+            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0);
+        mRowHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.IconMenuView_rowHeight, 64);
+        mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2);
+        mMaxItems = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItems, 6);
+        mMaxItemsPerRow = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3);
+        mMoreIcon = a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon);
+        a.recycle();
+        
+        a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuView, 0, 0);
+        mItemBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
+        mHorizontalDivider = a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
+        mHorizontalDividerRects = new ArrayList<Rect>();
+        mVerticalDivider =  a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
+        mVerticalDividerRects = new ArrayList<Rect>();
+        mAnimations = a.getResourceId(com.android.internal.R.styleable.MenuView_windowAnimationStyle, 0);
+        a.recycle();
+        
+        if (mHorizontalDivider != null) {
+            mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight();
+            // Make sure to have some height for the divider
+            if (mHorizontalDividerHeight == -1) mHorizontalDividerHeight = 1;
+        }
+        
+        if (mVerticalDivider != null) {
+            mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth();
+            // Make sure to have some width for the divider
+            if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1;
+        }
+        
+        mLayout = new int[mMaxRows];
+        
+        // This view will be drawing the dividers        
+        setWillNotDraw(false);
+        
+        // This is so we'll receive the MENU key in touch mode
+        setFocusableInTouchMode(true);
+        // This is so our children can still be arrow-key focused
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+    }
+
+    int getMaxItems() {
+        return mMaxItems;
+    }
+
+    /**
+     * Figures out the layout for the menu items.
+     * 
+     * @param width The available width for the icon menu.
+     */
+    private void layoutItems(int width) {
+        int numItems = getChildCount();
+        if (numItems == 0) {
+            mLayoutNumRows = 0;
+            return;
+        }
+        
+        // Start with the least possible number of rows
+        int curNumRows =
+                Math.min((int) Math.ceil(numItems / (float) mMaxItemsPerRow), mMaxRows);
+        
+        /*
+         * Increase the number of rows until we find a configuration that fits
+         * all of the items' titles. Worst case, we use mMaxRows.
+         */
+        for (; curNumRows <= mMaxRows; curNumRows++) {
+            layoutItemsUsingGravity(curNumRows, numItems);
+            
+            if (curNumRows >= numItems) {
+                // Can't have more rows than items
+                break;
+            }
+            
+            if (doItemsFit()) {
+                // All the items fit, so this is a good configuration
+                break;
+            }
+        }
+    }
+
+    /**
+     * Figures out the layout for the menu items by equally distributing, and
+     * adding any excess items equally to lower rows.
+     * 
+     * @param numRows The total number of rows for the menu view
+     * @param numItems The total number of items (across all rows) contained in
+     *            the menu view
+     * @return int[] Where the value of index i contains the number of items for row i
+     */
+    private void layoutItemsUsingGravity(int numRows, int numItems) {
+        int numBaseItemsPerRow = numItems / numRows;
+        int numLeftoverItems = numItems % numRows;
+        /**
+         * The bottom rows will each get a leftover item. Rows (indexed at 0)
+         * that are >= this get a leftover item. Note: if there are 0 leftover
+         * items, no rows will get them since this value will be greater than
+         * the last row.
+         */
+        int rowsThatGetALeftoverItem = numRows - numLeftoverItems;
+        
+        int[] layout = mLayout;
+        for (int i = 0; i < numRows; i++) {
+            layout[i] = numBaseItemsPerRow;
+
+            // Fill the bottom rows with a leftover item each
+            if (i >= rowsThatGetALeftoverItem) {
+                layout[i]++;
+            }
+        }
+        
+        mLayoutNumRows = numRows;
+    }
+
+    /**
+     * Checks whether each item's title is fully visible using the current
+     * layout.
+     * 
+     * @return True if the items fit (each item's text is fully visible), false
+     *         otherwise.
+     */
+    private boolean doItemsFit() {
+        int itemPos = 0;
+        
+        int[] layout = mLayout;
+        int numRows = mLayoutNumRows;
+        for (int row = 0; row < numRows; row++) {
+            int numItemsOnRow = layout[row];
+
+            /*
+             * If there is only one item on this row, increasing the
+             * number of rows won't help.
+             */ 
+            if (numItemsOnRow == 1) {
+                itemPos++;
+                continue;
+            }
+            
+            for (int itemsOnRowCounter = numItemsOnRow; itemsOnRowCounter > 0;
+                    itemsOnRowCounter--) {
+                View child = getChildAt(itemPos++);
+                LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp.maxNumItemsOnRow < numItemsOnRow) {
+                    return false;
+                }
+            }
+        }
+        
+        return true;
+    }
+
+    Drawable getItemBackgroundDrawable() {
+        return mItemBackground.getConstantState().newDrawable(getContext().getResources());
+    }
+
+    /**
+     * Creates the item view for the 'More' button which is used to switch to
+     * the expanded menu view. This button is a special case since it does not
+     * have a MenuItemData backing it.
+     * @return The IconMenuItemView for the 'More' button
+     */
+    IconMenuItemView createMoreItemView() {
+        Context context = getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        
+        final IconMenuItemView itemView = (IconMenuItemView) inflater.inflate(
+                com.android.internal.R.layout.icon_menu_item_layout, null);
+        
+        Resources r = context.getResources();
+        itemView.initialize(r.getText(com.android.internal.R.string.more_item_label), mMoreIcon);
+        
+        // Set up a click listener on the view since there will be no invocation sequence
+        // due to the lack of a MenuItemData this view
+        itemView.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                // Switches the menu to expanded mode. Requires support from
+                // the menu's active callback.
+                mMenu.changeMenuMode();
+            }
+        });
+        
+        return itemView;
+    }
+    
+    
+    public void initialize(MenuBuilder menu) {
+        mMenu = menu;
+    }
+
+    /**
+     * The positioning algorithm that gets called from onMeasure.  It
+     * just computes positions for each child, and then stores them in the child's layout params.
+     * @param menuWidth The width of this menu to assume for positioning
+     * @param menuHeight The height of this menu to assume for positioning
+     */
+    private void positionChildren(int menuWidth, int menuHeight) {
+        // Clear the containers for the positions where the dividers should be drawn
+        if (mHorizontalDivider != null) mHorizontalDividerRects.clear();
+        if (mVerticalDivider != null) mVerticalDividerRects.clear();
+
+        // Get the minimum number of rows needed
+        final int numRows = mLayoutNumRows;
+        final int numRowsMinus1 = numRows - 1;
+        final int numItemsForRow[] = mLayout;
+        
+        // The item position across all rows
+        int itemPos = 0;
+        View child;
+        IconMenuView.LayoutParams childLayoutParams = null; 
+
+        // Use float for this to get precise positions (uniform item widths
+        // instead of last one taking any slack), and then convert to ints at last opportunity
+        float itemLeft;
+        float itemTop = 0;
+        // Since each row can have a different number of items, this will be computed per row
+        float itemWidth;
+        // Subtract the space needed for the horizontal dividers
+        final float itemHeight = (menuHeight - mHorizontalDividerHeight * (numRows - 1))
+                / (float)numRows;
+        
+        for (int row = 0; row < numRows; row++) {
+            // Start at the left
+            itemLeft = 0;
+            
+            // Subtract the space needed for the vertical dividers, and divide by the number of items
+            itemWidth = (menuWidth - mVerticalDividerWidth * (numItemsForRow[row] - 1))
+                    / (float)numItemsForRow[row];
+            
+            for (int itemPosOnRow = 0; itemPosOnRow < numItemsForRow[row]; itemPosOnRow++) {
+                // Tell the child to be exactly this size
+                child = getChildAt(itemPos);
+                child.measure(MeasureSpec.makeMeasureSpec((int) itemWidth, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec((int) itemHeight, MeasureSpec.EXACTLY));
+                
+                // Remember the child's position for layout
+                childLayoutParams = (IconMenuView.LayoutParams) child.getLayoutParams();
+                childLayoutParams.left = (int) itemLeft;
+                childLayoutParams.right = (int) (itemLeft + itemWidth);
+                childLayoutParams.top = (int) itemTop;
+                childLayoutParams.bottom = (int) (itemTop + itemHeight); 
+                
+                // Increment by item width
+                itemLeft += itemWidth;
+                itemPos++;
+
+                // Add a vertical divider to draw
+                if (mVerticalDivider != null) {
+                    mVerticalDividerRects.add(new Rect((int) itemLeft,
+                            (int) itemTop, (int) (itemLeft + mVerticalDividerWidth),
+                            (int) (itemTop + itemHeight)));
+                }
+
+                // Increment by divider width (even if we're not computing
+                // dividers, since we need to leave room for them when
+                // calculating item positions)
+                itemLeft += mVerticalDividerWidth;
+            }
+            
+            // Last child on each row should extend to very right edge
+            if (childLayoutParams != null) {
+                childLayoutParams.right = menuWidth;
+            }
+            
+            itemTop += itemHeight;
+
+            // Add a horizontal divider to draw
+            if ((mHorizontalDivider != null) && (row < numRowsMinus1)) {
+                mHorizontalDividerRects.add(new Rect(0, (int) itemTop, menuWidth,
+                        (int) (itemTop + mHorizontalDividerHeight)));
+
+                itemTop += mHorizontalDividerHeight;
+            }
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int measuredWidth = resolveSize(Integer.MAX_VALUE, widthMeasureSpec);
+        calculateItemFittingMetadata(measuredWidth);
+        layoutItems(measuredWidth);
+        
+        // Get the desired height of the icon menu view (last row of items does
+        // not have a divider below)
+        final int layoutNumRows = mLayoutNumRows;
+        final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) *
+                layoutNumRows - mHorizontalDividerHeight;
+        
+        // Maximum possible width and desired height
+        setMeasuredDimension(measuredWidth,
+                resolveSize(desiredHeight, heightMeasureSpec));
+
+        // Position the children
+        if (layoutNumRows > 0) {
+            positionChildren(getMeasuredWidth(), getMeasuredHeight());
+        }
+    }
+
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        View child;
+        IconMenuView.LayoutParams childLayoutParams;
+        
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            child = getChildAt(i);
+            childLayoutParams = (IconMenuView.LayoutParams)child
+                    .getLayoutParams();
+
+            // Layout children according to positions set during the measure
+            child.layout(childLayoutParams.left, childLayoutParams.top, childLayoutParams.right,
+                    childLayoutParams.bottom);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        Drawable drawable = mHorizontalDivider;
+        if (drawable != null) {
+            // If we have a horizontal divider to draw, draw it at the remembered positions
+            final ArrayList<Rect> rects = mHorizontalDividerRects;
+            for (int i = rects.size() - 1; i >= 0; i--) {
+                drawable.setBounds(rects.get(i));
+                drawable.draw(canvas);
+            }
+        }
+
+        drawable = mVerticalDivider;
+        if (drawable != null) {
+            // If we have a vertical divider to draw, draw it at the remembered positions
+            final ArrayList<Rect> rects = mVerticalDividerRects;
+            for (int i = rects.size() - 1; i >= 0; i--) {
+                drawable.setBounds(rects.get(i));
+                drawable.draw(canvas);
+            }
+        }
+    }
+
+    public boolean invokeItem(MenuItemImpl item) {
+        return mMenu.performItemAction(item, 0);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new IconMenuView.LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        // Override to allow type-checking of LayoutParams. 
+        return p instanceof IconMenuView.LayoutParams;
+    }
+
+    /**
+     * Marks as having stale children.
+     */
+    void markStaleChildren() {
+        if (!mHasStaleChildren) {
+            mHasStaleChildren = true;
+            requestLayout();
+        }
+    }
+    
+    /**
+     * @return The number of actual items shown (those that are backed by an
+     *         {@link MenuView.ItemView} implementation--eg: excludes More
+     *         item).
+     */
+    int getNumActualItemsShown() {
+        return mNumActualItemsShown;
+    }
+    
+    void setNumActualItemsShown(int count) {
+        mNumActualItemsShown = count;
+    }
+    
+    public int getWindowAnimations() {
+        return mAnimations;
+    }
+
+    /**
+     * Returns the number of items per row.
+     * <p>
+     * This should only be used for testing.
+     * 
+     * @return The length of the array is the number of rows. A value at a
+     *         position is the number of items in that row.
+     * @hide
+     */
+    public int[] getLayout() {
+        return mLayout;
+    }
+    
+    /**
+     * Returns the number of rows in the layout.
+     * <p>
+     * This should only be used for testing.
+     * 
+     * @return The length of the array is the number of rows. A value at a
+     *         position is the number of items in that row.
+     * @hide
+     */
+    public int getLayoutNumRows() {
+        return mLayoutNumRows;
+    }
+    
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+
+        if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+                removeCallbacks(this);
+                postDelayed(this, ViewConfiguration.getLongPressTimeout());
+            } else if (event.getAction() == KeyEvent.ACTION_UP) {
+                
+                if (mMenuBeingLongpressed) {
+                    // It was in cycle mode, so reset it (will also remove us
+                    // from being called back)
+                    setCycleShortcutCaptionMode(false);
+                    return true;
+                    
+                } else {
+                    // Just remove us from being called back
+                    removeCallbacks(this);
+                    // Fall through to normal processing too
+                }
+            }
+        }
+        
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        
+        requestFocus();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        setCycleShortcutCaptionMode(false);
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+
+        if (!hasWindowFocus) {
+            setCycleShortcutCaptionMode(false);
+        }
+
+        super.onWindowFocusChanged(hasWindowFocus);
+    }
+
+    /**
+     * Sets the shortcut caption mode for IconMenuView. This mode will
+     * continuously cycle between a child's shortcut and its title.
+     * 
+     * @param cycleShortcutAndNormal Whether to go into cycling shortcut mode,
+     *        or to go back to normal.
+     */
+    private void setCycleShortcutCaptionMode(boolean cycleShortcutAndNormal) {
+
+        if (!cycleShortcutAndNormal) {
+            /*
+             * We're setting back to title, so remove any callbacks for setting
+             * to shortcut
+             */
+            removeCallbacks(this);
+            setChildrenCaptionMode(false);
+            mMenuBeingLongpressed = false;
+            
+        } else {
+            
+            // Set it the first time (the cycle will be started in run()).
+            setChildrenCaptionMode(true);
+        }
+        
+    }
+
+    /**
+     * When this method is invoked if the menu is currently not being
+     * longpressed, it means that the longpress has just been reached (so we set
+     * longpress flag, and start cycling). If it is being longpressed, we cycle
+     * to the next mode.
+     */
+    public void run() {
+        
+        if (mMenuBeingLongpressed) {
+
+            // Cycle to other caption mode on the children
+            setChildrenCaptionMode(!mLastChildrenCaptionMode);
+
+        } else {
+            
+            // Switch ourselves to continuously cycle the items captions
+            mMenuBeingLongpressed = true;
+            setCycleShortcutCaptionMode(true);
+        }
+
+        // We should run again soon to cycle to the other caption mode
+        postDelayed(this, ITEM_CAPTION_CYCLE_DELAY);
+    }
+
+    /**
+     * Iterates children and sets the desired shortcut mode. Only
+     * {@link #setCycleShortcutCaptionMode(boolean)} and {@link #run()} should call
+     * this.
+     * 
+     * @param shortcut Whether to show shortcut or the title.
+     */
+    private void setChildrenCaptionMode(boolean shortcut) {
+        
+        // Set the last caption mode pushed to children
+        mLastChildrenCaptionMode = shortcut;
+        
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            ((IconMenuItemView) getChildAt(i)).setCaptionMode(shortcut);
+        }
+    }
+
+    /**
+     * For each item, calculates the most dense row that fully shows the item's
+     * title.
+     * 
+     * @param width The available width of the icon menu.
+     */
+    private void calculateItemFittingMetadata(int width) {
+        int maxNumItemsPerRow = mMaxItemsPerRow;
+        int numItems = getChildCount();
+        for (int i = 0; i < numItems; i++) {
+            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+            // Start with 1, since that case does not get covered in the loop below
+            lp.maxNumItemsOnRow = 1;
+            for (int curNumItemsPerRow = maxNumItemsPerRow; curNumItemsPerRow > 0;
+                    curNumItemsPerRow--) {
+                // Check whether this item can fit into a row containing curNumItemsPerRow
+                if (lp.desiredWidth < width / curNumItemsPerRow) {
+                    // It can, mark this value as the most dense row it can fit into
+                    lp.maxNumItemsOnRow = curNumItemsPerRow;
+                    break;
+                }
+            }
+        }
+    }
+    
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        
+        View focusedView = getFocusedChild();
+        
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            if (getChildAt(i) == focusedView) {
+                return new SavedState(superState, i);
+            }
+        }
+        
+        return new SavedState(superState, -1);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        if (ss.focusedPosition >= getChildCount()) {
+            return;
+        }
+        
+        View v = getChildAt(ss.focusedPosition);
+        if (v != null) {
+            v.requestFocus();
+        }
+    }
+
+    private static class SavedState extends BaseSavedState {
+        int focusedPosition;
+
+        /**
+         * Constructor called from {@link IconMenuView#onSaveInstanceState()}
+         */
+        public SavedState(Parcelable superState, int focusedPosition) {
+            super(superState);
+            this.focusedPosition = focusedPosition;
+        }
+        
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            focusedPosition = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(focusedPosition);
+        }
+        
+        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+        
+    }
+    
+    /**
+     * Layout parameters specific to IconMenuView (stores the left, top, right, bottom from the
+     * measure pass). 
+     */
+    public static class LayoutParams extends ViewGroup.MarginLayoutParams
+    {
+        int left, top, right, bottom;
+        int desiredWidth;
+        int maxNumItemsOnRow;
+        
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+    }
+}
diff --git a/com/android/internal/view/menu/ListMenuItemView.java b/com/android/internal/view/menu/ListMenuItemView.java
new file mode 100644
index 0000000..f76c724
--- /dev/null
+++ b/com/android/internal/view/menu/ListMenuItemView.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.AbsListView;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+/**
+ * The item view for each item in the ListView-based MenuViews.
+ */
+public class ListMenuItemView extends LinearLayout
+        implements MenuView.ItemView, AbsListView.SelectionBoundsAdjuster {
+    private static final String TAG = "ListMenuItemView";
+    private MenuItemImpl mItemData;
+
+    private ImageView mIconView;
+    private RadioButton mRadioButton;
+    private TextView mTitleView;
+    private CheckBox mCheckBox;
+    private TextView mShortcutView;
+    private ImageView mSubMenuArrowView;
+    private ImageView mGroupDivider;
+
+    private Drawable mBackground;
+    private int mTextAppearance;
+    private Context mTextAppearanceContext;
+    private boolean mPreserveIconSpacing;
+    private Drawable mSubMenuArrow;
+    private boolean mHasListDivider;
+
+    private int mMenuType;
+
+    private LayoutInflater mInflater;
+
+    private boolean mForceShowIcon;
+
+    public ListMenuItemView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.MenuView, defStyleAttr, defStyleRes);
+
+        mBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
+        mTextAppearance = a.getResourceId(com.android.internal.R.styleable.
+                                          MenuView_itemTextAppearance, -1);
+        mPreserveIconSpacing = a.getBoolean(
+                com.android.internal.R.styleable.MenuView_preserveIconSpacing, false);
+        mTextAppearanceContext = context;
+        mSubMenuArrow = a.getDrawable(com.android.internal.R.styleable.MenuView_subMenuArrow);
+
+        final TypedArray b = context.getTheme()
+                .obtainStyledAttributes(null, new int[] { com.android.internal.R.attr.divider },
+                        com.android.internal.R.attr.dropDownListViewStyle, 0);
+        mHasListDivider = b.hasValue(0);
+
+        a.recycle();
+        b.recycle();
+    }
+
+    public ListMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ListMenuItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.listMenuViewStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        setBackgroundDrawable(mBackground);
+
+        mTitleView = findViewById(com.android.internal.R.id.title);
+        if (mTextAppearance != -1) {
+            mTitleView.setTextAppearance(mTextAppearanceContext,
+                                         mTextAppearance);
+        }
+
+        mShortcutView = findViewById(com.android.internal.R.id.shortcut);
+        mSubMenuArrowView = findViewById(com.android.internal.R.id.submenuarrow);
+        if (mSubMenuArrowView != null) {
+            mSubMenuArrowView.setImageDrawable(mSubMenuArrow);
+        }
+        mGroupDivider = findViewById(com.android.internal.R.id.group_divider);
+    }
+
+    public void initialize(MenuItemImpl itemData, int menuType) {
+        mItemData = itemData;
+        mMenuType = menuType;
+
+        setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
+
+        setTitle(itemData.getTitleForItemView(this));
+        setCheckable(itemData.isCheckable());
+        setShortcut(itemData.shouldShowShortcut(), itemData.getShortcut());
+        setIcon(itemData.getIcon());
+        setEnabled(itemData.isEnabled());
+        setSubMenuArrowVisible(itemData.hasSubMenu());
+        setContentDescription(itemData.getContentDescription());
+    }
+
+    public void setForceShowIcon(boolean forceShow) {
+        mPreserveIconSpacing = mForceShowIcon = forceShow;
+    }
+
+    public void setTitle(CharSequence title) {
+        if (title != null) {
+            mTitleView.setText(title);
+
+            if (mTitleView.getVisibility() != VISIBLE) mTitleView.setVisibility(VISIBLE);
+        } else {
+            if (mTitleView.getVisibility() != GONE) mTitleView.setVisibility(GONE);
+        }
+    }
+
+    public MenuItemImpl getItemData() {
+        return mItemData;
+    }
+
+    public void setCheckable(boolean checkable) {
+        if (!checkable && mRadioButton == null && mCheckBox == null) {
+            return;
+        }
+
+        // Depending on whether its exclusive check or not, the checkbox or
+        // radio button will be the one in use (and the other will be otherCompoundButton)
+        final CompoundButton compoundButton;
+        final CompoundButton otherCompoundButton;
+
+        if (mItemData.isExclusiveCheckable()) {
+            if (mRadioButton == null) {
+                insertRadioButton();
+            }
+            compoundButton = mRadioButton;
+            otherCompoundButton = mCheckBox;
+        } else {
+            if (mCheckBox == null) {
+                insertCheckBox();
+            }
+            compoundButton = mCheckBox;
+            otherCompoundButton = mRadioButton;
+        }
+
+        if (checkable) {
+            compoundButton.setChecked(mItemData.isChecked());
+
+            final int newVisibility = checkable ? VISIBLE : GONE;
+            if (compoundButton.getVisibility() != newVisibility) {
+                compoundButton.setVisibility(newVisibility);
+            }
+
+            // Make sure the other compound button isn't visible
+            if (otherCompoundButton != null && otherCompoundButton.getVisibility() != GONE) {
+                otherCompoundButton.setVisibility(GONE);
+            }
+        } else {
+            if (mCheckBox != null) mCheckBox.setVisibility(GONE);
+            if (mRadioButton != null) mRadioButton.setVisibility(GONE);
+        }
+    }
+
+    public void setChecked(boolean checked) {
+        CompoundButton compoundButton;
+
+        if (mItemData.isExclusiveCheckable()) {
+            if (mRadioButton == null) {
+                insertRadioButton();
+            }
+            compoundButton = mRadioButton;
+        } else {
+            if (mCheckBox == null) {
+                insertCheckBox();
+            }
+            compoundButton = mCheckBox;
+        }
+
+        compoundButton.setChecked(checked);
+    }
+
+    private void setSubMenuArrowVisible(boolean hasSubmenu) {
+        if (mSubMenuArrowView != null) {
+            mSubMenuArrowView.setVisibility(hasSubmenu ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    public void setShortcut(boolean showShortcut, char shortcutKey) {
+        final int newVisibility = (showShortcut && mItemData.shouldShowShortcut())
+                ? VISIBLE : GONE;
+
+        if (newVisibility == VISIBLE) {
+            mShortcutView.setText(mItemData.getShortcutLabel());
+        }
+
+        if (mShortcutView.getVisibility() != newVisibility) {
+            mShortcutView.setVisibility(newVisibility);
+        }
+    }
+
+    public void setIcon(Drawable icon) {
+        final boolean showIcon = mItemData.shouldShowIcon() || mForceShowIcon;
+        if (!showIcon && !mPreserveIconSpacing) {
+            return;
+        }
+
+        if (mIconView == null && icon == null && !mPreserveIconSpacing) {
+            return;
+        }
+
+        if (mIconView == null) {
+            insertIconView();
+        }
+
+        if (icon != null || mPreserveIconSpacing) {
+            mIconView.setImageDrawable(showIcon ? icon : null);
+
+            if (mIconView.getVisibility() != VISIBLE) {
+                mIconView.setVisibility(VISIBLE);
+            }
+        } else {
+            mIconView.setVisibility(GONE);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mIconView != null && mPreserveIconSpacing) {
+            // Enforce minimum icon spacing
+            ViewGroup.LayoutParams lp = getLayoutParams();
+            LayoutParams iconLp = (LayoutParams) mIconView.getLayoutParams();
+            if (lp.height > 0 && iconLp.width <= 0) {
+                iconLp.width = lp.height;
+            }
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    private void insertIconView() {
+        LayoutInflater inflater = getInflater();
+        mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon,
+                this, false);
+        addView(mIconView, 0);
+    }
+
+    private void insertRadioButton() {
+        LayoutInflater inflater = getInflater();
+        mRadioButton =
+                (RadioButton) inflater.inflate(com.android.internal.R.layout.list_menu_item_radio,
+                this, false);
+        addView(mRadioButton);
+    }
+
+    private void insertCheckBox() {
+        LayoutInflater inflater = getInflater();
+        mCheckBox =
+                (CheckBox) inflater.inflate(com.android.internal.R.layout.list_menu_item_checkbox,
+                this, false);
+        addView(mCheckBox);
+    }
+
+    public boolean prefersCondensedTitle() {
+        return false;
+    }
+
+    public boolean showsIcon() {
+        return mForceShowIcon;
+    }
+
+    private LayoutInflater getInflater() {
+        if (mInflater == null) {
+            mInflater = LayoutInflater.from(mContext);
+        }
+        return mInflater;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (mItemData != null && mItemData.hasSubMenu()) {
+            info.setCanOpenPopup(true);
+        }
+    }
+
+    /**
+     * Enable or disable group dividers for this view.
+     */
+    public void setGroupDividerEnabled(boolean groupDividerEnabled) {
+        // If mHasListDivider is true, disabling the groupDivider.
+        // Otherwise, checking enbling it according to groupDividerEnabled flag.
+        mGroupDivider.setVisibility(!mHasListDivider
+                && groupDividerEnabled ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public void adjustListItemSelectionBounds(Rect rect) {
+        if (mGroupDivider.getVisibility() == View.VISIBLE) {
+            // groupDivider is a part of MenuItemListView.
+            // If ListMenuItem with divider enabled is hovered/clicked, divider also gets selected.
+            // Clipping the selector bounds from the top divider portion when divider is enabled,
+            // so that divider does not get selected on hover or click.
+            final LayoutParams lp = (LayoutParams) mGroupDivider.getLayoutParams();
+            rect.top += mGroupDivider.getHeight() + lp.topMargin + lp.bottomMargin;
+        }
+    }
+}
diff --git a/com/android/internal/view/menu/ListMenuPresenter.java b/com/android/internal/view/menu/ListMenuPresenter.java
new file mode 100644
index 0000000..2fff3ba
--- /dev/null
+++ b/com/android/internal/view/menu/ListMenuPresenter.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2011 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.internal.view.menu;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * MenuPresenter for list-style menus.
+ */
+public class ListMenuPresenter implements MenuPresenter, AdapterView.OnItemClickListener {
+    private static final String TAG = "ListMenuPresenter";
+
+    Context mContext;
+    LayoutInflater mInflater;
+    MenuBuilder mMenu;
+
+    ExpandedMenuView mMenuView;
+
+    private int mItemIndexOffset;
+    int mThemeRes;
+    int mItemLayoutRes;
+
+    private Callback mCallback;
+    MenuAdapter mAdapter;
+
+    private int mId;
+
+    public static final String VIEWS_TAG = "android:menu:list";
+
+    /**
+     * Construct a new ListMenuPresenter.
+     * @param context Context to use for theming. This will supersede the context provided
+     *                to initForMenu when this presenter is added.
+     * @param itemLayoutRes Layout resource for individual item views.
+     */
+    public ListMenuPresenter(Context context, int itemLayoutRes) {
+        this(itemLayoutRes, 0);
+        mContext = context;
+        mInflater = LayoutInflater.from(mContext);
+    }
+
+    /**
+     * Construct a new ListMenuPresenter.
+     * @param itemLayoutRes Layout resource for individual item views.
+     * @param themeRes Resource ID of a theme to use for views.
+     */
+    public ListMenuPresenter(int itemLayoutRes, int themeRes) {
+        mItemLayoutRes = itemLayoutRes;
+        mThemeRes = themeRes;
+    }
+
+    @Override
+    public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+        if (mThemeRes != 0) {
+            mContext = new ContextThemeWrapper(context, mThemeRes);
+            mInflater = LayoutInflater.from(mContext);
+        } else if (mContext != null) {
+            mContext = context;
+            if (mInflater == null) {
+                mInflater = LayoutInflater.from(mContext);
+            }
+        }
+        mMenu = menu;
+        if (mAdapter != null) {
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    public MenuView getMenuView(ViewGroup root) {
+        if (mMenuView == null) {
+            mMenuView = (ExpandedMenuView) mInflater.inflate(
+                    com.android.internal.R.layout.expanded_menu_layout, root, false);
+            if (mAdapter == null) {
+                mAdapter = new MenuAdapter();
+            }
+            mMenuView.setAdapter(mAdapter);
+            mMenuView.setOnItemClickListener(this);
+        }
+        return mMenuView;
+    }
+
+    /**
+     * Call this instead of getMenuView if you want to manage your own ListView.
+     * For proper operation, the ListView hosting this adapter should add
+     * this presenter as an OnItemClickListener.
+     *
+     * @return A ListAdapter containing the items in the menu.
+     */
+    public ListAdapter getAdapter() {
+        if (mAdapter == null) {
+            mAdapter = new MenuAdapter();
+        }
+        return mAdapter;
+    }
+
+    @Override
+    public void updateMenuView(boolean cleared) {
+        if (mAdapter != null) mAdapter.notifyDataSetChanged();
+    }
+
+    @Override
+    public void setCallback(Callback cb) {
+        mCallback = cb;
+    }
+
+    @Override
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+        if (!subMenu.hasVisibleItems()) return false;
+
+        // The window manager will give us a token.
+        new MenuDialogHelper(subMenu).show(null);
+        if (mCallback != null) {
+            mCallback.onOpenSubMenu(subMenu);
+        }
+        return true;
+    }
+
+    @Override
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        if (mCallback != null) {
+            mCallback.onCloseMenu(menu, allMenusAreClosing);
+        }
+    }
+
+    int getItemIndexOffset() {
+        return mItemIndexOffset;
+    }
+
+    public void setItemIndexOffset(int offset) {
+        mItemIndexOffset = offset;
+        if (mMenuView != null) {
+            updateMenuView(false);
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        mMenu.performItemAction(mAdapter.getItem(position), this, 0);
+    }
+
+    @Override
+    public boolean flagActionItems() {
+        return false;
+    }
+
+    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+        return false;
+    }
+
+    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+        return false;
+    }
+
+    public void saveHierarchyState(Bundle outState) {
+        SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>();
+        if (mMenuView != null) {
+            ((View) mMenuView).saveHierarchyState(viewStates);
+        }
+        outState.putSparseParcelableArray(VIEWS_TAG, viewStates);
+    }
+
+    public void restoreHierarchyState(Bundle inState) {
+        SparseArray<Parcelable> viewStates = inState.getSparseParcelableArray(VIEWS_TAG);
+        if (viewStates != null) {
+            ((View) mMenuView).restoreHierarchyState(viewStates);
+        }
+    }
+
+    public void setId(int id) {
+        mId = id;
+    }
+
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        if (mMenuView == null) {
+            return null;
+        }
+
+        Bundle state = new Bundle();
+        saveHierarchyState(state);
+        return state;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        restoreHierarchyState((Bundle) state);
+    }
+
+    private class MenuAdapter extends BaseAdapter {
+        private int mExpandedIndex = -1;
+
+        public MenuAdapter() {
+            findExpandedIndex();
+        }
+
+        public int getCount() {
+            ArrayList<MenuItemImpl> items = mMenu.getNonActionItems();
+            int count = items.size() - mItemIndexOffset;
+            if (mExpandedIndex < 0) {
+                return count;
+            }
+            return count - 1;
+        }
+
+        public MenuItemImpl getItem(int position) {
+            ArrayList<MenuItemImpl> items = mMenu.getNonActionItems();
+            position += mItemIndexOffset;
+            if (mExpandedIndex >= 0 && position >= mExpandedIndex) {
+                position++;
+            }
+            return items.get(position);
+        }
+
+        public long getItemId(int position) {
+            // Since a menu item's ID is optional, we'll use the position as an
+            // ID for the item in the AdapterView
+            return position;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(mItemLayoutRes, parent, false);
+            }
+
+            MenuView.ItemView itemView = (MenuView.ItemView) convertView;
+            itemView.initialize(getItem(position), 0);
+            return convertView;
+        }
+
+        void findExpandedIndex() {
+            final MenuItemImpl expandedItem = mMenu.getExpandedItem();
+            if (expandedItem != null) {
+                final ArrayList<MenuItemImpl> items = mMenu.getNonActionItems();
+                final int count = items.size();
+                for (int i = 0; i < count; i++) {
+                    final MenuItemImpl item = items.get(i);
+                    if (item == expandedItem) {
+                        mExpandedIndex = i;
+                        return;
+                    }
+                }
+            }
+            mExpandedIndex = -1;
+        }
+
+        @Override
+        public void notifyDataSetChanged() {
+            findExpandedIndex();
+            super.notifyDataSetChanged();
+        }
+    }
+}
diff --git a/com/android/internal/view/menu/MenuAdapter.java b/com/android/internal/view/menu/MenuAdapter.java
new file mode 100644
index 0000000..2834d39
--- /dev/null
+++ b/com/android/internal/view/menu/MenuAdapter.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 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.internal.view.menu;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import java.util.ArrayList;
+
+public class MenuAdapter extends BaseAdapter {
+    static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout;
+
+    MenuBuilder mAdapterMenu;
+
+    private int mExpandedIndex = -1;
+
+    private boolean mForceShowIcon;
+    private final boolean mOverflowOnly;
+    private final LayoutInflater mInflater;
+
+    public MenuAdapter(MenuBuilder menu, LayoutInflater inflater, boolean overflowOnly) {
+        mOverflowOnly = overflowOnly;
+        mInflater = inflater;
+        mAdapterMenu = menu;
+        findExpandedIndex();
+    }
+
+    public boolean getForceShowIcon() {
+        return mForceShowIcon;
+    }
+
+    public void setForceShowIcon(boolean forceShow) {
+        mForceShowIcon = forceShow;
+    }
+
+    public int getCount() {
+        ArrayList<MenuItemImpl> items = mOverflowOnly ?
+                mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
+        if (mExpandedIndex < 0) {
+            return items.size();
+        }
+        return items.size() - 1;
+    }
+
+    public MenuBuilder getAdapterMenu() {
+        return mAdapterMenu;
+    }
+
+    public MenuItemImpl getItem(int position) {
+        ArrayList<MenuItemImpl> items = mOverflowOnly ?
+                mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
+        if (mExpandedIndex >= 0 && position >= mExpandedIndex) {
+            position++;
+        }
+        return items.get(position);
+    }
+
+    public long getItemId(int position) {
+        // Since a menu item's ID is optional, we'll use the position as an
+        // ID for the item in the AdapterView
+        return position;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = mInflater.inflate(ITEM_LAYOUT, parent, false);
+        }
+
+        final int currGroupId = getItem(position).getGroupId();
+        final int prevGroupId =
+                position - 1 >= 0 ? getItem(position - 1).getGroupId() : currGroupId;
+        // Show a divider if adjacent items are in different groups.
+        ((ListMenuItemView) convertView)
+                .setGroupDividerEnabled(mAdapterMenu.isGroupDividerEnabled()
+                        && (currGroupId != prevGroupId));
+
+        MenuView.ItemView itemView = (MenuView.ItemView) convertView;
+        if (mForceShowIcon) {
+            ((ListMenuItemView) convertView).setForceShowIcon(true);
+        }
+        itemView.initialize(getItem(position), 0);
+        return convertView;
+    }
+
+    void findExpandedIndex() {
+        final MenuItemImpl expandedItem = mAdapterMenu.getExpandedItem();
+        if (expandedItem != null) {
+            final ArrayList<MenuItemImpl> items = mAdapterMenu.getNonActionItems();
+            final int count = items.size();
+            for (int i = 0; i < count; i++) {
+                final MenuItemImpl item = items.get(i);
+                if (item == expandedItem) {
+                    mExpandedIndex = i;
+                    return;
+                }
+            }
+        }
+        mExpandedIndex = -1;
+    }
+
+    @Override
+    public void notifyDataSetChanged() {
+        findExpandedIndex();
+        super.notifyDataSetChanged();
+    }
+}
\ No newline at end of file
diff --git a/com/android/internal/view/menu/MenuBuilder.java b/com/android/internal/view/menu/MenuBuilder.java
new file mode 100644
index 0000000..b53459e
--- /dev/null
+++ b/com/android/internal/view/menu/MenuBuilder.java
@@ -0,0 +1,1323 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.ActionProvider;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Implementation of the {@link android.view.Menu} interface for creating a
+ * standard menu UI.
+ */
+public class MenuBuilder implements Menu {
+    private static final String TAG = "MenuBuilder";
+
+    private static final String PRESENTER_KEY = "android:menu:presenters";
+    private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates";
+    private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview";
+
+    private static final int[]  sCategoryToOrder = new int[] {
+        1, /* No category */
+        4, /* CONTAINER */
+        5, /* SYSTEM */
+        3, /* SECONDARY */
+        2, /* ALTERNATIVE */
+        0, /* SELECTED_ALTERNATIVE */
+    };
+
+    private final Context mContext;
+    private final Resources mResources;
+
+    /**
+     * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
+     * instead of accessing this directly.
+     */
+    private boolean mQwertyMode;
+
+    /**
+     * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
+     * instead of accessing this directly.
+     */ 
+    private boolean mShortcutsVisible;
+    
+    /**
+     * Callback that will receive the various menu-related events generated by
+     * this class. Use getCallback to get a reference to the callback.
+     */
+    private Callback mCallback;
+    
+    /** Contains all of the items for this menu */
+    private ArrayList<MenuItemImpl> mItems;
+
+    /** Contains only the items that are currently visible.  This will be created/refreshed from
+     * {@link #getVisibleItems()} */
+    private ArrayList<MenuItemImpl> mVisibleItems;
+    /**
+     * Whether or not the items (or any one item's shown state) has changed since it was last
+     * fetched from {@link #getVisibleItems()}
+     */ 
+    private boolean mIsVisibleItemsStale;
+    
+    /**
+     * Contains only the items that should appear in the Action Bar, if present.
+     */
+    private ArrayList<MenuItemImpl> mActionItems;
+    /**
+     * Contains items that should NOT appear in the Action Bar, if present.
+     */
+    private ArrayList<MenuItemImpl> mNonActionItems;
+
+    /**
+     * Whether or not the items (or any one item's action state) has changed since it was
+     * last fetched.
+     */
+    private boolean mIsActionItemsStale;
+
+    /**
+     * Default value for how added items should show in the action list.
+     */
+    private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
+
+    /**
+     * Current use case is Context Menus: As Views populate the context menu, each one has
+     * extra information that should be passed along.  This is the current menu info that
+     * should be set on all items added to this menu.
+     */
+    private ContextMenuInfo mCurrentMenuInfo;
+    
+    /** Header title for menu types that have a header (context and submenus) */
+    CharSequence mHeaderTitle;
+    /** Header icon for menu types that have a header and support icons (context) */
+    Drawable mHeaderIcon;
+    /** Header custom view for menu types that have a header and support custom views (context) */
+    View mHeaderView;
+
+    /**
+     * Contains the state of the View hierarchy for all menu views when the menu
+     * was frozen.
+     */
+    private SparseArray<Parcelable> mFrozenViewStates;
+
+    /**
+     * Prevents onItemsChanged from doing its junk, useful for batching commands
+     * that may individually call onItemsChanged.
+     */
+    private boolean mPreventDispatchingItemsChanged = false;
+    private boolean mItemsChangedWhileDispatchPrevented = false;
+    
+    private boolean mOptionalIconsVisible = false;
+
+    private boolean mIsClosing = false;
+
+    private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
+
+    private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
+            new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
+
+    /**
+     * Currently expanded menu item; must be collapsed when we clear.
+     */
+    private MenuItemImpl mExpandedItem;
+
+    /**
+     * Whether group dividers are enabled.
+     */
+    private boolean mGroupDividerEnabled = false;
+
+    /**
+     * Called by menu to notify of close and selection changes.
+     */
+    public interface Callback {
+        /**
+         * Called when a menu item is selected.
+         * @param menu The menu that is the parent of the item
+         * @param item The menu item that is selected
+         * @return whether the menu item selection was handled
+         */
+        public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
+        
+        /**
+         * Called when the mode of the menu changes (for example, from icon to expanded).
+         * 
+         * @param menu the menu that has changed modes
+         */
+        public void onMenuModeChange(MenuBuilder menu);
+    }
+
+    /**
+     * Called by menu items to execute their associated action
+     */
+    public interface ItemInvoker {
+        public boolean invokeItem(MenuItemImpl item);
+    }
+
+    public MenuBuilder(Context context) {
+        mContext = context;
+        mResources = context.getResources();
+        mItems = new ArrayList<MenuItemImpl>();
+        
+        mVisibleItems = new ArrayList<MenuItemImpl>();
+        mIsVisibleItemsStale = true;
+        
+        mActionItems = new ArrayList<MenuItemImpl>();
+        mNonActionItems = new ArrayList<MenuItemImpl>();
+        mIsActionItemsStale = true;
+        
+        setShortcutsVisibleInner(true);
+    }
+    
+    public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) {
+        mDefaultShowAsAction = defaultShowAsAction;
+        return this;
+    }
+
+    /**
+     * Add a presenter to this menu. This will only hold a WeakReference;
+     * you do not need to explicitly remove a presenter, but you can using
+     * {@link #removeMenuPresenter(MenuPresenter)}.
+     *
+     * @param presenter The presenter to add
+     */
+    public void addMenuPresenter(MenuPresenter presenter) {
+        addMenuPresenter(presenter, mContext);
+    }
+
+    /**
+     * Add a presenter to this menu that uses an alternate context for
+     * inflating menu items. This will only hold a WeakReference; you do not
+     * need to explicitly remove a presenter, but you can using
+     * {@link #removeMenuPresenter(MenuPresenter)}.
+     *
+     * @param presenter The presenter to add
+     * @param menuContext The context used to inflate menu items
+     */
+    public void addMenuPresenter(MenuPresenter presenter, Context menuContext) {
+        mPresenters.add(new WeakReference<MenuPresenter>(presenter));
+        presenter.initForMenu(menuContext, this);
+        mIsActionItemsStale = true;
+    }
+
+    /**
+     * Remove a presenter from this menu. That presenter will no longer
+     * receive notifications of updates to this menu's data.
+     *
+     * @param presenter The presenter to remove
+     */
+    public void removeMenuPresenter(MenuPresenter presenter) {
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter item = ref.get();
+            if (item == null || item == presenter) {
+                mPresenters.remove(ref);
+            }
+        }
+    }
+    
+    private void dispatchPresenterUpdate(boolean cleared) {
+        if (mPresenters.isEmpty()) return;
+
+        stopDispatchingItemsChanged();
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else {
+                presenter.updateMenuView(cleared);
+            }
+        }
+        startDispatchingItemsChanged();
+    }
+    
+    private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu,
+            MenuPresenter preferredPresenter) {
+        if (mPresenters.isEmpty()) return false;
+
+        boolean result = false;
+
+        // Try the preferred presenter first.
+        if (preferredPresenter != null) {
+            result = preferredPresenter.onSubMenuSelected(subMenu);
+        }
+
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else if (!result) {
+                result = presenter.onSubMenuSelected(subMenu);
+            }
+        }
+        return result;
+    }
+
+    private void dispatchSaveInstanceState(Bundle outState) {
+        if (mPresenters.isEmpty()) return;
+
+        SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>();
+
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else {
+                final int id = presenter.getId();
+                if (id > 0) {
+                    final Parcelable state = presenter.onSaveInstanceState();
+                    if (state != null) {
+                        presenterStates.put(id, state);
+                    }
+                }
+            }
+        }
+
+        outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates);
+    }
+
+    private void dispatchRestoreInstanceState(Bundle state) {
+        SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY);
+
+        if (presenterStates == null || mPresenters.isEmpty()) return;
+
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else {
+                final int id = presenter.getId();
+                if (id > 0) {
+                    Parcelable parcel = presenterStates.get(id);
+                    if (parcel != null) {
+                        presenter.onRestoreInstanceState(parcel);
+                    }
+                }
+            }
+        }
+    }
+
+    public void savePresenterStates(Bundle outState) {
+        dispatchSaveInstanceState(outState);
+    }
+
+    public void restorePresenterStates(Bundle state) {
+        dispatchRestoreInstanceState(state);
+    }
+
+    public void saveActionViewStates(Bundle outStates) {
+        SparseArray<Parcelable> viewStates = null;
+
+        final int itemCount = size();
+        for (int i = 0; i < itemCount; i++) {
+            final MenuItem item = getItem(i);
+            final View v = item.getActionView();
+            if (v != null && v.getId() != View.NO_ID) {
+                if (viewStates == null) {
+                    viewStates = new SparseArray<Parcelable>();
+                }
+                v.saveHierarchyState(viewStates);
+                if (item.isActionViewExpanded()) {
+                    outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId());
+                }
+            }
+            if (item.hasSubMenu()) {
+                final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
+                subMenu.saveActionViewStates(outStates);
+            }
+        }
+
+        if (viewStates != null) {
+            outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates);
+        }
+    }
+
+    public void restoreActionViewStates(Bundle states) {
+        if (states == null) {
+            return;
+        }
+
+        SparseArray<Parcelable> viewStates = states.getSparseParcelableArray(
+                getActionViewStatesKey());
+
+        final int itemCount = size();
+        for (int i = 0; i < itemCount; i++) {
+            final MenuItem item = getItem(i);
+            final View v = item.getActionView();
+            if (v != null && v.getId() != View.NO_ID) {
+                v.restoreHierarchyState(viewStates);
+            }
+            if (item.hasSubMenu()) {
+                final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
+                subMenu.restoreActionViewStates(states);
+            }
+        }
+
+        final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID);
+        if (expandedId > 0) {
+            MenuItem itemToExpand = findItem(expandedId);
+            if (itemToExpand != null) {
+                itemToExpand.expandActionView();
+            }
+        }
+    }
+
+    protected String getActionViewStatesKey() {
+        return ACTION_VIEW_STATES_KEY;
+    }
+
+    public void setCallback(Callback cb) {
+        mCallback = cb;
+    }
+    
+    /**
+     * Adds an item to the menu.  The other add methods funnel to this.
+     */
+    private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
+        final int ordering = getOrdering(categoryOrder);
+        
+        final MenuItemImpl item = createNewMenuItem(group, id, categoryOrder, ordering, title,
+                mDefaultShowAsAction);
+
+        if (mCurrentMenuInfo != null) {
+            // Pass along the current menu info
+            item.setMenuInfo(mCurrentMenuInfo);
+        }
+        
+        mItems.add(findInsertIndex(mItems, ordering), item);
+        onItemsChanged(true);
+        
+        return item;
+    }
+
+    // Layoutlib overrides this method to return its custom implementation of MenuItemImpl
+    private MenuItemImpl createNewMenuItem(int group, int id, int categoryOrder, int ordering,
+            CharSequence title, int defaultShowAsAction) {
+        return new MenuItemImpl(this, group, id, categoryOrder, ordering, title,
+                defaultShowAsAction);
+    }
+
+    public MenuItem add(CharSequence title) {
+        return addInternal(0, 0, 0, title);
+    }
+
+    public MenuItem add(int titleRes) {
+        return addInternal(0, 0, 0, mResources.getString(titleRes));
+    }
+
+    public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
+        return addInternal(group, id, categoryOrder, title);
+    }
+
+    public MenuItem add(int group, int id, int categoryOrder, int title) {
+        return addInternal(group, id, categoryOrder, mResources.getString(title));
+    }
+
+    public SubMenu addSubMenu(CharSequence title) {
+        return addSubMenu(0, 0, 0, title);
+    }
+
+    public SubMenu addSubMenu(int titleRes) {
+        return addSubMenu(0, 0, 0, mResources.getString(titleRes));
+    }
+
+    public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
+        final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
+        final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
+        item.setSubMenu(subMenu);
+        
+        return subMenu;
+    }
+
+    public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
+        return addSubMenu(group, id, categoryOrder, mResources.getString(title));
+    }
+
+    @Override
+    public void setGroupDividerEnabled(boolean groupDividerEnabled) {
+        mGroupDividerEnabled = groupDividerEnabled;
+    }
+
+    public boolean isGroupDividerEnabled() {
+        return mGroupDividerEnabled;
+    }
+
+    public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
+            Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
+        PackageManager pm = mContext.getPackageManager();
+        final List<ResolveInfo> lri =
+                pm.queryIntentActivityOptions(caller, specifics, intent, 0);
+        final int N = lri != null ? lri.size() : 0;
+
+        if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
+            removeGroup(group);
+        }
+
+        for (int i=0; i<N; i++) {
+            final ResolveInfo ri = lri.get(i);
+            Intent rintent = new Intent(
+                ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
+            rintent.setComponent(new ComponentName(
+                    ri.activityInfo.applicationInfo.packageName,
+                    ri.activityInfo.name));
+            final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
+                    .setIcon(ri.loadIcon(pm))
+                    .setIntent(rintent);
+            if (outSpecificItems != null && ri.specificIndex >= 0) {
+                outSpecificItems[ri.specificIndex] = item;
+            }
+        }
+
+        return N;
+    }
+
+    public void removeItem(int id) {
+        removeItemAtInt(findItemIndex(id), true);
+    }
+
+    public void removeGroup(int group) {
+        final int i = findGroupIndex(group);
+
+        if (i >= 0) {
+            final int maxRemovable = mItems.size() - i;
+            int numRemoved = 0;
+            while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
+                // Don't force update for each one, this method will do it at the end
+                removeItemAtInt(i, false);
+            }
+            
+            // Notify menu views
+            onItemsChanged(true);
+        }
+    }
+
+    /**
+     * Remove the item at the given index and optionally forces menu views to
+     * update.
+     * 
+     * @param index The index of the item to be removed. If this index is
+     *            invalid an exception is thrown.
+     * @param updateChildrenOnMenuViews Whether to force update on menu views.
+     *            Please make sure you eventually call this after your batch of
+     *            removals.
+     */
+    private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
+        if ((index < 0) || (index >= mItems.size())) return;
+
+        mItems.remove(index);
+        
+        if (updateChildrenOnMenuViews) onItemsChanged(true);
+    }
+    
+    public void removeItemAt(int index) {
+        removeItemAtInt(index, true);
+    }
+
+    public void clearAll() {
+        mPreventDispatchingItemsChanged = true;
+        clear();
+        clearHeader();
+        mPreventDispatchingItemsChanged = false;
+        mItemsChangedWhileDispatchPrevented = false;
+        onItemsChanged(true);
+    }
+    
+    public void clear() {
+        if (mExpandedItem != null) {
+            collapseItemActionView(mExpandedItem);
+        }
+        mItems.clear();
+        
+        onItemsChanged(true);
+    }
+
+    void setExclusiveItemChecked(MenuItem item) {
+        final int group = item.getGroupId();
+        
+        final int N = mItems.size();
+        for (int i = 0; i < N; i++) {
+            MenuItemImpl curItem = mItems.get(i);
+            if (curItem.getGroupId() == group) {
+                if (!curItem.isExclusiveCheckable()) continue;
+                if (!curItem.isCheckable()) continue;
+                
+                // Check the item meant to be checked, uncheck the others (that are in the group)
+                curItem.setCheckedInt(curItem == item);
+            }
+        }
+    }
+    
+    public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
+        final int N = mItems.size();
+       
+        for (int i = 0; i < N; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.getGroupId() == group) {
+                item.setExclusiveCheckable(exclusive);
+                item.setCheckable(checkable);
+            }
+        }
+    }
+
+    public void setGroupVisible(int group, boolean visible) {
+        final int N = mItems.size();
+
+        // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
+        // than setVisible and at the end notify of items being changed
+        
+        boolean changedAtLeastOneItem = false;
+        for (int i = 0; i < N; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.getGroupId() == group) {
+                if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
+            }
+        }
+
+        if (changedAtLeastOneItem) onItemsChanged(true);
+    }
+
+    public void setGroupEnabled(int group, boolean enabled) {
+        final int N = mItems.size();
+
+        for (int i = 0; i < N; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.getGroupId() == group) {
+                item.setEnabled(enabled);
+            }
+        }
+    }
+
+    public boolean hasVisibleItems() {
+        final int size = size();
+
+        for (int i = 0; i < size; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.isVisible()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public MenuItem findItem(int id) {
+        final int size = size();
+        for (int i = 0; i < size; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.getItemId() == id) {
+                return item;
+            } else if (item.hasSubMenu()) {
+                MenuItem possibleItem = item.getSubMenu().findItem(id);
+                
+                if (possibleItem != null) {
+                    return possibleItem;
+                }
+            }
+        }
+        
+        return null;
+    }
+
+    public int findItemIndex(int id) {
+        final int size = size();
+
+        for (int i = 0; i < size; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.getItemId() == id) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    public int findGroupIndex(int group) {
+        return findGroupIndex(group, 0);
+    }
+
+    public int findGroupIndex(int group, int start) {
+        final int size = size();
+        
+        if (start < 0) {
+            start = 0;
+        }
+        
+        for (int i = start; i < size; i++) {
+            final MenuItemImpl item = mItems.get(i);
+            
+            if (item.getGroupId() == group) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+    
+    public int size() {
+        return mItems.size();
+    }
+
+    /** {@inheritDoc} */
+    public MenuItem getItem(int index) {
+        return mItems.get(index);
+    }
+
+    public boolean isShortcutKey(int keyCode, KeyEvent event) {
+        return findItemWithShortcutForKey(keyCode, event) != null;
+    }
+
+    public void setQwertyMode(boolean isQwerty) {
+        mQwertyMode = isQwerty;
+
+        onItemsChanged(false);
+    }
+
+    /**
+     * Returns the ordering across all items. This will grab the category from
+     * the upper bits, find out how to order the category with respect to other
+     * categories, and combine it with the lower bits.
+     * 
+     * @param categoryOrder The category order for a particular item (if it has
+     *            not been or/add with a category, the default category is
+     *            assumed).
+     * @return An ordering integer that can be used to order this item across
+     *         all the items (even from other categories).
+     */
+    private static int getOrdering(int categoryOrder) {
+        final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
+        
+        if (index < 0 || index >= sCategoryToOrder.length) {
+            throw new IllegalArgumentException("order does not contain a valid category.");
+        }
+        
+        return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
+    }
+
+    /**
+     * @return whether the menu shortcuts are in qwerty mode or not
+     */
+    boolean isQwertyMode() {
+        return mQwertyMode;
+    }
+
+    /**
+     * Sets whether the shortcuts should be visible on menus.  Devices without hardware
+     * key input will never make shortcuts visible even if this method is passed 'true'.
+     * 
+     * @param shortcutsVisible Whether shortcuts should be visible (if true and a
+     *            menu item does not have a shortcut defined, that item will
+     *            still NOT show a shortcut)
+     */
+    public void setShortcutsVisible(boolean shortcutsVisible) {
+        if (mShortcutsVisible == shortcutsVisible) return;
+
+        setShortcutsVisibleInner(shortcutsVisible);
+        onItemsChanged(false);
+    }
+
+    private void setShortcutsVisibleInner(boolean shortcutsVisible) {
+        mShortcutsVisible = shortcutsVisible
+                && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
+                && mResources.getBoolean(
+                        com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
+    }
+
+    /**
+     * @return Whether shortcuts should be visible on menus.
+     */
+    public boolean isShortcutsVisible() {
+        return mShortcutsVisible;
+    }
+    
+    Resources getResources() {
+        return mResources;
+    }
+    
+    public Context getContext() {
+        return mContext;
+    }
+    
+    boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
+        return mCallback != null && mCallback.onMenuItemSelected(menu, item);
+    }
+
+    /**
+     * Dispatch a mode change event to this menu's callback.
+     */
+    public void changeMenuMode() {
+        if (mCallback != null) {
+            mCallback.onMenuModeChange(this);
+        }
+    }
+
+    private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
+        for (int i = items.size() - 1; i >= 0; i--) {
+            MenuItemImpl item = items.get(i);
+            if (item.getOrdering() <= ordering) {
+                return i + 1;
+            }
+        }
+        
+        return 0;
+    }
+    
+    public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
+        final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
+
+        boolean handled = false;
+        
+        if (item != null) {
+            handled = performItemAction(item, flags);
+        }
+        
+        if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
+            close(true /* closeAllMenus */);
+        }
+        
+        return handled;
+    }
+
+    /*
+     * This function will return all the menu and sub-menu items that can
+     * be directly (the shortcut directly corresponds) and indirectly
+     * (the ALT-enabled char corresponds to the shortcut) associated
+     * with the keyCode.
+     */
+    void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) {
+        final boolean qwerty = isQwertyMode();
+        final int modifierState = event.getModifiers();
+        final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
+        // Get the chars associated with the keyCode (i.e using any chording combo)
+        final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
+        // The delete key is not mapped to '\b' so we treat it specially
+        if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
+            return;
+        }
+
+        // Look for an item whose shortcut is this key.
+        final int N = mItems.size();
+        for (int i = 0; i < N; i++) {
+            MenuItemImpl item = mItems.get(i);
+            if (item.hasSubMenu()) {
+                ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event);
+            }
+            final char shortcutChar =
+                    qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
+            final int shortcutModifiers =
+                    qwerty ? item.getAlphabeticModifiers() : item.getNumericModifiers();
+            final boolean isModifiersExactMatch = (modifierState & SUPPORTED_MODIFIERS_MASK)
+                    == (shortcutModifiers & SUPPORTED_MODIFIERS_MASK);
+            if (isModifiersExactMatch && (shortcutChar != 0) &&
+                  (shortcutChar == possibleChars.meta[0]
+                      || shortcutChar == possibleChars.meta[2]
+                      || (qwerty && shortcutChar == '\b' &&
+                          keyCode == KeyEvent.KEYCODE_DEL)) &&
+                  item.isEnabled()) {
+                items.add(item);
+            }
+        }
+    }
+
+    /*
+     * We want to return the menu item associated with the key, but if there is no
+     * ambiguity (i.e. there is only one menu item corresponding to the key) we want
+     * to return it even if it's not an exact match; this allow the user to
+     * _not_ use the ALT key for example, making the use of shortcuts slightly more
+     * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
+     * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
+     *
+     * On the other hand, if two (or more) shortcuts corresponds to the same key,
+     * we have to only return the exact match.
+     */
+    MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
+        // Get all items that can be associated directly or indirectly with the keyCode
+        ArrayList<MenuItemImpl> items = mTempShortcutItemList;
+        items.clear();
+        findItemsWithShortcutForKey(items, keyCode, event);
+
+        if (items.isEmpty()) {
+            return null;
+        }
+
+        final int metaState = event.getMetaState();
+        final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
+        // Get the chars associated with the keyCode (i.e using any chording combo)
+        event.getKeyData(possibleChars);
+
+        // If we have only one element, we can safely returns it
+        final int size = items.size();
+        if (size == 1) {
+            return items.get(0);
+        }
+
+        final boolean qwerty = isQwertyMode();
+        // If we found more than one item associated with the key,
+        // we have to return the exact match
+        for (int i = 0; i < size; i++) {
+            final MenuItemImpl item = items.get(i);
+            final char shortcutChar = qwerty ? item.getAlphabeticShortcut() :
+                    item.getNumericShortcut();
+            if ((shortcutChar == possibleChars.meta[0] &&
+                    (metaState & KeyEvent.META_ALT_ON) == 0)
+                || (shortcutChar == possibleChars.meta[2] &&
+                    (metaState & KeyEvent.META_ALT_ON) != 0)
+                || (qwerty && shortcutChar == '\b' &&
+                    keyCode == KeyEvent.KEYCODE_DEL)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public boolean performIdentifierAction(int id, int flags) {
+        // Look for an item whose identifier is the id.
+        return performItemAction(findItem(id), flags);           
+    }
+
+    public boolean performItemAction(MenuItem item, int flags) {
+        return performItemAction(item, null, flags);
+    }
+
+    public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
+        MenuItemImpl itemImpl = (MenuItemImpl) item;
+        
+        if (itemImpl == null || !itemImpl.isEnabled()) {
+            return false;
+        }
+
+        boolean invoked = itemImpl.invoke();
+
+        final ActionProvider provider = item.getActionProvider();
+        final boolean providerHasSubMenu = provider != null && provider.hasSubMenu();
+        if (itemImpl.hasCollapsibleActionView()) {
+            invoked |= itemImpl.expandActionView();
+            if (invoked) {
+                close(true /* closeAllMenus */);
+            }
+        } else if (itemImpl.hasSubMenu() || providerHasSubMenu) {
+            if (!itemImpl.hasSubMenu()) {
+                itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl));
+            }
+
+            final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu();
+            if (providerHasSubMenu) {
+                provider.onPrepareSubMenu(subMenu);
+            }
+            invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter);
+            if (!invoked) {
+                close(true /* closeAllMenus */);
+            }
+        } else {
+            if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
+                close(true /* closeAllMenus */);
+            }
+        }
+        
+        return invoked;
+    }
+    
+    /**
+     * Closes the menu.
+     *
+     * @param closeAllMenus {@code true} if all displayed menus and submenus
+     *                      should be completely closed (as when a menu item is
+     *                      selected) or {@code false} if only this menu should
+     *                      be closed
+     */
+    public final void close(boolean closeAllMenus) {
+        if (mIsClosing) return;
+
+        mIsClosing = true;
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else {
+                presenter.onCloseMenu(this, closeAllMenus);
+            }
+        }
+        mIsClosing = false;
+    }
+
+    /** {@inheritDoc} */
+    public void close() {
+        close(true /* closeAllMenus */);
+    }
+
+    /**
+     * Called when an item is added or removed.
+     * 
+     * @param structureChanged true if the menu structure changed,
+     *                         false if only item properties changed.
+     *                         (Visibility is a structural property since it affects layout.)
+     */
+    public void onItemsChanged(boolean structureChanged) {
+        if (!mPreventDispatchingItemsChanged) {
+            if (structureChanged) {
+                mIsVisibleItemsStale = true;
+                mIsActionItemsStale = true;
+            }
+
+            dispatchPresenterUpdate(structureChanged);
+        } else {
+            mItemsChangedWhileDispatchPrevented = true;
+        }
+    }
+
+    /**
+     * Stop dispatching item changed events to presenters until
+     * {@link #startDispatchingItemsChanged()} is called. Useful when
+     * many menu operations are going to be performed as a batch.
+     */
+    public void stopDispatchingItemsChanged() {
+        if (!mPreventDispatchingItemsChanged) {
+            mPreventDispatchingItemsChanged = true;
+            mItemsChangedWhileDispatchPrevented = false;
+        }
+    }
+
+    public void startDispatchingItemsChanged() {
+        mPreventDispatchingItemsChanged = false;
+
+        if (mItemsChangedWhileDispatchPrevented) {
+            mItemsChangedWhileDispatchPrevented = false;
+            onItemsChanged(true);
+        }
+    }
+
+    /**
+     * Called by {@link MenuItemImpl} when its visible flag is changed.
+     * @param item The item that has gone through a visibility change.
+     */
+    void onItemVisibleChanged(MenuItemImpl item) {
+        // Notify of items being changed
+        mIsVisibleItemsStale = true;
+        onItemsChanged(true);
+    }
+    
+    /**
+     * Called by {@link MenuItemImpl} when its action request status is changed.
+     * @param item The item that has gone through a change in action request status.
+     */
+    void onItemActionRequestChanged(MenuItemImpl item) {
+        // Notify of items being changed
+        mIsActionItemsStale = true;
+        onItemsChanged(true);
+    }
+
+    @NonNull
+    public ArrayList<MenuItemImpl> getVisibleItems() {
+        if (!mIsVisibleItemsStale) return mVisibleItems;
+
+        // Refresh the visible items
+        mVisibleItems.clear();
+
+        final int itemsSize = mItems.size(); 
+        MenuItemImpl item;
+        for (int i = 0; i < itemsSize; i++) {
+            item = mItems.get(i);
+            if (item.isVisible()) mVisibleItems.add(item);
+        }
+
+        mIsVisibleItemsStale = false;
+        mIsActionItemsStale = true;
+
+        return mVisibleItems;
+    }
+
+    /**
+     * This method determines which menu items get to be 'action items' that will appear
+     * in an action bar and which items should be 'overflow items' in a secondary menu.
+     * The rules are as follows:
+     *
+     * <p>Items are considered for inclusion in the order specified within the menu.
+     * There is a limit of mMaxActionItems as a total count, optionally including the overflow
+     * menu button itself. This is a soft limit; if an item shares a group ID with an item
+     * previously included as an action item, the new item will stay with its group and become
+     * an action item itself even if it breaks the max item count limit. This is done to
+     * limit the conceptual complexity of the items presented within an action bar. Only a few
+     * unrelated concepts should be presented to the user in this space, and groups are treated
+     * as a single concept.
+     *
+     * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
+     * limit may be broken by a single item that exceeds the remaining space, but no further
+     * items may be added. If an item that is part of a group cannot fit within the remaining
+     * measured width, the entire group will be demoted to overflow. This is done to ensure room
+     * for navigation and other affordances in the action bar as well as reduce general UI clutter.
+     *
+     * <p>The space freed by demoting a full group cannot be consumed by future menu items.
+     * Once items begin to overflow, all future items become overflow items as well. This is
+     * to avoid inadvertent reordering that may break the app's intended design.
+     */
+    public void flagActionItems() {
+        // Important side effect: if getVisibleItems is stale it may refresh,
+        // which can affect action items staleness.
+        final ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
+
+        if (!mIsActionItemsStale) {
+            return;
+        }
+
+        // Presenters flag action items as needed.
+        boolean flagged = false;
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else {
+                flagged |= presenter.flagActionItems();
+            }
+        }
+
+        if (flagged) {
+            mActionItems.clear();
+            mNonActionItems.clear();
+            final int itemsSize = visibleItems.size();
+            for (int i = 0; i < itemsSize; i++) {
+                MenuItemImpl item = visibleItems.get(i);
+                if (item.isActionButton()) {
+                    mActionItems.add(item);
+                } else {
+                    mNonActionItems.add(item);
+                }
+            }
+        } else {
+            // Nobody flagged anything, everything is a non-action item.
+            // (This happens during a first pass with no action-item presenters.)
+            mActionItems.clear();
+            mNonActionItems.clear();
+            mNonActionItems.addAll(getVisibleItems());
+        }
+        mIsActionItemsStale = false;
+    }
+    
+    public ArrayList<MenuItemImpl> getActionItems() {
+        flagActionItems();
+        return mActionItems;
+    }
+    
+    public ArrayList<MenuItemImpl> getNonActionItems() {
+        flagActionItems();
+        return mNonActionItems;
+    }
+
+    public void clearHeader() {
+        mHeaderIcon = null;
+        mHeaderTitle = null;
+        mHeaderView = null;
+        
+        onItemsChanged(false);
+    }
+    
+    private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
+            final Drawable icon, final View view) {
+        final Resources r = getResources();
+
+        if (view != null) {
+            mHeaderView = view;
+            
+            // If using a custom view, then the title and icon aren't used
+            mHeaderTitle = null;
+            mHeaderIcon = null;
+        } else {
+            if (titleRes > 0) {
+                mHeaderTitle = r.getText(titleRes);
+            } else if (title != null) {
+                mHeaderTitle = title;
+            }
+            
+            if (iconRes > 0) {
+                mHeaderIcon = getContext().getDrawable(iconRes);
+            } else if (icon != null) {
+                mHeaderIcon = icon;
+            }
+            
+            // If using the title or icon, then a custom view isn't used
+            mHeaderView = null;
+        }
+        
+        // Notify of change
+        onItemsChanged(false);
+    }
+
+    /**
+     * Sets the header's title. This replaces the header view. Called by the
+     * builder-style methods of subclasses.
+     * 
+     * @param title The new title.
+     * @return This MenuBuilder so additional setters can be called.
+     */
+    protected MenuBuilder setHeaderTitleInt(CharSequence title) {
+        setHeaderInternal(0, title, 0, null, null);
+        return this;
+    }
+    
+    /**
+     * Sets the header's title. This replaces the header view. Called by the
+     * builder-style methods of subclasses.
+     * 
+     * @param titleRes The new title (as a resource ID).
+     * @return This MenuBuilder so additional setters can be called.
+     */
+    protected MenuBuilder setHeaderTitleInt(int titleRes) {
+        setHeaderInternal(titleRes, null, 0, null, null);
+        return this;
+    }
+    
+    /**
+     * Sets the header's icon. This replaces the header view. Called by the
+     * builder-style methods of subclasses.
+     * 
+     * @param icon The new icon.
+     * @return This MenuBuilder so additional setters can be called.
+     */
+    protected MenuBuilder setHeaderIconInt(Drawable icon) {
+        setHeaderInternal(0, null, 0, icon, null);
+        return this;
+    }
+    
+    /**
+     * Sets the header's icon. This replaces the header view. Called by the
+     * builder-style methods of subclasses.
+     * 
+     * @param iconRes The new icon (as a resource ID).
+     * @return This MenuBuilder so additional setters can be called.
+     */
+    protected MenuBuilder setHeaderIconInt(int iconRes) {
+        setHeaderInternal(0, null, iconRes, null, null);
+        return this;
+    }
+    
+    /**
+     * Sets the header's view. This replaces the title and icon. Called by the
+     * builder-style methods of subclasses.
+     * 
+     * @param view The new view.
+     * @return This MenuBuilder so additional setters can be called.
+     */
+    protected MenuBuilder setHeaderViewInt(View view) {
+        setHeaderInternal(0, null, 0, null, view);
+        return this;
+    }
+    
+    public CharSequence getHeaderTitle() {
+        return mHeaderTitle;
+    }
+    
+    public Drawable getHeaderIcon() {
+        return mHeaderIcon;
+    }
+    
+    public View getHeaderView() {
+        return mHeaderView;
+    }
+    
+    /**
+     * Gets the root menu (if this is a submenu, find its root menu).
+     * @return The root menu.
+     */
+    public MenuBuilder getRootMenu() {
+        return this;
+    }
+    
+    /**
+     * Sets the current menu info that is set on all items added to this menu
+     * (until this is called again with different menu info, in which case that
+     * one will be added to all subsequent item additions).
+     * 
+     * @param menuInfo The extra menu information to add.
+     */
+    public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
+        mCurrentMenuInfo = menuInfo;
+    }
+
+    void setOptionalIconsVisible(boolean visible) {
+        mOptionalIconsVisible = visible;
+    }
+    
+    boolean getOptionalIconsVisible() {
+        return mOptionalIconsVisible;
+    }
+
+    public boolean expandItemActionView(MenuItemImpl item) {
+        if (mPresenters.isEmpty()) return false;
+
+        boolean expanded = false;
+
+        stopDispatchingItemsChanged();
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else if ((expanded = presenter.expandItemActionView(this, item))) {
+                break;
+            }
+        }
+        startDispatchingItemsChanged();
+
+        if (expanded) {
+            mExpandedItem = item;
+        }
+        return expanded;
+    }
+
+    public boolean collapseItemActionView(MenuItemImpl item) {
+        if (mPresenters.isEmpty() || mExpandedItem != item) return false;
+
+        boolean collapsed = false;
+
+        stopDispatchingItemsChanged();
+        for (WeakReference<MenuPresenter> ref : mPresenters) {
+            final MenuPresenter presenter = ref.get();
+            if (presenter == null) {
+                mPresenters.remove(ref);
+            } else if ((collapsed = presenter.collapseItemActionView(this, item))) {
+                break;
+            }
+        }
+        startDispatchingItemsChanged();
+
+        if (collapsed) {
+            mExpandedItem = null;
+        }
+        return collapsed;
+    }
+
+    public MenuItemImpl getExpandedItem() {
+        return mExpandedItem;
+    }
+}
diff --git a/com/android/internal/view/menu/MenuBuilder_Delegate.java b/com/android/internal/view/menu/MenuBuilder_Delegate.java
new file mode 100644
index 0000000..505fb81
--- /dev/null
+++ b/com/android/internal/view/menu/MenuBuilder_Delegate.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 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.internal.view.menu;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link MenuBuilder}
+ * <p/>
+ * Through the layoutlib_create tool, the original  methods of {@code MenuBuilder} have been
+ * replaced by calls to methods of the same name in this delegate class.
+ */
+public class MenuBuilder_Delegate {
+    /**
+     * The method overrides the instantiation of the {@link MenuItemImpl} with an instance of
+     * {@link BridgeMenuItemImpl} so that view cookies may be stored.
+     */
+    @LayoutlibDelegate
+    /*package*/ static MenuItemImpl createNewMenuItem(MenuBuilder thisMenu, int group, int id,
+            int categoryOrder, int ordering, CharSequence title, int defaultShowAsAction) {
+        return new BridgeMenuItemImpl(thisMenu, group, id, categoryOrder, ordering, title,
+                defaultShowAsAction);
+    }
+}
diff --git a/com/android/internal/view/menu/MenuDialogHelper.java b/com/android/internal/view/menu/MenuDialogHelper.java
new file mode 100644
index 0000000..ecab29f
--- /dev/null
+++ b/com/android/internal/view/menu/MenuDialogHelper.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.IBinder;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+/**
+ * Presents a menu as a modal dialog.
+ */
+public class MenuDialogHelper implements MenuHelper, DialogInterface.OnKeyListener,
+        DialogInterface.OnClickListener, DialogInterface.OnDismissListener,
+        MenuPresenter.Callback {
+    private MenuBuilder mMenu;
+    private AlertDialog mDialog;
+    ListMenuPresenter mPresenter;
+    private MenuPresenter.Callback mPresenterCallback;
+    
+    public MenuDialogHelper(MenuBuilder menu) {
+        mMenu = menu;
+    }
+
+    /**
+     * Shows menu as a dialog. 
+     * 
+     * @param windowToken Optional token to assign to the window.
+     */
+    public void show(IBinder windowToken) {
+        // Many references to mMenu, create local reference
+        final MenuBuilder menu = mMenu;
+        
+        // Get the builder for the dialog
+        final AlertDialog.Builder builder = new AlertDialog.Builder(menu.getContext());
+
+        mPresenter = new ListMenuPresenter(builder.getContext(),
+                com.android.internal.R.layout.list_menu_item_layout);
+
+        mPresenter.setCallback(this);
+        mMenu.addMenuPresenter(mPresenter);
+        builder.setAdapter(mPresenter.getAdapter(), this);
+
+        // Set the title
+        final View headerView = menu.getHeaderView();
+        if (headerView != null) {
+            // Menu's client has given a custom header view, use it
+            builder.setCustomTitle(headerView);
+        } else {
+            // Otherwise use the (text) title and icon
+            builder.setIcon(menu.getHeaderIcon()).setTitle(menu.getHeaderTitle());
+        }
+        
+        // Set the key listener
+        builder.setOnKeyListener(this);
+        
+        // Show the menu
+        mDialog = builder.create();
+        mDialog.setOnDismissListener(this);
+        
+        WindowManager.LayoutParams lp = mDialog.getWindow().getAttributes();
+        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+        if (windowToken != null) {
+            lp.token = windowToken;
+        }
+        lp.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        
+        mDialog.show();
+    }
+    
+    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_BACK) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN
+                    && event.getRepeatCount() == 0) {
+                Window win = mDialog.getWindow();
+                if (win != null) {
+                    View decor = win.getDecorView();
+                    if (decor != null) {
+                        KeyEvent.DispatcherState ds = decor.getKeyDispatcherState();
+                        if (ds != null) {
+                            ds.startTracking(event, this);
+                            return true;
+                        }
+                    }
+                }
+            } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled()) {
+                Window win = mDialog.getWindow();
+                if (win != null) {
+                    View decor = win.getDecorView();
+                    if (decor != null) {
+                        KeyEvent.DispatcherState ds = decor.getKeyDispatcherState();
+                        if (ds != null && ds.isTracking(event)) {
+                            mMenu.close(true /* closeAllMenus */);
+                            dialog.dismiss();
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Menu shortcut matching
+        return mMenu.performShortcut(keyCode, event, 0);
+
+    }
+
+    @Override
+    public void setPresenterCallback(MenuPresenter.Callback cb) {
+        mPresenterCallback = cb;
+    }
+
+    /**
+     * Dismisses the menu's dialog.
+     * 
+     * @see Dialog#dismiss()
+     */
+    @Override
+    public void dismiss() {
+        if (mDialog != null) {
+            mDialog.dismiss();
+        }
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        mPresenter.onCloseMenu(mMenu, true);
+    }
+
+    @Override
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        if (allMenusAreClosing || menu == mMenu) {
+            dismiss();
+        }
+        if (mPresenterCallback != null) {
+            mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
+        }
+    }
+
+    @Override
+    public boolean onOpenSubMenu(MenuBuilder subMenu) {
+        if (mPresenterCallback != null) {
+            return mPresenterCallback.onOpenSubMenu(subMenu);
+        }
+        return false;
+    }
+
+    public void onClick(DialogInterface dialog, int which) {
+        mMenu.performItemAction((MenuItemImpl) mPresenter.getAdapter().getItem(which), 0);
+    }
+}
diff --git a/com/android/internal/view/menu/MenuHelper.java b/com/android/internal/view/menu/MenuHelper.java
new file mode 100644
index 0000000..9534722
--- /dev/null
+++ b/com/android/internal/view/menu/MenuHelper.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 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.internal.view.menu;
+
+/**
+ * Interface for a helper capable of presenting a menu.
+ */
+public interface MenuHelper {
+    void setPresenterCallback(MenuPresenter.Callback cb);
+    void dismiss();
+}
diff --git a/com/android/internal/view/menu/MenuItemImpl.java b/com/android/internal/view/menu/MenuItemImpl.java
new file mode 100644
index 0000000..15e271e
--- /dev/null
+++ b/com/android/internal/view/menu/MenuItemImpl.java
@@ -0,0 +1,832 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import com.android.internal.view.menu.MenuView.ItemView;
+
+import android.annotation.Nullable;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.ActionProvider;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewDebug;
+import android.widget.LinearLayout;
+
+/**
+ * @hide
+ */
+public final class MenuItemImpl implements MenuItem {
+    private static final String TAG = "MenuItemImpl";
+
+    private static final int SHOW_AS_ACTION_MASK = SHOW_AS_ACTION_NEVER |
+            SHOW_AS_ACTION_IF_ROOM |
+            SHOW_AS_ACTION_ALWAYS;
+
+    private final int mId;
+    private final int mGroup;
+    private final int mCategoryOrder;
+    private final int mOrdering;
+    private CharSequence mTitle;
+    private CharSequence mTitleCondensed;
+    private Intent mIntent;
+    private char mShortcutNumericChar;
+    private int mShortcutNumericModifiers = KeyEvent.META_CTRL_ON;
+    private char mShortcutAlphabeticChar;
+    private int mShortcutAlphabeticModifiers = KeyEvent.META_CTRL_ON;
+
+    /** The icon's drawable which is only created as needed */
+    private Drawable mIconDrawable;
+    /**
+     * The icon's resource ID which is used to get the Drawable when it is
+     * needed (if the Drawable isn't already obtained--only one of the two is
+     * needed).
+     */
+    private int mIconResId = NO_ICON;
+
+    private ColorStateList mIconTintList = null;
+    private PorterDuff.Mode mIconTintMode = null;
+    private boolean mHasIconTint = false;
+    private boolean mHasIconTintMode = false;
+    private boolean mNeedToApplyIconTint = false;
+
+    /** The menu to which this item belongs */
+    private MenuBuilder mMenu;
+    /** If this item should launch a sub menu, this is the sub menu to launch */
+    private SubMenuBuilder mSubMenu;
+
+    private Runnable mItemCallback;
+    private MenuItem.OnMenuItemClickListener mClickListener;
+
+    private int mFlags = ENABLED;
+    private static final int CHECKABLE      = 0x00000001;
+    private static final int CHECKED        = 0x00000002;
+    private static final int EXCLUSIVE      = 0x00000004;
+    private static final int HIDDEN         = 0x00000008;
+    private static final int ENABLED        = 0x00000010;
+    private static final int IS_ACTION      = 0x00000020;
+
+    private int mShowAsAction = SHOW_AS_ACTION_NEVER;
+
+    private View mActionView;
+    private ActionProvider mActionProvider;
+    private OnActionExpandListener mOnActionExpandListener;
+    private boolean mIsActionViewExpanded = false;
+
+    /** Used for the icon resource ID if this item does not have an icon */
+    static final int NO_ICON = 0;
+
+    /**
+     * Current use case is for context menu: Extra information linked to the
+     * View that added this item to the context menu.
+     */
+    private ContextMenuInfo mMenuInfo;
+
+    private CharSequence mContentDescription;
+    private CharSequence mTooltipText;
+
+    private static String sLanguage;
+    private static String sPrependShortcutLabel;
+    private static String sEnterShortcutLabel;
+    private static String sDeleteShortcutLabel;
+    private static String sSpaceShortcutLabel;
+
+
+    /**
+     * Instantiates this menu item.
+     *
+     * @param menu
+     * @param group Item ordering grouping control. The item will be added after
+     *            all other items whose order is <= this number, and before any
+     *            that are larger than it. This can also be used to define
+     *            groups of items for batch state changes. Normally use 0.
+     * @param id Unique item ID. Use 0 if you do not need a unique ID.
+     * @param categoryOrder The ordering for this item.
+     * @param title The text to display for the item.
+     */
+    MenuItemImpl(MenuBuilder menu, int group, int id, int categoryOrder, int ordering,
+            CharSequence title, int showAsAction) {
+
+        String lang = menu.getContext().getResources().getConfiguration().locale.toString();
+        if (sPrependShortcutLabel == null || !lang.equals(sLanguage)) {
+            sLanguage = lang;
+            // This is instantiated from the UI thread, so no chance of sync issues
+            sPrependShortcutLabel = menu.getContext().getResources().getString(
+                    com.android.internal.R.string.prepend_shortcut_label);
+            sEnterShortcutLabel = menu.getContext().getResources().getString(
+                    com.android.internal.R.string.menu_enter_shortcut_label);
+            sDeleteShortcutLabel = menu.getContext().getResources().getString(
+                    com.android.internal.R.string.menu_delete_shortcut_label);
+            sSpaceShortcutLabel = menu.getContext().getResources().getString(
+                    com.android.internal.R.string.menu_space_shortcut_label);
+        }
+
+        mMenu = menu;
+        mId = id;
+        mGroup = group;
+        mCategoryOrder = categoryOrder;
+        mOrdering = ordering;
+        mTitle = title;
+        mShowAsAction = showAsAction;
+    }
+
+    /**
+     * Invokes the item by calling various listeners or callbacks.
+     *
+     * @return true if the invocation was handled, false otherwise
+     */
+    public boolean invoke() {
+        if (mClickListener != null &&
+            mClickListener.onMenuItemClick(this)) {
+            return true;
+        }
+
+        if (mMenu.dispatchMenuItemSelected(mMenu, this)) {
+            return true;
+        }
+
+        if (mItemCallback != null) {
+            mItemCallback.run();
+            return true;
+        }
+
+        if (mIntent != null) {
+            try {
+                mMenu.getContext().startActivity(mIntent);
+                return true;
+            } catch (ActivityNotFoundException e) {
+                Log.e(TAG, "Can't find activity to handle intent; ignoring", e);
+            }
+        }
+
+        if (mActionProvider != null && mActionProvider.onPerformDefaultAction()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean isEnabled() {
+        return (mFlags & ENABLED) != 0;
+    }
+
+    public MenuItem setEnabled(boolean enabled) {
+        if (enabled) {
+            mFlags |= ENABLED;
+        } else {
+            mFlags &= ~ENABLED;
+        }
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    public int getGroupId() {
+        return mGroup;
+    }
+
+    @ViewDebug.CapturedViewProperty
+    public int getItemId() {
+        return mId;
+    }
+
+    public int getOrder() {
+        return mCategoryOrder;
+    }
+
+    public int getOrdering() {
+        return mOrdering;
+    }
+
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    public MenuItem setIntent(Intent intent) {
+        mIntent = intent;
+        return this;
+    }
+
+    Runnable getCallback() {
+        return mItemCallback;
+    }
+
+    public MenuItem setCallback(Runnable callback) {
+        mItemCallback = callback;
+        return this;
+    }
+
+    @Override
+    public char getAlphabeticShortcut() {
+        return mShortcutAlphabeticChar;
+    }
+
+    @Override
+    public int getAlphabeticModifiers() {
+        return mShortcutAlphabeticModifiers;
+    }
+
+    @Override
+    public MenuItem setAlphabeticShortcut(char alphaChar) {
+        if (mShortcutAlphabeticChar == alphaChar) return this;
+
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public MenuItem setAlphabeticShortcut(char alphaChar, int alphaModifiers){
+        if (mShortcutAlphabeticChar == alphaChar &&
+                mShortcutAlphabeticModifiers == alphaModifiers) {
+            return this;
+        }
+
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+        mShortcutAlphabeticModifiers = KeyEvent.normalizeMetaState(alphaModifiers);
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public char getNumericShortcut() {
+        return mShortcutNumericChar;
+    }
+
+    @Override
+    public int getNumericModifiers() {
+        return mShortcutNumericModifiers;
+    }
+
+    @Override
+    public MenuItem setNumericShortcut(char numericChar) {
+        if (mShortcutNumericChar == numericChar) return this;
+
+        mShortcutNumericChar = numericChar;
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public MenuItem setNumericShortcut(char numericChar, int numericModifiers){
+        if (mShortcutNumericChar == numericChar && mShortcutNumericModifiers == numericModifiers) {
+            return this;
+        }
+
+        mShortcutNumericChar = numericChar;
+        mShortcutNumericModifiers = KeyEvent.normalizeMetaState(numericModifiers);
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public MenuItem setShortcut(char numericChar, char alphaChar) {
+        mShortcutNumericChar = numericChar;
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public MenuItem setShortcut(char numericChar, char alphaChar, int numericModifiers,
+            int alphaModifiers) {
+        mShortcutNumericChar = numericChar;
+        mShortcutNumericModifiers = KeyEvent.normalizeMetaState(numericModifiers);
+        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
+        mShortcutAlphabeticModifiers = KeyEvent.normalizeMetaState(alphaModifiers);
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    /**
+     * @return The active shortcut (based on QWERTY-mode of the menu).
+     */
+    char getShortcut() {
+        return (mMenu.isQwertyMode() ? mShortcutAlphabeticChar : mShortcutNumericChar);
+    }
+
+    /**
+     * @return The label to show for the shortcut. This includes the chording
+     *         key (for example 'Menu+a'). Also, any non-human readable
+     *         characters should be human readable (for example 'Menu+enter').
+     */
+    String getShortcutLabel() {
+
+        char shortcut = getShortcut();
+        if (shortcut == 0) {
+            return "";
+        }
+
+        StringBuilder sb = new StringBuilder(sPrependShortcutLabel);
+        switch (shortcut) {
+
+            case '\n':
+                sb.append(sEnterShortcutLabel);
+                break;
+
+            case '\b':
+                sb.append(sDeleteShortcutLabel);
+                break;
+
+            case ' ':
+                sb.append(sSpaceShortcutLabel);
+                break;
+
+            default:
+                sb.append(shortcut);
+                break;
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * @return Whether this menu item should be showing shortcuts (depends on
+     *         whether the menu should show shortcuts and whether this item has
+     *         a shortcut defined)
+     */
+    boolean shouldShowShortcut() {
+        // Show shortcuts if the menu is supposed to show shortcuts AND this item has a shortcut
+        return mMenu.isShortcutsVisible() && (getShortcut() != 0);
+    }
+
+    public SubMenu getSubMenu() {
+        return mSubMenu;
+    }
+
+    public boolean hasSubMenu() {
+        return mSubMenu != null;
+    }
+
+    void setSubMenu(SubMenuBuilder subMenu) {
+        mSubMenu = subMenu;
+
+        subMenu.setHeaderTitle(getTitle());
+    }
+
+    @ViewDebug.CapturedViewProperty
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Gets the title for a particular {@link ItemView}
+     *
+     * @param itemView The ItemView that is receiving the title
+     * @return Either the title or condensed title based on what the ItemView
+     *         prefers
+     */
+    CharSequence getTitleForItemView(MenuView.ItemView itemView) {
+        return ((itemView != null) && itemView.prefersCondensedTitle())
+                ? getTitleCondensed()
+                : getTitle();
+    }
+
+    public MenuItem setTitle(CharSequence title) {
+        mTitle = title;
+
+        mMenu.onItemsChanged(false);
+
+        if (mSubMenu != null) {
+            mSubMenu.setHeaderTitle(title);
+        }
+
+        return this;
+    }
+
+    public MenuItem setTitle(int title) {
+        return setTitle(mMenu.getContext().getString(title));
+    }
+
+    public CharSequence getTitleCondensed() {
+        return mTitleCondensed != null ? mTitleCondensed : mTitle;
+    }
+
+    public MenuItem setTitleCondensed(CharSequence title) {
+        mTitleCondensed = title;
+
+        // Could use getTitle() in the loop below, but just cache what it would do here
+        if (title == null) {
+            title = mTitle;
+        }
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    public Drawable getIcon() {
+        if (mIconDrawable != null) {
+            return applyIconTintIfNecessary(mIconDrawable);
+        }
+
+        if (mIconResId != NO_ICON) {
+            Drawable icon =  mMenu.getContext().getDrawable(mIconResId);
+            mIconResId = NO_ICON;
+            mIconDrawable = icon;
+            return applyIconTintIfNecessary(icon);
+        }
+
+        return null;
+    }
+
+    public MenuItem setIcon(Drawable icon) {
+        mIconResId = NO_ICON;
+        mIconDrawable = icon;
+        mNeedToApplyIconTint = true;
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    public MenuItem setIcon(int iconResId) {
+        mIconDrawable = null;
+        mIconResId = iconResId;
+        mNeedToApplyIconTint = true;
+
+        // If we have a view, we need to push the Drawable to them
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public MenuItem setIconTintList(@Nullable ColorStateList iconTintList) {
+        mIconTintList = iconTintList;
+        mHasIconTint = true;
+        mNeedToApplyIconTint = true;
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Nullable
+    @Override
+    public ColorStateList getIconTintList() {
+        return mIconTintList;
+    }
+
+    @Override
+    public MenuItem setIconTintMode(PorterDuff.Mode iconTintMode) {
+        mIconTintMode = iconTintMode;
+        mHasIconTintMode = true;
+        mNeedToApplyIconTint = true;
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Nullable
+    @Override
+    public PorterDuff.Mode getIconTintMode() {
+        return mIconTintMode;
+    }
+
+    private Drawable applyIconTintIfNecessary(Drawable icon) {
+        if (icon != null && mNeedToApplyIconTint && (mHasIconTint || mHasIconTintMode)) {
+            icon = icon.mutate();
+
+            if (mHasIconTint) {
+                icon.setTintList(mIconTintList);
+            }
+
+            if (mHasIconTintMode) {
+                icon.setTintMode(mIconTintMode);
+            }
+
+            mNeedToApplyIconTint = false;
+        }
+
+        return icon;
+    }
+
+    public boolean isCheckable() {
+        return (mFlags & CHECKABLE) == CHECKABLE;
+    }
+
+    public MenuItem setCheckable(boolean checkable) {
+        final int oldFlags = mFlags;
+        mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
+        if (oldFlags != mFlags) {
+            mMenu.onItemsChanged(false);
+        }
+
+        return this;
+    }
+
+    public void setExclusiveCheckable(boolean exclusive) {
+        mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
+    }
+
+    public boolean isExclusiveCheckable() {
+        return (mFlags & EXCLUSIVE) != 0;
+    }
+
+    public boolean isChecked() {
+        return (mFlags & CHECKED) == CHECKED;
+    }
+
+    public MenuItem setChecked(boolean checked) {
+        if ((mFlags & EXCLUSIVE) != 0) {
+            // Call the method on the Menu since it knows about the others in this
+            // exclusive checkable group
+            mMenu.setExclusiveItemChecked(this);
+        } else {
+            setCheckedInt(checked);
+        }
+
+        return this;
+    }
+
+    void setCheckedInt(boolean checked) {
+        final int oldFlags = mFlags;
+        mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
+        if (oldFlags != mFlags) {
+            mMenu.onItemsChanged(false);
+        }
+    }
+
+    public boolean isVisible() {
+        if (mActionProvider != null && mActionProvider.overridesItemVisibility()) {
+            return (mFlags & HIDDEN) == 0 && mActionProvider.isVisible();
+        }
+        return (mFlags & HIDDEN) == 0;
+    }
+
+    /**
+     * Changes the visibility of the item. This method DOES NOT notify the
+     * parent menu of a change in this item, so this should only be called from
+     * methods that will eventually trigger this change.  If unsure, use {@link #setVisible(boolean)}
+     * instead.
+     *
+     * @param shown Whether to show (true) or hide (false).
+     * @return Whether the item's shown state was changed
+     */
+    boolean setVisibleInt(boolean shown) {
+        final int oldFlags = mFlags;
+        mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN);
+        return oldFlags != mFlags;
+    }
+
+    public MenuItem setVisible(boolean shown) {
+        // Try to set the shown state to the given state. If the shown state was changed
+        // (i.e. the previous state isn't the same as given state), notify the parent menu that
+        // the shown state has changed for this item
+        if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this);
+
+        return this;
+    }
+
+   public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) {
+        mClickListener = clickListener;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return mTitle != null ? mTitle.toString() : null;
+    }
+
+    void setMenuInfo(ContextMenuInfo menuInfo) {
+        mMenuInfo = menuInfo;
+    }
+
+    public ContextMenuInfo getMenuInfo() {
+        return mMenuInfo;
+    }
+
+    public void actionFormatChanged() {
+        mMenu.onItemActionRequestChanged(this);
+    }
+
+    /**
+     * @return Whether the menu should show icons for menu items.
+     */
+    public boolean shouldShowIcon() {
+        return mMenu.getOptionalIconsVisible();
+    }
+
+    public boolean isActionButton() {
+        return (mFlags & IS_ACTION) == IS_ACTION;
+    }
+
+    public boolean requestsActionButton() {
+        return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM;
+    }
+
+    public boolean requiresActionButton() {
+        return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS;
+    }
+
+    @Override
+    public boolean requiresOverflow() {
+        return (mShowAsAction & SHOW_AS_OVERFLOW_ALWAYS) == SHOW_AS_OVERFLOW_ALWAYS;
+    }
+
+    public void setIsActionButton(boolean isActionButton) {
+        if (isActionButton) {
+            mFlags |= IS_ACTION;
+        } else {
+            mFlags &= ~IS_ACTION;
+        }
+    }
+
+    public boolean showsTextAsAction() {
+        return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT;
+    }
+
+    public void setShowAsAction(int actionEnum) {
+        switch (actionEnum & SHOW_AS_ACTION_MASK) {
+            case SHOW_AS_ACTION_ALWAYS:
+            case SHOW_AS_ACTION_IF_ROOM:
+            case SHOW_AS_ACTION_NEVER:
+                // Looks good!
+                break;
+
+            default:
+                // Mutually exclusive options selected!
+                throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM,"
+                        + " and SHOW_AS_ACTION_NEVER are mutually exclusive.");
+        }
+        mShowAsAction = actionEnum;
+        mMenu.onItemActionRequestChanged(this);
+    }
+
+    public MenuItem setActionView(View view) {
+        mActionView = view;
+        mActionProvider = null;
+        if (view != null && view.getId() == View.NO_ID && mId > 0) {
+            view.setId(mId);
+        }
+        mMenu.onItemActionRequestChanged(this);
+        return this;
+    }
+
+    public MenuItem setActionView(int resId) {
+        final Context context = mMenu.getContext();
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        setActionView(inflater.inflate(resId, new LinearLayout(context), false));
+        return this;
+    }
+
+    public View getActionView() {
+        if (mActionView != null) {
+            return mActionView;
+        } else if (mActionProvider != null) {
+            mActionView = mActionProvider.onCreateActionView(this);
+            return mActionView;
+        } else {
+            return null;
+        }
+    }
+
+    public ActionProvider getActionProvider() {
+        return mActionProvider;
+    }
+
+    public MenuItem setActionProvider(ActionProvider actionProvider) {
+        if (mActionProvider != null) {
+            mActionProvider.reset();
+        }
+        mActionView = null;
+        mActionProvider = actionProvider;
+        mMenu.onItemsChanged(true); // Measurement can be changed
+        if (mActionProvider != null) {
+            mActionProvider.setVisibilityListener(new ActionProvider.VisibilityListener() {
+                @Override public void onActionProviderVisibilityChanged(boolean isVisible) {
+                    mMenu.onItemVisibleChanged(MenuItemImpl.this);
+                }
+            });
+        }
+        return this;
+    }
+
+    @Override
+    public MenuItem setShowAsActionFlags(int actionEnum) {
+        setShowAsAction(actionEnum);
+        return this;
+    }
+
+    @Override
+    public boolean expandActionView() {
+        if (!hasCollapsibleActionView()) {
+            return false;
+        }
+
+        if (mOnActionExpandListener == null ||
+                mOnActionExpandListener.onMenuItemActionExpand(this)) {
+            return mMenu.expandItemActionView(this);
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean collapseActionView() {
+        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) {
+            return false;
+        }
+        if (mActionView == null) {
+            // We're already collapsed if we have no action view.
+            return true;
+        }
+
+        if (mOnActionExpandListener == null ||
+                mOnActionExpandListener.onMenuItemActionCollapse(this)) {
+            return mMenu.collapseItemActionView(this);
+        }
+
+        return false;
+    }
+
+    @Override
+    public MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
+        mOnActionExpandListener = listener;
+        return this;
+    }
+
+    public boolean hasCollapsibleActionView() {
+        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0) {
+            if (mActionView == null && mActionProvider != null) {
+                mActionView = mActionProvider.onCreateActionView(this);
+            }
+            return mActionView != null;
+        }
+        return false;
+    }
+
+    public void setActionViewExpanded(boolean isExpanded) {
+        mIsActionViewExpanded = isExpanded;
+        mMenu.onItemsChanged(false);
+    }
+
+    public boolean isActionViewExpanded() {
+        return mIsActionViewExpanded;
+    }
+
+    @Override
+    public MenuItem setContentDescription(CharSequence contentDescription) {
+        mContentDescription = contentDescription;
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public CharSequence getContentDescription() {
+        return mContentDescription;
+    }
+
+    @Override
+    public MenuItem setTooltipText(CharSequence tooltipText) {
+        mTooltipText = tooltipText;
+
+        mMenu.onItemsChanged(false);
+
+        return this;
+    }
+
+    @Override
+    public CharSequence getTooltipText() {
+        return mTooltipText;
+    }
+}
diff --git a/com/android/internal/view/menu/MenuPopup.java b/com/android/internal/view/menu/MenuPopup.java
new file mode 100644
index 0000000..10bd66f
--- /dev/null
+++ b/com/android/internal/view/menu/MenuPopup.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2015 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.internal.view.menu;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ListAdapter;
+import android.widget.PopupWindow;
+
+/**
+ * Base class for a menu popup abstraction - i.e., some type of menu, housed in a popup window
+ * environment.
+ *
+ * @hide
+ */
+public abstract class MenuPopup implements ShowableListMenu, MenuPresenter,
+        AdapterView.OnItemClickListener {
+    private Rect mEpicenterBounds;
+
+    public abstract void setForceShowIcon(boolean forceShow);
+
+    /**
+     * Adds the given menu to the popup, if it is capable of displaying submenus within itself.
+     * If menu is the first menu shown, it won't be displayed until show() is called.
+     * If the popup was already showing, adding a submenu via this method will cause that new
+     * submenu to be shown immediately (that is, if this MenuPopup implementation is capable of
+     * showing its own submenus).
+     *
+     * @param menu
+     */
+    public abstract void addMenu(MenuBuilder menu);
+
+    public abstract void setGravity(int dropDownGravity);
+
+    public abstract void setAnchorView(View anchor);
+
+    public abstract void setHorizontalOffset(int x);
+
+    public abstract void setVerticalOffset(int y);
+
+    /**
+     * Specifies the anchor-relative bounds of the popup's transition
+     * epicenter.
+     *
+     * @param bounds anchor-relative bounds
+     */
+    public void setEpicenterBounds(Rect bounds) {
+        mEpicenterBounds = bounds;
+    }
+
+    /**
+     * @return anchor-relative bounds of the popup's transition epicenter
+     */
+    public Rect getEpicenterBounds() {
+        return mEpicenterBounds;
+    }
+
+    /**
+     * Set whether a title entry should be shown in the popup menu (if a title exists for the
+     * menu).
+     *
+     * @param showTitle
+     */
+    public abstract void setShowTitle(boolean showTitle);
+
+    /**
+     * Set a listener to receive a callback when the popup is dismissed.
+     *
+     * @param listener Listener that will be notified when the popup is dismissed.
+     */
+    public abstract void setOnDismissListener(PopupWindow.OnDismissListener listener);
+
+    @Override
+    public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+        // Don't need to do anything; we added as a presenter in the constructor.
+    }
+
+    @Override
+    public MenuView getMenuView(ViewGroup root) {
+        throw new UnsupportedOperationException("MenuPopups manage their own views");
+    }
+
+    @Override
+    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+        return false;
+    }
+
+    @Override
+    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+        return false;
+    }
+
+    @Override
+    public int getId() {
+        return 0;
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        ListAdapter outerAdapter = (ListAdapter) parent.getAdapter();
+        MenuAdapter wrappedAdapter = toMenuAdapter(outerAdapter);
+
+        // Use the position from the outer adapter so that if a header view was added, we don't get
+        // an off-by-1 error in position.
+        wrappedAdapter.mAdapterMenu.performItemAction((MenuItem) outerAdapter.getItem(position), 0);
+    }
+
+    /**
+     * Measures the width of the given menu view.
+     *
+     * @param view The view to measure.
+     * @return The width.
+     */
+    protected static int measureIndividualMenuWidth(ListAdapter adapter, ViewGroup parent,
+            Context context, int maxAllowedWidth) {
+        // Menus don't tend to be long, so this is more sane than it looks.
+        int maxWidth = 0;
+        View itemView = null;
+        int itemType = 0;
+
+        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        final int count = adapter.getCount();
+        for (int i = 0; i < count; i++) {
+            final int positionType = adapter.getItemViewType(i);
+            if (positionType != itemType) {
+                itemType = positionType;
+                itemView = null;
+            }
+
+            if (parent == null) {
+                parent = new FrameLayout(context);
+            }
+
+            itemView = adapter.getView(i, itemView, parent);
+            itemView.measure(widthMeasureSpec, heightMeasureSpec);
+
+            final int itemWidth = itemView.getMeasuredWidth();
+            if (itemWidth >= maxAllowedWidth) {
+                return maxAllowedWidth;
+            } else if (itemWidth > maxWidth) {
+                maxWidth = itemWidth;
+            }
+        }
+
+        return maxWidth;
+    }
+
+    /**
+     * Converts the given ListAdapter originating from a menu, to a MenuAdapter, accounting for
+     * the possibility of the parameter adapter actually wrapping the MenuAdapter. (That could
+     * happen if a header view was added on the menu.)
+     *
+     * @param adapter
+     * @return
+     */
+    protected static MenuAdapter toMenuAdapter(ListAdapter adapter) {
+        if (adapter instanceof HeaderViewListAdapter) {
+            return (MenuAdapter) ((HeaderViewListAdapter) adapter).getWrappedAdapter();
+        }
+        return (MenuAdapter) adapter;
+    }
+
+    /**
+     * Returns whether icon spacing needs to be preserved for the given menu, based on whether any
+     * of its items contains an icon.
+     *
+     * NOTE: This should only be used for non-overflow-only menus, because this method does not
+     * take into account whether the menu items are being shown as part of the popup or or being
+     * shown as actions in the action bar.
+     *
+     * @param menu
+     * @return Whether to preserve icon spacing.
+     */
+    protected static boolean shouldPreserveIconSpacing(MenuBuilder menu) {
+      boolean preserveIconSpacing = false;
+      final int count = menu.size();
+
+      for (int i = 0; i < count; i++) {
+          MenuItem childItem = menu.getItem(i);
+          if (childItem.isVisible() && childItem.getIcon() != null) {
+              preserveIconSpacing = true;
+              break;
+          }
+      }
+
+      return preserveIconSpacing;
+    }
+}
diff --git a/com/android/internal/view/menu/MenuPopupHelper.java b/com/android/internal/view/menu/MenuPopupHelper.java
new file mode 100644
index 0000000..6af41a5
--- /dev/null
+++ b/com/android/internal/view/menu/MenuPopupHelper.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2010 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.internal.view.menu;
+
+import com.android.internal.view.menu.MenuPresenter.Callback;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.PopupWindow.OnDismissListener;
+
+/**
+ * Presents a menu as a small, simple popup anchored to another view.
+ */
+public class MenuPopupHelper implements MenuHelper {
+    private static final int TOUCH_EPICENTER_SIZE_DP = 48;
+
+    private final Context mContext;
+
+    // Immutable cached popup menu properties.
+    private final MenuBuilder mMenu;
+    private final boolean mOverflowOnly;
+    private final int mPopupStyleAttr;
+    private final int mPopupStyleRes;
+
+    // Mutable cached popup menu properties.
+    private View mAnchorView;
+    private int mDropDownGravity = Gravity.START;
+    private boolean mForceShowIcon;
+    private Callback mPresenterCallback;
+
+    private MenuPopup mPopup;
+    private OnDismissListener mOnDismissListener;
+
+    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
+        this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
+    }
+
+    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
+            @NonNull View anchorView) {
+        this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
+    }
+
+    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
+            @NonNull View anchorView,
+            boolean overflowOnly, @AttrRes int popupStyleAttr) {
+        this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
+    }
+
+    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
+            @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
+            @StyleRes int popupStyleRes) {
+        mContext = context;
+        mMenu = menu;
+        mAnchorView = anchorView;
+        mOverflowOnly = overflowOnly;
+        mPopupStyleAttr = popupStyleAttr;
+        mPopupStyleRes = popupStyleRes;
+    }
+
+    public void setOnDismissListener(@Nullable OnDismissListener listener) {
+        mOnDismissListener = listener;
+    }
+
+    /**
+      * Sets the view to which the popup window is anchored.
+      * <p>
+      * Changes take effect on the next call to show().
+      *
+      * @param anchor the view to which the popup window should be anchored
+      */
+    public void setAnchorView(@NonNull View anchor) {
+        mAnchorView = anchor;
+    }
+
+    /**
+     * Sets whether the popup menu's adapter is forced to show icons in the
+     * menu item views.
+     * <p>
+     * Changes take effect on the next call to show().
+     *
+     * @param forceShowIcon {@code true} to force icons to be shown, or
+     *                  {@code false} for icons to be optionally shown
+     */
+    public void setForceShowIcon(boolean forceShowIcon) {
+        mForceShowIcon = forceShowIcon;
+        if (mPopup != null) {
+            mPopup.setForceShowIcon(forceShowIcon);
+        }
+    }
+
+    /**
+      * Sets the alignment of the popup window relative to the anchor view.
+      * <p>
+      * Changes take effect on the next call to show().
+      *
+      * @param gravity alignment of the popup relative to the anchor
+      */
+    public void setGravity(int gravity) {
+        mDropDownGravity = gravity;
+    }
+
+    /**
+     * @return alignment of the popup relative to the anchor
+     */
+    public int getGravity() {
+        return mDropDownGravity;
+    }
+
+    public void show() {
+        if (!tryShow()) {
+            throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
+        }
+    }
+
+    public void show(int x, int y) {
+        if (!tryShow(x, y)) {
+            throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
+        }
+    }
+
+    @NonNull
+    public MenuPopup getPopup() {
+        if (mPopup == null) {
+            mPopup = createPopup();
+        }
+        return mPopup;
+    }
+
+    /**
+     * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
+     *
+     * @return {@code true} if the popup was shown or was already showing prior to calling this
+     *         method, {@code false} otherwise
+     */
+    public boolean tryShow() {
+        if (isShowing()) {
+            return true;
+        }
+
+        if (mAnchorView == null) {
+            return false;
+        }
+
+        showPopup(0, 0, false, false);
+        return true;
+    }
+
+    /**
+     * Shows the popup menu and makes a best-effort to anchor it to the
+     * specified (x,y) coordinate relative to the anchor view.
+     * <p>
+     * Additionally, the popup's transition epicenter (see
+     * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
+     * centered on the specified coordinate, rather than using the bounds of
+     * the anchor view.
+     * <p>
+     * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
+     * display the popup with its top-left corner at (x,y) relative to the
+     * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
+     * popup's top-right corner will be at (x,y).
+     * <p>
+     * If the popup cannot be displayed fully on-screen, this method will
+     * attempt to scroll the anchor view's ancestors and/or offset the popup
+     * such that it may be displayed fully on-screen.
+     *
+     * @param x x coordinate relative to the anchor view
+     * @param y y coordinate relative to the anchor view
+     * @return {@code true} if the popup was shown or was already showing prior
+     *         to calling this method, {@code false} otherwise
+     */
+    public boolean tryShow(int x, int y) {
+        if (isShowing()) {
+            return true;
+        }
+
+        if (mAnchorView == null) {
+            return false;
+        }
+
+        showPopup(x, y, true, true);
+        return true;
+    }
+
+    /**
+     * Creates the popup and assigns cached properties.
+     *
+     * @return an initialized popup
+     */
+    @NonNull
+    private MenuPopup createPopup() {
+        final WindowManager windowManager = (WindowManager) mContext.getSystemService(
+            Context.WINDOW_SERVICE);
+        final Display display = windowManager.getDefaultDisplay();
+        final Point displaySize = new Point();
+        display.getRealSize(displaySize);
+
+        final int smallestWidth = Math.min(displaySize.x, displaySize.y);
+        final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
+            com.android.internal.R.dimen.cascading_menus_min_smallest_width);
+        final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading;
+
+        final MenuPopup popup;
+        if (enableCascadingSubmenus) {
+            popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
+                    mPopupStyleRes, mOverflowOnly);
+        } else {
+            popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
+                    mPopupStyleRes, mOverflowOnly);
+        }
+
+        // Assign immutable properties.
+        popup.addMenu(mMenu);
+        popup.setOnDismissListener(mInternalOnDismissListener);
+
+        // Assign mutable properties. These may be reassigned later.
+        popup.setAnchorView(mAnchorView);
+        popup.setCallback(mPresenterCallback);
+        popup.setForceShowIcon(mForceShowIcon);
+        popup.setGravity(mDropDownGravity);
+
+        return popup;
+    }
+
+    private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
+        final MenuPopup popup = getPopup();
+        popup.setShowTitle(showTitle);
+
+        if (useOffsets) {
+            // If the resolved drop-down gravity is RIGHT, the popup's right
+            // edge will be aligned with the anchor view. Adjust by the anchor
+            // width such that the top-right corner is at the X offset.
+            final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
+                    mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
+            if (hgrav == Gravity.RIGHT) {
+                xOffset += mAnchorView.getWidth();
+            }
+
+            popup.setHorizontalOffset(xOffset);
+            popup.setVerticalOffset(yOffset);
+
+            // Set the transition epicenter to be roughly finger (or mouse
+            // cursor) sized and centered around the offset position. This
+            // will give the appearance that the window is emerging from
+            // the touch point.
+            final float density = mContext.getResources().getDisplayMetrics().density;
+            final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
+            final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
+                    xOffset + halfSize, yOffset + halfSize);
+            popup.setEpicenterBounds(epicenter);
+        }
+
+        popup.show();
+    }
+
+    /**
+     * Dismisses the popup, if showing.
+     */
+    @Override
+    public void dismiss() {
+        if (isShowing()) {
+            mPopup.dismiss();
+        }
+    }
+
+    /**
+     * Called after the popup has been dismissed.
+     * <p>
+     * <strong>Note:</strong> Subclasses should call the super implementation
+     * last to ensure that any necessary tear down has occurred before the
+     * listener specified by {@link #setOnDismissListener(OnDismissListener)}
+     * is called.
+     */
+    protected void onDismiss() {
+        mPopup = null;
+
+        if (mOnDismissListener != null) {
+            mOnDismissListener.onDismiss();
+        }
+    }
+
+    public boolean isShowing() {
+        return mPopup != null && mPopup.isShowing();
+    }
+
+    @Override
+    public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
+        mPresenterCallback = cb;
+        if (mPopup != null) {
+            mPopup.setCallback(cb);
+        }
+    }
+
+    /**
+     * Listener used to proxy dismiss callbacks to the helper's owner.
+     */
+    private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
+        @Override
+        public void onDismiss() {
+            MenuPopupHelper.this.onDismiss();
+        }
+    };
+}
diff --git a/com/android/internal/view/menu/MenuPresenter.java b/com/android/internal/view/menu/MenuPresenter.java
new file mode 100644
index 0000000..65bdc09
--- /dev/null
+++ b/com/android/internal/view/menu/MenuPresenter.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2011 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.internal.view.menu;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Parcelable;
+import android.view.ViewGroup;
+
+/**
+ * A MenuPresenter is responsible for building views for a Menu object.
+ * It takes over some responsibility from the old style monolithic MenuBuilder class.
+ */
+public interface MenuPresenter {
+    /**
+     * Called by menu implementation to notify another component of open/close events.
+     */
+    public interface Callback {
+        /**
+         * Called when a menu is closing.
+         * @param menu
+         * @param allMenusAreClosing
+         */
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing);
+
+        /**
+         * Called when a submenu opens. Useful for notifying the application
+         * of menu state so that it does not attempt to hide the action bar
+         * while a submenu is open or similar.
+         *
+         * @param subMenu Submenu currently being opened
+         * @return true if the Callback will handle presenting the submenu, false if
+         *         the presenter should attempt to do so.
+         */
+        public boolean onOpenSubMenu(MenuBuilder subMenu);
+    }
+
+    /**
+     * Initializes this presenter for the given context and menu.
+     * <p>
+     * This method is called by MenuBuilder when a presenter is added. See
+     * {@link MenuBuilder#addMenuPresenter(MenuPresenter)}.
+     *
+     * @param context the context for this presenter; used for view creation
+     *                and resource management, must be non-{@code null}
+     * @param menu the menu to host, or {@code null} to clear the hosted menu
+     */
+    public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu);
+
+    /**
+     * Retrieve a MenuView to display the menu specified in
+     * {@link #initForMenu(Context, MenuBuilder)}.
+     *
+     * @param root Intended parent of the MenuView.
+     * @return A freshly created MenuView.
+     */
+    public MenuView getMenuView(ViewGroup root);
+
+    /**
+     * Update the menu UI in response to a change. Called by
+     * MenuBuilder during the normal course of operation.
+     *
+     * @param cleared true if the menu was entirely cleared
+     */
+    public void updateMenuView(boolean cleared);
+
+    /**
+     * Set a callback object that will be notified of menu events
+     * related to this specific presentation.
+     * @param cb Callback that will be notified of future events
+     */
+    public void setCallback(Callback cb);
+
+    /**
+     * Called by Menu implementations to indicate that a submenu item
+     * has been selected. An active Callback should be notified, and
+     * if applicable the presenter should present the submenu.
+     *
+     * @param subMenu SubMenu being opened
+     * @return true if the the event was handled, false otherwise.
+     */
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu);
+
+    /**
+     * Called by Menu implementations to indicate that a menu or submenu is
+     * closing. Presenter implementations should close the representation
+     * of the menu indicated as necessary and notify a registered callback.
+     *
+     * @param menu the menu or submenu that is closing
+     * @param allMenusAreClosing {@code true} if all displayed menus and
+     *                           submenus are closing, {@code false} if only
+     *                           the specified menu is closing
+     */
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing);
+
+    /**
+     * Called by Menu implementations to flag items that will be shown as actions.
+     * @return true if this presenter changed the action status of any items.
+     */
+    public boolean flagActionItems();
+
+    /**
+     * Called when a menu item with a collapsable action view should expand its action view.
+     *
+     * @param menu Menu containing the item to be expanded
+     * @param item Item to be expanded
+     * @return true if this presenter expanded the action view, false otherwise.
+     */
+    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item);
+
+    /**
+     * Called when a menu item with a collapsable action view should collapse its action view.
+     *
+     * @param menu Menu containing the item to be collapsed
+     * @param item Item to be collapsed
+     * @return true if this presenter collapsed the action view, false otherwise.
+     */
+    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item);
+
+    /**
+     * Returns an ID for determining how to save/restore instance state.
+     * @return a valid ID value.
+     */
+    public int getId();
+
+    /**
+     * Returns a Parcelable describing the current state of the presenter.
+     * It will be passed to the {@link #onRestoreInstanceState(Parcelable)}
+     * method of the presenter sharing the same ID later.
+     * @return The saved instance state
+     */
+    public Parcelable onSaveInstanceState();
+
+    /**
+     * Supplies the previously saved instance state to be restored.
+     * @param state The previously saved instance state
+     */
+    public void onRestoreInstanceState(Parcelable state);
+}
diff --git a/com/android/internal/view/menu/MenuView.java b/com/android/internal/view/menu/MenuView.java
new file mode 100644
index 0000000..407caae
--- /dev/null
+++ b/com/android/internal/view/menu/MenuView.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Minimal interface for a menu view.  {@link #initialize(MenuBuilder)} must be called for the
+ * menu to be functional.
+ * 
+ * @hide
+ */
+public interface MenuView {
+    /**
+     * Initializes the menu to the given menu. This should be called after the
+     * view is inflated.
+     * 
+     * @param menu The menu that this MenuView should display.
+     */
+    public void initialize(MenuBuilder menu);
+
+    /**
+     * Returns the default animations to be used for this menu when entering/exiting.
+     * @return A resource ID for the default animations to be used for this menu.
+     */
+    public int getWindowAnimations();
+    
+    /**
+     * Minimal interface for a menu item view.  {@link #initialize(MenuItemImpl, int)} must be called
+     * for the item to be functional.
+     */
+    public interface ItemView {
+        /**
+         * Initializes with the provided MenuItemData.  This should be called after the view is
+         * inflated.
+         * @param itemData The item that this ItemView should display.
+         * @param menuType The type of this menu, one of 
+         *            {@link MenuBuilder#TYPE_ICON}, {@link MenuBuilder#TYPE_EXPANDED},
+         *            {@link MenuBuilder#TYPE_DIALOG}).
+         */
+        public void initialize(MenuItemImpl itemData, int menuType);
+        
+        /**
+         * Gets the item data that this view is displaying.
+         * @return the item data, or null if there is not one
+         */
+        public MenuItemImpl getItemData();
+        
+        /**
+         * Sets the title of the item view.
+         * @param title The title to set.
+         */
+        public void setTitle(CharSequence title);
+
+        /**
+         * Sets the enabled state of the item view.
+         * @param enabled Whether the item view should be enabled.
+         */
+        public void setEnabled(boolean enabled);
+        
+        /**
+         * Displays the checkbox for the item view.  This does not ensure the item view will be
+         * checked, for that use {@link #setChecked}. 
+         * @param checkable Whether to display the checkbox or to hide it
+         */
+        public void setCheckable(boolean checkable);
+        
+        /**
+         * Checks the checkbox for the item view.  If the checkbox is hidden, it will NOT be
+         * made visible, call {@link #setCheckable(boolean)} for that.
+         * @param checked Whether the checkbox should be checked
+         */
+        public void setChecked(boolean checked);
+
+        /**
+         * Sets the shortcut for the item.
+         * @param showShortcut Whether a shortcut should be shown(if false, the value of 
+         * shortcutKey should be ignored).
+         * @param shortcutKey The shortcut key that should be shown on the ItemView.
+         */
+        public void setShortcut(boolean showShortcut, char shortcutKey);
+        
+        /**
+         * Set the icon of this item view.
+         * @param icon The icon of this item. null to hide the icon.
+         */
+        public void setIcon(Drawable icon);
+        
+        /**
+         * Whether this item view prefers displaying the condensed title rather
+         * than the normal title. If a condensed title is not available, the
+         * normal title will be used.
+         * 
+         * @return Whether this item view prefers displaying the condensed
+         *         title.
+         */
+        public boolean prefersCondensedTitle();
+
+        /**
+         * Whether this item view shows an icon.
+         * 
+         * @return Whether this item view shows an icon.
+         */
+        public boolean showsIcon();
+    }
+}
diff --git a/com/android/internal/view/menu/ShowableListMenu.java b/com/android/internal/view/menu/ShowableListMenu.java
new file mode 100644
index 0000000..ca158fd
--- /dev/null
+++ b/com/android/internal/view/menu/ShowableListMenu.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.internal.view.menu;
+
+import android.widget.ListView;
+
+/**
+ * A list menu which can be shown and hidden and which is internally represented by a ListView.
+ */
+public interface ShowableListMenu {
+    public void show();
+
+    public void dismiss();
+
+    public boolean isShowing();
+
+    /**
+     * @return The internal ListView for the visible menu.
+     */
+    public ListView getListView();
+}
diff --git a/com/android/internal/view/menu/StandardMenuPopup.java b/com/android/internal/view/menu/StandardMenuPopup.java
new file mode 100644
index 0000000..d9ca5be
--- /dev/null
+++ b/com/android/internal/view/menu/StandardMenuPopup.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2015 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.internal.view.menu;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Parcelable;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import android.widget.MenuPopupWindow;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.PopupWindow.OnDismissListener;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A standard menu popup in which when a submenu is opened, it replaces its parent menu in the
+ * viewport.
+ */
+final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener,
+        MenuPresenter, OnKeyListener {
+
+    private final Context mContext;
+
+    private final MenuBuilder mMenu;
+    private final MenuAdapter mAdapter;
+    private final boolean mOverflowOnly;
+    private final int mPopupMaxWidth;
+    private final int mPopupStyleAttr;
+    private final int mPopupStyleRes;
+    // The popup window is final in order to couple its lifecycle to the lifecycle of the
+    // StandardMenuPopup.
+    private final MenuPopupWindow mPopup;
+
+    private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() {
+        @Override
+        public void onGlobalLayout() {
+            // Only move the popup if it's showing and non-modal. We don't want
+            // to be moving around the only interactive window, since there's a
+            // good chance the user is interacting with it.
+            if (isShowing() && !mPopup.isModal()) {
+                final View anchor = mShownAnchorView;
+                if (anchor == null || !anchor.isShown()) {
+                    dismiss();
+                } else {
+                    // Recompute window size and position
+                    mPopup.show();
+                }
+            }
+        }
+    };
+
+    private final OnAttachStateChangeListener mAttachStateChangeListener =
+            new OnAttachStateChangeListener() {
+        @Override
+        public void onViewAttachedToWindow(View v) {
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {
+            if (mTreeObserver != null) {
+                if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver();
+                mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
+            }
+            v.removeOnAttachStateChangeListener(this);
+        }
+    };
+
+    private PopupWindow.OnDismissListener mOnDismissListener;
+
+    private View mAnchorView;
+    private View mShownAnchorView;
+    private Callback mPresenterCallback;
+    private ViewTreeObserver mTreeObserver;
+
+    /** Whether the popup has been dismissed. Once dismissed, it cannot be opened again. */
+    private boolean mWasDismissed;
+
+    /** Whether the cached content width value is valid. */
+    private boolean mHasContentWidth;
+
+    /** Cached content width. */
+    private int mContentWidth;
+
+    private int mDropDownGravity = Gravity.NO_GRAVITY;
+
+    private boolean mShowTitle;
+
+    public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr,
+            int popupStyleRes, boolean overflowOnly) {
+        mContext = Preconditions.checkNotNull(context);
+        mMenu = menu;
+        mOverflowOnly = overflowOnly;
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly);
+        mPopupStyleAttr = popupStyleAttr;
+        mPopupStyleRes = popupStyleRes;
+
+        final Resources res = context.getResources();
+        mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
+                res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
+
+        mAnchorView = anchorView;
+
+        mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes);
+
+        // Present the menu using our context, not the menu builder's context.
+        menu.addMenuPresenter(this, context);
+    }
+
+    @Override
+    public void setForceShowIcon(boolean forceShow) {
+        mAdapter.setForceShowIcon(forceShow);
+    }
+
+    @Override
+    public void setGravity(int gravity) {
+        mDropDownGravity = gravity;
+    }
+
+    private boolean tryShow() {
+        if (isShowing()) {
+            return true;
+        }
+
+        if (mWasDismissed || mAnchorView == null) {
+            return false;
+        }
+
+        mShownAnchorView = mAnchorView;
+
+        mPopup.setOnDismissListener(this);
+        mPopup.setOnItemClickListener(this);
+        mPopup.setAdapter(mAdapter);
+        mPopup.setModal(true);
+
+        final View anchor = mShownAnchorView;
+        final boolean addGlobalListener = mTreeObserver == null;
+        mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
+        if (addGlobalListener) {
+            mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
+        }
+        anchor.addOnAttachStateChangeListener(mAttachStateChangeListener);
+        mPopup.setAnchorView(anchor);
+        mPopup.setDropDownGravity(mDropDownGravity);
+
+        if (!mHasContentWidth) {
+            mContentWidth = measureIndividualMenuWidth(mAdapter, null, mContext, mPopupMaxWidth);
+            mHasContentWidth = true;
+        }
+
+        mPopup.setContentWidth(mContentWidth);
+        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+        mPopup.setEpicenterBounds(getEpicenterBounds());
+        mPopup.show();
+
+        ListView listView = mPopup.getListView();
+        listView.setOnKeyListener(this);
+
+        if (mShowTitle && mMenu.getHeaderTitle() != null) {
+            FrameLayout titleItemView =
+                    (FrameLayout) LayoutInflater.from(mContext).inflate(
+                            com.android.internal.R.layout.popup_menu_header_item_layout,
+                            listView,
+                            false);
+            TextView titleView = (TextView) titleItemView.findViewById(
+                    com.android.internal.R.id.title);
+            if (titleView != null) {
+                titleView.setText(mMenu.getHeaderTitle());
+            }
+            titleItemView.setEnabled(false);
+            listView.addHeaderView(titleItemView, null, false);
+
+            // Update to show the title.
+            mPopup.show();
+        }
+        return true;
+    }
+
+    @Override
+    public void show() {
+        if (!tryShow()) {
+            throw new IllegalStateException("StandardMenuPopup cannot be used without an anchor");
+        }
+    }
+
+    @Override
+    public void dismiss() {
+        if (isShowing()) {
+            mPopup.dismiss();
+        }
+    }
+
+    @Override
+    public void addMenu(MenuBuilder menu) {
+        // No-op: standard implementation has only one menu which is set in the constructor.
+    }
+
+    @Override
+    public boolean isShowing() {
+        return !mWasDismissed && mPopup.isShowing();
+    }
+
+    @Override
+    public void onDismiss() {
+        mWasDismissed = true;
+        mMenu.close();
+
+        if (mTreeObserver != null) {
+            if (!mTreeObserver.isAlive()) mTreeObserver = mShownAnchorView.getViewTreeObserver();
+            mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
+            mTreeObserver = null;
+        }
+        mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
+
+        if (mOnDismissListener != null) {
+            mOnDismissListener.onDismiss();
+        }
+    }
+
+    @Override
+    public void updateMenuView(boolean cleared) {
+        mHasContentWidth = false;
+
+        if (mAdapter != null) {
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    public void setCallback(Callback cb) {
+        mPresenterCallback = cb;
+    }
+
+    @Override
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+        if (subMenu.hasVisibleItems()) {
+            final MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu,
+                    mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes);
+            subPopup.setPresenterCallback(mPresenterCallback);
+            subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu));
+            subPopup.setGravity(mDropDownGravity);
+
+            // Pass responsibility for handling onDismiss to the submenu.
+            subPopup.setOnDismissListener(mOnDismissListener);
+            mOnDismissListener = null;
+
+            // Close this menu popup to make room for the submenu popup.
+            mMenu.close(false /* closeAllMenus */);
+
+            // Show the new sub-menu popup at the same location as this popup.
+            final int horizontalOffset = mPopup.getHorizontalOffset();
+            final int verticalOffset = mPopup.getVerticalOffset();
+            if (subPopup.tryShow(horizontalOffset, verticalOffset)) {
+                if (mPresenterCallback != null) {
+                    mPresenterCallback.onOpenSubMenu(subMenu);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        // Only care about the (sub)menu we're presenting.
+        if (menu != mMenu) return;
+
+        dismiss();
+        if (mPresenterCallback != null) {
+            mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
+        }
+    }
+
+    @Override
+    public boolean flagActionItems() {
+        return false;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        return null;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+    }
+
+    @Override
+    public void setAnchorView(View anchor) {
+        mAnchorView = anchor;
+    }
+
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
+            dismiss();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void setOnDismissListener(OnDismissListener listener) {
+        mOnDismissListener = listener;
+    }
+
+    @Override
+    public ListView getListView() {
+        return mPopup.getListView();
+    }
+
+
+    @Override
+    public void setHorizontalOffset(int x) {
+        mPopup.setHorizontalOffset(x);
+    }
+
+    @Override
+    public void setVerticalOffset(int y) {
+        mPopup.setVerticalOffset(y);
+    }
+
+    @Override
+    public void setShowTitle(boolean showTitle) {
+        mShowTitle = showTitle;
+    }
+}
diff --git a/com/android/internal/view/menu/SubMenuBuilder.java b/com/android/internal/view/menu/SubMenuBuilder.java
new file mode 100644
index 0000000..897440e
--- /dev/null
+++ b/com/android/internal/view/menu/SubMenuBuilder.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2006 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.internal.view.menu;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+
+/**
+ * The model for a sub menu, which is an extension of the menu.  Most methods are proxied to
+ * the parent menu.
+ */
+public class SubMenuBuilder extends MenuBuilder implements SubMenu {
+    private MenuBuilder mParentMenu;
+    private MenuItemImpl mItem;
+    
+    public SubMenuBuilder(Context context, MenuBuilder parentMenu, MenuItemImpl item) {
+        super(context);
+
+        mParentMenu = parentMenu;
+        mItem = item;
+    }
+
+    @Override
+    public void setQwertyMode(boolean isQwerty) {
+        mParentMenu.setQwertyMode(isQwerty);
+    }
+
+    @Override
+    public boolean isQwertyMode() {
+        return mParentMenu.isQwertyMode();
+    }
+    
+    @Override
+    public void setShortcutsVisible(boolean shortcutsVisible) {
+        mParentMenu.setShortcutsVisible(shortcutsVisible);
+    }
+
+    @Override
+    public boolean isShortcutsVisible() {
+        return mParentMenu.isShortcutsVisible();
+    }
+
+    public Menu getParentMenu() {
+        return mParentMenu;
+    }
+
+    public MenuItem getItem() {
+        return mItem;
+    }
+
+    @Override
+    public void setCallback(Callback callback) {
+        mParentMenu.setCallback(callback);
+    }
+
+    @Override
+    public MenuBuilder getRootMenu() {
+        return mParentMenu.getRootMenu();
+    }
+
+    @Override
+    boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
+        return super.dispatchMenuItemSelected(menu, item) ||
+                mParentMenu.dispatchMenuItemSelected(menu, item);
+    }
+
+    public SubMenu setIcon(Drawable icon) {
+        mItem.setIcon(icon);
+        return this;
+    }
+
+    public SubMenu setIcon(int iconRes) {
+        mItem.setIcon(iconRes);
+        return this;
+    }
+
+    public SubMenu setHeaderIcon(Drawable icon) {
+        return (SubMenu) super.setHeaderIconInt(icon);
+    }
+
+    public SubMenu setHeaderIcon(int iconRes) {
+        return (SubMenu) super.setHeaderIconInt(iconRes);
+    }
+
+    public SubMenu setHeaderTitle(CharSequence title) {
+        return (SubMenu) super.setHeaderTitleInt(title);
+    }
+
+    public SubMenu setHeaderTitle(int titleRes) {
+        return (SubMenu) super.setHeaderTitleInt(titleRes);
+    }
+
+    public SubMenu setHeaderView(View view) {
+        return (SubMenu) super.setHeaderViewInt(view);
+    }
+
+    @Override
+    public boolean expandItemActionView(MenuItemImpl item) {
+        return mParentMenu.expandItemActionView(item);
+    }
+
+    @Override
+    public boolean collapseItemActionView(MenuItemImpl item) {
+        return mParentMenu.collapseItemActionView(item);
+    }
+
+    @Override
+    public String getActionViewStatesKey() {
+        final int itemId = mItem != null ? mItem.getItemId() : 0;
+        if (itemId == 0) {
+            return null;
+        }
+        return super.getActionViewStatesKey() + ":" + itemId;
+    }
+
+    @Override
+    public void setGroupDividerEnabled(boolean groupDividerEnabled) {
+        mParentMenu.setGroupDividerEnabled(groupDividerEnabled);
+    }
+
+    @Override
+    public boolean isGroupDividerEnabled() {
+        return mParentMenu.isGroupDividerEnabled();
+    }
+}
diff --git a/com/android/internal/widget/AbsActionBarView.java b/com/android/internal/widget/AbsActionBarView.java
new file mode 100644
index 0000000..582c4f1
--- /dev/null
+++ b/com/android/internal/widget/AbsActionBarView.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+import com.android.internal.R;
+
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.MotionEvent;
+import android.widget.ActionMenuPresenter;
+import android.widget.ActionMenuView;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+
+public abstract class AbsActionBarView extends ViewGroup {
+    private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator();
+
+    private static final int FADE_DURATION = 200;
+
+    protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener();
+
+    /** Context against which to inflate popup menus. */
+    protected final Context mPopupContext;
+
+    protected ActionMenuView mMenuView;
+    protected ActionMenuPresenter mActionMenuPresenter;
+    protected ViewGroup mSplitView;
+    protected boolean mSplitActionBar;
+    protected boolean mSplitWhenNarrow;
+    protected int mContentHeight;
+
+    protected Animator mVisibilityAnim;
+
+    private boolean mEatingTouch;
+    private boolean mEatingHover;
+
+    public AbsActionBarView(Context context) {
+        this(context, null);
+    }
+
+    public AbsActionBarView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AbsActionBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AbsActionBarView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedValue tv = new TypedValue();
+        if (context.getTheme().resolveAttribute(R.attr.actionBarPopupTheme, tv, true)
+                && tv.resourceId != 0) {
+            mPopupContext = new ContextThemeWrapper(context, tv.resourceId);
+        } else {
+            mPopupContext = context;
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        // Action bar can change size on configuration changes.
+        // Reread the desired height from the theme-specified style.
+        TypedArray a = getContext().obtainStyledAttributes(null, R.styleable.ActionBar,
+                com.android.internal.R.attr.actionBarStyle, 0);
+        setContentHeight(a.getLayoutDimension(R.styleable.ActionBar_height, 0));
+        a.recycle();
+        if (mSplitWhenNarrow) {
+            setSplitToolbar(getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.split_action_bar_is_narrow));
+        }
+        if (mActionMenuPresenter != null) {
+            mActionMenuPresenter.onConfigurationChanged(newConfig);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        // ActionBarViews always eat touch events, but should still respect the touch event dispatch
+        // contract. If the normal View implementation doesn't want the events, we'll just silently
+        // eat the rest of the gesture without reporting the events to the default implementation
+        // since that's what it expects.
+
+        final int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mEatingTouch = false;
+        }
+
+        if (!mEatingTouch) {
+            final boolean handled = super.onTouchEvent(ev);
+            if (action == MotionEvent.ACTION_DOWN && !handled) {
+                mEatingTouch = true;
+            }
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            mEatingTouch = false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent ev) {
+        // Same deal as onTouchEvent() above. Eat all hover events, but still
+        // respect the touch event dispatch contract.
+
+        final int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_HOVER_ENTER) {
+            mEatingHover = false;
+        }
+
+        if (!mEatingHover) {
+            final boolean handled = super.onHoverEvent(ev);
+            if (action == MotionEvent.ACTION_HOVER_ENTER && !handled) {
+                mEatingHover = true;
+            }
+        }
+
+        if (action == MotionEvent.ACTION_HOVER_EXIT
+                || action == MotionEvent.ACTION_CANCEL) {
+            mEatingHover = false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Sets whether the bar should be split right now, no questions asked.
+     * @param split true if the bar should split
+     */
+    public void setSplitToolbar(boolean split) {
+        mSplitActionBar = split;
+    }
+
+    /**
+     * Sets whether the bar should split if we enter a narrow screen configuration.
+     * @param splitWhenNarrow true if the bar should check to split after a config change
+     */
+    public void setSplitWhenNarrow(boolean splitWhenNarrow) {
+        mSplitWhenNarrow = splitWhenNarrow;
+    }
+
+    public void setContentHeight(int height) {
+        mContentHeight = height;
+        requestLayout();
+    }
+
+    public int getContentHeight() {
+        return mContentHeight;
+    }
+
+    public void setSplitView(ViewGroup splitView) {
+        mSplitView = splitView;
+    }
+
+    /**
+     * @return Current visibility or if animating, the visibility being animated to.
+     */
+    public int getAnimatedVisibility() {
+        if (mVisibilityAnim != null) {
+            return mVisAnimListener.mFinalVisibility;
+        }
+        return getVisibility();
+    }
+
+    public Animator setupAnimatorToVisibility(int visibility, long duration) {
+        if (mVisibilityAnim != null) {
+            mVisibilityAnim.cancel();
+        }
+
+        if (visibility == VISIBLE) {
+            if (getVisibility() != VISIBLE) {
+                setAlpha(0);
+                if (mSplitView != null && mMenuView != null) {
+                    mMenuView.setAlpha(0);
+                }
+            }
+            ObjectAnimator anim = ObjectAnimator.ofFloat(this, View.ALPHA, 1);
+            anim.setDuration(duration);
+            anim.setInterpolator(sAlphaInterpolator);
+            if (mSplitView != null && mMenuView != null) {
+                AnimatorSet set = new AnimatorSet();
+                ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, View.ALPHA, 1);
+                splitAnim.setDuration(duration);
+                set.addListener(mVisAnimListener.withFinalVisibility(visibility));
+                set.play(anim).with(splitAnim);
+                return set;
+            } else {
+                anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+                return anim;
+            }
+        } else {
+            ObjectAnimator anim = ObjectAnimator.ofFloat(this, View.ALPHA, 0);
+            anim.setDuration(duration);
+            anim.setInterpolator(sAlphaInterpolator);
+            if (mSplitView != null && mMenuView != null) {
+                AnimatorSet set = new AnimatorSet();
+                ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, View.ALPHA, 0);
+                splitAnim.setDuration(duration);
+                set.addListener(mVisAnimListener.withFinalVisibility(visibility));
+                set.play(anim).with(splitAnim);
+                return set;
+            } else {
+                anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+                return anim;
+            }
+        }
+    }
+
+    public void animateToVisibility(int visibility) {
+        Animator anim = setupAnimatorToVisibility(visibility, FADE_DURATION);
+        anim.start();
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        if (visibility != getVisibility()) {
+            if (mVisibilityAnim != null) {
+                mVisibilityAnim.end();
+            }
+            super.setVisibility(visibility);
+        }
+    }
+
+    public boolean showOverflowMenu() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.showOverflowMenu();
+        }
+        return false;
+    }
+
+    public void postShowOverflowMenu() {
+        post(new Runnable() {
+            public void run() {
+                showOverflowMenu();
+            }
+        });
+    }
+
+    public boolean hideOverflowMenu() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.hideOverflowMenu();
+        }
+        return false;
+    }
+
+    public boolean isOverflowMenuShowing() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.isOverflowMenuShowing();
+        }
+        return false;
+    }
+
+    public boolean isOverflowMenuShowPending() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.isOverflowMenuShowPending();
+        }
+        return false;
+    }
+
+    public boolean isOverflowReserved() {
+        return mActionMenuPresenter != null && mActionMenuPresenter.isOverflowReserved();
+    }
+
+    public boolean canShowOverflowMenu() {
+        return isOverflowReserved() && getVisibility() == VISIBLE;
+    }
+
+    public void dismissPopupMenus() {
+        if (mActionMenuPresenter != null) {
+            mActionMenuPresenter.dismissPopupMenus();
+        }
+    }
+
+    protected int measureChildView(View child, int availableWidth, int childSpecHeight,
+            int spacing) {
+        child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+                childSpecHeight);
+
+        availableWidth -= child.getMeasuredWidth();
+        availableWidth -= spacing;
+
+        return Math.max(0, availableWidth);
+    }
+
+    static protected int next(int x, int val, boolean isRtl) {
+        return isRtl ? x - val : x + val;
+    }
+
+    protected int positionChild(View child, int x, int y, int contentHeight, boolean reverse) {
+        int childWidth = child.getMeasuredWidth();
+        int childHeight = child.getMeasuredHeight();
+        int childTop = y + (contentHeight - childHeight) / 2;
+
+        if (reverse) {
+            child.layout(x - childWidth, childTop, x, childTop + childHeight);
+        } else {
+            child.layout(x, childTop, x + childWidth, childTop + childHeight);
+        }
+
+        return  (reverse ? -childWidth : childWidth);
+    }
+
+    protected class VisibilityAnimListener implements Animator.AnimatorListener {
+        private boolean mCanceled = false;
+        int mFinalVisibility;
+
+        public VisibilityAnimListener withFinalVisibility(int visibility) {
+            mFinalVisibility = visibility;
+            return this;
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+            setVisibility(VISIBLE);
+            mVisibilityAnim = animation;
+            mCanceled = false;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (mCanceled) return;
+
+            mVisibilityAnim = null;
+            setVisibility(mFinalVisibility);
+            if (mSplitView != null && mMenuView != null) {
+                mMenuView.setVisibility(mFinalVisibility);
+            }
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mCanceled = true;
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+        }
+    }
+}
diff --git a/com/android/internal/widget/AccountItemView.java b/com/android/internal/widget/AccountItemView.java
new file mode 100644
index 0000000..a521428
--- /dev/null
+++ b/com/android/internal/widget/AccountItemView.java
@@ -0,0 +1,102 @@
+/*
+* Copyright (C) 2011-2014 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.widget.AccountViewAdapter.AccountElements;
+
+
+/**
+ * An LinearLayout view, to show Accounts elements.
+ */
+public class AccountItemView extends LinearLayout {
+
+    private ImageView mAccountIcon;
+    private TextView mAccountName;
+    private TextView mAccountNumber;
+
+    /**
+     * Constructor.
+     */
+    public AccountItemView(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Constructor.
+     */
+    public AccountItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LayoutInflater inflator = (LayoutInflater)
+                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflator.inflate(R.layout.simple_account_item, null);
+        addView(view);
+        initViewItem(view);
+    }
+
+    private void initViewItem(View view) {
+        mAccountIcon = (ImageView)view.findViewById(android.R.id.icon);
+        mAccountName = (TextView)view.findViewById(android.R.id.title);
+        mAccountNumber = (TextView)view.findViewById(android.R.id.summary);
+    }
+
+    public void setViewItem(AccountElements element) {
+        Drawable drawable = element.getDrawable();
+        if (drawable != null) {
+            setAccountIcon(drawable);
+        } else {
+            setAccountIcon(element.getIcon());
+        }
+        setAccountName(element.getName());
+        setAccountNumber(element.getNumber());
+    }
+
+    public void setAccountIcon(int resId) {
+        mAccountIcon.setImageResource(resId);
+    }
+
+    public void setAccountIcon(Drawable drawable) {
+        mAccountIcon.setBackgroundDrawable(drawable);
+    }
+
+    public void setAccountName(String name) {
+        setText(mAccountName, name);
+    }
+
+    public void setAccountNumber(String number) {
+        setText(mAccountNumber, number);
+    }
+
+    private void setText(TextView view, String text) {
+        if (TextUtils.isEmpty(text)) {
+            view.setVisibility(View.GONE);
+        } else {
+            view.setText(text);
+            view.setVisibility(View.VISIBLE);
+        }
+    }
+}
diff --git a/com/android/internal/widget/AccountViewAdapter.java b/com/android/internal/widget/AccountViewAdapter.java
new file mode 100644
index 0000000..8a7a9a6
--- /dev/null
+++ b/com/android/internal/widget/AccountViewAdapter.java
@@ -0,0 +1,127 @@
+/*
+* Copyright (C) 2011-2014 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import java.util.List;
+
+public class AccountViewAdapter extends BaseAdapter {
+
+    private List<AccountElements> mData;
+    private Context mContext;
+
+    /**
+     * Constructor
+     *
+     * @param context The context where the View associated with this Adapter is running
+     * @param data A list with AccountElements data type. The list contains the data of each
+     *         account and the each member of AccountElements will correspond to one item view.
+     */
+    public AccountViewAdapter(Context context, final List<AccountElements> data) {
+        mContext = context;
+        mData = data;
+    }
+
+    @Override
+    public int getCount() {
+        return mData.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return mData.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    public void updateData(final List<AccountElements> data) {
+        mData = data;
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        AccountItemView view;
+        if (convertView == null) {
+            view = new AccountItemView(mContext);
+        } else {
+            view = (AccountItemView) convertView;
+        }
+        AccountElements elements = (AccountElements) getItem(position);
+        view.setViewItem(elements);
+        return view;
+    }
+
+    public static class AccountElements {
+        private int mIcon;
+        private Drawable mDrawable;
+        private String mName;
+        private String mNumber;
+
+        /**
+         * Constructor
+         * A structure with basic element of an Account, icon, name and number
+         *
+         * @param icon Account icon id
+         * @param name Account name
+         * @param num Account number
+         */
+        public AccountElements(int icon, String name, String number) {
+            this(icon, null, name, number);
+        }
+
+        /**
+         * Constructor
+         * A structure with basic element of an Account, drawable, name and number
+         *
+         * @param drawable Account drawable
+         * @param name Account name
+         * @param num Account number
+         */
+        public AccountElements(Drawable drawable, String name, String number) {
+            this(0, drawable, name, number);
+        }
+
+        private AccountElements(int icon, Drawable drawable, String name, String number) {
+            mIcon = icon;
+            mDrawable = drawable;
+            mName = name;
+            mNumber = number;
+        }
+
+        public int getIcon() {
+            return mIcon;
+        }
+        public String getName() {
+            return mName;
+        }
+        public String getNumber() {
+            return mNumber;
+        }
+        public Drawable getDrawable() {
+            return mDrawable;
+        }
+    }
+}
diff --git a/com/android/internal/widget/ActionBarAccessor.java b/com/android/internal/widget/ActionBarAccessor.java
new file mode 100644
index 0000000..40b6220
--- /dev/null
+++ b/com/android/internal/widget/ActionBarAccessor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.widget.ActionMenuPresenter;
+
+/**
+ * To access non public members of AbsActionBarView
+ */
+public class ActionBarAccessor {
+
+    /**
+     * Returns the {@link ActionMenuPresenter} associated with the {@link AbsActionBarView}
+     */
+    public static ActionMenuPresenter getActionMenuPresenter(AbsActionBarView view) {
+        return view.mActionMenuPresenter;
+    }
+}
diff --git a/com/android/internal/widget/ActionBarContainer.java b/com/android/internal/widget/ActionBarContainer.java
new file mode 100644
index 0000000..baf3188
--- /dev/null
+++ b/com/android/internal/widget/ActionBarContainer.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2010 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.internal.widget;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * This class acts as a container for the action bar view and action mode context views.
+ * It applies special styles as needed to help handle animated transitions between them.
+ * @hide
+ */
+public class ActionBarContainer extends FrameLayout {
+    private boolean mIsTransitioning;
+    private View mTabContainer;
+    private View mActionBarView;
+    private View mActionContextView;
+
+    private Drawable mBackground;
+    private Drawable mStackedBackground;
+    private Drawable mSplitBackground;
+    private boolean mIsSplit;
+    private boolean mIsStacked;
+    private int mHeight;
+
+    public ActionBarContainer(Context context) {
+        this(context, null);
+    }
+
+    public ActionBarContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // Set a transparent background so that we project appropriately.
+        setBackground(new ActionBarBackgroundDrawable());
+
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.ActionBar);
+        mBackground = a.getDrawable(com.android.internal.R.styleable.ActionBar_background);
+        mStackedBackground = a.getDrawable(
+                com.android.internal.R.styleable.ActionBar_backgroundStacked);
+        mHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.ActionBar_height, -1);
+
+        if (getId() == com.android.internal.R.id.split_action_bar) {
+            mIsSplit = true;
+            mSplitBackground = a.getDrawable(
+                    com.android.internal.R.styleable.ActionBar_backgroundSplit);
+        }
+        a.recycle();
+
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        mActionBarView = findViewById(com.android.internal.R.id.action_bar);
+        mActionContextView = findViewById(com.android.internal.R.id.action_context_bar);
+    }
+
+    public void setPrimaryBackground(Drawable bg) {
+        if (mBackground != null) {
+            mBackground.setCallback(null);
+            unscheduleDrawable(mBackground);
+        }
+        mBackground = bg;
+        if (bg != null) {
+            bg.setCallback(this);
+            if (mActionBarView != null) {
+                mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(),
+                        mActionBarView.getRight(), mActionBarView.getBottom());
+            }
+        }
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
+        invalidate();
+    }
+
+    public void setStackedBackground(Drawable bg) {
+        if (mStackedBackground != null) {
+            mStackedBackground.setCallback(null);
+            unscheduleDrawable(mStackedBackground);
+        }
+        mStackedBackground = bg;
+        if (bg != null) {
+            bg.setCallback(this);
+            if ((mIsStacked && mStackedBackground != null)) {
+                mStackedBackground.setBounds(mTabContainer.getLeft(), mTabContainer.getTop(),
+                        mTabContainer.getRight(), mTabContainer.getBottom());
+            }
+        }
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
+        invalidate();
+    }
+
+    public void setSplitBackground(Drawable bg) {
+        if (mSplitBackground != null) {
+            mSplitBackground.setCallback(null);
+            unscheduleDrawable(mSplitBackground);
+        }
+        mSplitBackground = bg;
+        if (bg != null) {
+            bg.setCallback(this);
+            if (mIsSplit && mSplitBackground != null) {
+                mSplitBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
+            }
+        }
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
+        invalidate();
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        final boolean isVisible = visibility == VISIBLE;
+        if (mBackground != null) mBackground.setVisible(isVisible, false);
+        if (mStackedBackground != null) mStackedBackground.setVisible(isVisible, false);
+        if (mSplitBackground != null) mSplitBackground.setVisible(isVisible, false);
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return (who == mBackground && !mIsSplit) || (who == mStackedBackground && mIsStacked) ||
+                (who == mSplitBackground && mIsSplit) || super.verifyDrawable(who);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final int[] state = getDrawableState();
+        boolean changed = false;
+
+        final Drawable background = mBackground;
+        if (background != null && background.isStateful()) {
+            changed |= background.setState(state);
+        }
+
+        final Drawable stackedBackground = mStackedBackground;
+        if (stackedBackground != null && stackedBackground.isStateful()) {
+            changed |= stackedBackground.setState(state);
+        }
+
+        final Drawable splitBackground = mSplitBackground;
+        if (splitBackground != null && splitBackground.isStateful()) {
+            changed |= splitBackground.setState(state);
+        }
+
+        if (changed) {
+            invalidate();
+        }
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mBackground != null) {
+            mBackground.jumpToCurrentState();
+        }
+        if (mStackedBackground != null) {
+            mStackedBackground.jumpToCurrentState();
+        }
+        if (mSplitBackground != null) {
+            mSplitBackground.jumpToCurrentState();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void onResolveDrawables(int layoutDirection) {
+        super.onResolveDrawables(layoutDirection);
+        if (mBackground != null) {
+            mBackground.setLayoutDirection(layoutDirection);
+        }
+        if (mStackedBackground != null) {
+            mStackedBackground.setLayoutDirection(layoutDirection);
+        }
+        if (mSplitBackground != null) {
+            mSplitBackground.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    /**
+     * Set the action bar into a "transitioning" state. While transitioning
+     * the bar will block focus and touch from all of its descendants. This
+     * prevents the user from interacting with the bar while it is animating
+     * in or out.
+     *
+     * @param isTransitioning true if the bar is currently transitioning, false otherwise.
+     */
+    public void setTransitioning(boolean isTransitioning) {
+        mIsTransitioning = isTransitioning;
+        setDescendantFocusability(isTransitioning ? FOCUS_BLOCK_DESCENDANTS
+                : FOCUS_AFTER_DESCENDANTS);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return mIsTransitioning || super.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        super.onTouchEvent(ev);
+
+        // An action bar always eats touch events.
+        return true;
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent ev) {
+        super.onHoverEvent(ev);
+
+        // An action bar always eats hover events.
+        return true;
+    }
+
+    public void setTabContainer(ScrollingTabContainerView tabView) {
+        if (mTabContainer != null) {
+            removeView(mTabContainer);
+        }
+        mTabContainer = tabView;
+        if (tabView != null) {
+            addView(tabView);
+            final ViewGroup.LayoutParams lp = tabView.getLayoutParams();
+            lp.width = LayoutParams.MATCH_PARENT;
+            lp.height = LayoutParams.WRAP_CONTENT;
+            tabView.setAllowCollapse(false);
+        }
+    }
+
+    public View getTabContainer() {
+        return mTabContainer;
+    }
+
+    @Override
+    public ActionMode startActionModeForChild(
+            View child, ActionMode.Callback callback, int type) {
+        if (type != ActionMode.TYPE_PRIMARY) {
+            return super.startActionModeForChild(child, callback, type);
+        }
+        return null;
+    }
+
+    private static boolean isCollapsed(View view) {
+        return view == null || view.getVisibility() == GONE || view.getMeasuredHeight() == 0;
+    }
+
+    private int getMeasuredHeightWithMargins(View view) {
+        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+        return view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mActionBarView == null &&
+                MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST && mHeight >= 0) {
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    Math.min(mHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.AT_MOST);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (mActionBarView == null) return;
+
+        if (mTabContainer != null && mTabContainer.getVisibility() != GONE) {
+            int nonTabMaxHeight = 0;
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                if (child == mTabContainer) {
+                    continue;
+                }
+                nonTabMaxHeight = Math.max(nonTabMaxHeight, isCollapsed(child) ? 0 :
+                        getMeasuredHeightWithMargins(child));
+            }
+            final int mode = MeasureSpec.getMode(heightMeasureSpec);
+            final int maxHeight = mode == MeasureSpec.AT_MOST ?
+                    MeasureSpec.getSize(heightMeasureSpec) : Integer.MAX_VALUE;
+            setMeasuredDimension(getMeasuredWidth(),
+                    Math.min(nonTabMaxHeight + getMeasuredHeightWithMargins(mTabContainer),
+                            maxHeight));
+        }
+    }
+
+    @Override
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        final View tabContainer = mTabContainer;
+        final boolean hasTabs = tabContainer != null && tabContainer.getVisibility() != GONE;
+
+        if (tabContainer != null && tabContainer.getVisibility() != GONE) {
+            final int containerHeight = getMeasuredHeight();
+            final LayoutParams lp = (LayoutParams) tabContainer.getLayoutParams();
+            final int tabHeight = tabContainer.getMeasuredHeight();
+            tabContainer.layout(l, containerHeight - tabHeight - lp.bottomMargin, r,
+                    containerHeight - lp.bottomMargin);
+        }
+
+        boolean needsInvalidate = false;
+        if (mIsSplit) {
+            if (mSplitBackground != null) {
+                mSplitBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
+                needsInvalidate = true;
+            }
+        } else {
+            if (mBackground != null) {
+                if (mActionBarView.getVisibility() == View.VISIBLE) {
+                    mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(),
+                            mActionBarView.getRight(), mActionBarView.getBottom());
+                } else if (mActionContextView != null &&
+                        mActionContextView.getVisibility() == View.VISIBLE) {
+                    mBackground.setBounds(mActionContextView.getLeft(), mActionContextView.getTop(),
+                            mActionContextView.getRight(), mActionContextView.getBottom());
+                } else {
+                    mBackground.setBounds(0, 0, 0, 0);
+                }
+                needsInvalidate = true;
+            }
+            mIsStacked = hasTabs;
+            if (hasTabs && mStackedBackground != null) {
+                mStackedBackground.setBounds(tabContainer.getLeft(), tabContainer.getTop(),
+                        tabContainer.getRight(), tabContainer.getBottom());
+                needsInvalidate = true;
+            }
+        }
+
+        if (needsInvalidate) {
+            invalidate();
+        }
+    }
+
+    /**
+     * Dummy drawable so that we don't break background display lists and
+     * projection surfaces.
+     */
+    private class ActionBarBackgroundDrawable extends Drawable {
+        @Override
+        public void draw(Canvas canvas) {
+            if (mIsSplit) {
+                if (mSplitBackground != null) {
+                    mSplitBackground.draw(canvas);
+                }
+            } else {
+                if (mBackground != null) {
+                    mBackground.draw(canvas);
+                }
+                if (mStackedBackground != null && mIsStacked) {
+                    mStackedBackground.draw(canvas);
+                }
+            }
+        }
+
+        @Override
+        public void getOutline(@NonNull Outline outline) {
+            if (mIsSplit) {
+                if (mSplitBackground != null) {
+                    mSplitBackground.getOutline(outline);
+                }
+            } else {
+                // ignore the stacked background for shadow casting
+                if (mBackground != null) {
+                    mBackground.getOutline(outline);
+                }
+            }
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+        }
+
+        @Override
+        public int getOpacity() {
+            if (mIsSplit) {
+                if (mSplitBackground != null
+                        && mSplitBackground.getOpacity() == PixelFormat.OPAQUE) {
+                    return PixelFormat.OPAQUE;
+                }
+            } else {
+                if (mIsStacked && (mStackedBackground == null
+                        || mStackedBackground.getOpacity() != PixelFormat.OPAQUE)) {
+                    return PixelFormat.UNKNOWN;
+                }
+                if (!isCollapsed(mActionBarView) && mBackground != null
+                        && mBackground.getOpacity() == PixelFormat.OPAQUE) {
+                    return PixelFormat.OPAQUE;
+                }
+            }
+
+            return PixelFormat.UNKNOWN;
+        }
+    }
+}
diff --git a/com/android/internal/widget/ActionBarContextView.java b/com/android/internal/widget/ActionBarContextView.java
new file mode 100644
index 0000000..693b194
--- /dev/null
+++ b/com/android/internal/widget/ActionBarContextView.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2010 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.internal.widget;
+
+import com.android.internal.R;
+
+import android.widget.ActionMenuPresenter;
+import android.widget.ActionMenuView;
+import com.android.internal.view.menu.MenuBuilder;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * @hide
+ */
+public class ActionBarContextView extends AbsActionBarView {
+    private static final String TAG = "ActionBarContextView";
+
+    private CharSequence mTitle;
+    private CharSequence mSubtitle;
+
+    private View mClose;
+    private View mCustomView;
+    private LinearLayout mTitleLayout;
+    private TextView mTitleView;
+    private TextView mSubtitleView;
+    private int mTitleStyleRes;
+    private int mSubtitleStyleRes;
+    private Drawable mSplitBackground;
+    private boolean mTitleOptional;
+    private int mCloseItemLayout;
+
+    public ActionBarContextView(Context context) {
+        this(context, null);
+    }
+    
+    public ActionBarContextView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.actionModeStyle);
+    }
+    
+    public ActionBarContextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ActionBarContextView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.ActionMode, defStyleAttr, defStyleRes);
+        setBackground(a.getDrawable(
+                com.android.internal.R.styleable.ActionMode_background));
+        mTitleStyleRes = a.getResourceId(
+                com.android.internal.R.styleable.ActionMode_titleTextStyle, 0);
+        mSubtitleStyleRes = a.getResourceId(
+                com.android.internal.R.styleable.ActionMode_subtitleTextStyle, 0);
+
+        mContentHeight = a.getLayoutDimension(
+                com.android.internal.R.styleable.ActionMode_height, 0);
+
+        mSplitBackground = a.getDrawable(
+                com.android.internal.R.styleable.ActionMode_backgroundSplit);
+
+        mCloseItemLayout = a.getResourceId(
+                com.android.internal.R.styleable.ActionMode_closeItemLayout,
+                R.layout.action_mode_close_item);
+
+        a.recycle();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mActionMenuPresenter != null) {
+            mActionMenuPresenter.hideOverflowMenu();
+            mActionMenuPresenter.hideSubMenus();
+        }
+    }
+
+    @Override
+    public void setSplitToolbar(boolean split) {
+        if (mSplitActionBar != split) {
+            if (mActionMenuPresenter != null) {
+                // Mode is already active; move everything over and adjust the menu itself.
+                final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                        LayoutParams.MATCH_PARENT);
+                if (!split) {
+                    mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+                    mMenuView.setBackground(null);
+                    final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+                    if (oldParent != null) oldParent.removeView(mMenuView);
+                    addView(mMenuView, layoutParams);
+                } else {
+                    // Allow full screen width in split mode.
+                    mActionMenuPresenter.setWidthLimit(
+                            getContext().getResources().getDisplayMetrics().widthPixels, true);
+                    // No limit to the item count; use whatever will fit.
+                    mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+                    // Span the whole width
+                    layoutParams.width = LayoutParams.MATCH_PARENT;
+                    layoutParams.height = mContentHeight;
+                    mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+                    mMenuView.setBackground(mSplitBackground);
+                    final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+                    if (oldParent != null) oldParent.removeView(mMenuView);
+                    mSplitView.addView(mMenuView, layoutParams);
+                }
+            }
+            super.setSplitToolbar(split);
+        }
+    }
+
+    public void setContentHeight(int height) {
+        mContentHeight = height;
+    }
+
+    public void setCustomView(View view) {
+        if (mCustomView != null) {
+            removeView(mCustomView);
+        }
+        mCustomView = view;
+        if (view != null && mTitleLayout != null) {
+            removeView(mTitleLayout);
+            mTitleLayout = null;
+        }
+        if (view != null) {
+            addView(view);
+        }
+        requestLayout();
+    }
+
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+        initTitle();
+    }
+
+    public void setSubtitle(CharSequence subtitle) {
+        mSubtitle = subtitle;
+        initTitle();
+    }
+
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    public CharSequence getSubtitle() {
+        return mSubtitle;
+    }
+
+    private void initTitle() {
+        if (mTitleLayout == null) {
+            LayoutInflater inflater = LayoutInflater.from(getContext());
+            inflater.inflate(R.layout.action_bar_title_item, this);
+            mTitleLayout = (LinearLayout) getChildAt(getChildCount() - 1);
+            mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
+            mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
+            if (mTitleStyleRes != 0) {
+                mTitleView.setTextAppearance(mTitleStyleRes);
+            }
+            if (mSubtitleStyleRes != 0) {
+                mSubtitleView.setTextAppearance(mSubtitleStyleRes);
+            }
+        }
+
+        mTitleView.setText(mTitle);
+        mSubtitleView.setText(mSubtitle);
+
+        final boolean hasTitle = !TextUtils.isEmpty(mTitle);
+        final boolean hasSubtitle = !TextUtils.isEmpty(mSubtitle);
+        mSubtitleView.setVisibility(hasSubtitle ? VISIBLE : GONE);
+        mTitleLayout.setVisibility(hasTitle || hasSubtitle ? VISIBLE : GONE);
+        if (mTitleLayout.getParent() == null) {
+            addView(mTitleLayout);
+        }
+    }
+
+    public void initForMode(final ActionMode mode) {
+        if (mClose == null) {
+            LayoutInflater inflater = LayoutInflater.from(mContext);
+            mClose = inflater.inflate(mCloseItemLayout, this, false);
+            addView(mClose);
+        } else if (mClose.getParent() == null) {
+            addView(mClose);
+        }
+
+        View closeButton = mClose.findViewById(R.id.action_mode_close_button);
+        closeButton.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                mode.finish();
+            }
+        });
+
+        final MenuBuilder menu = (MenuBuilder) mode.getMenu();
+        if (mActionMenuPresenter != null) {
+            mActionMenuPresenter.dismissPopupMenus();
+        }
+        mActionMenuPresenter = new ActionMenuPresenter(mContext);
+        mActionMenuPresenter.setReserveOverflow(true);
+
+        final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.MATCH_PARENT);
+        if (!mSplitActionBar) {
+            menu.addMenuPresenter(mActionMenuPresenter, mPopupContext);
+            mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+            mMenuView.setBackground(null);
+            addView(mMenuView, layoutParams);
+        } else {
+            // Allow full screen width in split mode.
+            mActionMenuPresenter.setWidthLimit(
+                    getContext().getResources().getDisplayMetrics().widthPixels, true);
+            // No limit to the item count; use whatever will fit.
+            mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+            // Span the whole width
+            layoutParams.width = LayoutParams.MATCH_PARENT;
+            layoutParams.height = mContentHeight;
+            menu.addMenuPresenter(mActionMenuPresenter, mPopupContext);
+            mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+            mMenuView.setBackgroundDrawable(mSplitBackground);
+            mSplitView.addView(mMenuView, layoutParams);
+        }
+    }
+
+    public void closeMode() {
+        if (mClose == null) {
+            killMode();
+            return;
+        }
+
+    }
+
+    public void killMode() {
+        removeAllViews();
+        if (mSplitView != null) {
+            mSplitView.removeView(mMenuView);
+        }
+        mCustomView = null;
+        mMenuView = null;
+    }
+
+    @Override
+    public boolean showOverflowMenu() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.showOverflowMenu();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean hideOverflowMenu() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.hideOverflowMenu();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isOverflowMenuShowing() {
+        if (mActionMenuPresenter != null) {
+            return mActionMenuPresenter.isOverflowMenuShowing();
+        }
+        return false;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        // Used by custom views if they don't supply layout params. Everything else
+        // added to an ActionBarContextView should have them already.
+        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new MarginLayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        if (widthMode != MeasureSpec.EXACTLY) {
+            throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+                    "with android:layout_width=\"match_parent\" (or fill_parent)");
+        }
+
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (heightMode == MeasureSpec.UNSPECIFIED) {
+            throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+                    "with android:layout_height=\"wrap_content\"");
+        }
+
+        final int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
+
+        int maxHeight = mContentHeight > 0 ?
+                mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
+
+        final int verticalPadding = getPaddingTop() + getPaddingBottom();
+        int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
+        final int height = maxHeight - verticalPadding;
+        final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+
+        if (mClose != null) {
+            availableWidth = measureChildView(mClose, availableWidth, childSpecHeight, 0);
+            MarginLayoutParams lp = (MarginLayoutParams) mClose.getLayoutParams();
+            availableWidth -= lp.leftMargin + lp.rightMargin;
+        }
+
+        if (mMenuView != null && mMenuView.getParent() == this) {
+            availableWidth = measureChildView(mMenuView, availableWidth,
+                    childSpecHeight, 0);
+        }
+
+        if (mTitleLayout != null && mCustomView == null) {
+            if (mTitleOptional) {
+                final int titleWidthSpec = MeasureSpec.makeSafeMeasureSpec(contentWidth,
+                        MeasureSpec.UNSPECIFIED);
+                mTitleLayout.measure(titleWidthSpec, childSpecHeight);
+                final int titleWidth = mTitleLayout.getMeasuredWidth();
+                final boolean titleFits = titleWidth <= availableWidth;
+                if (titleFits) {
+                    availableWidth -= titleWidth;
+                }
+                mTitleLayout.setVisibility(titleFits ? VISIBLE : GONE);
+            } else {
+                availableWidth = measureChildView(mTitleLayout, availableWidth, childSpecHeight, 0);
+            }
+        }
+
+        if (mCustomView != null) {
+            ViewGroup.LayoutParams lp = mCustomView.getLayoutParams();
+            final int customWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
+                    MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+            final int customWidth = lp.width >= 0 ?
+                    Math.min(lp.width, availableWidth) : availableWidth;
+            final int customHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
+                    MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+            final int customHeight = lp.height >= 0 ?
+                    Math.min(lp.height, height) : height;
+            mCustomView.measure(MeasureSpec.makeMeasureSpec(customWidth, customWidthMode),
+                    MeasureSpec.makeMeasureSpec(customHeight, customHeightMode));
+        }
+
+        if (mContentHeight <= 0) {
+            int measuredHeight = 0;
+            final int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                View v = getChildAt(i);
+                int paddedViewHeight = v.getMeasuredHeight() + verticalPadding;
+                if (paddedViewHeight > measuredHeight) {
+                    measuredHeight = paddedViewHeight;
+                }
+            }
+            setMeasuredDimension(contentWidth, measuredHeight);
+        } else {
+            setMeasuredDimension(contentWidth, maxHeight);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final boolean isLayoutRtl = isLayoutRtl();
+        int x = isLayoutRtl ? r - l - getPaddingRight() : getPaddingLeft();
+        final int y = getPaddingTop();
+        final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
+
+        if (mClose != null && mClose.getVisibility() != GONE) {
+            MarginLayoutParams lp = (MarginLayoutParams) mClose.getLayoutParams();
+            final int startMargin = (isLayoutRtl ? lp.rightMargin : lp.leftMargin);
+            final int endMargin = (isLayoutRtl ? lp.leftMargin : lp.rightMargin);
+            x = next(x, startMargin, isLayoutRtl);
+            x += positionChild(mClose, x, y, contentHeight, isLayoutRtl);
+            x = next(x, endMargin, isLayoutRtl);
+
+        }
+
+        if (mTitleLayout != null && mCustomView == null && mTitleLayout.getVisibility() != GONE) {
+            x += positionChild(mTitleLayout, x, y, contentHeight, isLayoutRtl);
+        }
+        
+        if (mCustomView != null) {
+            x += positionChild(mCustomView, x, y, contentHeight, isLayoutRtl);
+        }
+
+        x = isLayoutRtl ? getPaddingLeft() : r - l - getPaddingRight();
+
+        if (mMenuView != null) {
+            x += positionChild(mMenuView, x, y, contentHeight, !isLayoutRtl);
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+            // Action mode started
+            event.setSource(this);
+            event.setClassName(getClass().getName());
+            event.setPackageName(getContext().getPackageName());
+            event.setContentDescription(mTitle);
+        } else {
+            super.onInitializeAccessibilityEventInternal(event);
+        }
+    }
+
+    public void setTitleOptional(boolean titleOptional) {
+        if (titleOptional != mTitleOptional) {
+            requestLayout();
+        }
+        mTitleOptional = titleOptional;
+    }
+
+    public boolean isTitleOptional() {
+        return mTitleOptional;
+    }
+}
diff --git a/com/android/internal/widget/ActionBarOverlayLayout.java b/com/android/internal/widget/ActionBarOverlayLayout.java
new file mode 100644
index 0000000..5d7fa6a
--- /dev/null
+++ b/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -0,0 +1,843 @@
+/*
+ * Copyright (C) 2012 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.IntProperty;
+import android.util.Log;
+import android.util.Property;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.widget.OverScroller;
+import android.widget.Toolbar;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Special layout for the containing of an overlay action bar (and its
+ * content) to correctly handle fitting system windows when the content
+ * has request that its layout ignore them.
+ */
+public class ActionBarOverlayLayout extends ViewGroup implements DecorContentParent {
+    private static final String TAG = "ActionBarOverlayLayout";
+
+    private int mActionBarHeight;
+    //private WindowDecorActionBar mActionBar;
+    private int mWindowVisibility = View.VISIBLE;
+
+    // The main UI elements that we handle the layout of.
+    private View mContent;
+    private ActionBarContainer mActionBarBottom;
+    private ActionBarContainer mActionBarTop;
+
+    // Some interior UI elements.
+    private DecorToolbar mDecorToolbar;
+
+    // Content overlay drawable - generally the action bar's shadow
+    private Drawable mWindowContentOverlay;
+    private boolean mIgnoreWindowContentOverlay;
+
+    private boolean mOverlayMode;
+    private boolean mHasNonEmbeddedTabs;
+    private boolean mHideOnContentScroll;
+    private boolean mAnimatingForFling;
+    private int mHideOnContentScrollReference;
+    private int mLastSystemUiVisibility;
+    private final Rect mBaseContentInsets = new Rect();
+    private final Rect mLastBaseContentInsets = new Rect();
+    private final Rect mContentInsets = new Rect();
+    private final Rect mBaseInnerInsets = new Rect();
+    private final Rect mLastBaseInnerInsets = new Rect();
+    private final Rect mInnerInsets = new Rect();
+    private final Rect mLastInnerInsets = new Rect();
+
+    private ActionBarVisibilityCallback mActionBarVisibilityCallback;
+
+    private final int ACTION_BAR_ANIMATE_DELAY = 600; // ms
+
+    private OverScroller mFlingEstimator;
+
+    private ViewPropertyAnimator mCurrentActionBarTopAnimator;
+    private ViewPropertyAnimator mCurrentActionBarBottomAnimator;
+
+    private final Animator.AnimatorListener mTopAnimatorListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mCurrentActionBarTopAnimator = null;
+            mAnimatingForFling = false;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mCurrentActionBarTopAnimator = null;
+            mAnimatingForFling = false;
+        }
+    };
+
+    private final Animator.AnimatorListener mBottomAnimatorListener =
+            new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mCurrentActionBarBottomAnimator = null;
+            mAnimatingForFling = false;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mCurrentActionBarBottomAnimator = null;
+            mAnimatingForFling = false;
+        }
+    };
+
+    private final Runnable mRemoveActionBarHideOffset = new Runnable() {
+        public void run() {
+            haltActionBarHideOffsetAnimations();
+            mCurrentActionBarTopAnimator = mActionBarTop.animate().translationY(0)
+                    .setListener(mTopAnimatorListener);
+            if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) {
+                mCurrentActionBarBottomAnimator = mActionBarBottom.animate().translationY(0)
+                        .setListener(mBottomAnimatorListener);
+            }
+        }
+    };
+
+    private final Runnable mAddActionBarHideOffset = new Runnable() {
+        public void run() {
+            haltActionBarHideOffsetAnimations();
+            mCurrentActionBarTopAnimator = mActionBarTop.animate()
+                    .translationY(-mActionBarTop.getHeight())
+                    .setListener(mTopAnimatorListener);
+            if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) {
+                mCurrentActionBarBottomAnimator = mActionBarBottom.animate()
+                        .translationY(mActionBarBottom.getHeight())
+                        .setListener(mBottomAnimatorListener);
+            }
+        }
+    };
+
+    public static final Property<ActionBarOverlayLayout, Integer> ACTION_BAR_HIDE_OFFSET =
+            new IntProperty<ActionBarOverlayLayout>("actionBarHideOffset") {
+
+                @Override
+                public void setValue(ActionBarOverlayLayout object, int value) {
+                    object.setActionBarHideOffset(value);
+                }
+
+                @Override
+                public Integer get(ActionBarOverlayLayout object) {
+                    return object.getActionBarHideOffset();
+                }
+            };
+
+    static final int[] ATTRS = new int [] {
+            com.android.internal.R.attr.actionBarSize,
+            com.android.internal.R.attr.windowContentOverlay
+    };
+
+    public ActionBarOverlayLayout(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public ActionBarOverlayLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    private void init(Context context) {
+        TypedArray ta = getContext().getTheme().obtainStyledAttributes(ATTRS);
+        mActionBarHeight = ta.getDimensionPixelSize(0, 0);
+        mWindowContentOverlay = ta.getDrawable(1);
+        setWillNotDraw(mWindowContentOverlay == null);
+        ta.recycle();
+
+        mIgnoreWindowContentOverlay = context.getApplicationInfo().targetSdkVersion <
+                Build.VERSION_CODES.KITKAT;
+
+        mFlingEstimator = new OverScroller(context);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        haltActionBarHideOffsetAnimations();
+    }
+
+    public void setActionBarVisibilityCallback(ActionBarVisibilityCallback cb) {
+        mActionBarVisibilityCallback = cb;
+        if (getWindowToken() != null) {
+            // This is being initialized after being added to a window;
+            // make sure to update all state now.
+            mActionBarVisibilityCallback.onWindowVisibilityChanged(mWindowVisibility);
+            if (mLastSystemUiVisibility != 0) {
+                int newVis = mLastSystemUiVisibility;
+                onWindowSystemUiVisibilityChanged(newVis);
+                requestApplyInsets();
+            }
+        }
+    }
+
+    public void setOverlayMode(boolean overlayMode) {
+        mOverlayMode = overlayMode;
+
+        /*
+         * Drawing the window content overlay was broken before K so starting to draw it
+         * again unexpectedly will cause artifacts in some apps. They should fix it.
+         */
+        mIgnoreWindowContentOverlay = overlayMode &&
+                getContext().getApplicationInfo().targetSdkVersion <
+                        Build.VERSION_CODES.KITKAT;
+    }
+
+    public boolean isInOverlayMode() {
+        return mOverlayMode;
+    }
+
+    public void setHasNonEmbeddedTabs(boolean hasNonEmbeddedTabs) {
+        mHasNonEmbeddedTabs = hasNonEmbeddedTabs;
+    }
+
+    public void setShowingForActionMode(boolean showing) {
+        if (showing) {
+            // Here's a fun hack: if the status bar is currently being hidden,
+            // and the application has asked for stable content insets, then
+            // we will end up with the action mode action bar being shown
+            // without the status bar, but moved below where the status bar
+            // would be.  Not nice.  Trying to have this be positioned
+            // correctly is not easy (basically we need yet *another* content
+            // inset from the window manager to know where to put it), so
+            // instead we will just temporarily force the status bar to be shown.
+            if ((getWindowSystemUiVisibility() & (SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                    | SYSTEM_UI_FLAG_LAYOUT_STABLE))
+                    == (SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE)) {
+                setDisabledSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
+            }
+        } else {
+            setDisabledSystemUiVisibility(0);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        init(getContext());
+        requestApplyInsets();
+    }
+
+    @Override
+    public void onWindowSystemUiVisibilityChanged(int visible) {
+        super.onWindowSystemUiVisibilityChanged(visible);
+        pullChildren();
+        final int diff = mLastSystemUiVisibility ^ visible;
+        mLastSystemUiVisibility = visible;
+        final boolean barVisible = (visible & SYSTEM_UI_FLAG_FULLSCREEN) == 0;
+        final boolean stable = (visible & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0;
+        if (mActionBarVisibilityCallback != null) {
+            // We want the bar to be visible if it is not being hidden,
+            // or the app has not turned on a stable UI mode (meaning they
+            // are performing explicit layout around the action bar).
+            mActionBarVisibilityCallback.enableContentAnimations(!stable);
+            if (barVisible || !stable) mActionBarVisibilityCallback.showForSystem();
+            else mActionBarVisibilityCallback.hideForSystem();
+        }
+        if ((diff & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) {
+            if (mActionBarVisibilityCallback != null) {
+                requestApplyInsets();
+            }
+        }
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        mWindowVisibility = visibility;
+        if (mActionBarVisibilityCallback != null) {
+            mActionBarVisibilityCallback.onWindowVisibilityChanged(visibility);
+        }
+    }
+
+    private boolean applyInsets(View view, Rect insets, boolean left, boolean top,
+            boolean bottom, boolean right) {
+        boolean changed = false;
+        LayoutParams lp = (LayoutParams)view.getLayoutParams();
+        if (left && lp.leftMargin != insets.left) {
+            changed = true;
+            lp.leftMargin = insets.left;
+        }
+        if (top && lp.topMargin != insets.top) {
+            changed = true;
+            lp.topMargin = insets.top;
+        }
+        if (right && lp.rightMargin != insets.right) {
+            changed = true;
+            lp.rightMargin = insets.right;
+        }
+        if (bottom && lp.bottomMargin != insets.bottom) {
+            changed = true;
+            lp.bottomMargin = insets.bottom;
+        }
+        return changed;
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        pullChildren();
+
+        final int vis = getWindowSystemUiVisibility();
+        final boolean stable = (vis & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0;
+        final Rect systemInsets = insets.getSystemWindowInsets();
+
+        // The top and bottom action bars are always within the content area.
+        boolean changed = applyInsets(mActionBarTop, systemInsets, true, true, false, true);
+        if (mActionBarBottom != null) {
+            changed |= applyInsets(mActionBarBottom, systemInsets, true, false, true, true);
+        }
+
+        mBaseInnerInsets.set(systemInsets);
+        computeFitSystemWindows(mBaseInnerInsets, mBaseContentInsets);
+        if (!mLastBaseInnerInsets.equals(mBaseInnerInsets)) {
+            changed = true;
+            mLastBaseContentInsets.set(mBaseContentInsets);
+        }
+        if (!mLastBaseContentInsets.equals(mBaseContentInsets)) {
+            changed = true;
+            mLastBaseContentInsets.set(mBaseContentInsets);
+        }
+
+        if (changed) {
+            requestLayout();
+        }
+
+        // We don't do any more at this point.  To correctly compute the content/inner
+        // insets in all cases, we need to know the measured size of the various action
+        // bar elements.  onApplyWindowInsets() happens before the measure pass, so we can't
+        // do that here.  Instead we will take this up in onMeasure().
+        return WindowInsets.CONSUMED;
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LayoutParams;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        pullChildren();
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+        int childState = 0;
+
+        int topInset = 0;
+        int bottomInset = 0;
+
+        measureChildWithMargins(mActionBarTop, widthMeasureSpec, 0, heightMeasureSpec, 0);
+        LayoutParams lp = (LayoutParams) mActionBarTop.getLayoutParams();
+        maxWidth = Math.max(maxWidth,
+                mActionBarTop.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+        maxHeight = Math.max(maxHeight,
+                mActionBarTop.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+        childState = combineMeasuredStates(childState, mActionBarTop.getMeasuredState());
+
+        // xlarge screen layout doesn't have bottom action bar.
+        if (mActionBarBottom != null) {
+            measureChildWithMargins(mActionBarBottom, widthMeasureSpec, 0, heightMeasureSpec, 0);
+            lp = (LayoutParams) mActionBarBottom.getLayoutParams();
+            maxWidth = Math.max(maxWidth,
+                    mActionBarBottom.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+            maxHeight = Math.max(maxHeight,
+                    mActionBarBottom.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+            childState = combineMeasuredStates(childState, mActionBarBottom.getMeasuredState());
+        }
+
+        final int vis = getWindowSystemUiVisibility();
+        final boolean stable = (vis & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0;
+
+        if (stable) {
+            // This is the standard space needed for the action bar.  For stable measurement,
+            // we can't depend on the size currently reported by it -- this must remain constant.
+            topInset = mActionBarHeight;
+            if (mHasNonEmbeddedTabs) {
+                final View tabs = mActionBarTop.getTabContainer();
+                if (tabs != null) {
+                    // If tabs are not embedded, increase space on top to account for them.
+                    topInset += mActionBarHeight;
+                }
+            }
+        } else if (mActionBarTop.getVisibility() != GONE) {
+            // This is the space needed on top of the window for all of the action bar
+            // and tabs.
+            topInset = mActionBarTop.getMeasuredHeight();
+        }
+
+        if (mDecorToolbar.isSplit()) {
+            // If action bar is split, adjust bottom insets for it.
+            if (mActionBarBottom != null) {
+                if (stable) {
+                    bottomInset = mActionBarHeight;
+                } else {
+                    bottomInset = mActionBarBottom.getMeasuredHeight();
+                }
+            }
+        }
+
+        // If the window has not requested system UI layout flags, we need to
+        // make sure its content is not being covered by system UI...  though it
+        // will still be covered by the action bar if they have requested it to
+        // overlay.
+        mContentInsets.set(mBaseContentInsets);
+        mInnerInsets.set(mBaseInnerInsets);
+        if (!mOverlayMode && !stable) {
+            mContentInsets.top += topInset;
+            mContentInsets.bottom += bottomInset;
+        } else {
+            mInnerInsets.top += topInset;
+            mInnerInsets.bottom += bottomInset;
+        }
+        applyInsets(mContent, mContentInsets, true, true, true, true);
+
+        if (!mLastInnerInsets.equals(mInnerInsets)) {
+            // If the inner insets have changed, we need to dispatch this down to
+            // the app's fitSystemWindows().  We do this before measuring the content
+            // view to keep the same semantics as the normal fitSystemWindows() call.
+            mLastInnerInsets.set(mInnerInsets);
+            mContent.dispatchApplyWindowInsets(new WindowInsets(mInnerInsets));
+        }
+
+        measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
+        lp = (LayoutParams) mContent.getLayoutParams();
+        maxWidth = Math.max(maxWidth,
+                mContent.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+        maxHeight = Math.max(maxHeight,
+                mContent.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+        childState = combineMeasuredStates(childState, mContent.getMeasuredState());
+
+        // Account for padding too
+        maxWidth += getPaddingLeft() + getPaddingRight();
+        maxHeight += getPaddingTop() + getPaddingBottom();
+
+        // Check against our minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int count = getChildCount();
+
+        final int parentLeft = getPaddingLeft();
+        final int parentRight = right - left - getPaddingRight();
+
+        final int parentTop = getPaddingTop();
+        final int parentBottom = bottom - top - getPaddingBottom();
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+                final int width = child.getMeasuredWidth();
+                final int height = child.getMeasuredHeight();
+
+                int childLeft = parentLeft + lp.leftMargin;
+                int childTop;
+                if (child == mActionBarBottom) {
+                    childTop = parentBottom - height - lp.bottomMargin;
+                } else {
+                    childTop = parentTop + lp.topMargin;
+                }
+
+                child.layout(childLeft, childTop, childLeft + width, childTop + height);
+            }
+        }
+    }
+
+    @Override
+    public void draw(Canvas c) {
+        super.draw(c);
+        if (mWindowContentOverlay != null && !mIgnoreWindowContentOverlay) {
+            final int top = mActionBarTop.getVisibility() == VISIBLE ?
+                    (int) (mActionBarTop.getBottom() + mActionBarTop.getTranslationY() + 0.5f) : 0;
+            mWindowContentOverlay.setBounds(0, top, getWidth(),
+                    top + mWindowContentOverlay.getIntrinsicHeight());
+            mWindowContentOverlay.draw(c);
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    @Override
+    public boolean onStartNestedScroll(View child, View target, int axes) {
+        if ((axes & SCROLL_AXIS_VERTICAL) == 0 || mActionBarTop.getVisibility() != VISIBLE) {
+            return false;
+        }
+        return mHideOnContentScroll;
+    }
+
+    @Override
+    public void onNestedScrollAccepted(View child, View target, int axes) {
+        super.onNestedScrollAccepted(child, target, axes);
+        mHideOnContentScrollReference = getActionBarHideOffset();
+        haltActionBarHideOffsetAnimations();
+        if (mActionBarVisibilityCallback != null) {
+            mActionBarVisibilityCallback.onContentScrollStarted();
+        }
+    }
+
+    @Override
+    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed) {
+        mHideOnContentScrollReference += dyConsumed;
+        setActionBarHideOffset(mHideOnContentScrollReference);
+    }
+
+    @Override
+    public void onStopNestedScroll(View target) {
+        super.onStopNestedScroll(target);
+        if (mHideOnContentScroll && !mAnimatingForFling) {
+            if (mHideOnContentScrollReference <= mActionBarTop.getHeight()) {
+                postRemoveActionBarHideOffset();
+            } else {
+                postAddActionBarHideOffset();
+            }
+        }
+        if (mActionBarVisibilityCallback != null) {
+            mActionBarVisibilityCallback.onContentScrollStopped();
+        }
+    }
+
+    @Override
+    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+        if (!mHideOnContentScroll || !consumed) {
+            return false;
+        }
+        if (shouldHideActionBarOnFling(velocityX, velocityY)) {
+            addActionBarHideOffset();
+        } else {
+            removeActionBarHideOffset();
+        }
+        mAnimatingForFling = true;
+        return true;
+    }
+
+    void pullChildren() {
+        if (mContent == null) {
+            mContent = findViewById(com.android.internal.R.id.content);
+            mActionBarTop = findViewById(
+                    com.android.internal.R.id.action_bar_container);
+            mDecorToolbar = getDecorToolbar(findViewById(com.android.internal.R.id.action_bar));
+            mActionBarBottom = findViewById(
+                    com.android.internal.R.id.split_action_bar);
+        }
+    }
+
+    private DecorToolbar getDecorToolbar(View view) {
+        if (view instanceof DecorToolbar) {
+            return (DecorToolbar) view;
+        } else if (view instanceof Toolbar) {
+            return ((Toolbar) view).getWrapper();
+        } else {
+            throw new IllegalStateException("Can't make a decor toolbar out of " +
+                    view.getClass().getSimpleName());
+        }
+    }
+
+    public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) {
+        if (hideOnContentScroll != mHideOnContentScroll) {
+            mHideOnContentScroll = hideOnContentScroll;
+            if (!hideOnContentScroll) {
+                stopNestedScroll();
+                haltActionBarHideOffsetAnimations();
+                setActionBarHideOffset(0);
+            }
+        }
+    }
+
+    public boolean isHideOnContentScrollEnabled() {
+        return mHideOnContentScroll;
+    }
+
+    public int getActionBarHideOffset() {
+        return mActionBarTop != null ? -((int) mActionBarTop.getTranslationY()) : 0;
+    }
+
+    public void setActionBarHideOffset(int offset) {
+        haltActionBarHideOffsetAnimations();
+        final int topHeight = mActionBarTop.getHeight();
+        offset = Math.max(0, Math.min(offset, topHeight));
+        mActionBarTop.setTranslationY(-offset);
+        if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) {
+            // Match the hide offset proportionally for a split bar
+            final float fOffset = (float) offset / topHeight;
+            final int bOffset = (int) (mActionBarBottom.getHeight() * fOffset);
+            mActionBarBottom.setTranslationY(bOffset);
+        }
+    }
+
+    private void haltActionBarHideOffsetAnimations() {
+        removeCallbacks(mRemoveActionBarHideOffset);
+        removeCallbacks(mAddActionBarHideOffset);
+        if (mCurrentActionBarTopAnimator != null) {
+            mCurrentActionBarTopAnimator.cancel();
+        }
+        if (mCurrentActionBarBottomAnimator != null) {
+            mCurrentActionBarBottomAnimator.cancel();
+        }
+    }
+
+    private void postRemoveActionBarHideOffset() {
+        haltActionBarHideOffsetAnimations();
+        postDelayed(mRemoveActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY);
+    }
+
+    private void postAddActionBarHideOffset() {
+        haltActionBarHideOffsetAnimations();
+        postDelayed(mAddActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY);
+    }
+
+    private void removeActionBarHideOffset() {
+        haltActionBarHideOffsetAnimations();
+        mRemoveActionBarHideOffset.run();
+    }
+
+    private void addActionBarHideOffset() {
+        haltActionBarHideOffsetAnimations();
+        mAddActionBarHideOffset.run();
+    }
+
+    private boolean shouldHideActionBarOnFling(float velocityX, float velocityY) {
+        mFlingEstimator.fling(0, 0, 0, (int) velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
+        final int finalY = mFlingEstimator.getFinalY();
+        return finalY > mActionBarTop.getHeight();
+    }
+
+    @Override
+    public void setWindowCallback(Window.Callback cb) {
+        pullChildren();
+        mDecorToolbar.setWindowCallback(cb);
+    }
+
+    @Override
+    public void setWindowTitle(CharSequence title) {
+        pullChildren();
+        mDecorToolbar.setWindowTitle(title);
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        pullChildren();
+        return mDecorToolbar.getTitle();
+    }
+
+    @Override
+    public void initFeature(int windowFeature) {
+        pullChildren();
+        switch (windowFeature) {
+            case Window.FEATURE_PROGRESS:
+                mDecorToolbar.initProgress();
+                break;
+            case Window.FEATURE_INDETERMINATE_PROGRESS:
+                mDecorToolbar.initIndeterminateProgress();
+                break;
+            case Window.FEATURE_ACTION_BAR_OVERLAY:
+                setOverlayMode(true);
+                break;
+        }
+    }
+
+    @Override
+    public void setUiOptions(int uiOptions) {
+        boolean splitActionBar = false;
+        final boolean splitWhenNarrow =
+                (uiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;
+        if (splitWhenNarrow) {
+            splitActionBar = getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.split_action_bar_is_narrow);
+        }
+        if (splitActionBar) {
+            pullChildren();
+            if (mActionBarBottom != null && mDecorToolbar.canSplit()) {
+                mDecorToolbar.setSplitView(mActionBarBottom);
+                mDecorToolbar.setSplitToolbar(splitActionBar);
+                mDecorToolbar.setSplitWhenNarrow(splitWhenNarrow);
+
+                final ActionBarContextView cab = findViewById(
+                        com.android.internal.R.id.action_context_bar);
+                cab.setSplitView(mActionBarBottom);
+                cab.setSplitToolbar(splitActionBar);
+                cab.setSplitWhenNarrow(splitWhenNarrow);
+            } else if (splitActionBar) {
+                Log.e(TAG, "Requested split action bar with " +
+                        "incompatible window decor! Ignoring request.");
+            }
+        }
+    }
+
+    @Override
+    public boolean hasIcon() {
+        pullChildren();
+        return mDecorToolbar.hasIcon();
+    }
+
+    @Override
+    public boolean hasLogo() {
+        pullChildren();
+        return mDecorToolbar.hasLogo();
+    }
+
+    @Override
+    public void setIcon(int resId) {
+        pullChildren();
+        mDecorToolbar.setIcon(resId);
+    }
+
+    @Override
+    public void setIcon(Drawable d) {
+        pullChildren();
+        mDecorToolbar.setIcon(d);
+    }
+
+    @Override
+    public void setLogo(int resId) {
+        pullChildren();
+        mDecorToolbar.setLogo(resId);
+    }
+
+    @Override
+    public boolean canShowOverflowMenu() {
+        pullChildren();
+        return mDecorToolbar.canShowOverflowMenu();
+    }
+
+    @Override
+    public boolean isOverflowMenuShowing() {
+        pullChildren();
+        return mDecorToolbar.isOverflowMenuShowing();
+    }
+
+    @Override
+    public boolean isOverflowMenuShowPending() {
+        pullChildren();
+        return mDecorToolbar.isOverflowMenuShowPending();
+    }
+
+    @Override
+    public boolean showOverflowMenu() {
+        pullChildren();
+        return mDecorToolbar.showOverflowMenu();
+    }
+
+    @Override
+    public boolean hideOverflowMenu() {
+        pullChildren();
+        return mDecorToolbar.hideOverflowMenu();
+    }
+
+    @Override
+    public void setMenuPrepared() {
+        pullChildren();
+        mDecorToolbar.setMenuPrepared();
+    }
+
+    @Override
+    public void setMenu(Menu menu, MenuPresenter.Callback cb) {
+        pullChildren();
+        mDecorToolbar.setMenu(menu, cb);
+    }
+
+    @Override
+    public void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates) {
+        pullChildren();
+        mDecorToolbar.saveHierarchyState(toolbarStates);
+    }
+
+    @Override
+    public void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates) {
+        pullChildren();
+        mDecorToolbar.restoreHierarchyState(toolbarStates);
+    }
+
+    @Override
+    public void dismissPopups() {
+        pullChildren();
+        mDecorToolbar.dismissPopupMenus();
+    }
+
+    public static class LayoutParams extends MarginLayoutParams {
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(ViewGroup.MarginLayoutParams source) {
+            super(source);
+        }
+    }
+
+    public interface ActionBarVisibilityCallback {
+        void onWindowVisibilityChanged(int visibility);
+        void showForSystem();
+        void hideForSystem();
+        void enableContentAnimations(boolean enable);
+        void onContentScrollStarted();
+        void onContentScrollStopped();
+    }
+}
diff --git a/com/android/internal/widget/ActionBarView.java b/com/android/internal/widget/ActionBarView.java
new file mode 100644
index 0000000..f90b59d
--- /dev/null
+++ b/com/android/internal/widget/ActionBarView.java
@@ -0,0 +1,1730 @@
+/*
+ * Copyright (C) 2010 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.internal.widget;
+
+import android.animation.LayoutTransition;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.CollapsibleActionView;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.Window;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ActionMenuPresenter;
+import android.widget.ActionMenuView;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+import com.android.internal.R;
+import com.android.internal.view.menu.ActionMenuItem;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.view.menu.SubMenuBuilder;
+
+/**
+ * @hide
+ */
+public class ActionBarView extends AbsActionBarView implements DecorToolbar {
+    private static final String TAG = "ActionBarView";
+
+    /**
+     * Display options applied by default
+     */
+    public static final int DISPLAY_DEFAULT = 0;
+
+    /**
+     * Display options that require re-layout as opposed to a simple invalidate
+     */
+    private static final int DISPLAY_RELAYOUT_MASK =
+            ActionBar.DISPLAY_SHOW_HOME |
+            ActionBar.DISPLAY_USE_LOGO |
+            ActionBar.DISPLAY_HOME_AS_UP |
+            ActionBar.DISPLAY_SHOW_CUSTOM |
+            ActionBar.DISPLAY_SHOW_TITLE |
+            ActionBar.DISPLAY_TITLE_MULTIPLE_LINES;
+
+    private static final int DEFAULT_CUSTOM_GRAVITY = Gravity.START | Gravity.CENTER_VERTICAL;
+
+    private int mNavigationMode;
+    private int mDisplayOptions = -1;
+    private CharSequence mTitle;
+    private CharSequence mSubtitle;
+    private Drawable mIcon;
+    private Drawable mLogo;
+    private CharSequence mHomeDescription;
+    private int mHomeDescriptionRes;
+
+    private HomeView mHomeLayout;
+    private HomeView mExpandedHomeLayout;
+    private LinearLayout mTitleLayout;
+    private TextView mTitleView;
+    private TextView mSubtitleView;
+    private ViewGroup mUpGoerFive;
+
+    private Spinner mSpinner;
+    private LinearLayout mListNavLayout;
+    private ScrollingTabContainerView mTabScrollView;
+    private View mCustomNavView;
+    private ProgressBar mProgressView;
+    private ProgressBar mIndeterminateProgressView;
+
+    private int mProgressBarPadding;
+    private int mItemPadding;
+
+    private final int mTitleStyleRes;
+    private final int mSubtitleStyleRes;
+    private final int mProgressStyle;
+    private final int mIndeterminateProgressStyle;
+
+    private boolean mUserTitle;
+    private boolean mIncludeTabs;
+    private boolean mIsCollapsible;
+    private boolean mWasHomeEnabled; // Was it enabled before action view expansion?
+
+    private MenuBuilder mOptionsMenu;
+    private boolean mMenuPrepared;
+
+    private ActionBarContextView mContextView;
+
+    private ActionMenuItem mLogoNavItem;
+
+    private SpinnerAdapter mSpinnerAdapter;
+    private AdapterView.OnItemSelectedListener mNavItemSelectedListener;
+
+    private Runnable mTabSelector;
+
+    private ExpandedActionViewMenuPresenter mExpandedMenuPresenter;
+    View mExpandedActionView;
+    private int mDefaultUpDescription = R.string.action_bar_up_description;
+
+    Window.Callback mWindowCallback;
+
+    private final OnClickListener mExpandedActionViewUpListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final MenuItemImpl item = mExpandedMenuPresenter.mCurrentExpandedItem;
+            if (item != null) {
+                item.collapseActionView();
+            }
+        }
+    };
+
+    private final OnClickListener mUpClickListener = new OnClickListener() {
+        public void onClick(View v) {
+            if (mMenuPrepared) {
+                // Only invoke the window callback if the options menu has been initialized.
+                mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, mLogoNavItem);
+            }
+        }
+    };
+
+    public ActionBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // Background is always provided by the container.
+        setBackgroundResource(0);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar,
+                com.android.internal.R.attr.actionBarStyle, 0);
+
+        mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode,
+                ActionBar.NAVIGATION_MODE_STANDARD);
+        mTitle = a.getText(R.styleable.ActionBar_title);
+        mSubtitle = a.getText(R.styleable.ActionBar_subtitle);
+        mLogo = a.getDrawable(R.styleable.ActionBar_logo);
+        mIcon = a.getDrawable(R.styleable.ActionBar_icon);
+
+        final LayoutInflater inflater = LayoutInflater.from(context);
+
+        final int homeResId = a.getResourceId(
+                com.android.internal.R.styleable.ActionBar_homeLayout,
+                com.android.internal.R.layout.action_bar_home);
+
+        mUpGoerFive = (ViewGroup) inflater.inflate(
+                com.android.internal.R.layout.action_bar_up_container, this, false);
+        mHomeLayout = (HomeView) inflater.inflate(homeResId, mUpGoerFive, false);
+
+        mExpandedHomeLayout = (HomeView) inflater.inflate(homeResId, mUpGoerFive, false);
+        mExpandedHomeLayout.setShowUp(true);
+        mExpandedHomeLayout.setOnClickListener(mExpandedActionViewUpListener);
+        mExpandedHomeLayout.setContentDescription(getResources().getText(
+                mDefaultUpDescription));
+
+        // This needs to highlight/be focusable on its own.
+        // TODO: Clean up the handoff between expanded/normal.
+        final Drawable upBackground = mUpGoerFive.getBackground();
+        if (upBackground != null) {
+            mExpandedHomeLayout.setBackground(upBackground.getConstantState().newDrawable());
+        }
+        mExpandedHomeLayout.setEnabled(true);
+        mExpandedHomeLayout.setFocusable(true);
+
+        mTitleStyleRes = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0);
+        mSubtitleStyleRes = a.getResourceId(R.styleable.ActionBar_subtitleTextStyle, 0);
+        mProgressStyle = a.getResourceId(R.styleable.ActionBar_progressBarStyle, 0);
+        mIndeterminateProgressStyle = a.getResourceId(
+                R.styleable.ActionBar_indeterminateProgressStyle, 0);
+
+        mProgressBarPadding = a.getDimensionPixelOffset(R.styleable.ActionBar_progressBarPadding, 0);
+        mItemPadding = a.getDimensionPixelOffset(R.styleable.ActionBar_itemPadding, 0);
+
+        setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT));
+
+        final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0);
+        if (customNavId != 0) {
+            mCustomNavView = (View) inflater.inflate(customNavId, this, false);
+            mNavigationMode = ActionBar.NAVIGATION_MODE_STANDARD;
+            setDisplayOptions(mDisplayOptions | ActionBar.DISPLAY_SHOW_CUSTOM);
+        }
+
+        mContentHeight = a.getLayoutDimension(R.styleable.ActionBar_height, 0);
+
+        a.recycle();
+
+        mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle);
+
+        mUpGoerFive.setOnClickListener(mUpClickListener);
+        mUpGoerFive.setClickable(true);
+        mUpGoerFive.setFocusable(true);
+
+        if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        mTitleView = null;
+        mSubtitleView = null;
+        if (mTitleLayout != null && mTitleLayout.getParent() == mUpGoerFive) {
+            mUpGoerFive.removeView(mTitleLayout);
+        }
+        mTitleLayout = null;
+        if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+            initTitle();
+        }
+
+        if (mHomeDescriptionRes != 0) {
+            setNavigationContentDescription(mHomeDescriptionRes);
+        }
+
+        if (mTabScrollView != null && mIncludeTabs) {
+            ViewGroup.LayoutParams lp = mTabScrollView.getLayoutParams();
+            if (lp != null) {
+                lp.width = LayoutParams.WRAP_CONTENT;
+                lp.height = LayoutParams.MATCH_PARENT;
+            }
+            mTabScrollView.setAllowCollapse(true);
+        }
+    }
+
+    /**
+     * Set the window callback used to invoke menu items; used for dispatching home button presses.
+     * @param cb Window callback to dispatch to
+     */
+    public void setWindowCallback(Window.Callback cb) {
+        mWindowCallback = cb;
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        removeCallbacks(mTabSelector);
+        if (mActionMenuPresenter != null) {
+            mActionMenuPresenter.hideOverflowMenu();
+            mActionMenuPresenter.hideSubMenus();
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    public void initProgress() {
+        mProgressView = new ProgressBar(mContext, null, 0, mProgressStyle);
+        mProgressView.setId(R.id.progress_horizontal);
+        mProgressView.setMax(10000);
+        mProgressView.setVisibility(GONE);
+        addView(mProgressView);
+    }
+
+    public void initIndeterminateProgress() {
+        mIndeterminateProgressView = new ProgressBar(mContext, null, 0,
+                mIndeterminateProgressStyle);
+        mIndeterminateProgressView.setId(R.id.progress_circular);
+        mIndeterminateProgressView.setVisibility(GONE);
+        addView(mIndeterminateProgressView);
+    }
+
+    @Override
+    public void setSplitToolbar(boolean splitActionBar) {
+        if (mSplitActionBar != splitActionBar) {
+            if (mMenuView != null) {
+                final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+                if (oldParent != null) {
+                    oldParent.removeView(mMenuView);
+                }
+                if (splitActionBar) {
+                    if (mSplitView != null) {
+                        mSplitView.addView(mMenuView);
+                    }
+                    mMenuView.getLayoutParams().width = LayoutParams.MATCH_PARENT;
+                } else {
+                    addView(mMenuView);
+                    mMenuView.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
+                }
+                mMenuView.requestLayout();
+            }
+            if (mSplitView != null) {
+                mSplitView.setVisibility(splitActionBar ? VISIBLE : GONE);
+            }
+
+            if (mActionMenuPresenter != null) {
+                if (!splitActionBar) {
+                    mActionMenuPresenter.setExpandedActionViewsExclusive(
+                            getResources().getBoolean(
+                                    com.android.internal.R.bool.action_bar_expanded_action_views_exclusive));
+                } else {
+                    mActionMenuPresenter.setExpandedActionViewsExclusive(false);
+                    // Allow full screen width in split mode.
+                    mActionMenuPresenter.setWidthLimit(
+                            getContext().getResources().getDisplayMetrics().widthPixels, true);
+                    // No limit to the item count; use whatever will fit.
+                    mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+                }
+            }
+            super.setSplitToolbar(splitActionBar);
+        }
+    }
+
+    public boolean isSplit() {
+        return mSplitActionBar;
+    }
+
+    public boolean canSplit() {
+        return true;
+    }
+
+    public boolean hasEmbeddedTabs() {
+        return mIncludeTabs;
+    }
+
+    @Override
+    public void setEmbeddedTabView(ScrollingTabContainerView tabs) {
+        if (mTabScrollView != null) {
+            removeView(mTabScrollView);
+        }
+        mTabScrollView = tabs;
+        mIncludeTabs = tabs != null;
+        if (mIncludeTabs && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) {
+            addView(mTabScrollView);
+            ViewGroup.LayoutParams lp = mTabScrollView.getLayoutParams();
+            lp.width = LayoutParams.WRAP_CONTENT;
+            lp.height = LayoutParams.MATCH_PARENT;
+            tabs.setAllowCollapse(true);
+        }
+    }
+
+    public void setMenuPrepared() {
+        mMenuPrepared = true;
+    }
+
+    public void setMenu(Menu menu, MenuPresenter.Callback cb) {
+        if (menu == mOptionsMenu) return;
+
+        if (mOptionsMenu != null) {
+            mOptionsMenu.removeMenuPresenter(mActionMenuPresenter);
+            mOptionsMenu.removeMenuPresenter(mExpandedMenuPresenter);
+        }
+
+        MenuBuilder builder = (MenuBuilder) menu;
+        mOptionsMenu = builder;
+        if (mMenuView != null) {
+            final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+            if (oldParent != null) {
+                oldParent.removeView(mMenuView);
+            }
+        }
+        if (mActionMenuPresenter == null) {
+            mActionMenuPresenter = new ActionMenuPresenter(mContext);
+            mActionMenuPresenter.setCallback(cb);
+            mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
+            mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
+        }
+
+        ActionMenuView menuView;
+        final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.MATCH_PARENT);
+        if (!mSplitActionBar) {
+            mActionMenuPresenter.setExpandedActionViewsExclusive(
+                    getResources().getBoolean(
+                    com.android.internal.R.bool.action_bar_expanded_action_views_exclusive));
+            configPresenters(builder);
+            menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+            final ViewGroup oldParent = (ViewGroup) menuView.getParent();
+            if (oldParent != null && oldParent != this) {
+                oldParent.removeView(menuView);
+            }
+            addView(menuView, layoutParams);
+        } else {
+            mActionMenuPresenter.setExpandedActionViewsExclusive(false);
+            // Allow full screen width in split mode.
+            mActionMenuPresenter.setWidthLimit(
+                    getContext().getResources().getDisplayMetrics().widthPixels, true);
+            // No limit to the item count; use whatever will fit.
+            mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+            // Span the whole width
+            layoutParams.width = LayoutParams.MATCH_PARENT;
+            layoutParams.height = LayoutParams.WRAP_CONTENT;
+            configPresenters(builder);
+            menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+            if (mSplitView != null) {
+                final ViewGroup oldParent = (ViewGroup) menuView.getParent();
+                if (oldParent != null && oldParent != mSplitView) {
+                    oldParent.removeView(menuView);
+                }
+                menuView.setVisibility(getAnimatedVisibility());
+                mSplitView.addView(menuView, layoutParams);
+            } else {
+                // We'll add this later if we missed it this time.
+                menuView.setLayoutParams(layoutParams);
+            }
+        }
+        mMenuView = menuView;
+    }
+
+    private void configPresenters(MenuBuilder builder) {
+        if (builder != null) {
+            builder.addMenuPresenter(mActionMenuPresenter, mPopupContext);
+            builder.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
+        } else {
+            mActionMenuPresenter.initForMenu(mPopupContext, null);
+            mExpandedMenuPresenter.initForMenu(mPopupContext, null);
+            mActionMenuPresenter.updateMenuView(true);
+            mExpandedMenuPresenter.updateMenuView(true);
+        }
+    }
+
+    public boolean hasExpandedActionView() {
+        return mExpandedMenuPresenter != null &&
+                mExpandedMenuPresenter.mCurrentExpandedItem != null;
+    }
+
+    public void collapseActionView() {
+        final MenuItemImpl item = mExpandedMenuPresenter == null ? null :
+                mExpandedMenuPresenter.mCurrentExpandedItem;
+        if (item != null) {
+            item.collapseActionView();
+        }
+    }
+
+    public void setCustomView(View view) {
+        final boolean showCustom = (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0;
+        if (mCustomNavView != null && showCustom) {
+            removeView(mCustomNavView);
+        }
+        mCustomNavView = view;
+        if (mCustomNavView != null && showCustom) {
+            addView(mCustomNavView);
+        }
+    }
+
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Set the action bar title. This will always replace or override window titles.
+     * @param title Title to set
+     *
+     * @see #setWindowTitle(CharSequence)
+     */
+    public void setTitle(CharSequence title) {
+        mUserTitle = true;
+        setTitleImpl(title);
+    }
+
+    /**
+     * Set the window title. A window title will always be replaced or overridden by a user title.
+     * @param title Title to set
+     *
+     * @see #setTitle(CharSequence)
+     */
+    public void setWindowTitle(CharSequence title) {
+        if (!mUserTitle) {
+            setTitleImpl(title);
+        }
+    }
+
+    private void setTitleImpl(CharSequence title) {
+        mTitle = title;
+        if (mTitleView != null) {
+            mTitleView.setText(title);
+            final boolean visible = mExpandedActionView == null &&
+                    (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0 &&
+                    (!TextUtils.isEmpty(mTitle) || !TextUtils.isEmpty(mSubtitle));
+            mTitleLayout.setVisibility(visible ? VISIBLE : GONE);
+        }
+        if (mLogoNavItem != null) {
+            mLogoNavItem.setTitle(title);
+        }
+        updateHomeAccessibility(mUpGoerFive.isEnabled());
+    }
+
+    public CharSequence getSubtitle() {
+        return mSubtitle;
+    }
+
+    public void setSubtitle(CharSequence subtitle) {
+        mSubtitle = subtitle;
+        if (mSubtitleView != null) {
+            mSubtitleView.setText(subtitle);
+            mSubtitleView.setVisibility(subtitle != null ? VISIBLE : GONE);
+            final boolean visible = mExpandedActionView == null &&
+                    (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0 &&
+                    (!TextUtils.isEmpty(mTitle) || !TextUtils.isEmpty(mSubtitle));
+            mTitleLayout.setVisibility(visible ? VISIBLE : GONE);
+        }
+        updateHomeAccessibility(mUpGoerFive.isEnabled());
+    }
+
+    public void setHomeButtonEnabled(boolean enable) {
+        setHomeButtonEnabled(enable, true);
+    }
+
+    private void setHomeButtonEnabled(boolean enable, boolean recordState) {
+        if (recordState) {
+            mWasHomeEnabled = enable;
+        }
+
+        if (mExpandedActionView != null) {
+            // There's an action view currently showing and we want to keep the state
+            // configured for the action view at the moment. If we needed to record the
+            // new state for later we will have done so above.
+            return;
+        }
+
+        mUpGoerFive.setEnabled(enable);
+        mUpGoerFive.setFocusable(enable);
+        // Make sure the home button has an accurate content description for accessibility.
+        updateHomeAccessibility(enable);
+    }
+
+    private void updateHomeAccessibility(boolean homeEnabled) {
+        if (!homeEnabled) {
+            mUpGoerFive.setContentDescription(null);
+            mUpGoerFive.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+        } else {
+            mUpGoerFive.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            mUpGoerFive.setContentDescription(buildHomeContentDescription());
+        }
+    }
+
+    /**
+     * Compose a content description for the Home/Up affordance.
+     *
+     * <p>As this encompasses the icon/logo, title and subtitle all in one, we need
+     * a description for the whole wad of stuff that can be localized properly.</p>
+     */
+    private CharSequence buildHomeContentDescription() {
+        final CharSequence homeDesc;
+        if (mHomeDescription != null) {
+            homeDesc = mHomeDescription;
+        } else {
+            if ((mDisplayOptions & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+                homeDesc = mContext.getResources().getText(mDefaultUpDescription);
+            } else {
+                homeDesc = mContext.getResources().getText(R.string.action_bar_home_description);
+            }
+        }
+
+        final CharSequence title = getTitle();
+        final CharSequence subtitle = getSubtitle();
+        if (!TextUtils.isEmpty(title)) {
+            final String result;
+            if (!TextUtils.isEmpty(subtitle)) {
+                result = getResources().getString(
+                        R.string.action_bar_home_subtitle_description_format,
+                        title, subtitle, homeDesc);
+            } else {
+                result = getResources().getString(R.string.action_bar_home_description_format,
+                        title, homeDesc);
+            }
+            return result;
+        }
+        return homeDesc;
+    }
+
+    public void setDisplayOptions(int options) {
+        final int flagsChanged = mDisplayOptions == -1 ? -1 : options ^ mDisplayOptions;
+        mDisplayOptions = options;
+
+        if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) {
+
+            if ((flagsChanged & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+                final boolean setUp = (options & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+                mHomeLayout.setShowUp(setUp);
+
+                // Showing home as up implicitly enables interaction with it.
+                // In honeycomb it was always enabled, so make this transition
+                // a bit easier for developers in the common case.
+                // (It would be silly to show it as up without responding to it.)
+                if (setUp) {
+                    setHomeButtonEnabled(true);
+                }
+            }
+
+            if ((flagsChanged & ActionBar.DISPLAY_USE_LOGO) != 0) {
+                final boolean logoVis = mLogo != null && (options & ActionBar.DISPLAY_USE_LOGO) != 0;
+                mHomeLayout.setIcon(logoVis ? mLogo : mIcon);
+            }
+
+            if ((flagsChanged & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+                if ((options & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+                    initTitle();
+                } else {
+                    mUpGoerFive.removeView(mTitleLayout);
+                }
+            }
+
+            final boolean showHome = (options & ActionBar.DISPLAY_SHOW_HOME) != 0;
+            final boolean homeAsUp = (mDisplayOptions & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+            final boolean titleUp = !showHome && homeAsUp;
+            mHomeLayout.setShowIcon(showHome);
+
+            final int homeVis = (showHome || titleUp) && mExpandedActionView == null ?
+                    VISIBLE : GONE;
+            mHomeLayout.setVisibility(homeVis);
+
+            if ((flagsChanged & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && mCustomNavView != null) {
+                if ((options & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+                    addView(mCustomNavView);
+                } else {
+                    removeView(mCustomNavView);
+                }
+            }
+
+            if (mTitleLayout != null &&
+                    (flagsChanged & ActionBar.DISPLAY_TITLE_MULTIPLE_LINES) != 0) {
+                if ((options & ActionBar.DISPLAY_TITLE_MULTIPLE_LINES) != 0) {
+                    mTitleView.setSingleLine(false);
+                    mTitleView.setMaxLines(2);
+                } else {
+                    mTitleView.setMaxLines(1);
+                    mTitleView.setSingleLine(true);
+                }
+            }
+
+            requestLayout();
+        } else {
+            invalidate();
+        }
+
+        // Make sure the home button has an accurate content description for accessibility.
+        updateHomeAccessibility(mUpGoerFive.isEnabled());
+    }
+
+    public void setIcon(Drawable icon) {
+        mIcon = icon;
+        if (icon != null &&
+                ((mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) == 0 || mLogo == null)) {
+            mHomeLayout.setIcon(icon);
+        }
+        if (mExpandedActionView != null) {
+            mExpandedHomeLayout.setIcon(mIcon.getConstantState().newDrawable(getResources()));
+        }
+    }
+
+    public void setIcon(int resId) {
+        setIcon(resId != 0 ? mContext.getDrawable(resId) : null);
+    }
+
+    public boolean hasIcon() {
+        return mIcon != null;
+    }
+
+    public void setLogo(Drawable logo) {
+        mLogo = logo;
+        if (logo != null && (mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) {
+            mHomeLayout.setIcon(logo);
+        }
+    }
+
+    public void setLogo(int resId) {
+        setLogo(resId != 0 ? mContext.getDrawable(resId) : null);
+    }
+
+    public boolean hasLogo() {
+        return mLogo != null;
+    }
+
+    public void setNavigationMode(int mode) {
+        final int oldMode = mNavigationMode;
+        if (mode != oldMode) {
+            switch (oldMode) {
+            case ActionBar.NAVIGATION_MODE_LIST:
+                if (mListNavLayout != null) {
+                    removeView(mListNavLayout);
+                }
+                break;
+            case ActionBar.NAVIGATION_MODE_TABS:
+                if (mTabScrollView != null && mIncludeTabs) {
+                    removeView(mTabScrollView);
+                }
+            }
+
+            switch (mode) {
+            case ActionBar.NAVIGATION_MODE_LIST:
+                if (mSpinner == null) {
+                    mSpinner = new Spinner(mContext, null,
+                            com.android.internal.R.attr.actionDropDownStyle);
+                    mSpinner.setId(com.android.internal.R.id.action_bar_spinner);
+                    mListNavLayout = new LinearLayout(mContext, null,
+                            com.android.internal.R.attr.actionBarTabBarStyle);
+                    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+                            LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+                    params.gravity = Gravity.CENTER;
+                    mListNavLayout.addView(mSpinner, params);
+                }
+                if (mSpinner.getAdapter() != mSpinnerAdapter) {
+                    mSpinner.setAdapter(mSpinnerAdapter);
+                }
+                mSpinner.setOnItemSelectedListener(mNavItemSelectedListener);
+                addView(mListNavLayout);
+                break;
+            case ActionBar.NAVIGATION_MODE_TABS:
+                if (mTabScrollView != null && mIncludeTabs) {
+                    addView(mTabScrollView);
+                }
+                break;
+            }
+            mNavigationMode = mode;
+            requestLayout();
+        }
+    }
+
+    public void setDropdownParams(SpinnerAdapter adapter, AdapterView.OnItemSelectedListener l) {
+        mSpinnerAdapter = adapter;
+        mNavItemSelectedListener = l;
+        if (mSpinner != null) {
+            mSpinner.setAdapter(adapter);
+            mSpinner.setOnItemSelectedListener(l);
+        }
+    }
+
+    public int getDropdownItemCount() {
+        return mSpinnerAdapter != null ? mSpinnerAdapter.getCount() : 0;
+    }
+
+    public void setDropdownSelectedPosition(int position) {
+        mSpinner.setSelection(position);
+    }
+
+    public int getDropdownSelectedPosition() {
+        return mSpinner.getSelectedItemPosition();
+    }
+
+    public View getCustomView() {
+        return mCustomNavView;
+    }
+
+    public int getNavigationMode() {
+        return mNavigationMode;
+    }
+
+    public int getDisplayOptions() {
+        return mDisplayOptions;
+    }
+
+    @Override
+    public ViewGroup getViewGroup() {
+        return this;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        // Used by custom nav views if they don't supply layout params. Everything else
+        // added to an ActionBarView should have them already.
+        return new ActionBar.LayoutParams(DEFAULT_CUSTOM_GRAVITY);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mUpGoerFive.addView(mHomeLayout, 0);
+        addView(mUpGoerFive);
+
+        if (mCustomNavView != null && (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+            final ViewParent parent = mCustomNavView.getParent();
+            if (parent != this) {
+                if (parent instanceof ViewGroup) {
+                    ((ViewGroup) parent).removeView(mCustomNavView);
+                }
+                addView(mCustomNavView);
+            }
+        }
+    }
+
+    private void initTitle() {
+        if (mTitleLayout == null) {
+            LayoutInflater inflater = LayoutInflater.from(getContext());
+            mTitleLayout = (LinearLayout) inflater.inflate(R.layout.action_bar_title_item,
+                    this, false);
+            mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
+            mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
+
+            if (mTitleStyleRes != 0) {
+                mTitleView.setTextAppearance(mTitleStyleRes);
+            }
+            if (mTitle != null) {
+                mTitleView.setText(mTitle);
+            }
+
+            if (mSubtitleStyleRes != 0) {
+                mSubtitleView.setTextAppearance(mSubtitleStyleRes);
+            }
+            if (mSubtitle != null) {
+                mSubtitleView.setText(mSubtitle);
+                mSubtitleView.setVisibility(VISIBLE);
+            }
+        }
+
+        mUpGoerFive.addView(mTitleLayout);
+        if (mExpandedActionView != null ||
+                (TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mSubtitle))) {
+            // Don't show while in expanded mode or with empty text
+            mTitleLayout.setVisibility(GONE);
+        } else {
+            mTitleLayout.setVisibility(VISIBLE);
+        }
+    }
+
+    public void setContextView(ActionBarContextView view) {
+        mContextView = view;
+    }
+
+    public void setCollapsible(boolean collapsible) {
+        mIsCollapsible = collapsible;
+    }
+
+    /**
+     * @return True if any characters in the title were truncated
+     */
+    public boolean isTitleTruncated() {
+        if (mTitleView == null) {
+            return false;
+        }
+
+        final Layout titleLayout = mTitleView.getLayout();
+        if (titleLayout == null) {
+            return false;
+        }
+
+        final int lineCount = titleLayout.getLineCount();
+        for (int i = 0; i < lineCount; i++) {
+            if (titleLayout.getEllipsisCount(i) > 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int childCount = getChildCount();
+        if (mIsCollapsible) {
+            int visibleChildren = 0;
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                if (child.getVisibility() != GONE &&
+                        !(child == mMenuView && mMenuView.getChildCount() == 0) &&
+                        child != mUpGoerFive) {
+                    visibleChildren++;
+                }
+            }
+
+            final int upChildCount = mUpGoerFive.getChildCount();
+            for (int i = 0; i < upChildCount; i++) {
+                final View child = mUpGoerFive.getChildAt(i);
+                if (child.getVisibility() != GONE) {
+                    visibleChildren++;
+                }
+            }
+
+            if (visibleChildren == 0) {
+                // No size for an empty action bar when collapsable.
+                setMeasuredDimension(0, 0);
+                return;
+            }
+        }
+
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        if (widthMode != MeasureSpec.EXACTLY) {
+            throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+                    "with android:layout_width=\"match_parent\" (or fill_parent)");
+        }
+
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (heightMode != MeasureSpec.AT_MOST) {
+            throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+                    "with android:layout_height=\"wrap_content\"");
+        }
+
+        int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
+
+        int maxHeight = mContentHeight >= 0 ?
+                mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
+
+        final int verticalPadding = getPaddingTop() + getPaddingBottom();
+        final int paddingLeft = getPaddingLeft();
+        final int paddingRight = getPaddingRight();
+        final int height = maxHeight - verticalPadding;
+        final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+        final int exactHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+        int availableWidth = contentWidth - paddingLeft - paddingRight;
+        int leftOfCenter = availableWidth / 2;
+        int rightOfCenter = leftOfCenter;
+
+        final boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE &&
+                (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0;
+
+        HomeView homeLayout = mExpandedActionView != null ? mExpandedHomeLayout : mHomeLayout;
+
+        final ViewGroup.LayoutParams homeLp = homeLayout.getLayoutParams();
+        int homeWidthSpec;
+        if (homeLp.width < 0) {
+            homeWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST);
+        } else {
+            homeWidthSpec = MeasureSpec.makeMeasureSpec(homeLp.width, MeasureSpec.EXACTLY);
+        }
+
+        /*
+         * This is a little weird.
+         * We're only measuring the *home* affordance within the Up container here
+         * on purpose, because we want to give the available space to all other views before
+         * the title text. We'll remeasure the whole up container again later.
+         * We need to measure this container so we know the right offset for the up affordance
+         * no matter what.
+         */
+        homeLayout.measure(homeWidthSpec, exactHeightSpec);
+
+        int homeWidth = 0;
+        if ((homeLayout.getVisibility() != GONE && homeLayout.getParent() == mUpGoerFive)
+                || showTitle) {
+            homeWidth = homeLayout.getMeasuredWidth();
+            final int homeOffsetWidth = homeWidth + homeLayout.getStartOffset();
+            availableWidth = Math.max(0, availableWidth - homeOffsetWidth);
+            leftOfCenter = Math.max(0, availableWidth - homeOffsetWidth);
+        }
+
+        if (mMenuView != null && mMenuView.getParent() == this) {
+            availableWidth = measureChildView(mMenuView, availableWidth, exactHeightSpec, 0);
+            rightOfCenter = Math.max(0, rightOfCenter - mMenuView.getMeasuredWidth());
+        }
+
+        if (mIndeterminateProgressView != null &&
+                mIndeterminateProgressView.getVisibility() != GONE) {
+            availableWidth = measureChildView(mIndeterminateProgressView, availableWidth,
+                    childSpecHeight, 0);
+            rightOfCenter = Math.max(0,
+                    rightOfCenter - mIndeterminateProgressView.getMeasuredWidth());
+        }
+
+        if (mExpandedActionView == null) {
+            switch (mNavigationMode) {
+                case ActionBar.NAVIGATION_MODE_LIST:
+                    if (mListNavLayout != null) {
+                        final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding;
+                        availableWidth = Math.max(0, availableWidth - itemPaddingSize);
+                        leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize);
+                        mListNavLayout.measure(
+                                MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+                                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+                        final int listNavWidth = mListNavLayout.getMeasuredWidth();
+                        availableWidth = Math.max(0, availableWidth - listNavWidth);
+                        leftOfCenter = Math.max(0, leftOfCenter - listNavWidth);
+                    }
+                    break;
+                case ActionBar.NAVIGATION_MODE_TABS:
+                    if (mTabScrollView != null) {
+                        final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding;
+                        availableWidth = Math.max(0, availableWidth - itemPaddingSize);
+                        leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize);
+                        mTabScrollView.measure(
+                                MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+                                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+                        final int tabWidth = mTabScrollView.getMeasuredWidth();
+                        availableWidth = Math.max(0, availableWidth - tabWidth);
+                        leftOfCenter = Math.max(0, leftOfCenter - tabWidth);
+                    }
+                    break;
+            }
+        }
+
+        View customView = null;
+        if (mExpandedActionView != null) {
+            customView = mExpandedActionView;
+        } else if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 &&
+                mCustomNavView != null) {
+            customView = mCustomNavView;
+        }
+
+        if (customView != null) {
+            final ViewGroup.LayoutParams lp = generateLayoutParams(customView.getLayoutParams());
+            final ActionBar.LayoutParams ablp = lp instanceof ActionBar.LayoutParams ?
+                    (ActionBar.LayoutParams) lp : null;
+
+            int horizontalMargin = 0;
+            int verticalMargin = 0;
+            if (ablp != null) {
+                horizontalMargin = ablp.leftMargin + ablp.rightMargin;
+                verticalMargin = ablp.topMargin + ablp.bottomMargin;
+            }
+
+            // If the action bar is wrapping to its content height, don't allow a custom
+            // view to MATCH_PARENT.
+            int customNavHeightMode;
+            if (mContentHeight <= 0) {
+                customNavHeightMode = MeasureSpec.AT_MOST;
+            } else {
+                customNavHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
+                        MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+            }
+            final int customNavHeight = Math.max(0,
+                    (lp.height >= 0 ? Math.min(lp.height, height) : height) - verticalMargin);
+
+            final int customNavWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
+                    MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+            int customNavWidth = Math.max(0,
+                    (lp.width >= 0 ? Math.min(lp.width, availableWidth) : availableWidth)
+                    - horizontalMargin);
+            final int hgrav = (ablp != null ? ablp.gravity : DEFAULT_CUSTOM_GRAVITY) &
+                    Gravity.HORIZONTAL_GRAVITY_MASK;
+
+            // Centering a custom view is treated specially; we try to center within the whole
+            // action bar rather than in the available space.
+            if (hgrav == Gravity.CENTER_HORIZONTAL && lp.width == LayoutParams.MATCH_PARENT) {
+                customNavWidth = Math.min(leftOfCenter, rightOfCenter) * 2;
+            }
+
+            customView.measure(
+                    MeasureSpec.makeMeasureSpec(customNavWidth, customNavWidthMode),
+                    MeasureSpec.makeMeasureSpec(customNavHeight, customNavHeightMode));
+            availableWidth -= horizontalMargin + customView.getMeasuredWidth();
+        }
+
+        /*
+         * Measure the whole up container now, allowing for the full home+title sections.
+         * (This will re-measure the home view.)
+         */
+        availableWidth = measureChildView(mUpGoerFive, availableWidth + homeWidth,
+                MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY), 0);
+        if (mTitleLayout != null) {
+            leftOfCenter = Math.max(0, leftOfCenter - mTitleLayout.getMeasuredWidth());
+        }
+
+        if (mContentHeight <= 0) {
+            int measuredHeight = 0;
+            for (int i = 0; i < childCount; i++) {
+                View v = getChildAt(i);
+                int paddedViewHeight = v.getMeasuredHeight() + verticalPadding;
+                if (paddedViewHeight > measuredHeight) {
+                    measuredHeight = paddedViewHeight;
+                }
+            }
+            setMeasuredDimension(contentWidth, measuredHeight);
+        } else {
+            setMeasuredDimension(contentWidth, maxHeight);
+        }
+
+        if (mContextView != null) {
+            mContextView.setContentHeight(getMeasuredHeight());
+        }
+
+        if (mProgressView != null && mProgressView.getVisibility() != GONE) {
+            mProgressView.measure(MeasureSpec.makeMeasureSpec(
+                    contentWidth - mProgressBarPadding * 2, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST));
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
+
+        if (contentHeight <= 0) {
+            // Nothing to do if we can't see anything.
+            return;
+        }
+
+        final boolean isLayoutRtl = isLayoutRtl();
+        final int direction = isLayoutRtl ? 1 : -1;
+        int menuStart = isLayoutRtl ? getPaddingLeft() : r - l - getPaddingRight();
+        // In LTR mode, we start from left padding and go to the right; in RTL mode, we start
+        // from the padding right and go to the left (in reverse way)
+        int x = isLayoutRtl ? r - l - getPaddingRight() : getPaddingLeft();
+        final int y = getPaddingTop();
+
+        HomeView homeLayout = mExpandedActionView != null ? mExpandedHomeLayout : mHomeLayout;
+        final boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE &&
+                (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0;
+        int startOffset = 0;
+        if (homeLayout.getParent() == mUpGoerFive) {
+            if (homeLayout.getVisibility() != GONE) {
+                startOffset = homeLayout.getStartOffset();
+            } else if (showTitle) {
+                startOffset = homeLayout.getUpWidth();
+            }
+        }
+
+        // Position the up container based on where the edge of the home layout should go.
+        x += positionChild(mUpGoerFive,
+                next(x, startOffset, isLayoutRtl), y, contentHeight, isLayoutRtl);
+        x = next(x, startOffset, isLayoutRtl);
+
+        if (mExpandedActionView == null) {
+            switch (mNavigationMode) {
+                case ActionBar.NAVIGATION_MODE_STANDARD:
+                    break;
+                case ActionBar.NAVIGATION_MODE_LIST:
+                    if (mListNavLayout != null) {
+                        if (showTitle) {
+                            x = next(x, mItemPadding, isLayoutRtl);
+                        }
+                        x += positionChild(mListNavLayout, x, y, contentHeight, isLayoutRtl);
+                        x = next(x, mItemPadding, isLayoutRtl);
+                    }
+                    break;
+                case ActionBar.NAVIGATION_MODE_TABS:
+                    if (mTabScrollView != null) {
+                        if (showTitle) x = next(x, mItemPadding, isLayoutRtl);
+                        x += positionChild(mTabScrollView, x, y, contentHeight, isLayoutRtl);
+                        x = next(x, mItemPadding, isLayoutRtl);
+                    }
+                    break;
+            }
+        }
+
+        if (mMenuView != null && mMenuView.getParent() == this) {
+            positionChild(mMenuView, menuStart, y, contentHeight, !isLayoutRtl);
+            menuStart += direction * mMenuView.getMeasuredWidth();
+        }
+
+        if (mIndeterminateProgressView != null &&
+                mIndeterminateProgressView.getVisibility() != GONE) {
+            positionChild(mIndeterminateProgressView, menuStart, y, contentHeight, !isLayoutRtl);
+            menuStart += direction * mIndeterminateProgressView.getMeasuredWidth();
+        }
+
+        View customView = null;
+        if (mExpandedActionView != null) {
+            customView = mExpandedActionView;
+        } else if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 &&
+                mCustomNavView != null) {
+            customView = mCustomNavView;
+        }
+        if (customView != null) {
+            final int layoutDirection = getLayoutDirection();
+            ViewGroup.LayoutParams lp = customView.getLayoutParams();
+            final ActionBar.LayoutParams ablp = lp instanceof ActionBar.LayoutParams ?
+                    (ActionBar.LayoutParams) lp : null;
+            final int gravity = ablp != null ? ablp.gravity : DEFAULT_CUSTOM_GRAVITY;
+            final int navWidth = customView.getMeasuredWidth();
+
+            int topMargin = 0;
+            int bottomMargin = 0;
+            if (ablp != null) {
+                x = next(x, ablp.getMarginStart(), isLayoutRtl);
+                menuStart += direction * ablp.getMarginEnd();
+                topMargin = ablp.topMargin;
+                bottomMargin = ablp.bottomMargin;
+            }
+
+            int hgravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+            // See if we actually have room to truly center; if not push against left or right.
+            if (hgravity == Gravity.CENTER_HORIZONTAL) {
+                final int centeredLeft = ((mRight - mLeft) - navWidth) / 2;
+                if (isLayoutRtl) {
+                    final int centeredStart = centeredLeft + navWidth;
+                    final int centeredEnd = centeredLeft;
+                    if (centeredStart > x) {
+                        hgravity = Gravity.RIGHT;
+                    } else if (centeredEnd < menuStart) {
+                        hgravity = Gravity.LEFT;
+                    }
+                } else {
+                    final int centeredStart = centeredLeft;
+                    final int centeredEnd = centeredLeft + navWidth;
+                    if (centeredStart < x) {
+                        hgravity = Gravity.LEFT;
+                    } else if (centeredEnd > menuStart) {
+                        hgravity = Gravity.RIGHT;
+                    }
+                }
+            } else if (gravity == Gravity.NO_GRAVITY) {
+                hgravity = Gravity.START;
+            }
+
+            int xpos = 0;
+            switch (Gravity.getAbsoluteGravity(hgravity, layoutDirection)) {
+                case Gravity.CENTER_HORIZONTAL:
+                    xpos = ((mRight - mLeft) - navWidth) / 2;
+                    break;
+                case Gravity.LEFT:
+                    xpos = isLayoutRtl ? menuStart : x;
+                    break;
+                case Gravity.RIGHT:
+                    xpos = isLayoutRtl ? x - navWidth : menuStart - navWidth;
+                    break;
+            }
+
+            int vgravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+            if (gravity == Gravity.NO_GRAVITY) {
+                vgravity = Gravity.CENTER_VERTICAL;
+            }
+
+            int ypos = 0;
+            switch (vgravity) {
+                case Gravity.CENTER_VERTICAL:
+                    final int paddedTop = getPaddingTop();
+                    final int paddedBottom = mBottom - mTop - getPaddingBottom();
+                    ypos = ((paddedBottom - paddedTop) - customView.getMeasuredHeight()) / 2;
+                    break;
+                case Gravity.TOP:
+                    ypos = getPaddingTop() + topMargin;
+                    break;
+                case Gravity.BOTTOM:
+                    ypos = getHeight() - getPaddingBottom() - customView.getMeasuredHeight()
+                            - bottomMargin;
+                    break;
+            }
+            final int customWidth = customView.getMeasuredWidth();
+            customView.layout(xpos, ypos, xpos + customWidth,
+                    ypos + customView.getMeasuredHeight());
+            x = next(x, customWidth, isLayoutRtl);
+        }
+
+        if (mProgressView != null) {
+            mProgressView.bringToFront();
+            final int halfProgressHeight = mProgressView.getMeasuredHeight() / 2;
+            mProgressView.layout(mProgressBarPadding, -halfProgressHeight,
+                    mProgressBarPadding + mProgressView.getMeasuredWidth(), halfProgressHeight);
+        }
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new ActionBar.LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        if (lp == null) {
+            lp = generateDefaultLayoutParams();
+        }
+        return lp;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState state = new SavedState(superState);
+
+        if (mExpandedMenuPresenter != null && mExpandedMenuPresenter.mCurrentExpandedItem != null) {
+            state.expandedMenuItemId = mExpandedMenuPresenter.mCurrentExpandedItem.getItemId();
+        }
+
+        state.isOverflowOpen = isOverflowMenuShowing();
+
+        return state;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable p) {
+        SavedState state = (SavedState) p;
+
+        super.onRestoreInstanceState(state.getSuperState());
+
+        if (state.expandedMenuItemId != 0 &&
+                mExpandedMenuPresenter != null && mOptionsMenu != null) {
+            final MenuItem item = mOptionsMenu.findItem(state.expandedMenuItemId);
+            if (item != null) {
+                item.expandActionView();
+            }
+        }
+
+        if (state.isOverflowOpen) {
+            postShowOverflowMenu();
+        }
+    }
+
+    public void setNavigationIcon(Drawable indicator) {
+        mHomeLayout.setUpIndicator(indicator);
+    }
+
+    @Override
+    public void setDefaultNavigationIcon(Drawable icon) {
+        mHomeLayout.setDefaultUpIndicator(icon);
+    }
+
+    public void setNavigationIcon(int resId) {
+        mHomeLayout.setUpIndicator(resId);
+    }
+
+    public void setNavigationContentDescription(CharSequence description) {
+        mHomeDescription = description;
+        updateHomeAccessibility(mUpGoerFive.isEnabled());
+    }
+
+    public void setNavigationContentDescription(int resId) {
+        mHomeDescriptionRes = resId;
+        mHomeDescription = resId != 0 ? getResources().getText(resId) : null;
+        updateHomeAccessibility(mUpGoerFive.isEnabled());
+    }
+
+    @Override
+    public void setDefaultNavigationContentDescription(int defaultNavigationContentDescription) {
+        if (mDefaultUpDescription == defaultNavigationContentDescription) {
+            return;
+        }
+        mDefaultUpDescription = defaultNavigationContentDescription;
+        updateHomeAccessibility(mUpGoerFive.isEnabled());
+    }
+
+    @Override
+    public void setMenuCallbacks(MenuPresenter.Callback presenterCallback,
+            MenuBuilder.Callback menuBuilderCallback) {
+        if (mActionMenuPresenter != null) {
+            mActionMenuPresenter.setCallback(presenterCallback);
+        }
+        if (mOptionsMenu != null) {
+            mOptionsMenu.setCallback(menuBuilderCallback);
+        }
+    }
+
+    @Override
+    public Menu getMenu() {
+        return mOptionsMenu;
+    }
+
+    static class SavedState extends BaseSavedState {
+        int expandedMenuItemId;
+        boolean isOverflowOpen;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            expandedMenuItemId = in.readInt();
+            isOverflowOpen = in.readInt() != 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(expandedMenuItemId);
+            out.writeInt(isOverflowOpen ? 1 : 0);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    private static class HomeView extends FrameLayout {
+        private ImageView mUpView;
+        private ImageView mIconView;
+        private int mUpWidth;
+        private int mStartOffset;
+        private int mUpIndicatorRes;
+        private Drawable mDefaultUpIndicator;
+        private Drawable mUpIndicator;
+
+        private static final long DEFAULT_TRANSITION_DURATION = 150;
+
+        public HomeView(Context context) {
+            this(context, null);
+        }
+
+        public HomeView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            LayoutTransition t = getLayoutTransition();
+            if (t != null) {
+                // Set a lower duration than the default
+                t.setDuration(DEFAULT_TRANSITION_DURATION);
+            }
+        }
+
+        public void setShowUp(boolean isUp) {
+            mUpView.setVisibility(isUp ? VISIBLE : GONE);
+        }
+
+        public void setShowIcon(boolean showIcon) {
+            mIconView.setVisibility(showIcon ? VISIBLE : GONE);
+        }
+
+        public void setIcon(Drawable icon) {
+            mIconView.setImageDrawable(icon);
+        }
+
+        public void setUpIndicator(Drawable d) {
+            mUpIndicator = d;
+            mUpIndicatorRes = 0;
+            updateUpIndicator();
+        }
+
+        public void setDefaultUpIndicator(Drawable d) {
+            mDefaultUpIndicator = d;
+            updateUpIndicator();
+        }
+
+        public void setUpIndicator(int resId) {
+            mUpIndicatorRes = resId;
+            mUpIndicator = null;
+            updateUpIndicator();
+        }
+
+        private void updateUpIndicator() {
+            if (mUpIndicator != null) {
+                mUpView.setImageDrawable(mUpIndicator);
+            } else if (mUpIndicatorRes != 0) {
+                mUpView.setImageDrawable(getContext().getDrawable(mUpIndicatorRes));
+            } else {
+                mUpView.setImageDrawable(mDefaultUpIndicator);
+            }
+        }
+
+        @Override
+        protected void onConfigurationChanged(Configuration newConfig) {
+            super.onConfigurationChanged(newConfig);
+            if (mUpIndicatorRes != 0) {
+                // Reload for config change
+                updateUpIndicator();
+            }
+        }
+
+        @Override
+        public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+            onPopulateAccessibilityEvent(event);
+            return true;
+        }
+
+        @Override
+        public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+            super.onPopulateAccessibilityEventInternal(event);
+            final CharSequence cdesc = getContentDescription();
+            if (!TextUtils.isEmpty(cdesc)) {
+                event.getText().add(cdesc);
+            }
+        }
+
+        @Override
+        public boolean dispatchHoverEvent(MotionEvent event) {
+            // Don't allow children to hover; we want this to be treated as a single component.
+            return onHoverEvent(event);
+        }
+
+        @Override
+        protected void onFinishInflate() {
+            mUpView = (ImageView) findViewById(com.android.internal.R.id.up);
+            mIconView = (ImageView) findViewById(com.android.internal.R.id.home);
+            mDefaultUpIndicator = mUpView.getDrawable();
+        }
+
+        public int getStartOffset() {
+            return mUpView.getVisibility() == GONE ? mStartOffset : 0;
+        }
+
+        public int getUpWidth() {
+            return mUpWidth;
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            measureChildWithMargins(mUpView, widthMeasureSpec, 0, heightMeasureSpec, 0);
+            final LayoutParams upLp = (LayoutParams) mUpView.getLayoutParams();
+            final int upMargins = upLp.leftMargin + upLp.rightMargin;
+            mUpWidth = mUpView.getMeasuredWidth();
+            mStartOffset = mUpWidth + upMargins;
+            int width = mUpView.getVisibility() == GONE ? 0 : mStartOffset;
+            int height = upLp.topMargin + mUpView.getMeasuredHeight() + upLp.bottomMargin;
+
+            if (mIconView.getVisibility() != GONE) {
+                measureChildWithMargins(mIconView, widthMeasureSpec, width, heightMeasureSpec, 0);
+                final LayoutParams iconLp = (LayoutParams) mIconView.getLayoutParams();
+                width += iconLp.leftMargin + mIconView.getMeasuredWidth() + iconLp.rightMargin;
+                height = Math.max(height,
+                        iconLp.topMargin + mIconView.getMeasuredHeight() + iconLp.bottomMargin);
+            } else if (upMargins < 0) {
+                // Remove the measurement effects of negative margins used for offsets
+                width -= upMargins;
+            }
+
+            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+            switch (widthMode) {
+                case MeasureSpec.AT_MOST:
+                    width = Math.min(width, widthSize);
+                    break;
+                case MeasureSpec.EXACTLY:
+                    width = widthSize;
+                    break;
+                case MeasureSpec.UNSPECIFIED:
+                default:
+                    break;
+            }
+            switch (heightMode) {
+                case MeasureSpec.AT_MOST:
+                    height = Math.min(height, heightSize);
+                    break;
+                case MeasureSpec.EXACTLY:
+                    height = heightSize;
+                    break;
+                case MeasureSpec.UNSPECIFIED:
+                default:
+                    break;
+            }
+            setMeasuredDimension(width, height);
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {
+            final int vCenter = (b - t) / 2;
+            final boolean isLayoutRtl = isLayoutRtl();
+            final int width = getWidth();
+            int upOffset = 0;
+            if (mUpView.getVisibility() != GONE) {
+                final LayoutParams upLp = (LayoutParams) mUpView.getLayoutParams();
+                final int upHeight = mUpView.getMeasuredHeight();
+                final int upWidth = mUpView.getMeasuredWidth();
+                upOffset = upLp.leftMargin + upWidth + upLp.rightMargin;
+                final int upTop = vCenter - upHeight / 2;
+                final int upBottom = upTop + upHeight;
+                final int upRight;
+                final int upLeft;
+                if (isLayoutRtl) {
+                    upRight = width;
+                    upLeft = upRight - upWidth;
+                    r -= upOffset;
+                } else {
+                    upRight = upWidth;
+                    upLeft = 0;
+                    l += upOffset;
+                }
+                mUpView.layout(upLeft, upTop, upRight, upBottom);
+            }
+
+            final LayoutParams iconLp = (LayoutParams) mIconView.getLayoutParams();
+            final int iconHeight = mIconView.getMeasuredHeight();
+            final int iconWidth = mIconView.getMeasuredWidth();
+            final int hCenter = (r - l) / 2;
+            final int iconTop = Math.max(iconLp.topMargin, vCenter - iconHeight / 2);
+            final int iconBottom = iconTop + iconHeight;
+            final int iconLeft;
+            final int iconRight;
+            int marginStart = iconLp.getMarginStart();
+            final int delta = Math.max(marginStart, hCenter - iconWidth / 2);
+            if (isLayoutRtl) {
+                iconRight = width - upOffset - delta;
+                iconLeft = iconRight - iconWidth;
+            } else {
+                iconLeft = upOffset + delta;
+                iconRight = iconLeft + iconWidth;
+            }
+            mIconView.layout(iconLeft, iconTop, iconRight, iconBottom);
+        }
+    }
+
+    private class ExpandedActionViewMenuPresenter implements MenuPresenter {
+        MenuBuilder mMenu;
+        MenuItemImpl mCurrentExpandedItem;
+
+        @Override
+        public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+            // Clear the expanded action view when menus change.
+            if (mMenu != null && mCurrentExpandedItem != null) {
+                mMenu.collapseItemActionView(mCurrentExpandedItem);
+            }
+            mMenu = menu;
+        }
+
+        @Override
+        public MenuView getMenuView(ViewGroup root) {
+            return null;
+        }
+
+        @Override
+        public void updateMenuView(boolean cleared) {
+            // Make sure the expanded item we have is still there.
+            if (mCurrentExpandedItem != null) {
+                boolean found = false;
+
+                if (mMenu != null) {
+                    final int count = mMenu.size();
+                    for (int i = 0; i < count; i++) {
+                        final MenuItem item = mMenu.getItem(i);
+                        if (item == mCurrentExpandedItem) {
+                            found = true;
+                            break;
+                        }
+                    }
+                }
+
+                if (!found) {
+                    // The item we had expanded disappeared. Collapse.
+                    collapseItemActionView(mMenu, mCurrentExpandedItem);
+                }
+            }
+        }
+
+        @Override
+        public void setCallback(Callback cb) {
+        }
+
+        @Override
+        public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+            return false;
+        }
+
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        }
+
+        @Override
+        public boolean flagActionItems() {
+            return false;
+        }
+
+        @Override
+        public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+
+            mExpandedActionView = item.getActionView();
+            mExpandedHomeLayout.setIcon(mIcon.getConstantState().newDrawable(getResources()));
+            mCurrentExpandedItem = item;
+            if (mExpandedActionView.getParent() != ActionBarView.this) {
+                addView(mExpandedActionView);
+            }
+            if (mExpandedHomeLayout.getParent() != mUpGoerFive) {
+                mUpGoerFive.addView(mExpandedHomeLayout);
+            }
+            mHomeLayout.setVisibility(GONE);
+            if (mTitleLayout != null) mTitleLayout.setVisibility(GONE);
+            if (mTabScrollView != null) mTabScrollView.setVisibility(GONE);
+            if (mSpinner != null) mSpinner.setVisibility(GONE);
+            if (mCustomNavView != null) mCustomNavView.setVisibility(GONE);
+            setHomeButtonEnabled(false, false);
+            requestLayout();
+            item.setActionViewExpanded(true);
+
+            if (mExpandedActionView instanceof CollapsibleActionView) {
+                ((CollapsibleActionView) mExpandedActionView).onActionViewExpanded();
+            }
+
+            return true;
+        }
+
+        @Override
+        public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+
+            // Do this before detaching the actionview from the hierarchy, in case
+            // it needs to dismiss the soft keyboard, etc.
+            if (mExpandedActionView instanceof CollapsibleActionView) {
+                ((CollapsibleActionView) mExpandedActionView).onActionViewCollapsed();
+            }
+
+            removeView(mExpandedActionView);
+            mUpGoerFive.removeView(mExpandedHomeLayout);
+            mExpandedActionView = null;
+            if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_HOME) != 0) {
+                mHomeLayout.setVisibility(VISIBLE);
+            }
+            if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+                if (mTitleLayout == null) {
+                    initTitle();
+                } else {
+                    mTitleLayout.setVisibility(VISIBLE);
+                }
+            }
+            if (mTabScrollView != null) mTabScrollView.setVisibility(VISIBLE);
+            if (mSpinner != null) mSpinner.setVisibility(VISIBLE);
+            if (mCustomNavView != null) mCustomNavView.setVisibility(VISIBLE);
+
+            mExpandedHomeLayout.setIcon(null);
+            mCurrentExpandedItem = null;
+            setHomeButtonEnabled(mWasHomeEnabled); // Set by expandItemActionView above
+            requestLayout();
+            item.setActionViewExpanded(false);
+
+            return true;
+        }
+
+        @Override
+        public int getId() {
+            return 0;
+        }
+
+        @Override
+        public Parcelable onSaveInstanceState() {
+            return null;
+        }
+
+        @Override
+        public void onRestoreInstanceState(Parcelable state) {
+        }
+    }
+}
diff --git a/com/android/internal/widget/AdapterHelper.java b/com/android/internal/widget/AdapterHelper.java
new file mode 100644
index 0000000..f47d430
--- /dev/null
+++ b/com/android/internal/widget/AdapterHelper.java
@@ -0,0 +1,775 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.util.Log;
+import android.util.Pools;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Helper class that can enqueue and process adapter update operations.
+ * <p>
+ * To support animations, RecyclerView presents an older version the Adapter to best represent
+ * previous state of the layout. Sometimes, this is not trivial when items are removed that were
+ * not laid out, in which case, RecyclerView has no way of providing that item's view for
+ * animations.
+ * <p>
+ * AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
+ * pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
+ * and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
+ * according to previously deferred operation and dispatch them before the first layout pass. It
+ * also takes care of updating deferred UpdateOps since order of operations is changed by this
+ * process.
+ * <p>
+ * Although operations may be forwarded to LayoutManager in different orders, resulting data set
+ * is guaranteed to be the consistent.
+ */
+class AdapterHelper implements OpReorderer.Callback {
+
+    static final int POSITION_TYPE_INVISIBLE = 0;
+
+    static final int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
+
+    private static final boolean DEBUG = false;
+
+    private static final String TAG = "AHT";
+
+    private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
+
+    final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
+
+    final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
+
+    final Callback mCallback;
+
+    Runnable mOnItemProcessedCallback;
+
+    final boolean mDisableRecycler;
+
+    final OpReorderer mOpReorderer;
+
+    private int mExistingUpdateTypes = 0;
+
+    AdapterHelper(Callback callback) {
+        this(callback, false);
+    }
+
+    AdapterHelper(Callback callback, boolean disableRecycler) {
+        mCallback = callback;
+        mDisableRecycler = disableRecycler;
+        mOpReorderer = new OpReorderer(this);
+    }
+
+    AdapterHelper addUpdateOp(UpdateOp... ops) {
+        Collections.addAll(mPendingUpdates, ops);
+        return this;
+    }
+
+    void reset() {
+        recycleUpdateOpsAndClearList(mPendingUpdates);
+        recycleUpdateOpsAndClearList(mPostponedList);
+        mExistingUpdateTypes = 0;
+    }
+
+    void preProcess() {
+        mOpReorderer.reorderOps(mPendingUpdates);
+        final int count = mPendingUpdates.size();
+        for (int i = 0; i < count; i++) {
+            UpdateOp op = mPendingUpdates.get(i);
+            switch (op.cmd) {
+                case UpdateOp.ADD:
+                    applyAdd(op);
+                    break;
+                case UpdateOp.REMOVE:
+                    applyRemove(op);
+                    break;
+                case UpdateOp.UPDATE:
+                    applyUpdate(op);
+                    break;
+                case UpdateOp.MOVE:
+                    applyMove(op);
+                    break;
+            }
+            if (mOnItemProcessedCallback != null) {
+                mOnItemProcessedCallback.run();
+            }
+        }
+        mPendingUpdates.clear();
+    }
+
+    void consumePostponedUpdates() {
+        final int count = mPostponedList.size();
+        for (int i = 0; i < count; i++) {
+            mCallback.onDispatchSecondPass(mPostponedList.get(i));
+        }
+        recycleUpdateOpsAndClearList(mPostponedList);
+        mExistingUpdateTypes = 0;
+    }
+
+    private void applyMove(UpdateOp op) {
+        // MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
+        // otherwise, it would be converted into a REMOVE operation
+        postponeAndUpdateViewHolders(op);
+    }
+
+    private void applyRemove(UpdateOp op) {
+        int tmpStart = op.positionStart;
+        int tmpCount = 0;
+        int tmpEnd = op.positionStart + op.itemCount;
+        int type = -1;
+        for (int position = op.positionStart; position < tmpEnd; position++) {
+            boolean typeChanged = false;
+            RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
+            if (vh != null || canFindInPreLayout(position)) {
+                // If a ViewHolder exists or this is a newly added item, we can defer this update
+                // to post layout stage.
+                // * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
+                // * For items that are added and removed in the same process cycle, they won't
+                // have any effect in pre-layout since their add ops are already deferred to
+                // post-layout pass.
+                if (type == POSITION_TYPE_INVISIBLE) {
+                    // Looks like we have other updates that we cannot merge with this one.
+                    // Create an UpdateOp and dispatch it to LayoutManager.
+                    UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
+                    dispatchAndUpdateViewHolders(newOp);
+                    typeChanged = true;
+                }
+                type = POSITION_TYPE_NEW_OR_LAID_OUT;
+            } else {
+                // This update cannot be recovered because we don't have a ViewHolder representing
+                // this position. Instead, post it to LayoutManager immediately
+                if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
+                    // Looks like we have other updates that we cannot merge with this one.
+                    // Create UpdateOp op and dispatch it to LayoutManager.
+                    UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
+                    postponeAndUpdateViewHolders(newOp);
+                    typeChanged = true;
+                }
+                type = POSITION_TYPE_INVISIBLE;
+            }
+            if (typeChanged) {
+                position -= tmpCount; // also equal to tmpStart
+                tmpEnd -= tmpCount;
+                tmpCount = 1;
+            } else {
+                tmpCount++;
+            }
+        }
+        if (tmpCount != op.itemCount) { // all 1 effect
+            recycleUpdateOp(op);
+            op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
+        }
+        if (type == POSITION_TYPE_INVISIBLE) {
+            dispatchAndUpdateViewHolders(op);
+        } else {
+            postponeAndUpdateViewHolders(op);
+        }
+    }
+
+    private void applyUpdate(UpdateOp op) {
+        int tmpStart = op.positionStart;
+        int tmpCount = 0;
+        int tmpEnd = op.positionStart + op.itemCount;
+        int type = -1;
+        for (int position = op.positionStart; position < tmpEnd; position++) {
+            RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
+            if (vh != null || canFindInPreLayout(position)) { // deferred
+                if (type == POSITION_TYPE_INVISIBLE) {
+                    UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
+                            op.payload);
+                    dispatchAndUpdateViewHolders(newOp);
+                    tmpCount = 0;
+                    tmpStart = position;
+                }
+                type = POSITION_TYPE_NEW_OR_LAID_OUT;
+            } else { // applied
+                if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
+                    UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
+                            op.payload);
+                    postponeAndUpdateViewHolders(newOp);
+                    tmpCount = 0;
+                    tmpStart = position;
+                }
+                type = POSITION_TYPE_INVISIBLE;
+            }
+            tmpCount++;
+        }
+        if (tmpCount != op.itemCount) { // all 1 effect
+            Object payload = op.payload;
+            recycleUpdateOp(op);
+            op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
+        }
+        if (type == POSITION_TYPE_INVISIBLE) {
+            dispatchAndUpdateViewHolders(op);
+        } else {
+            postponeAndUpdateViewHolders(op);
+        }
+    }
+
+    private void dispatchAndUpdateViewHolders(UpdateOp op) {
+        // tricky part.
+        // traverse all postpones and revert their changes on this op if necessary, apply updated
+        // dispatch to them since now they are after this op.
+        if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
+            throw new IllegalArgumentException("should not dispatch add or move for pre layout");
+        }
+        if (DEBUG) {
+            Log.d(TAG, "dispatch (pre)" + op);
+            Log.d(TAG, "postponed state before:");
+            for (UpdateOp updateOp : mPostponedList) {
+                Log.d(TAG, updateOp.toString());
+            }
+            Log.d(TAG, "----");
+        }
+
+        // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
+        // TODO Since move ops are pushed to end, we should not need this anymore
+        int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
+        if (DEBUG) {
+            Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
+        }
+        int tmpCnt = 1;
+        int offsetPositionForPartial = op.positionStart;
+        final int positionMultiplier;
+        switch (op.cmd) {
+            case UpdateOp.UPDATE:
+                positionMultiplier = 1;
+                break;
+            case UpdateOp.REMOVE:
+                positionMultiplier = 0;
+                break;
+            default:
+                throw new IllegalArgumentException("op should be remove or update." + op);
+        }
+        for (int p = 1; p < op.itemCount; p++) {
+            final int pos = op.positionStart + (positionMultiplier * p);
+            int updatedPos = updatePositionWithPostponed(pos, op.cmd);
+            if (DEBUG) {
+                Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
+            }
+            boolean continuous = false;
+            switch (op.cmd) {
+                case UpdateOp.UPDATE:
+                    continuous = updatedPos == tmpStart + 1;
+                    break;
+                case UpdateOp.REMOVE:
+                    continuous = updatedPos == tmpStart;
+                    break;
+            }
+            if (continuous) {
+                tmpCnt++;
+            } else {
+                // need to dispatch this separately
+                UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, op.payload);
+                if (DEBUG) {
+                    Log.d(TAG, "need to dispatch separately " + tmp);
+                }
+                dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
+                recycleUpdateOp(tmp);
+                if (op.cmd == UpdateOp.UPDATE) {
+                    offsetPositionForPartial += tmpCnt;
+                }
+                tmpStart = updatedPos; // need to remove previously dispatched
+                tmpCnt = 1;
+            }
+        }
+        Object payload = op.payload;
+        recycleUpdateOp(op);
+        if (tmpCnt > 0) {
+            UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload);
+            if (DEBUG) {
+                Log.d(TAG, "dispatching:" + tmp);
+            }
+            dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
+            recycleUpdateOp(tmp);
+        }
+        if (DEBUG) {
+            Log.d(TAG, "post dispatch");
+            Log.d(TAG, "postponed state after:");
+            for (UpdateOp updateOp : mPostponedList) {
+                Log.d(TAG, updateOp.toString());
+            }
+            Log.d(TAG, "----");
+        }
+    }
+
+    void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
+        mCallback.onDispatchFirstPass(op);
+        switch (op.cmd) {
+            case UpdateOp.REMOVE:
+                mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
+                break;
+            case UpdateOp.UPDATE:
+                mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);
+                break;
+            default:
+                throw new IllegalArgumentException("only remove and update ops can be dispatched"
+                        + " in first pass");
+        }
+    }
+
+    private int updatePositionWithPostponed(int pos, int cmd) {
+        final int count = mPostponedList.size();
+        for (int i = count - 1; i >= 0; i--) {
+            UpdateOp postponed = mPostponedList.get(i);
+            if (postponed.cmd == UpdateOp.MOVE) {
+                int start, end;
+                if (postponed.positionStart < postponed.itemCount) {
+                    start = postponed.positionStart;
+                    end = postponed.itemCount;
+                } else {
+                    start = postponed.itemCount;
+                    end = postponed.positionStart;
+                }
+                if (pos >= start && pos <= end) {
+                    //i'm affected
+                    if (start == postponed.positionStart) {
+                        if (cmd == UpdateOp.ADD) {
+                            postponed.itemCount++;
+                        } else if (cmd == UpdateOp.REMOVE) {
+                            postponed.itemCount--;
+                        }
+                        // op moved to left, move it right to revert
+                        pos++;
+                    } else {
+                        if (cmd == UpdateOp.ADD) {
+                            postponed.positionStart++;
+                        } else if (cmd == UpdateOp.REMOVE) {
+                            postponed.positionStart--;
+                        }
+                        // op was moved right, move left to revert
+                        pos--;
+                    }
+                } else if (pos < postponed.positionStart) {
+                    // postponed MV is outside the dispatched OP. if it is before, offset
+                    if (cmd == UpdateOp.ADD) {
+                        postponed.positionStart++;
+                        postponed.itemCount++;
+                    } else if (cmd == UpdateOp.REMOVE) {
+                        postponed.positionStart--;
+                        postponed.itemCount--;
+                    }
+                }
+            } else {
+                if (postponed.positionStart <= pos) {
+                    if (postponed.cmd == UpdateOp.ADD) {
+                        pos -= postponed.itemCount;
+                    } else if (postponed.cmd == UpdateOp.REMOVE) {
+                        pos += postponed.itemCount;
+                    }
+                } else {
+                    if (cmd == UpdateOp.ADD) {
+                        postponed.positionStart++;
+                    } else if (cmd == UpdateOp.REMOVE) {
+                        postponed.positionStart--;
+                    }
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "dispath (step" + i + ")");
+                Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
+                for (UpdateOp updateOp : mPostponedList) {
+                    Log.d(TAG, updateOp.toString());
+                }
+                Log.d(TAG, "----");
+            }
+        }
+        for (int i = mPostponedList.size() - 1; i >= 0; i--) {
+            UpdateOp op = mPostponedList.get(i);
+            if (op.cmd == UpdateOp.MOVE) {
+                if (op.itemCount == op.positionStart || op.itemCount < 0) {
+                    mPostponedList.remove(i);
+                    recycleUpdateOp(op);
+                }
+            } else if (op.itemCount <= 0) {
+                mPostponedList.remove(i);
+                recycleUpdateOp(op);
+            }
+        }
+        return pos;
+    }
+
+    private boolean canFindInPreLayout(int position) {
+        final int count = mPostponedList.size();
+        for (int i = 0; i < count; i++) {
+            UpdateOp op = mPostponedList.get(i);
+            if (op.cmd == UpdateOp.MOVE) {
+                if (findPositionOffset(op.itemCount, i + 1) == position) {
+                    return true;
+                }
+            } else if (op.cmd == UpdateOp.ADD) {
+                // TODO optimize.
+                final int end = op.positionStart + op.itemCount;
+                for (int pos = op.positionStart; pos < end; pos++) {
+                    if (findPositionOffset(pos, i + 1) == position) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private void applyAdd(UpdateOp op) {
+        postponeAndUpdateViewHolders(op);
+    }
+
+    private void postponeAndUpdateViewHolders(UpdateOp op) {
+        if (DEBUG) {
+            Log.d(TAG, "postponing " + op);
+        }
+        mPostponedList.add(op);
+        switch (op.cmd) {
+            case UpdateOp.ADD:
+                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
+                break;
+            case UpdateOp.MOVE:
+                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
+                break;
+            case UpdateOp.REMOVE:
+                mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
+                        op.itemCount);
+                break;
+            case UpdateOp.UPDATE:
+                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown update op type for " + op);
+        }
+    }
+
+    boolean hasPendingUpdates() {
+        return mPendingUpdates.size() > 0;
+    }
+
+    boolean hasAnyUpdateTypes(int updateTypes) {
+        return (mExistingUpdateTypes & updateTypes) != 0;
+    }
+
+    int findPositionOffset(int position) {
+        return findPositionOffset(position, 0);
+    }
+
+    int findPositionOffset(int position, int firstPostponedItem) {
+        int count = mPostponedList.size();
+        for (int i = firstPostponedItem; i < count; ++i) {
+            UpdateOp op = mPostponedList.get(i);
+            if (op.cmd == UpdateOp.MOVE) {
+                if (op.positionStart == position) {
+                    position = op.itemCount;
+                } else {
+                    if (op.positionStart < position) {
+                        position--; // like a remove
+                    }
+                    if (op.itemCount <= position) {
+                        position++; // like an add
+                    }
+                }
+            } else if (op.positionStart <= position) {
+                if (op.cmd == UpdateOp.REMOVE) {
+                    if (position < op.positionStart + op.itemCount) {
+                        return -1;
+                    }
+                    position -= op.itemCount;
+                } else if (op.cmd == UpdateOp.ADD) {
+                    position += op.itemCount;
+                }
+            }
+        }
+        return position;
+    }
+
+    /**
+     * @return True if updates should be processed.
+     */
+    boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+        if (itemCount < 1) {
+            return false;
+        }
+        mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
+        mExistingUpdateTypes |= UpdateOp.UPDATE;
+        return mPendingUpdates.size() == 1;
+    }
+
+    /**
+     * @return True if updates should be processed.
+     */
+    boolean onItemRangeInserted(int positionStart, int itemCount) {
+        if (itemCount < 1) {
+            return false;
+        }
+        mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
+        mExistingUpdateTypes |= UpdateOp.ADD;
+        return mPendingUpdates.size() == 1;
+    }
+
+    /**
+     * @return True if updates should be processed.
+     */
+    boolean onItemRangeRemoved(int positionStart, int itemCount) {
+        if (itemCount < 1) {
+            return false;
+        }
+        mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
+        mExistingUpdateTypes |= UpdateOp.REMOVE;
+        return mPendingUpdates.size() == 1;
+    }
+
+    /**
+     * @return True if updates should be processed.
+     */
+    boolean onItemRangeMoved(int from, int to, int itemCount) {
+        if (from == to) {
+            return false; // no-op
+        }
+        if (itemCount != 1) {
+            throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
+        }
+        mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null));
+        mExistingUpdateTypes |= UpdateOp.MOVE;
+        return mPendingUpdates.size() == 1;
+    }
+
+    /**
+     * Skips pre-processing and applies all updates in one pass.
+     */
+    void consumeUpdatesInOnePass() {
+        // we still consume postponed updates (if there is) in case there was a pre-process call
+        // w/o a matching consumePostponedUpdates.
+        consumePostponedUpdates();
+        final int count = mPendingUpdates.size();
+        for (int i = 0; i < count; i++) {
+            UpdateOp op = mPendingUpdates.get(i);
+            switch (op.cmd) {
+                case UpdateOp.ADD:
+                    mCallback.onDispatchSecondPass(op);
+                    mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
+                    break;
+                case UpdateOp.REMOVE:
+                    mCallback.onDispatchSecondPass(op);
+                    mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
+                    break;
+                case UpdateOp.UPDATE:
+                    mCallback.onDispatchSecondPass(op);
+                    mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
+                    break;
+                case UpdateOp.MOVE:
+                    mCallback.onDispatchSecondPass(op);
+                    mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
+                    break;
+            }
+            if (mOnItemProcessedCallback != null) {
+                mOnItemProcessedCallback.run();
+            }
+        }
+        recycleUpdateOpsAndClearList(mPendingUpdates);
+        mExistingUpdateTypes = 0;
+    }
+
+    public int applyPendingUpdatesToPosition(int position) {
+        final int size = mPendingUpdates.size();
+        for (int i = 0; i < size; i++) {
+            UpdateOp op = mPendingUpdates.get(i);
+            switch (op.cmd) {
+                case UpdateOp.ADD:
+                    if (op.positionStart <= position) {
+                        position += op.itemCount;
+                    }
+                    break;
+                case UpdateOp.REMOVE:
+                    if (op.positionStart <= position) {
+                        final int end = op.positionStart + op.itemCount;
+                        if (end > position) {
+                            return RecyclerView.NO_POSITION;
+                        }
+                        position -= op.itemCount;
+                    }
+                    break;
+                case UpdateOp.MOVE:
+                    if (op.positionStart == position) {
+                        position = op.itemCount; //position end
+                    } else {
+                        if (op.positionStart < position) {
+                            position -= 1;
+                        }
+                        if (op.itemCount <= position) {
+                            position += 1;
+                        }
+                    }
+                    break;
+            }
+        }
+        return position;
+    }
+
+    boolean hasUpdates() {
+        return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty();
+    }
+
+    /**
+     * Queued operation to happen when child views are updated.
+     */
+    static class UpdateOp {
+
+        static final int ADD = 1;
+
+        static final int REMOVE = 1 << 1;
+
+        static final int UPDATE = 1 << 2;
+
+        static final int MOVE = 1 << 3;
+
+        static final int POOL_SIZE = 30;
+
+        int cmd;
+
+        int positionStart;
+
+        Object payload;
+
+        // holds the target position if this is a MOVE
+        int itemCount;
+
+        UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
+            this.cmd = cmd;
+            this.positionStart = positionStart;
+            this.itemCount = itemCount;
+            this.payload = payload;
+        }
+
+        String cmdToString() {
+            switch (cmd) {
+                case ADD:
+                    return "add";
+                case REMOVE:
+                    return "rm";
+                case UPDATE:
+                    return "up";
+                case MOVE:
+                    return "mv";
+            }
+            return "??";
+        }
+
+        @Override
+        public String toString() {
+            return Integer.toHexString(System.identityHashCode(this))
+                    + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
+                    + ",p:" + payload + "]";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            UpdateOp op = (UpdateOp) o;
+
+            if (cmd != op.cmd) {
+                return false;
+            }
+            if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
+                // reverse of this is also true
+                if (itemCount == op.positionStart && positionStart == op.itemCount) {
+                    return true;
+                }
+            }
+            if (itemCount != op.itemCount) {
+                return false;
+            }
+            if (positionStart != op.positionStart) {
+                return false;
+            }
+            if (payload != null) {
+                if (!payload.equals(op.payload)) {
+                    return false;
+                }
+            } else if (op.payload != null) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = cmd;
+            result = 31 * result + positionStart;
+            result = 31 * result + itemCount;
+            return result;
+        }
+    }
+
+    @Override
+    public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
+        UpdateOp op = mUpdateOpPool.acquire();
+        if (op == null) {
+            op = new UpdateOp(cmd, positionStart, itemCount, payload);
+        } else {
+            op.cmd = cmd;
+            op.positionStart = positionStart;
+            op.itemCount = itemCount;
+            op.payload = payload;
+        }
+        return op;
+    }
+
+    @Override
+    public void recycleUpdateOp(UpdateOp op) {
+        if (!mDisableRecycler) {
+            op.payload = null;
+            mUpdateOpPool.release(op);
+        }
+    }
+
+    void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
+        final int count = ops.size();
+        for (int i = 0; i < count; i++) {
+            recycleUpdateOp(ops.get(i));
+        }
+        ops.clear();
+    }
+
+    /**
+     * Contract between AdapterHelper and RecyclerView.
+     */
+    interface Callback {
+
+        RecyclerView.ViewHolder findViewHolder(int position);
+
+        void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
+
+        void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
+
+        void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads);
+
+        void onDispatchFirstPass(UpdateOp updateOp);
+
+        void onDispatchSecondPass(UpdateOp updateOp);
+
+        void offsetPositionsForAdd(int positionStart, int itemCount);
+
+        void offsetPositionsForMove(int from, int to);
+    }
+}
diff --git a/com/android/internal/widget/AlertDialogLayout.java b/com/android/internal/widget/AlertDialogLayout.java
new file mode 100644
index 0000000..9bf0948
--- /dev/null
+++ b/com/android/internal/widget/AlertDialogLayout.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.internal.R;
+
+/**
+ * Special implementation of linear layout that's capable of laying out alert
+ * dialog components.
+ * <p>
+ * A dialog consists of up to three panels. All panels are optional, and a
+ * dialog may contain only a single panel. The panels are laid out according
+ * to the following guidelines:
+ * <ul>
+ *     <li>topPanel: exactly wrap_content</li>
+ *     <li>contentPanel OR customPanel: at most fill_parent, first priority for
+ *         extra space</li>
+ *     <li>buttonPanel: at least minHeight, at most wrap_content, second
+ *         priority for extra space</li>
+ * </ul>
+ */
+public class AlertDialogLayout extends LinearLayout {
+
+    public AlertDialogLayout(@Nullable Context context) {
+        super(context);
+    }
+
+    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) {
+            // Failed to perform custom measurement, let superclass handle it.
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        View topPanel = null;
+        View buttonPanel = null;
+        View middlePanel = null;
+
+        final int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == View.GONE) {
+                continue;
+            }
+
+            final int id = child.getId();
+            switch (id) {
+                case R.id.topPanel:
+                    topPanel = child;
+                    break;
+                case R.id.buttonPanel:
+                    buttonPanel = child;
+                    break;
+                case R.id.contentPanel:
+                case R.id.customPanel:
+                    if (middlePanel != null) {
+                        // Both the content and custom are visible. Abort!
+                        return false;
+                    }
+                    middlePanel = child;
+                    break;
+                default:
+                    // Unknown top-level child. Abort!
+                    return false;
+            }
+        }
+
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+
+        int childState = 0;
+        int usedHeight = getPaddingTop() + getPaddingBottom();
+
+        if (topPanel != null) {
+            topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
+
+            usedHeight += topPanel.getMeasuredHeight();
+            childState = combineMeasuredStates(childState, topPanel.getMeasuredState());
+        }
+
+        int buttonHeight = 0;
+        int buttonWantsHeight = 0;
+        if (buttonPanel != null) {
+            buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
+            buttonHeight = resolveMinimumHeight(buttonPanel);
+            buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight;
+
+            usedHeight += buttonHeight;
+            childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
+        }
+
+        int middleHeight = 0;
+        if (middlePanel != null) {
+            final int childHeightSpec;
+            if (heightMode == MeasureSpec.UNSPECIFIED) {
+                childHeightSpec = MeasureSpec.UNSPECIFIED;
+            } else {
+                childHeightSpec = MeasureSpec.makeMeasureSpec(
+                        Math.max(0, heightSize - usedHeight), heightMode);
+            }
+
+            middlePanel.measure(widthMeasureSpec, childHeightSpec);
+            middleHeight = middlePanel.getMeasuredHeight();
+
+            usedHeight += middleHeight;
+            childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
+        }
+
+        int remainingHeight = heightSize - usedHeight;
+
+        // Time for the "real" button measure pass. If we have remaining space,
+        // make the button pane bigger up to its target height. Otherwise,
+        // just remeasure the button at whatever height it needs.
+        if (buttonPanel != null) {
+            usedHeight -= buttonHeight;
+
+            final int heightToGive = Math.min(remainingHeight, buttonWantsHeight);
+            if (heightToGive > 0) {
+                remainingHeight -= heightToGive;
+                buttonHeight += heightToGive;
+            }
+
+            final int childHeightSpec = MeasureSpec.makeMeasureSpec(
+                    buttonHeight, MeasureSpec.EXACTLY);
+            buttonPanel.measure(widthMeasureSpec, childHeightSpec);
+
+            usedHeight += buttonPanel.getMeasuredHeight();
+            childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
+        }
+
+        // If we still have remaining space, make the middle pane bigger up
+        // to the maximum height.
+        if (middlePanel != null && remainingHeight > 0) {
+            usedHeight -= middleHeight;
+
+            final int heightToGive = remainingHeight;
+            remainingHeight -= heightToGive;
+            middleHeight += heightToGive;
+
+            // Pass the same height mode as we're using for the dialog itself.
+            // If it's EXACTLY, then the middle pane MUST use the entire
+            // height.
+            final int childHeightSpec = MeasureSpec.makeMeasureSpec(
+                    middleHeight, heightMode);
+            middlePanel.measure(widthMeasureSpec, childHeightSpec);
+
+            usedHeight += middlePanel.getMeasuredHeight();
+            childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
+        }
+
+        // Compute desired width as maximum child width.
+        int maxWidth = 0;
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE) {
+                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+            }
+        }
+
+        maxWidth += getPaddingLeft() + getPaddingRight();
+
+        final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState);
+        final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0);
+        setMeasuredDimension(widthSizeAndState, heightSizeAndState);
+
+        // If the children weren't already measured EXACTLY, we need to run
+        // another measure pass to for MATCH_PARENT widths.
+        if (widthMode != MeasureSpec.EXACTLY) {
+            forceUniformWidth(count, heightMeasureSpec);
+        }
+
+        return true;
+    }
+
+    /**
+     * Remeasures child views to exactly match the layout's measured width.
+     *
+     * @param count the number of child views
+     * @param heightMeasureSpec the original height measure spec
+     */
+    private void forceUniformWidth(int count, int heightMeasureSpec) {
+        // Pretend that the linear layout has an exact size.
+        final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(
+                getMeasuredWidth(), MeasureSpec.EXACTLY);
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp.width == LayoutParams.MATCH_PARENT) {
+                    // Temporarily force children to reuse their old measured
+                    // height.
+                    final int oldHeight = lp.height;
+                    lp.height = child.getMeasuredHeight();
+
+                    // Remeasure with new dimensions.
+                    measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
+                    lp.height = oldHeight;
+                }
+            }
+        }
+    }
+
+    /**
+     * Attempts to resolve the minimum height of a view.
+     * <p>
+     * If the view doesn't have a minimum height set and only contains a single
+     * child, attempts to resolve the minimum height of the child view.
+     *
+     * @param v the view whose minimum height to resolve
+     * @return the minimum height
+     */
+    private int resolveMinimumHeight(View v) {
+        final int minHeight = v.getMinimumHeight();
+        if (minHeight > 0) {
+            return minHeight;
+        }
+
+        if (v instanceof ViewGroup) {
+            final ViewGroup vg = (ViewGroup) v;
+            if (vg.getChildCount() == 1) {
+                return resolveMinimumHeight(vg.getChildAt(0));
+            }
+        }
+
+        return 0;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int paddingLeft = mPaddingLeft;
+
+        // Where right end of child should go
+        final int width = right - left;
+        final int childRight = width - mPaddingRight;
+
+        // Space available for child
+        final int childSpace = width - paddingLeft - mPaddingRight;
+
+        final int totalLength = getMeasuredHeight();
+        final int count = getChildCount();
+        final int gravity = getGravity();
+        final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+        final int minorGravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+
+        int childTop;
+        switch (majorGravity) {
+            case Gravity.BOTTOM:
+                // totalLength contains the padding already
+                childTop = mPaddingTop + bottom - top - totalLength;
+                break;
+
+            // totalLength contains the padding already
+            case Gravity.CENTER_VERTICAL:
+                childTop = mPaddingTop + (bottom - top - totalLength) / 2;
+                break;
+
+            case Gravity.TOP:
+            default:
+                childTop = mPaddingTop;
+                break;
+        }
+
+        final Drawable dividerDrawable = getDividerDrawable();
+        final int dividerHeight = dividerDrawable == null ?
+                0 : dividerDrawable.getIntrinsicHeight();
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child != null && child.getVisibility() != GONE) {
+                final int childWidth = child.getMeasuredWidth();
+                final int childHeight = child.getMeasuredHeight();
+
+                final LinearLayout.LayoutParams lp =
+                        (LinearLayout.LayoutParams) child.getLayoutParams();
+
+                int layoutGravity = lp.gravity;
+                if (layoutGravity < 0) {
+                    layoutGravity = minorGravity;
+                }
+                final int layoutDirection = getLayoutDirection();
+                final int absoluteGravity = Gravity.getAbsoluteGravity(
+                        layoutGravity, layoutDirection);
+
+                final int childLeft;
+                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                    case Gravity.CENTER_HORIZONTAL:
+                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+                                + lp.leftMargin - lp.rightMargin;
+                        break;
+
+                    case Gravity.RIGHT:
+                        childLeft = childRight - childWidth - lp.rightMargin;
+                        break;
+
+                    case Gravity.LEFT:
+                    default:
+                        childLeft = paddingLeft + lp.leftMargin;
+                        break;
+                }
+
+                if (hasDividerBeforeChildAt(i)) {
+                    childTop += dividerHeight;
+                }
+
+                childTop += lp.topMargin;
+                setChildFrame(child, childLeft, childTop, childWidth, childHeight);
+                childTop += childHeight + lp.bottomMargin;
+            }
+        }
+    }
+
+    private void setChildFrame(View child, int left, int top, int width, int height) {
+        child.layout(left, top, left + width, top + height);
+    }
+}
diff --git a/com/android/internal/widget/AutoScrollHelper.java b/com/android/internal/widget/AutoScrollHelper.java
new file mode 100644
index 0000000..0d468ca
--- /dev/null
+++ b/com/android/internal/widget/AutoScrollHelper.java
@@ -0,0 +1,928 @@
+/*
+ * 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.internal.widget;
+
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.AbsListView;
+
+/**
+ * AutoScrollHelper is a utility class for adding automatic edge-triggered
+ * scrolling to Views.
+ * <p>
+ * <b>Note:</b> Implementing classes are responsible for overriding the
+ * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
+ * {@link #canTargetScrollVertically} methods. See
+ * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
+ * -specific implementation.
+ * <p>
+ * <h1>Activation</h1> Automatic scrolling starts when the user touches within
+ * an activation area. By default, activation areas are defined as the top,
+ * left, right, and bottom 20% of the host view's total area. Touching within
+ * the top activation area scrolls up, left scrolls to the left, and so on.
+ * <p>
+ * As the user touches closer to the extreme edge of the activation area,
+ * scrolling accelerates up to a maximum velocity. When using the default edge
+ * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
+ * will scroll at the maximum velocity.
+ * <p>
+ * The following activation properties may be configured:
+ * <ul>
+ * <li>Delay after entering activation area before auto-scrolling begins, see
+ * {@link #setActivationDelay}. Default value is
+ * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
+ * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
+ * {@link #EDGE_TYPE_INSIDE_EXTEND}.
+ * <li>Size of activation areas relative to view size, see
+ * {@link #setRelativeEdges}. Default value is 20% for both vertical and
+ * horizontal edges.
+ * <li>Maximum size used to constrain relative size, see
+ * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
+ * </ul>
+ * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
+ * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets.
+ * <p>
+ * The following scrolling properties may be configured:
+ * <ul>
+ * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
+ * value is 500 milliseconds.
+ * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}.
+ * Default value is 500 milliseconds.
+ * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
+ * Default value is 100% per second for both vertical and horizontal.
+ * <li>Minimum velocity used to constrain relative velocity, see
+ * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
+ * larger of either this value or the relative target value. Default value is
+ * approximately 5 centimeters or 315 dips per second.
+ * <li>Maximum velocity used to constrain relative velocity, see
+ * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
+ * 1575 dips per second.
+ * </ul>
+ */
+public abstract class AutoScrollHelper implements View.OnTouchListener {
+    /**
+     * Constant passed to {@link #setRelativeEdges} or
+     * {@link #setRelativeVelocity}. Using this value ensures that the computed
+     * relative value is ignored and the absolute maximum value is always used.
+     */
+    public static final float RELATIVE_UNSPECIFIED = 0;
+
+    /**
+     * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
+     * or {@link #setMinimumVelocity}. Using this value ensures that the
+     * computed relative value is always used without constraining to a
+     * particular minimum or maximum value.
+     */
+    public static final float NO_MAX = Float.MAX_VALUE;
+
+    /**
+     * Constant passed to {@link #setMaximumEdges}, or
+     * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
+     * value ensures that the computed relative value is always used without
+     * constraining to a particular minimum or maximum value.
+     */
+    public static final float NO_MIN = 0;
+
+    /**
+     * Edge type that specifies an activation area starting at the view bounds
+     * and extending inward. Moving outside the view bounds will stop scrolling.
+     *
+     * @see #setEdgeType
+     */
+    public static final int EDGE_TYPE_INSIDE = 0;
+
+    /**
+     * Edge type that specifies an activation area starting at the view bounds
+     * and extending inward. After activation begins, moving outside the view
+     * bounds will continue scrolling.
+     *
+     * @see #setEdgeType
+     */
+    public static final int EDGE_TYPE_INSIDE_EXTEND = 1;
+
+    /**
+     * Edge type that specifies an activation area starting at the view bounds
+     * and extending outward. Moving inside the view bounds will stop scrolling.
+     *
+     * @see #setEdgeType
+     */
+    public static final int EDGE_TYPE_OUTSIDE = 2;
+
+    private static final int HORIZONTAL = 0;
+    private static final int VERTICAL = 1;
+
+    /** Scroller used to control acceleration toward maximum velocity. */
+    private final ClampedScroller mScroller = new ClampedScroller();
+
+    /** Interpolator used to scale velocity with touch position. */
+    private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();
+
+    /** The view to auto-scroll. Might not be the source of touch events. */
+    private final View mTarget;
+
+    /** Runnable used to animate scrolling. */
+    private Runnable mRunnable;
+
+    /** Edge insets used to activate auto-scrolling. */
+    private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
+
+    /** Clamping values for edge insets used to activate auto-scrolling. */
+    private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };
+
+    /** The type of edge being used. */
+    private int mEdgeType;
+
+    /** Delay after entering an activation edge before auto-scrolling begins. */
+    private int mActivationDelay;
+
+    /** Relative scrolling velocity at maximum edge distance. */
+    private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
+
+    /** Clamping values used for scrolling velocity. */
+    private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };
+
+    /** Clamping values used for scrolling velocity. */
+    private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };
+
+    /** Whether to start activation immediately. */
+    private boolean mAlreadyDelayed;
+
+    /** Whether to reset the scroller start time on the next animation. */
+    private boolean mNeedsReset;
+
+    /** Whether to send a cancel motion event to the target view. */
+    private boolean mNeedsCancel;
+
+    /** Whether the auto-scroller is actively scrolling. */
+    private boolean mAnimating;
+
+    /** Whether the auto-scroller is enabled. */
+    private boolean mEnabled;
+
+    /** Whether the auto-scroller consumes events when scrolling. */
+    private boolean mExclusive;
+
+    // Default values.
+    private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
+    private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315;
+    private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575;
+    private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX;
+    private static final float DEFAULT_RELATIVE_EDGE = 0.2f;
+    private static final float DEFAULT_RELATIVE_VELOCITY = 1f;
+    private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout();
+    private static final int DEFAULT_RAMP_UP_DURATION = 500;
+    private static final int DEFAULT_RAMP_DOWN_DURATION = 500;
+
+    /**
+     * Creates a new helper for scrolling the specified target view.
+     * <p>
+     * The resulting helper may be configured by chaining setter calls and
+     * should be set as a touch listener on the target view.
+     * <p>
+     * By default, the helper is disabled and will not respond to touch events
+     * until it is enabled using {@link #setEnabled}.
+     *
+     * @param target The view to automatically scroll.
+     */
+    public AutoScrollHelper(View target) {
+        mTarget = target;
+
+        final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
+        final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
+        final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
+        setMaximumVelocity(maxVelocity, maxVelocity);
+        setMinimumVelocity(minVelocity, minVelocity);
+
+        setEdgeType(DEFAULT_EDGE_TYPE);
+        setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE);
+        setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE);
+        setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
+        setActivationDelay(DEFAULT_ACTIVATION_DELAY);
+        setRampUpDuration(DEFAULT_RAMP_UP_DURATION);
+        setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
+    }
+
+    /**
+     * Sets whether the scroll helper is enabled and should respond to touch
+     * events.
+     *
+     * @param enabled Whether the scroll helper is enabled.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setEnabled(boolean enabled) {
+        if (mEnabled && !enabled) {
+            requestStop();
+        }
+
+        mEnabled = enabled;
+        return this;
+    }
+
+    /**
+     * @return True if this helper is enabled and responding to touch events.
+     */
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    /**
+     * Enables or disables exclusive handling of touch events during scrolling.
+     * By default, exclusive handling is disabled and the target view receives
+     * all touch events.
+     * <p>
+     * When enabled, {@link #onTouch} will return true if the helper is
+     * currently scrolling and false otherwise.
+     *
+     * @param exclusive True to exclusively handle touch events during scrolling,
+     *            false to allow the target view to receive all touch events.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setExclusive(boolean exclusive) {
+        mExclusive = exclusive;
+        return this;
+    }
+
+    /**
+     * Indicates whether the scroll helper handles touch events exclusively
+     * during scrolling.
+     *
+     * @return True if exclusive handling of touch events during scrolling is
+     *         enabled, false otherwise.
+     * @see #setExclusive(boolean)
+     */
+    public boolean isExclusive() {
+        return mExclusive;
+    }
+
+    /**
+     * Sets the absolute maximum scrolling velocity.
+     * <p>
+     * If relative velocity is not specified, scrolling will always reach the
+     * same maximum velocity. If both relative and maximum velocities are
+     * specified, the maximum velocity will be used to clamp the calculated
+     * relative velocity.
+     *
+     * @param horizontalMax The maximum horizontal scrolling velocity, or
+     *            {@link #NO_MAX} to leave the relative value unconstrained.
+     * @param verticalMax The maximum vertical scrolling velocity, or
+     *            {@link #NO_MAX} to leave the relative value unconstrained.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
+        mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
+        mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
+        return this;
+    }
+
+    /**
+     * Sets the absolute minimum scrolling velocity.
+     * <p>
+     * If both relative and minimum velocities are specified, the minimum
+     * velocity will be used to clamp the calculated relative velocity.
+     *
+     * @param horizontalMin The minimum horizontal scrolling velocity, or
+     *            {@link #NO_MIN} to leave the relative value unconstrained.
+     * @param verticalMin The minimum vertical scrolling velocity, or
+     *            {@link #NO_MIN} to leave the relative value unconstrained.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
+        mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
+        mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
+        return this;
+    }
+
+    /**
+     * Sets the target scrolling velocity relative to the host view's
+     * dimensions.
+     * <p>
+     * If both relative and maximum velocities are specified, the maximum
+     * velocity will be used to clamp the calculated relative velocity.
+     *
+     * @param horizontal The target horizontal velocity as a fraction of the
+     *            host view width per second, or {@link #RELATIVE_UNSPECIFIED}
+     *            to ignore.
+     * @param vertical The target vertical velocity as a fraction of the host
+     *            view height per second, or {@link #RELATIVE_UNSPECIFIED} to
+     *            ignore.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
+        mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
+        mRelativeVelocity[VERTICAL] = vertical / 1000f;
+        return this;
+    }
+
+    /**
+     * Sets the activation edge type, one of:
+     * <ul>
+     * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside
+     * the bounds of the host view. If touch moves outside the bounds, scrolling
+     * will stop.
+     * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to
+     * scroll when touch moves outside the bounds of the host view.
+     * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches
+     * that move outside the bounds of the host view.
+     * </ul>
+     *
+     * @param type The type of edge to use.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setEdgeType(int type) {
+        mEdgeType = type;
+        return this;
+    }
+
+    /**
+     * Sets the activation edge size relative to the host view's dimensions.
+     * <p>
+     * If both relative and maximum edges are specified, the maximum edge will
+     * be used to constrain the calculated relative edge size.
+     *
+     * @param horizontal The horizontal edge size as a fraction of the host view
+     *            width, or {@link #RELATIVE_UNSPECIFIED} to always use the
+     *            maximum value.
+     * @param vertical The vertical edge size as a fraction of the host view
+     *            height, or {@link #RELATIVE_UNSPECIFIED} to always use the
+     *            maximum value.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
+        mRelativeEdges[HORIZONTAL] = horizontal;
+        mRelativeEdges[VERTICAL] = vertical;
+        return this;
+    }
+
+    /**
+     * Sets the absolute maximum edge size.
+     * <p>
+     * If relative edge size is not specified, activation edges will always be
+     * the maximum edge size. If both relative and maximum edges are specified,
+     * the maximum edge will be used to constrain the calculated relative edge
+     * size.
+     *
+     * @param horizontalMax The maximum horizontal edge size in pixels, or
+     *            {@link #NO_MAX} to use the unconstrained calculated relative
+     *            value.
+     * @param verticalMax The maximum vertical edge size in pixels, or
+     *            {@link #NO_MAX} to use the unconstrained calculated relative
+     *            value.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
+        mMaximumEdges[HORIZONTAL] = horizontalMax;
+        mMaximumEdges[VERTICAL] = verticalMax;
+        return this;
+    }
+
+    /**
+     * Sets the delay after entering an activation edge before activation of
+     * auto-scrolling. By default, the activation delay is set to
+     * {@link ViewConfiguration#getTapTimeout()}.
+     * <p>
+     * Specifying a delay of zero will start auto-scrolling immediately after
+     * the touch position enters an activation edge.
+     *
+     * @param delayMillis The activation delay in milliseconds.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setActivationDelay(int delayMillis) {
+        mActivationDelay = delayMillis;
+        return this;
+    }
+
+    /**
+     * Sets the amount of time after activation of auto-scrolling that is takes
+     * to reach target velocity for the current touch position.
+     * <p>
+     * Specifying a duration greater than zero prevents sudden jumps in
+     * velocity.
+     *
+     * @param durationMillis The ramp-up duration in milliseconds.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setRampUpDuration(int durationMillis) {
+        mScroller.setRampUpDuration(durationMillis);
+        return this;
+    }
+
+    /**
+     * Sets the amount of time after de-activation of auto-scrolling that is
+     * takes to slow to a stop.
+     * <p>
+     * Specifying a duration greater than zero prevents sudden jumps in
+     * velocity.
+     *
+     * @param durationMillis The ramp-down duration in milliseconds.
+     * @return The scroll helper, which may used to chain setter calls.
+     */
+    public AutoScrollHelper setRampDownDuration(int durationMillis) {
+        mScroller.setRampDownDuration(durationMillis);
+        return this;
+    }
+
+    /**
+     * Handles touch events by activating automatic scrolling, adjusting scroll
+     * velocity, or stopping.
+     * <p>
+     * If {@link #isExclusive()} is false, always returns false so that
+     * the host view may handle touch events. Otherwise, returns true when
+     * automatic scrolling is active and false otherwise.
+     */
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        if (!mEnabled) {
+            return false;
+        }
+
+        final int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mNeedsCancel = true;
+                mAlreadyDelayed = false;
+                // $FALL-THROUGH$
+            case MotionEvent.ACTION_MOVE:
+                final float xTargetVelocity = computeTargetVelocity(
+                        HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth());
+                final float yTargetVelocity = computeTargetVelocity(
+                        VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight());
+                mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity);
+
+                // If the auto scroller was not previously active, but it should
+                // be, then update the state and start animations.
+                if (!mAnimating && shouldAnimate()) {
+                    startAnimating();
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                requestStop();
+                break;
+        }
+
+        return mExclusive && mAnimating;
+    }
+
+    /**
+     * @return whether the target is able to scroll in the requested direction
+     */
+    private boolean shouldAnimate() {
+        final ClampedScroller scroller = mScroller;
+        final int verticalDirection = scroller.getVerticalDirection();
+        final int horizontalDirection = scroller.getHorizontalDirection();
+
+        return verticalDirection != 0 && canTargetScrollVertically(verticalDirection)
+                || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection);
+    }
+
+    /**
+     * Starts the scroll animation.
+     */
+    private void startAnimating() {
+        if (mRunnable == null) {
+            mRunnable = new ScrollAnimationRunnable();
+        }
+
+        mAnimating = true;
+        mNeedsReset = true;
+
+        if (!mAlreadyDelayed && mActivationDelay > 0) {
+            mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
+        } else {
+            mRunnable.run();
+        }
+
+        // If we start animating again before the user lifts their finger, we
+        // already know it's not a tap and don't need an activation delay.
+        mAlreadyDelayed = true;
+    }
+
+    /**
+     * Requests that the scroll animation slow to a stop. If there is an
+     * activation delay, this may occur between posting the animation and
+     * actually running it.
+     */
+    private void requestStop() {
+        if (mNeedsReset) {
+            // The animation has been posted, but hasn't run yet. Manually
+            // stopping animation will prevent it from running.
+            mAnimating = false;
+        } else {
+            mScroller.requestStop();
+        }
+    }
+
+    private float computeTargetVelocity(
+            int direction, float coordinate, float srcSize, float dstSize) {
+        final float relativeEdge = mRelativeEdges[direction];
+        final float maximumEdge = mMaximumEdges[direction];
+        final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate);
+        if (value == 0) {
+            // The edge in this direction is not activated.
+            return 0;
+        }
+
+        final float relativeVelocity = mRelativeVelocity[direction];
+        final float minimumVelocity = mMinimumVelocity[direction];
+        final float maximumVelocity = mMaximumVelocity[direction];
+        final float targetVelocity = relativeVelocity * dstSize;
+
+        // Target velocity is adjusted for interpolated edge position, then
+        // clamped to the minimum and maximum values. Later, this value will be
+        // adjusted for time-based acceleration.
+        if (value > 0) {
+            return constrain(value * targetVelocity, minimumVelocity, maximumVelocity);
+        } else {
+            return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity);
+        }
+    }
+
+    /**
+     * Override this method to scroll the target view by the specified number of
+     * pixels.
+     *
+     * @param deltaX The number of pixels to scroll by horizontally.
+     * @param deltaY The number of pixels to scroll by vertically.
+     */
+    public abstract void scrollTargetBy(int deltaX, int deltaY);
+
+    /**
+     * Override this method to return whether the target view can be scrolled
+     * horizontally in a certain direction.
+     *
+     * @param direction Negative to check scrolling left, positive to check
+     *            scrolling right.
+     * @return true if the target view is able to horizontally scroll in the
+     *         specified direction.
+     */
+    public abstract boolean canTargetScrollHorizontally(int direction);
+
+    /**
+     * Override this method to return whether the target view can be scrolled
+     * vertically in a certain direction.
+     *
+     * @param direction Negative to check scrolling up, positive to check
+     *            scrolling down.
+     * @return true if the target view is able to vertically scroll in the
+     *         specified direction.
+     */
+    public abstract boolean canTargetScrollVertically(int direction);
+
+    /**
+     * Returns the interpolated position of a touch point relative to an edge
+     * defined by its relative inset, its maximum absolute inset, and the edge
+     * interpolator.
+     *
+     * @param relativeValue The size of the inset relative to the total size.
+     * @param size Total size.
+     * @param maxValue The maximum size of the inset, used to clamp (relative *
+     *            total).
+     * @param current Touch position within within the total size.
+     * @return Interpolated value of the touch position within the edge.
+     */
+    private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
+        // For now, leading and trailing edges are always the same size.
+        final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
+        final float valueLeading = constrainEdgeValue(current, edgeSize);
+        final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
+        final float value = (valueTrailing - valueLeading);
+        final float interpolated;
+        if (value < 0) {
+            interpolated = -mEdgeInterpolator.getInterpolation(-value);
+        } else if (value > 0) {
+            interpolated = mEdgeInterpolator.getInterpolation(value);
+        } else {
+            return 0;
+        }
+
+        return constrain(interpolated, -1, 1);
+    }
+
+    private float constrainEdgeValue(float current, float leading) {
+        if (leading == 0) {
+            return 0;
+        }
+
+        switch (mEdgeType) {
+            case EDGE_TYPE_INSIDE:
+            case EDGE_TYPE_INSIDE_EXTEND:
+                if (current < leading) {
+                    if (current >= 0) {
+                        // Movement up to the edge is scaled.
+                        return 1f - current / leading;
+                    } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
+                        // Movement beyond the edge is always maximum.
+                        return 1f;
+                    }
+                }
+                break;
+            case EDGE_TYPE_OUTSIDE:
+                if (current < 0) {
+                    // Movement beyond the edge is scaled.
+                    return current / -leading;
+                }
+                break;
+        }
+
+        return 0;
+    }
+
+    private static int constrain(int value, int min, int max) {
+        if (value > max) {
+            return max;
+        } else if (value < min) {
+            return min;
+        } else {
+            return value;
+        }
+    }
+
+    private static float constrain(float value, float min, float max) {
+        if (value > max) {
+            return max;
+        } else if (value < min) {
+            return min;
+        } else {
+            return value;
+        }
+    }
+
+    /**
+     * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
+     * canceling any ongoing touch events.
+     */
+    private void cancelTargetTouch() {
+        final long eventTime = SystemClock.uptimeMillis();
+        final MotionEvent cancel = MotionEvent.obtain(
+                eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        mTarget.onTouchEvent(cancel);
+        cancel.recycle();
+    }
+
+    private class ScrollAnimationRunnable implements Runnable {
+        @Override
+        public void run() {
+            if (!mAnimating) {
+                return;
+            }
+
+            if (mNeedsReset) {
+                mNeedsReset = false;
+                mScroller.start();
+            }
+
+            final ClampedScroller scroller = mScroller;
+            if (scroller.isFinished() || !shouldAnimate()) {
+                mAnimating = false;
+                return;
+            }
+
+            if (mNeedsCancel) {
+                mNeedsCancel = false;
+                cancelTargetTouch();
+            }
+
+            scroller.computeScrollDelta();
+
+            final int deltaX = scroller.getDeltaX();
+            final int deltaY = scroller.getDeltaY();
+            scrollTargetBy(deltaX,  deltaY);
+
+            // Keep going until the scroller has permanently stopped.
+            mTarget.postOnAnimation(this);
+        }
+    }
+
+    /**
+     * Scroller whose velocity follows the curve of an {@link Interpolator} and
+     * is clamped to the interpolated 0f value before starting and the
+     * interpolated 1f value after a specified duration.
+     */
+    private static class ClampedScroller {
+        private int mRampUpDuration;
+        private int mRampDownDuration;
+        private float mTargetVelocityX;
+        private float mTargetVelocityY;
+
+        private long mStartTime;
+
+        private long mDeltaTime;
+        private int mDeltaX;
+        private int mDeltaY;
+
+        private long mStopTime;
+        private float mStopValue;
+        private int mEffectiveRampDown;
+
+        /**
+         * Creates a new ramp-up scroller that reaches full velocity after a
+         * specified duration.
+         */
+        public ClampedScroller() {
+            mStartTime = Long.MIN_VALUE;
+            mStopTime = -1;
+            mDeltaTime = 0;
+            mDeltaX = 0;
+            mDeltaY = 0;
+        }
+
+        public void setRampUpDuration(int durationMillis) {
+            mRampUpDuration = durationMillis;
+        }
+
+        public void setRampDownDuration(int durationMillis) {
+            mRampDownDuration = durationMillis;
+        }
+
+        /**
+         * Starts the scroller at the current animation time.
+         */
+        public void start() {
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mStopTime = -1;
+            mDeltaTime = mStartTime;
+            mStopValue = 0.5f;
+            mDeltaX = 0;
+            mDeltaY = 0;
+        }
+
+        /**
+         * Stops the scroller at the current animation time.
+         */
+        public void requestStop() {
+            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+            mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
+            mStopValue = getValueAt(currentTime);
+            mStopTime = currentTime;
+        }
+
+        public boolean isFinished() {
+            return mStopTime > 0
+                    && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
+        }
+
+        private float getValueAt(long currentTime) {
+            if (currentTime < mStartTime) {
+                return 0f;
+            } else if (mStopTime < 0 || currentTime < mStopTime) {
+                final long elapsedSinceStart = currentTime - mStartTime;
+                return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
+            } else {
+                final long elapsedSinceEnd = currentTime - mStopTime;
+                return (1 - mStopValue) + mStopValue
+                        * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
+            }
+        }
+
+        /**
+         * Interpolates the value along a parabolic curve corresponding to the equation
+         * <code>y = -4x * (x-1)</code>.
+         *
+         * @param value The value to interpolate, between 0 and 1.
+         * @return the interpolated value, between 0 and 1.
+         */
+        private float interpolateValue(float value) {
+            return -4 * value * value + 4 * value;
+        }
+
+        /**
+         * Computes the current scroll deltas. This usually only be called after
+         * starting the scroller with {@link #start()}.
+         *
+         * @see #getDeltaX()
+         * @see #getDeltaY()
+         */
+        public void computeScrollDelta() {
+            if (mDeltaTime == 0) {
+                throw new RuntimeException("Cannot compute scroll delta before calling start()");
+            }
+
+            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+            final float value = getValueAt(currentTime);
+            final float scale = interpolateValue(value);
+            final long elapsedSinceDelta = currentTime - mDeltaTime;
+
+            mDeltaTime = currentTime;
+            mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
+            mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
+        }
+
+        /**
+         * Sets the target velocity for this scroller.
+         *
+         * @param x The target X velocity in pixels per millisecond.
+         * @param y The target Y velocity in pixels per millisecond.
+         */
+        public void setTargetVelocity(float x, float y) {
+            mTargetVelocityX = x;
+            mTargetVelocityY = y;
+        }
+
+        public int getHorizontalDirection() {
+            return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
+        }
+
+        public int getVerticalDirection() {
+            return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
+        }
+
+        /**
+         * The distance traveled in the X-coordinate computed by the last call
+         * to {@link #computeScrollDelta()}.
+         */
+        public int getDeltaX() {
+            return mDeltaX;
+        }
+
+        /**
+         * The distance traveled in the Y-coordinate computed by the last call
+         * to {@link #computeScrollDelta()}.
+         */
+        public int getDeltaY() {
+            return mDeltaY;
+        }
+    }
+
+    /**
+     * An implementation of {@link AutoScrollHelper} that knows how to scroll
+     * through an {@link AbsListView}.
+     */
+    public static class AbsListViewAutoScroller extends AutoScrollHelper {
+        private final AbsListView mTarget;
+
+        public AbsListViewAutoScroller(AbsListView target) {
+            super(target);
+
+            mTarget = target;
+        }
+
+        @Override
+        public void scrollTargetBy(int deltaX, int deltaY) {
+            mTarget.scrollListBy(deltaY);
+        }
+
+        @Override
+        public boolean canTargetScrollHorizontally(int direction) {
+            // List do not scroll horizontally.
+            return false;
+        }
+
+        @Override
+        public boolean canTargetScrollVertically(int direction) {
+            final AbsListView target = mTarget;
+            final int itemCount = target.getCount();
+            if (itemCount == 0) {
+                return false;
+            }
+
+            final int childCount = target.getChildCount();
+            final int firstPosition = target.getFirstVisiblePosition();
+            final int lastPosition = firstPosition + childCount;
+
+            if (direction > 0) {
+                // Are we already showing the entire last item?
+                if (lastPosition >= itemCount) {
+                    final View lastView = target.getChildAt(childCount - 1);
+                    if (lastView.getBottom() <= target.getHeight()) {
+                        return false;
+                    }
+                }
+            } else if (direction < 0) {
+                // Are we already showing the entire first item?
+                if (firstPosition <= 0) {
+                    final View firstView = target.getChildAt(0);
+                    if (firstView.getTop() >= 0) {
+                        return false;
+                    }
+                }
+            } else {
+                // The behavior for direction 0 is undefined and we can return
+                // whatever we want.
+                return false;
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/com/android/internal/widget/BackgroundFallback.java b/com/android/internal/widget/BackgroundFallback.java
new file mode 100644
index 0000000..309f80c
--- /dev/null
+++ b/com/android/internal/widget/BackgroundFallback.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper class for drawing a fallback background in framework decor layouts.
+ * Useful for when an app has not set a window background but we're asked to draw
+ * an uncovered area.
+ */
+public class BackgroundFallback {
+    private Drawable mBackgroundFallback;
+
+    public void setDrawable(Drawable d) {
+        mBackgroundFallback = d;
+    }
+
+    public boolean hasFallback() {
+        return mBackgroundFallback != null;
+    }
+
+    /**
+     * Draws the fallback background.
+     *
+     * @param boundsView The view determining with which bounds the background should be drawn.
+     * @param root The view group containing the content.
+     * @param c The canvas to draw the background onto.
+     * @param content The view where the actual app content is contained in.
+     */
+    public void draw(ViewGroup boundsView, ViewGroup root, Canvas c, View content) {
+        if (!hasFallback()) {
+            return;
+        }
+
+        // Draw the fallback in the padding.
+        final int width = boundsView.getWidth();
+        final int height = boundsView.getHeight();
+        int left = width;
+        int top = height;
+        int right = 0;
+        int bottom = 0;
+
+        final int childCount = root.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = root.getChildAt(i);
+            final Drawable childBg = child.getBackground();
+            if (child == content) {
+                // We always count the content view container unless it has no background
+                // and no children.
+                if (childBg == null && child instanceof ViewGroup &&
+                        ((ViewGroup) child).getChildCount() == 0) {
+                    continue;
+                }
+            } else if (child.getVisibility() != View.VISIBLE || childBg == null ||
+                    childBg.getOpacity() != PixelFormat.OPAQUE) {
+                // Potentially translucent or invisible children don't count, and we assume
+                // the content view will cover the whole area if we're in a background
+                // fallback situation.
+                continue;
+            }
+            left = Math.min(left, child.getLeft());
+            top = Math.min(top, child.getTop());
+            right = Math.max(right, child.getRight());
+            bottom = Math.max(bottom, child.getBottom());
+        }
+
+        if (left >= right || top >= bottom) {
+            // No valid area to draw in.
+            return;
+        }
+
+        if (top > 0) {
+            mBackgroundFallback.setBounds(0, 0, width, top);
+            mBackgroundFallback.draw(c);
+        }
+        if (left > 0) {
+            mBackgroundFallback.setBounds(0, top, left, height);
+            mBackgroundFallback.draw(c);
+        }
+        if (right < width) {
+            mBackgroundFallback.setBounds(right, top, width, height);
+            mBackgroundFallback.draw(c);
+        }
+        if (bottom < height) {
+            mBackgroundFallback.setBounds(left, bottom, right, height);
+            mBackgroundFallback.draw(c);
+        }
+    }
+}
diff --git a/com/android/internal/widget/ButtonBarLayout.java b/com/android/internal/widget/ButtonBarLayout.java
new file mode 100644
index 0000000..6a0edef
--- /dev/null
+++ b/com/android/internal/widget/ButtonBarLayout.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.internal.R;
+
+/**
+ * An extension of LinearLayout that automatically switches to vertical
+ * orientation when it can't fit its child views horizontally.
+ */
+public class ButtonBarLayout extends LinearLayout {
+    /** Minimum screen height required for button stacking. */
+    private static final int ALLOW_STACKING_MIN_HEIGHT_DP = 320;
+
+    /** Amount of the second button to "peek" above the fold when stacked. */
+    private static final int PEEK_BUTTON_DP = 16;
+
+    /** Whether the current configuration allows stacking. */
+    private boolean mAllowStacking;
+
+    private int mLastWidthSize = -1;
+
+    private int mMinimumHeight = 0;
+
+    public ButtonBarLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        final boolean allowStackingDefault =
+                context.getResources().getConfiguration().screenHeightDp
+                        >= ALLOW_STACKING_MIN_HEIGHT_DP;
+        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
+        mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking,
+                allowStackingDefault);
+        ta.recycle();
+    }
+
+    public void setAllowStacking(boolean allowStacking) {
+        if (mAllowStacking != allowStacking) {
+            mAllowStacking = allowStacking;
+            if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) {
+                setStacked(false);
+            }
+            requestLayout();
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+        if (mAllowStacking) {
+            if (widthSize > mLastWidthSize && isStacked()) {
+                // We're being measured wider this time, try un-stacking.
+                setStacked(false);
+            }
+
+            mLastWidthSize = widthSize;
+        }
+
+        boolean needsRemeasure = false;
+
+        // If we're not stacked, make sure the measure spec is AT_MOST rather
+        // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
+        // know to stack the buttons.
+        final int initialWidthMeasureSpec;
+        if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+            initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
+
+            // We'll need to remeasure again to fill excess space.
+            needsRemeasure = true;
+        } else {
+            initialWidthMeasureSpec = widthMeasureSpec;
+        }
+
+        super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
+
+        if (mAllowStacking && !isStacked()) {
+            final int measuredWidth = getMeasuredWidthAndState();
+            final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK;
+            if (measuredWidthState == MEASURED_STATE_TOO_SMALL) {
+                setStacked(true);
+
+                // Measure again in the new orientation.
+                needsRemeasure = true;
+            }
+        }
+
+        if (needsRemeasure) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+
+        // Compute minimum height such that, when stacked, some portion of the
+        // second button is visible.
+        int minHeight = 0;
+        final int firstVisible = getNextVisibleChildIndex(0);
+        if (firstVisible >= 0) {
+            final View firstButton = getChildAt(firstVisible);
+            final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams();
+            minHeight += getPaddingTop() + firstButton.getMeasuredHeight()
+                    + firstParams.topMargin + firstParams.bottomMargin;
+            if (isStacked()) {
+                final int secondVisible = getNextVisibleChildIndex(firstVisible + 1);
+                if (secondVisible >= 0) {
+                    minHeight += getChildAt(secondVisible).getPaddingTop()
+                            + PEEK_BUTTON_DP * getResources().getDisplayMetrics().density;
+                }
+            } else {
+                minHeight += getPaddingBottom();
+            }
+        }
+
+        if (getMinimumHeight() != minHeight) {
+            setMinimumHeight(minHeight);
+        }
+    }
+
+    private int getNextVisibleChildIndex(int index) {
+        for (int i = index, count = getChildCount(); i < count; i++) {
+            if (getChildAt(i).getVisibility() == View.VISIBLE) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public int getMinimumHeight() {
+        return Math.max(mMinimumHeight, super.getMinimumHeight());
+    }
+
+    private void setStacked(boolean stacked) {
+        setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
+        setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM);
+
+        final View spacer = findViewById(R.id.spacer);
+        if (spacer != null) {
+            spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE);
+        }
+
+        // Reverse the child order. This is specific to the Material button
+        // bar's layout XML and will probably not generalize.
+        final int childCount = getChildCount();
+        for (int i = childCount - 2; i >= 0; i--) {
+            bringChildToFront(getChildAt(i));
+        }
+    }
+
+    private boolean isStacked() {
+        return getOrientation() == LinearLayout.VERTICAL;
+    }
+}
diff --git a/com/android/internal/widget/CachingIconView.java b/com/android/internal/widget/CachingIconView.java
new file mode 100644
index 0000000..b172dbc
--- /dev/null
+++ b/com/android/internal/widget/CachingIconView.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import libcore.util.Objects;
+
+/**
+ * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
+ */
[email protected]
+public class CachingIconView extends ImageView {
+
+    private String mLastPackage;
+    private int mLastResId;
+    private boolean mInternalSetDrawable;
+    private boolean mForceHidden;
+    private int mDesiredVisibility;
+
+    public CachingIconView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    @RemotableViewMethod(asyncImpl="setImageIconAsync")
+    public void setImageIcon(@Nullable Icon icon) {
+        if (!testAndSetCache(icon)) {
+            mInternalSetDrawable = true;
+            // This calls back to setImageDrawable, make sure we don't clear the cache there.
+            super.setImageIcon(icon);
+            mInternalSetDrawable = false;
+        }
+    }
+
+    @Override
+    public Runnable setImageIconAsync(@Nullable Icon icon) {
+        resetCache();
+        return super.setImageIconAsync(icon);
+    }
+
+    @Override
+    @RemotableViewMethod(asyncImpl="setImageResourceAsync")
+    public void setImageResource(@DrawableRes int resId) {
+        if (!testAndSetCache(resId)) {
+            mInternalSetDrawable = true;
+            // This calls back to setImageDrawable, make sure we don't clear the cache there.
+            super.setImageResource(resId);
+            mInternalSetDrawable = false;
+        }
+    }
+
+    @Override
+    public Runnable setImageResourceAsync(@DrawableRes int resId) {
+        resetCache();
+        return super.setImageResourceAsync(resId);
+    }
+
+    @Override
+    @RemotableViewMethod(asyncImpl="setImageURIAsync")
+    public void setImageURI(@Nullable Uri uri) {
+        resetCache();
+        super.setImageURI(uri);
+    }
+
+    @Override
+    public Runnable setImageURIAsync(@Nullable Uri uri) {
+        resetCache();
+        return super.setImageURIAsync(uri);
+    }
+
+    @Override
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        if (!mInternalSetDrawable) {
+            // Only clear the cache if we were externally called.
+            resetCache();
+        }
+        super.setImageDrawable(drawable);
+    }
+
+    @Override
+    @RemotableViewMethod
+    public void setImageBitmap(Bitmap bm) {
+        resetCache();
+        super.setImageBitmap(bm);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        resetCache();
+    }
+
+    /**
+     * @return true if the currently set image is the same as {@param icon}
+     */
+    private synchronized boolean testAndSetCache(Icon icon) {
+        if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
+            String iconPackage = normalizeIconPackage(icon);
+
+            boolean isCached = mLastResId != 0
+                    && icon.getResId() == mLastResId
+                    && Objects.equal(iconPackage, mLastPackage);
+
+            mLastPackage = iconPackage;
+            mLastResId = icon.getResId();
+
+            return isCached;
+        } else {
+            resetCache();
+            return false;
+        }
+    }
+
+    /**
+     * @return true if the currently set image is the same as {@param resId}
+     */
+    private synchronized boolean testAndSetCache(int resId) {
+        boolean isCached;
+        if (resId == 0 || mLastResId == 0) {
+            isCached = false;
+        } else {
+            isCached = resId == mLastResId && null == mLastPackage;
+        }
+        mLastPackage = null;
+        mLastResId = resId;
+        return isCached;
+    }
+
+    /**
+     * Returns the normalized package name of {@param icon}.
+     * @return null if icon is null or if the icons package is null, empty or matches the current
+     *         context. Otherwise returns the icon's package context.
+     */
+    private String normalizeIconPackage(Icon icon) {
+        if (icon == null) {
+            return null;
+        }
+
+        String pkg = icon.getResPackage();
+        if (TextUtils.isEmpty(pkg)) {
+            return null;
+        }
+        if (pkg.equals(mContext.getPackageName())) {
+            return null;
+        }
+        return pkg;
+    }
+
+    private synchronized void resetCache() {
+        mLastResId = 0;
+        mLastPackage = null;
+    }
+
+    /**
+     * Set the icon to be forcibly hidden, even when it's visibility is changed to visible.
+     */
+    public void setForceHidden(boolean forceHidden) {
+        mForceHidden = forceHidden;
+        updateVisibility();
+    }
+
+    @Override
+    @RemotableViewMethod
+    public void setVisibility(int visibility) {
+        mDesiredVisibility = visibility;
+        updateVisibility();
+    }
+
+    private void updateVisibility() {
+        int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE
+                : mDesiredVisibility;
+        super.setVisibility(visibility);
+    }
+}
diff --git a/com/android/internal/widget/ChildHelper.java b/com/android/internal/widget/ChildHelper.java
new file mode 100644
index 0000000..e9136d0
--- /dev/null
+++ b/com/android/internal/widget/ChildHelper.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage children.
+ * <p>
+ * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
+ * provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
+ * like getChildAt, getChildCount etc. These methods ignore hidden children.
+ * <p>
+ * When RecyclerView needs direct access to the view group children, it can call unfiltered
+ * methods like get getUnfilteredChildCount or getUnfilteredChildAt.
+ */
+class ChildHelper {
+
+    private static final boolean DEBUG = false;
+
+    private static final String TAG = "ChildrenHelper";
+
+    final Callback mCallback;
+
+    final Bucket mBucket;
+
+    final List<View> mHiddenViews;
+
+    ChildHelper(Callback callback) {
+        mCallback = callback;
+        mBucket = new Bucket();
+        mHiddenViews = new ArrayList<View>();
+    }
+
+    /**
+     * Marks a child view as hidden
+     *
+     * @param child  View to hide.
+     */
+    private void hideViewInternal(View child) {
+        mHiddenViews.add(child);
+        mCallback.onEnteredHiddenState(child);
+    }
+
+    /**
+     * Unmarks a child view as hidden.
+     *
+     * @param child  View to hide.
+     */
+    private boolean unhideViewInternal(View child) {
+        if (mHiddenViews.remove(child)) {
+            mCallback.onLeftHiddenState(child);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Adds a view to the ViewGroup
+     *
+     * @param child  View to add.
+     * @param hidden If set to true, this item will be invisible from regular methods.
+     */
+    void addView(View child, boolean hidden) {
+        addView(child, -1, hidden);
+    }
+
+    /**
+     * Add a view to the ViewGroup at an index
+     *
+     * @param child  View to add.
+     * @param index  Index of the child from the regular perspective (excluding hidden views).
+     *               ChildHelper offsets this index to actual ViewGroup index.
+     * @param hidden If set to true, this item will be invisible from regular methods.
+     */
+    void addView(View child, int index, boolean hidden) {
+        final int offset;
+        if (index < 0) {
+            offset = mCallback.getChildCount();
+        } else {
+            offset = getOffset(index);
+        }
+        mBucket.insert(offset, hidden);
+        if (hidden) {
+            hideViewInternal(child);
+        }
+        mCallback.addView(child, offset);
+        if (DEBUG) {
+            Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
+        }
+    }
+
+    private int getOffset(int index) {
+        if (index < 0) {
+            return -1; //anything below 0 won't work as diff will be undefined.
+        }
+        final int limit = mCallback.getChildCount();
+        int offset = index;
+        while (offset < limit) {
+            final int removedBefore = mBucket.countOnesBefore(offset);
+            final int diff = index - (offset - removedBefore);
+            if (diff == 0) {
+                while (mBucket.get(offset)) { // ensure this offset is not hidden
+                    offset++;
+                }
+                return offset;
+            } else {
+                offset += diff;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Removes the provided View from underlying RecyclerView.
+     *
+     * @param view The view to remove.
+     */
+    void removeView(View view) {
+        int index = mCallback.indexOfChild(view);
+        if (index < 0) {
+            return;
+        }
+        if (mBucket.remove(index)) {
+            unhideViewInternal(view);
+        }
+        mCallback.removeViewAt(index);
+        if (DEBUG) {
+            Log.d(TAG, "remove View off:" + index + "," + this);
+        }
+    }
+
+    /**
+     * Removes the view at the provided index from RecyclerView.
+     *
+     * @param index Index of the child from the regular perspective (excluding hidden views).
+     *              ChildHelper offsets this index to actual ViewGroup index.
+     */
+    void removeViewAt(int index) {
+        final int offset = getOffset(index);
+        final View view = mCallback.getChildAt(offset);
+        if (view == null) {
+            return;
+        }
+        if (mBucket.remove(offset)) {
+            unhideViewInternal(view);
+        }
+        mCallback.removeViewAt(offset);
+        if (DEBUG) {
+            Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
+        }
+    }
+
+    /**
+     * Returns the child at provided index.
+     *
+     * @param index Index of the child to return in regular perspective.
+     */
+    View getChildAt(int index) {
+        final int offset = getOffset(index);
+        return mCallback.getChildAt(offset);
+    }
+
+    /**
+     * Removes all views from the ViewGroup including the hidden ones.
+     */
+    void removeAllViewsUnfiltered() {
+        mBucket.reset();
+        for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
+            mCallback.onLeftHiddenState(mHiddenViews.get(i));
+            mHiddenViews.remove(i);
+        }
+        mCallback.removeAllViews();
+        if (DEBUG) {
+            Log.d(TAG, "removeAllViewsUnfiltered");
+        }
+    }
+
+    /**
+     * This can be used to find a disappearing view by position.
+     *
+     * @param position The adapter position of the item.
+     * @return         A hidden view with a valid ViewHolder that matches the position.
+     */
+    View findHiddenNonRemovedView(int position) {
+        final int count = mHiddenViews.size();
+        for (int i = 0; i < count; i++) {
+            final View view = mHiddenViews.get(i);
+            RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
+            if (holder.getLayoutPosition() == position
+                    && !holder.isInvalid()
+                    && !holder.isRemoved()) {
+                return view;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Attaches the provided view to the underlying ViewGroup.
+     *
+     * @param child        Child to attach.
+     * @param index        Index of the child to attach in regular perspective.
+     * @param layoutParams LayoutParams for the child.
+     * @param hidden       If set to true, this item will be invisible to the regular methods.
+     */
+    void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
+            boolean hidden) {
+        final int offset;
+        if (index < 0) {
+            offset = mCallback.getChildCount();
+        } else {
+            offset = getOffset(index);
+        }
+        mBucket.insert(offset, hidden);
+        if (hidden) {
+            hideViewInternal(child);
+        }
+        mCallback.attachViewToParent(child, offset, layoutParams);
+        if (DEBUG) {
+            Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + ","
+                    + "h:" + hidden + ", " + this);
+        }
+    }
+
+    /**
+     * Returns the number of children that are not hidden.
+     *
+     * @return Number of children that are not hidden.
+     * @see #getChildAt(int)
+     */
+    int getChildCount() {
+        return mCallback.getChildCount() - mHiddenViews.size();
+    }
+
+    /**
+     * Returns the total number of children.
+     *
+     * @return The total number of children including the hidden views.
+     * @see #getUnfilteredChildAt(int)
+     */
+    int getUnfilteredChildCount() {
+        return mCallback.getChildCount();
+    }
+
+    /**
+     * Returns a child by ViewGroup offset. ChildHelper won't offset this index.
+     *
+     * @param index ViewGroup index of the child to return.
+     * @return The view in the provided index.
+     */
+    View getUnfilteredChildAt(int index) {
+        return mCallback.getChildAt(index);
+    }
+
+    /**
+     * Detaches the view at the provided index.
+     *
+     * @param index Index of the child to return in regular perspective.
+     */
+    void detachViewFromParent(int index) {
+        final int offset = getOffset(index);
+        mBucket.remove(offset);
+        mCallback.detachViewFromParent(offset);
+        if (DEBUG) {
+            Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
+        }
+    }
+
+    /**
+     * Returns the index of the child in regular perspective.
+     *
+     * @param child The child whose index will be returned.
+     * @return The regular perspective index of the child or -1 if it does not exists.
+     */
+    int indexOfChild(View child) {
+        final int index = mCallback.indexOfChild(child);
+        if (index == -1) {
+            return -1;
+        }
+        if (mBucket.get(index)) {
+            if (DEBUG) {
+                throw new IllegalArgumentException("cannot get index of a hidden child");
+            } else {
+                return -1;
+            }
+        }
+        // reverse the index
+        return index - mBucket.countOnesBefore(index);
+    }
+
+    /**
+     * Returns whether a View is visible to LayoutManager or not.
+     *
+     * @param view The child view to check. Should be a child of the Callback.
+     * @return True if the View is not visible to LayoutManager
+     */
+    boolean isHidden(View view) {
+        return mHiddenViews.contains(view);
+    }
+
+    /**
+     * Marks a child view as hidden.
+     *
+     * @param view The view to hide.
+     */
+    void hide(View view) {
+        final int offset = mCallback.indexOfChild(view);
+        if (offset < 0) {
+            throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+        }
+        if (DEBUG && mBucket.get(offset)) {
+            throw new RuntimeException("trying to hide same view twice, how come ? " + view);
+        }
+        mBucket.set(offset);
+        hideViewInternal(view);
+        if (DEBUG) {
+            Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this);
+        }
+    }
+
+    /**
+     * Moves a child view from hidden list to regular list.
+     * Calling this method should probably be followed by a detach, otherwise, it will suddenly
+     * show up in LayoutManager's children list.
+     *
+     * @param view The hidden View to unhide
+     */
+    void unhide(View view) {
+        final int offset = mCallback.indexOfChild(view);
+        if (offset < 0) {
+            throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+        }
+        if (!mBucket.get(offset)) {
+            throw new RuntimeException("trying to unhide a view that was not hidden" + view);
+        }
+        mBucket.clear(offset);
+        unhideViewInternal(view);
+    }
+
+    @Override
+    public String toString() {
+        return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
+    }
+
+    /**
+     * Removes a view from the ViewGroup if it is hidden.
+     *
+     * @param view The view to remove.
+     * @return True if the View is found and it is hidden. False otherwise.
+     */
+    boolean removeViewIfHidden(View view) {
+        final int index = mCallback.indexOfChild(view);
+        if (index == -1) {
+            if (unhideViewInternal(view) && DEBUG) {
+                throw new IllegalStateException("view is in hidden list but not in view group");
+            }
+            return true;
+        }
+        if (mBucket.get(index)) {
+            mBucket.remove(index);
+            if (!unhideViewInternal(view) && DEBUG) {
+                throw new IllegalStateException(
+                        "removed a hidden view but it is not in hidden views list");
+            }
+            mCallback.removeViewAt(index);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Bitset implementation that provides methods to offset indices.
+     */
+    static class Bucket {
+
+        static final int BITS_PER_WORD = Long.SIZE;
+
+        static final long LAST_BIT = 1L << (Long.SIZE - 1);
+
+        long mData = 0;
+
+        Bucket mNext;
+
+        void set(int index) {
+            if (index >= BITS_PER_WORD) {
+                ensureNext();
+                mNext.set(index - BITS_PER_WORD);
+            } else {
+                mData |= 1L << index;
+            }
+        }
+
+        private void ensureNext() {
+            if (mNext == null) {
+                mNext = new Bucket();
+            }
+        }
+
+        void clear(int index) {
+            if (index >= BITS_PER_WORD) {
+                if (mNext != null) {
+                    mNext.clear(index - BITS_PER_WORD);
+                }
+            } else {
+                mData &= ~(1L << index);
+            }
+
+        }
+
+        boolean get(int index) {
+            if (index >= BITS_PER_WORD) {
+                ensureNext();
+                return mNext.get(index - BITS_PER_WORD);
+            } else {
+                return (mData & (1L << index)) != 0;
+            }
+        }
+
+        void reset() {
+            mData = 0;
+            if (mNext != null) {
+                mNext.reset();
+            }
+        }
+
+        void insert(int index, boolean value) {
+            if (index >= BITS_PER_WORD) {
+                ensureNext();
+                mNext.insert(index - BITS_PER_WORD, value);
+            } else {
+                final boolean lastBit = (mData & LAST_BIT) != 0;
+                long mask = (1L << index) - 1;
+                final long before = mData & mask;
+                final long after = ((mData & ~mask)) << 1;
+                mData = before | after;
+                if (value) {
+                    set(index);
+                } else {
+                    clear(index);
+                }
+                if (lastBit || mNext != null) {
+                    ensureNext();
+                    mNext.insert(0, lastBit);
+                }
+            }
+        }
+
+        boolean remove(int index) {
+            if (index >= BITS_PER_WORD) {
+                ensureNext();
+                return mNext.remove(index - BITS_PER_WORD);
+            } else {
+                long mask = (1L << index);
+                final boolean value = (mData & mask) != 0;
+                mData &= ~mask;
+                mask = mask - 1;
+                final long before = mData & mask;
+                // cannot use >> because it adds one.
+                final long after = Long.rotateRight(mData & ~mask, 1);
+                mData = before | after;
+                if (mNext != null) {
+                    if (mNext.get(0)) {
+                        set(BITS_PER_WORD - 1);
+                    }
+                    mNext.remove(0);
+                }
+                return value;
+            }
+        }
+
+        int countOnesBefore(int index) {
+            if (mNext == null) {
+                if (index >= BITS_PER_WORD) {
+                    return Long.bitCount(mData);
+                }
+                return Long.bitCount(mData & ((1L << index) - 1));
+            }
+            if (index < BITS_PER_WORD) {
+                return Long.bitCount(mData & ((1L << index) - 1));
+            } else {
+                return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return mNext == null ? Long.toBinaryString(mData)
+                    : mNext.toString() + "xx" + Long.toBinaryString(mData);
+        }
+    }
+
+    interface Callback {
+
+        int getChildCount();
+
+        void addView(View child, int index);
+
+        int indexOfChild(View view);
+
+        void removeViewAt(int index);
+
+        View getChildAt(int offset);
+
+        void removeAllViews();
+
+        RecyclerView.ViewHolder getChildViewHolder(View view);
+
+        void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
+
+        void detachViewFromParent(int offset);
+
+        void onEnteredHiddenState(View child);
+
+        void onLeftHiddenState(View child);
+    }
+}
+
diff --git a/com/android/internal/widget/DecorCaptionView.java b/com/android/internal/widget/DecorCaptionView.java
new file mode 100644
index 0000000..b419113
--- /dev/null
+++ b/com/android/internal/widget/DecorCaptionView.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.Window;
+
+import com.android.internal.R;
+import com.android.internal.policy.PhoneWindow;
+
+import java.util.ArrayList;
+
+/**
+ * This class represents the special screen elements to control a window on freeform
+ * environment.
+ * As such this class handles the following things:
+ * <ul>
+ * <li>The caption, containing the system buttons like maximize, close and such as well as
+ * allowing the user to drag the window around.</li>
+ * </ul>
+ * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
+ * the connection to it's owning PhoneWindow.
+ * Note: At this time the application can change various attributes of the DecorView which
+ * will break things (in subtle/unexpected ways):
+ * <ul>
+ * <li>setOutlineProvider</li>
+ * <li>setSurfaceFormat</li>
+ * <li>..</li>
+ * </ul>
+ *
+ * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to
+ * overlaying caption on the content and drawing.
+ *
+ * First, no matter where the content View gets added, it will always be the first child and the
+ * caption will be the second. This way the caption will always be drawn on top of the content when
+ * overlaying is enabled.
+ *
+ * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch
+ * is dispatched on the caption area while overlaying it on content:
+ * <ul>
+ * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
+ * down action is performed on top close or maximize buttons; the reason for that is we want these
+ * buttons to always work.</li>
+ * <li>The content View will receive the touch event. Mind that content is actually underneath the
+ * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding
+ * {@link #buildTouchDispatchChildList()}.</li>
+ * <li>If the touch event is not consumed by the content View, it will go to the caption View
+ * and the dragging logic will be executed.</li>
+ * </ul>
+ */
+public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
+        GestureDetector.OnGestureListener {
+    private final static String TAG = "DecorCaptionView";
+    private PhoneWindow mOwner = null;
+    private boolean mShow = false;
+
+    // True if the window is being dragged.
+    private boolean mDragging = false;
+
+    private boolean mOverlayWithAppContent = false;
+
+    private View mCaption;
+    private View mContent;
+    private View mMaximize;
+    private View mClose;
+
+    // Fields for detecting drag events.
+    private int mTouchDownX;
+    private int mTouchDownY;
+    private boolean mCheckForDragging;
+    private int mDragSlop;
+
+    // Fields for detecting and intercepting click events on close/maximize.
+    private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
+    // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
+    // with existing click detection.
+    private GestureDetector mGestureDetector;
+    private final Rect mCloseRect = new Rect();
+    private final Rect mMaximizeRect = new Rect();
+    private View mClickTarget;
+
+    public DecorCaptionView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public DecorCaptionView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mGestureDetector = new GestureDetector(context, this);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mCaption = getChildAt(0);
+    }
+
+    public void setPhoneWindow(PhoneWindow owner, boolean show) {
+        mOwner = owner;
+        mShow = show;
+        mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
+        if (mOverlayWithAppContent) {
+            // The caption is covering the content, so we make its background transparent to make
+            // the content visible.
+            mCaption.setBackgroundColor(Color.TRANSPARENT);
+        }
+        updateCaptionVisibility();
+        // By changing the outline provider to BOUNDS, the window can remove its
+        // background without removing the shadow.
+        mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
+        mMaximize = findViewById(R.id.maximize_window);
+        mClose = findViewById(R.id.close_window);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        // If the user starts touch on the maximize/close buttons, we immediately intercept, so
+        // that these buttons are always clickable.
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            final int x = (int) ev.getX();
+            final int y = (int) ev.getY();
+            if (mMaximizeRect.contains(x, y)) {
+                mClickTarget = mMaximize;
+            }
+            if (mCloseRect.contains(x, y)) {
+                mClickTarget = mClose;
+            }
+        }
+        return mClickTarget != null;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mClickTarget != null) {
+            mGestureDetector.onTouchEvent(event);
+            final int action = event.getAction();
+            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+                mClickTarget = null;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent e) {
+        // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
+        // the old input device events get cancelled first. So no need to remember the kind of
+        // input device we are listening to.
+        final int x = (int) e.getX();
+        final int y = (int) e.getY();
+        final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
+        final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
+        switch (e.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                if (!mShow) {
+                    // When there is no caption we should not react to anything.
+                    return false;
+                }
+                // Checking for a drag action is started if we aren't dragging already and the
+                // starting event is either a left mouse button or any other input device.
+                if (!fromMouse || primaryButton) {
+                    mCheckForDragging = true;
+                    mTouchDownX = x;
+                    mTouchDownY = y;
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
+                    mCheckForDragging = false;
+                    mDragging = true;
+                    startMovingTask(e.getRawX(), e.getRawY());
+                    // After the above call the framework will take over the input.
+                    // This handler will receive ACTION_CANCEL soon (possible after a few spurious
+                    // ACTION_MOVE events which are safe to ignore).
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (!mDragging) {
+                    break;
+                }
+                // Abort the ongoing dragging.
+                mDragging = false;
+                return !mCheckForDragging;
+        }
+        return mDragging || mCheckForDragging;
+    }
+
+    @Override
+    public ArrayList<View> buildTouchDispatchChildList() {
+        mTouchDispatchList.ensureCapacity(3);
+        if (mCaption != null) {
+            mTouchDispatchList.add(mCaption);
+        }
+        if (mContent != null) {
+            mTouchDispatchList.add(mContent);
+        }
+        return mTouchDispatchList;
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    private boolean passedSlop(int x, int y) {
+        return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
+    }
+
+    /**
+     * The phone window configuration has changed and the caption needs to be updated.
+     * @param show True if the caption should be shown.
+     */
+    public void onConfigurationChanged(boolean show) {
+        mShow = show;
+        updateCaptionVisibility();
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (!(params instanceof MarginLayoutParams)) {
+            throw new IllegalArgumentException(
+                    "params " + params + " must subclass MarginLayoutParams");
+        }
+        // Make sure that we never get more then one client area in our view.
+        if (index >= 2 || getChildCount() >= 2) {
+            throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
+        }
+        // To support the overlaying content in the caption, we need to put the content view as the
+        // first child to get the right Z-Ordering.
+        super.addView(child, 0, params);
+        mContent = child;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int captionHeight;
+        if (mCaption.getVisibility() != View.GONE) {
+            measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
+            captionHeight = mCaption.getMeasuredHeight();
+        } else {
+            captionHeight = 0;
+        }
+        if (mContent != null) {
+            if (mOverlayWithAppContent) {
+                measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
+            } else {
+                measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
+                        captionHeight);
+            }
+        }
+
+        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+                MeasureSpec.getSize(heightMeasureSpec));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int captionHeight;
+        if (mCaption.getVisibility() != View.GONE) {
+            mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
+            captionHeight = mCaption.getBottom() - mCaption.getTop();
+            mMaximize.getHitRect(mMaximizeRect);
+            mClose.getHitRect(mCloseRect);
+        } else {
+            captionHeight = 0;
+            mMaximizeRect.setEmpty();
+            mCloseRect.setEmpty();
+        }
+
+        if (mContent != null) {
+            if (mOverlayWithAppContent) {
+                mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
+            } else {
+                mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
+                        captionHeight + mContent.getMeasuredHeight());
+            }
+        }
+
+        // This assumes that the caption bar is at the top.
+        mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
+                mClose.getRight(), mClose.getBottom());
+    }
+    /**
+     * Determine if the workspace is entirely covered by the window.
+     * @return Returns true when the window is filling the entire screen/workspace.
+     **/
+    private boolean isFillingScreen() {
+        return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
+                (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+                        View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
+    }
+
+    /**
+     * Updates the visibility of the caption.
+     **/
+    private void updateCaptionVisibility() {
+        // Don't show the caption if the window has e.g. entered full screen.
+        boolean invisible = isFillingScreen() || !mShow;
+        mCaption.setVisibility(invisible ? GONE : VISIBLE);
+        mCaption.setOnTouchListener(this);
+    }
+
+    /**
+     * Maximize the window by moving it to the maximized workspace stack.
+     **/
+    private void maximizeWindow() {
+        Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
+        if (callback != null) {
+            try {
+                callback.exitFreeformMode();
+            } catch (RemoteException ex) {
+                Log.e(TAG, "Cannot change task workspace.");
+            }
+        }
+    }
+
+    public boolean isCaptionShowing() {
+        return mShow;
+    }
+
+    public int getCaptionHeight() {
+        return (mCaption != null) ? mCaption.getHeight() : 0;
+    }
+
+    public void removeContentView() {
+        if (mContent != null) {
+            removeView(mContent);
+            mContent = null;
+        }
+    }
+
+    public View getCaption() {
+        return mCaption;
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new MarginLayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
+                MarginLayoutParams.MATCH_PARENT);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(LayoutParams p) {
+        return new MarginLayoutParams(p);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof MarginLayoutParams;
+    }
+
+    @Override
+    public boolean onDown(MotionEvent e) {
+        return false;
+    }
+
+    @Override
+    public void onShowPress(MotionEvent e) {
+
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        if (mClickTarget == mMaximize) {
+            maximizeWindow();
+        } else if (mClickTarget == mClose) {
+            mOwner.dispatchOnWindowDismissed(
+                    true /*finishTask*/, false /*suppressWindowTransition*/);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        return false;
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {
+
+    }
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        return false;
+    }
+}
diff --git a/com/android/internal/widget/DecorContentParent.java b/com/android/internal/widget/DecorContentParent.java
new file mode 100644
index 0000000..ac524f9
--- /dev/null
+++ b/com/android/internal/widget/DecorContentParent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.Window;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Implemented by the top-level decor layout for a window. DecorContentParent offers
+ * entry points for a number of title/window decor features.
+ */
+public interface DecorContentParent {
+    void setWindowCallback(Window.Callback cb);
+    void setWindowTitle(CharSequence title);
+    CharSequence getTitle();
+    void initFeature(int windowFeature);
+    void setUiOptions(int uiOptions);
+    boolean hasIcon();
+    boolean hasLogo();
+    void setIcon(int resId);
+    void setIcon(Drawable d);
+    void setLogo(int resId);
+    boolean canShowOverflowMenu();
+    boolean isOverflowMenuShowing();
+    boolean isOverflowMenuShowPending();
+    boolean showOverflowMenu();
+    boolean hideOverflowMenu();
+    void setMenuPrepared();
+    void setMenu(Menu menu, MenuPresenter.Callback cb);
+    void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
+    void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
+    void dismissPopups();
+}
diff --git a/com/android/internal/widget/DecorToolbar.java b/com/android/internal/widget/DecorToolbar.java
new file mode 100644
index 0000000..fe70d7b
--- /dev/null
+++ b/com/android/internal/widget/DecorToolbar.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.SpinnerAdapter;
+
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Common interface for a toolbar that sits as part of the window decor.
+ * Layouts that control window decor use this as a point of interaction with different
+ * bar implementations.
+ *
+ * @hide
+ */
+public interface DecorToolbar {
+    ViewGroup getViewGroup();
+    Context getContext();
+    boolean isSplit();
+    boolean hasExpandedActionView();
+    void collapseActionView();
+    void setWindowCallback(Window.Callback cb);
+    void setWindowTitle(CharSequence title);
+    CharSequence getTitle();
+    void setTitle(CharSequence title);
+    CharSequence getSubtitle();
+    void setSubtitle(CharSequence subtitle);
+    void initProgress();
+    void initIndeterminateProgress();
+    boolean canSplit();
+    void setSplitView(ViewGroup splitView);
+    void setSplitToolbar(boolean split);
+    void setSplitWhenNarrow(boolean splitWhenNarrow);
+    boolean hasIcon();
+    boolean hasLogo();
+    void setIcon(int resId);
+    void setIcon(Drawable d);
+    void setLogo(int resId);
+    void setLogo(Drawable d);
+    boolean canShowOverflowMenu();
+    boolean isOverflowMenuShowing();
+    boolean isOverflowMenuShowPending();
+    boolean showOverflowMenu();
+    boolean hideOverflowMenu();
+    void setMenuPrepared();
+    void setMenu(Menu menu, MenuPresenter.Callback cb);
+    void dismissPopupMenus();
+
+    int getDisplayOptions();
+    void setDisplayOptions(int opts);
+    void setEmbeddedTabView(ScrollingTabContainerView tabView);
+    boolean hasEmbeddedTabs();
+    boolean isTitleTruncated();
+    void setCollapsible(boolean collapsible);
+    void setHomeButtonEnabled(boolean enable);
+    int getNavigationMode();
+    void setNavigationMode(int mode);
+    void setDropdownParams(SpinnerAdapter adapter, AdapterView.OnItemSelectedListener listener);
+    void setDropdownSelectedPosition(int position);
+    int getDropdownSelectedPosition();
+    int getDropdownItemCount();
+    void setCustomView(View view);
+    View getCustomView();
+    void animateToVisibility(int visibility);
+    Animator setupAnimatorToVisibility(int visibility, long duration);
+    void setNavigationIcon(Drawable icon);
+    void setNavigationIcon(int resId);
+    void setNavigationContentDescription(CharSequence description);
+    void setNavigationContentDescription(int resId);
+    void setDefaultNavigationContentDescription(int defaultNavigationContentDescription);
+    void setDefaultNavigationIcon(Drawable icon);
+    void saveHierarchyState(SparseArray<Parcelable> toolbarStates);
+    void restoreHierarchyState(SparseArray<Parcelable> toolbarStates);
+    void setBackgroundDrawable(Drawable d);
+    int getHeight();
+    void setVisibility(int visible);
+    int getVisibility();
+    void setMenuCallbacks(MenuPresenter.Callback presenterCallback,
+            MenuBuilder.Callback menuBuilderCallback);
+    Menu getMenu();
+}
diff --git a/com/android/internal/widget/DefaultItemAnimator.java b/com/android/internal/widget/DefaultItemAnimator.java
new file mode 100644
index 0000000..92345af
--- /dev/null
+++ b/com/android/internal/widget/DefaultItemAnimator.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+
+import com.android.internal.widget.RecyclerView.ViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This implementation of {@link RecyclerView.ItemAnimator} provides basic
+ * animations on remove, add, and move events that happen to the items in
+ * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
+ *
+ * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
+ */
+public class DefaultItemAnimator extends SimpleItemAnimator {
+    private static final boolean DEBUG = false;
+
+    private static TimeInterpolator sDefaultInterpolator;
+
+    private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
+    private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
+    private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
+    private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
+
+    ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
+    ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
+    ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
+
+    ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
+    ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
+    ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
+    ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
+
+    private static class MoveInfo {
+        public ViewHolder holder;
+        public int fromX, fromY, toX, toY;
+
+        MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+            this.holder = holder;
+            this.fromX = fromX;
+            this.fromY = fromY;
+            this.toX = toX;
+            this.toY = toY;
+        }
+    }
+
+    private static class ChangeInfo {
+        public ViewHolder oldHolder, newHolder;
+        public int fromX, fromY, toX, toY;
+        private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
+            this.oldHolder = oldHolder;
+            this.newHolder = newHolder;
+        }
+
+        ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
+                int fromX, int fromY, int toX, int toY) {
+            this(oldHolder, newHolder);
+            this.fromX = fromX;
+            this.fromY = fromY;
+            this.toX = toX;
+            this.toY = toY;
+        }
+
+        @Override
+        public String toString() {
+            return "ChangeInfo{"
+                    + "oldHolder=" + oldHolder
+                    + ", newHolder=" + newHolder
+                    + ", fromX=" + fromX
+                    + ", fromY=" + fromY
+                    + ", toX=" + toX
+                    + ", toY=" + toY
+                    + '}';
+        }
+    }
+
+    @Override
+    public void runPendingAnimations() {
+        boolean removalsPending = !mPendingRemovals.isEmpty();
+        boolean movesPending = !mPendingMoves.isEmpty();
+        boolean changesPending = !mPendingChanges.isEmpty();
+        boolean additionsPending = !mPendingAdditions.isEmpty();
+        if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
+            // nothing to animate
+            return;
+        }
+        // First, remove stuff
+        for (ViewHolder holder : mPendingRemovals) {
+            animateRemoveImpl(holder);
+        }
+        mPendingRemovals.clear();
+        // Next, move stuff
+        if (movesPending) {
+            final ArrayList<MoveInfo> moves = new ArrayList<>();
+            moves.addAll(mPendingMoves);
+            mMovesList.add(moves);
+            mPendingMoves.clear();
+            Runnable mover = new Runnable() {
+                @Override
+                public void run() {
+                    for (MoveInfo moveInfo : moves) {
+                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
+                                moveInfo.toX, moveInfo.toY);
+                    }
+                    moves.clear();
+                    mMovesList.remove(moves);
+                }
+            };
+            if (removalsPending) {
+                View view = moves.get(0).holder.itemView;
+                view.postOnAnimationDelayed(mover, getRemoveDuration());
+            } else {
+                mover.run();
+            }
+        }
+        // Next, change stuff, to run in parallel with move animations
+        if (changesPending) {
+            final ArrayList<ChangeInfo> changes = new ArrayList<>();
+            changes.addAll(mPendingChanges);
+            mChangesList.add(changes);
+            mPendingChanges.clear();
+            Runnable changer = new Runnable() {
+                @Override
+                public void run() {
+                    for (ChangeInfo change : changes) {
+                        animateChangeImpl(change);
+                    }
+                    changes.clear();
+                    mChangesList.remove(changes);
+                }
+            };
+            if (removalsPending) {
+                ViewHolder holder = changes.get(0).oldHolder;
+                holder.itemView.postOnAnimationDelayed(changer, getRemoveDuration());
+            } else {
+                changer.run();
+            }
+        }
+        // Next, add stuff
+        if (additionsPending) {
+            final ArrayList<ViewHolder> additions = new ArrayList<>();
+            additions.addAll(mPendingAdditions);
+            mAdditionsList.add(additions);
+            mPendingAdditions.clear();
+            Runnable adder = new Runnable() {
+                @Override
+                public void run() {
+                    for (ViewHolder holder : additions) {
+                        animateAddImpl(holder);
+                    }
+                    additions.clear();
+                    mAdditionsList.remove(additions);
+                }
+            };
+            if (removalsPending || movesPending || changesPending) {
+                long removeDuration = removalsPending ? getRemoveDuration() : 0;
+                long moveDuration = movesPending ? getMoveDuration() : 0;
+                long changeDuration = changesPending ? getChangeDuration() : 0;
+                long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
+                View view = additions.get(0).itemView;
+                view.postOnAnimationDelayed(adder, totalDelay);
+            } else {
+                adder.run();
+            }
+        }
+    }
+
+    @Override
+    public boolean animateRemove(final ViewHolder holder) {
+        resetAnimation(holder);
+        mPendingRemovals.add(holder);
+        return true;
+    }
+
+    private void animateRemoveImpl(final ViewHolder holder) {
+        final View view = holder.itemView;
+        final ViewPropertyAnimator animation = view.animate();
+        mRemoveAnimations.add(holder);
+        animation.setDuration(getRemoveDuration()).alpha(0).setListener(
+                new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animator) {
+                        dispatchRemoveStarting(holder);
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animator) {
+                        animation.setListener(null);
+                        view.setAlpha(1);
+                        dispatchRemoveFinished(holder);
+                        mRemoveAnimations.remove(holder);
+                        dispatchFinishedWhenDone();
+                    }
+                }).start();
+    }
+
+    @Override
+    public boolean animateAdd(final ViewHolder holder) {
+        resetAnimation(holder);
+        holder.itemView.setAlpha(0);
+        mPendingAdditions.add(holder);
+        return true;
+    }
+
+    void animateAddImpl(final ViewHolder holder) {
+        final View view = holder.itemView;
+        final ViewPropertyAnimator animation = view.animate();
+        mAddAnimations.add(holder);
+        animation.alpha(1).setDuration(getAddDuration())
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animator) {
+                        dispatchAddStarting(holder);
+                    }
+
+                    @Override
+                    public void onAnimationCancel(Animator animator) {
+                        view.setAlpha(1);
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animator) {
+                        animation.setListener(null);
+                        dispatchAddFinished(holder);
+                        mAddAnimations.remove(holder);
+                        dispatchFinishedWhenDone();
+                    }
+                }).start();
+    }
+
+    @Override
+    public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
+            int toX, int toY) {
+        final View view = holder.itemView;
+        fromX += holder.itemView.getTranslationX();
+        fromY += holder.itemView.getTranslationY();
+        resetAnimation(holder);
+        int deltaX = toX - fromX;
+        int deltaY = toY - fromY;
+        if (deltaX == 0 && deltaY == 0) {
+            dispatchMoveFinished(holder);
+            return false;
+        }
+        if (deltaX != 0) {
+            view.setTranslationX(-deltaX);
+        }
+        if (deltaY != 0) {
+            view.setTranslationY(-deltaY);
+        }
+        mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
+        return true;
+    }
+
+    void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+        final View view = holder.itemView;
+        final int deltaX = toX - fromX;
+        final int deltaY = toY - fromY;
+        if (deltaX != 0) {
+            view.animate().translationX(0);
+        }
+        if (deltaY != 0) {
+            view.animate().translationY(0);
+        }
+        // TODO: make EndActions end listeners instead, since end actions aren't called when
+        // vpas are canceled (and can't end them. why?)
+        // need listener functionality in VPACompat for this. Ick.
+        final ViewPropertyAnimator animation = view.animate();
+        mMoveAnimations.add(holder);
+        animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+                dispatchMoveStarting(holder);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {
+                if (deltaX != 0) {
+                    view.setTranslationX(0);
+                }
+                if (deltaY != 0) {
+                    view.setTranslationY(0);
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                animation.setListener(null);
+                dispatchMoveFinished(holder);
+                mMoveAnimations.remove(holder);
+                dispatchFinishedWhenDone();
+            }
+        }).start();
+    }
+
+    @Override
+    public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
+            int fromX, int fromY, int toX, int toY) {
+        if (oldHolder == newHolder) {
+            // Don't know how to run change animations when the same view holder is re-used.
+            // run a move animation to handle position changes.
+            return animateMove(oldHolder, fromX, fromY, toX, toY);
+        }
+        final float prevTranslationX = oldHolder.itemView.getTranslationX();
+        final float prevTranslationY = oldHolder.itemView.getTranslationY();
+        final float prevAlpha = oldHolder.itemView.getAlpha();
+        resetAnimation(oldHolder);
+        int deltaX = (int) (toX - fromX - prevTranslationX);
+        int deltaY = (int) (toY - fromY - prevTranslationY);
+        // recover prev translation state after ending animation
+        oldHolder.itemView.setTranslationX(prevTranslationX);
+        oldHolder.itemView.setTranslationY(prevTranslationY);
+        oldHolder.itemView.setAlpha(prevAlpha);
+        if (newHolder != null) {
+            // carry over translation values
+            resetAnimation(newHolder);
+            newHolder.itemView.setTranslationX(-deltaX);
+            newHolder.itemView.setTranslationY(-deltaY);
+            newHolder.itemView.setAlpha(0);
+        }
+        mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
+        return true;
+    }
+
+    void animateChangeImpl(final ChangeInfo changeInfo) {
+        final ViewHolder holder = changeInfo.oldHolder;
+        final View view = holder == null ? null : holder.itemView;
+        final ViewHolder newHolder = changeInfo.newHolder;
+        final View newView = newHolder != null ? newHolder.itemView : null;
+        if (view != null) {
+            final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
+                    getChangeDuration());
+            mChangeAnimations.add(changeInfo.oldHolder);
+            oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
+            oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
+            oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animator) {
+                    dispatchChangeStarting(changeInfo.oldHolder, true);
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animator) {
+                    oldViewAnim.setListener(null);
+                    view.setAlpha(1);
+                    view.setTranslationX(0);
+                    view.setTranslationY(0);
+                    dispatchChangeFinished(changeInfo.oldHolder, true);
+                    mChangeAnimations.remove(changeInfo.oldHolder);
+                    dispatchFinishedWhenDone();
+                }
+            }).start();
+        }
+        if (newView != null) {
+            final ViewPropertyAnimator newViewAnimation = newView.animate();
+            mChangeAnimations.add(changeInfo.newHolder);
+            newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
+                    .alpha(1).setListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationStart(Animator animator) {
+                            dispatchChangeStarting(changeInfo.newHolder, false);
+                        }
+                        @Override
+                        public void onAnimationEnd(Animator animator) {
+                            newViewAnimation.setListener(null);
+                            newView.setAlpha(1);
+                            newView.setTranslationX(0);
+                            newView.setTranslationY(0);
+                            dispatchChangeFinished(changeInfo.newHolder, false);
+                            mChangeAnimations.remove(changeInfo.newHolder);
+                            dispatchFinishedWhenDone();
+                        }
+                    }).start();
+        }
+    }
+
+    private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
+        for (int i = infoList.size() - 1; i >= 0; i--) {
+            ChangeInfo changeInfo = infoList.get(i);
+            if (endChangeAnimationIfNecessary(changeInfo, item)) {
+                if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
+                    infoList.remove(changeInfo);
+                }
+            }
+        }
+    }
+
+    private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
+        if (changeInfo.oldHolder != null) {
+            endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
+        }
+        if (changeInfo.newHolder != null) {
+            endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
+        }
+    }
+    private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
+        boolean oldItem = false;
+        if (changeInfo.newHolder == item) {
+            changeInfo.newHolder = null;
+        } else if (changeInfo.oldHolder == item) {
+            changeInfo.oldHolder = null;
+            oldItem = true;
+        } else {
+            return false;
+        }
+        item.itemView.setAlpha(1);
+        item.itemView.setTranslationX(0);
+        item.itemView.setTranslationY(0);
+        dispatchChangeFinished(item, oldItem);
+        return true;
+    }
+
+    @Override
+    public void endAnimation(ViewHolder item) {
+        final View view = item.itemView;
+        // this will trigger end callback which should set properties to their target values.
+        view.animate().cancel();
+        // TODO if some other animations are chained to end, how do we cancel them as well?
+        for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
+            MoveInfo moveInfo = mPendingMoves.get(i);
+            if (moveInfo.holder == item) {
+                view.setTranslationY(0);
+                view.setTranslationX(0);
+                dispatchMoveFinished(item);
+                mPendingMoves.remove(i);
+            }
+        }
+        endChangeAnimation(mPendingChanges, item);
+        if (mPendingRemovals.remove(item)) {
+            view.setAlpha(1);
+            dispatchRemoveFinished(item);
+        }
+        if (mPendingAdditions.remove(item)) {
+            view.setAlpha(1);
+            dispatchAddFinished(item);
+        }
+
+        for (int i = mChangesList.size() - 1; i >= 0; i--) {
+            ArrayList<ChangeInfo> changes = mChangesList.get(i);
+            endChangeAnimation(changes, item);
+            if (changes.isEmpty()) {
+                mChangesList.remove(i);
+            }
+        }
+        for (int i = mMovesList.size() - 1; i >= 0; i--) {
+            ArrayList<MoveInfo> moves = mMovesList.get(i);
+            for (int j = moves.size() - 1; j >= 0; j--) {
+                MoveInfo moveInfo = moves.get(j);
+                if (moveInfo.holder == item) {
+                    view.setTranslationY(0);
+                    view.setTranslationX(0);
+                    dispatchMoveFinished(item);
+                    moves.remove(j);
+                    if (moves.isEmpty()) {
+                        mMovesList.remove(i);
+                    }
+                    break;
+                }
+            }
+        }
+        for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
+            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+            if (additions.remove(item)) {
+                view.setAlpha(1);
+                dispatchAddFinished(item);
+                if (additions.isEmpty()) {
+                    mAdditionsList.remove(i);
+                }
+            }
+        }
+
+        // animations should be ended by the cancel above.
+        //noinspection PointlessBooleanExpression,ConstantConditions
+        if (mRemoveAnimations.remove(item) && DEBUG) {
+            throw new IllegalStateException("after animation is cancelled, item should not be in "
+                    + "mRemoveAnimations list");
+        }
+
+        //noinspection PointlessBooleanExpression,ConstantConditions
+        if (mAddAnimations.remove(item) && DEBUG) {
+            throw new IllegalStateException("after animation is cancelled, item should not be in "
+                    + "mAddAnimations list");
+        }
+
+        //noinspection PointlessBooleanExpression,ConstantConditions
+        if (mChangeAnimations.remove(item) && DEBUG) {
+            throw new IllegalStateException("after animation is cancelled, item should not be in "
+                    + "mChangeAnimations list");
+        }
+
+        //noinspection PointlessBooleanExpression,ConstantConditions
+        if (mMoveAnimations.remove(item) && DEBUG) {
+            throw new IllegalStateException("after animation is cancelled, item should not be in "
+                    + "mMoveAnimations list");
+        }
+        dispatchFinishedWhenDone();
+    }
+
+    private void resetAnimation(ViewHolder holder) {
+        if (sDefaultInterpolator == null) {
+            sDefaultInterpolator = new ValueAnimator().getInterpolator();
+        }
+        holder.itemView.animate().setInterpolator(sDefaultInterpolator);
+        endAnimation(holder);
+    }
+
+    @Override
+    public boolean isRunning() {
+        return (!mPendingAdditions.isEmpty()
+                || !mPendingChanges.isEmpty()
+                || !mPendingMoves.isEmpty()
+                || !mPendingRemovals.isEmpty()
+                || !mMoveAnimations.isEmpty()
+                || !mRemoveAnimations.isEmpty()
+                || !mAddAnimations.isEmpty()
+                || !mChangeAnimations.isEmpty()
+                || !mMovesList.isEmpty()
+                || !mAdditionsList.isEmpty()
+                || !mChangesList.isEmpty());
+    }
+
+    /**
+     * Check the state of currently pending and running animations. If there are none
+     * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
+     * listeners.
+     */
+    void dispatchFinishedWhenDone() {
+        if (!isRunning()) {
+            dispatchAnimationsFinished();
+        }
+    }
+
+    @Override
+    public void endAnimations() {
+        int count = mPendingMoves.size();
+        for (int i = count - 1; i >= 0; i--) {
+            MoveInfo item = mPendingMoves.get(i);
+            View view = item.holder.itemView;
+            view.setTranslationY(0);
+            view.setTranslationX(0);
+            dispatchMoveFinished(item.holder);
+            mPendingMoves.remove(i);
+        }
+        count = mPendingRemovals.size();
+        for (int i = count - 1; i >= 0; i--) {
+            ViewHolder item = mPendingRemovals.get(i);
+            dispatchRemoveFinished(item);
+            mPendingRemovals.remove(i);
+        }
+        count = mPendingAdditions.size();
+        for (int i = count - 1; i >= 0; i--) {
+            ViewHolder item = mPendingAdditions.get(i);
+            item.itemView.setAlpha(1);
+            dispatchAddFinished(item);
+            mPendingAdditions.remove(i);
+        }
+        count = mPendingChanges.size();
+        for (int i = count - 1; i >= 0; i--) {
+            endChangeAnimationIfNecessary(mPendingChanges.get(i));
+        }
+        mPendingChanges.clear();
+        if (!isRunning()) {
+            return;
+        }
+
+        int listCount = mMovesList.size();
+        for (int i = listCount - 1; i >= 0; i--) {
+            ArrayList<MoveInfo> moves = mMovesList.get(i);
+            count = moves.size();
+            for (int j = count - 1; j >= 0; j--) {
+                MoveInfo moveInfo = moves.get(j);
+                ViewHolder item = moveInfo.holder;
+                View view = item.itemView;
+                view.setTranslationY(0);
+                view.setTranslationX(0);
+                dispatchMoveFinished(moveInfo.holder);
+                moves.remove(j);
+                if (moves.isEmpty()) {
+                    mMovesList.remove(moves);
+                }
+            }
+        }
+        listCount = mAdditionsList.size();
+        for (int i = listCount - 1; i >= 0; i--) {
+            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+            count = additions.size();
+            for (int j = count - 1; j >= 0; j--) {
+                ViewHolder item = additions.get(j);
+                View view = item.itemView;
+                view.setAlpha(1);
+                dispatchAddFinished(item);
+                additions.remove(j);
+                if (additions.isEmpty()) {
+                    mAdditionsList.remove(additions);
+                }
+            }
+        }
+        listCount = mChangesList.size();
+        for (int i = listCount - 1; i >= 0; i--) {
+            ArrayList<ChangeInfo> changes = mChangesList.get(i);
+            count = changes.size();
+            for (int j = count - 1; j >= 0; j--) {
+                endChangeAnimationIfNecessary(changes.get(j));
+                if (changes.isEmpty()) {
+                    mChangesList.remove(changes);
+                }
+            }
+        }
+
+        cancelAll(mRemoveAnimations);
+        cancelAll(mMoveAnimations);
+        cancelAll(mAddAnimations);
+        cancelAll(mChangeAnimations);
+
+        dispatchAnimationsFinished();
+    }
+
+    void cancelAll(List<ViewHolder> viewHolders) {
+        for (int i = viewHolders.size() - 1; i >= 0; i--) {
+            viewHolders.get(i).itemView.animate().cancel();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
+     * When this is the case:
+     * <ul>
+     * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both
+     * ViewHolder arguments will be the same instance.
+     * </li>
+     * <li>
+     * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
+     * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
+     * run a move animation instead.
+     * </li>
+     * </ul>
+     */
+    @Override
+    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+            @NonNull List<Object> payloads) {
+        return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
+    }
+}
diff --git a/com/android/internal/widget/DialogTitle.java b/com/android/internal/widget/DialogTitle.java
new file mode 100644
index 0000000..7ea3d6b
--- /dev/null
+++ b/com/android/internal/widget/DialogTitle.java
@@ -0,0 +1,77 @@
+/* 
+ * Copyright (C) 2008 Google Inc.
+ *
+ * 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Used by dialogs to change the font size and number of lines to try to fit
+ * the text to the available space.
+ */
+public class DialogTitle extends TextView {
+
+    public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public DialogTitle(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DialogTitle(Context context) {
+        super(context);
+    }
+    
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        final Layout layout = getLayout();
+        if (layout != null) {
+            final int lineCount = layout.getLineCount();
+            if (lineCount > 0) {
+                final int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
+                if (ellipsisCount > 0) {
+                    setSingleLine(false);
+                    setMaxLines(2);
+
+                    final TypedArray a = mContext.obtainStyledAttributes(null,
+                            android.R.styleable.TextAppearance, android.R.attr.textAppearanceMedium,
+                            android.R.style.TextAppearance_Medium);
+                    final int textSize = a.getDimensionPixelSize(
+                            android.R.styleable.TextAppearance_textSize, 0);
+                    if (textSize != 0) {
+                        // textSize is already expressed in pixels
+                        setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+                    }
+                    a.recycle();
+
+                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);      
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/internal/widget/DialogViewAnimator.java b/com/android/internal/widget/DialogViewAnimator.java
new file mode 100644
index 0000000..bdfc1af
--- /dev/null
+++ b/com/android/internal/widget/DialogViewAnimator.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ViewAnimator;
+
+import java.util.ArrayList;
+
+/**
+ * ViewAnimator with a more reasonable handling of MATCH_PARENT.
+ */
+public class DialogViewAnimator extends ViewAnimator {
+    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+    public DialogViewAnimator(Context context) {
+        super(context);
+    }
+
+    public DialogViewAnimator(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final boolean measureMatchParentChildren =
+                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+                        MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+        int childState = 0;
+
+        // First measure all children and record maximum dimensions where the
+        // spec isn't MATCH_PARENT.
+        final int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (getMeasureAllChildren() || child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                final boolean matchWidth = lp.width == LayoutParams.MATCH_PARENT;
+                final boolean matchHeight = lp.height == LayoutParams.MATCH_PARENT;
+                if (measureMatchParentChildren && (matchWidth || matchHeight)) {
+                    mMatchParentChildren.add(child);
+                }
+
+                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+
+                // Measured dimensions only count against the maximum
+                // dimensions if they're not MATCH_PARENT.
+                int state = 0;
+
+                if (measureMatchParentChildren && !matchWidth) {
+                    maxWidth = Math.max(maxWidth, child.getMeasuredWidth()
+                            + lp.leftMargin + lp.rightMargin);
+                    state |= child.getMeasuredWidthAndState() & MEASURED_STATE_MASK;
+                }
+
+                if (measureMatchParentChildren && !matchHeight) {
+                    maxHeight = Math.max(maxHeight, child.getMeasuredHeight()
+                            + lp.topMargin + lp.bottomMargin);
+                    state |= (child.getMeasuredHeightAndState() >> MEASURED_HEIGHT_STATE_SHIFT)
+                            & (MEASURED_STATE_MASK >> MEASURED_HEIGHT_STATE_SHIFT);
+                }
+
+                childState = combineMeasuredStates(childState, state);
+            }
+        }
+
+        // Account for padding too.
+        maxWidth += getPaddingLeft() + getPaddingRight();
+        maxHeight += getPaddingTop() + getPaddingBottom();
+
+        // Check against our minimum height and width.
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        // Check against our foreground's minimum height and width.
+        final Drawable drawable = getForeground();
+        if (drawable != null) {
+            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+        }
+
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+        // Measure remaining MATCH_PARENT children again using real dimensions.
+        final int matchCount = mMatchParentChildren.size();
+        for (int i = 0; i < matchCount; i++) {
+            final View child = mMatchParentChildren.get(i);
+            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+            final int childWidthMeasureSpec;
+            if (lp.width == LayoutParams.MATCH_PARENT) {
+                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        getMeasuredWidth() - getPaddingLeft() - getPaddingRight()
+                                - lp.leftMargin - lp.rightMargin,
+                        MeasureSpec.EXACTLY);
+            } else {
+                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                        getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
+                        lp.width);
+            }
+
+            final int childHeightMeasureSpec;
+            if (lp.height == LayoutParams.MATCH_PARENT) {
+                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
+                                - lp.topMargin - lp.bottomMargin,
+                        MeasureSpec.EXACTLY);
+            } else {
+                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                        getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin,
+                        lp.height);
+            }
+
+            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+        }
+
+        mMatchParentChildren.clear();
+    }
+}
diff --git a/com/android/internal/widget/DrawableHolder.java b/com/android/internal/widget/DrawableHolder.java
new file mode 100644
index 0000000..947e0f3
--- /dev/null
+++ b/com/android/internal/widget/DrawableHolder.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2010 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.internal.widget;
+
+import java.util.ArrayList;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.animation.Animator.AnimatorListener;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * This class is a container for a Drawable with multiple animated properties.
+ *
+ */
+public class DrawableHolder implements AnimatorListener {
+    public static final DecelerateInterpolator EASE_OUT_INTERPOLATOR = new DecelerateInterpolator();
+    private static final String TAG = "DrawableHolder";
+    private static final boolean DBG = false;
+    private float mX = 0.0f;
+    private float mY = 0.0f;
+    private float mScaleX = 1.0f;
+    private float mScaleY = 1.0f;
+    private BitmapDrawable mDrawable;
+    private float mAlpha = 1f;
+    private ArrayList<ObjectAnimator> mAnimators = new ArrayList<ObjectAnimator>();
+    private ArrayList<ObjectAnimator> mNeedToStart = new ArrayList<ObjectAnimator>();
+
+    public DrawableHolder(BitmapDrawable drawable) {
+        this(drawable, 0.0f, 0.0f);
+    }
+
+    public DrawableHolder(BitmapDrawable drawable, float x, float y) {
+        mDrawable = drawable;
+        mX = x;
+        mY = y;
+        mDrawable.getPaint().setAntiAlias(true); // Force AA
+        mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
+    }
+
+    /**
+     *
+     * Adds an animation that interpolates given property from its current value
+     * to the given value.
+     *
+     * @param duration the duration, in ms.
+     * @param delay the delay to start the animation, in ms.
+     * @param property the property to animate
+     * @param toValue the target value
+     * @param replace if true, replace the current animation with this one.
+     */
+    public ObjectAnimator addAnimTo(long duration, long delay,
+            String property, float toValue, boolean replace) {
+
+        if (replace) removeAnimationFor(property);
+
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, property, toValue);
+        anim.setDuration(duration);
+        anim.setStartDelay(delay);
+        anim.setInterpolator(EASE_OUT_INTERPOLATOR);
+        this.addAnimation(anim, replace);
+        if (DBG) Log.v(TAG, "animationCount = " + mAnimators.size());
+        return anim;
+    }
+
+    /**
+     * Stops all animations for the given property and removes it from the list.
+     *
+     * @param property
+     */
+    public void removeAnimationFor(String property) {
+        ArrayList<ObjectAnimator> removalList = (ArrayList<ObjectAnimator>)mAnimators.clone();
+        for (ObjectAnimator currentAnim : removalList) {
+            if (property.equals(currentAnim.getPropertyName())) {
+                currentAnim.cancel();
+            }
+        }
+    }
+
+    /**
+     * Stops all animations and removes them from the list.
+     */
+    public void clearAnimations() {
+        for (ObjectAnimator currentAnim : mAnimators) {
+            currentAnim.cancel();
+        }
+        mAnimators.clear();
+    }
+
+    /**
+     * Adds the given animation to the list of animations for this object.
+     *
+     * @param anim
+     * @param overwrite
+     * @return
+     */
+    private DrawableHolder addAnimation(ObjectAnimator anim, boolean overwrite) {
+        if (anim != null)
+            mAnimators.add(anim);
+        mNeedToStart.add(anim);
+        return this;
+    }
+
+    /**
+     * Draw this object to the canvas using the properties defined in this class.
+     *
+     * @param canvas canvas to draw into
+     */
+    public void draw(Canvas canvas) {
+        final float threshold = 1.0f / 256.0f; // contribution less than 1 LSB of RGB byte
+        if (mAlpha <= threshold) // don't bother if it won't show up
+            return;
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.translate(mX, mY);
+        canvas.scale(mScaleX, mScaleY);
+        canvas.translate(-0.5f*getWidth(), -0.5f*getHeight());
+        mDrawable.setAlpha((int) Math.round(mAlpha * 255f));
+        mDrawable.draw(canvas);
+        canvas.restore();
+    }
+
+    /**
+     * Starts all animations added since the last call to this function.  Used to synchronize
+     * animations.
+     *
+     * @param listener an optional listener to add to the animations. Typically used to know when
+     * to invalidate the surface these are being drawn to.
+     */
+    public void startAnimations(ValueAnimator.AnimatorUpdateListener listener) {
+        for (int i = 0; i < mNeedToStart.size(); i++) {
+            ObjectAnimator anim = mNeedToStart.get(i);
+            anim.addUpdateListener(listener);
+            anim.addListener(this);
+            anim.start();
+        }
+        mNeedToStart.clear();
+    }
+
+
+    public void setX(float value) {
+        mX = value;
+    }
+
+    public void setY(float value) {
+        mY = value;
+    }
+
+    public void setScaleX(float value) {
+        mScaleX = value;
+    }
+
+    public void setScaleY(float value) {
+        mScaleY = value;
+    }
+
+    public void setAlpha(float alpha) {
+        mAlpha = alpha;
+    }
+
+    public float getX() {
+        return mX;
+    }
+
+    public float getY() {
+        return mY;
+    }
+
+    public float getScaleX() {
+        return mScaleX;
+    }
+
+    public float getScaleY() {
+        return mScaleY;
+    }
+
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    public BitmapDrawable getDrawable() {
+        return mDrawable;
+    }
+
+    public int getWidth() {
+        return mDrawable.getIntrinsicWidth();
+    }
+
+    public int getHeight() {
+        return mDrawable.getIntrinsicHeight();
+    }
+
+    public void onAnimationCancel(Animator animation) {
+
+    }
+
+    public void onAnimationEnd(Animator animation) {
+        mAnimators.remove(animation);
+    }
+
+    public void onAnimationRepeat(Animator animation) {
+
+    }
+
+    public void onAnimationStart(Animator animation) {
+
+    }
+}
diff --git a/com/android/internal/widget/EditableInputConnection.java b/com/android/internal/widget/EditableInputConnection.java
new file mode 100644
index 0000000..7f7528d
--- /dev/null
+++ b/com/android/internal/widget/EditableInputConnection.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2007-2008 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.internal.widget;
+
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Spanned;
+import android.text.method.KeyListener;
+import android.text.style.SuggestionSpan;
+import android.util.Log;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+public class EditableInputConnection extends BaseInputConnection {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "EditableInputConnection";
+
+    private final TextView mTextView;
+
+    // Keeps track of nested begin/end batch edit to ensure this connection always has a
+    // balanced impact on its associated TextView.
+    // A negative value means that this connection has been finished by the InputMethodManager.
+    private int mBatchEditNesting;
+
+    public EditableInputConnection(TextView textview) {
+        super(textview, true);
+        mTextView = textview;
+    }
+
+    @Override
+    public Editable getEditable() {
+        TextView tv = mTextView;
+        if (tv != null) {
+            return tv.getEditableText();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean beginBatchEdit() {
+        synchronized(this) {
+            if (mBatchEditNesting >= 0) {
+                mTextView.beginBatchEdit();
+                mBatchEditNesting++;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean endBatchEdit() {
+        synchronized(this) {
+            if (mBatchEditNesting > 0) {
+                // When the connection is reset by the InputMethodManager and reportFinish
+                // is called, some endBatchEdit calls may still be asynchronously received from the
+                // IME. Do not take these into account, thus ensuring that this IC's final
+                // contribution to mTextView's nested batch edit count is zero.
+                mTextView.endBatchEdit();
+                mBatchEditNesting--;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void closeConnection() {
+        super.closeConnection();
+        synchronized(this) {
+            while (mBatchEditNesting > 0) {
+                endBatchEdit();
+            }
+            // Will prevent any further calls to begin or endBatchEdit
+            mBatchEditNesting = -1;
+        }
+    }
+
+    @Override
+    public boolean clearMetaKeyStates(int states) {
+        final Editable content = getEditable();
+        if (content == null) return false;
+        KeyListener kl = mTextView.getKeyListener();
+        if (kl != null) {
+            try {
+                kl.clearMetaKeyState(mTextView, content, states);
+            } catch (AbstractMethodError e) {
+                // This is an old listener that doesn't implement the
+                // new method.
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean commitCompletion(CompletionInfo text) {
+        if (DEBUG) Log.v(TAG, "commitCompletion " + text);
+        mTextView.beginBatchEdit();
+        mTextView.onCommitCompletion(text);
+        mTextView.endBatchEdit();
+        return true;
+    }
+
+    /**
+     * Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
+     */
+    @Override
+    public boolean commitCorrection(CorrectionInfo correctionInfo) {
+        if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
+        mTextView.beginBatchEdit();
+        mTextView.onCommitCorrection(correctionInfo);
+        mTextView.endBatchEdit();
+        return true;
+    }
+
+    @Override
+    public boolean performEditorAction(int actionCode) {
+        if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
+        mTextView.onEditorAction(actionCode);
+        return true;
+    }
+    
+    @Override
+    public boolean performContextMenuAction(int id) {
+        if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
+        mTextView.beginBatchEdit();
+        mTextView.onTextContextMenuItem(id);
+        mTextView.endBatchEdit();
+        return true;
+    }
+    
+    @Override
+    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+        if (mTextView != null) {
+            ExtractedText et = new ExtractedText();
+            if (mTextView.extractText(request, et)) {
+                if ((flags&GET_EXTRACTED_TEXT_MONITOR) != 0) {
+                    mTextView.setExtracting(request);
+                }
+                return et;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean performPrivateCommand(String action, Bundle data) {
+        mTextView.onPrivateIMECommand(action, data);
+        return true;
+    }
+
+    @Override
+    public boolean commitText(CharSequence text, int newCursorPosition) {
+        if (mTextView == null) {
+            return super.commitText(text, newCursorPosition);
+        }
+        if (text instanceof Spanned) {
+            Spanned spanned = ((Spanned) text);
+            SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
+            mIMM.registerSuggestionSpansForNotification(spans);
+        }
+
+        mTextView.resetErrorChangedFlag();
+        boolean success = super.commitText(text, newCursorPosition);
+        mTextView.hideErrorIfUnchanged();
+
+        return success;
+    }
+
+    @Override
+    public boolean requestCursorUpdates(int cursorUpdateMode) {
+        if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
+
+        // It is possible that any other bit is used as a valid flag in a future release.
+        // We should reject the entire request in such a case.
+        final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE |
+                InputConnection.CURSOR_UPDATE_MONITOR;
+        final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
+        if (unknownFlags != 0) {
+            if (DEBUG) {
+                Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." +
+                        " cursorUpdateMode=" + cursorUpdateMode +
+                        " unknownFlags=" + unknownFlags);
+            }
+            return false;
+        }
+
+        if (mIMM == null) {
+            // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled.
+            // TODO: Return some notification code rather than false to indicate method that
+            // CursorAnchorInfo is temporarily unavailable.
+            return false;
+        }
+        mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode);
+        if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+            if (mTextView == null) {
+                // In this case, FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is silently ignored.
+                // TODO: Return some notification code for the input method that indicates
+                // FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is ignored.
+            } else if (mTextView.isInLayout()) {
+                // In this case, the view hierarchy is currently undergoing a layout pass.
+                // IMM#updateCursorAnchorInfo is supposed to be called soon after the layout
+                // pass is finished.
+            } else {
+                // This will schedule a layout pass of the view tree, and the layout event
+                // eventually triggers IMM#updateCursorAnchorInfo.
+                mTextView.requestLayout();
+            }
+        }
+        return true;
+    }
+}
diff --git a/com/android/internal/widget/ExploreByTouchHelper.java b/com/android/internal/widget/ExploreByTouchHelper.java
new file mode 100644
index 0000000..50ad547
--- /dev/null
+++ b/com/android/internal/widget/ExploreByTouchHelper.java
@@ -0,0 +1,827 @@
+/*
+ * 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.IntArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+/**
+ * ExploreByTouchHelper is a utility class for implementing accessibility
+ * support in custom {@link android.view.View}s that represent a collection of View-like
+ * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and
+ * simplifies many aspects of providing information to accessibility services
+ * and managing accessibility focus. This class does not currently support
+ * hierarchies of logical items.
+ * <p>
+ * This should be applied to the parent view using
+ * {@link android.view.View#setAccessibilityDelegate}:
+ *
+ * <pre>
+ * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
+ * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
+ * </pre>
+ */
+public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
+    /** Virtual node identifier value for invalid nodes. */
+    public static final int INVALID_ID = Integer.MIN_VALUE;
+
+    /** Virtual node identifier value for the host view's node. */
+    public static final int HOST_ID = View.NO_ID;
+
+    /** Default class name used for virtual views. */
+    private static final String DEFAULT_CLASS_NAME = View.class.getName();
+
+    /** Default bounds used to determine if the client didn't set any. */
+    private static final Rect INVALID_PARENT_BOUNDS = new Rect(
+            Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
+
+    // Lazily-created temporary data structures used when creating nodes.
+    private Rect mTempScreenRect;
+    private Rect mTempParentRect;
+    private int[] mTempGlobalRect;
+
+    /** Lazily-created temporary data structure used to compute visibility. */
+    private Rect mTempVisibleRect;
+
+    /** Lazily-created temporary data structure used to obtain child IDs. */
+    private IntArray mTempArray;
+
+    /** System accessibility manager, used to check state and send events. */
+    private final AccessibilityManager mManager;
+
+    /** View whose internal structure is exposed through this helper. */
+    private final View mView;
+
+    /** Context of the host view. **/
+    private final Context mContext;
+
+    /** Node provider that handles creating nodes and performing actions. */
+    private ExploreByTouchNodeProvider mNodeProvider;
+
+    /** Virtual view id for the currently focused logical item. */
+    private int mFocusedVirtualViewId = INVALID_ID;
+
+    /** Virtual view id for the currently hovered logical item. */
+    private int mHoveredVirtualViewId = INVALID_ID;
+
+    /**
+     * Factory method to create a new {@link ExploreByTouchHelper}.
+     *
+     * @param forView View whose logical children are exposed by this helper.
+     */
+    public ExploreByTouchHelper(View forView) {
+        if (forView == null) {
+            throw new IllegalArgumentException("View may not be null");
+        }
+
+        mView = forView;
+        mContext = forView.getContext();
+        mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+    }
+
+    /**
+     * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper.
+     *
+     * @param host View whose logical children are exposed by this helper.
+     * @return The accessibility node provider for this helper.
+     */
+    @Override
+    public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+        if (mNodeProvider == null) {
+            mNodeProvider = new ExploreByTouchNodeProvider();
+        }
+        return mNodeProvider;
+    }
+
+    /**
+     * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when
+     * the Explore by Touch feature is enabled.
+     * <p>
+     * This method should be called by overriding
+     * {@link View#dispatchHoverEvent}:
+     *
+     * <pre>&#64;Override
+     * public boolean dispatchHoverEvent(MotionEvent event) {
+     *   if (mHelper.dispatchHoverEvent(this, event) {
+     *     return true;
+     *   }
+     *   return super.dispatchHoverEvent(event);
+     * }
+     * </pre>
+     *
+     * @param event The hover event to dispatch to the virtual view hierarchy.
+     * @return Whether the hover event was handled.
+     */
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) {
+            return false;
+        }
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_HOVER_MOVE:
+            case MotionEvent.ACTION_HOVER_ENTER:
+                final int virtualViewId = getVirtualViewAt(event.getX(), event.getY());
+                updateHoveredVirtualView(virtualViewId);
+                return (virtualViewId != INVALID_ID);
+            case MotionEvent.ACTION_HOVER_EXIT:
+                if (mFocusedVirtualViewId != INVALID_ID) {
+                    updateHoveredVirtualView(INVALID_ID);
+                    return true;
+                }
+                return false;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Populates an event of the specified type with information about an item
+     * and attempts to send it up through the view hierarchy.
+     * <p>
+     * You should call this method after performing a user action that normally
+     * fires an accessibility event, such as clicking on an item.
+     *
+     * <pre>public void performItemClick(T item) {
+     *   ...
+     *   sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
+     * }
+     * </pre>
+     *
+     * @param virtualViewId The virtual view id for which to send an event.
+     * @param eventType The type of event to send.
+     * @return true if the event was sent successfully.
+     */
+    public boolean sendEventForVirtualView(int virtualViewId, int eventType) {
+        if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) {
+            return false;
+        }
+
+        final ViewParent parent = mView.getParent();
+        if (parent == null) {
+            return false;
+        }
+
+        final AccessibilityEvent event = createEvent(virtualViewId, eventType);
+        return parent.requestSendAccessibilityEvent(mView, event);
+    }
+
+    /**
+     * Notifies the accessibility framework that the properties of the parent
+     * view have changed.
+     * <p>
+     * You <b>must</b> call this method after adding or removing items from the
+     * parent view.
+     */
+    public void invalidateRoot() {
+        invalidateVirtualView(HOST_ID, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+    }
+
+    /**
+     * Notifies the accessibility framework that the properties of a particular
+     * item have changed.
+     * <p>
+     * You <b>must</b> call this method after changing any of the properties set
+     * in {@link #onPopulateNodeForVirtualView}.
+     *
+     * @param virtualViewId The virtual view id to invalidate, or
+     *                      {@link #HOST_ID} to invalidate the root view.
+     * @see #invalidateVirtualView(int, int)
+     */
+    public void invalidateVirtualView(int virtualViewId) {
+        invalidateVirtualView(virtualViewId,
+                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+    }
+
+    /**
+     * Notifies the accessibility framework that the properties of a particular
+     * item have changed.
+     * <p>
+     * You <b>must</b> call this method after changing any of the properties set
+     * in {@link #onPopulateNodeForVirtualView}.
+     *
+     * @param virtualViewId The virtual view id to invalidate, or
+     *                      {@link #HOST_ID} to invalidate the root view.
+     * @param changeTypes The bit mask of change types. May be {@code 0} for the
+     *                    default (undefined) change type or one or more of:
+     *         <ul>
+     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
+     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
+     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
+     *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
+     *         </ul>
+     */
+    public void invalidateVirtualView(int virtualViewId, int changeTypes) {
+        if (virtualViewId != INVALID_ID && mManager.isEnabled()) {
+            final ViewParent parent = mView.getParent();
+            if (parent != null) {
+                final AccessibilityEvent event = createEvent(virtualViewId,
+                        AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+                event.setContentChangeTypes(changeTypes);
+                parent.requestSendAccessibilityEvent(mView, event);
+            }
+        }
+    }
+
+    /**
+     * Returns the virtual view id for the currently focused item,
+     *
+     * @return A virtual view id, or {@link #INVALID_ID} if no item is
+     *         currently focused.
+     */
+    public int getFocusedVirtualView() {
+        return mFocusedVirtualViewId;
+    }
+
+    /**
+     * Sets the currently hovered item, sending hover accessibility events as
+     * necessary to maintain the correct state.
+     *
+     * @param virtualViewId The virtual view id for the item currently being
+     *            hovered, or {@link #INVALID_ID} if no item is hovered within
+     *            the parent view.
+     */
+    private void updateHoveredVirtualView(int virtualViewId) {
+        if (mHoveredVirtualViewId == virtualViewId) {
+            return;
+        }
+
+        final int previousVirtualViewId = mHoveredVirtualViewId;
+        mHoveredVirtualViewId = virtualViewId;
+
+        // Stay consistent with framework behavior by sending ENTER/EXIT pairs
+        // in reverse order. This is accurate as of API 18.
+        sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+        sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+    }
+
+    /**
+     * Constructs and returns an {@link AccessibilityEvent} for the specified
+     * virtual view id, which includes the host view ({@link #HOST_ID}).
+     *
+     * @param virtualViewId The virtual view id for the item for which to
+     *            construct an event.
+     * @param eventType The type of event to construct.
+     * @return An {@link AccessibilityEvent} populated with information about
+     *         the specified item.
+     */
+    private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
+        switch (virtualViewId) {
+            case HOST_ID:
+                return createEventForHost(eventType);
+            default:
+                return createEventForChild(virtualViewId, eventType);
+        }
+    }
+
+    /**
+     * Constructs and returns an {@link AccessibilityEvent} for the host node.
+     *
+     * @param eventType The type of event to construct.
+     * @return An {@link AccessibilityEvent} populated with information about
+     *         the specified item.
+     */
+    private AccessibilityEvent createEventForHost(int eventType) {
+        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+        mView.onInitializeAccessibilityEvent(event);
+
+        // Allow the client to populate the event.
+        onPopulateEventForHost(event);
+
+        return event;
+    }
+
+    /**
+     * Constructs and returns an {@link AccessibilityEvent} populated with
+     * information about the specified item.
+     *
+     * @param virtualViewId The virtual view id for the item for which to
+     *            construct an event.
+     * @param eventType The type of event to construct.
+     * @return An {@link AccessibilityEvent} populated with information about
+     *         the specified item.
+     */
+    private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
+        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+        event.setEnabled(true);
+        event.setClassName(DEFAULT_CLASS_NAME);
+
+        // Allow the client to populate the event.
+        onPopulateEventForVirtualView(virtualViewId, event);
+
+        // Make sure the developer is following the rules.
+        if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
+            throw new RuntimeException("Callbacks must add text or a content description in "
+                    + "populateEventForVirtualViewId()");
+        }
+
+        // Don't allow the client to override these properties.
+        event.setPackageName(mView.getContext().getPackageName());
+        event.setSource(mView, virtualViewId);
+
+        return event;
+    }
+
+    /**
+     * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the
+     * specified virtual view id, which includes the host view
+     * ({@link #HOST_ID}).
+     *
+     * @param virtualViewId The virtual view id for the item for which to
+     *            construct a node.
+     * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information
+     *         about the specified item.
+     */
+    private AccessibilityNodeInfo createNode(int virtualViewId) {
+        switch (virtualViewId) {
+            case HOST_ID:
+                return createNodeForHost();
+            default:
+                return createNodeForChild(virtualViewId);
+        }
+    }
+
+    /**
+     * Constructs and returns an {@link AccessibilityNodeInfo} for the
+     * host view populated with its virtual descendants.
+     *
+     * @return An {@link AccessibilityNodeInfo} for the parent node.
+     */
+    private AccessibilityNodeInfo createNodeForHost() {
+        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView);
+        mView.onInitializeAccessibilityNodeInfo(node);
+        final int realNodeCount = node.getChildCount();
+
+        // Allow the client to populate the host node.
+        onPopulateNodeForHost(node);
+
+        // Add the virtual descendants.
+        if (mTempArray == null) {
+            mTempArray = new IntArray();
+        } else {
+            mTempArray.clear();
+        }
+        final IntArray virtualViewIds = mTempArray;
+        getVisibleVirtualViews(virtualViewIds);
+        if (realNodeCount > 0 && virtualViewIds.size() > 0) {
+            throw new RuntimeException("Views cannot have both real and virtual children");
+        }
+
+        final int N = virtualViewIds.size();
+        for (int i = 0; i < N; i++) {
+            node.addChild(mView, virtualViewIds.get(i));
+        }
+
+        return node;
+    }
+
+    /**
+     * Constructs and returns an {@link AccessibilityNodeInfo} for the
+     * specified item. Automatically manages accessibility focus actions.
+     * <p>
+     * Allows the implementing class to specify most node properties, but
+     * overrides the following:
+     * <ul>
+     * <li>{@link AccessibilityNodeInfo#setPackageName}
+     * <li>{@link AccessibilityNodeInfo#setClassName}
+     * <li>{@link AccessibilityNodeInfo#setParent(View)}
+     * <li>{@link AccessibilityNodeInfo#setSource(View, int)}
+     * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
+     * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)}
+     * </ul>
+     * <p>
+     * Uses the bounds of the parent view and the parent-relative bounding
+     * rectangle specified by
+     * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically
+     * update the following properties:
+     * <ul>
+     * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
+     * <li>{@link AccessibilityNodeInfo#setBoundsInParent}
+     * </ul>
+     *
+     * @param virtualViewId The virtual view id for item for which to construct
+     *            a node.
+     * @return An {@link AccessibilityNodeInfo} for the specified item.
+     */
+    private AccessibilityNodeInfo createNodeForChild(int virtualViewId) {
+        ensureTempRects();
+        final Rect tempParentRect = mTempParentRect;
+        final int[] tempGlobalRect = mTempGlobalRect;
+        final Rect tempScreenRect = mTempScreenRect;
+
+        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+
+        // Ensure the client has good defaults.
+        node.setEnabled(true);
+        node.setClassName(DEFAULT_CLASS_NAME);
+        node.setBoundsInParent(INVALID_PARENT_BOUNDS);
+
+        // Allow the client to populate the node.
+        onPopulateNodeForVirtualView(virtualViewId, node);
+
+        // Make sure the developer is following the rules.
+        if ((node.getText() == null) && (node.getContentDescription() == null)) {
+            throw new RuntimeException("Callbacks must add text or a content description in "
+                    + "populateNodeForVirtualViewId()");
+        }
+
+        node.getBoundsInParent(tempParentRect);
+        if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) {
+            throw new RuntimeException("Callbacks must set parent bounds in "
+                    + "populateNodeForVirtualViewId()");
+        }
+
+        final int actions = node.getActions();
+        if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) {
+            throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
+                    + "populateNodeForVirtualViewId()");
+        }
+        if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
+            throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
+                    + "populateNodeForVirtualViewId()");
+        }
+
+        // Don't allow the client to override these properties.
+        node.setPackageName(mView.getContext().getPackageName());
+        node.setSource(mView, virtualViewId);
+        node.setParent(mView);
+
+        // Manage internal accessibility focus state.
+        if (mFocusedVirtualViewId == virtualViewId) {
+            node.setAccessibilityFocused(true);
+            node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+        } else {
+            node.setAccessibilityFocused(false);
+            node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+        }
+
+        // Set the visibility based on the parent bound.
+        if (intersectVisibleToUser(tempParentRect)) {
+            node.setVisibleToUser(true);
+            node.setBoundsInParent(tempParentRect);
+        }
+
+        // Calculate screen-relative bound.
+        mView.getLocationOnScreen(tempGlobalRect);
+        final int offsetX = tempGlobalRect[0];
+        final int offsetY = tempGlobalRect[1];
+        tempScreenRect.set(tempParentRect);
+        tempScreenRect.offset(offsetX, offsetY);
+        node.setBoundsInScreen(tempScreenRect);
+
+        return node;
+    }
+
+    private void ensureTempRects() {
+        mTempGlobalRect = new int[2];
+        mTempParentRect = new Rect();
+        mTempScreenRect = new Rect();
+    }
+
+    private boolean performAction(int virtualViewId, int action, Bundle arguments) {
+        switch (virtualViewId) {
+            case HOST_ID:
+                return performActionForHost(action, arguments);
+            default:
+                return performActionForChild(virtualViewId, action, arguments);
+        }
+    }
+
+    private boolean performActionForHost(int action, Bundle arguments) {
+        return mView.performAccessibilityAction(action, arguments);
+    }
+
+    private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+                return manageFocusForChild(virtualViewId, action);
+            default:
+                return onPerformActionForVirtualView(virtualViewId, action, arguments);
+        }
+    }
+
+    private boolean manageFocusForChild(int virtualViewId, int action) {
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+                return requestAccessibilityFocus(virtualViewId);
+            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+                return clearAccessibilityFocus(virtualViewId);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Computes whether the specified {@link Rect} intersects with the visible
+     * portion of its parent {@link View}. Modifies {@code localRect} to contain
+     * only the visible portion.
+     *
+     * @param localRect A rectangle in local (parent) coordinates.
+     * @return Whether the specified {@link Rect} is visible on the screen.
+     */
+    private boolean intersectVisibleToUser(Rect localRect) {
+        // Missing or empty bounds mean this view is not visible.
+        if ((localRect == null) || localRect.isEmpty()) {
+            return false;
+        }
+
+        // Attached to invisible window means this view is not visible.
+        if (mView.getWindowVisibility() != View.VISIBLE) {
+            return false;
+        }
+
+        // An invisible predecessor means that this view is not visible.
+        ViewParent viewParent = mView.getParent();
+        while (viewParent instanceof View) {
+            final View view = (View) viewParent;
+            if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
+                return false;
+            }
+            viewParent = view.getParent();
+        }
+
+        // A null parent implies the view is not visible.
+        if (viewParent == null) {
+            return false;
+        }
+
+        // If no portion of the parent is visible, this view is not visible.
+        if (mTempVisibleRect == null) {
+            mTempVisibleRect = new Rect();
+        }
+        final Rect tempVisibleRect = mTempVisibleRect;
+        if (!mView.getLocalVisibleRect(tempVisibleRect)) {
+            return false;
+        }
+
+        // Check if the view intersects the visible portion of the parent.
+        return localRect.intersect(tempVisibleRect);
+    }
+
+    /**
+     * Returns whether this virtual view is accessibility focused.
+     *
+     * @return True if the view is accessibility focused.
+     */
+    private boolean isAccessibilityFocused(int virtualViewId) {
+        return (mFocusedVirtualViewId == virtualViewId);
+    }
+
+    /**
+     * Attempts to give accessibility focus to a virtual view.
+     * <p>
+     * A virtual view will not actually take focus if
+     * {@link AccessibilityManager#isEnabled()} returns false,
+     * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
+     * or the view already has accessibility focus.
+     *
+     * @param virtualViewId The id of the virtual view on which to place
+     *            accessibility focus.
+     * @return Whether this virtual view actually took accessibility focus.
+     */
+    private boolean requestAccessibilityFocus(int virtualViewId) {
+        final AccessibilityManager accessibilityManager =
+                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+        if (!mManager.isEnabled()
+                || !accessibilityManager.isTouchExplorationEnabled()) {
+            return false;
+        }
+        // TODO: Check virtual view visibility.
+        if (!isAccessibilityFocused(virtualViewId)) {
+            // Clear focus from the previously focused view, if applicable.
+            if (mFocusedVirtualViewId != INVALID_ID) {
+                sendEventForVirtualView(mFocusedVirtualViewId,
+                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+            }
+
+            // Set focus on the new view.
+            mFocusedVirtualViewId = virtualViewId;
+
+            // TODO: Only invalidate virtual view bounds.
+            mView.invalidate();
+            sendEventForVirtualView(virtualViewId,
+                    AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Attempts to clear accessibility focus from a virtual view.
+     *
+     * @param virtualViewId The id of the virtual view from which to clear
+     *            accessibility focus.
+     * @return Whether this virtual view actually cleared accessibility focus.
+     */
+    private boolean clearAccessibilityFocus(int virtualViewId) {
+        if (isAccessibilityFocused(virtualViewId)) {
+            mFocusedVirtualViewId = INVALID_ID;
+            mView.invalidate();
+            sendEventForVirtualView(virtualViewId,
+                    AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Provides a mapping between view-relative coordinates and logical
+     * items.
+     *
+     * @param x The view-relative x coordinate
+     * @param y The view-relative y coordinate
+     * @return virtual view identifier for the logical item under
+     *         coordinates (x,y)
+     */
+    protected abstract int getVirtualViewAt(float x, float y);
+
+    /**
+     * Populates a list with the view's visible items. The ordering of items
+     * within {@code virtualViewIds} specifies order of accessibility focus
+     * traversal.
+     *
+     * @param virtualViewIds The list to populate with visible items
+     */
+    protected abstract void getVisibleVirtualViews(IntArray virtualViewIds);
+
+    /**
+     * Populates an {@link AccessibilityEvent} with information about the
+     * specified item.
+     * <p>
+     * Implementations <b>must</b> populate the following required fields:
+     * <ul>
+     * <li>event text, see {@link AccessibilityEvent#getText} or
+     * {@link AccessibilityEvent#setContentDescription}
+     * </ul>
+     * <p>
+     * The helper class automatically populates the following fields with
+     * default values, but implementations may optionally override them:
+     * <ul>
+     * <li>item class name, set to android.view.View, see
+     * {@link AccessibilityEvent#setClassName}
+     * </ul>
+     * <p>
+     * The following required fields are automatically populated by the
+     * helper class and may not be overridden:
+     * <ul>
+     * <li>package name, set to the package of the host view's
+     * {@link Context}, see {@link AccessibilityEvent#setPackageName}
+     * <li>event source, set to the host view and virtual view identifier,
+     * see {@link android.view.accessibility.AccessibilityRecord#setSource(View, int)}
+     * </ul>
+     *
+     * @param virtualViewId The virtual view id for the item for which to
+     *            populate the event
+     * @param event The event to populate
+     */
+    protected abstract void onPopulateEventForVirtualView(
+            int virtualViewId, AccessibilityEvent event);
+
+    /**
+     * Populates an {@link AccessibilityEvent} with information about the host
+     * view.
+     * <p>
+     * The default implementation is a no-op.
+     *
+     * @param event the event to populate with information about the host view
+     */
+    protected void onPopulateEventForHost(AccessibilityEvent event) {
+        // Default implementation is no-op.
+    }
+
+    /**
+     * Populates an {@link AccessibilityNodeInfo} with information
+     * about the specified item.
+     * <p>
+     * Implementations <b>must</b> populate the following required fields:
+     * <ul>
+     * <li>event text, see {@link AccessibilityNodeInfo#setText} or
+     * {@link AccessibilityNodeInfo#setContentDescription}
+     * <li>bounds in parent coordinates, see
+     * {@link AccessibilityNodeInfo#setBoundsInParent}
+     * </ul>
+     * <p>
+     * The helper class automatically populates the following fields with
+     * default values, but implementations may optionally override them:
+     * <ul>
+     * <li>enabled state, set to true, see
+     * {@link AccessibilityNodeInfo#setEnabled}
+     * <li>item class name, identical to the class name set by
+     * {@link #onPopulateEventForVirtualView}, see
+     * {@link AccessibilityNodeInfo#setClassName}
+     * </ul>
+     * <p>
+     * The following required fields are automatically populated by the
+     * helper class and may not be overridden:
+     * <ul>
+     * <li>package name, identical to the package name set by
+     * {@link #onPopulateEventForVirtualView}, see
+     * {@link AccessibilityNodeInfo#setPackageName}
+     * <li>node source, identical to the event source set in
+     * {@link #onPopulateEventForVirtualView}, see
+     * {@link AccessibilityNodeInfo#setSource(View, int)}
+     * <li>parent view, set to the host view, see
+     * {@link AccessibilityNodeInfo#setParent(View)}
+     * <li>visibility, computed based on parent-relative bounds, see
+     * {@link AccessibilityNodeInfo#setVisibleToUser}
+     * <li>accessibility focus, computed based on internal helper state, see
+     * {@link AccessibilityNodeInfo#setAccessibilityFocused}
+     * <li>bounds in screen coordinates, computed based on host view bounds,
+     * see {@link AccessibilityNodeInfo#setBoundsInScreen}
+     * </ul>
+     * <p>
+     * Additionally, the helper class automatically handles accessibility
+     * focus management by adding the appropriate
+     * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or
+     * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
+     * action. Implementations must <b>never</b> manually add these actions.
+     * <p>
+     * The helper class also automatically modifies parent- and
+     * screen-relative bounds to reflect the portion of the item visible
+     * within its parent.
+     *
+     * @param virtualViewId The virtual view identifier of the item for
+     *            which to populate the node
+     * @param node The node to populate
+     */
+    protected abstract void onPopulateNodeForVirtualView(
+            int virtualViewId, AccessibilityNodeInfo node);
+
+    /**
+     * Populates an {@link AccessibilityNodeInfo} with information about the
+     * host view.
+     * <p>
+     * The default implementation is a no-op.
+     *
+     * @param node the node to populate with information about the host view
+     */
+    protected void onPopulateNodeForHost(AccessibilityNodeInfo node) {
+        // Default implementation is no-op.
+    }
+
+    /**
+     * Performs the specified accessibility action on the item associated
+     * with the virtual view identifier. See
+     * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for
+     * more information.
+     * <p>
+     * Implementations <b>must</b> handle any actions added manually in
+     * {@link #onPopulateNodeForVirtualView}.
+     * <p>
+     * The helper class automatically handles focus management resulting
+     * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS}
+     * and
+     * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
+     * actions.
+     *
+     * @param virtualViewId The virtual view identifier of the item on which
+     *            to perform the action
+     * @param action The accessibility action to perform
+     * @param arguments (Optional) A bundle with additional arguments, or
+     *            null
+     * @return true if the action was performed
+     */
+    protected abstract boolean onPerformActionForVirtualView(
+            int virtualViewId, int action, Bundle arguments);
+
+    /**
+     * Exposes a virtual view hierarchy to the accessibility framework. Only
+     * used in API 16+.
+     */
+    private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider {
+        @Override
+        public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+            return ExploreByTouchHelper.this.createNode(virtualViewId);
+        }
+
+        @Override
+        public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+            return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments);
+        }
+    }
+}
diff --git a/com/android/internal/widget/FloatingToolbar.java b/com/android/internal/widget/FloatingToolbar.java
new file mode 100644
index 0000000..f63b5a2
--- /dev/null
+++ b/com/android/internal/widget/FloatingToolbar.java
@@ -0,0 +1,1774 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Size;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A floating toolbar for showing contextual menu items.
+ * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
+ * the remaining menu items in a vertical overflow view when the overflow button is clicked.
+ * The horizontal toolbar morphs into the vertical overflow view.
+ */
+public final class FloatingToolbar {
+
+    // This class is responsible for the public API of the floating toolbar.
+    // It delegates rendering operations to the FloatingToolbarPopup.
+
+    public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
+
+    private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
+            item -> false;
+
+    private final Context mContext;
+    private final Window mWindow;
+    private final FloatingToolbarPopup mPopup;
+
+    private final Rect mContentRect = new Rect();
+    private final Rect mPreviousContentRect = new Rect();
+
+    private Menu mMenu;
+    private List<MenuItem> mShowingMenuItems = new ArrayList<>();
+    private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
+
+    private int mSuggestedWidth;
+    private boolean mWidthChanged = true;
+
+    private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
+
+        private final Rect mNewRect = new Rect();
+        private final Rect mOldRect = new Rect();
+
+        @Override
+        public void onLayoutChange(
+                View view,
+                int newLeft, int newRight, int newTop, int newBottom,
+                int oldLeft, int oldRight, int oldTop, int oldBottom) {
+            mNewRect.set(newLeft, newRight, newTop, newBottom);
+            mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
+            if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
+                mWidthChanged = true;
+                updateLayout();
+            }
+        }
+    };
+
+    /**
+     * Initializes a floating toolbar.
+     */
+    public FloatingToolbar(Window window) {
+        // TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
+        // supports multi-display.
+        mContext = applyDefaultTheme(window.getContext());
+        mWindow = Preconditions.checkNotNull(window);
+        mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
+    }
+
+    /**
+     * Sets the menu to be shown in this floating toolbar.
+     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
+     * toolbar.
+     */
+    public FloatingToolbar setMenu(Menu menu) {
+        mMenu = Preconditions.checkNotNull(menu);
+        return this;
+    }
+
+    /**
+     * Sets the custom listener for invocation of menu items in this floating toolbar.
+     */
+    public FloatingToolbar setOnMenuItemClickListener(
+            MenuItem.OnMenuItemClickListener menuItemClickListener) {
+        if (menuItemClickListener != null) {
+            mMenuItemClickListener = menuItemClickListener;
+        } else {
+            mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
+        }
+        return this;
+    }
+
+    /**
+     * Sets the content rectangle. This is the area of the interesting content that this toolbar
+     * should avoid obstructing.
+     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
+     * toolbar.
+     */
+    public FloatingToolbar setContentRect(Rect rect) {
+        mContentRect.set(Preconditions.checkNotNull(rect));
+        return this;
+    }
+
+    /**
+     * Sets the suggested width of this floating toolbar.
+     * The actual width will be about this size but there are no guarantees that it will be exactly
+     * the suggested width.
+     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
+     * toolbar.
+     */
+    public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
+        // Check if there's been a substantial width spec change.
+        int difference = Math.abs(suggestedWidth - mSuggestedWidth);
+        mWidthChanged = difference > (mSuggestedWidth * 0.2);
+
+        mSuggestedWidth = suggestedWidth;
+        return this;
+    }
+
+    /**
+     * Shows this floating toolbar.
+     */
+    public FloatingToolbar show() {
+        registerOrientationHandler();
+        doShow();
+        return this;
+    }
+
+    /**
+     * Updates this floating toolbar to reflect recent position and view updates.
+     * NOTE: This method is a no-op if the toolbar isn't showing.
+     */
+    public FloatingToolbar updateLayout() {
+        if (mPopup.isShowing()) {
+            doShow();
+        }
+        return this;
+    }
+
+    /**
+     * Dismisses this floating toolbar.
+     */
+    public void dismiss() {
+        unregisterOrientationHandler();
+        mPopup.dismiss();
+    }
+
+    /**
+     * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
+     * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
+     */
+    public void hide() {
+        mPopup.hide();
+    }
+
+    /**
+     * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
+     */
+    public boolean isShowing() {
+        return mPopup.isShowing();
+    }
+
+    /**
+     * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
+     */
+    public boolean isHidden() {
+        return mPopup.isHidden();
+    }
+
+    private void doShow() {
+        List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
+        tidy(menuItems);
+        if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
+            mPopup.dismiss();
+            mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
+            mShowingMenuItems = menuItems;
+        }
+        if (!mPopup.isShowing()) {
+            mPopup.show(mContentRect);
+        } else if (!mPreviousContentRect.equals(mContentRect)) {
+            mPopup.updateCoordinates(mContentRect);
+        }
+        mWidthChanged = false;
+        mPreviousContentRect.set(mContentRect);
+    }
+
+    /**
+     * Returns true if this floating toolbar is currently showing the specified menu items.
+     */
+    private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
+        if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
+            return false;
+        }
+
+        final int size = menuItems.size();
+        for (int i = 0; i < size; i++) {
+            final MenuItem menuItem = menuItems.get(i);
+            final MenuItem showingItem = mShowingMenuItems.get(i);
+            if (menuItem.getItemId() != showingItem.getItemId()
+                    || !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
+                    || !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
+                    || menuItem.getGroupId() != showingItem.getGroupId()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the visible and enabled menu items in the specified menu.
+     * This method is recursive.
+     */
+    private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
+        List<MenuItem> menuItems = new ArrayList<>();
+        for (int i = 0; (menu != null) && (i < menu.size()); i++) {
+            MenuItem menuItem = menu.getItem(i);
+            if (menuItem.isVisible() && menuItem.isEnabled()) {
+                Menu subMenu = menuItem.getSubMenu();
+                if (subMenu != null) {
+                    menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
+                } else {
+                    menuItems.add(menuItem);
+                }
+            }
+        }
+        return menuItems;
+    }
+
+    /**
+     * Update the list of menu items to conform to certain requirements.
+     */
+    private void tidy(List<MenuItem> menuItems) {
+        int assistItemIndex = -1;
+        Drawable assistItemDrawable = null;
+
+        final int size = menuItems.size();
+        for (int i = 0; i < size; i++) {
+            final MenuItem menuItem = menuItems.get(i);
+
+            if (menuItem.getItemId() == android.R.id.textAssist) {
+                assistItemIndex = i;
+                assistItemDrawable = menuItem.getIcon();
+            }
+
+            // Remove icons for all menu items with text.
+            if (!TextUtils.isEmpty(menuItem.getTitle())) {
+                menuItem.setIcon(null);
+            }
+        }
+        if (assistItemIndex > -1) {
+            final MenuItem assistMenuItem = menuItems.remove(assistItemIndex);
+            // Ensure the assist menu item preserves its icon.
+            assistMenuItem.setIcon(assistItemDrawable);
+            // Ensure the assist menu item is always the first item.
+            menuItems.add(0, assistMenuItem);
+        }
+    }
+
+    private void registerOrientationHandler() {
+        unregisterOrientationHandler();
+        mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
+    }
+
+    private void unregisterOrientationHandler() {
+        mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
+    }
+
+
+    /**
+     * A popup window used by the floating toolbar.
+     *
+     * This class is responsible for the rendering/animation of the floating toolbar.
+     * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
+     * to transition between panels.
+     */
+    private static final class FloatingToolbarPopup {
+
+        /* Minimum and maximum number of items allowed in the overflow. */
+        private static final int MIN_OVERFLOW_SIZE = 2;
+        private static final int MAX_OVERFLOW_SIZE = 4;
+
+        private final Context mContext;
+        private final View mParent;  // Parent for the popup window.
+        private final PopupWindow mPopupWindow;
+
+        /* Margins between the popup window and it's content. */
+        private final int mMarginHorizontal;
+        private final int mMarginVertical;
+
+        /* View components */
+        private final ViewGroup mContentContainer;  // holds all contents.
+        private final ViewGroup mMainPanel;  // holds menu items that are initially displayed.
+        private final OverflowPanel mOverflowPanel;  // holds menu items hidden in the overflow.
+        private final ImageButton mOverflowButton;  // opens/closes the overflow.
+        /* overflow button drawables. */
+        private final Drawable mArrow;
+        private final Drawable mOverflow;
+        private final AnimatedVectorDrawable mToArrow;
+        private final AnimatedVectorDrawable mToOverflow;
+
+        private final OverflowPanelViewHelper mOverflowPanelViewHelper;
+
+        /* Animation interpolators. */
+        private final Interpolator mLogAccelerateInterpolator;
+        private final Interpolator mFastOutSlowInInterpolator;
+        private final Interpolator mLinearOutSlowInInterpolator;
+        private final Interpolator mFastOutLinearInInterpolator;
+
+        /* Animations. */
+        private final AnimatorSet mShowAnimation;
+        private final AnimatorSet mDismissAnimation;
+        private final AnimatorSet mHideAnimation;
+        private final AnimationSet mOpenOverflowAnimation;
+        private final AnimationSet mCloseOverflowAnimation;
+        private final Animation.AnimationListener mOverflowAnimationListener;
+
+        private final Rect mViewPortOnScreen = new Rect();  // portion of screen we can draw in.
+        private final Point mCoordsOnWindow = new Point();  // popup window coordinates.
+        /* Temporary data holders. Reset values before using. */
+        private final int[] mTmpCoords = new int[2];
+
+        private final Region mTouchableRegion = new Region();
+        private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
+                info -> {
+                    info.contentInsets.setEmpty();
+                    info.visibleInsets.setEmpty();
+                    info.touchableRegion.set(mTouchableRegion);
+                    info.setTouchableInsets(
+                            ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+                };
+
+        private final int mLineHeight;
+        private final int mIconTextSpacing;
+
+        /**
+         * @see OverflowPanelViewHelper#preparePopupContent().
+         */
+        private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
+            @Override
+            public void run() {
+                setPanelsStatesAtRestingPosition();
+                setContentAreaAsTouchableSurface();
+                mContentContainer.setAlpha(1);
+            }
+        };
+
+        private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
+        private boolean mHidden; // tracks whether this popup is hidden or hiding.
+
+        /* Calculated sizes for panels and overflow button. */
+        private final Size mOverflowButtonSize;
+        private Size mOverflowPanelSize;  // Should be null when there is no overflow.
+        private Size mMainPanelSize;
+
+        /* Item click listeners */
+        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
+        private final View.OnClickListener mMenuItemButtonOnClickListener =
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        if (v.getTag() instanceof MenuItem) {
+                            if (mOnMenuItemClickListener != null) {
+                                mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
+                            }
+                        }
+                    }
+                };
+
+        private boolean mOpenOverflowUpwards;  // Whether the overflow opens upwards or downwards.
+        private boolean mIsOverflowOpen;
+
+        private int mTransitionDurationScale;  // Used to scale the toolbar transition duration.
+
+        /**
+         * Initializes a new floating toolbar popup.
+         *
+         * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
+         *      from.
+         */
+        public FloatingToolbarPopup(Context context, View parent) {
+            mParent = Preconditions.checkNotNull(parent);
+            mContext = Preconditions.checkNotNull(context);
+            mContentContainer = createContentContainer(context);
+            mPopupWindow = createPopupWindow(mContentContainer);
+            mMarginHorizontal = parent.getResources()
+                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
+            mMarginVertical = parent.getResources()
+                    .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
+            mLineHeight = context.getResources()
+                    .getDimensionPixelSize(R.dimen.floating_toolbar_height);
+            mIconTextSpacing = context.getResources()
+                    .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_side_padding);
+
+            // Interpolators
+            mLogAccelerateInterpolator = new LogAccelerateInterpolator();
+            mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                    mContext, android.R.interpolator.fast_out_slow_in);
+            mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                    mContext, android.R.interpolator.linear_out_slow_in);
+            mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+                    mContext, android.R.interpolator.fast_out_linear_in);
+
+            // Drawables. Needed for views.
+            mArrow = mContext.getResources()
+                    .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
+            mArrow.setAutoMirrored(true);
+            mOverflow = mContext.getResources()
+                    .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
+            mOverflow.setAutoMirrored(true);
+            mToArrow = (AnimatedVectorDrawable) mContext.getResources()
+                    .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
+            mToArrow.setAutoMirrored(true);
+            mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
+                    .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
+            mToOverflow.setAutoMirrored(true);
+
+            // Views
+            mOverflowButton = createOverflowButton();
+            mOverflowButtonSize = measure(mOverflowButton);
+            mMainPanel = createMainPanel();
+            mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext);
+            mOverflowPanel = createOverflowPanel();
+
+            // Animation. Need views.
+            mOverflowAnimationListener = createOverflowAnimationListener();
+            mOpenOverflowAnimation = new AnimationSet(true);
+            mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
+            mCloseOverflowAnimation = new AnimationSet(true);
+            mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
+            mShowAnimation = createEnterAnimation(mContentContainer);
+            mDismissAnimation = createExitAnimation(
+                    mContentContainer,
+                    150,  // startDelay
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mPopupWindow.dismiss();
+                            mContentContainer.removeAllViews();
+                        }
+                    });
+            mHideAnimation = createExitAnimation(
+                    mContentContainer,
+                    0,  // startDelay
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mPopupWindow.dismiss();
+                        }
+                    });
+        }
+
+        /**
+         * Lays out buttons for the specified menu items.
+         * Requires a subsequent call to {@link #show()} to show the items.
+         */
+        public void layoutMenuItems(
+                List<MenuItem> menuItems,
+                MenuItem.OnMenuItemClickListener menuItemClickListener,
+                int suggestedWidth) {
+            mOnMenuItemClickListener = menuItemClickListener;
+            cancelOverflowAnimations();
+            clearPanels();
+            menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
+            if (!menuItems.isEmpty()) {
+                // Add remaining items to the overflow.
+                layoutOverflowPanelItems(menuItems);
+            }
+            updatePopupSize();
+        }
+
+        /**
+         * Shows this popup at the specified coordinates.
+         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
+         */
+        public void show(Rect contentRectOnScreen) {
+            Preconditions.checkNotNull(contentRectOnScreen);
+
+            if (isShowing()) {
+                return;
+            }
+
+            mHidden = false;
+            mDismissed = false;
+            cancelDismissAndHideAnimations();
+            cancelOverflowAnimations();
+
+            refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
+            preparePopupContent();
+            // We need to specify the position in window coordinates.
+            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
+            // specify the popup position in screen coordinates.
+            mPopupWindow.showAtLocation(
+                    mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
+            setTouchableSurfaceInsetsComputer();
+            runShowAnimation();
+        }
+
+        /**
+         * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
+         */
+        public void dismiss() {
+            if (mDismissed) {
+                return;
+            }
+
+            mHidden = false;
+            mDismissed = true;
+            mHideAnimation.cancel();
+
+            runDismissAnimation();
+            setZeroTouchableSurface();
+        }
+
+        /**
+         * Hides this popup. This is a no-op if this popup is not showing.
+         * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
+         */
+        public void hide() {
+            if (!isShowing()) {
+                return;
+            }
+
+            mHidden = true;
+            runHideAnimation();
+            setZeroTouchableSurface();
+        }
+
+        /**
+         * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
+         */
+        public boolean isShowing() {
+            return !mDismissed && !mHidden;
+        }
+
+        /**
+         * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
+         */
+        public boolean isHidden() {
+            return mHidden;
+        }
+
+        /**
+         * Updates the coordinates of this popup.
+         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
+         * This is a no-op if this popup is not showing.
+         */
+        public void updateCoordinates(Rect contentRectOnScreen) {
+            Preconditions.checkNotNull(contentRectOnScreen);
+
+            if (!isShowing() || !mPopupWindow.isShowing()) {
+                return;
+            }
+
+            cancelOverflowAnimations();
+            refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
+            preparePopupContent();
+            // We need to specify the position in window coordinates.
+            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
+            // specify the popup position in screen coordinates.
+            mPopupWindow.update(
+                    mCoordsOnWindow.x, mCoordsOnWindow.y,
+                    mPopupWindow.getWidth(), mPopupWindow.getHeight());
+        }
+
+        private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
+            refreshViewPort();
+
+            // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
+            // landscape.
+            final int x = Math.min(
+                    contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
+                    mViewPortOnScreen.right - mPopupWindow.getWidth());
+
+            final int y;
+
+            final int availableHeightAboveContent =
+                    contentRectOnScreen.top - mViewPortOnScreen.top;
+            final int availableHeightBelowContent =
+                    mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
+
+            final int margin = 2 * mMarginVertical;
+            final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
+
+            if (!hasOverflow()) {
+                if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
+                    // There is enough space at the top of the content.
+                    y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
+                } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
+                    // There is enough space at the bottom of the content.
+                    y = contentRectOnScreen.bottom;
+                } else if (availableHeightBelowContent >= mLineHeight) {
+                    // Just enough space to fit the toolbar with no vertical margins.
+                    y = contentRectOnScreen.bottom - mMarginVertical;
+                } else {
+                    // Not enough space. Prefer to position as high as possible.
+                    y = Math.max(
+                            mViewPortOnScreen.top,
+                            contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
+                }
+            } else {
+                // Has an overflow.
+                final int minimumOverflowHeightWithMargin =
+                        calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
+                final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
+                        contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
+                final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
+                        mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
+
+                if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
+                    // There is enough space at the top of the content rect for the overflow.
+                    // Position above and open upwards.
+                    updateOverflowHeight(availableHeightAboveContent - margin);
+                    y = contentRectOnScreen.top - mPopupWindow.getHeight();
+                    mOpenOverflowUpwards = true;
+                } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
+                        && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
+                    // There is enough space at the top of the content rect for the main panel
+                    // but not the overflow.
+                    // Position above but open downwards.
+                    updateOverflowHeight(availableHeightThroughContentDown - margin);
+                    y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
+                    mOpenOverflowUpwards = false;
+                } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
+                    // There is enough space at the bottom of the content rect for the overflow.
+                    // Position below and open downwards.
+                    updateOverflowHeight(availableHeightBelowContent - margin);
+                    y = contentRectOnScreen.bottom;
+                    mOpenOverflowUpwards = false;
+                } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
+                        && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
+                    // There is enough space at the bottom of the content rect for the main panel
+                    // but not the overflow.
+                    // Position below but open upwards.
+                    updateOverflowHeight(availableHeightThroughContentUp - margin);
+                    y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
+                            mPopupWindow.getHeight();
+                    mOpenOverflowUpwards = true;
+                } else {
+                    // Not enough space.
+                    // Position at the top of the view port and open downwards.
+                    updateOverflowHeight(mViewPortOnScreen.height() - margin);
+                    y = mViewPortOnScreen.top;
+                    mOpenOverflowUpwards = false;
+                }
+            }
+
+            // We later specify the location of PopupWindow relative to the attached window.
+            // The idea here is that 1) we can get the location of a View in both window coordinates
+            // and screen coordiantes, where the offset between them should be equal to the window
+            // origin, and 2) we can use an arbitrary for this calculation while calculating the
+            // location of the rootview is supposed to be least expensive.
+            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid
+            // the following calculation.
+            mParent.getRootView().getLocationOnScreen(mTmpCoords);
+            int rootViewLeftOnScreen = mTmpCoords[0];
+            int rootViewTopOnScreen = mTmpCoords[1];
+            mParent.getRootView().getLocationInWindow(mTmpCoords);
+            int rootViewLeftOnWindow = mTmpCoords[0];
+            int rootViewTopOnWindow = mTmpCoords[1];
+            int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
+            int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
+            mCoordsOnWindow.set(
+                    Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
+        }
+
+        /**
+         * Performs the "show" animation on the floating popup.
+         */
+        private void runShowAnimation() {
+            mShowAnimation.start();
+        }
+
+        /**
+         * Performs the "dismiss" animation on the floating popup.
+         */
+        private void runDismissAnimation() {
+            mDismissAnimation.start();
+        }
+
+        /**
+         * Performs the "hide" animation on the floating popup.
+         */
+        private void runHideAnimation() {
+            mHideAnimation.start();
+        }
+
+        private void cancelDismissAndHideAnimations() {
+            mDismissAnimation.cancel();
+            mHideAnimation.cancel();
+        }
+
+        private void cancelOverflowAnimations() {
+            mContentContainer.clearAnimation();
+            mMainPanel.animate().cancel();
+            mOverflowPanel.animate().cancel();
+            mToArrow.stop();
+            mToOverflow.stop();
+        }
+
+        private void openOverflow() {
+            final int targetWidth = mOverflowPanelSize.getWidth();
+            final int targetHeight = mOverflowPanelSize.getHeight();
+            final int startWidth = mContentContainer.getWidth();
+            final int startHeight = mContentContainer.getHeight();
+            final float startY = mContentContainer.getY();
+            final float left = mContentContainer.getX();
+            final float right = left + mContentContainer.getWidth();
+            Animation widthAnimation = new Animation() {
+                @Override
+                protected void applyTransformation(float interpolatedTime, Transformation t) {
+                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+                    setWidth(mContentContainer, startWidth + deltaWidth);
+                    if (isInRTLMode()) {
+                        mContentContainer.setX(left);
+
+                        // Lock the panels in place.
+                        mMainPanel.setX(0);
+                        mOverflowPanel.setX(0);
+                    } else {
+                        mContentContainer.setX(right - mContentContainer.getWidth());
+
+                        // Offset the panels' positions so they look like they're locked in place
+                        // on the screen.
+                        mMainPanel.setX(mContentContainer.getWidth() - startWidth);
+                        mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
+                    }
+                }
+            };
+            Animation heightAnimation = new Animation() {
+                @Override
+                protected void applyTransformation(float interpolatedTime, Transformation t) {
+                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+                    setHeight(mContentContainer, startHeight + deltaHeight);
+                    if (mOpenOverflowUpwards) {
+                        mContentContainer.setY(
+                                startY - (mContentContainer.getHeight() - startHeight));
+                        positionContentYCoordinatesIfOpeningOverflowUpwards();
+                    }
+                }
+            };
+            final float overflowButtonStartX = mOverflowButton.getX();
+            final float overflowButtonTargetX = isInRTLMode() ?
+                    overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
+                    overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
+            Animation overflowButtonAnimation = new Animation() {
+                @Override
+                protected void applyTransformation(float interpolatedTime, Transformation t) {
+                    float overflowButtonX = overflowButtonStartX
+                            + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
+                    float deltaContainerWidth = isInRTLMode() ?
+                            0 :
+                            mContentContainer.getWidth() - startWidth;
+                    float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
+                    mOverflowButton.setX(actualOverflowButtonX);
+                }
+            };
+            widthAnimation.setInterpolator(mLogAccelerateInterpolator);
+            widthAnimation.setDuration(getAdjustedDuration(250));
+            heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
+            heightAnimation.setDuration(getAdjustedDuration(250));
+            overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
+            overflowButtonAnimation.setDuration(getAdjustedDuration(250));
+            mOpenOverflowAnimation.getAnimations().clear();
+            mOpenOverflowAnimation.getAnimations().clear();
+            mOpenOverflowAnimation.addAnimation(widthAnimation);
+            mOpenOverflowAnimation.addAnimation(heightAnimation);
+            mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
+            mContentContainer.startAnimation(mOpenOverflowAnimation);
+            mIsOverflowOpen = true;
+            mMainPanel.animate()
+                    .alpha(0).withLayer()
+                    .setInterpolator(mLinearOutSlowInInterpolator)
+                    .setDuration(250)
+                    .start();
+            mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
+        }
+
+        private void closeOverflow() {
+            final int targetWidth = mMainPanelSize.getWidth();
+            final int startWidth = mContentContainer.getWidth();
+            final float left = mContentContainer.getX();
+            final float right = left + mContentContainer.getWidth();
+            Animation widthAnimation = new Animation() {
+                @Override
+                protected void applyTransformation(float interpolatedTime, Transformation t) {
+                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+                    setWidth(mContentContainer, startWidth + deltaWidth);
+                    if (isInRTLMode()) {
+                        mContentContainer.setX(left);
+
+                        // Lock the panels in place.
+                        mMainPanel.setX(0);
+                        mOverflowPanel.setX(0);
+                    } else {
+                        mContentContainer.setX(right - mContentContainer.getWidth());
+
+                        // Offset the panels' positions so they look like they're locked in place
+                        // on the screen.
+                        mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
+                        mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
+                    }
+                }
+            };
+            final int targetHeight = mMainPanelSize.getHeight();
+            final int startHeight = mContentContainer.getHeight();
+            final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
+            Animation heightAnimation = new Animation() {
+                @Override
+                protected void applyTransformation(float interpolatedTime, Transformation t) {
+                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+                    setHeight(mContentContainer, startHeight + deltaHeight);
+                    if (mOpenOverflowUpwards) {
+                        mContentContainer.setY(bottom - mContentContainer.getHeight());
+                        positionContentYCoordinatesIfOpeningOverflowUpwards();
+                    }
+                }
+            };
+            final float overflowButtonStartX = mOverflowButton.getX();
+            final float overflowButtonTargetX = isInRTLMode() ?
+                    overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
+                    overflowButtonStartX + startWidth - mOverflowButton.getWidth();
+            Animation overflowButtonAnimation = new Animation() {
+                @Override
+                protected void applyTransformation(float interpolatedTime, Transformation t) {
+                    float overflowButtonX = overflowButtonStartX
+                            + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
+                    float deltaContainerWidth = isInRTLMode() ?
+                            0 :
+                            mContentContainer.getWidth() - startWidth;
+                    float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
+                    mOverflowButton.setX(actualOverflowButtonX);
+                }
+            };
+            widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
+            widthAnimation.setDuration(getAdjustedDuration(250));
+            heightAnimation.setInterpolator(mLogAccelerateInterpolator);
+            heightAnimation.setDuration(getAdjustedDuration(250));
+            overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
+            overflowButtonAnimation.setDuration(getAdjustedDuration(250));
+            mCloseOverflowAnimation.getAnimations().clear();
+            mCloseOverflowAnimation.addAnimation(widthAnimation);
+            mCloseOverflowAnimation.addAnimation(heightAnimation);
+            mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
+            mContentContainer.startAnimation(mCloseOverflowAnimation);
+            mIsOverflowOpen = false;
+            mMainPanel.animate()
+                    .alpha(1).withLayer()
+                    .setInterpolator(mFastOutLinearInInterpolator)
+                    .setDuration(100)
+                    .start();
+            mOverflowPanel.animate()
+                    .alpha(0).withLayer()
+                    .setInterpolator(mLinearOutSlowInInterpolator)
+                    .setDuration(150)
+                    .start();
+        }
+
+        /**
+         * Defines the position of the floating toolbar popup panels when transition animation has
+         * stopped.
+         */
+        private void setPanelsStatesAtRestingPosition() {
+            mOverflowButton.setEnabled(true);
+            mOverflowPanel.awakenScrollBars();
+
+            if (mIsOverflowOpen) {
+                // Set open state.
+                final Size containerSize = mOverflowPanelSize;
+                setSize(mContentContainer, containerSize);
+                mMainPanel.setAlpha(0);
+                mMainPanel.setVisibility(View.INVISIBLE);
+                mOverflowPanel.setAlpha(1);
+                mOverflowPanel.setVisibility(View.VISIBLE);
+                mOverflowButton.setImageDrawable(mArrow);
+                mOverflowButton.setContentDescription(mContext.getString(
+                        R.string.floating_toolbar_close_overflow_description));
+
+                // Update x-coordinates depending on RTL state.
+                if (isInRTLMode()) {
+                    mContentContainer.setX(mMarginHorizontal);  // align left
+                    mMainPanel.setX(0);  // align left
+                    mOverflowButton.setX(  // align right
+                            containerSize.getWidth() - mOverflowButtonSize.getWidth());
+                    mOverflowPanel.setX(0);  // align left
+                } else {
+                    mContentContainer.setX(  // align right
+                            mPopupWindow.getWidth() -
+                                    containerSize.getWidth() - mMarginHorizontal);
+                    mMainPanel.setX(-mContentContainer.getX());  // align right
+                    mOverflowButton.setX(0);  // align left
+                    mOverflowPanel.setX(0);  // align left
+                }
+
+                // Update y-coordinates depending on overflow's open direction.
+                if (mOpenOverflowUpwards) {
+                    mContentContainer.setY(mMarginVertical);  // align top
+                    mMainPanel.setY(  // align bottom
+                            containerSize.getHeight() - mContentContainer.getHeight());
+                    mOverflowButton.setY(  // align bottom
+                            containerSize.getHeight() - mOverflowButtonSize.getHeight());
+                    mOverflowPanel.setY(0);  // align top
+                } else {
+                    // opens downwards.
+                    mContentContainer.setY(mMarginVertical);  // align top
+                    mMainPanel.setY(0);  // align top
+                    mOverflowButton.setY(0);  // align top
+                    mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
+                }
+            } else {
+                // Overflow not open. Set closed state.
+                final Size containerSize = mMainPanelSize;
+                setSize(mContentContainer, containerSize);
+                mMainPanel.setAlpha(1);
+                mMainPanel.setVisibility(View.VISIBLE);
+                mOverflowPanel.setAlpha(0);
+                mOverflowPanel.setVisibility(View.INVISIBLE);
+                mOverflowButton.setImageDrawable(mOverflow);
+                mOverflowButton.setContentDescription(mContext.getString(
+                        R.string.floating_toolbar_open_overflow_description));
+
+                if (hasOverflow()) {
+                    // Update x-coordinates depending on RTL state.
+                    if (isInRTLMode()) {
+                        mContentContainer.setX(mMarginHorizontal);  // align left
+                        mMainPanel.setX(0);  // align left
+                        mOverflowButton.setX(0);  // align left
+                        mOverflowPanel.setX(0);  // align left
+                    } else {
+                        mContentContainer.setX(  // align right
+                                mPopupWindow.getWidth() -
+                                        containerSize.getWidth() - mMarginHorizontal);
+                        mMainPanel.setX(0);  // align left
+                        mOverflowButton.setX(  // align right
+                                containerSize.getWidth() - mOverflowButtonSize.getWidth());
+                        mOverflowPanel.setX(  // align right
+                                containerSize.getWidth() - mOverflowPanelSize.getWidth());
+                    }
+
+                    // Update y-coordinates depending on overflow's open direction.
+                    if (mOpenOverflowUpwards) {
+                        mContentContainer.setY(  // align bottom
+                                mMarginVertical +
+                                        mOverflowPanelSize.getHeight() - containerSize.getHeight());
+                        mMainPanel.setY(0);  // align top
+                        mOverflowButton.setY(0);  // align top
+                        mOverflowPanel.setY(  // align bottom
+                                containerSize.getHeight() - mOverflowPanelSize.getHeight());
+                    } else {
+                        // opens downwards.
+                        mContentContainer.setY(mMarginVertical);  // align top
+                        mMainPanel.setY(0);  // align top
+                        mOverflowButton.setY(0);  // align top
+                        mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
+                    }
+                } else {
+                    // No overflow.
+                    mContentContainer.setX(mMarginHorizontal);  // align left
+                    mContentContainer.setY(mMarginVertical);  // align top
+                    mMainPanel.setX(0);  // align left
+                    mMainPanel.setY(0);  // align top
+                }
+            }
+        }
+
+        private void updateOverflowHeight(int suggestedHeight) {
+            if (hasOverflow()) {
+                final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
+                        mLineHeight;
+                final int newHeight = calculateOverflowHeight(maxItemSize);
+                if (mOverflowPanelSize.getHeight() != newHeight) {
+                    mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
+                }
+                setSize(mOverflowPanel, mOverflowPanelSize);
+                if (mIsOverflowOpen) {
+                    setSize(mContentContainer, mOverflowPanelSize);
+                    if (mOpenOverflowUpwards) {
+                        final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
+                        mContentContainer.setY(mContentContainer.getY() + deltaHeight);
+                        mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
+                    }
+                } else {
+                    setSize(mContentContainer, mMainPanelSize);
+                }
+                updatePopupSize();
+            }
+        }
+
+        private void updatePopupSize() {
+            int width = 0;
+            int height = 0;
+            if (mMainPanelSize != null) {
+                width = Math.max(width, mMainPanelSize.getWidth());
+                height = Math.max(height, mMainPanelSize.getHeight());
+            }
+            if (mOverflowPanelSize != null) {
+                width = Math.max(width, mOverflowPanelSize.getWidth());
+                height = Math.max(height, mOverflowPanelSize.getHeight());
+            }
+            mPopupWindow.setWidth(width + mMarginHorizontal * 2);
+            mPopupWindow.setHeight(height + mMarginVertical * 2);
+            maybeComputeTransitionDurationScale();
+        }
+
+        private void refreshViewPort() {
+            mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
+        }
+
+        private int getAdjustedToolbarWidth(int suggestedWidth) {
+            int width = suggestedWidth;
+            refreshViewPort();
+            int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
+                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
+            if (width <= 0) {
+                width = mParent.getResources()
+                        .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
+            }
+            return Math.min(width, maximumWidth);
+        }
+
+        /**
+         * Sets the touchable region of this popup to be zero. This means that all touch events on
+         * this popup will go through to the surface behind it.
+         */
+        private void setZeroTouchableSurface() {
+            mTouchableRegion.setEmpty();
+        }
+
+        /**
+         * Sets the touchable region of this popup to be the area occupied by its content.
+         */
+        private void setContentAreaAsTouchableSurface() {
+            Preconditions.checkNotNull(mMainPanelSize);
+            final int width;
+            final int height;
+            if (mIsOverflowOpen) {
+                Preconditions.checkNotNull(mOverflowPanelSize);
+                width = mOverflowPanelSize.getWidth();
+                height = mOverflowPanelSize.getHeight();
+            } else {
+                width = mMainPanelSize.getWidth();
+                height = mMainPanelSize.getHeight();
+            }
+            mTouchableRegion.set(
+                    (int) mContentContainer.getX(),
+                    (int) mContentContainer.getY(),
+                    (int) mContentContainer.getX() + width,
+                    (int) mContentContainer.getY() + height);
+        }
+
+        /**
+         * Make the touchable area of this popup be the area specified by mTouchableRegion.
+         * This should be called after the popup window has been dismissed (dismiss/hide)
+         * and is probably being re-shown with a new content root view.
+         */
+        private void setTouchableSurfaceInsetsComputer() {
+            ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
+                    .getRootView()
+                    .getViewTreeObserver();
+            viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
+            viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
+        }
+
+        private boolean isInRTLMode() {
+            return mContext.getApplicationInfo().hasRtlSupport()
+                    && mContext.getResources().getConfiguration().getLayoutDirection()
+                            == View.LAYOUT_DIRECTION_RTL;
+        }
+
+        private boolean hasOverflow() {
+            return mOverflowPanelSize != null;
+        }
+
+        /**
+         * Fits as many menu items in the main panel and returns a list of the menu items that
+         * were not fit in.
+         *
+         * @return The menu items that are not included in this main panel.
+         */
+        public List<MenuItem> layoutMainPanelItems(
+                List<MenuItem> menuItems, final int toolbarWidth) {
+            Preconditions.checkNotNull(menuItems);
+
+            int availableWidth = toolbarWidth;
+
+            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
+            // add the overflow menu items to the end of the remainingMenuItems list.
+            final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
+            for (MenuItem menuItem : menuItems) {
+                if (menuItem.requiresOverflow()) {
+                    overflowMenuItems.add(menuItem);
+                } else {
+                    remainingMenuItems.add(menuItem);
+                }
+            }
+            remainingMenuItems.addAll(overflowMenuItems);
+
+            mMainPanel.removeAllViews();
+            mMainPanel.setPaddingRelative(0, 0, 0, 0);
+
+            int lastGroupId = -1;
+            boolean isFirstItem = true;
+            while (!remainingMenuItems.isEmpty()) {
+                final MenuItem menuItem = remainingMenuItems.peek();
+
+                // if this is the first item, regardless of requiresOverflow(), it should be
+                // displayed on the main panel. Otherwise all items including this one will be
+                // overflow items, and should be displayed in overflow panel.
+                if(!isFirstItem && menuItem.requiresOverflow()) {
+                    break;
+                }
+
+                View menuItemButton = createMenuItemButton(mContext, menuItem, mIconTextSpacing);
+
+                // Adding additional start padding for the first button to even out button spacing.
+                if (isFirstItem) {
+                    menuItemButton.setPaddingRelative(
+                            (int) (1.5 * menuItemButton.getPaddingStart()),
+                            menuItemButton.getPaddingTop(),
+                            menuItemButton.getPaddingEnd(),
+                            menuItemButton.getPaddingBottom());
+                }
+
+                // Adding additional end padding for the last button to even out button spacing.
+                boolean isLastItem = remainingMenuItems.size() == 1;
+                if (isLastItem) {
+                    menuItemButton.setPaddingRelative(
+                            menuItemButton.getPaddingStart(),
+                            menuItemButton.getPaddingTop(),
+                            (int) (1.5 * menuItemButton.getPaddingEnd()),
+                            menuItemButton.getPaddingBottom());
+                }
+
+                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+                final int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
+
+                final boolean isNewGroup = !isFirstItem && lastGroupId != menuItem.getGroupId();
+                final int extraPadding = isNewGroup ? menuItemButton.getPaddingEnd() * 2 : 0;
+
+                // Check if we can fit an item while reserving space for the overflowButton.
+                boolean canFitWithOverflow =
+                        menuItemButtonWidth <=
+                                availableWidth - mOverflowButtonSize.getWidth() - extraPadding;
+                boolean canFitNoOverflow =
+                        isLastItem && menuItemButtonWidth <= availableWidth - extraPadding;
+                if (canFitWithOverflow || canFitNoOverflow) {
+                    if (isNewGroup) {
+                        final View divider = createDivider(mContext);
+                        final int dividerWidth = divider.getLayoutParams().width;
+
+                        // Add extra padding to the end of the previous button.
+                        // Half of the extra padding (less borderWidth) goes to the previous button.
+                        View previousButton = mMainPanel.getChildAt(mMainPanel.getChildCount() - 1);
+                        final int prevPaddingEnd = previousButton.getPaddingEnd()
+                                + extraPadding / 2 - dividerWidth;
+                        previousButton.setPaddingRelative(
+                                previousButton.getPaddingStart(),
+                                previousButton.getPaddingTop(),
+                                prevPaddingEnd,
+                                previousButton.getPaddingBottom());
+                        final ViewGroup.LayoutParams prevParams = previousButton.getLayoutParams();
+                        prevParams.width += extraPadding / 2 - dividerWidth;
+                        previousButton.setLayoutParams(prevParams);
+
+                        // Add extra padding to the start of this button.
+                        // Other half of the extra padding goes to this button.
+                        final int paddingStart = menuItemButton.getPaddingStart()
+                                + extraPadding / 2;
+                        menuItemButton.setPaddingRelative(
+                                paddingStart,
+                                menuItemButton.getPaddingTop(),
+                                menuItemButton.getPaddingEnd(),
+                                menuItemButton.getPaddingBottom());
+
+                        // Include a divider.
+                        mMainPanel.addView(divider);
+                    }
+
+                    setButtonTagAndClickListener(menuItemButton, menuItem);
+                    // Set tooltips for main panel items, but not overflow items (b/35726766).
+                    menuItemButton.setTooltipText(menuItem.getTooltipText());
+                    mMainPanel.addView(menuItemButton);
+                    final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
+                    params.width = menuItemButtonWidth + extraPadding / 2;
+                    menuItemButton.setLayoutParams(params);
+                    availableWidth -= menuItemButtonWidth + extraPadding;
+                    remainingMenuItems.pop();
+                } else {
+                    break;
+                }
+                lastGroupId = menuItem.getGroupId();
+                isFirstItem = false;
+            }
+
+            if (!remainingMenuItems.isEmpty()) {
+                // Reserve space for overflowButton.
+                mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
+            }
+
+            mMainPanelSize = measure(mMainPanel);
+            return remainingMenuItems;
+        }
+
+        private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
+            ArrayAdapter<MenuItem> overflowPanelAdapter =
+                    (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
+            overflowPanelAdapter.clear();
+            final int size = menuItems.size();
+            for (int i = 0; i < size; i++) {
+                overflowPanelAdapter.add(menuItems.get(i));
+            }
+            mOverflowPanel.setAdapter(overflowPanelAdapter);
+            if (mOpenOverflowUpwards) {
+                mOverflowPanel.setY(0);
+            } else {
+                mOverflowPanel.setY(mOverflowButtonSize.getHeight());
+            }
+
+            int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
+            int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
+            mOverflowPanelSize = new Size(width, height);
+            setSize(mOverflowPanel, mOverflowPanelSize);
+        }
+
+        /**
+         * Resets the content container and appropriately position it's panels.
+         */
+        private void preparePopupContent() {
+            mContentContainer.removeAllViews();
+
+            // Add views in the specified order so they stack up as expected.
+            // Order: overflowPanel, mainPanel, overflowButton.
+            if (hasOverflow()) {
+                mContentContainer.addView(mOverflowPanel);
+            }
+            mContentContainer.addView(mMainPanel);
+            if (hasOverflow()) {
+                mContentContainer.addView(mOverflowButton);
+            }
+            setPanelsStatesAtRestingPosition();
+            setContentAreaAsTouchableSurface();
+
+            // The positioning of contents in RTL is wrong when the view is first rendered.
+            // Hide the view and post a runnable to recalculate positions and render the view.
+            // TODO: Investigate why this happens and fix.
+            if (isInRTLMode()) {
+                mContentContainer.setAlpha(0);
+                mContentContainer.post(mPreparePopupContentRTLHelper);
+            }
+        }
+
+        /**
+         * Clears out the panels and their container. Resets their calculated sizes.
+         */
+        private void clearPanels() {
+            mOverflowPanelSize = null;
+            mMainPanelSize = null;
+            mIsOverflowOpen = false;
+            mMainPanel.removeAllViews();
+            ArrayAdapter<MenuItem> overflowPanelAdapter =
+                    (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
+            overflowPanelAdapter.clear();
+            mOverflowPanel.setAdapter(overflowPanelAdapter);
+            mContentContainer.removeAllViews();
+        }
+
+        private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
+            if (mOpenOverflowUpwards) {
+                mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
+                mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
+                mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
+            }
+        }
+
+        private int getOverflowWidth() {
+            int overflowWidth = 0;
+            final int count = mOverflowPanel.getAdapter().getCount();
+            for (int i = 0; i < count; i++) {
+                MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
+                overflowWidth =
+                        Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
+            }
+            return overflowWidth;
+        }
+
+        private int calculateOverflowHeight(int maxItemSize) {
+            // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
+            int actualSize = Math.min(
+                    MAX_OVERFLOW_SIZE,
+                    Math.min(
+                            Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
+                            mOverflowPanel.getCount()));
+            int extension = 0;
+            if (actualSize < mOverflowPanel.getCount()) {
+                // The overflow will require scrolling to get to all the items.
+                // Extend the height so that part of the hidden items is displayed.
+                extension = (int) (mLineHeight * 0.5f);
+            }
+            return actualSize * mLineHeight
+                    + mOverflowButtonSize.getHeight()
+                    + extension;
+        }
+
+        private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
+            menuItemButton.setTag(menuItem);
+            menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
+        }
+
+        /**
+         * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
+         * animations. See comment about this in the code.
+         */
+        private int getAdjustedDuration(int originalDuration) {
+            if (mTransitionDurationScale < 150) {
+                // For smaller transition, decrease the time.
+                return Math.max(originalDuration - 50, 0);
+            } else if (mTransitionDurationScale > 300) {
+                // For bigger transition, increase the time.
+                return originalDuration + 50;
+            }
+
+            // Scale the animation duration with getDurationScale(). This allows
+            // android.view.animation.* animations to scale just like android.animation.* animations
+            // when  animator duration scale is adjusted in "Developer Options".
+            // For this reason, do not use this method for android.animation.* animations.
+            return (int) (originalDuration * ValueAnimator.getDurationScale());
+        }
+
+        private void maybeComputeTransitionDurationScale() {
+            if (mMainPanelSize != null && mOverflowPanelSize != null) {
+                int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
+                int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
+                mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
+                        mContentContainer.getContext().getResources().getDisplayMetrics().density);
+            }
+        }
+
+        private ViewGroup createMainPanel() {
+            ViewGroup mainPanel = new LinearLayout(mContext) {
+                @Override
+                protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                    if (isOverflowAnimating()) {
+                        // Update widthMeasureSpec to make sure that this view is not clipped
+                        // as we offset it's coordinates with respect to it's parent.
+                        widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                                mMainPanelSize.getWidth(),
+                                MeasureSpec.EXACTLY);
+                    }
+                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+                }
+
+                @Override
+                public boolean onInterceptTouchEvent(MotionEvent ev) {
+                    // Intercept the touch event while the overflow is animating.
+                    return isOverflowAnimating();
+                }
+            };
+            return mainPanel;
+        }
+
+        private ImageButton createOverflowButton() {
+            final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
+                    .inflate(R.layout.floating_popup_overflow_button, null);
+            overflowButton.setImageDrawable(mOverflow);
+            overflowButton.setOnClickListener(v -> {
+                if (mIsOverflowOpen) {
+                    overflowButton.setImageDrawable(mToOverflow);
+                    mToOverflow.start();
+                    closeOverflow();
+                } else {
+                    overflowButton.setImageDrawable(mToArrow);
+                    mToArrow.start();
+                    openOverflow();
+                }
+            });
+            return overflowButton;
+        }
+
+        private OverflowPanel createOverflowPanel() {
+            final OverflowPanel overflowPanel = new OverflowPanel(this);
+            overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+            overflowPanel.setDivider(null);
+            overflowPanel.setDividerHeight(0);
+
+            final ArrayAdapter adapter =
+                    new ArrayAdapter<MenuItem>(mContext, 0) {
+                        @Override
+                        public View getView(int position, View convertView, ViewGroup parent) {
+                            return mOverflowPanelViewHelper.getView(
+                                    getItem(position), mOverflowPanelSize.getWidth(), convertView);
+                        }
+                    };
+            overflowPanel.setAdapter(adapter);
+
+            overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
+                MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
+                if (mOnMenuItemClickListener != null) {
+                    mOnMenuItemClickListener.onMenuItemClick(menuItem);
+                }
+            });
+
+            return overflowPanel;
+        }
+
+        private boolean isOverflowAnimating() {
+            final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
+                    && !mOpenOverflowAnimation.hasEnded();
+            final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
+                    && !mCloseOverflowAnimation.hasEnded();
+            return overflowOpening || overflowClosing;
+        }
+
+        private Animation.AnimationListener createOverflowAnimationListener() {
+            Animation.AnimationListener listener = new Animation.AnimationListener() {
+                @Override
+                public void onAnimationStart(Animation animation) {
+                    // Disable the overflow button while it's animating.
+                    // It will be re-enabled when the animation stops.
+                    mOverflowButton.setEnabled(false);
+                    // Ensure both panels have visibility turned on when the overflow animation
+                    // starts.
+                    mMainPanel.setVisibility(View.VISIBLE);
+                    mOverflowPanel.setVisibility(View.VISIBLE);
+                }
+
+                @Override
+                public void onAnimationEnd(Animation animation) {
+                    // Posting this because it seems like this is called before the animation
+                    // actually ends.
+                    mContentContainer.post(() -> {
+                        setPanelsStatesAtRestingPosition();
+                        setContentAreaAsTouchableSurface();
+                    });
+                }
+
+                @Override
+                public void onAnimationRepeat(Animation animation) {
+                }
+            };
+            return listener;
+        }
+
+        private static Size measure(View view) {
+            Preconditions.checkState(view.getParent() == null);
+            view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+            return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
+        }
+
+        private static void setSize(View view, int width, int height) {
+            view.setMinimumWidth(width);
+            view.setMinimumHeight(height);
+            ViewGroup.LayoutParams params = view.getLayoutParams();
+            params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
+            params.width = width;
+            params.height = height;
+            view.setLayoutParams(params);
+        }
+
+        private static void setSize(View view, Size size) {
+            setSize(view, size.getWidth(), size.getHeight());
+        }
+
+        private static void setWidth(View view, int width) {
+            ViewGroup.LayoutParams params = view.getLayoutParams();
+            setSize(view, width, params.height);
+        }
+
+        private static void setHeight(View view, int height) {
+            ViewGroup.LayoutParams params = view.getLayoutParams();
+            setSize(view, params.width, height);
+        }
+
+        /**
+         * A custom ListView for the overflow panel.
+         */
+        private static final class OverflowPanel extends ListView {
+
+            private final FloatingToolbarPopup mPopup;
+
+            OverflowPanel(FloatingToolbarPopup popup) {
+                super(Preconditions.checkNotNull(popup).mContext);
+                this.mPopup = popup;
+                setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
+                setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
+            }
+
+            @Override
+            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+                // Update heightMeasureSpec to make sure that this view is not clipped
+                // as we offset it's coordinates with respect to it's parent.
+                int height = mPopup.mOverflowPanelSize.getHeight()
+                        - mPopup.mOverflowButtonSize.getHeight();
+                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            }
+
+            @Override
+            public boolean dispatchTouchEvent(MotionEvent ev) {
+                if (mPopup.isOverflowAnimating()) {
+                    // Eat the touch event.
+                    return true;
+                }
+                return super.dispatchTouchEvent(ev);
+            }
+
+            @Override
+            protected boolean awakenScrollBars() {
+                return super.awakenScrollBars();
+            }
+        }
+
+        /**
+         * A custom interpolator used for various floating toolbar animations.
+         */
+        private static final class LogAccelerateInterpolator implements Interpolator {
+
+            private static final int BASE = 100;
+            private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
+
+            private static float computeLog(float t, int base) {
+                return (float) (1 - Math.pow(base, -t));
+            }
+
+            @Override
+            public float getInterpolation(float t) {
+                return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
+            }
+        }
+
+        /**
+         * A helper for generating views for the overflow panel.
+         */
+        private static final class OverflowPanelViewHelper {
+
+            private final View mCalculator;
+            private final int mIconTextSpacing;
+            private final int mSidePadding;
+
+            private final Context mContext;
+
+            public OverflowPanelViewHelper(Context context) {
+                mContext = Preconditions.checkNotNull(context);
+                mIconTextSpacing = context.getResources()
+                        .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_side_padding);
+                mSidePadding = context.getResources()
+                        .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
+                mCalculator = createMenuButton(null);
+            }
+
+            public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
+                Preconditions.checkNotNull(menuItem);
+                if (convertView != null) {
+                    updateMenuItemButton(convertView, menuItem, mIconTextSpacing);
+                } else {
+                    convertView = createMenuButton(menuItem);
+                }
+                convertView.setMinimumWidth(minimumWidth);
+                return convertView;
+            }
+
+            public int calculateWidth(MenuItem menuItem) {
+                updateMenuItemButton(mCalculator, menuItem, mIconTextSpacing);
+                mCalculator.measure(
+                        View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+                return mCalculator.getMeasuredWidth();
+            }
+
+            private View createMenuButton(MenuItem menuItem) {
+                View button = createMenuItemButton(mContext, menuItem, mIconTextSpacing);
+                button.setPadding(mSidePadding, 0, mSidePadding, 0);
+                return button;
+            }
+        }
+    }
+
+    /**
+     * Creates and returns a menu button for the specified menu item.
+     */
+    private static View createMenuItemButton(
+            Context context, MenuItem menuItem, int iconTextSpacing) {
+        final View menuItemButton = LayoutInflater.from(context)
+                .inflate(R.layout.floating_popup_menu_button, null);
+        if (menuItem != null) {
+            updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing);
+        }
+        return menuItemButton;
+    }
+
+    /**
+     * Updates the specified menu item button with the specified menu item data.
+     */
+    private static void updateMenuItemButton(
+            View menuItemButton, MenuItem menuItem, int iconTextSpacing) {
+        final TextView buttonText = (TextView) menuItemButton.findViewById(
+                R.id.floating_toolbar_menu_item_text);
+        if (TextUtils.isEmpty(menuItem.getTitle())) {
+            buttonText.setVisibility(View.GONE);
+        } else {
+            buttonText.setVisibility(View.VISIBLE);
+            buttonText.setText(menuItem.getTitle());
+        }
+        final ImageView buttonIcon = (ImageView) menuItemButton
+                .findViewById(R.id.floating_toolbar_menu_item_image);
+        if (menuItem.getIcon() == null) {
+            buttonIcon.setVisibility(View.GONE);
+            if (buttonText != null) {
+                buttonText.setPaddingRelative(0, 0, 0, 0);
+            }
+        } else {
+            buttonIcon.setVisibility(View.VISIBLE);
+            buttonIcon.setImageDrawable(menuItem.getIcon());
+            if (buttonText != null) {
+                buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
+            }
+        }
+        final CharSequence contentDescription = menuItem.getContentDescription();
+        if (TextUtils.isEmpty(contentDescription)) {
+            menuItemButton.setContentDescription(menuItem.getTitle());
+        } else {
+            menuItemButton.setContentDescription(contentDescription);
+        }
+    }
+
+    private static ViewGroup createContentContainer(Context context) {
+        ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
+                .inflate(R.layout.floating_popup_container, null);
+        contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+        contentContainer.setTag(FLOATING_TOOLBAR_TAG);
+        return contentContainer;
+    }
+
+    private static PopupWindow createPopupWindow(ViewGroup content) {
+        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
+        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
+        // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
+        // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
+        popupWindow.setClippingEnabled(false);
+        popupWindow.setWindowLayoutType(
+                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
+        popupWindow.setAnimationStyle(0);
+        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+        content.setLayoutParams(new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+        popupContentHolder.addView(content);
+        return popupWindow;
+    }
+
+    private static View createDivider(Context context) {
+        // TODO: Inflate this instead.
+        View divider = new View(context);
+
+        int _1dp = (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
+        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+                _1dp, ViewGroup.LayoutParams.MATCH_PARENT);
+        params.setMarginsRelative(0, _1dp * 10, 0, _1dp * 10);
+        divider.setLayoutParams(params);
+
+        TypedArray a = context.obtainStyledAttributes(
+                new TypedValue().data, new int[] { R.attr.floatingToolbarDividerColor });
+        divider.setBackgroundColor(a.getColor(0, 0));
+        a.recycle();
+
+        divider.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+        divider.setEnabled(false);
+        divider.setFocusable(false);
+        divider.setContentDescription(null);
+
+        return divider;
+    }
+
+    /**
+     * Creates an "appear" animation for the specified view.
+     *
+     * @param view  The view to animate
+     */
+    private static AnimatorSet createEnterAnimation(View view) {
+        AnimatorSet animation = new AnimatorSet();
+        animation.playTogether(
+                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
+        return animation;
+    }
+
+    /**
+     * Creates a "disappear" animation for the specified view.
+     *
+     * @param view  The view to animate
+     * @param startDelay  The start delay of the animation
+     * @param listener  The animation listener
+     */
+    private static AnimatorSet createExitAnimation(
+            View view, int startDelay, Animator.AnimatorListener listener) {
+        AnimatorSet animation =  new AnimatorSet();
+        animation.playTogether(
+                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
+        animation.setStartDelay(startDelay);
+        animation.addListener(listener);
+        return animation;
+    }
+
+    /**
+     * Returns a re-themed context with controlled look and feel for views.
+     */
+    private static Context applyDefaultTheme(Context originalContext) {
+        TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
+        boolean isLightTheme = a.getBoolean(0, true);
+        int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
+        a.recycle();
+        return new ContextThemeWrapper(originalContext, themeId);
+    }
+}
diff --git a/com/android/internal/widget/GapWorker.java b/com/android/internal/widget/GapWorker.java
new file mode 100644
index 0000000..5972396
--- /dev/null
+++ b/com/android/internal/widget/GapWorker.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.os.Trace;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.TimeUnit;
+
+final class GapWorker implements Runnable {
+
+    static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
+
+    ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
+    long mPostTimeNs;
+    long mFrameIntervalNs;
+
+    static class Task {
+        public boolean immediate;
+        public int viewVelocity;
+        public int distanceToItem;
+        public RecyclerView view;
+        public int position;
+
+        public void clear() {
+            immediate = false;
+            viewVelocity = 0;
+            distanceToItem = 0;
+            view = null;
+            position = 0;
+        }
+    }
+
+    /**
+     * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects
+     * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared
+     * in between calls.
+     */
+    private ArrayList<Task> mTasks = new ArrayList<>();
+
+    /**
+     * Prefetch information associated with a specific RecyclerView.
+     */
+    static class LayoutPrefetchRegistryImpl
+            implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
+        int mPrefetchDx;
+        int mPrefetchDy;
+        int[] mPrefetchArray;
+
+        int mCount;
+
+        void setPrefetchVector(int dx, int dy) {
+            mPrefetchDx = dx;
+            mPrefetchDy = dy;
+        }
+
+        void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
+            mCount = 0;
+            if (mPrefetchArray != null) {
+                Arrays.fill(mPrefetchArray, -1);
+            }
+
+            final RecyclerView.LayoutManager layout = view.mLayout;
+            if (view.mAdapter != null
+                    && layout != null
+                    && layout.isItemPrefetchEnabled()) {
+                if (nested) {
+                    // nested prefetch, only if no adapter updates pending. Note: we don't query
+                    // view.hasPendingAdapterUpdates(), as first layout may not have occurred
+                    if (!view.mAdapterHelper.hasPendingUpdates()) {
+                        layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
+                    }
+                } else {
+                    // momentum based prefetch, only if we trust current child/adapter state
+                    if (!view.hasPendingAdapterUpdates()) {
+                        layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
+                                view.mState, this);
+                    }
+                }
+
+                if (mCount > layout.mPrefetchMaxCountObserved) {
+                    layout.mPrefetchMaxCountObserved = mCount;
+                    layout.mPrefetchMaxObservedInInitialPrefetch = nested;
+                    view.mRecycler.updateViewCacheSize();
+                }
+            }
+        }
+
+        @Override
+        public void addPosition(int layoutPosition, int pixelDistance) {
+            if (pixelDistance < 0) {
+                throw new IllegalArgumentException("Pixel distance must be non-negative");
+            }
+
+            // allocate or expand array as needed, doubling when needed
+            final int storagePosition = mCount * 2;
+            if (mPrefetchArray == null) {
+                mPrefetchArray = new int[4];
+                Arrays.fill(mPrefetchArray, -1);
+            } else if (storagePosition >= mPrefetchArray.length) {
+                final int[] oldArray = mPrefetchArray;
+                mPrefetchArray = new int[storagePosition * 2];
+                System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
+            }
+
+            // add position
+            mPrefetchArray[storagePosition] = layoutPosition;
+            mPrefetchArray[storagePosition + 1] = pixelDistance;
+
+            mCount++;
+        }
+
+        boolean lastPrefetchIncludedPosition(int position) {
+            if (mPrefetchArray != null) {
+                final int count = mCount * 2;
+                for (int i = 0; i < count; i += 2) {
+                    if (mPrefetchArray[i] == position) return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Called when prefetch indices are no longer valid for cache prioritization.
+         */
+        void clearPrefetchPositions() {
+            if (mPrefetchArray != null) {
+                Arrays.fill(mPrefetchArray, -1);
+            }
+        }
+    }
+
+    public void add(RecyclerView recyclerView) {
+        if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) {
+            throw new IllegalStateException("RecyclerView already present in worker list!");
+        }
+        mRecyclerViews.add(recyclerView);
+    }
+
+    public void remove(RecyclerView recyclerView) {
+        boolean removeSuccess = mRecyclerViews.remove(recyclerView);
+        if (RecyclerView.DEBUG && !removeSuccess) {
+            throw new IllegalStateException("RecyclerView removal failed!");
+        }
+    }
+
+    /**
+     * Schedule a prefetch immediately after the current traversal.
+     */
+    void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
+        if (recyclerView.isAttachedToWindow()) {
+            if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
+                throw new IllegalStateException("attempting to post unregistered view!");
+            }
+            if (mPostTimeNs == 0) {
+                mPostTimeNs = recyclerView.getNanoTime();
+                recyclerView.post(this);
+            }
+        }
+
+        recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
+    }
+
+    static Comparator<Task> sTaskComparator = new Comparator<Task>() {
+        @Override
+        public int compare(Task lhs, Task rhs) {
+            // first, prioritize non-cleared tasks
+            if ((lhs.view == null) != (rhs.view == null)) {
+                return lhs.view == null ? 1 : -1;
+            }
+
+            // then prioritize immediate
+            if (lhs.immediate != rhs.immediate) {
+                return lhs.immediate ? -1 : 1;
+            }
+
+            // then prioritize _highest_ view velocity
+            int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity;
+            if (deltaViewVelocity != 0) return deltaViewVelocity;
+
+            // then prioritize _lowest_ distance to item
+            int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem;
+            if (deltaDistanceToItem != 0) return deltaDistanceToItem;
+
+            return 0;
+        }
+    };
+
+    private void buildTaskList() {
+        // Update PrefetchRegistry in each view
+        final int viewCount = mRecyclerViews.size();
+        int totalTaskCount = 0;
+        for (int i = 0; i < viewCount; i++) {
+            RecyclerView view = mRecyclerViews.get(i);
+            view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
+            totalTaskCount += view.mPrefetchRegistry.mCount;
+        }
+
+        // Populate task list from prefetch data...
+        mTasks.ensureCapacity(totalTaskCount);
+        int totalTaskIndex = 0;
+        for (int i = 0; i < viewCount; i++) {
+            RecyclerView view = mRecyclerViews.get(i);
+            LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
+            final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+                    + Math.abs(prefetchRegistry.mPrefetchDy);
+            for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
+                final Task task;
+                if (totalTaskIndex >= mTasks.size()) {
+                    task = new Task();
+                    mTasks.add(task);
+                } else {
+                    task = mTasks.get(totalTaskIndex);
+                }
+                final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
+
+                task.immediate = distanceToItem <= viewVelocity;
+                task.viewVelocity = viewVelocity;
+                task.distanceToItem = distanceToItem;
+                task.view = view;
+                task.position = prefetchRegistry.mPrefetchArray[j];
+
+                totalTaskIndex++;
+            }
+        }
+
+        // ... and priority sort
+        Collections.sort(mTasks, sTaskComparator);
+    }
+
+    static boolean isPrefetchPositionAttached(RecyclerView view, int position) {
+        final int childCount = view.mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View attachedView = view.mChildHelper.getUnfilteredChildAt(i);
+            RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView);
+            // Note: can use mPosition here because adapter doesn't have pending updates
+            if (holder.mPosition == position && !holder.isInvalid()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
+            int position, long deadlineNs) {
+        if (isPrefetchPositionAttached(view, position)) {
+            // don't attempt to prefetch attached views
+            return null;
+        }
+
+        RecyclerView.Recycler recycler = view.mRecycler;
+        RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
+                position, false, deadlineNs);
+
+        if (holder != null) {
+            if (holder.isBound()) {
+                // Only give the view a chance to go into the cache if binding succeeded
+                // Note that we must use public method, since item may need cleanup
+                recycler.recycleView(holder.itemView);
+            } else {
+                // Didn't bind, so we can't cache the view, but it will stay in the pool until
+                // next prefetch/traversal. If a View fails to bind, it means we didn't have
+                // enough time prior to the deadline (and won't for other instances of this
+                // type, during this GapWorker prefetch pass).
+                recycler.addViewHolderToRecycledViewPool(holder, false);
+            }
+        }
+        return holder;
+    }
+
+    private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView,
+            long deadlineNs) {
+        if (innerView == null) {
+            return;
+        }
+
+        if (innerView.mDataSetHasChangedAfterLayout
+                && innerView.mChildHelper.getUnfilteredChildCount() != 0) {
+            // RecyclerView has new data, but old attached views. Clear everything, so that
+            // we can prefetch without partially stale data.
+            innerView.removeAndRecycleViews();
+        }
+
+        // do nested prefetch!
+        final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
+        innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);
+
+        if (innerPrefetchRegistry.mCount != 0) {
+            try {
+                Trace.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG);
+                innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
+                for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
+                    // Note that we ignore immediate flag for inner items because
+                    // we have lower confidence they're needed next frame.
+                    final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
+                    prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs);
+                }
+            } finally {
+                Trace.endSection();
+            }
+        }
+    }
+
+    private void flushTaskWithDeadline(Task task, long deadlineNs) {
+        long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
+        RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
+                task.position, taskDeadlineNs);
+        if (holder != null && holder.mNestedRecyclerView != null) {
+            prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
+        }
+    }
+
+    private void flushTasksWithDeadline(long deadlineNs) {
+        for (int i = 0; i < mTasks.size(); i++) {
+            final Task task = mTasks.get(i);
+            if (task.view == null) {
+                break; // done with populated tasks
+            }
+            flushTaskWithDeadline(task, deadlineNs);
+            task.clear();
+        }
+    }
+
+    void prefetch(long deadlineNs) {
+        buildTaskList();
+        flushTasksWithDeadline(deadlineNs);
+    }
+
+    @Override
+    public void run() {
+        try {
+            Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
+
+            if (mRecyclerViews.isEmpty()) {
+                // abort - no work to do
+                return;
+            }
+
+            // Query last vsync so we can predict next one. Note that drawing time not yet
+            // valid in animation/input callbacks, so query it here to be safe.
+            long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
+                    mRecyclerViews.get(0).getDrawingTime());
+            if (lastFrameVsyncNs == 0) {
+                // abort - couldn't get last vsync for estimating next
+                return;
+            }
+
+            // TODO: consider rebasing deadline if frame was already dropped due to long UI work.
+            // Next frame will still wait for VSYNC, so we can still use the gap if it exists.
+            long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
+
+            prefetch(nextFrameNs);
+
+            // TODO: consider rescheduling self, if there's more work to do
+        } finally {
+            mPostTimeNs = 0;
+            Trace.endSection();
+        }
+    }
+}
diff --git a/com/android/internal/widget/ImageFloatingTextView.java b/com/android/internal/widget/ImageFloatingTextView.java
new file mode 100644
index 0000000..7870333
--- /dev/null
+++ b/com/android/internal/widget/ImageFloatingTextView.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.text.BoringLayout;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.method.TransformationMethod;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+/**
+ * A TextView that can float around an image on the end.
+ *
+ * @hide
+ */
[email protected]
+public class ImageFloatingTextView extends TextView {
+
+    /** Number of lines from the top to indent */
+    private int mIndentLines;
+
+    /** Resolved layout direction */
+    private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED;
+    private int mMaxLinesForHeight = -1;
+    private boolean mFirstMeasure = true;
+    private int mLayoutMaxLines = -1;
+    private boolean mBlockLayouts;
+
+    public ImageFloatingTextView(Context context) {
+        this(context, null);
+    }
+
+    public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
+            Layout.Alignment alignment, boolean shouldEllipsize,
+            TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
+        TransformationMethod transformationMethod = getTransformationMethod();
+        CharSequence text = getText();
+        if (transformationMethod != null) {
+            text = transformationMethod.getTransformation(text, this);
+        }
+        text = text == null ? "" : text;
+        StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
+                getPaint(), wantWidth)
+                .setAlignment(alignment)
+                .setTextDirection(getTextDirectionHeuristic())
+                .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
+                .setIncludePad(getIncludeFontPadding())
+                .setUseLineSpacingFromFallbacks(true)
+                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
+        int maxLines;
+        if (mMaxLinesForHeight > 0) {
+            maxLines = mMaxLinesForHeight;
+        } else {
+            maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
+        }
+        builder.setMaxLines(maxLines);
+        mLayoutMaxLines = maxLines;
+        if (shouldEllipsize) {
+            builder.setEllipsize(effectiveEllipsize)
+                    .setEllipsizedWidth(ellipsisWidth);
+        }
+
+        // we set the endmargin on the requested number of lines.
+        int endMargin = getContext().getResources().getDimensionPixelSize(
+                R.dimen.notification_content_picture_margin);
+        int[] margins = null;
+        if (mIndentLines > 0) {
+            margins = new int[mIndentLines + 1];
+            for (int i = 0; i < mIndentLines; i++) {
+                margins[i] = endMargin;
+            }
+        }
+        if (mResolvedDirection == LAYOUT_DIRECTION_RTL) {
+            builder.setIndents(margins, null);
+        } else {
+            builder.setIndents(null, margins);
+        }
+
+        return builder.build();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        // Lets calculate how many lines the given measurement allows us.
+        int availableHeight = height - mPaddingTop - mPaddingBottom;
+        int maxLines = availableHeight / getLineHeight();
+        maxLines = Math.max(1, maxLines);
+        if (getMaxLines() > 0) {
+            maxLines = Math.min(getMaxLines(), maxLines);
+        }
+        if (maxLines != mMaxLinesForHeight) {
+            mMaxLinesForHeight = maxLines;
+            if (getLayout() != null && mMaxLinesForHeight != mLayoutMaxLines) {
+                // Invalidate layout.
+                mBlockLayouts = true;
+                setHint(getHint());
+                mBlockLayouts = false;
+            }
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    public void requestLayout() {
+        if (!mBlockLayouts) {
+            super.requestLayout();
+        }
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) {
+            mResolvedDirection = layoutDirection;
+            if (mIndentLines > 0) {
+                // Invalidate layout.
+                setHint(getHint());
+            }
+        }
+    }
+
+    @RemotableViewMethod
+    public void setHasImage(boolean hasImage) {
+        setNumIndentLines(hasImage ? 2 : 0);
+    }
+
+    /**
+     * @param lines the number of lines at the top that should be indented by indentEnd
+     * @return whether a change was made
+     */
+    public boolean setNumIndentLines(int lines) {
+        if (mIndentLines != lines) {
+            mIndentLines = lines;
+            // Invalidate layout.
+            setHint(getHint());
+            return true;
+        }
+        return false;
+    }
+
+    public int getLayoutHeight() {
+        return getLayout().getHeight();
+    }
+}
diff --git a/com/android/internal/widget/LinearLayoutManager.java b/com/android/internal/widget/LinearLayoutManager.java
new file mode 100644
index 0000000..d82c746
--- /dev/null
+++ b/com/android/internal/widget/LinearLayoutManager.java
@@ -0,0 +1,2398 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import static com.android.internal.widget.RecyclerView.NO_POSITION;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.widget.RecyclerView.LayoutParams;
+import com.android.internal.widget.helper.ItemTouchHelper;
+
+import java.util.List;
+
+/**
+ * A {@link com.android.internal.widget.RecyclerView.LayoutManager} implementation which provides
+ * similar functionality to {@link android.widget.ListView}.
+ */
+public class LinearLayoutManager extends RecyclerView.LayoutManager implements
+        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
+
+    private static final String TAG = "LinearLayoutManager";
+
+    static final boolean DEBUG = false;
+
+    public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+
+    public static final int VERTICAL = OrientationHelper.VERTICAL;
+
+    public static final int INVALID_OFFSET = Integer.MIN_VALUE;
+
+
+    /**
+     * While trying to find next view to focus, LayoutManager will not try to scroll more
+     * than this factor times the total space of the list. If layout is vertical, total space is the
+     * height minus padding, if layout is horizontal, total space is the width minus padding.
+     */
+    private static final float MAX_SCROLL_FACTOR = 1 / 3f;
+
+
+    /**
+     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
+     */
+    int mOrientation;
+
+    /**
+     * Helper class that keeps temporary layout state.
+     * It does not keep state after layout is complete but we still keep a reference to re-use
+     * the same object.
+     */
+    private LayoutState mLayoutState;
+
+    /**
+     * Many calculations are made depending on orientation. To keep it clean, this interface
+     * helps {@link LinearLayoutManager} make those decisions.
+     * Based on {@link #mOrientation}, an implementation is lazily created in
+     * {@link #ensureLayoutState} method.
+     */
+    OrientationHelper mOrientationHelper;
+
+    /**
+     * We need to track this so that we can ignore current position when it changes.
+     */
+    private boolean mLastStackFromEnd;
+
+
+    /**
+     * Defines if layout should be calculated from end to start.
+     *
+     * @see #mShouldReverseLayout
+     */
+    private boolean mReverseLayout = false;
+
+    /**
+     * This keeps the final value for how LayoutManager should start laying out views.
+     * It is calculated by checking {@link #getReverseLayout()} and View's layout direction.
+     * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run.
+     */
+    boolean mShouldReverseLayout = false;
+
+    /**
+     * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and
+     * it supports both orientations.
+     * see {@link android.widget.AbsListView#setStackFromBottom(boolean)}
+     */
+    private boolean mStackFromEnd = false;
+
+    /**
+     * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
+     * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
+     */
+    private boolean mSmoothScrollbarEnabled = true;
+
+    /**
+     * When LayoutManager needs to scroll to a position, it sets this variable and requests a
+     * layout which will check this variable and re-layout accordingly.
+     */
+    int mPendingScrollPosition = NO_POSITION;
+
+    /**
+     * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
+     * called.
+     */
+    int mPendingScrollPositionOffset = INVALID_OFFSET;
+
+    private boolean mRecycleChildrenOnDetach;
+
+    SavedState mPendingSavedState = null;
+
+    /**
+     *  Re-used variable to keep anchor information on re-layout.
+     *  Anchor position and coordinate defines the reference point for LLM while doing a layout.
+     * */
+    final AnchorInfo mAnchorInfo = new AnchorInfo();
+
+    /**
+     * Stashed to avoid allocation, currently only used in #fill()
+     */
+    private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult();
+
+    /**
+     * Number of items to prefetch when first coming on screen with new data.
+     */
+    private int mInitialItemPrefetchCount = 2;
+
+    /**
+     * Creates a vertical LinearLayoutManager
+     *
+     * @param context Current context, will be used to access resources.
+     */
+    public LinearLayoutManager(Context context) {
+        this(context, VERTICAL, false);
+    }
+
+    /**
+     * @param context       Current context, will be used to access resources.
+     * @param orientation   Layout orientation. Should be {@link #HORIZONTAL} or {@link
+     *                      #VERTICAL}.
+     * @param reverseLayout When set to true, layouts from end to start.
+     */
+    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
+        setOrientation(orientation);
+        setReverseLayout(reverseLayout);
+        setAutoMeasureEnabled(true);
+    }
+
+    /**
+     * Constructor used when layout manager is set in XML by RecyclerView attribute
+     * "layoutManager". Defaults to vertical orientation.
+     *
+     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation
+     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout
+     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd
+     */
+    public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
+        setOrientation(properties.orientation);
+        setReverseLayout(properties.reverseLayout);
+        setStackFromEnd(properties.stackFromEnd);
+        setAutoMeasureEnabled(true);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+    }
+
+    /**
+     * Returns whether LayoutManager will recycle its children when it is detached from
+     * RecyclerView.
+     *
+     * @return true if LayoutManager will recycle its children when it is detached from
+     * RecyclerView.
+     */
+    public boolean getRecycleChildrenOnDetach() {
+        return mRecycleChildrenOnDetach;
+    }
+
+    /**
+     * Set whether LayoutManager will recycle its children when it is detached from
+     * RecyclerView.
+     * <p>
+     * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
+     * this flag to <code>true</code> so that views will be available to other RecyclerViews
+     * immediately.
+     * <p>
+     * Note that, setting this flag will result in a performance drop if RecyclerView
+     * is restored.
+     *
+     * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
+     */
+    public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+        mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+    }
+
+    @Override
+    public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+        super.onDetachedFromWindow(view, recycler);
+        if (mRecycleChildrenOnDetach) {
+            removeAndRecycleAllViews(recycler);
+            recycler.clear();
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (getChildCount() > 0) {
+            event.setFromIndex(findFirstVisibleItemPosition());
+            event.setToIndex(findLastVisibleItemPosition());
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        if (mPendingSavedState != null) {
+            return new SavedState(mPendingSavedState);
+        }
+        SavedState state = new SavedState();
+        if (getChildCount() > 0) {
+            ensureLayoutState();
+            boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
+            state.mAnchorLayoutFromEnd = didLayoutFromEnd;
+            if (didLayoutFromEnd) {
+                final View refChild = getChildClosestToEnd();
+                state.mAnchorOffset = mOrientationHelper.getEndAfterPadding()
+                        - mOrientationHelper.getDecoratedEnd(refChild);
+                state.mAnchorPosition = getPosition(refChild);
+            } else {
+                final View refChild = getChildClosestToStart();
+                state.mAnchorPosition = getPosition(refChild);
+                state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild)
+                        - mOrientationHelper.getStartAfterPadding();
+            }
+        } else {
+            state.invalidateAnchor();
+        }
+        return state;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            mPendingSavedState = (SavedState) state;
+            requestLayout();
+            if (DEBUG) {
+                Log.d(TAG, "loaded saved state");
+            }
+        } else if (DEBUG) {
+            Log.d(TAG, "invalid saved state class");
+        }
+    }
+
+    /**
+     * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
+     */
+    @Override
+    public boolean canScrollHorizontally() {
+        return mOrientation == HORIZONTAL;
+    }
+
+    /**
+     * @return true if {@link #getOrientation()} is {@link #VERTICAL}
+     */
+    @Override
+    public boolean canScrollVertically() {
+        return mOrientation == VERTICAL;
+    }
+
+    /**
+     * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)}
+     */
+    public void setStackFromEnd(boolean stackFromEnd) {
+        assertNotInLayoutOrScroll(null);
+        if (mStackFromEnd == stackFromEnd) {
+            return;
+        }
+        mStackFromEnd = stackFromEnd;
+        requestLayout();
+    }
+
+    public boolean getStackFromEnd() {
+        return mStackFromEnd;
+    }
+
+    /**
+     * Returns the current orientation of the layout.
+     *
+     * @return Current orientation,  either {@link #HORIZONTAL} or {@link #VERTICAL}
+     * @see #setOrientation(int)
+     */
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    /**
+     * Sets the orientation of the layout. {@link com.android.internal.widget.LinearLayoutManager}
+     * will do its best to keep scroll position.
+     *
+     * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+     */
+    public void setOrientation(int orientation) {
+        if (orientation != HORIZONTAL && orientation != VERTICAL) {
+            throw new IllegalArgumentException("invalid orientation:" + orientation);
+        }
+        assertNotInLayoutOrScroll(null);
+        if (orientation == mOrientation) {
+            return;
+        }
+        mOrientation = orientation;
+        mOrientationHelper = null;
+        requestLayout();
+    }
+
+    /**
+     * Calculates the view layout order. (e.g. from end to start or start to end)
+     * RTL layout support is applied automatically. So if layout is RTL and
+     * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+     */
+    private void resolveShouldLayoutReverse() {
+        // A == B is the same result, but we rather keep it readable
+        if (mOrientation == VERTICAL || !isLayoutRTL()) {
+            mShouldReverseLayout = mReverseLayout;
+        } else {
+            mShouldReverseLayout = !mReverseLayout;
+        }
+    }
+
+    /**
+     * Returns if views are laid out from the opposite direction of the layout.
+     *
+     * @return If layout is reversed or not.
+     * @see #setReverseLayout(boolean)
+     */
+    public boolean getReverseLayout() {
+        return mReverseLayout;
+    }
+
+    /**
+     * Used to reverse item traversal and layout order.
+     * This behaves similar to the layout change for RTL views. When set to true, first item is
+     * laid out at the end of the UI, second item is laid out before it etc.
+     *
+     * For horizontal layouts, it depends on the layout direction.
+     * When set to true, If {@link com.android.internal.widget.RecyclerView} is LTR, than it will
+     * layout from RTL, if {@link com.android.internal.widget.RecyclerView}} is RTL, it will layout
+     * from LTR.
+     *
+     * If you are looking for the exact same behavior of
+     * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use
+     * {@link #setStackFromEnd(boolean)}
+     */
+    public void setReverseLayout(boolean reverseLayout) {
+        assertNotInLayoutOrScroll(null);
+        if (reverseLayout == mReverseLayout) {
+            return;
+        }
+        mReverseLayout = reverseLayout;
+        requestLayout();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public View findViewByPosition(int position) {
+        final int childCount = getChildCount();
+        if (childCount == 0) {
+            return null;
+        }
+        final int firstChild = getPosition(getChildAt(0));
+        final int viewPosition = position - firstChild;
+        if (viewPosition >= 0 && viewPosition < childCount) {
+            final View child = getChildAt(viewPosition);
+            if (getPosition(child) == position) {
+                return child; // in pre-layout, this may not match
+            }
+        }
+        // fallback to traversal. This might be necessary in pre-layout.
+        return super.findViewByPosition(position);
+    }
+
+    /**
+     * <p>Returns the amount of extra space that should be laid out by LayoutManager.</p>
+     *
+     * <p>By default, {@link com.android.internal.widget.LinearLayoutManager} lays out 1 extra page
+     * of items while smooth scrolling and 0 otherwise. You can override this method to implement
+     * your custom layout pre-cache logic.</p>
+     *
+     * <p><strong>Note:</strong>Laying out invisible elements generally comes with significant
+     * performance cost. It's typically only desirable in places like smooth scrolling to an unknown
+     * location, where 1) the extra content helps LinearLayoutManager know in advance when its
+     * target is approaching, so it can decelerate early and smoothly and 2) while motion is
+     * continuous.</p>
+     *
+     * <p>Extending the extra layout space is especially expensive if done while the user may change
+     * scrolling direction. Changing direction will cause the extra layout space to swap to the
+     * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large
+     * enough to handle it.</p>
+     *
+     * @return The extra space that should be laid out (in pixels).
+     */
+    protected int getExtraLayoutSpace(RecyclerView.State state) {
+        if (state.hasTargetScrollPosition()) {
+            return mOrientationHelper.getTotalSpace();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+            int position) {
+        LinearSmoothScroller linearSmoothScroller =
+                new LinearSmoothScroller(recyclerView.getContext());
+        linearSmoothScroller.setTargetPosition(position);
+        startSmoothScroll(linearSmoothScroller);
+    }
+
+    @Override
+    public PointF computeScrollVectorForPosition(int targetPosition) {
+        if (getChildCount() == 0) {
+            return null;
+        }
+        final int firstChildPos = getPosition(getChildAt(0));
+        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
+        if (mOrientation == HORIZONTAL) {
+            return new PointF(direction, 0);
+        } else {
+            return new PointF(0, direction);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        // layout algorithm:
+        // 1) by checking children and other variables, find an anchor coordinate and an anchor
+        //  item position.
+        // 2) fill towards start, stacking from bottom
+        // 3) fill towards end, stacking from top
+        // 4) scroll to fulfill requirements like stack from bottom.
+        // create layout state
+        if (DEBUG) {
+            Log.d(TAG, "is pre layout:" + state.isPreLayout());
+        }
+        if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
+            if (state.getItemCount() == 0) {
+                removeAndRecycleAllViews(recycler);
+                return;
+            }
+        }
+        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+        }
+
+        ensureLayoutState();
+        mLayoutState.mRecycle = false;
+        // resolve layout direction
+        resolveShouldLayoutReverse();
+
+        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
+                || mPendingSavedState != null) {
+            mAnchorInfo.reset();
+            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
+            // calculate anchor position and coordinate
+            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
+            mAnchorInfo.mValid = true;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Anchor info:" + mAnchorInfo);
+        }
+
+        // LLM may decide to layout items for "extra" pixels to account for scrolling target,
+        // caching or predictive animations.
+        int extraForStart;
+        int extraForEnd;
+        final int extra = getExtraLayoutSpace(state);
+        // If the previous scroll delta was less than zero, the extra space should be laid out
+        // at the start. Otherwise, it should be at the end.
+        if (mLayoutState.mLastScrollDelta >= 0) {
+            extraForEnd = extra;
+            extraForStart = 0;
+        } else {
+            extraForStart = extra;
+            extraForEnd = 0;
+        }
+        extraForStart += mOrientationHelper.getStartAfterPadding();
+        extraForEnd += mOrientationHelper.getEndPadding();
+        if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION
+                && mPendingScrollPositionOffset != INVALID_OFFSET) {
+            // if the child is visible and we are going to move it around, we should layout
+            // extra items in the opposite direction to make sure new items animate nicely
+            // instead of just fading in
+            final View existing = findViewByPosition(mPendingScrollPosition);
+            if (existing != null) {
+                final int current;
+                final int upcomingOffset;
+                if (mShouldReverseLayout) {
+                    current = mOrientationHelper.getEndAfterPadding()
+                            - mOrientationHelper.getDecoratedEnd(existing);
+                    upcomingOffset = current - mPendingScrollPositionOffset;
+                } else {
+                    current = mOrientationHelper.getDecoratedStart(existing)
+                            - mOrientationHelper.getStartAfterPadding();
+                    upcomingOffset = mPendingScrollPositionOffset - current;
+                }
+                if (upcomingOffset > 0) {
+                    extraForStart += upcomingOffset;
+                } else {
+                    extraForEnd -= upcomingOffset;
+                }
+            }
+        }
+        int startOffset;
+        int endOffset;
+        final int firstLayoutDirection;
+        if (mAnchorInfo.mLayoutFromEnd) {
+            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
+                    : LayoutState.ITEM_DIRECTION_HEAD;
+        } else {
+            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
+                    : LayoutState.ITEM_DIRECTION_TAIL;
+        }
+
+        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
+        detachAndScrapAttachedViews(recycler);
+        mLayoutState.mInfinite = resolveIsInfinite();
+        mLayoutState.mIsPreLayout = state.isPreLayout();
+        if (mAnchorInfo.mLayoutFromEnd) {
+            // fill towards start
+            updateLayoutStateToFillStart(mAnchorInfo);
+            mLayoutState.mExtra = extraForStart;
+            fill(recycler, mLayoutState, state, false);
+            startOffset = mLayoutState.mOffset;
+            final int firstElement = mLayoutState.mCurrentPosition;
+            if (mLayoutState.mAvailable > 0) {
+                extraForEnd += mLayoutState.mAvailable;
+            }
+            // fill towards end
+            updateLayoutStateToFillEnd(mAnchorInfo);
+            mLayoutState.mExtra = extraForEnd;
+            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+            fill(recycler, mLayoutState, state, false);
+            endOffset = mLayoutState.mOffset;
+
+            if (mLayoutState.mAvailable > 0) {
+                // end could not consume all. add more items towards start
+                extraForStart = mLayoutState.mAvailable;
+                updateLayoutStateToFillStart(firstElement, startOffset);
+                mLayoutState.mExtra = extraForStart;
+                fill(recycler, mLayoutState, state, false);
+                startOffset = mLayoutState.mOffset;
+            }
+        } else {
+            // fill towards end
+            updateLayoutStateToFillEnd(mAnchorInfo);
+            mLayoutState.mExtra = extraForEnd;
+            fill(recycler, mLayoutState, state, false);
+            endOffset = mLayoutState.mOffset;
+            final int lastElement = mLayoutState.mCurrentPosition;
+            if (mLayoutState.mAvailable > 0) {
+                extraForStart += mLayoutState.mAvailable;
+            }
+            // fill towards start
+            updateLayoutStateToFillStart(mAnchorInfo);
+            mLayoutState.mExtra = extraForStart;
+            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+            fill(recycler, mLayoutState, state, false);
+            startOffset = mLayoutState.mOffset;
+
+            if (mLayoutState.mAvailable > 0) {
+                extraForEnd = mLayoutState.mAvailable;
+                // start could not consume all it should. add more items towards end
+                updateLayoutStateToFillEnd(lastElement, endOffset);
+                mLayoutState.mExtra = extraForEnd;
+                fill(recycler, mLayoutState, state, false);
+                endOffset = mLayoutState.mOffset;
+            }
+        }
+
+        // changes may cause gaps on the UI, try to fix them.
+        // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
+        // changed
+        if (getChildCount() > 0) {
+            // because layout from end may be changed by scroll to position
+            // we re-calculate it.
+            // find which side we should check for gaps.
+            if (mShouldReverseLayout ^ mStackFromEnd) {
+                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
+                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
+            } else {
+                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
+                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
+            }
+        }
+        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
+        if (!state.isPreLayout()) {
+            mOrientationHelper.onLayoutComplete();
+        } else {
+            mAnchorInfo.reset();
+        }
+        mLastStackFromEnd = mStackFromEnd;
+        if (DEBUG) {
+            validateChildOrder();
+        }
+    }
+
+    @Override
+    public void onLayoutCompleted(RecyclerView.State state) {
+        super.onLayoutCompleted(state);
+        mPendingSavedState = null; // we don't need this anymore
+        mPendingScrollPosition = NO_POSITION;
+        mPendingScrollPositionOffset = INVALID_OFFSET;
+        mAnchorInfo.reset();
+    }
+
+    /**
+     * Method called when Anchor position is decided. Extending class can setup accordingly or
+     * even update anchor info if necessary.
+     * @param recycler The recycler for the layout
+     * @param state The layout state
+     * @param anchorInfo The mutable POJO that keeps the position and offset.
+     * @param firstLayoutItemDirection The direction of the first layout filling in terms of adapter
+     *                                 indices.
+     */
+    void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state,
+            AnchorInfo anchorInfo, int firstLayoutItemDirection) {
+    }
+
+    /**
+     * If necessary, layouts new items for predictive animations
+     */
+    private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
+            RecyclerView.State state, int startOffset,  int endOffset) {
+        // If there are scrap children that we did not layout, we need to find where they did go
+        // and layout them accordingly so that animations can work as expected.
+        // This case may happen if new views are added or an existing view expands and pushes
+        // another view out of bounds.
+        if (!state.willRunPredictiveAnimations() ||  getChildCount() == 0 || state.isPreLayout()
+                || !supportsPredictiveItemAnimations()) {
+            return;
+        }
+        // to make the logic simpler, we calculate the size of children and call fill.
+        int scrapExtraStart = 0, scrapExtraEnd = 0;
+        final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
+        final int scrapSize = scrapList.size();
+        final int firstChildPos = getPosition(getChildAt(0));
+        for (int i = 0; i < scrapSize; i++) {
+            RecyclerView.ViewHolder scrap = scrapList.get(i);
+            if (scrap.isRemoved()) {
+                continue;
+            }
+            final int position = scrap.getLayoutPosition();
+            final int direction = position < firstChildPos != mShouldReverseLayout
+                    ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
+            if (direction == LayoutState.LAYOUT_START) {
+                scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+            } else {
+                scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+            }
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart
+                    + " towards start and " + scrapExtraEnd + " towards end");
+        }
+        mLayoutState.mScrapList = scrapList;
+        if (scrapExtraStart > 0) {
+            View anchor = getChildClosestToStart();
+            updateLayoutStateToFillStart(getPosition(anchor), startOffset);
+            mLayoutState.mExtra = scrapExtraStart;
+            mLayoutState.mAvailable = 0;
+            mLayoutState.assignPositionFromScrapList();
+            fill(recycler, mLayoutState, state, false);
+        }
+
+        if (scrapExtraEnd > 0) {
+            View anchor = getChildClosestToEnd();
+            updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
+            mLayoutState.mExtra = scrapExtraEnd;
+            mLayoutState.mAvailable = 0;
+            mLayoutState.assignPositionFromScrapList();
+            fill(recycler, mLayoutState, state, false);
+        }
+        mLayoutState.mScrapList = null;
+    }
+
+    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
+            AnchorInfo anchorInfo) {
+        if (updateAnchorFromPendingData(state, anchorInfo)) {
+            if (DEBUG) {
+                Log.d(TAG, "updated anchor info from pending information");
+            }
+            return;
+        }
+
+        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
+            if (DEBUG) {
+                Log.d(TAG, "updated anchor info from existing children");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "deciding anchor info for fresh state");
+        }
+        anchorInfo.assignCoordinateFromPadding();
+        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
+    }
+
+    /**
+     * Finds an anchor child from existing Views. Most of the time, this is the view closest to
+     * start or end that has a valid position (e.g. not removed).
+     * <p>
+     * If a child has focus, it is given priority.
+     */
+    private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
+            RecyclerView.State state, AnchorInfo anchorInfo) {
+        if (getChildCount() == 0) {
+            return false;
+        }
+        final View focused = getFocusedChild();
+        if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
+            anchorInfo.assignFromViewAndKeepVisibleRect(focused);
+            return true;
+        }
+        if (mLastStackFromEnd != mStackFromEnd) {
+            return false;
+        }
+        View referenceChild = anchorInfo.mLayoutFromEnd
+                ? findReferenceChildClosestToEnd(recycler, state)
+                : findReferenceChildClosestToStart(recycler, state);
+        if (referenceChild != null) {
+            anchorInfo.assignFromView(referenceChild);
+            // If all visible views are removed in 1 pass, reference child might be out of bounds.
+            // If that is the case, offset it back to 0 so that we use these pre-layout children.
+            if (!state.isPreLayout() && supportsPredictiveItemAnimations()) {
+                // validate this child is at least partially visible. if not, offset it to start
+                final boolean notVisible =
+                        mOrientationHelper.getDecoratedStart(referenceChild) >= mOrientationHelper
+                                .getEndAfterPadding()
+                                || mOrientationHelper.getDecoratedEnd(referenceChild)
+                                < mOrientationHelper.getStartAfterPadding();
+                if (notVisible) {
+                    anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
+                            ? mOrientationHelper.getEndAfterPadding()
+                            : mOrientationHelper.getStartAfterPadding();
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * If there is a pending scroll position or saved states, updates the anchor info from that
+     * data and returns true
+     */
+    private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
+        if (state.isPreLayout() || mPendingScrollPosition == NO_POSITION) {
+            return false;
+        }
+        // validate scroll position
+        if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
+            mPendingScrollPosition = NO_POSITION;
+            mPendingScrollPositionOffset = INVALID_OFFSET;
+            if (DEBUG) {
+                Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition);
+            }
+            return false;
+        }
+
+        // if child is visible, try to make it a reference child and ensure it is fully visible.
+        // if child is not visible, align it depending on its virtual position.
+        anchorInfo.mPosition = mPendingScrollPosition;
+        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+            // Anchor offset depends on how that child was laid out. Here, we update it
+            // according to our current view bounds
+            anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+            if (anchorInfo.mLayoutFromEnd) {
+                anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
+                        - mPendingSavedState.mAnchorOffset;
+            } else {
+                anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
+                        + mPendingSavedState.mAnchorOffset;
+            }
+            return true;
+        }
+
+        if (mPendingScrollPositionOffset == INVALID_OFFSET) {
+            View child = findViewByPosition(mPendingScrollPosition);
+            if (child != null) {
+                final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
+                if (childSize > mOrientationHelper.getTotalSpace()) {
+                    // item does not fit. fix depending on layout direction
+                    anchorInfo.assignCoordinateFromPadding();
+                    return true;
+                }
+                final int startGap = mOrientationHelper.getDecoratedStart(child)
+                        - mOrientationHelper.getStartAfterPadding();
+                if (startGap < 0) {
+                    anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding();
+                    anchorInfo.mLayoutFromEnd = false;
+                    return true;
+                }
+                final int endGap = mOrientationHelper.getEndAfterPadding()
+                        - mOrientationHelper.getDecoratedEnd(child);
+                if (endGap < 0) {
+                    anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding();
+                    anchorInfo.mLayoutFromEnd = true;
+                    return true;
+                }
+                anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
+                        ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper
+                                .getTotalSpaceChange())
+                        : mOrientationHelper.getDecoratedStart(child);
+            } else { // item is not visible.
+                if (getChildCount() > 0) {
+                    // get position of any child, does not matter
+                    int pos = getPosition(getChildAt(0));
+                    anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos
+                            == mShouldReverseLayout;
+                }
+                anchorInfo.assignCoordinateFromPadding();
+            }
+            return true;
+        }
+        // override layout from end values for consistency
+        anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
+        // if this changes, we should update prepareForDrop as well
+        if (mShouldReverseLayout) {
+            anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
+                    - mPendingScrollPositionOffset;
+        } else {
+            anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
+                    + mPendingScrollPositionOffset;
+        }
+        return true;
+    }
+
+    /**
+     * @return The final offset amount for children
+     */
+    private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler,
+            RecyclerView.State state, boolean canOffsetChildren) {
+        int gap = mOrientationHelper.getEndAfterPadding() - endOffset;
+        int fixOffset = 0;
+        if (gap > 0) {
+            fixOffset = -scrollBy(-gap, recycler, state);
+        } else {
+            return 0; // nothing to fix
+        }
+        // move offset according to scroll amount
+        endOffset += fixOffset;
+        if (canOffsetChildren) {
+            // re-calculate gap, see if we could fix it
+            gap = mOrientationHelper.getEndAfterPadding() - endOffset;
+            if (gap > 0) {
+                mOrientationHelper.offsetChildren(gap);
+                return gap + fixOffset;
+            }
+        }
+        return fixOffset;
+    }
+
+    /**
+     * @return The final offset amount for children
+     */
+    private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler,
+            RecyclerView.State state, boolean canOffsetChildren) {
+        int gap = startOffset - mOrientationHelper.getStartAfterPadding();
+        int fixOffset = 0;
+        if (gap > 0) {
+            // check if we should fix this gap.
+            fixOffset = -scrollBy(gap, recycler, state);
+        } else {
+            return 0; // nothing to fix
+        }
+        startOffset += fixOffset;
+        if (canOffsetChildren) {
+            // re-calculate gap, see if we could fix it
+            gap = startOffset - mOrientationHelper.getStartAfterPadding();
+            if (gap > 0) {
+                mOrientationHelper.offsetChildren(-gap);
+                return fixOffset - gap;
+            }
+        }
+        return fixOffset;
+    }
+
+    private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
+        updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
+    }
+
+    private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
+        mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
+        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
+                LayoutState.ITEM_DIRECTION_TAIL;
+        mLayoutState.mCurrentPosition = itemPosition;
+        mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
+        mLayoutState.mOffset = offset;
+        mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
+    }
+
+    private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) {
+        updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
+    }
+
+    private void updateLayoutStateToFillStart(int itemPosition, int offset) {
+        mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
+        mLayoutState.mCurrentPosition = itemPosition;
+        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
+                LayoutState.ITEM_DIRECTION_HEAD;
+        mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START;
+        mLayoutState.mOffset = offset;
+        mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
+
+    }
+
+    protected boolean isLayoutRTL() {
+        return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+    }
+
+    void ensureLayoutState() {
+        if (mLayoutState == null) {
+            mLayoutState = createLayoutState();
+        }
+        if (mOrientationHelper == null) {
+            mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
+        }
+    }
+
+    /**
+     * Test overrides this to plug some tracking and verification.
+     *
+     * @return A new LayoutState
+     */
+    LayoutState createLayoutState() {
+        return new LayoutState();
+    }
+
+    /**
+     * <p>Scroll the RecyclerView to make the position visible.</p>
+     *
+     * <p>RecyclerView will scroll the minimum amount that is necessary to make the
+     * target position visible. If you are looking for a similar behavior to
+     * {@link android.widget.ListView#setSelection(int)} or
+     * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
+     * {@link #scrollToPositionWithOffset(int, int)}.</p>
+     *
+     * <p>Note that scroll position change will not be reflected until the next layout call.</p>
+     *
+     * @param position Scroll to this adapter position
+     * @see #scrollToPositionWithOffset(int, int)
+     */
+    @Override
+    public void scrollToPosition(int position) {
+        mPendingScrollPosition = position;
+        mPendingScrollPositionOffset = INVALID_OFFSET;
+        if (mPendingSavedState != null) {
+            mPendingSavedState.invalidateAnchor();
+        }
+        requestLayout();
+    }
+
+    /**
+     * Scroll to the specified adapter position with the given offset from resolved layout
+     * start. Resolved layout start depends on {@link #getReverseLayout()},
+     * {@link View#getLayoutDirection()} and {@link #getStackFromEnd()}.
+     * <p>
+     * For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling
+     * <code>scrollToPositionWithOffset(10, 20)</code> will layout such that
+     * <code>item[10]</code>'s bottom is 20 pixels above the RecyclerView's bottom.
+     * <p>
+     * Note that scroll position change will not be reflected until the next layout call.
+     * <p>
+     * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
+     *
+     * @param position Index (starting at 0) of the reference item.
+     * @param offset   The distance (in pixels) between the start edge of the item view and
+     *                 start edge of the RecyclerView.
+     * @see #setReverseLayout(boolean)
+     * @see #scrollToPosition(int)
+     */
+    public void scrollToPositionWithOffset(int position, int offset) {
+        mPendingScrollPosition = position;
+        mPendingScrollPositionOffset = offset;
+        if (mPendingSavedState != null) {
+            mPendingSavedState.invalidateAnchor();
+        }
+        requestLayout();
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        if (mOrientation == VERTICAL) {
+            return 0;
+        }
+        return scrollBy(dx, recycler, state);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        if (mOrientation == HORIZONTAL) {
+            return 0;
+        }
+        return scrollBy(dy, recycler, state);
+    }
+
+    @Override
+    public int computeHorizontalScrollOffset(RecyclerView.State state) {
+        return computeScrollOffset(state);
+    }
+
+    @Override
+    public int computeVerticalScrollOffset(RecyclerView.State state) {
+        return computeScrollOffset(state);
+    }
+
+    @Override
+    public int computeHorizontalScrollExtent(RecyclerView.State state) {
+        return computeScrollExtent(state);
+    }
+
+    @Override
+    public int computeVerticalScrollExtent(RecyclerView.State state) {
+        return computeScrollExtent(state);
+    }
+
+    @Override
+    public int computeHorizontalScrollRange(RecyclerView.State state) {
+        return computeScrollRange(state);
+    }
+
+    @Override
+    public int computeVerticalScrollRange(RecyclerView.State state) {
+        return computeScrollRange(state);
+    }
+
+    private int computeScrollOffset(RecyclerView.State state) {
+        if (getChildCount() == 0) {
+            return 0;
+        }
+        ensureLayoutState();
+        return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper,
+                findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
+                findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
+                this, mSmoothScrollbarEnabled, mShouldReverseLayout);
+    }
+
+    private int computeScrollExtent(RecyclerView.State state) {
+        if (getChildCount() == 0) {
+            return 0;
+        }
+        ensureLayoutState();
+        return ScrollbarHelper.computeScrollExtent(state, mOrientationHelper,
+                findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
+                findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
+                this,  mSmoothScrollbarEnabled);
+    }
+
+    private int computeScrollRange(RecyclerView.State state) {
+        if (getChildCount() == 0) {
+            return 0;
+        }
+        ensureLayoutState();
+        return ScrollbarHelper.computeScrollRange(state, mOrientationHelper,
+                findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
+                findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
+                this, mSmoothScrollbarEnabled);
+    }
+
+    /**
+     * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed
+     * based on the number of visible pixels in the visible items. This however assumes that all
+     * list items have similar or equal widths or heights (depending on list orientation).
+     * If you use a list in which items have different dimensions, the scrollbar will change
+     * appearance as the user scrolls through the list. To avoid this issue,  you need to disable
+     * this property.
+     *
+     * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
+     * solely on the number of items in the adapter and the position of the visible items inside
+     * the adapter. This provides a stable scrollbar as the user navigates through a list of items
+     * with varying widths / heights.
+     *
+     * @param enabled Whether or not to enable smooth scrollbar.
+     *
+     * @see #setSmoothScrollbarEnabled(boolean)
+     */
+    public void setSmoothScrollbarEnabled(boolean enabled) {
+        mSmoothScrollbarEnabled = enabled;
+    }
+
+    /**
+     * Returns the current state of the smooth scrollbar feature. It is enabled by default.
+     *
+     * @return True if smooth scrollbar is enabled, false otherwise.
+     *
+     * @see #setSmoothScrollbarEnabled(boolean)
+     */
+    public boolean isSmoothScrollbarEnabled() {
+        return mSmoothScrollbarEnabled;
+    }
+
+    private void updateLayoutState(int layoutDirection, int requiredSpace,
+            boolean canUseExistingSpace, RecyclerView.State state) {
+        // If parent provides a hint, don't measure unlimited.
+        mLayoutState.mInfinite = resolveIsInfinite();
+        mLayoutState.mExtra = getExtraLayoutSpace(state);
+        mLayoutState.mLayoutDirection = layoutDirection;
+        int scrollingOffset;
+        if (layoutDirection == LayoutState.LAYOUT_END) {
+            mLayoutState.mExtra += mOrientationHelper.getEndPadding();
+            // get the first child in the direction we are going
+            final View child = getChildClosestToEnd();
+            // the direction in which we are traversing children
+            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
+                    : LayoutState.ITEM_DIRECTION_TAIL;
+            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
+            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
+            // calculate how much we can scroll without adding new children (independent of layout)
+            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
+                    - mOrientationHelper.getEndAfterPadding();
+
+        } else {
+            final View child = getChildClosestToStart();
+            mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding();
+            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
+                    : LayoutState.ITEM_DIRECTION_HEAD;
+            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
+            mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
+            scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
+                    + mOrientationHelper.getStartAfterPadding();
+        }
+        mLayoutState.mAvailable = requiredSpace;
+        if (canUseExistingSpace) {
+            mLayoutState.mAvailable -= scrollingOffset;
+        }
+        mLayoutState.mScrollingOffset = scrollingOffset;
+    }
+
+    boolean resolveIsInfinite() {
+        return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
+                && mOrientationHelper.getEnd() == 0;
+    }
+
+    void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
+            LayoutPrefetchRegistry layoutPrefetchRegistry) {
+        final int pos = layoutState.mCurrentPosition;
+        if (pos >= 0 && pos < state.getItemCount()) {
+            layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+        }
+    }
+
+    @Override
+    public void collectInitialPrefetchPositions(int adapterItemCount,
+            LayoutPrefetchRegistry layoutPrefetchRegistry) {
+        final boolean fromEnd;
+        final int anchorPos;
+        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+            // use restored state, since it hasn't been resolved yet
+            fromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+            anchorPos = mPendingSavedState.mAnchorPosition;
+        } else {
+            resolveShouldLayoutReverse();
+            fromEnd = mShouldReverseLayout;
+            if (mPendingScrollPosition == NO_POSITION) {
+                anchorPos = fromEnd ? adapterItemCount - 1 : 0;
+            } else {
+                anchorPos = mPendingScrollPosition;
+            }
+        }
+
+        final int direction = fromEnd
+                ? LayoutState.ITEM_DIRECTION_HEAD
+                : LayoutState.ITEM_DIRECTION_TAIL;
+        int targetPos = anchorPos;
+        for (int i = 0; i < mInitialItemPrefetchCount; i++) {
+            if (targetPos >= 0 && targetPos < adapterItemCount) {
+                layoutPrefetchRegistry.addPosition(targetPos, 0);
+            } else {
+                break; // no more to prefetch
+            }
+            targetPos += direction;
+        }
+    }
+
+    /**
+     * Sets the number of items to prefetch in
+     * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines
+     * how many inner items should be prefetched when this LayoutManager's RecyclerView
+     * is nested inside another RecyclerView.
+     *
+     * <p>Set this value to the number of items this inner LayoutManager will display when it is
+     * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items
+     * so they are ready, avoiding jank as the inner RecyclerView is scrolled into the viewport.</p>
+     *
+     * <p>For example, take a vertically scrolling RecyclerView with horizontally scrolling inner
+     * RecyclerViews. The rows always have 4 items visible in them (or 5 if not aligned). Passing
+     * <code>4</code> to this method for each inner RecyclerView's LinearLayoutManager will enable
+     * RecyclerView's prefetching feature to do create/bind work for 4 views within a row early,
+     * before it is scrolled on screen, instead of just the default 2.</p>
+     *
+     * <p>Calling this method does nothing unless the LayoutManager is in a RecyclerView
+     * nested in another RecyclerView.</p>
+     *
+     * <p class="note"><strong>Note:</strong> Setting this value to be larger than the number of
+     * views that will be visible in this view can incur unnecessary bind work, and an increase to
+     * the number of Views created and in active use.</p>
+     *
+     * @param itemCount Number of items to prefetch
+     *
+     * @see #isItemPrefetchEnabled()
+     * @see #getInitialItemPrefetchCount()
+     * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+     */
+    public void setInitialPrefetchItemCount(int itemCount) {
+        mInitialItemPrefetchCount = itemCount;
+    }
+
+    /**
+     * Gets the number of items to prefetch in
+     * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines
+     * how many inner items should be prefetched when this LayoutManager's RecyclerView
+     * is nested inside another RecyclerView.
+     *
+     * @see #isItemPrefetchEnabled()
+     * @see #setInitialPrefetchItemCount(int)
+     * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+     *
+     * @return number of items to prefetch.
+     */
+    public int getInitialItemPrefetchCount() {
+        return mInitialItemPrefetchCount;
+    }
+
+    @Override
+    public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
+            LayoutPrefetchRegistry layoutPrefetchRegistry) {
+        int delta = (mOrientation == HORIZONTAL) ? dx : dy;
+        if (getChildCount() == 0 || delta == 0) {
+            // can't support this scroll, so don't bother prefetching
+            return;
+        }
+
+        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
+        final int absDy = Math.abs(delta);
+        updateLayoutState(layoutDirection, absDy, true, state);
+        collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
+    }
+
+    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
+        if (getChildCount() == 0 || dy == 0) {
+            return 0;
+        }
+        mLayoutState.mRecycle = true;
+        ensureLayoutState();
+        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
+        final int absDy = Math.abs(dy);
+        updateLayoutState(layoutDirection, absDy, true, state);
+        final int consumed = mLayoutState.mScrollingOffset
+                + fill(recycler, mLayoutState, state, false);
+        if (consumed < 0) {
+            if (DEBUG) {
+                Log.d(TAG, "Don't have any more elements to scroll");
+            }
+            return 0;
+        }
+        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
+        mOrientationHelper.offsetChildren(-scrolled);
+        if (DEBUG) {
+            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
+        }
+        mLayoutState.mLastScrollDelta = scrolled;
+        return scrolled;
+    }
+
+    @Override
+    public void assertNotInLayoutOrScroll(String message) {
+        if (mPendingSavedState == null) {
+            super.assertNotInLayoutOrScroll(message);
+        }
+    }
+
+    /**
+     * Recycles children between given indices.
+     *
+     * @param startIndex inclusive
+     * @param endIndex   exclusive
+     */
+    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
+        if (startIndex == endIndex) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
+        }
+        if (endIndex > startIndex) {
+            for (int i = endIndex - 1; i >= startIndex; i--) {
+                removeAndRecycleViewAt(i, recycler);
+            }
+        } else {
+            for (int i = startIndex; i > endIndex; i--) {
+                removeAndRecycleViewAt(i, recycler);
+            }
+        }
+    }
+
+    /**
+     * Recycles views that went out of bounds after scrolling towards the end of the layout.
+     * <p>
+     * Checks both layout position and visible position to guarantee that the view is not visible.
+     *
+     * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView}
+     * @param dt       This can be used to add additional padding to the visible area. This is used
+     *                 to detect children that will go out of bounds after scrolling, without
+     *                 actually moving them.
+     */
+    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
+        if (dt < 0) {
+            if (DEBUG) {
+                Log.d(TAG, "Called recycle from start with a negative value. This might happen"
+                        + " during layout changes but may be sign of a bug");
+            }
+            return;
+        }
+        // ignore padding, ViewGroup may not clip children.
+        final int limit = dt;
+        final int childCount = getChildCount();
+        if (mShouldReverseLayout) {
+            for (int i = childCount - 1; i >= 0; i--) {
+                View child = getChildAt(i);
+                if (mOrientationHelper.getDecoratedEnd(child) > limit
+                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
+                    // stop here
+                    recycleChildren(recycler, childCount - 1, i);
+                    return;
+                }
+            }
+        } else {
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                if (mOrientationHelper.getDecoratedEnd(child) > limit
+                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
+                    // stop here
+                    recycleChildren(recycler, 0, i);
+                    return;
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Recycles views that went out of bounds after scrolling towards the start of the layout.
+     * <p>
+     * Checks both layout position and visible position to guarantee that the view is not visible.
+     *
+     * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView}
+     * @param dt       This can be used to add additional padding to the visible area. This is used
+     *                 to detect children that will go out of bounds after scrolling, without
+     *                 actually moving them.
+     */
+    private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) {
+        final int childCount = getChildCount();
+        if (dt < 0) {
+            if (DEBUG) {
+                Log.d(TAG, "Called recycle from end with a negative value. This might happen"
+                        + " during layout changes but may be sign of a bug");
+            }
+            return;
+        }
+        final int limit = mOrientationHelper.getEnd() - dt;
+        if (mShouldReverseLayout) {
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                if (mOrientationHelper.getDecoratedStart(child) < limit
+                        || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
+                    // stop here
+                    recycleChildren(recycler, 0, i);
+                    return;
+                }
+            }
+        } else {
+            for (int i = childCount - 1; i >= 0; i--) {
+                View child = getChildAt(i);
+                if (mOrientationHelper.getDecoratedStart(child) < limit
+                        || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
+                    // stop here
+                    recycleChildren(recycler, childCount - 1, i);
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper method to call appropriate recycle method depending on current layout direction
+     *
+     * @param recycler    Current recycler that is attached to RecyclerView
+     * @param layoutState Current layout state. Right now, this object does not change but
+     *                    we may consider moving it out of this view so passing around as a
+     *                    parameter for now, rather than accessing {@link #mLayoutState}
+     * @see #recycleViewsFromStart(com.android.internal.widget.RecyclerView.Recycler, int)
+     * @see #recycleViewsFromEnd(com.android.internal.widget.RecyclerView.Recycler, int)
+     * @see com.android.internal.widget.LinearLayoutManager.LayoutState#mLayoutDirection
+     */
+    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
+        if (!layoutState.mRecycle || layoutState.mInfinite) {
+            return;
+        }
+        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
+        } else {
+            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
+        }
+    }
+
+    /**
+     * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
+     * independent from the rest of the {@link com.android.internal.widget.LinearLayoutManager}
+     * and with little change, can be made publicly available as a helper class.
+     *
+     * @param recycler        Current recycler that is attached to RecyclerView
+     * @param layoutState     Configuration on how we should fill out the available space.
+     * @param state           Context passed by the RecyclerView to control scroll steps.
+     * @param stopOnFocusable If true, filling stops in the first focusable new child
+     * @return Number of pixels that it added. Useful for scroll functions.
+     */
+    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
+            RecyclerView.State state, boolean stopOnFocusable) {
+        // max offset we should set is mFastScroll + available
+        final int start = layoutState.mAvailable;
+        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
+            // TODO ugly bug fix. should not happen
+            if (layoutState.mAvailable < 0) {
+                layoutState.mScrollingOffset += layoutState.mAvailable;
+            }
+            recycleByLayoutState(recycler, layoutState);
+        }
+        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
+        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
+        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
+            layoutChunkResult.resetInternal();
+            layoutChunk(recycler, state, layoutState, layoutChunkResult);
+            if (layoutChunkResult.mFinished) {
+                break;
+            }
+            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
+            /**
+             * Consume the available space if:
+             * * layoutChunk did not request to be ignored
+             * * OR we are laying out scrap children
+             * * OR we are not doing pre-layout
+             */
+            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
+                    || !state.isPreLayout()) {
+                layoutState.mAvailable -= layoutChunkResult.mConsumed;
+                // we keep a separate remaining space because mAvailable is important for recycling
+                remainingSpace -= layoutChunkResult.mConsumed;
+            }
+
+            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
+                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
+                if (layoutState.mAvailable < 0) {
+                    layoutState.mScrollingOffset += layoutState.mAvailable;
+                }
+                recycleByLayoutState(recycler, layoutState);
+            }
+            if (stopOnFocusable && layoutChunkResult.mFocusable) {
+                break;
+            }
+        }
+        if (DEBUG) {
+            validateChildOrder();
+        }
+        return start - layoutState.mAvailable;
+    }
+
+    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
+            LayoutState layoutState, LayoutChunkResult result) {
+        View view = layoutState.next(recycler);
+        if (view == null) {
+            if (DEBUG && layoutState.mScrapList == null) {
+                throw new RuntimeException("received null view when unexpected");
+            }
+            // if we are laying out views in scrap, this may return null which means there is
+            // no more items to layout.
+            result.mFinished = true;
+            return;
+        }
+        LayoutParams params = (LayoutParams) view.getLayoutParams();
+        if (layoutState.mScrapList == null) {
+            if (mShouldReverseLayout == (layoutState.mLayoutDirection
+                    == LayoutState.LAYOUT_START)) {
+                addView(view);
+            } else {
+                addView(view, 0);
+            }
+        } else {
+            if (mShouldReverseLayout == (layoutState.mLayoutDirection
+                    == LayoutState.LAYOUT_START)) {
+                addDisappearingView(view);
+            } else {
+                addDisappearingView(view, 0);
+            }
+        }
+        measureChildWithMargins(view, 0, 0);
+        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
+        int left, top, right, bottom;
+        if (mOrientation == VERTICAL) {
+            if (isLayoutRTL()) {
+                right = getWidth() - getPaddingRight();
+                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
+            } else {
+                left = getPaddingLeft();
+                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
+            }
+            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+                bottom = layoutState.mOffset;
+                top = layoutState.mOffset - result.mConsumed;
+            } else {
+                top = layoutState.mOffset;
+                bottom = layoutState.mOffset + result.mConsumed;
+            }
+        } else {
+            top = getPaddingTop();
+            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
+
+            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+                right = layoutState.mOffset;
+                left = layoutState.mOffset - result.mConsumed;
+            } else {
+                left = layoutState.mOffset;
+                right = layoutState.mOffset + result.mConsumed;
+            }
+        }
+        // We calculate everything with View's bounding box (which includes decor and margins)
+        // To calculate correct layout position, we subtract margins.
+        layoutDecoratedWithMargins(view, left, top, right, bottom);
+        if (DEBUG) {
+            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
+        }
+        // Consume the available space if the view is not removed OR changed
+        if (params.isItemRemoved() || params.isItemChanged()) {
+            result.mIgnoreConsumed = true;
+        }
+        result.mFocusable = view.isFocusable();
+    }
+
+    @Override
+    boolean shouldMeasureTwice() {
+        return getHeightMode() != View.MeasureSpec.EXACTLY
+                && getWidthMode() != View.MeasureSpec.EXACTLY
+                && hasFlexibleChildInBothOrientations();
+    }
+
+    /**
+     * Converts a focusDirection to orientation.
+     *
+     * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+     *                       {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+     *                       {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+     *                       or 0 for not applicable
+     * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction
+     * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise.
+     */
+    int convertFocusDirectionToLayoutDirection(int focusDirection) {
+        switch (focusDirection) {
+            case View.FOCUS_BACKWARD:
+                if (mOrientation == VERTICAL) {
+                    return LayoutState.LAYOUT_START;
+                } else if (isLayoutRTL()) {
+                    return LayoutState.LAYOUT_END;
+                } else {
+                    return LayoutState.LAYOUT_START;
+                }
+            case View.FOCUS_FORWARD:
+                if (mOrientation == VERTICAL) {
+                    return LayoutState.LAYOUT_END;
+                } else if (isLayoutRTL()) {
+                    return LayoutState.LAYOUT_START;
+                } else {
+                    return LayoutState.LAYOUT_END;
+                }
+            case View.FOCUS_UP:
+                return mOrientation == VERTICAL ? LayoutState.LAYOUT_START
+                        : LayoutState.INVALID_LAYOUT;
+            case View.FOCUS_DOWN:
+                return mOrientation == VERTICAL ? LayoutState.LAYOUT_END
+                        : LayoutState.INVALID_LAYOUT;
+            case View.FOCUS_LEFT:
+                return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START
+                        : LayoutState.INVALID_LAYOUT;
+            case View.FOCUS_RIGHT:
+                return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END
+                        : LayoutState.INVALID_LAYOUT;
+            default:
+                if (DEBUG) {
+                    Log.d(TAG, "Unknown focus request:" + focusDirection);
+                }
+                return LayoutState.INVALID_LAYOUT;
+        }
+
+    }
+
+    /**
+     * Convenience method to find the child closes to start. Caller should check it has enough
+     * children.
+     *
+     * @return The child closes to start of the layout from user's perspective.
+     */
+    private View getChildClosestToStart() {
+        return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0);
+    }
+
+    /**
+     * Convenience method to find the child closes to end. Caller should check it has enough
+     * children.
+     *
+     * @return The child closes to end of the layout from user's perspective.
+     */
+    private View getChildClosestToEnd() {
+        return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1);
+    }
+
+    /**
+     * Convenience method to find the visible child closes to start. Caller should check if it has
+     * enough children.
+     *
+     * @param completelyVisible Whether child should be completely visible or not
+     * @return The first visible child closest to start of the layout from user's perspective.
+     */
+    private View findFirstVisibleChildClosestToStart(boolean completelyVisible,
+            boolean acceptPartiallyVisible) {
+        if (mShouldReverseLayout) {
+            return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible,
+                    acceptPartiallyVisible);
+        } else {
+            return findOneVisibleChild(0, getChildCount(), completelyVisible,
+                    acceptPartiallyVisible);
+        }
+    }
+
+    /**
+     * Convenience method to find the visible child closes to end. Caller should check if it has
+     * enough children.
+     *
+     * @param completelyVisible Whether child should be completely visible or not
+     * @return The first visible child closest to end of the layout from user's perspective.
+     */
+    private View findFirstVisibleChildClosestToEnd(boolean completelyVisible,
+            boolean acceptPartiallyVisible) {
+        if (mShouldReverseLayout) {
+            return findOneVisibleChild(0, getChildCount(), completelyVisible,
+                    acceptPartiallyVisible);
+        } else {
+            return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible,
+                    acceptPartiallyVisible);
+        }
+    }
+
+
+    /**
+     * Among the children that are suitable to be considered as an anchor child, returns the one
+     * closest to the end of the layout.
+     * <p>
+     * Due to ambiguous adapter updates or children being removed, some children's positions may be
+     * invalid. This method is a best effort to find a position within adapter bounds if possible.
+     * <p>
+     * It also prioritizes children that are within the visible bounds.
+     * @return A View that can be used an an anchor View.
+     */
+    private View findReferenceChildClosestToEnd(RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return mShouldReverseLayout ? findFirstReferenceChild(recycler, state) :
+                findLastReferenceChild(recycler, state);
+    }
+
+    /**
+     * Among the children that are suitable to be considered as an anchor child, returns the one
+     * closest to the start of the layout.
+     * <p>
+     * Due to ambiguous adapter updates or children being removed, some children's positions may be
+     * invalid. This method is a best effort to find a position within adapter bounds if possible.
+     * <p>
+     * It also prioritizes children that are within the visible bounds.
+     *
+     * @return A View that can be used an an anchor View.
+     */
+    private View findReferenceChildClosestToStart(RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return mShouldReverseLayout ? findLastReferenceChild(recycler, state) :
+                findFirstReferenceChild(recycler, state);
+    }
+
+    private View findFirstReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        return findReferenceChild(recycler, state, 0, getChildCount(), state.getItemCount());
+    }
+
+    private View findLastReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        return findReferenceChild(recycler, state, getChildCount() - 1, -1, state.getItemCount());
+    }
+
+    // overridden by GridLayoutManager
+    View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
+            int start, int end, int itemCount) {
+        ensureLayoutState();
+        View invalidMatch = null;
+        View outOfBoundsMatch = null;
+        final int boundsStart = mOrientationHelper.getStartAfterPadding();
+        final int boundsEnd = mOrientationHelper.getEndAfterPadding();
+        final int diff = end > start ? 1 : -1;
+        for (int i = start; i != end; i += diff) {
+            final View view = getChildAt(i);
+            final int position = getPosition(view);
+            if (position >= 0 && position < itemCount) {
+                if (((LayoutParams) view.getLayoutParams()).isItemRemoved()) {
+                    if (invalidMatch == null) {
+                        invalidMatch = view; // removed item, least preferred
+                    }
+                } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd
+                        || mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
+                    if (outOfBoundsMatch == null) {
+                        outOfBoundsMatch = view; // item is not visible, less preferred
+                    }
+                } else {
+                    return view;
+                }
+            }
+        }
+        return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
+    }
+
+    /**
+     * Returns the adapter position of the first visible view. This position does not include
+     * adapter changes that were dispatched after the last layout pass.
+     * <p>
+     * Note that, this value is not affected by layout orientation or item order traversal.
+     * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
+     * not in the layout.
+     * <p>
+     * If RecyclerView has item decorators, they will be considered in calculations as well.
+     * <p>
+     * LayoutManager may pre-cache some views that are not necessarily visible. Those views
+     * are ignored in this method.
+     *
+     * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if
+     * there aren't any visible items.
+     * @see #findFirstCompletelyVisibleItemPosition()
+     * @see #findLastVisibleItemPosition()
+     */
+    public int findFirstVisibleItemPosition() {
+        final View child = findOneVisibleChild(0, getChildCount(), false, true);
+        return child == null ? NO_POSITION : getPosition(child);
+    }
+
+    /**
+     * Returns the adapter position of the first fully visible view. This position does not include
+     * adapter changes that were dispatched after the last layout pass.
+     * <p>
+     * Note that bounds check is only performed in the current orientation. That means, if
+     * LayoutManager is horizontal, it will only check the view's left and right edges.
+     *
+     * @return The adapter position of the first fully visible item or
+     * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
+     * @see #findFirstVisibleItemPosition()
+     * @see #findLastCompletelyVisibleItemPosition()
+     */
+    public int findFirstCompletelyVisibleItemPosition() {
+        final View child = findOneVisibleChild(0, getChildCount(), true, false);
+        return child == null ? NO_POSITION : getPosition(child);
+    }
+
+    /**
+     * Returns the adapter position of the last visible view. This position does not include
+     * adapter changes that were dispatched after the last layout pass.
+     * <p>
+     * Note that, this value is not affected by layout orientation or item order traversal.
+     * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
+     * not in the layout.
+     * <p>
+     * If RecyclerView has item decorators, they will be considered in calculations as well.
+     * <p>
+     * LayoutManager may pre-cache some views that are not necessarily visible. Those views
+     * are ignored in this method.
+     *
+     * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if
+     * there aren't any visible items.
+     * @see #findLastCompletelyVisibleItemPosition()
+     * @see #findFirstVisibleItemPosition()
+     */
+    public int findLastVisibleItemPosition() {
+        final View child = findOneVisibleChild(getChildCount() - 1, -1, false, true);
+        return child == null ? NO_POSITION : getPosition(child);
+    }
+
+    /**
+     * Returns the adapter position of the last fully visible view. This position does not include
+     * adapter changes that were dispatched after the last layout pass.
+     * <p>
+     * Note that bounds check is only performed in the current orientation. That means, if
+     * LayoutManager is horizontal, it will only check the view's left and right edges.
+     *
+     * @return The adapter position of the last fully visible view or
+     * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
+     * @see #findLastVisibleItemPosition()
+     * @see #findFirstCompletelyVisibleItemPosition()
+     */
+    public int findLastCompletelyVisibleItemPosition() {
+        final View child = findOneVisibleChild(getChildCount() - 1, -1, true, false);
+        return child == null ? NO_POSITION : getPosition(child);
+    }
+
+    View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible,
+            boolean acceptPartiallyVisible) {
+        ensureLayoutState();
+        final int start = mOrientationHelper.getStartAfterPadding();
+        final int end = mOrientationHelper.getEndAfterPadding();
+        final int next = toIndex > fromIndex ? 1 : -1;
+        View partiallyVisible = null;
+        for (int i = fromIndex; i != toIndex; i += next) {
+            final View child = getChildAt(i);
+            final int childStart = mOrientationHelper.getDecoratedStart(child);
+            final int childEnd = mOrientationHelper.getDecoratedEnd(child);
+            if (childStart < end && childEnd > start) {
+                if (completelyVisible) {
+                    if (childStart >= start && childEnd <= end) {
+                        return child;
+                    } else if (acceptPartiallyVisible && partiallyVisible == null) {
+                        partiallyVisible = child;
+                    }
+                } else {
+                    return child;
+                }
+            }
+        }
+        return partiallyVisible;
+    }
+
+    @Override
+    public View onFocusSearchFailed(View focused, int focusDirection,
+            RecyclerView.Recycler recycler, RecyclerView.State state) {
+        resolveShouldLayoutReverse();
+        if (getChildCount() == 0) {
+            return null;
+        }
+
+        final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);
+        if (layoutDir == LayoutState.INVALID_LAYOUT) {
+            return null;
+        }
+        ensureLayoutState();
+        final View referenceChild;
+        if (layoutDir == LayoutState.LAYOUT_START) {
+            referenceChild = findReferenceChildClosestToStart(recycler, state);
+        } else {
+            referenceChild = findReferenceChildClosestToEnd(recycler, state);
+        }
+        if (referenceChild == null) {
+            if (DEBUG) {
+                Log.d(TAG,
+                        "Cannot find a child with a valid position to be used for focus search.");
+            }
+            return null;
+        }
+        ensureLayoutState();
+        final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
+        updateLayoutState(layoutDir, maxScroll, false, state);
+        mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
+        mLayoutState.mRecycle = false;
+        fill(recycler, mLayoutState, state, true);
+        final View nextFocus;
+        if (layoutDir == LayoutState.LAYOUT_START) {
+            nextFocus = getChildClosestToStart();
+        } else {
+            nextFocus = getChildClosestToEnd();
+        }
+        if (nextFocus == referenceChild || !nextFocus.isFocusable()) {
+            return null;
+        }
+        return nextFocus;
+    }
+
+    /**
+     * Used for debugging.
+     * Logs the internal representation of children to default logger.
+     */
+    private void logChildren() {
+        Log.d(TAG, "internal representation of views on the screen");
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            Log.d(TAG, "item " + getPosition(child) + ", coord:"
+                    + mOrientationHelper.getDecoratedStart(child));
+        }
+        Log.d(TAG, "==============");
+    }
+
+    /**
+     * Used for debugging.
+     * Validates that child views are laid out in correct order. This is important because rest of
+     * the algorithm relies on this constraint.
+     *
+     * In default layout, child 0 should be closest to screen position 0 and last child should be
+     * closest to position WIDTH or HEIGHT.
+     * In reverse layout, last child should be closes to screen position 0 and first child should
+     * be closest to position WIDTH  or HEIGHT
+     */
+    void validateChildOrder() {
+        Log.d(TAG, "validating child count " + getChildCount());
+        if (getChildCount() < 1) {
+            return;
+        }
+        int lastPos = getPosition(getChildAt(0));
+        int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0));
+        if (mShouldReverseLayout) {
+            for (int i = 1; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                int pos = getPosition(child);
+                int screenLoc = mOrientationHelper.getDecoratedStart(child);
+                if (pos < lastPos) {
+                    logChildren();
+                    throw new RuntimeException("detected invalid position. loc invalid? "
+                            + (screenLoc < lastScreenLoc));
+                }
+                if (screenLoc > lastScreenLoc) {
+                    logChildren();
+                    throw new RuntimeException("detected invalid location");
+                }
+            }
+        } else {
+            for (int i = 1; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                int pos = getPosition(child);
+                int screenLoc = mOrientationHelper.getDecoratedStart(child);
+                if (pos < lastPos) {
+                    logChildren();
+                    throw new RuntimeException("detected invalid position. loc invalid? "
+                            + (screenLoc < lastScreenLoc));
+                }
+                if (screenLoc < lastScreenLoc) {
+                    logChildren();
+                    throw new RuntimeException("detected invalid location");
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean supportsPredictiveItemAnimations() {
+        return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd;
+    }
+
+    /**
+     * @hide This method should be called by ItemTouchHelper only.
+     */
+    @Override
+    public void prepareForDrop(View view, View target, int x, int y) {
+        assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation");
+        ensureLayoutState();
+        resolveShouldLayoutReverse();
+        final int myPos = getPosition(view);
+        final int targetPos = getPosition(target);
+        final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL
+                : LayoutState.ITEM_DIRECTION_HEAD;
+        if (mShouldReverseLayout) {
+            if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) {
+                scrollToPositionWithOffset(targetPos,
+                        mOrientationHelper.getEndAfterPadding()
+                                - (mOrientationHelper.getDecoratedStart(target)
+                                        + mOrientationHelper.getDecoratedMeasurement(view)));
+            } else {
+                scrollToPositionWithOffset(targetPos,
+                        mOrientationHelper.getEndAfterPadding()
+                                - mOrientationHelper.getDecoratedEnd(target));
+            }
+        } else {
+            if (dropDirection == LayoutState.ITEM_DIRECTION_HEAD) {
+                scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedStart(target));
+            } else {
+                scrollToPositionWithOffset(targetPos,
+                        mOrientationHelper.getDecoratedEnd(target)
+                                - mOrientationHelper.getDecoratedMeasurement(view));
+            }
+        }
+    }
+
+    /**
+     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
+     * space.
+     */
+    static class LayoutState {
+
+        static final String TAG = "LLM#LayoutState";
+
+        static final int LAYOUT_START = -1;
+
+        static final int LAYOUT_END = 1;
+
+        static final int INVALID_LAYOUT = Integer.MIN_VALUE;
+
+        static final int ITEM_DIRECTION_HEAD = -1;
+
+        static final int ITEM_DIRECTION_TAIL = 1;
+
+        static final int SCROLLING_OFFSET_NaN = Integer.MIN_VALUE;
+
+        /**
+         * We may not want to recycle children in some cases (e.g. layout)
+         */
+        boolean mRecycle = true;
+
+        /**
+         * Pixel offset where layout should start
+         */
+        int mOffset;
+
+        /**
+         * Number of pixels that we should fill, in the layout direction.
+         */
+        int mAvailable;
+
+        /**
+         * Current position on the adapter to get the next item.
+         */
+        int mCurrentPosition;
+
+        /**
+         * Defines the direction in which the data adapter is traversed.
+         * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
+         */
+        int mItemDirection;
+
+        /**
+         * Defines the direction in which the layout is filled.
+         * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
+         */
+        int mLayoutDirection;
+
+        /**
+         * Used when LayoutState is constructed in a scrolling state.
+         * It should be set the amount of scrolling we can make without creating a new view.
+         * Settings this is required for efficient view recycling.
+         */
+        int mScrollingOffset;
+
+        /**
+         * Used if you want to pre-layout items that are not yet visible.
+         * The difference with {@link #mAvailable} is that, when recycling, distance laid out for
+         * {@link #mExtra} is not considered to avoid recycling visible children.
+         */
+        int mExtra = 0;
+
+        /**
+         * Equal to {@link RecyclerView.State#isPreLayout()}. When consuming scrap, if this value
+         * is set to true, we skip removed views since they should not be laid out in post layout
+         * step.
+         */
+        boolean mIsPreLayout = false;
+
+        /**
+         * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)}
+         * amount.
+         */
+        int mLastScrollDelta;
+
+        /**
+         * When LLM needs to layout particular views, it sets this list in which case, LayoutState
+         * will only return views from this list and return null if it cannot find an item.
+         */
+        List<RecyclerView.ViewHolder> mScrapList = null;
+
+        /**
+         * Used when there is no limit in how many views can be laid out.
+         */
+        boolean mInfinite;
+
+        /**
+         * @return true if there are more items in the data adapter
+         */
+        boolean hasMore(RecyclerView.State state) {
+            return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
+        }
+
+        /**
+         * Gets the view for the next element that we should layout.
+         * Also updates current item index to the next item, based on {@link #mItemDirection}
+         *
+         * @return The next element that we should layout.
+         */
+        View next(RecyclerView.Recycler recycler) {
+            if (mScrapList != null) {
+                return nextViewFromScrapList();
+            }
+            final View view = recycler.getViewForPosition(mCurrentPosition);
+            mCurrentPosition += mItemDirection;
+            return view;
+        }
+
+        /**
+         * Returns the next item from the scrap list.
+         * <p>
+         * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection
+         *
+         * @return View if an item in the current position or direction exists if not null.
+         */
+        private View nextViewFromScrapList() {
+            final int size = mScrapList.size();
+            for (int i = 0; i < size; i++) {
+                final View view = mScrapList.get(i).itemView;
+                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+                if (lp.isItemRemoved()) {
+                    continue;
+                }
+                if (mCurrentPosition == lp.getViewLayoutPosition()) {
+                    assignPositionFromScrapList(view);
+                    return view;
+                }
+            }
+            return null;
+        }
+
+        public void assignPositionFromScrapList() {
+            assignPositionFromScrapList(null);
+        }
+
+        public void assignPositionFromScrapList(View ignore) {
+            final View closest = nextViewInLimitedList(ignore);
+            if (closest == null) {
+                mCurrentPosition = NO_POSITION;
+            } else {
+                mCurrentPosition = ((LayoutParams) closest.getLayoutParams())
+                        .getViewLayoutPosition();
+            }
+        }
+
+        public View nextViewInLimitedList(View ignore) {
+            int size = mScrapList.size();
+            View closest = null;
+            int closestDistance = Integer.MAX_VALUE;
+            if (DEBUG && mIsPreLayout) {
+                throw new IllegalStateException("Scrap list cannot be used in pre layout");
+            }
+            for (int i = 0; i < size; i++) {
+                View view = mScrapList.get(i).itemView;
+                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+                if (view == ignore || lp.isItemRemoved()) {
+                    continue;
+                }
+                final int distance = (lp.getViewLayoutPosition() - mCurrentPosition)
+                        * mItemDirection;
+                if (distance < 0) {
+                    continue; // item is not in current direction
+                }
+                if (distance < closestDistance) {
+                    closest = view;
+                    closestDistance = distance;
+                    if (distance == 0) {
+                        break;
+                    }
+                }
+            }
+            return closest;
+        }
+
+        void log() {
+            Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:"
+                    + mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static class SavedState implements Parcelable {
+
+        int mAnchorPosition;
+
+        int mAnchorOffset;
+
+        boolean mAnchorLayoutFromEnd;
+
+        public SavedState() {
+
+        }
+
+        SavedState(Parcel in) {
+            mAnchorPosition = in.readInt();
+            mAnchorOffset = in.readInt();
+            mAnchorLayoutFromEnd = in.readInt() == 1;
+        }
+
+        public SavedState(SavedState other) {
+            mAnchorPosition = other.mAnchorPosition;
+            mAnchorOffset = other.mAnchorOffset;
+            mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
+        }
+
+        boolean hasValidAnchor() {
+            return mAnchorPosition >= 0;
+        }
+
+        void invalidateAnchor() {
+            mAnchorPosition = NO_POSITION;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mAnchorPosition);
+            dest.writeInt(mAnchorOffset);
+            dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel in) {
+                        return new SavedState(in);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+    }
+
+    /**
+     * Simple data class to keep Anchor information
+     */
+    class AnchorInfo {
+        int mPosition;
+        int mCoordinate;
+        boolean mLayoutFromEnd;
+        boolean mValid;
+
+        AnchorInfo() {
+            reset();
+        }
+
+        void reset() {
+            mPosition = NO_POSITION;
+            mCoordinate = INVALID_OFFSET;
+            mLayoutFromEnd = false;
+            mValid = false;
+        }
+
+        /**
+         * assigns anchor coordinate from the RecyclerView's padding depending on current
+         * layoutFromEnd value
+         */
+        void assignCoordinateFromPadding() {
+            mCoordinate = mLayoutFromEnd
+                    ? mOrientationHelper.getEndAfterPadding()
+                    : mOrientationHelper.getStartAfterPadding();
+        }
+
+        @Override
+        public String toString() {
+            return "AnchorInfo{"
+                    + "mPosition=" + mPosition
+                    + ", mCoordinate=" + mCoordinate
+                    + ", mLayoutFromEnd=" + mLayoutFromEnd
+                    + ", mValid=" + mValid
+                    + '}';
+        }
+
+        boolean isViewValidAsAnchor(View child, RecyclerView.State state) {
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0
+                    && lp.getViewLayoutPosition() < state.getItemCount();
+        }
+
+        public void assignFromViewAndKeepVisibleRect(View child) {
+            final int spaceChange = mOrientationHelper.getTotalSpaceChange();
+            if (spaceChange >= 0) {
+                assignFromView(child);
+                return;
+            }
+            mPosition = getPosition(child);
+            if (mLayoutFromEnd) {
+                final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange;
+                final int childEnd = mOrientationHelper.getDecoratedEnd(child);
+                final int previousEndMargin = prevLayoutEnd - childEnd;
+                mCoordinate = mOrientationHelper.getEndAfterPadding() - previousEndMargin;
+                // ensure we did not push child's top out of bounds because of this
+                if (previousEndMargin > 0) { // we have room to shift bottom if necessary
+                    final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
+                    final int estimatedChildStart = mCoordinate - childSize;
+                    final int layoutStart = mOrientationHelper.getStartAfterPadding();
+                    final int previousStartMargin = mOrientationHelper.getDecoratedStart(child)
+                            - layoutStart;
+                    final int startReference = layoutStart + Math.min(previousStartMargin, 0);
+                    final int startMargin = estimatedChildStart - startReference;
+                    if (startMargin < 0) {
+                        // offset to make top visible but not too much
+                        mCoordinate += Math.min(previousEndMargin, -startMargin);
+                    }
+                }
+            } else {
+                final int childStart = mOrientationHelper.getDecoratedStart(child);
+                final int startMargin = childStart - mOrientationHelper.getStartAfterPadding();
+                mCoordinate = childStart;
+                if (startMargin > 0) { // we have room to fix end as well
+                    final int estimatedEnd = childStart
+                            + mOrientationHelper.getDecoratedMeasurement(child);
+                    final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding()
+                            - spaceChange;
+                    final int previousEndMargin = previousLayoutEnd
+                            - mOrientationHelper.getDecoratedEnd(child);
+                    final int endReference = mOrientationHelper.getEndAfterPadding()
+                            - Math.min(0, previousEndMargin);
+                    final int endMargin = endReference - estimatedEnd;
+                    if (endMargin < 0) {
+                        mCoordinate -= Math.min(startMargin, -endMargin);
+                    }
+                }
+            }
+        }
+
+        public void assignFromView(View child) {
+            if (mLayoutFromEnd) {
+                mCoordinate = mOrientationHelper.getDecoratedEnd(child)
+                        + mOrientationHelper.getTotalSpaceChange();
+            } else {
+                mCoordinate = mOrientationHelper.getDecoratedStart(child);
+            }
+
+            mPosition = getPosition(child);
+        }
+    }
+
+    protected static class LayoutChunkResult {
+        public int mConsumed;
+        public boolean mFinished;
+        public boolean mIgnoreConsumed;
+        public boolean mFocusable;
+
+        void resetInternal() {
+            mConsumed = 0;
+            mFinished = false;
+            mIgnoreConsumed = false;
+            mFocusable = false;
+        }
+    }
+}
diff --git a/com/android/internal/widget/LinearLayoutWithDefaultTouchRecepient.java b/com/android/internal/widget/LinearLayoutWithDefaultTouchRecepient.java
new file mode 100644
index 0000000..b2001cb
--- /dev/null
+++ b/com/android/internal/widget/LinearLayoutWithDefaultTouchRecepient.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2008 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+
+
+/**
+ * Like a normal linear layout, but supports dispatching all otherwise unhandled
+ * touch events to a particular descendant.  This is for the unlock screen, so
+ * that a wider range of touch events than just the lock pattern widget can kick
+ * off a lock pattern if the finger is eventually dragged into the bounds of the
+ * lock pattern view.
+ */
+public class LinearLayoutWithDefaultTouchRecepient extends LinearLayout {
+
+    private final Rect mTempRect = new Rect();
+    private View mDefaultTouchRecepient;
+
+    public LinearLayoutWithDefaultTouchRecepient(Context context) {
+        super(context);
+    }
+
+    public LinearLayoutWithDefaultTouchRecepient(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setDefaultTouchRecepient(View defaultTouchRecepient) {
+        mDefaultTouchRecepient = defaultTouchRecepient;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (mDefaultTouchRecepient == null) {
+            return super.dispatchTouchEvent(ev);
+        }
+
+        if (super.dispatchTouchEvent(ev)) {
+            return true;
+        }
+        mTempRect.set(0, 0, 0, 0);
+        offsetRectIntoDescendantCoords(mDefaultTouchRecepient, mTempRect);
+        ev.setLocation(ev.getX() + mTempRect.left, ev.getY() + mTempRect.top);
+        return mDefaultTouchRecepient.dispatchTouchEvent(ev);
+    }
+
+}
diff --git a/com/android/internal/widget/LinearSmoothScroller.java b/com/android/internal/widget/LinearSmoothScroller.java
new file mode 100644
index 0000000..d024f21
--- /dev/null
+++ b/com/android/internal/widget/LinearSmoothScroller.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until
+ * the target position becomes a child of the RecyclerView and then uses a
+ * {@link DecelerateInterpolator} to slowly approach to target position.
+ * <p>
+ * If the {@link RecyclerView.LayoutManager} you are using does not implement the
+ * {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the
+ * {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with
+ * the support library implement this interface.
+ */
+public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
+
+    private static final String TAG = "LinearSmoothScroller";
+
+    private static final boolean DEBUG = false;
+
+    private static final float MILLISECONDS_PER_INCH = 25f;
+
+    private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
+
+    /**
+     * Align child view's left or top with parent view's left or top
+     *
+     * @see #calculateDtToFit(int, int, int, int, int)
+     * @see #calculateDxToMakeVisible(android.view.View, int)
+     * @see #calculateDyToMakeVisible(android.view.View, int)
+     */
+    public static final int SNAP_TO_START = -1;
+
+    /**
+     * Align child view's right or bottom with parent view's right or bottom
+     *
+     * @see #calculateDtToFit(int, int, int, int, int)
+     * @see #calculateDxToMakeVisible(android.view.View, int)
+     * @see #calculateDyToMakeVisible(android.view.View, int)
+     */
+    public static final int SNAP_TO_END = 1;
+
+    /**
+     * <p>Decides if the child should be snapped from start or end, depending on where it
+     * currently is in relation to its parent.</p>
+     * <p>For instance, if the view is virtually on the left of RecyclerView, using
+     * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
+     *
+     * @see #calculateDtToFit(int, int, int, int, int)
+     * @see #calculateDxToMakeVisible(android.view.View, int)
+     * @see #calculateDyToMakeVisible(android.view.View, int)
+     */
+    public static final int SNAP_TO_ANY = 0;
+
+    // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
+    // view is not laid out until interim target position is reached, we can detect the case before
+    // scrolling slows down and reschedule another interim target scroll
+    private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
+
+    protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
+
+    protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
+
+    protected PointF mTargetVector;
+
+    private final float MILLISECONDS_PER_PX;
+
+    // Temporary variables to keep track of the interim scroll target. These values do not
+    // point to a real item position, rather point to an estimated location pixels.
+    protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
+
+    public LinearSmoothScroller(Context context) {
+        MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onStart() {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+        final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
+        final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
+        final int distance = (int) Math.sqrt(dx * dx + dy * dy);
+        final int time = calculateTimeForDeceleration(distance);
+        if (time > 0) {
+            action.update(-dx, -dy, time, mDecelerateInterpolator);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
+        if (getChildCount() == 0) {
+            stop();
+            return;
+        }
+        //noinspection PointlessBooleanExpression
+        if (DEBUG && mTargetVector != null
+                && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
+            throw new IllegalStateException("Scroll happened in the opposite direction"
+                    + " of the target. Some calculations are wrong");
+        }
+        mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
+        mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
+
+        if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
+            updateActionForInterimTarget(action);
+        } // everything is valid, keep going
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onStop() {
+        mInterimTargetDx = mInterimTargetDy = 0;
+        mTargetVector = null;
+    }
+
+    /**
+     * Calculates the scroll speed.
+     *
+     * @param displayMetrics DisplayMetrics to be used for real dimension calculations
+     * @return The time (in ms) it should take for each pixel. For instance, if returned value is
+     * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
+     */
+    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+    }
+
+    /**
+     * <p>Calculates the time for deceleration so that transition from LinearInterpolator to
+     * DecelerateInterpolator looks smooth.</p>
+     *
+     * @param dx Distance to scroll
+     * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
+     * from LinearInterpolation
+     */
+    protected int calculateTimeForDeceleration(int dx) {
+        // we want to cover same area with the linear interpolator for the first 10% of the
+        // interpolation. After that, deceleration will take control.
+        // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
+        // which gives 0.100028 when x = .3356
+        // this is why we divide linear scrolling time with .3356
+        return  (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
+    }
+
+    /**
+     * Calculates the time it should take to scroll the given distance (in pixels)
+     *
+     * @param dx Distance in pixels that we want to scroll
+     * @return Time in milliseconds
+     * @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
+     */
+    protected int calculateTimeForScrolling(int dx) {
+        // In a case where dx is very small, rounding may return 0 although dx > 0.
+        // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
+        // time.
+        return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
+    }
+
+    /**
+     * When scrolling towards a child view, this method defines whether we should align the left
+     * or the right edge of the child with the parent RecyclerView.
+     *
+     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
+     * @see #SNAP_TO_START
+     * @see #SNAP_TO_END
+     * @see #SNAP_TO_ANY
+     */
+    protected int getHorizontalSnapPreference() {
+        return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
+                mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
+    }
+
+    /**
+     * When scrolling towards a child view, this method defines whether we should align the top
+     * or the bottom edge of the child with the parent RecyclerView.
+     *
+     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
+     * @see #SNAP_TO_START
+     * @see #SNAP_TO_END
+     * @see #SNAP_TO_ANY
+     */
+    protected int getVerticalSnapPreference() {
+        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
+                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
+    }
+
+    /**
+     * When the target scroll position is not a child of the RecyclerView, this method calculates
+     * a direction vector towards that child and triggers a smooth scroll.
+     *
+     * @see #computeScrollVectorForPosition(int)
+     */
+    protected void updateActionForInterimTarget(Action action) {
+        // find an interim target position
+        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
+        if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
+            final int target = getTargetPosition();
+            action.jumpTo(target);
+            stop();
+            return;
+        }
+        normalize(scrollVector);
+        mTargetVector = scrollVector;
+
+        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
+        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
+        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
+        // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
+        // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
+        // won't actually scroll more than what we need.
+        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
+                (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
+                (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
+    }
+
+    private int clampApplyScroll(int tmpDt, int dt) {
+        final int before = tmpDt;
+        tmpDt -= dt;
+        if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
+            return 0;
+        }
+        return tmpDt;
+    }
+
+    /**
+     * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
+     * {@link #calculateDyToMakeVisible(android.view.View, int)}
+     */
+    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
+            snapPreference) {
+        switch (snapPreference) {
+            case SNAP_TO_START:
+                return boxStart - viewStart;
+            case SNAP_TO_END:
+                return boxEnd - viewEnd;
+            case SNAP_TO_ANY:
+                final int dtStart = boxStart - viewStart;
+                if (dtStart > 0) {
+                    return dtStart;
+                }
+                final int dtEnd = boxEnd - viewEnd;
+                if (dtEnd < 0) {
+                    return dtEnd;
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("snap preference should be one of the"
+                        + " constants defined in SmoothScroller, starting with SNAP_");
+        }
+        return 0;
+    }
+
+    /**
+     * Calculates the vertical scroll amount necessary to make the given view fully visible
+     * inside the RecyclerView.
+     *
+     * @param view           The view which we want to make fully visible
+     * @param snapPreference The edge which the view should snap to when entering the visible
+     *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
+     *                       {@link #SNAP_TO_ANY}.
+     * @return The vertical scroll amount necessary to make the view visible with the given
+     * snap preference.
+     */
+    public int calculateDyToMakeVisible(View view, int snapPreference) {
+        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+        if (layoutManager == null || !layoutManager.canScrollVertically()) {
+            return 0;
+        }
+        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                view.getLayoutParams();
+        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
+        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
+        final int start = layoutManager.getPaddingTop();
+        final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
+        return calculateDtToFit(top, bottom, start, end, snapPreference);
+    }
+
+    /**
+     * Calculates the horizontal scroll amount necessary to make the given view fully visible
+     * inside the RecyclerView.
+     *
+     * @param view           The view which we want to make fully visible
+     * @param snapPreference The edge which the view should snap to when entering the visible
+     *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
+     *                       {@link #SNAP_TO_END}
+     * @return The vertical scroll amount necessary to make the view visible with the given
+     * snap preference.
+     */
+    public int calculateDxToMakeVisible(View view, int snapPreference) {
+        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+        if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
+            return 0;
+        }
+        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                view.getLayoutParams();
+        final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
+        final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
+        final int start = layoutManager.getPaddingLeft();
+        final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
+        return calculateDtToFit(left, right, start, end, snapPreference);
+    }
+
+    /**
+     * Compute the scroll vector for a given target position.
+     * <p>
+     * This method can return null if the layout manager cannot calculate a scroll vector
+     * for the given position (e.g. it has no current scroll position).
+     *
+     * @param targetPosition the position to which the scroller is scrolling
+     *
+     * @return the scroll vector for a given target position
+     */
+    @Nullable
+    public PointF computeScrollVectorForPosition(int targetPosition) {
+        RecyclerView.LayoutManager layoutManager = getLayoutManager();
+        if (layoutManager instanceof ScrollVectorProvider) {
+            return ((ScrollVectorProvider) layoutManager)
+                    .computeScrollVectorForPosition(targetPosition);
+        }
+        Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
+                + " does not implement " + ScrollVectorProvider.class.getCanonicalName());
+        return null;
+    }
+}
diff --git a/com/android/internal/widget/LockPatternChecker.java b/com/android/internal/widget/LockPatternChecker.java
new file mode 100644
index 0000000..586ece0
--- /dev/null
+++ b/com/android/internal/widget/LockPatternChecker.java
@@ -0,0 +1,256 @@
+package com.android.internal.widget;
+
+import android.os.AsyncTask;
+
+import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to check/verify PIN/Password/Pattern asynchronously.
+ */
+public final class LockPatternChecker {
+    /**
+     * Interface for a callback to be invoked after security check.
+     */
+    public interface OnCheckCallback {
+
+        /**
+         * Invoked as soon as possible we know that the credentials match. This will be called
+         * earlier than {@link #onChecked} but only if the credentials match.
+         */
+        default void onEarlyMatched() {}
+
+        /**
+         * Invoked when a security check is finished.
+         *
+         * @param matched Whether the PIN/Password/Pattern matches the stored one.
+         * @param throttleTimeoutMs The amount of time in ms to wait before reattempting
+         * the call. Only non-0 if matched is false.
+         */
+        void onChecked(boolean matched, int throttleTimeoutMs);
+
+        /**
+         * Called when the underlying AsyncTask was cancelled.
+         */
+        default void onCancelled() {}
+    }
+
+    /**
+     * Interface for a callback to be invoked after security verification.
+     */
+    public interface OnVerifyCallback {
+        /**
+         * Invoked when a security verification is finished.
+         *
+         * @param attestation The attestation that the challenge was verified, or null.
+         * @param throttleTimeoutMs The amount of time in ms to wait before reattempting
+         * the call. Only non-0 if attestation is null.
+         */
+        void onVerified(byte[] attestation, int throttleTimeoutMs);
+    }
+
+    /**
+     * Verify a pattern asynchronously.
+     *
+     * @param utils The LockPatternUtils instance to use.
+     * @param pattern The pattern to check.
+     * @param challenge The challenge to verify against the pattern.
+     * @param userId The user to check against the pattern.
+     * @param callback The callback to be invoked with the verification result.
+     */
+    public static AsyncTask<?, ?, ?> verifyPattern(final LockPatternUtils utils,
+            final List<LockPatternView.Cell> pattern,
+            final long challenge,
+            final int userId,
+            final OnVerifyCallback callback) {
+        AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
+            private int mThrottleTimeout;
+            private List<LockPatternView.Cell> patternCopy;
+
+            @Override
+            protected void onPreExecute() {
+                // Make a copy of the pattern to prevent race conditions.
+                // No need to clone the individual cells because they are immutable.
+                patternCopy = new ArrayList(pattern);
+            }
+
+            @Override
+            protected byte[] doInBackground(Void... args) {
+                try {
+                    return utils.verifyPattern(patternCopy, challenge, userId);
+                } catch (RequestThrottledException ex) {
+                    mThrottleTimeout = ex.getTimeoutMs();
+                    return null;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(byte[] result) {
+                callback.onVerified(result, mThrottleTimeout);
+            }
+        };
+        task.execute();
+        return task;
+    }
+
+    /**
+     * Checks a pattern asynchronously.
+     *
+     * @param utils The LockPatternUtils instance to use.
+     * @param pattern The pattern to check.
+     * @param userId The user to check against the pattern.
+     * @param callback The callback to be invoked with the check result.
+     */
+    public static AsyncTask<?, ?, ?> checkPattern(final LockPatternUtils utils,
+            final List<LockPatternView.Cell> pattern,
+            final int userId,
+            final OnCheckCallback callback) {
+        AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
+            private int mThrottleTimeout;
+            private List<LockPatternView.Cell> patternCopy;
+
+            @Override
+            protected void onPreExecute() {
+                // Make a copy of the pattern to prevent race conditions.
+                // No need to clone the individual cells because they are immutable.
+                patternCopy = new ArrayList(pattern);
+            }
+
+            @Override
+            protected Boolean doInBackground(Void... args) {
+                try {
+                    return utils.checkPattern(patternCopy, userId, callback::onEarlyMatched);
+                } catch (RequestThrottledException ex) {
+                    mThrottleTimeout = ex.getTimeoutMs();
+                    return false;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(Boolean result) {
+                callback.onChecked(result, mThrottleTimeout);
+            }
+
+            @Override
+            protected void onCancelled() {
+                callback.onCancelled();
+            }
+        };
+        task.execute();
+        return task;
+    }
+
+    /**
+     * Verify a password asynchronously.
+     *
+     * @param utils The LockPatternUtils instance to use.
+     * @param password The password to check.
+     * @param challenge The challenge to verify against the pattern.
+     * @param userId The user to check against the pattern.
+     * @param callback The callback to be invoked with the verification result.
+     */
+    public static AsyncTask<?, ?, ?> verifyPassword(final LockPatternUtils utils,
+            final String password,
+            final long challenge,
+            final int userId,
+            final OnVerifyCallback callback) {
+        AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
+            private int mThrottleTimeout;
+
+            @Override
+            protected byte[] doInBackground(Void... args) {
+                try {
+                    return utils.verifyPassword(password, challenge, userId);
+                } catch (RequestThrottledException ex) {
+                    mThrottleTimeout = ex.getTimeoutMs();
+                    return null;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(byte[] result) {
+                callback.onVerified(result, mThrottleTimeout);
+            }
+        };
+        task.execute();
+        return task;
+    }
+
+    /**
+     * Verify a password asynchronously.
+     *
+     * @param utils The LockPatternUtils instance to use.
+     * @param password The password to check.
+     * @param challenge The challenge to verify against the pattern.
+     * @param userId The user to check against the pattern.
+     * @param callback The callback to be invoked with the verification result.
+     */
+    public static AsyncTask<?, ?, ?> verifyTiedProfileChallenge(final LockPatternUtils utils,
+            final String password,
+            final boolean isPattern,
+            final long challenge,
+            final int userId,
+            final OnVerifyCallback callback) {
+        AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
+            private int mThrottleTimeout;
+
+            @Override
+            protected byte[] doInBackground(Void... args) {
+                try {
+                    return utils.verifyTiedProfileChallenge(password, isPattern, challenge, userId);
+                } catch (RequestThrottledException ex) {
+                    mThrottleTimeout = ex.getTimeoutMs();
+                    return null;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(byte[] result) {
+                callback.onVerified(result, mThrottleTimeout);
+            }
+        };
+        task.execute();
+        return task;
+    }
+
+    /**
+     * Checks a password asynchronously.
+     *
+     * @param utils The LockPatternUtils instance to use.
+     * @param password The password to check.
+     * @param userId The user to check against the pattern.
+     * @param callback The callback to be invoked with the check result.
+     */
+    public static AsyncTask<?, ?, ?> checkPassword(final LockPatternUtils utils,
+            final String password,
+            final int userId,
+            final OnCheckCallback callback) {
+        AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
+            private int mThrottleTimeout;
+
+            @Override
+            protected Boolean doInBackground(Void... args) {
+                try {
+                    return utils.checkPassword(password, userId, callback::onEarlyMatched);
+                } catch (RequestThrottledException ex) {
+                    mThrottleTimeout = ex.getTimeoutMs();
+                    return false;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(Boolean result) {
+                callback.onChecked(result, mThrottleTimeout);
+            }
+
+            @Override
+            protected void onCancelled() {
+                callback.onCancelled();
+            }
+        };
+        task.execute();
+        return task;
+    }
+}
diff --git a/com/android/internal/widget/LockPatternUtils.java b/com/android/internal/widget/LockPatternUtils.java
new file mode 100644
index 0000000..b8ef82b
--- /dev/null
+++ b/com/android/internal/widget/LockPatternUtils.java
@@ -0,0 +1,1784 @@
+/*
+ * Copyright (C) 2007 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.internal.widget;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.PasswordMetrics;
+import android.app.trust.IStrongAuthTracker;
+import android.app.trust.TrustManager;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.IStorageManager;
+import android.os.storage.StorageManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.google.android.collect.Lists;
+
+import libcore.util.HexEncoding;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Utilities for the lock pattern and its settings.
+ */
+public class LockPatternUtils {
+
+    private static final String TAG = "LockPatternUtils";
+    private static final boolean DEBUG = false;
+    private static final boolean FRP_CREDENTIAL_ENABLED = true;
+
+    /**
+     * The key to identify when the lock pattern enabled flag is being accessed for legacy reasons.
+     */
+    public static final String LEGACY_LOCK_PATTERN_ENABLED = "legacy_lock_pattern_enabled";
+
+    /**
+     * The number of incorrect attempts before which we fall back on an alternative
+     * method of verifying the user, and resetting their lock pattern.
+     */
+    public static final int FAILED_ATTEMPTS_BEFORE_RESET = 20;
+
+    /**
+     * The interval of the countdown for showing progress of the lockout.
+     */
+    public static final long FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS = 1000L;
+
+
+    /**
+     * This dictates when we start telling the user that continued failed attempts will wipe
+     * their device.
+     */
+    public static final int FAILED_ATTEMPTS_BEFORE_WIPE_GRACE = 5;
+
+    /**
+     * The minimum number of dots in a valid pattern.
+     */
+    public static final int MIN_LOCK_PATTERN_SIZE = 4;
+
+    /**
+     * The minimum size of a valid password.
+     */
+    public static final int MIN_LOCK_PASSWORD_SIZE = 4;
+
+    /**
+     * The minimum number of dots the user must include in a wrong pattern
+     * attempt for it to be counted against the counts that affect
+     * {@link #FAILED_ATTEMPTS_BEFORE_TIMEOUT} and {@link #FAILED_ATTEMPTS_BEFORE_RESET}
+     */
+    public static final int MIN_PATTERN_REGISTER_FAIL = MIN_LOCK_PATTERN_SIZE;
+
+    public static final int CREDENTIAL_TYPE_NONE = -1;
+
+    public static final int CREDENTIAL_TYPE_PATTERN = 1;
+
+    public static final int CREDENTIAL_TYPE_PASSWORD = 2;
+
+    /**
+     * Special user id for triggering the FRP verification flow.
+     */
+    public static final int USER_FRP = UserHandle.USER_NULL + 1;
+
+    @Deprecated
+    public final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently";
+    public final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline";
+    public final static String LOCKOUT_ATTEMPT_TIMEOUT_MS = "lockscreen.lockoutattempttimeoutmss";
+    public final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen";
+    public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type";
+    @Deprecated
+    public final static String PASSWORD_TYPE_ALTERNATE_KEY = "lockscreen.password_type_alternate";
+    public final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt";
+    public final static String DISABLE_LOCKSCREEN_KEY = "lockscreen.disabled";
+    public final static String LOCKSCREEN_OPTIONS = "lockscreen.options";
+    @Deprecated
+    public final static String LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK
+            = "lockscreen.biometric_weak_fallback";
+    @Deprecated
+    public final static String BIOMETRIC_WEAK_EVER_CHOSEN_KEY
+            = "lockscreen.biometricweakeverchosen";
+    public final static String LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS
+            = "lockscreen.power_button_instantly_locks";
+    @Deprecated
+    public final static String LOCKSCREEN_WIDGETS_ENABLED = "lockscreen.widgets_enabled";
+
+    public final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory";
+
+    private static final String LOCK_SCREEN_OWNER_INFO = Settings.Secure.LOCK_SCREEN_OWNER_INFO;
+    private static final String LOCK_SCREEN_OWNER_INFO_ENABLED =
+            Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED;
+
+    private static final String LOCK_SCREEN_DEVICE_OWNER_INFO = "lockscreen.device_owner_info";
+
+    private static final String ENABLED_TRUST_AGENTS = "lockscreen.enabledtrustagents";
+    private static final String IS_TRUST_USUALLY_MANAGED = "lockscreen.istrustusuallymanaged";
+
+    public static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_";
+    public static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_";
+    public static final String SYNTHETIC_PASSWORD_KEY_PREFIX = "synthetic_password_";
+
+    public static final String SYNTHETIC_PASSWORD_HANDLE_KEY = "sp-handle";
+    public static final String SYNTHETIC_PASSWORD_ENABLED_KEY = "enable-sp";
+
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private DevicePolicyManager mDevicePolicyManager;
+    private ILockSettings mLockSettingsService;
+    private UserManager mUserManager;
+    private final Handler mHandler;
+
+    /**
+     * Use {@link TrustManager#isTrustUsuallyManaged(int)}.
+     *
+     * This returns the lazily-peristed value and should only be used by TrustManagerService.
+     */
+    public boolean isTrustUsuallyManaged(int userId) {
+        if (!(mLockSettingsService instanceof ILockSettings.Stub)) {
+            throw new IllegalStateException("May only be called by TrustManagerService. "
+                    + "Use TrustManager.isTrustUsuallyManaged()");
+        }
+        try {
+            return getLockSettings().getBoolean(IS_TRUST_USUALLY_MANAGED, false, userId);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public void setTrustUsuallyManaged(boolean managed, int userId) {
+        try {
+            getLockSettings().setBoolean(IS_TRUST_USUALLY_MANAGED, managed, userId);
+        } catch (RemoteException e) {
+            // System dead.
+        }
+    }
+
+    public void userPresent(int userId) {
+        try {
+            getLockSettings().userPresent(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    public static final class RequestThrottledException extends Exception {
+        private int mTimeoutMs;
+        public RequestThrottledException(int timeoutMs) {
+            mTimeoutMs = timeoutMs;
+        }
+
+        /**
+         * @return The amount of time in ms before another request may
+         * be executed
+         */
+        public int getTimeoutMs() {
+            return mTimeoutMs;
+        }
+
+    }
+
+    public DevicePolicyManager getDevicePolicyManager() {
+        if (mDevicePolicyManager == null) {
+            mDevicePolicyManager =
+                (DevicePolicyManager)mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+            if (mDevicePolicyManager == null) {
+                Log.e(TAG, "Can't get DevicePolicyManagerService: is it running?",
+                        new IllegalStateException("Stack trace:"));
+            }
+        }
+        return mDevicePolicyManager;
+    }
+
+    private UserManager getUserManager() {
+        if (mUserManager == null) {
+            mUserManager = UserManager.get(mContext);
+        }
+        return mUserManager;
+    }
+
+    private TrustManager getTrustManager() {
+        TrustManager trust = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
+        if (trust == null) {
+            Log.e(TAG, "Can't get TrustManagerService: is it running?",
+                    new IllegalStateException("Stack trace:"));
+        }
+        return trust;
+    }
+
+    public LockPatternUtils(Context context) {
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+
+        Looper looper = Looper.myLooper();
+        mHandler = looper != null ? new Handler(looper) : null;
+    }
+
+    @VisibleForTesting
+    public ILockSettings getLockSettings() {
+        if (mLockSettingsService == null) {
+            ILockSettings service = ILockSettings.Stub.asInterface(
+                    ServiceManager.getService("lock_settings"));
+            mLockSettingsService = service;
+        }
+        return mLockSettingsService;
+    }
+
+    public int getRequestedMinimumPasswordLength(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumLength(null, userId);
+    }
+
+    /**
+     * Gets the device policy password mode. If the mode is non-specific, returns
+     * MODE_PATTERN which allows the user to choose anything.
+     */
+    public int getRequestedPasswordQuality(int userId) {
+        return getDevicePolicyManager().getPasswordQuality(null, userId);
+    }
+
+    private int getRequestedPasswordHistoryLength(int userId) {
+        return getDevicePolicyManager().getPasswordHistoryLength(null, userId);
+    }
+
+    public int getRequestedPasswordMinimumLetters(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumLetters(null, userId);
+    }
+
+    public int getRequestedPasswordMinimumUpperCase(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumUpperCase(null, userId);
+    }
+
+    public int getRequestedPasswordMinimumLowerCase(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumLowerCase(null, userId);
+    }
+
+    public int getRequestedPasswordMinimumNumeric(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumNumeric(null, userId);
+    }
+
+    public int getRequestedPasswordMinimumSymbols(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumSymbols(null, userId);
+    }
+
+    public int getRequestedPasswordMinimumNonLetter(int userId) {
+        return getDevicePolicyManager().getPasswordMinimumNonLetter(null, userId);
+    }
+
+    public void reportFailedPasswordAttempt(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return;
+        }
+        getDevicePolicyManager().reportFailedPasswordAttempt(userId);
+        getTrustManager().reportUnlockAttempt(false /* authenticated */, userId);
+    }
+
+    public void reportSuccessfulPasswordAttempt(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return;
+        }
+        getDevicePolicyManager().reportSuccessfulPasswordAttempt(userId);
+        getTrustManager().reportUnlockAttempt(true /* authenticated */, userId);
+    }
+
+    public void reportPasswordLockout(int timeoutMs, int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return;
+        }
+        getTrustManager().reportUnlockLockout(timeoutMs, userId);
+    }
+
+    public int getCurrentFailedPasswordAttempts(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return 0;
+        }
+        return getDevicePolicyManager().getCurrentFailedPasswordAttempts(userId);
+    }
+
+    public int getMaximumFailedPasswordsForWipe(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return 0;
+        }
+        return getDevicePolicyManager().getMaximumFailedPasswordsForWipe(
+                null /* componentName */, userId);
+    }
+
+    private byte[] verifyCredential(String credential, int type, long challenge, int userId)
+            throws RequestThrottledException {
+        try {
+            VerifyCredentialResponse response = getLockSettings().verifyCredential(credential,
+                    type, challenge, userId);
+            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+                return response.getPayload();
+            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+                throw new RequestThrottledException(response.getTimeout());
+            } else {
+                return null;
+            }
+        } catch (RemoteException re) {
+            return null;
+        }
+    }
+
+    private boolean checkCredential(String credential, int type, int userId,
+            @Nullable CheckCredentialProgressCallback progressCallback)
+            throws RequestThrottledException {
+        try {
+            VerifyCredentialResponse response = getLockSettings().checkCredential(credential, type,
+                    userId, wrapCallback(progressCallback));
+
+            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+                return true;
+            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+                throw new RequestThrottledException(response.getTimeout());
+            } else {
+                return false;
+            }
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Check to see if a pattern matches the saved pattern.
+     * If pattern matches, return an opaque attestation that the challenge
+     * was verified.
+     *
+     * @param pattern The pattern to check.
+     * @param challenge The challenge to verify against the pattern
+     * @return the attestation that the challenge was verified, or null.
+     */
+    public byte[] verifyPattern(List<LockPatternView.Cell> pattern, long challenge, int userId)
+            throws RequestThrottledException {
+        throwIfCalledOnMainThread();
+        return verifyCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN, challenge,
+                userId);
+    }
+
+    /**
+     * Check to see if a pattern matches the saved pattern.  If no pattern exists,
+     * always returns true.
+     * @param pattern The pattern to check.
+     * @return Whether the pattern matches the stored one.
+     */
+    public boolean checkPattern(List<LockPatternView.Cell> pattern, int userId)
+            throws RequestThrottledException {
+        return checkPattern(pattern, userId, null /* progressCallback */);
+    }
+
+    /**
+     * Check to see if a pattern matches the saved pattern.  If no pattern exists,
+     * always returns true.
+     * @param pattern The pattern to check.
+     * @return Whether the pattern matches the stored one.
+     */
+    public boolean checkPattern(List<LockPatternView.Cell> pattern, int userId,
+            @Nullable CheckCredentialProgressCallback progressCallback)
+            throws RequestThrottledException {
+        throwIfCalledOnMainThread();
+        return checkCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN, userId,
+                progressCallback);
+    }
+
+    /**
+     * Check to see if a password matches the saved password.
+     * If password matches, return an opaque attestation that the challenge
+     * was verified.
+     *
+     * @param password The password to check.
+     * @param challenge The challenge to verify against the password
+     * @return the attestation that the challenge was verified, or null.
+     */
+    public byte[] verifyPassword(String password, long challenge, int userId)
+            throws RequestThrottledException {
+        throwIfCalledOnMainThread();
+        return verifyCredential(password, CREDENTIAL_TYPE_PASSWORD, challenge, userId);
+    }
+
+
+    /**
+     * Check to see if a password matches the saved password.
+     * If password matches, return an opaque attestation that the challenge
+     * was verified.
+     *
+     * @param password The password to check.
+     * @param challenge The challenge to verify against the password
+     * @return the attestation that the challenge was verified, or null.
+     */
+    public byte[] verifyTiedProfileChallenge(String password, boolean isPattern, long challenge,
+            int userId) throws RequestThrottledException {
+        throwIfCalledOnMainThread();
+        try {
+            VerifyCredentialResponse response =
+                    getLockSettings().verifyTiedProfileChallenge(password,
+                            isPattern ? CREDENTIAL_TYPE_PATTERN : CREDENTIAL_TYPE_PASSWORD, challenge,
+                            userId);
+
+            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+                return response.getPayload();
+            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+                throw new RequestThrottledException(response.getTimeout());
+            } else {
+                return null;
+            }
+        } catch (RemoteException re) {
+            return null;
+        }
+    }
+
+    /**
+     * Check to see if a password matches the saved password.  If no password exists,
+     * always returns true.
+     * @param password The password to check.
+     * @return Whether the password matches the stored one.
+     */
+    public boolean checkPassword(String password, int userId) throws RequestThrottledException {
+        return checkPassword(password, userId, null /* progressCallback */);
+    }
+
+    /**
+     * Check to see if a password matches the saved password.  If no password exists,
+     * always returns true.
+     * @param password The password to check.
+     * @return Whether the password matches the stored one.
+     */
+    public boolean checkPassword(String password, int userId,
+            @Nullable CheckCredentialProgressCallback progressCallback)
+            throws RequestThrottledException {
+        throwIfCalledOnMainThread();
+        return checkCredential(password, CREDENTIAL_TYPE_PASSWORD, userId, progressCallback);
+    }
+
+    /**
+     * Check to see if vold already has the password.
+     * Note that this also clears vold's copy of the password.
+     * @return Whether the vold password matches or not.
+     */
+    public boolean checkVoldPassword(int userId) {
+        try {
+            return getLockSettings().checkVoldPassword(userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Check to see if a password matches any of the passwords stored in the
+     * password history.
+     *
+     * @param password The password to check.
+     * @return Whether the password matches any in the history.
+     */
+    public boolean checkPasswordHistory(String password, int userId) {
+        String passwordHashString = new String(
+                passwordToHash(password, userId), StandardCharsets.UTF_8);
+        String passwordHistory = getString(PASSWORD_HISTORY_KEY, userId);
+        if (passwordHistory == null) {
+            return false;
+        }
+        // Password History may be too long...
+        int passwordHashLength = passwordHashString.length();
+        int passwordHistoryLength = getRequestedPasswordHistoryLength(userId);
+        if(passwordHistoryLength == 0) {
+            return false;
+        }
+        int neededPasswordHistoryLength = passwordHashLength * passwordHistoryLength
+                + passwordHistoryLength - 1;
+        if (passwordHistory.length() > neededPasswordHistoryLength) {
+            passwordHistory = passwordHistory.substring(0, neededPasswordHistoryLength);
+        }
+        return passwordHistory.contains(passwordHashString);
+    }
+
+    /**
+     * Check to see if the user has stored a lock pattern.
+     * @return Whether a saved pattern exists.
+     */
+    private boolean savedPatternExists(int userId) {
+        try {
+            return getLockSettings().havePattern(userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Check to see if the user has stored a lock pattern.
+     * @return Whether a saved pattern exists.
+     */
+    private boolean savedPasswordExists(int userId) {
+        try {
+            return getLockSettings().havePassword(userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Return true if the user has ever chosen a pattern.  This is true even if the pattern is
+     * currently cleared.
+     *
+     * @return True if the user has ever chosen a pattern.
+     */
+    public boolean isPatternEverChosen(int userId) {
+        return getBoolean(PATTERN_EVER_CHOSEN_KEY, false, userId);
+    }
+
+    /**
+     * Records that the user has chosen a pattern at some time, even if the pattern is
+     * currently cleared.
+     */
+    public void reportPatternWasChosen(int userId) {
+        setBoolean(PATTERN_EVER_CHOSEN_KEY, true, userId);
+    }
+
+    /**
+     * Used by device policy manager to validate the current password
+     * information it has.
+     */
+    public int getActivePasswordQuality(int userId) {
+        int quality = getKeyguardStoredPasswordQuality(userId);
+
+        if (isLockPasswordEnabled(quality, userId)) {
+            // Quality is a password and a password exists. Return the quality.
+            return quality;
+        }
+
+        if (isLockPatternEnabled(quality, userId)) {
+            // Quality is a pattern and a pattern exists. Return the quality.
+            return quality;
+        }
+
+        return DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+    }
+
+    /**
+     * Use it to reset keystore without wiping work profile
+     */
+    public void resetKeyStore(int userId) {
+        try {
+            getLockSettings().resetKeyStore(userId);
+        } catch (RemoteException e) {
+            // It should not happen
+            Log.e(TAG, "Couldn't reset keystore " + e);
+        }
+    }
+
+    /**
+     * Clear any lock pattern or password.
+     */
+    public void clearLock(String savedCredential, int userHandle) {
+        setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle);
+
+        try{
+            getLockSettings().setLockCredential(null, CREDENTIAL_TYPE_NONE, savedCredential,
+                    DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle);
+        } catch (RemoteException e) {
+            // well, we tried...
+        }
+
+        if (userHandle == UserHandle.USER_SYSTEM) {
+            // Set the encryption password to default.
+            updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+            setCredentialRequiredToDecrypt(false);
+        }
+
+        onAfterChangingPassword(userHandle);
+    }
+
+    /**
+     * Disable showing lock screen at all for a given user.
+     * This is only meaningful if pattern, pin or password are not set.
+     *
+     * @param disable Disables lock screen when true
+     * @param userId User ID of the user this has effect on
+     */
+    public void setLockScreenDisabled(boolean disable, int userId) {
+        setBoolean(DISABLE_LOCKSCREEN_KEY, disable, userId);
+    }
+
+    /**
+     * Determine if LockScreen is disabled for the current user. This is used to decide whether
+     * LockScreen is shown after reboot or after screen timeout / short press on power.
+     *
+     * @return true if lock screen is disabled
+     */
+    public boolean isLockScreenDisabled(int userId) {
+        if (isSecure(userId)) {
+            return false;
+        }
+        boolean disabledByDefault = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_disableLockscreenByDefault);
+        boolean isSystemUser = UserManager.isSplitSystemUser() && userId == UserHandle.USER_SYSTEM;
+        UserInfo userInfo = getUserManager().getUserInfo(userId);
+        boolean isDemoUser = UserManager.isDeviceInDemoMode(mContext) && userInfo != null
+                && userInfo.isDemo();
+        return getBoolean(DISABLE_LOCKSCREEN_KEY, false, userId)
+                || (disabledByDefault && !isSystemUser)
+                || isDemoUser;
+    }
+
+    /**
+     * Save a lock pattern.
+     * @param pattern The new pattern to save.
+     * @param userId the user whose pattern is to be saved.
+     */
+    public void saveLockPattern(List<LockPatternView.Cell> pattern, int userId) {
+        this.saveLockPattern(pattern, null, userId);
+    }
+    /**
+     * Save a lock pattern.
+     * @param pattern The new pattern to save.
+     * @param savedPattern The previously saved pattern, converted to String format
+     * @param userId the user whose pattern is to be saved.
+     */
+    public void saveLockPattern(List<LockPatternView.Cell> pattern, String savedPattern, int userId) {
+        try {
+            if (pattern == null || pattern.size() < MIN_LOCK_PATTERN_SIZE) {
+                throw new IllegalArgumentException("pattern must not be null and at least "
+                        + MIN_LOCK_PATTERN_SIZE + " dots long.");
+            }
+
+            setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);
+            getLockSettings().setLockCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN,
+                    savedPattern, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);
+
+            // Update the device encryption password.
+            if (userId == UserHandle.USER_SYSTEM
+                    && LockPatternUtils.isDeviceEncryptionEnabled()) {
+                if (!shouldEncryptWithCredentials(true)) {
+                    clearEncryptionPassword();
+                } else {
+                    String stringPattern = patternToString(pattern);
+                    updateEncryptionPassword(StorageManager.CRYPT_TYPE_PATTERN, stringPattern);
+                }
+            }
+
+            reportPatternWasChosen(userId);
+            onAfterChangingPassword(userId);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Couldn't save lock pattern " + re);
+        }
+    }
+
+    private void updateCryptoUserInfo(int userId) {
+        if (userId != UserHandle.USER_SYSTEM) {
+            return;
+        }
+
+        final String ownerInfo = isOwnerInfoEnabled(userId) ? getOwnerInfo(userId) : "";
+
+        IBinder service = ServiceManager.getService("mount");
+        if (service == null) {
+            Log.e(TAG, "Could not find the mount service to update the user info");
+            return;
+        }
+
+        IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+        try {
+            Log.d(TAG, "Setting owner info");
+            storageManager.setField(StorageManager.OWNER_INFO_KEY, ownerInfo);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error changing user info", e);
+        }
+    }
+
+    public void setOwnerInfo(String info, int userId) {
+        setString(LOCK_SCREEN_OWNER_INFO, info, userId);
+        updateCryptoUserInfo(userId);
+    }
+
+    public void setOwnerInfoEnabled(boolean enabled, int userId) {
+        setBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, enabled, userId);
+        updateCryptoUserInfo(userId);
+    }
+
+    public String getOwnerInfo(int userId) {
+        return getString(LOCK_SCREEN_OWNER_INFO, userId);
+    }
+
+    public boolean isOwnerInfoEnabled(int userId) {
+        return getBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, false, userId);
+    }
+
+    /**
+     * Sets the device owner information. If the information is {@code null} or empty then the
+     * device owner info is cleared.
+     *
+     * @param info Device owner information which will be displayed instead of the user
+     * owner info.
+     */
+    public void setDeviceOwnerInfo(String info) {
+        if (info != null && info.isEmpty()) {
+            info = null;
+        }
+
+        setString(LOCK_SCREEN_DEVICE_OWNER_INFO, info, UserHandle.USER_SYSTEM);
+    }
+
+    public String getDeviceOwnerInfo() {
+        return getString(LOCK_SCREEN_DEVICE_OWNER_INFO, UserHandle.USER_SYSTEM);
+    }
+
+    public boolean isDeviceOwnerInfoEnabled() {
+        return getDeviceOwnerInfo() != null;
+    }
+
+    /** Update the encryption password if it is enabled **/
+    private void updateEncryptionPassword(final int type, final String password) {
+        if (!isDeviceEncryptionEnabled()) {
+            return;
+        }
+        final IBinder service = ServiceManager.getService("mount");
+        if (service == null) {
+            Log.e(TAG, "Could not find the mount service to update the encryption password");
+            return;
+        }
+
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... dummy) {
+                IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+                try {
+                    storageManager.changeEncryptionPassword(type, password);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error changing encryption password", e);
+                }
+                return null;
+            }
+        }.execute();
+    }
+
+    /**
+     * Save a lock password.  Does not ensure that the password is as good
+     * as the requested mode, but will adjust the mode to be as good as the
+     * password.
+     * @param password The password to save
+     * @param savedPassword The previously saved lock password, or null if none
+     * @param requestedQuality {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
+     * @param userHandle The userId of the user to change the password for
+     */
+    public void saveLockPassword(String password, String savedPassword, int requestedQuality,
+            int userHandle) {
+        try {
+            if (password == null || password.length() < MIN_LOCK_PASSWORD_SIZE) {
+                throw new IllegalArgumentException("password must not be null and at least "
+                        + "of length " + MIN_LOCK_PASSWORD_SIZE);
+            }
+
+            setLong(PASSWORD_TYPE_KEY,
+                    computePasswordQuality(CREDENTIAL_TYPE_PASSWORD, password, requestedQuality),
+                    userHandle);
+            getLockSettings().setLockCredential(password, CREDENTIAL_TYPE_PASSWORD, savedPassword,
+                    requestedQuality, userHandle);
+
+            updateEncryptionPasswordIfNeeded(password,
+                    PasswordMetrics.computeForPassword(password).quality, userHandle);
+            updatePasswordHistory(password, userHandle);
+        } catch (RemoteException re) {
+            // Cant do much
+            Log.e(TAG, "Unable to save lock password " + re);
+        }
+    }
+
+    /**
+     * Update device encryption password if calling user is USER_SYSTEM and device supports
+     * encryption.
+     */
+    private void updateEncryptionPasswordIfNeeded(String password, int quality, int userHandle) {
+        // Update the device encryption password.
+        if (userHandle == UserHandle.USER_SYSTEM
+                && LockPatternUtils.isDeviceEncryptionEnabled()) {
+            if (!shouldEncryptWithCredentials(true)) {
+                clearEncryptionPassword();
+            } else {
+                boolean numeric = quality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+                boolean numericComplex = quality
+                        == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
+                int type = numeric || numericComplex ? StorageManager.CRYPT_TYPE_PIN
+                        : StorageManager.CRYPT_TYPE_PASSWORD;
+                updateEncryptionPassword(type, password);
+            }
+        }
+    }
+
+    private void updatePasswordHistory(String password, int userHandle) {
+
+        // Add the password to the password history. We assume all
+        // password hashes have the same length for simplicity of implementation.
+        String passwordHistory = getString(PASSWORD_HISTORY_KEY, userHandle);
+        if (passwordHistory == null) {
+            passwordHistory = "";
+        }
+        int passwordHistoryLength = getRequestedPasswordHistoryLength(userHandle);
+        if (passwordHistoryLength == 0) {
+            passwordHistory = "";
+        } else {
+            byte[] hash = passwordToHash(password, userHandle);
+            passwordHistory = new String(hash, StandardCharsets.UTF_8) + "," + passwordHistory;
+            // Cut it to contain passwordHistoryLength hashes
+            // and passwordHistoryLength -1 commas.
+            passwordHistory = passwordHistory.substring(0, Math.min(hash.length
+                    * passwordHistoryLength + passwordHistoryLength - 1, passwordHistory
+                    .length()));
+        }
+        setString(PASSWORD_HISTORY_KEY, passwordHistory, userHandle);
+        onAfterChangingPassword(userHandle);
+    }
+
+    /**
+     * Determine if the device supports encryption, even if it's set to default. This
+     * differs from isDeviceEncrypted() in that it returns true even if the device is
+     * encrypted with the default password.
+     * @return true if device encryption is enabled
+     */
+    public static boolean isDeviceEncryptionEnabled() {
+        return StorageManager.isEncrypted();
+    }
+
+    /**
+     * Determine if the device is file encrypted
+     * @return true if device is file encrypted
+     */
+    public static boolean isFileEncryptionEnabled() {
+        return StorageManager.isFileEncryptedNativeOrEmulated();
+    }
+
+    /**
+     * Clears the encryption password.
+     */
+    public void clearEncryptionPassword() {
+        updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+    }
+
+    /**
+     * Retrieves the quality mode for {@param userHandle}.
+     * {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
+     *
+     * @return stored password quality
+     */
+    public int getKeyguardStoredPasswordQuality(int userHandle) {
+        return (int) getLong(PASSWORD_TYPE_KEY,
+                DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle);
+    }
+
+    /**
+     * Returns the password quality of the given credential, promoting it to a higher level
+     * if DevicePolicyManager has a stronger quality requirement. This value will be written
+     * to PASSWORD_TYPE_KEY.
+     */
+    private int computePasswordQuality(int type, String credential, int requestedQuality) {
+        final int quality;
+        if (type == CREDENTIAL_TYPE_PASSWORD) {
+            int computedQuality = PasswordMetrics.computeForPassword(credential).quality;
+            quality = Math.max(requestedQuality, computedQuality);
+        } else if (type == CREDENTIAL_TYPE_PATTERN)  {
+            quality = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+        } else /* if (type == CREDENTIAL_TYPE_NONE) */ {
+            quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+        }
+        return quality;
+    }
+
+    /**
+     * Enables/disables the Separate Profile Challenge for this {@param userHandle}. This is a no-op
+     * for user handles that do not belong to a managed profile.
+     *
+     * @param userHandle Managed profile user id
+     * @param enabled True if separate challenge is enabled
+     * @param managedUserPassword Managed profile previous password. Null when {@param enabled} is
+     *            true
+     */
+    public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled,
+            String managedUserPassword) {
+        if (!isManagedProfile(userHandle)) {
+            return;
+        }
+        try {
+            getLockSettings().setSeparateProfileChallengeEnabled(userHandle, enabled,
+                    managedUserPassword);
+            onAfterChangingPassword(userHandle);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Couldn't update work profile challenge enabled");
+        }
+    }
+
+    /**
+     * Returns true if {@param userHandle} is a managed profile with separate challenge.
+     */
+    public boolean isSeparateProfileChallengeEnabled(int userHandle) {
+        return isManagedProfile(userHandle) && hasSeparateChallenge(userHandle);
+    }
+
+    /**
+     * Returns true if {@param userHandle} is a managed profile with unified challenge.
+     */
+    public boolean isManagedProfileWithUnifiedChallenge(int userHandle) {
+        return isManagedProfile(userHandle) && !hasSeparateChallenge(userHandle);
+    }
+
+    /**
+     * Retrieves whether the current DPM allows use of the Profile Challenge.
+     */
+    public boolean isSeparateProfileChallengeAllowed(int userHandle) {
+        return isManagedProfile(userHandle)
+                && getDevicePolicyManager().isSeparateProfileChallengeAllowed(userHandle);
+    }
+
+    /**
+     * Retrieves whether the current profile and device locks can be unified.
+     */
+    public boolean isSeparateProfileChallengeAllowedToUnify(int userHandle) {
+        return getDevicePolicyManager().isProfileActivePasswordSufficientForParent(userHandle);
+    }
+
+    private boolean hasSeparateChallenge(int userHandle) {
+        try {
+            return getLockSettings().getSeparateProfileChallengeEnabled(userHandle);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Couldn't get separate profile challenge enabled");
+            // Default value is false
+            return false;
+        }
+    }
+
+    private boolean isManagedProfile(int userHandle) {
+        final UserInfo info = getUserManager().getUserInfo(userHandle);
+        return info != null && info.isManagedProfile();
+    }
+
+    /**
+     * Deserialize a pattern.
+     * @param string The pattern serialized with {@link #patternToString}
+     * @return The pattern.
+     */
+    public static List<LockPatternView.Cell> stringToPattern(String string) {
+        if (string == null) {
+            return null;
+        }
+
+        List<LockPatternView.Cell> result = Lists.newArrayList();
+
+        final byte[] bytes = string.getBytes();
+        for (int i = 0; i < bytes.length; i++) {
+            byte b = (byte) (bytes[i] - '1');
+            result.add(LockPatternView.Cell.of(b / 3, b % 3));
+        }
+        return result;
+    }
+
+    /**
+     * Serialize a pattern.
+     * @param pattern The pattern.
+     * @return The pattern in string form.
+     */
+    public static String patternToString(List<LockPatternView.Cell> pattern) {
+        if (pattern == null) {
+            return "";
+        }
+        final int patternSize = pattern.size();
+
+        byte[] res = new byte[patternSize];
+        for (int i = 0; i < patternSize; i++) {
+            LockPatternView.Cell cell = pattern.get(i);
+            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn() + '1');
+        }
+        return new String(res);
+    }
+
+    public static String patternStringToBaseZero(String pattern) {
+        if (pattern == null) {
+            return "";
+        }
+        final int patternSize = pattern.length();
+
+        byte[] res = new byte[patternSize];
+        final byte[] bytes = pattern.getBytes();
+        for (int i = 0; i < patternSize; i++) {
+            res[i] = (byte) (bytes[i] - '1');
+        }
+        return new String(res);
+    }
+
+    /*
+     * Generate an SHA-1 hash for the pattern. Not the most secure, but it is
+     * at least a second level of protection. First level is that the file
+     * is in a location only readable by the system process.
+     * @param pattern the gesture pattern.
+     * @return the hash of the pattern in a byte array.
+     */
+    public static byte[] patternToHash(List<LockPatternView.Cell> pattern) {
+        if (pattern == null) {
+            return null;
+        }
+
+        final int patternSize = pattern.size();
+        byte[] res = new byte[patternSize];
+        for (int i = 0; i < patternSize; i++) {
+            LockPatternView.Cell cell = pattern.get(i);
+            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
+        }
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            byte[] hash = md.digest(res);
+            return hash;
+        } catch (NoSuchAlgorithmException nsa) {
+            return res;
+        }
+    }
+
+    private String getSalt(int userId) {
+        long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0, userId);
+        if (salt == 0) {
+            try {
+                salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
+                setLong(LOCK_PASSWORD_SALT_KEY, salt, userId);
+                Log.v(TAG, "Initialized lock password salt for user: " + userId);
+            } catch (NoSuchAlgorithmException e) {
+                // Throw an exception rather than storing a password we'll never be able to recover
+                throw new IllegalStateException("Couldn't get SecureRandom number", e);
+            }
+        }
+        return Long.toHexString(salt);
+    }
+
+    /*
+     * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash.
+     * Not the most secure, but it is at least a second level of protection. First level is that
+     * the file is in a location only readable by the system process.
+     *
+     * @param password the gesture pattern.
+     *
+     * @return the hash of the pattern in a byte array.
+     */
+    public byte[] passwordToHash(String password, int userId) {
+        if (password == null) {
+            return null;
+        }
+
+        try {
+            byte[] saltedPassword = (password + getSalt(userId)).getBytes();
+            byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
+            byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
+
+            byte[] combined = new byte[sha1.length + md5.length];
+            System.arraycopy(sha1, 0, combined, 0, sha1.length);
+            System.arraycopy(md5, 0, combined, sha1.length, md5.length);
+
+            final char[] hexEncoded = HexEncoding.encode(combined);
+            return new String(hexEncoded).getBytes(StandardCharsets.UTF_8);
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError("Missing digest algorithm: ", e);
+        }
+    }
+
+    /**
+     * @param userId the user for which to report the value
+     * @return Whether the lock screen is secured.
+     */
+    public boolean isSecure(int userId) {
+        int mode = getKeyguardStoredPasswordQuality(userId);
+        return isLockPatternEnabled(mode, userId) || isLockPasswordEnabled(mode, userId);
+    }
+
+    public boolean isLockPasswordEnabled(int userId) {
+        return isLockPasswordEnabled(getKeyguardStoredPasswordQuality(userId), userId);
+    }
+
+    private boolean isLockPasswordEnabled(int mode, int userId) {
+        final boolean passwordEnabled = mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX
+                || mode == DevicePolicyManager.PASSWORD_QUALITY_MANAGED;
+        return passwordEnabled && savedPasswordExists(userId);
+    }
+
+    /**
+     * @return Whether the lock pattern is enabled
+     */
+    public boolean isLockPatternEnabled(int userId) {
+        return isLockPatternEnabled(getKeyguardStoredPasswordQuality(userId), userId);
+    }
+
+    @Deprecated
+    public boolean isLegacyLockPatternEnabled(int userId) {
+        // Note: this value should default to {@code true} to avoid any reset that might result.
+        // We must use a special key to read this value, since it will by default return the value
+        // based on the new logic.
+        return getBoolean(LEGACY_LOCK_PATTERN_ENABLED, true, userId);
+    }
+
+    @Deprecated
+    public void setLegacyLockPatternEnabled(int userId) {
+        setBoolean(Settings.Secure.LOCK_PATTERN_ENABLED, true, userId);
+    }
+
+    private boolean isLockPatternEnabled(int mode, int userId) {
+        return mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+                && savedPatternExists(userId);
+    }
+
+    /**
+     * @return Whether the visible pattern is enabled.
+     */
+    public boolean isVisiblePatternEnabled(int userId) {
+        return getBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, false, userId);
+    }
+
+    /**
+     * Set whether the visible pattern is enabled.
+     */
+    public void setVisiblePatternEnabled(boolean enabled, int userId) {
+        setBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, enabled, userId);
+
+        // Update for crypto if owner
+        if (userId != UserHandle.USER_SYSTEM) {
+            return;
+        }
+
+        IBinder service = ServiceManager.getService("mount");
+        if (service == null) {
+            Log.e(TAG, "Could not find the mount service to update the user info");
+            return;
+        }
+
+        IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+        try {
+            storageManager.setField(StorageManager.PATTERN_VISIBLE_KEY, enabled ? "1" : "0");
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error changing pattern visible state", e);
+        }
+    }
+
+    public boolean isVisiblePatternEverChosen(int userId) {
+        return getString(Settings.Secure.LOCK_PATTERN_VISIBLE, userId) != null;
+    }
+
+    /**
+     * Set whether the visible password is enabled for cryptkeeper screen.
+     */
+    public void setVisiblePasswordEnabled(boolean enabled, int userId) {
+        // Update for crypto if owner
+        if (userId != UserHandle.USER_SYSTEM) {
+            return;
+        }
+
+        IBinder service = ServiceManager.getService("mount");
+        if (service == null) {
+            Log.e(TAG, "Could not find the mount service to update the user info");
+            return;
+        }
+
+        IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+        try {
+            storageManager.setField(StorageManager.PASSWORD_VISIBLE_KEY, enabled ? "1" : "0");
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error changing password visible state", e);
+        }
+    }
+
+    /**
+     * @return Whether tactile feedback for the pattern is enabled.
+     */
+    public boolean isTactileFeedbackEnabled() {
+        return Settings.System.getIntForUser(mContentResolver,
+                Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
+    }
+
+    /**
+     * Set and store the lockout deadline, meaning the user can't attempt his/her unlock
+     * pattern until the deadline has passed.
+     * @return the chosen deadline.
+     */
+    public long setLockoutAttemptDeadline(int userId, int timeoutMs) {
+        final long deadline = SystemClock.elapsedRealtime() + timeoutMs;
+        if (userId == USER_FRP) {
+            // For secure password storage (that is required for FRP), the underlying storage also
+            // enforces the deadline. Since we cannot store settings for the FRP user, don't.
+            return deadline;
+        }
+        setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline, userId);
+        setLong(LOCKOUT_ATTEMPT_TIMEOUT_MS, timeoutMs, userId);
+        return deadline;
+    }
+
+    /**
+     * @return The elapsed time in millis in the future when the user is allowed to
+     *   attempt to enter his/her lock pattern, or 0 if the user is welcome to
+     *   enter a pattern.
+     */
+    public long getLockoutAttemptDeadline(int userId) {
+        long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L, userId);
+        final long timeoutMs = getLong(LOCKOUT_ATTEMPT_TIMEOUT_MS, 0L, userId);
+        final long now = SystemClock.elapsedRealtime();
+        if (deadline < now && deadline != 0) {
+            // timeout expired
+            setLong(LOCKOUT_ATTEMPT_DEADLINE, 0, userId);
+            setLong(LOCKOUT_ATTEMPT_TIMEOUT_MS, 0, userId);
+            return 0L;
+        }
+
+        if (deadline > (now + timeoutMs)) {
+            // device was rebooted, set new deadline
+            deadline = now + timeoutMs;
+            setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline, userId);
+        }
+
+        return deadline;
+    }
+
+    private boolean getBoolean(String secureSettingKey, boolean defaultValue, int userId) {
+        try {
+            return getLockSettings().getBoolean(secureSettingKey, defaultValue, userId);
+        } catch (RemoteException re) {
+            return defaultValue;
+        }
+    }
+
+    private void setBoolean(String secureSettingKey, boolean enabled, int userId) {
+        try {
+            getLockSettings().setBoolean(secureSettingKey, enabled, userId);
+        } catch (RemoteException re) {
+            // What can we do?
+            Log.e(TAG, "Couldn't write boolean " + secureSettingKey + re);
+        }
+    }
+
+    private long getLong(String secureSettingKey, long defaultValue, int userHandle) {
+        try {
+            return getLockSettings().getLong(secureSettingKey, defaultValue, userHandle);
+        } catch (RemoteException re) {
+            return defaultValue;
+        }
+    }
+
+    private void setLong(String secureSettingKey, long value, int userHandle) {
+        try {
+            getLockSettings().setLong(secureSettingKey, value, userHandle);
+        } catch (RemoteException re) {
+            // What can we do?
+            Log.e(TAG, "Couldn't write long " + secureSettingKey + re);
+        }
+    }
+
+    private String getString(String secureSettingKey, int userHandle) {
+        try {
+            return getLockSettings().getString(secureSettingKey, null, userHandle);
+        } catch (RemoteException re) {
+            return null;
+        }
+    }
+
+    private void setString(String secureSettingKey, String value, int userHandle) {
+        try {
+            getLockSettings().setString(secureSettingKey, value, userHandle);
+        } catch (RemoteException re) {
+            // What can we do?
+            Log.e(TAG, "Couldn't write string " + secureSettingKey + re);
+        }
+    }
+
+    public void setPowerButtonInstantlyLocks(boolean enabled, int userId) {
+        setBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, enabled, userId);
+    }
+
+    public boolean getPowerButtonInstantlyLocks(int userId) {
+        return getBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, true, userId);
+    }
+
+    public boolean isPowerButtonInstantlyLocksEverChosen(int userId) {
+        return getString(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, userId) != null;
+    }
+
+    public void setEnabledTrustAgents(Collection<ComponentName> activeTrustAgents, int userId) {
+        StringBuilder sb = new StringBuilder();
+        for (ComponentName cn : activeTrustAgents) {
+            if (sb.length() > 0) {
+                sb.append(',');
+            }
+            sb.append(cn.flattenToShortString());
+        }
+        setString(ENABLED_TRUST_AGENTS, sb.toString(), userId);
+        getTrustManager().reportEnabledTrustAgentsChanged(userId);
+    }
+
+    public List<ComponentName> getEnabledTrustAgents(int userId) {
+        String serialized = getString(ENABLED_TRUST_AGENTS, userId);
+        if (TextUtils.isEmpty(serialized)) {
+            return null;
+        }
+        String[] split = serialized.split(",");
+        ArrayList<ComponentName> activeTrustAgents = new ArrayList<ComponentName>(split.length);
+        for (String s : split) {
+            if (!TextUtils.isEmpty(s)) {
+                activeTrustAgents.add(ComponentName.unflattenFromString(s));
+            }
+        }
+        return activeTrustAgents;
+    }
+
+    /**
+     * Disable trust until credentials have been entered for user {@param userId}.
+     *
+     * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+     *
+     * @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
+     */
+    public void requireCredentialEntry(int userId) {
+        requireStrongAuth(StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId);
+    }
+
+    /**
+     * Requests strong authentication for user {@param userId}.
+     *
+     * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+     *
+     * @param strongAuthReason a combination of {@link StrongAuthTracker.StrongAuthFlags} indicating
+     *                         the reason for and the strength of the requested authentication.
+     * @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
+     */
+    public void requireStrongAuth(@StrongAuthTracker.StrongAuthFlags int strongAuthReason,
+            int userId) {
+        try {
+            getLockSettings().requireStrongAuth(strongAuthReason, userId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error while requesting strong auth: " + e);
+        }
+    }
+
+    private void onAfterChangingPassword(int userHandle) {
+        getTrustManager().reportEnabledTrustAgentsChanged(userHandle);
+    }
+
+    public boolean isCredentialRequiredToDecrypt(boolean defaultValue) {
+        final int value = Settings.Global.getInt(mContentResolver,
+                Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, -1);
+        return value == -1 ? defaultValue : (value != 0);
+    }
+
+    public void setCredentialRequiredToDecrypt(boolean required) {
+        if (!(getUserManager().isSystemUser() || getUserManager().isPrimaryUser())) {
+            throw new IllegalStateException(
+                    "Only the system or primary user may call setCredentialRequiredForDecrypt()");
+        }
+
+        if (isDeviceEncryptionEnabled()){
+            Settings.Global.putInt(mContext.getContentResolver(),
+               Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, required ? 1 : 0);
+        }
+    }
+
+    private boolean isDoNotAskCredentialsOnBootSet() {
+        return getDevicePolicyManager().getDoNotAskCredentialsOnBoot();
+    }
+
+    private boolean shouldEncryptWithCredentials(boolean defaultValue) {
+        return isCredentialRequiredToDecrypt(defaultValue) && !isDoNotAskCredentialsOnBootSet();
+    }
+
+    private void throwIfCalledOnMainThread() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            throw new IllegalStateException("should not be called from the main thread.");
+        }
+    }
+
+    public void registerStrongAuthTracker(final StrongAuthTracker strongAuthTracker) {
+        try {
+            getLockSettings().registerStrongAuthTracker(strongAuthTracker.mStub);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Could not register StrongAuthTracker");
+        }
+    }
+
+    public void unregisterStrongAuthTracker(final StrongAuthTracker strongAuthTracker) {
+        try {
+            getLockSettings().unregisterStrongAuthTracker(strongAuthTracker.mStub);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not unregister StrongAuthTracker", e);
+        }
+    }
+
+    /**
+     * @see StrongAuthTracker#getStrongAuthForUser
+     */
+    public int getStrongAuthForUser(int userId) {
+        try {
+            return getLockSettings().getStrongAuthForUser(userId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not get StrongAuth", e);
+            return StrongAuthTracker.getDefaultFlags(mContext);
+        }
+    }
+
+    /**
+     * @see StrongAuthTracker#isTrustAllowedForUser
+     */
+    public boolean isTrustAllowedForUser(int userId) {
+        return getStrongAuthForUser(userId) == StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
+    }
+
+    /**
+     * @see StrongAuthTracker#isFingerprintAllowedForUser
+     */
+    public boolean isFingerprintAllowedForUser(int userId) {
+        return (getStrongAuthForUser(userId) & ~StrongAuthTracker.ALLOWING_FINGERPRINT) == 0;
+    }
+
+    private ICheckCredentialProgressCallback wrapCallback(
+            final CheckCredentialProgressCallback callback) {
+        if (callback == null) {
+            return null;
+        } else {
+            if (mHandler == null) {
+                throw new IllegalStateException("Must construct LockPatternUtils on a looper thread"
+                        + " to use progress callbacks.");
+            }
+            return new ICheckCredentialProgressCallback.Stub() {
+
+                @Override
+                public void onCredentialVerified() throws RemoteException {
+                    mHandler.post(callback::onEarlyMatched);
+                }
+            };
+        }
+    }
+
+    /**
+     * Create an escrow token for the current user, which can later be used to unlock FBE
+     * or change user password.
+     *
+     * After adding, if the user currently has lockscreen password, he will need to perform a
+     * confirm credential operation in order to activate the token for future use. If the user
+     * has no secure lockscreen, then the token is activated immediately.
+     *
+     * @return a unique 64-bit token handle which is needed to refer to this token later.
+     */
+    public long addEscrowToken(byte[] token, int userId) {
+        try {
+            return getLockSettings().addEscrowToken(token, userId);
+        } catch (RemoteException re) {
+            return 0L;
+        }
+    }
+
+    /**
+     * Remove an escrow token.
+     * @return true if the given handle refers to a valid token previously returned from
+     * {@link #addEscrowToken}, whether it's active or not. return false otherwise.
+     */
+    public boolean removeEscrowToken(long handle, int userId) {
+        try {
+            return getLockSettings().removeEscrowToken(handle, userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Check if the given escrow token is active or not. Only active token can be used to call
+     * {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
+     */
+    public boolean isEscrowTokenActive(long handle, int userId) {
+        try {
+            return getLockSettings().isEscrowTokenActive(handle, userId);
+        } catch (RemoteException re) {
+            return false;
+        }
+    }
+
+    /**
+     * Change a user's lock credential with a pre-configured escrow token.
+     *
+     * @param credential The new credential to be set
+     * @param type Credential type: password / pattern / none.
+     * @param requestedQuality the requested password quality by DevicePolicyManager.
+     *        See {@link DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
+     * @param tokenHandle Handle of the escrow token
+     * @param token Escrow token
+     * @param userId The user who's lock credential to be changed
+     * @return {@code true} if the operation is successful.
+     */
+    public boolean setLockCredentialWithToken(String credential, int type, int requestedQuality,
+            long tokenHandle, byte[] token, int userId) {
+        try {
+            if (type != CREDENTIAL_TYPE_NONE) {
+                if (TextUtils.isEmpty(credential) || credential.length() < MIN_LOCK_PASSWORD_SIZE) {
+                    throw new IllegalArgumentException("password must not be null and at least "
+                            + "of length " + MIN_LOCK_PASSWORD_SIZE);
+                }
+                final int quality = computePasswordQuality(type, credential, requestedQuality);
+                if (!getLockSettings().setLockCredentialWithToken(credential, type, tokenHandle,
+                        token, quality, userId)) {
+                    return false;
+                }
+                setLong(PASSWORD_TYPE_KEY, quality, userId);
+
+                updateEncryptionPasswordIfNeeded(credential, quality, userId);
+                updatePasswordHistory(credential, userId);
+            } else {
+                if (!TextUtils.isEmpty(credential)) {
+                    throw new IllegalArgumentException("password must be emtpy for NONE type");
+                }
+                if (!getLockSettings().setLockCredentialWithToken(null, CREDENTIAL_TYPE_NONE,
+                        tokenHandle, token, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
+                        userId)) {
+                    return false;
+                }
+                setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
+                        userId);
+
+                if (userId == UserHandle.USER_SYSTEM) {
+                    // Set the encryption password to default.
+                    updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+                    setCredentialRequiredToDecrypt(false);
+                }
+            }
+            onAfterChangingPassword(userId);
+            return true;
+        } catch (RemoteException re) {
+            Log.e(TAG, "Unable to save lock password ", re);
+            re.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+    public void unlockUserWithToken(long tokenHandle, byte[] token, int userId) {
+        try {
+            getLockSettings().unlockUserWithToken(tokenHandle, token, userId);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Unable to unlock user with token", re);
+            re.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Callback to be notified about progress when checking credentials.
+     */
+    public interface CheckCredentialProgressCallback {
+
+        /**
+         * Called as soon as possible when we know that the credentials match but the user hasn't
+         * been fully unlocked.
+         */
+        void onEarlyMatched();
+    }
+
+    /**
+     * Tracks the global strong authentication state.
+     */
+    public static class StrongAuthTracker {
+
+        @IntDef(flag = true,
+                value = { STRONG_AUTH_NOT_REQUIRED,
+                        STRONG_AUTH_REQUIRED_AFTER_BOOT,
+                        STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW,
+                        SOME_AUTH_REQUIRED_AFTER_USER_REQUEST,
+                        STRONG_AUTH_REQUIRED_AFTER_LOCKOUT,
+                        STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
+                        STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN})
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface StrongAuthFlags {}
+
+        /**
+         * Strong authentication is not required.
+         */
+        public static final int STRONG_AUTH_NOT_REQUIRED = 0x0;
+
+        /**
+         * Strong authentication is required because the user has not authenticated since boot.
+         */
+        public static final int STRONG_AUTH_REQUIRED_AFTER_BOOT = 0x1;
+
+        /**
+         * Strong authentication is required because a device admin has requested it.
+         */
+        public static final int STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW = 0x2;
+
+        /**
+         * Some authentication is required because the user has temporarily disabled trust.
+         */
+        public static final int SOME_AUTH_REQUIRED_AFTER_USER_REQUEST = 0x4;
+
+        /**
+         * Strong authentication is required because the user has been locked out after too many
+         * attempts.
+         */
+        public static final int STRONG_AUTH_REQUIRED_AFTER_LOCKOUT = 0x8;
+
+        /**
+         * Strong authentication is required because it hasn't been used for a time required by
+         * a device admin.
+         */
+        public static final int STRONG_AUTH_REQUIRED_AFTER_TIMEOUT = 0x10;
+
+        /**
+         * Strong authentication is required because the user has triggered lockdown.
+         */
+        public static final int STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN = 0x20;
+
+        /**
+         * Strong auth flags that do not prevent fingerprint from being accepted as auth.
+         *
+         * If any other flags are set, fingerprint is disabled.
+         */
+        private static final int ALLOWING_FINGERPRINT = STRONG_AUTH_NOT_REQUIRED
+                | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
+
+        private final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray();
+        private final H mHandler;
+        private final int mDefaultStrongAuthFlags;
+
+        public StrongAuthTracker(Context context) {
+            this(context, Looper.myLooper());
+        }
+
+        /**
+         * @param looper the looper on whose thread calls to {@link #onStrongAuthRequiredChanged}
+         *               will be scheduled.
+         * @param context the current {@link Context}
+         */
+        public StrongAuthTracker(Context context, Looper looper) {
+            mHandler = new H(looper);
+            mDefaultStrongAuthFlags = getDefaultFlags(context);
+        }
+
+        public static @StrongAuthFlags int getDefaultFlags(Context context) {
+            boolean strongAuthRequired = context.getResources().getBoolean(
+                    com.android.internal.R.bool.config_strongAuthRequiredOnBoot);
+            return strongAuthRequired ? STRONG_AUTH_REQUIRED_AFTER_BOOT : STRONG_AUTH_NOT_REQUIRED;
+        }
+
+        /**
+         * Returns {@link #STRONG_AUTH_NOT_REQUIRED} if strong authentication is not required,
+         * otherwise returns a combination of {@link StrongAuthFlags} indicating why strong
+         * authentication is required.
+         *
+         * @param userId the user for whom the state is queried.
+         */
+        public @StrongAuthFlags int getStrongAuthForUser(int userId) {
+            return mStrongAuthRequiredForUser.get(userId, mDefaultStrongAuthFlags);
+        }
+
+        /**
+         * @return true if unlocking with trust alone is allowed for {@param userId} by the current
+         * strong authentication requirements.
+         */
+        public boolean isTrustAllowedForUser(int userId) {
+            return getStrongAuthForUser(userId) == STRONG_AUTH_NOT_REQUIRED;
+        }
+
+        /**
+         * @return true if unlocking with fingerprint alone is allowed for {@param userId} by the
+         * current strong authentication requirements.
+         */
+        public boolean isFingerprintAllowedForUser(int userId) {
+            return (getStrongAuthForUser(userId) & ~ALLOWING_FINGERPRINT) == 0;
+        }
+
+        /**
+         * Called when the strong authentication requirements for {@param userId} changed.
+         */
+        public void onStrongAuthRequiredChanged(int userId) {
+        }
+
+        protected void handleStrongAuthRequiredChanged(@StrongAuthFlags int strongAuthFlags,
+                int userId) {
+            int oldValue = getStrongAuthForUser(userId);
+            if (strongAuthFlags != oldValue) {
+                if (strongAuthFlags == mDefaultStrongAuthFlags) {
+                    mStrongAuthRequiredForUser.delete(userId);
+                } else {
+                    mStrongAuthRequiredForUser.put(userId, strongAuthFlags);
+                }
+                onStrongAuthRequiredChanged(userId);
+            }
+        }
+
+
+        protected final IStrongAuthTracker.Stub mStub = new IStrongAuthTracker.Stub() {
+            @Override
+            public void onStrongAuthRequiredChanged(@StrongAuthFlags int strongAuthFlags,
+                    int userId) {
+                mHandler.obtainMessage(H.MSG_ON_STRONG_AUTH_REQUIRED_CHANGED,
+                        strongAuthFlags, userId).sendToTarget();
+            }
+        };
+
+        private class H extends Handler {
+            static final int MSG_ON_STRONG_AUTH_REQUIRED_CHANGED = 1;
+
+            public H(Looper looper) {
+                super(looper);
+            }
+
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_ON_STRONG_AUTH_REQUIRED_CHANGED:
+                        handleStrongAuthRequiredChanged(msg.arg1, msg.arg2);
+                        break;
+                }
+            }
+        }
+    }
+
+    public void enableSyntheticPassword() {
+        setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1L, UserHandle.USER_SYSTEM);
+    }
+
+    public void disableSyntheticPassword() {
+        setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0L, UserHandle.USER_SYSTEM);
+    }
+
+    public boolean isSyntheticPasswordEnabled() {
+        return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM) != 0;
+    }
+
+    public static boolean userOwnsFrpCredential(UserInfo info) {
+        return info != null && info.isPrimary() && info.isAdmin() && frpCredentialEnabled();
+    }
+
+    public static boolean frpCredentialEnabled() {
+        return FRP_CREDENTIAL_ENABLED;
+    }
+}
diff --git a/com/android/internal/widget/LockPatternView.java b/com/android/internal/widget/LockPatternView.java
new file mode 100644
index 0000000..32a7a2d
--- /dev/null
+++ b/com/android/internal/widget/LockPatternView.java
@@ -0,0 +1,1519 @@
+/*
+ * Copyright (C) 2007 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.drawable.Drawable;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.DisplayListCanvas;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.RenderNodeAnimator;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays and detects the user's unlock attempt, which is a drag of a finger
+ * across 9 regions of the screen.
+ *
+ * Is also capable of displaying a static pattern in "in progress", "wrong" or
+ * "correct" states.
+ */
+public class LockPatternView extends View {
+    // Aspect to use when rendering this view
+    private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height
+    private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)
+    private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)
+
+    private static final boolean PROFILE_DRAWING = false;
+    private final CellState[][] mCellStates;
+
+    private final int mDotSize;
+    private final int mDotSizeActivated;
+    private final int mPathWidth;
+
+    private boolean mDrawingProfilingStarted = false;
+
+    private final Paint mPaint = new Paint();
+    private final Paint mPathPaint = new Paint();
+
+    /**
+     * How many milliseconds we spend animating each circle of a lock pattern
+     * if the animating mode is set.  The entire animation should take this
+     * constant * the length of the pattern to complete.
+     */
+    private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
+
+    /**
+     * This can be used to avoid updating the display for very small motions or noisy panels.
+     * It didn't seem to have much impact on the devices tested, so currently set to 0.
+     */
+    private static final float DRAG_THRESHHOLD = 0.0f;
+    public static final int VIRTUAL_BASE_VIEW_ID = 1;
+    public static final boolean DEBUG_A11Y = false;
+    private static final String TAG = "LockPatternView";
+
+    private OnPatternListener mOnPatternListener;
+    private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
+
+    /**
+     * Lookup table for the circles of the pattern we are currently drawing.
+     * This will be the cells of the complete pattern unless we are animating,
+     * in which case we use this to hold the cells we are drawing for the in
+     * progress animation.
+     */
+    private final boolean[][] mPatternDrawLookup = new boolean[3][3];
+
+    /**
+     * the in progress point:
+     * - during interaction: where the user's finger is
+     * - during animation: the current tip of the animating line
+     */
+    private float mInProgressX = -1;
+    private float mInProgressY = -1;
+
+    private long mAnimatingPeriodStart;
+
+    private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
+    private boolean mInputEnabled = true;
+    private boolean mInStealthMode = false;
+    private boolean mEnableHapticFeedback = true;
+    private boolean mPatternInProgress = false;
+
+    private float mHitFactor = 0.6f;
+
+    private float mSquareWidth;
+    private float mSquareHeight;
+
+    private final Path mCurrentPath = new Path();
+    private final Rect mInvalidate = new Rect();
+    private final Rect mTmpInvalidateRect = new Rect();
+
+    private int mAspect;
+    private int mRegularColor;
+    private int mErrorColor;
+    private int mSuccessColor;
+
+    private final Interpolator mFastOutSlowInInterpolator;
+    private final Interpolator mLinearOutSlowInInterpolator;
+    private PatternExploreByTouchHelper mExploreByTouchHelper;
+    private AudioManager mAudioManager;
+
+    private Drawable mSelectedDrawable;
+    private Drawable mNotSelectedDrawable;
+    private boolean mUseLockPatternDrawable;
+
+    /**
+     * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
+     */
+    public static final class Cell {
+        final int row;
+        final int column;
+
+        // keep # objects limited to 9
+        private static final Cell[][] sCells = createCells();
+
+        private static Cell[][] createCells() {
+            Cell[][] res = new Cell[3][3];
+            for (int i = 0; i < 3; i++) {
+                for (int j = 0; j < 3; j++) {
+                    res[i][j] = new Cell(i, j);
+                }
+            }
+            return res;
+        }
+
+        /**
+         * @param row The row of the cell.
+         * @param column The column of the cell.
+         */
+        private Cell(int row, int column) {
+            checkRange(row, column);
+            this.row = row;
+            this.column = column;
+        }
+
+        public int getRow() {
+            return row;
+        }
+
+        public int getColumn() {
+            return column;
+        }
+
+        public static Cell of(int row, int column) {
+            checkRange(row, column);
+            return sCells[row][column];
+        }
+
+        private static void checkRange(int row, int column) {
+            if (row < 0 || row > 2) {
+                throw new IllegalArgumentException("row must be in range 0-2");
+            }
+            if (column < 0 || column > 2) {
+                throw new IllegalArgumentException("column must be in range 0-2");
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "(row=" + row + ",clmn=" + column + ")";
+        }
+    }
+
+    public static class CellState {
+        int row;
+        int col;
+        boolean hwAnimating;
+        CanvasProperty<Float> hwRadius;
+        CanvasProperty<Float> hwCenterX;
+        CanvasProperty<Float> hwCenterY;
+        CanvasProperty<Paint> hwPaint;
+        float radius;
+        float translationY;
+        float alpha = 1f;
+        public float lineEndX = Float.MIN_VALUE;
+        public float lineEndY = Float.MIN_VALUE;
+        public ValueAnimator lineAnimator;
+     }
+
+    /**
+     * How to display the current pattern.
+     */
+    public enum DisplayMode {
+
+        /**
+         * The pattern drawn is correct (i.e draw it in a friendly color)
+         */
+        Correct,
+
+        /**
+         * Animate the pattern (for demo, and help).
+         */
+        Animate,
+
+        /**
+         * The pattern is wrong (i.e draw a foreboding color)
+         */
+        Wrong
+    }
+
+    /**
+     * The call back interface for detecting patterns entered by the user.
+     */
+    public static interface OnPatternListener {
+
+        /**
+         * A new pattern has begun.
+         */
+        void onPatternStart();
+
+        /**
+         * The pattern was cleared.
+         */
+        void onPatternCleared();
+
+        /**
+         * The user extended the pattern currently being drawn by one cell.
+         * @param pattern The pattern with newly added cell.
+         */
+        void onPatternCellAdded(List<Cell> pattern);
+
+        /**
+         * A pattern was detected from the user.
+         * @param pattern The pattern.
+         */
+        void onPatternDetected(List<Cell> pattern);
+    }
+
+    public LockPatternView(Context context) {
+        this(context, null);
+    }
+
+    public LockPatternView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView,
+                R.attr.lockPatternStyle, R.style.Widget_LockPatternView);
+
+        final String aspect = a.getString(R.styleable.LockPatternView_aspect);
+
+        if ("square".equals(aspect)) {
+            mAspect = ASPECT_SQUARE;
+        } else if ("lock_width".equals(aspect)) {
+            mAspect = ASPECT_LOCK_WIDTH;
+        } else if ("lock_height".equals(aspect)) {
+            mAspect = ASPECT_LOCK_HEIGHT;
+        } else {
+            mAspect = ASPECT_SQUARE;
+        }
+
+        setClickable(true);
+
+
+        mPathPaint.setAntiAlias(true);
+        mPathPaint.setDither(true);
+
+        mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, 0);
+        mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, 0);
+        mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, 0);
+
+        int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor);
+        mPathPaint.setColor(pathColor);
+
+        mPathPaint.setStyle(Paint.Style.STROKE);
+        mPathPaint.setStrokeJoin(Paint.Join.ROUND);
+        mPathPaint.setStrokeCap(Paint.Cap.ROUND);
+
+        mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
+        mPathPaint.setStrokeWidth(mPathWidth);
+
+        mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
+        mDotSizeActivated = getResources().getDimensionPixelSize(
+                R.dimen.lock_pattern_dot_size_activated);
+
+        mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable);
+        if (mUseLockPatternDrawable) {
+            mSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_selected);
+            mNotSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_notselected);
+        }
+
+        mPaint.setAntiAlias(true);
+        mPaint.setDither(true);
+
+        mCellStates = new CellState[3][3];
+        for (int i = 0; i < 3; i++) {
+            for (int j = 0; j < 3; j++) {
+                mCellStates[i][j] = new CellState();
+                mCellStates[i][j].radius = mDotSize/2;
+                mCellStates[i][j].row = i;
+                mCellStates[i][j].col = j;
+            }
+        }
+
+        mFastOutSlowInInterpolator =
+                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
+        mLinearOutSlowInInterpolator =
+                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
+        mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
+        setAccessibilityDelegate(mExploreByTouchHelper);
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        a.recycle();
+    }
+
+    public CellState[][] getCellStates() {
+        return mCellStates;
+    }
+
+    /**
+     * @return Whether the view is in stealth mode.
+     */
+    public boolean isInStealthMode() {
+        return mInStealthMode;
+    }
+
+    /**
+     * @return Whether the view has tactile feedback enabled.
+     */
+    public boolean isTactileFeedbackEnabled() {
+        return mEnableHapticFeedback;
+    }
+
+    /**
+     * Set whether the view is in stealth mode.  If true, there will be no
+     * visible feedback as the user enters the pattern.
+     *
+     * @param inStealthMode Whether in stealth mode.
+     */
+    public void setInStealthMode(boolean inStealthMode) {
+        mInStealthMode = inStealthMode;
+    }
+
+    /**
+     * Set whether the view will use tactile feedback.  If true, there will be
+     * tactile feedback as the user enters the pattern.
+     *
+     * @param tactileFeedbackEnabled Whether tactile feedback is enabled
+     */
+    public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
+        mEnableHapticFeedback = tactileFeedbackEnabled;
+    }
+
+    /**
+     * Set the call back for pattern detection.
+     * @param onPatternListener The call back.
+     */
+    public void setOnPatternListener(
+            OnPatternListener onPatternListener) {
+        mOnPatternListener = onPatternListener;
+    }
+
+    /**
+     * Set the pattern explicitely (rather than waiting for the user to input
+     * a pattern).
+     * @param displayMode How to display the pattern.
+     * @param pattern The pattern.
+     */
+    public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
+        mPattern.clear();
+        mPattern.addAll(pattern);
+        clearPatternDrawLookup();
+        for (Cell cell : pattern) {
+            mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
+        }
+
+        setDisplayMode(displayMode);
+    }
+
+    /**
+     * Set the display mode of the current pattern.  This can be useful, for
+     * instance, after detecting a pattern to tell this view whether change the
+     * in progress result to correct or wrong.
+     * @param displayMode The display mode.
+     */
+    public void setDisplayMode(DisplayMode displayMode) {
+        mPatternDisplayMode = displayMode;
+        if (displayMode == DisplayMode.Animate) {
+            if (mPattern.size() == 0) {
+                throw new IllegalStateException("you must have a pattern to "
+                        + "animate if you want to set the display mode to animate");
+            }
+            mAnimatingPeriodStart = SystemClock.elapsedRealtime();
+            final Cell first = mPattern.get(0);
+            mInProgressX = getCenterXForColumn(first.getColumn());
+            mInProgressY = getCenterYForRow(first.getRow());
+            clearPatternDrawLookup();
+        }
+        invalidate();
+    }
+
+    public void startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha,
+            float startTranslationY, float endTranslationY, float startScale, float endScale,
+            long delay, long duration,
+            Interpolator interpolator, Runnable finishRunnable) {
+        if (isHardwareAccelerated()) {
+            startCellStateAnimationHw(cellState, startAlpha, endAlpha, startTranslationY,
+                    endTranslationY, startScale, endScale, delay, duration, interpolator,
+                    finishRunnable);
+        } else {
+            startCellStateAnimationSw(cellState, startAlpha, endAlpha, startTranslationY,
+                    endTranslationY, startScale, endScale, delay, duration, interpolator,
+                    finishRunnable);
+        }
+    }
+
+    private void startCellStateAnimationSw(final CellState cellState,
+            final float startAlpha, final float endAlpha,
+            final float startTranslationY, final float endTranslationY,
+            final float startScale, final float endScale,
+            long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) {
+        cellState.alpha = startAlpha;
+        cellState.translationY = startTranslationY;
+        cellState.radius = mDotSize/2 * startScale;
+        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+        animator.setDuration(duration);
+        animator.setStartDelay(delay);
+        animator.setInterpolator(interpolator);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float t = (float) animation.getAnimatedValue();
+                cellState.alpha = (1 - t) * startAlpha + t * endAlpha;
+                cellState.translationY = (1 - t) * startTranslationY + t * endTranslationY;
+                cellState.radius = mDotSize/2 * ((1 - t) * startScale + t * endScale);
+                invalidate();
+            }
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (finishRunnable != null) {
+                    finishRunnable.run();
+                }
+            }
+        });
+        animator.start();
+    }
+
+    private void startCellStateAnimationHw(final CellState cellState,
+            float startAlpha, float endAlpha,
+            float startTranslationY, float endTranslationY,
+            float startScale, float endScale,
+            long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) {
+        cellState.alpha = endAlpha;
+        cellState.translationY = endTranslationY;
+        cellState.radius = mDotSize/2 * endScale;
+        cellState.hwAnimating = true;
+        cellState.hwCenterY = CanvasProperty.createFloat(
+                getCenterYForRow(cellState.row) + startTranslationY);
+        cellState.hwCenterX = CanvasProperty.createFloat(getCenterXForColumn(cellState.col));
+        cellState.hwRadius = CanvasProperty.createFloat(mDotSize/2 * startScale);
+        mPaint.setColor(getCurrentColor(false));
+        mPaint.setAlpha((int) (startAlpha * 255));
+        cellState.hwPaint = CanvasProperty.createPaint(new Paint(mPaint));
+
+        startRtFloatAnimation(cellState.hwCenterY,
+                getCenterYForRow(cellState.row) + endTranslationY, delay, duration, interpolator);
+        startRtFloatAnimation(cellState.hwRadius, mDotSize/2 * endScale, delay, duration,
+                interpolator);
+        startRtAlphaAnimation(cellState, endAlpha, delay, duration, interpolator,
+                new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        cellState.hwAnimating = false;
+                        if (finishRunnable != null) {
+                            finishRunnable.run();
+                        }
+                    }
+                });
+
+        invalidate();
+    }
+
+    private void startRtAlphaAnimation(CellState cellState, float endAlpha,
+            long delay, long duration, Interpolator interpolator,
+            Animator.AnimatorListener listener) {
+        RenderNodeAnimator animator = new RenderNodeAnimator(cellState.hwPaint,
+                RenderNodeAnimator.PAINT_ALPHA, (int) (endAlpha * 255));
+        animator.setDuration(duration);
+        animator.setStartDelay(delay);
+        animator.setInterpolator(interpolator);
+        animator.setTarget(this);
+        animator.addListener(listener);
+        animator.start();
+    }
+
+    private void startRtFloatAnimation(CanvasProperty<Float> property, float endValue,
+            long delay, long duration, Interpolator interpolator) {
+        RenderNodeAnimator animator = new RenderNodeAnimator(property, endValue);
+        animator.setDuration(duration);
+        animator.setStartDelay(delay);
+        animator.setInterpolator(interpolator);
+        animator.setTarget(this);
+        animator.start();
+    }
+
+    private void notifyCellAdded() {
+        // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
+        if (mOnPatternListener != null) {
+            mOnPatternListener.onPatternCellAdded(mPattern);
+        }
+        // Disable used cells for accessibility as they get added
+        if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added.");
+        mExploreByTouchHelper.invalidateRoot();
+    }
+
+    private void notifyPatternStarted() {
+        sendAccessEvent(R.string.lockscreen_access_pattern_start);
+        if (mOnPatternListener != null) {
+            mOnPatternListener.onPatternStart();
+        }
+    }
+
+    private void notifyPatternDetected() {
+        sendAccessEvent(R.string.lockscreen_access_pattern_detected);
+        if (mOnPatternListener != null) {
+            mOnPatternListener.onPatternDetected(mPattern);
+        }
+    }
+
+    private void notifyPatternCleared() {
+        sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
+        if (mOnPatternListener != null) {
+            mOnPatternListener.onPatternCleared();
+        }
+    }
+
+    /**
+     * Clear the pattern.
+     */
+    public void clearPattern() {
+        resetPattern();
+    }
+
+    @Override
+    protected boolean dispatchHoverEvent(MotionEvent event) {
+        // Dispatch to onHoverEvent first so mPatternInProgress is up to date when the
+        // helper gets the event.
+        boolean handled = super.dispatchHoverEvent(event);
+        handled |= mExploreByTouchHelper.dispatchHoverEvent(event);
+        return handled;
+    }
+
+    /**
+     * Reset all pattern state.
+     */
+    private void resetPattern() {
+        mPattern.clear();
+        clearPatternDrawLookup();
+        mPatternDisplayMode = DisplayMode.Correct;
+        invalidate();
+    }
+
+    /**
+     * Clear the pattern lookup table.
+     */
+    private void clearPatternDrawLookup() {
+        for (int i = 0; i < 3; i++) {
+            for (int j = 0; j < 3; j++) {
+                mPatternDrawLookup[i][j] = false;
+            }
+        }
+    }
+
+    /**
+     * Disable input (for instance when displaying a message that will
+     * timeout so user doesn't get view into messy state).
+     */
+    public void disableInput() {
+        mInputEnabled = false;
+    }
+
+    /**
+     * Enable input.
+     */
+    public void enableInput() {
+        mInputEnabled = true;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        final int width = w - mPaddingLeft - mPaddingRight;
+        mSquareWidth = width / 3.0f;
+
+        if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
+        final int height = h - mPaddingTop - mPaddingBottom;
+        mSquareHeight = height / 3.0f;
+        mExploreByTouchHelper.invalidateRoot();
+
+        if (mUseLockPatternDrawable) {
+            mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
+            mSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
+        }
+    }
+
+    private int resolveMeasured(int measureSpec, int desired)
+    {
+        int result = 0;
+        int specSize = MeasureSpec.getSize(measureSpec);
+        switch (MeasureSpec.getMode(measureSpec)) {
+            case MeasureSpec.UNSPECIFIED:
+                result = desired;
+                break;
+            case MeasureSpec.AT_MOST:
+                result = Math.max(specSize, desired);
+                break;
+            case MeasureSpec.EXACTLY:
+            default:
+                result = specSize;
+        }
+        return result;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int minimumWidth = getSuggestedMinimumWidth();
+        final int minimumHeight = getSuggestedMinimumHeight();
+        int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
+        int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
+
+        switch (mAspect) {
+            case ASPECT_SQUARE:
+                viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
+                break;
+            case ASPECT_LOCK_WIDTH:
+                viewHeight = Math.min(viewWidth, viewHeight);
+                break;
+            case ASPECT_LOCK_HEIGHT:
+                viewWidth = Math.min(viewWidth, viewHeight);
+                break;
+        }
+        // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
+        setMeasuredDimension(viewWidth, viewHeight);
+    }
+
+    /**
+     * Determines whether the point x, y will add a new point to the current
+     * pattern (in addition to finding the cell, also makes heuristic choices
+     * such as filling in gaps based on current pattern).
+     * @param x The x coordinate.
+     * @param y The y coordinate.
+     */
+    private Cell detectAndAddHit(float x, float y) {
+        final Cell cell = checkForNewHit(x, y);
+        if (cell != null) {
+
+            // check for gaps in existing pattern
+            Cell fillInGapCell = null;
+            final ArrayList<Cell> pattern = mPattern;
+            if (!pattern.isEmpty()) {
+                final Cell lastCell = pattern.get(pattern.size() - 1);
+                int dRow = cell.row - lastCell.row;
+                int dColumn = cell.column - lastCell.column;
+
+                int fillInRow = lastCell.row;
+                int fillInColumn = lastCell.column;
+
+                if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
+                    fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
+                }
+
+                if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
+                    fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
+                }
+
+                fillInGapCell = Cell.of(fillInRow, fillInColumn);
+            }
+
+            if (fillInGapCell != null &&
+                    !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
+                addCellToPattern(fillInGapCell);
+            }
+            addCellToPattern(cell);
+            if (mEnableHapticFeedback) {
+                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+                        | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
+            }
+            return cell;
+        }
+        return null;
+    }
+
+    private void addCellToPattern(Cell newCell) {
+        mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
+        mPattern.add(newCell);
+        if (!mInStealthMode) {
+            startCellActivatedAnimation(newCell);
+        }
+        notifyCellAdded();
+    }
+
+    private void startCellActivatedAnimation(Cell cell) {
+        final CellState cellState = mCellStates[cell.row][cell.column];
+        startRadiusAnimation(mDotSize/2, mDotSizeActivated/2, 96, mLinearOutSlowInInterpolator,
+                cellState, new Runnable() {
+                    @Override
+                    public void run() {
+                        startRadiusAnimation(mDotSizeActivated/2, mDotSize/2, 192,
+                                mFastOutSlowInInterpolator,
+                                cellState, null);
+                    }
+                });
+        startLineEndAnimation(cellState, mInProgressX, mInProgressY,
+                getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
+    }
+
+    private void startLineEndAnimation(final CellState state,
+            final float startX, final float startY, final float targetX, final float targetY) {
+        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
+        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float t = (float) animation.getAnimatedValue();
+                state.lineEndX = (1 - t) * startX + t * targetX;
+                state.lineEndY = (1 - t) * startY + t * targetY;
+                invalidate();
+            }
+        });
+        valueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                state.lineAnimator = null;
+            }
+        });
+        valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
+        valueAnimator.setDuration(100);
+        valueAnimator.start();
+        state.lineAnimator = valueAnimator;
+    }
+
+    private void startRadiusAnimation(float start, float end, long duration,
+            Interpolator interpolator, final CellState state, final Runnable endRunnable) {
+        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
+        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                state.radius = (float) animation.getAnimatedValue();
+                invalidate();
+            }
+        });
+        if (endRunnable != null) {
+            valueAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    endRunnable.run();
+                }
+            });
+        }
+        valueAnimator.setInterpolator(interpolator);
+        valueAnimator.setDuration(duration);
+        valueAnimator.start();
+    }
+
+    // helper method to find which cell a point maps to
+    private Cell checkForNewHit(float x, float y) {
+
+        final int rowHit = getRowHit(y);
+        if (rowHit < 0) {
+            return null;
+        }
+        final int columnHit = getColumnHit(x);
+        if (columnHit < 0) {
+            return null;
+        }
+
+        if (mPatternDrawLookup[rowHit][columnHit]) {
+            return null;
+        }
+        return Cell.of(rowHit, columnHit);
+    }
+
+    /**
+     * Helper method to find the row that y falls into.
+     * @param y The y coordinate
+     * @return The row that y falls in, or -1 if it falls in no row.
+     */
+    private int getRowHit(float y) {
+
+        final float squareHeight = mSquareHeight;
+        float hitSize = squareHeight * mHitFactor;
+
+        float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
+        for (int i = 0; i < 3; i++) {
+
+            final float hitTop = offset + squareHeight * i;
+            if (y >= hitTop && y <= hitTop + hitSize) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Helper method to find the column x fallis into.
+     * @param x The x coordinate.
+     * @return The column that x falls in, or -1 if it falls in no column.
+     */
+    private int getColumnHit(float x) {
+        final float squareWidth = mSquareWidth;
+        float hitSize = squareWidth * mHitFactor;
+
+        float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
+        for (int i = 0; i < 3; i++) {
+
+            final float hitLeft = offset + squareWidth * i;
+            if (x >= hitLeft && x <= hitLeft + hitSize) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
+            final int action = event.getAction();
+            switch (action) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    event.setAction(MotionEvent.ACTION_DOWN);
+                    break;
+                case MotionEvent.ACTION_HOVER_MOVE:
+                    event.setAction(MotionEvent.ACTION_MOVE);
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    event.setAction(MotionEvent.ACTION_UP);
+                    break;
+            }
+            onTouchEvent(event);
+            event.setAction(action);
+        }
+        return super.onHoverEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mInputEnabled || !isEnabled()) {
+            return false;
+        }
+
+        switch(event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                handleActionDown(event);
+                return true;
+            case MotionEvent.ACTION_UP:
+                handleActionUp();
+                return true;
+            case MotionEvent.ACTION_MOVE:
+                handleActionMove(event);
+                return true;
+            case MotionEvent.ACTION_CANCEL:
+                if (mPatternInProgress) {
+                    setPatternInProgress(false);
+                    resetPattern();
+                    notifyPatternCleared();
+                }
+                if (PROFILE_DRAWING) {
+                    if (mDrawingProfilingStarted) {
+                        Debug.stopMethodTracing();
+                        mDrawingProfilingStarted = false;
+                    }
+                }
+                return true;
+        }
+        return false;
+    }
+
+    private void setPatternInProgress(boolean progress) {
+        mPatternInProgress = progress;
+        mExploreByTouchHelper.invalidateRoot();
+    }
+
+    private void handleActionMove(MotionEvent event) {
+        // Handle all recent motion events so we don't skip any cells even when the device
+        // is busy...
+        final float radius = mPathWidth;
+        final int historySize = event.getHistorySize();
+        mTmpInvalidateRect.setEmpty();
+        boolean invalidateNow = false;
+        for (int i = 0; i < historySize + 1; i++) {
+            final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
+            final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
+            Cell hitCell = detectAndAddHit(x, y);
+            final int patternSize = mPattern.size();
+            if (hitCell != null && patternSize == 1) {
+                setPatternInProgress(true);
+                notifyPatternStarted();
+            }
+            // note current x and y for rubber banding of in progress patterns
+            final float dx = Math.abs(x - mInProgressX);
+            final float dy = Math.abs(y - mInProgressY);
+            if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
+                invalidateNow = true;
+            }
+
+            if (mPatternInProgress && patternSize > 0) {
+                final ArrayList<Cell> pattern = mPattern;
+                final Cell lastCell = pattern.get(patternSize - 1);
+                float lastCellCenterX = getCenterXForColumn(lastCell.column);
+                float lastCellCenterY = getCenterYForRow(lastCell.row);
+
+                // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
+                float left = Math.min(lastCellCenterX, x) - radius;
+                float right = Math.max(lastCellCenterX, x) + radius;
+                float top = Math.min(lastCellCenterY, y) - radius;
+                float bottom = Math.max(lastCellCenterY, y) + radius;
+
+                // Invalidate between the pattern's new cell and the pattern's previous cell
+                if (hitCell != null) {
+                    final float width = mSquareWidth * 0.5f;
+                    final float height = mSquareHeight * 0.5f;
+                    final float hitCellCenterX = getCenterXForColumn(hitCell.column);
+                    final float hitCellCenterY = getCenterYForRow(hitCell.row);
+
+                    left = Math.min(hitCellCenterX - width, left);
+                    right = Math.max(hitCellCenterX + width, right);
+                    top = Math.min(hitCellCenterY - height, top);
+                    bottom = Math.max(hitCellCenterY + height, bottom);
+                }
+
+                // Invalidate between the pattern's last cell and the previous location
+                mTmpInvalidateRect.union(Math.round(left), Math.round(top),
+                        Math.round(right), Math.round(bottom));
+            }
+        }
+        mInProgressX = event.getX();
+        mInProgressY = event.getY();
+
+        // To save updates, we only invalidate if the user moved beyond a certain amount.
+        if (invalidateNow) {
+            mInvalidate.union(mTmpInvalidateRect);
+            invalidate(mInvalidate);
+            mInvalidate.set(mTmpInvalidateRect);
+        }
+    }
+
+    private void sendAccessEvent(int resId) {
+        announceForAccessibility(mContext.getString(resId));
+    }
+
+    private void handleActionUp() {
+        // report pattern detected
+        if (!mPattern.isEmpty()) {
+            setPatternInProgress(false);
+            cancelLineAnimations();
+            notifyPatternDetected();
+            invalidate();
+        }
+        if (PROFILE_DRAWING) {
+            if (mDrawingProfilingStarted) {
+                Debug.stopMethodTracing();
+                mDrawingProfilingStarted = false;
+            }
+        }
+    }
+
+    private void cancelLineAnimations() {
+        for (int i = 0; i < 3; i++) {
+            for (int j = 0; j < 3; j++) {
+                CellState state = mCellStates[i][j];
+                if (state.lineAnimator != null) {
+                    state.lineAnimator.cancel();
+                    state.lineEndX = Float.MIN_VALUE;
+                    state.lineEndY = Float.MIN_VALUE;
+                }
+            }
+        }
+    }
+    private void handleActionDown(MotionEvent event) {
+        resetPattern();
+        final float x = event.getX();
+        final float y = event.getY();
+        final Cell hitCell = detectAndAddHit(x, y);
+        if (hitCell != null) {
+            setPatternInProgress(true);
+            mPatternDisplayMode = DisplayMode.Correct;
+            notifyPatternStarted();
+        } else if (mPatternInProgress) {
+            setPatternInProgress(false);
+            notifyPatternCleared();
+        }
+        if (hitCell != null) {
+            final float startX = getCenterXForColumn(hitCell.column);
+            final float startY = getCenterYForRow(hitCell.row);
+
+            final float widthOffset = mSquareWidth / 2f;
+            final float heightOffset = mSquareHeight / 2f;
+
+            invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
+                    (int) (startX + widthOffset), (int) (startY + heightOffset));
+        }
+        mInProgressX = x;
+        mInProgressY = y;
+        if (PROFILE_DRAWING) {
+            if (!mDrawingProfilingStarted) {
+                Debug.startMethodTracing("LockPatternDrawing");
+                mDrawingProfilingStarted = true;
+            }
+        }
+    }
+
+    private float getCenterXForColumn(int column) {
+        return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
+    }
+
+    private float getCenterYForRow(int row) {
+        return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        final ArrayList<Cell> pattern = mPattern;
+        final int count = pattern.size();
+        final boolean[][] drawLookup = mPatternDrawLookup;
+
+        if (mPatternDisplayMode == DisplayMode.Animate) {
+
+            // figure out which circles to draw
+
+            // + 1 so we pause on complete pattern
+            final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
+            final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
+                    mAnimatingPeriodStart) % oneCycle;
+            final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
+
+            clearPatternDrawLookup();
+            for (int i = 0; i < numCircles; i++) {
+                final Cell cell = pattern.get(i);
+                drawLookup[cell.getRow()][cell.getColumn()] = true;
+            }
+
+            // figure out in progress portion of ghosting line
+
+            final boolean needToUpdateInProgressPoint = numCircles > 0
+                    && numCircles < count;
+
+            if (needToUpdateInProgressPoint) {
+                final float percentageOfNextCircle =
+                        ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
+                                MILLIS_PER_CIRCLE_ANIMATING;
+
+                final Cell currentCell = pattern.get(numCircles - 1);
+                final float centerX = getCenterXForColumn(currentCell.column);
+                final float centerY = getCenterYForRow(currentCell.row);
+
+                final Cell nextCell = pattern.get(numCircles);
+                final float dx = percentageOfNextCircle *
+                        (getCenterXForColumn(nextCell.column) - centerX);
+                final float dy = percentageOfNextCircle *
+                        (getCenterYForRow(nextCell.row) - centerY);
+                mInProgressX = centerX + dx;
+                mInProgressY = centerY + dy;
+            }
+            // TODO: Infinite loop here...
+            invalidate();
+        }
+
+        final Path currentPath = mCurrentPath;
+        currentPath.rewind();
+
+        // draw the circles
+        for (int i = 0; i < 3; i++) {
+            float centerY = getCenterYForRow(i);
+            for (int j = 0; j < 3; j++) {
+                CellState cellState = mCellStates[i][j];
+                float centerX = getCenterXForColumn(j);
+                float translationY = cellState.translationY;
+
+                if (mUseLockPatternDrawable) {
+                    drawCellDrawable(canvas, i, j, cellState.radius, drawLookup[i][j]);
+                } else {
+                    if (isHardwareAccelerated() && cellState.hwAnimating) {
+                        DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;
+                        displayListCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY,
+                                cellState.hwRadius, cellState.hwPaint);
+                    } else {
+                        drawCircle(canvas, (int) centerX, (int) centerY + translationY,
+                                cellState.radius, drawLookup[i][j], cellState.alpha);
+                    }
+                }
+            }
+        }
+
+        // TODO: the path should be created and cached every time we hit-detect a cell
+        // only the last segment of the path should be computed here
+        // draw the path of the pattern (unless we are in stealth mode)
+        final boolean drawPath = !mInStealthMode;
+
+        if (drawPath) {
+            mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
+
+            boolean anyCircles = false;
+            float lastX = 0f;
+            float lastY = 0f;
+            for (int i = 0; i < count; i++) {
+                Cell cell = pattern.get(i);
+
+                // only draw the part of the pattern stored in
+                // the lookup table (this is only different in the case
+                // of animation).
+                if (!drawLookup[cell.row][cell.column]) {
+                    break;
+                }
+                anyCircles = true;
+
+                float centerX = getCenterXForColumn(cell.column);
+                float centerY = getCenterYForRow(cell.row);
+                if (i != 0) {
+                    CellState state = mCellStates[cell.row][cell.column];
+                    currentPath.rewind();
+                    currentPath.moveTo(lastX, lastY);
+                    if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
+                        currentPath.lineTo(state.lineEndX, state.lineEndY);
+                    } else {
+                        currentPath.lineTo(centerX, centerY);
+                    }
+                    canvas.drawPath(currentPath, mPathPaint);
+                }
+                lastX = centerX;
+                lastY = centerY;
+            }
+
+            // draw last in progress section
+            if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
+                    && anyCircles) {
+                currentPath.rewind();
+                currentPath.moveTo(lastX, lastY);
+                currentPath.lineTo(mInProgressX, mInProgressY);
+
+                mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
+                        mInProgressX, mInProgressY, lastX, lastY) * 255f));
+                canvas.drawPath(currentPath, mPathPaint);
+            }
+        }
+    }
+
+    private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
+        float diffX = x - lastX;
+        float diffY = y - lastY;
+        float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
+        float frac = dist/mSquareWidth;
+        return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
+    }
+
+    private int getCurrentColor(boolean partOfPattern) {
+        if (!partOfPattern || mInStealthMode || mPatternInProgress) {
+            // unselected circle
+            return mRegularColor;
+        } else if (mPatternDisplayMode == DisplayMode.Wrong) {
+            // the pattern is wrong
+            return mErrorColor;
+        } else if (mPatternDisplayMode == DisplayMode.Correct ||
+                mPatternDisplayMode == DisplayMode.Animate) {
+            return mSuccessColor;
+        } else {
+            throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
+        }
+    }
+
+    /**
+     * @param partOfPattern Whether this circle is part of the pattern.
+     */
+    private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,
+            boolean partOfPattern, float alpha) {
+        mPaint.setColor(getCurrentColor(partOfPattern));
+        mPaint.setAlpha((int) (alpha * 255));
+        canvas.drawCircle(centerX, centerY, radius, mPaint);
+    }
+
+    /**
+     * @param partOfPattern Whether this circle is part of the pattern.
+     */
+    private void drawCellDrawable(Canvas canvas, int i, int j, float radius,
+            boolean partOfPattern) {
+        Rect dst = new Rect(
+            (int) (mPaddingLeft + j * mSquareWidth),
+            (int) (mPaddingTop + i * mSquareHeight),
+            (int) (mPaddingLeft + (j + 1) * mSquareWidth),
+            (int) (mPaddingTop + (i + 1) * mSquareHeight));
+        float scale = radius / (mDotSize / 2);
+
+        // Only draw on this square with the appropriate scale.
+        canvas.save();
+        canvas.clipRect(dst);
+        canvas.scale(scale, scale, dst.centerX(), dst.centerY());
+        if (!partOfPattern || scale > 1) {
+            mNotSelectedDrawable.draw(canvas);
+        } else {
+            mSelectedDrawable.draw(canvas);
+        }
+        canvas.restore();
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        return new SavedState(superState,
+                LockPatternUtils.patternToString(mPattern),
+                mPatternDisplayMode.ordinal(),
+                mInputEnabled, mInStealthMode, mEnableHapticFeedback);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        final SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        setPattern(
+                DisplayMode.Correct,
+                LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
+        mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
+        mInputEnabled = ss.isInputEnabled();
+        mInStealthMode = ss.isInStealthMode();
+        mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
+    }
+
+    /**
+     * The parecelable for saving and restoring a lock pattern view.
+     */
+    private static class SavedState extends BaseSavedState {
+
+        private final String mSerializedPattern;
+        private final int mDisplayMode;
+        private final boolean mInputEnabled;
+        private final boolean mInStealthMode;
+        private final boolean mTactileFeedbackEnabled;
+
+        /**
+         * Constructor called from {@link LockPatternView#onSaveInstanceState()}
+         */
+        private SavedState(Parcelable superState, String serializedPattern, int displayMode,
+                boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
+            super(superState);
+            mSerializedPattern = serializedPattern;
+            mDisplayMode = displayMode;
+            mInputEnabled = inputEnabled;
+            mInStealthMode = inStealthMode;
+            mTactileFeedbackEnabled = tactileFeedbackEnabled;
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            mSerializedPattern = in.readString();
+            mDisplayMode = in.readInt();
+            mInputEnabled = (Boolean) in.readValue(null);
+            mInStealthMode = (Boolean) in.readValue(null);
+            mTactileFeedbackEnabled = (Boolean) in.readValue(null);
+        }
+
+        public String getSerializedPattern() {
+            return mSerializedPattern;
+        }
+
+        public int getDisplayMode() {
+            return mDisplayMode;
+        }
+
+        public boolean isInputEnabled() {
+            return mInputEnabled;
+        }
+
+        public boolean isInStealthMode() {
+            return mInStealthMode;
+        }
+
+        public boolean isTactileFeedbackEnabled(){
+            return mTactileFeedbackEnabled;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeString(mSerializedPattern);
+            dest.writeInt(mDisplayMode);
+            dest.writeValue(mInputEnabled);
+            dest.writeValue(mInStealthMode);
+            dest.writeValue(mTactileFeedbackEnabled);
+        }
+
+        @SuppressWarnings({ "unused", "hiding" }) // Found using reflection
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
+        private Rect mTempRect = new Rect();
+        private final SparseArray<VirtualViewContainer> mItems = new SparseArray<>();
+
+        class VirtualViewContainer {
+            public VirtualViewContainer(CharSequence description) {
+                this.description = description;
+            }
+            CharSequence description;
+        };
+
+        public PatternExploreByTouchHelper(View forView) {
+            super(forView);
+            for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
+                mItems.put(i, new VirtualViewContainer(getTextForVirtualView(i)));
+            }
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            // This must use the same hit logic for the screen to ensure consistency whether
+            // accessibility is on or off.
+            int id = getVirtualViewIdForHit(x, y);
+            return id;
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+            if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
+            if (!mPatternInProgress) {
+                return;
+            }
+            for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
+                // Add all views. As views are added to the pattern, we remove them
+                // from notification by making them non-clickable below.
+                virtualViewIds.add(i);
+            }
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+            if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
+            // Announce this view
+            VirtualViewContainer container = mItems.get(virtualViewId);
+            if (container != null) {
+                event.getText().add(container.description);
+            }
+        }
+
+        @Override
+        public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+            super.onPopulateAccessibilityEvent(host, event);
+            if (!mPatternInProgress) {
+                CharSequence contentDescription = getContext().getText(
+                        com.android.internal.R.string.lockscreen_access_pattern_area);
+                event.setContentDescription(contentDescription);
+            }
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+            if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");
+
+            // Node and event text and content descriptions are usually
+            // identical, so we'll use the exact same string as before.
+            node.setText(getTextForVirtualView(virtualViewId));
+            node.setContentDescription(getTextForVirtualView(virtualViewId));
+
+            if (mPatternInProgress) {
+                node.setFocusable(true);
+
+                if (isClickable(virtualViewId)) {
+                    // Mark this node of interest by making it clickable.
+                    node.addAction(AccessibilityAction.ACTION_CLICK);
+                    node.setClickable(isClickable(virtualViewId));
+                }
+            }
+
+            // Compute bounds for this object
+            final Rect bounds = getBoundsForVirtualView(virtualViewId);
+            if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
+            node.setBoundsInParent(bounds);
+        }
+
+        private boolean isClickable(int virtualViewId) {
+            // Dots are clickable if they're not part of the current pattern.
+            if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
+                int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
+                int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
+                return !mPatternDrawLookup[row][col];
+            }
+            return false;
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+                Bundle arguments) {
+            if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
+                    + ", action=" + action);
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_CLICK:
+                    // Click handling should be consistent with
+                    // onTouchEvent(). This ensures that the view works the
+                    // same whether accessibility is turned on or off.
+                    return onItemClicked(virtualViewId);
+                default:
+                    if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
+                            + "onPerformActionForVirtualView(viewId="
+                            + virtualViewId + "action=" + action + ")");
+            }
+            return false;
+        }
+
+        boolean onItemClicked(int index) {
+            if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");
+
+            // Since the item's checked state is exposed to accessibility
+            // services through its AccessibilityNodeInfo, we need to invalidate
+            // the item's virtual view. At some point in the future, the
+            // framework will obtain an updated version of the virtual view.
+            invalidateVirtualView(index);
+
+            // We need to let the framework know what type of event
+            // happened. Accessibility services may use this event to provide
+            // appropriate feedback to the user.
+            sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+            return true;
+        }
+
+        private Rect getBoundsForVirtualView(int virtualViewId) {
+            int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
+            final Rect bounds = mTempRect;
+            final int row = ordinal / 3;
+            final int col = ordinal % 3;
+            final CellState cell = mCellStates[row][col];
+            float centerX = getCenterXForColumn(col);
+            float centerY = getCenterYForRow(row);
+            float cellheight = mSquareHeight * mHitFactor * 0.5f;
+            float cellwidth = mSquareWidth * mHitFactor * 0.5f;
+            bounds.left = (int) (centerX - cellwidth);
+            bounds.right = (int) (centerX + cellwidth);
+            bounds.top = (int) (centerY - cellheight);
+            bounds.bottom = (int) (centerY + cellheight);
+            return bounds;
+        }
+
+        private CharSequence getTextForVirtualView(int virtualViewId) {
+            final Resources res = getResources();
+            return res.getString(R.string.lockscreen_access_pattern_cell_added_verbose,
+                    virtualViewId);
+        }
+
+        /**
+         * Helper method to find which cell a point maps to
+         *
+         * if there's no hit.
+         * @param x touch position x
+         * @param y touch position y
+         * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
+         */
+        private int getVirtualViewIdForHit(float x, float y) {
+            final int rowHit = getRowHit(y);
+            if (rowHit < 0) {
+                return ExploreByTouchHelper.INVALID_ID;
+            }
+            final int columnHit = getColumnHit(x);
+            if (columnHit < 0) {
+                return ExploreByTouchHelper.INVALID_ID;
+            }
+            boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
+            int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
+            int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
+            if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
+                    + view + "avail =" + dotAvailable);
+            return view;
+        }
+    }
+}
diff --git a/com/android/internal/widget/LockScreenWidgetCallback.java b/com/android/internal/widget/LockScreenWidgetCallback.java
new file mode 100644
index 0000000..d7ad6c0
--- /dev/null
+++ b/com/android/internal/widget/LockScreenWidgetCallback.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+import android.view.View;
+
+/**
+ * An interface used by LockScreenWidgets to send messages to lock screen.
+ */
+public interface LockScreenWidgetCallback {
+    // Sends a message to lock screen requesting the given view be shown.  May be ignored, depending
+    // on lock screen state. View must be the top-level lock screen widget or it will be ignored.
+    public void requestShow(View self);
+
+    // Sends a message to lock screen requesting the view to be hidden.
+    public void requestHide(View self);
+
+    // Whether or not this view is currently visible on LockScreen
+    public boolean isVisible(View self);
+
+    // Sends a message to lock screen that user has interacted with widget. This should be used
+    // exclusively in response to user activity, i.e. user hits a button in the view.
+    public void userActivity(View self);
+
+}
diff --git a/com/android/internal/widget/LockScreenWidgetInterface.java b/com/android/internal/widget/LockScreenWidgetInterface.java
new file mode 100644
index 0000000..8f80cfc
--- /dev/null
+++ b/com/android/internal/widget/LockScreenWidgetInterface.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+public interface LockScreenWidgetInterface {
+
+    public void setCallback(LockScreenWidgetCallback callback);
+
+    public boolean providesClock();
+
+}
diff --git a/com/android/internal/widget/MediaNotificationView.java b/com/android/internal/widget/MediaNotificationView.java
new file mode 100644
index 0000000..7609b67
--- /dev/null
+++ b/com/android/internal/widget/MediaNotificationView.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+/**
+ * A TextView that can float around an image on the end.
+ *
+ * @hide
+ */
[email protected]
+public class MediaNotificationView extends FrameLayout {
+
+    private final int mNotificationContentMarginEnd;
+    private final int mNotificationContentImageMarginEnd;
+    private ImageView mRightIcon;
+    private View mActions;
+    private View mHeader;
+    private View mMainColumn;
+    private int mImagePushIn;
+
+    public MediaNotificationView(Context context) {
+        this(context, null);
+    }
+
+    public MediaNotificationView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MediaNotificationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        boolean hasIcon = mRightIcon.getVisibility() != GONE;
+        if (!hasIcon) {
+            resetHeaderIndention();
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int mode = MeasureSpec.getMode(widthMeasureSpec);
+        boolean reMeasure = false;
+        mImagePushIn = 0;
+        if (hasIcon && mode != MeasureSpec.UNSPECIFIED) {
+            int size = MeasureSpec.getSize(widthMeasureSpec);
+            size = size - mActions.getMeasuredWidth();
+            ViewGroup.MarginLayoutParams layoutParams =
+                    (MarginLayoutParams) mRightIcon.getLayoutParams();
+            int imageEndMargin = layoutParams.getMarginEnd();
+            size -= imageEndMargin;
+            int fullHeight = getMeasuredHeight();
+            if (size > fullHeight) {
+                size = fullHeight;
+            } else if (size < fullHeight) {
+                size = Math.max(0, size);
+                mImagePushIn = fullHeight - size;
+            }
+            if (layoutParams.width != fullHeight || layoutParams.height != fullHeight) {
+                layoutParams.width = fullHeight;
+                layoutParams.height = fullHeight;
+                mRightIcon.setLayoutParams(layoutParams);
+                reMeasure = true;
+            }
+
+            // lets ensure that the main column doesn't run into the image
+            ViewGroup.MarginLayoutParams params
+                    = (MarginLayoutParams) mMainColumn.getLayoutParams();
+            int marginEnd = size + imageEndMargin + mNotificationContentMarginEnd;
+            if (marginEnd != params.getMarginEnd()) {
+                params.setMarginEnd(marginEnd);
+                mMainColumn.setLayoutParams(params);
+                reMeasure = true;
+            }
+            int headerMarginEnd = size + imageEndMargin;
+            params = (MarginLayoutParams) mHeader.getLayoutParams();
+            if (params.getMarginEnd() != headerMarginEnd) {
+                params.setMarginEnd(headerMarginEnd);
+                mHeader.setLayoutParams(params);
+                reMeasure = true;
+            }
+            if (mHeader.getPaddingEnd() != mNotificationContentImageMarginEnd) {
+                mHeader.setPaddingRelative(mHeader.getPaddingStart(),
+                        mHeader.getPaddingTop(),
+                        mNotificationContentImageMarginEnd,
+                        mHeader.getPaddingBottom());
+                reMeasure = true;
+            }
+        }
+        if (reMeasure) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (mImagePushIn > 0) {
+            mRightIcon.layout(mRightIcon.getLeft() + mImagePushIn, mRightIcon.getTop(),
+                    mRightIcon.getRight()  + mImagePushIn, mRightIcon.getBottom());
+        }
+    }
+
+    private void resetHeaderIndention() {
+        if (mHeader.getPaddingEnd() != mNotificationContentMarginEnd) {
+            mHeader.setPaddingRelative(mHeader.getPaddingStart(),
+                    mHeader.getPaddingTop(),
+                    mNotificationContentMarginEnd,
+                    mHeader.getPaddingBottom());
+        }
+        ViewGroup.MarginLayoutParams headerParams =
+                (MarginLayoutParams) mHeader.getLayoutParams();
+        headerParams.setMarginEnd(0);
+        if (headerParams.getMarginEnd() != 0) {
+            headerParams.setMarginEnd(0);
+            mHeader.setLayoutParams(headerParams);
+        }
+    }
+
+    public MediaNotificationView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mNotificationContentMarginEnd = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_content_margin_end);
+        mNotificationContentImageMarginEnd = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_content_image_margin_end);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mRightIcon = findViewById(com.android.internal.R.id.right_icon);
+        mActions = findViewById(com.android.internal.R.id.media_actions);
+        mHeader = findViewById(com.android.internal.R.id.notification_header);
+        mMainColumn = findViewById(com.android.internal.R.id.notification_main_column);
+    }
+}
diff --git a/com/android/internal/widget/MessagingLinearLayout.java b/com/android/internal/widget/MessagingLinearLayout.java
new file mode 100644
index 0000000..70473a0
--- /dev/null
+++ b/com/android/internal/widget/MessagingLinearLayout.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle.
+ *
+ * Evicts children until they all fit.
+ */
[email protected]
+public class MessagingLinearLayout extends ViewGroup {
+
+    private static final int NOT_MEASURED_BEFORE = -1;
+    /**
+     * Spacing to be applied between views.
+     */
+    private int mSpacing;
+
+    /**
+     * The maximum height allowed.
+     */
+    private int mMaxHeight;
+
+    private int mIndentLines;
+
+    /**
+     * Id of the child that's also visible in the contracted layout.
+     */
+    private int mContractedChildId;
+    /**
+     * The last measured with in a layout pass if it was measured before or
+     * {@link #NOT_MEASURED_BEFORE} if this is the first layout pass.
+     */
+    private int mLastMeasuredWidth = NOT_MEASURED_BEFORE;
+
+    public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.MessagingLinearLayout, 0,
+                0);
+
+        final int N = a.getIndexCount();
+        for (int i = 0; i < N; i++) {
+            int attr = a.getIndex(i);
+            switch (attr) {
+                case R.styleable.MessagingLinearLayout_spacing:
+                    mSpacing = a.getDimensionPixelSize(i, 0);
+                    break;
+            }
+        }
+
+        a.recycle();
+    }
+
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // This is essentially a bottom-up linear layout that only adds children that fit entirely
+        // up to a maximum height.
+        int targetHeight = MeasureSpec.getSize(heightMeasureSpec);
+        switch (MeasureSpec.getMode(heightMeasureSpec)) {
+            case MeasureSpec.UNSPECIFIED:
+                targetHeight = Integer.MAX_VALUE;
+                break;
+        }
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        boolean recalculateVisibility = mLastMeasuredWidth == NOT_MEASURED_BEFORE
+                || getMeasuredHeight() != targetHeight
+                || mLastMeasuredWidth != widthSize;
+
+        final int count = getChildCount();
+        if (recalculateVisibility) {
+            // We only need to recalculate the view visibilities if the view wasn't measured already
+            // in this pass, otherwise we may drop messages here already since we are measured
+            // exactly with what we returned before, which was optimized already with the
+            // line-indents.
+            for (int i = 0; i < count; ++i) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                lp.hide = true;
+            }
+
+            int totalHeight = mPaddingTop + mPaddingBottom;
+            boolean first = true;
+
+            // Starting from the bottom: we measure every view as if it were the only one. If it still
+
+            // fits, we take it, otherwise we stop there.
+            for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) {
+                if (getChildAt(i).getVisibility() == GONE) {
+                    continue;
+                }
+                final View child = getChildAt(i);
+                LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+                ImageFloatingTextView textChild = null;
+                if (child instanceof ImageFloatingTextView) {
+                    // Pretend we need the image padding for all views, we don't know which
+                    // one will end up needing to do this (might end up not using all the space,
+                    // but calculating this exactly would be more expensive).
+                    textChild = (ImageFloatingTextView) child;
+                    textChild.setNumIndentLines(mIndentLines == 2 ? 3 : mIndentLines);
+                }
+
+                int spacing = first ? 0 : mSpacing;
+                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
+                        - mPaddingTop - mPaddingBottom + spacing);
+
+                final int childHeight = child.getMeasuredHeight();
+                int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
+                        lp.bottomMargin + spacing);
+                first = false;
+                boolean measuredTooSmall = false;
+                if (textChild != null) {
+                    measuredTooSmall = childHeight < textChild.getLayoutHeight()
+                            + textChild.getPaddingTop() + textChild.getPaddingBottom();
+                }
+
+                if (newHeight <= targetHeight && !measuredTooSmall) {
+                    totalHeight = newHeight;
+                    lp.hide = false;
+                } else {
+                    break;
+                }
+            }
+        }
+
+        // Now that we know which views to take, fix up the indents and see what width we get.
+        int measuredWidth = mPaddingLeft + mPaddingRight;
+        int imageLines = mIndentLines;
+        // Need to redo the height because it may change due to changing indents.
+        int totalHeight = mPaddingTop + mPaddingBottom;
+        boolean first = true;
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            if (child.getVisibility() == GONE || lp.hide) {
+                continue;
+            }
+
+            if (child instanceof ImageFloatingTextView) {
+                ImageFloatingTextView textChild = (ImageFloatingTextView) child;
+                if (imageLines == 2 && textChild.getLineCount() > 2) {
+                    // HACK: If we need indent for two lines, and they're coming from the same
+                    // view, we need extra spacing to compensate for the lack of margins,
+                    // so add an extra line of indent.
+                    imageLines = 3;
+                }
+                boolean changed = textChild.setNumIndentLines(Math.max(0, imageLines));
+                if (changed || !recalculateVisibility) {
+                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
+                            lp.width);
+                    // we want to measure it at most as high as it is currently, otherwise we'll
+                    // drop later lines
+                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                            targetHeight - child.getMeasuredHeight(), lp.height);
+
+                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);;
+                }
+                imageLines -= textChild.getLineCount();
+            }
+
+            measuredWidth = Math.max(measuredWidth,
+                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
+                            + mPaddingLeft + mPaddingRight);
+            totalHeight = Math.max(totalHeight, totalHeight + child.getMeasuredHeight() +
+                    lp.topMargin + lp.bottomMargin + (first ? 0 : mSpacing));
+            first = false;
+        }
+
+
+        setMeasuredDimension(
+                resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
+                        widthMeasureSpec),
+                resolveSize(Math.max(getSuggestedMinimumHeight(), totalHeight),
+                        heightMeasureSpec));
+        mLastMeasuredWidth = widthSize;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int paddingLeft = mPaddingLeft;
+
+        int childTop;
+
+        // Where right end of child should go
+        final int width = right - left;
+        final int childRight = width - mPaddingRight;
+
+        final int layoutDirection = getLayoutDirection();
+        final int count = getChildCount();
+
+        childTop = mPaddingTop;
+
+        boolean first = true;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            if (child.getVisibility() == GONE || lp.hide) {
+                continue;
+            }
+
+            final int childWidth = child.getMeasuredWidth();
+            final int childHeight = child.getMeasuredHeight();
+
+            int childLeft;
+            if (layoutDirection == LAYOUT_DIRECTION_RTL) {
+                childLeft = childRight - childWidth - lp.rightMargin;
+            } else {
+                childLeft = paddingLeft + lp.leftMargin;
+            }
+
+            if (!first) {
+                childTop += mSpacing;
+            }
+
+            childTop += lp.topMargin;
+            child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+
+            childTop += childHeight + lp.bottomMargin;
+
+            first = false;
+        }
+        mLastMeasuredWidth = NOT_MEASURED_BEFORE;
+    }
+
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        if (lp.hide) {
+            return true;
+        }
+        return super.drawChild(canvas, child, drawingTime);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(mContext, attrs);
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        LayoutParams copy = new LayoutParams(lp.width, lp.height);
+        if (lp instanceof MarginLayoutParams) {
+            copy.copyMarginsFrom((MarginLayoutParams) lp);
+        }
+        return copy;
+    }
+
+    /**
+     * Sets how many lines should be indented to avoid a floating image.
+     */
+    @RemotableViewMethod
+    public void setNumIndentLines(int numberLines) {
+        mIndentLines = numberLines;
+    }
+
+    /**
+     * Set id of the child that's also visible in the contracted layout.
+     */
+    @RemotableViewMethod
+    public void setContractedChildId(int contractedChildId) {
+        mContractedChildId = contractedChildId;
+    }
+
+    /**
+     * Get id of the child that's also visible in the contracted layout.
+     */
+    public int getContractedChildId() {
+        return mContractedChildId;
+    }
+
+    public static class LayoutParams extends MarginLayoutParams {
+
+        boolean hide = false;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+    }
+}
diff --git a/com/android/internal/widget/NestedScrollingChild.java b/com/android/internal/widget/NestedScrollingChild.java
new file mode 100644
index 0000000..20285b5
--- /dev/null
+++ b/com/android/internal/widget/NestedScrollingChild.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+
+/**
+ * This interface should be implemented by {@link android.view.View View} subclasses that wish
+ * to support dispatching nested scrolling operations to a cooperating parent
+ * {@link android.view.ViewGroup ViewGroup}.
+ *
+ * <p>Classes implementing this interface should create a final instance of a
+ * {@link NestedScrollingChildHelper} as a field and delegate any View methods to the
+ * <code>NestedScrollingChildHelper</code> methods of the same signature.</p>
+ *
+ * <p>Views invoking nested scrolling functionality should always do so from the relevant
+ * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility
+ * shim static methods. This ensures interoperability with nested scrolling views on Android
+ * 5.0 Lollipop and newer.</p>
+ */
+public interface NestedScrollingChild {
+    /**
+     * Enable or disable nested scrolling for this view.
+     *
+     * <p>If this property is set to true the view will be permitted to initiate nested
+     * scrolling operations with a compatible parent view in the current hierarchy. If this
+     * view does not implement nested scrolling this will have no effect. Disabling nested scrolling
+     * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping}
+     * the nested scroll.</p>
+     *
+     * @param enabled true to enable nested scrolling, false to disable
+     *
+     * @see #isNestedScrollingEnabled()
+     */
+    void setNestedScrollingEnabled(boolean enabled);
+
+    /**
+     * Returns true if nested scrolling is enabled for this view.
+     *
+     * <p>If nested scrolling is enabled and this View class implementation supports it,
+     * this view will act as a nested scrolling child view when applicable, forwarding data
+     * about the scroll operation in progress to a compatible and cooperating nested scrolling
+     * parent.</p>
+     *
+     * @return true if nested scrolling is enabled
+     *
+     * @see #setNestedScrollingEnabled(boolean)
+     */
+    boolean isNestedScrollingEnabled();
+
+    /**
+     * Begin a nestable scroll operation along the given axes.
+     *
+     * <p>A view starting a nested scroll promises to abide by the following contract:</p>
+     *
+     * <p>The view will call startNestedScroll upon initiating a scroll operation. In the case
+     * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}.
+     * In the case of touch scrolling the nested scroll will be terminated automatically in
+     * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}.
+     * In the event of programmatic scrolling the caller must explicitly call
+     * {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p>
+     *
+     * <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found.
+     * If it returns false the caller may ignore the rest of this contract until the next scroll.
+     * Calling startNestedScroll while a nested scroll is already in progress will return true.</p>
+     *
+     * <p>At each incremental step of the scroll the caller should invoke
+     * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll}
+     * once it has calculated the requested scrolling delta. If it returns true the nested scrolling
+     * parent at least partially consumed the scroll and the caller should adjust the amount it
+     * scrolls by.</p>
+     *
+     * <p>After applying the remainder of the scroll delta the caller should invoke
+     * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing
+     * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat
+     * these values differently. See
+     * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}.
+     * </p>
+     *
+     * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}
+     *             and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}.
+     * @return true if a cooperative parent was found and nested scrolling has been enabled for
+     *         the current gesture.
+     *
+     * @see #stopNestedScroll()
+     * @see #dispatchNestedPreScroll(int, int, int[], int[])
+     * @see #dispatchNestedScroll(int, int, int, int, int[])
+     */
+    boolean startNestedScroll(int axes);
+
+    /**
+     * Stop a nested scroll in progress.
+     *
+     * <p>Calling this method when a nested scroll is not currently in progress is harmless.</p>
+     *
+     * @see #startNestedScroll(int)
+     */
+    void stopNestedScroll();
+
+    /**
+     * Returns true if this view has a nested scrolling parent.
+     *
+     * <p>The presence of a nested scrolling parent indicates that this view has initiated
+     * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.</p>
+     *
+     * @return whether this view has a nested scrolling parent
+     */
+    boolean hasNestedScrollingParent();
+
+    /**
+     * Dispatch one step of a nested scroll in progress.
+     *
+     * <p>Implementations of views that support nested scrolling should call this to report
+     * info about a scroll in progress to the current nested scrolling parent. If a nested scroll
+     * is not currently in progress or nested scrolling is not
+     * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p>
+     *
+     * <p>Compatible View implementations should also call
+     * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before
+     * consuming a component of the scroll event themselves.</p>
+     *
+     * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
+     * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
+     * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view
+     * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view
+     * @param offsetInWindow Optional. If not null, on return this will contain the offset
+     *                       in local view coordinates of this view from before this operation
+     *                       to after it completes. View implementations may use this to adjust
+     *                       expected input coordinate tracking.
+     * @return true if the event was dispatched, false if it could not be dispatched.
+     * @see #dispatchNestedPreScroll(int, int, int[], int[])
+     */
+    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
+
+    /**
+     * Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
+     *
+     * <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
+     * <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested
+     * scrolling operation to consume some or all of the scroll operation before the child view
+     * consumes it.</p>
+     *
+     * @param dx Horizontal scroll distance in pixels
+     * @param dy Vertical scroll distance in pixels
+     * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx
+     *                 and consumed[1] the consumed dy.
+     * @param offsetInWindow Optional. If not null, on return this will contain the offset
+     *                       in local view coordinates of this view from before this operation
+     *                       to after it completes. View implementations may use this to adjust
+     *                       expected input coordinate tracking.
+     * @return true if the parent consumed some or all of the scroll delta
+     * @see #dispatchNestedScroll(int, int, int, int, int[])
+     */
+    boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
+
+    /**
+     * Dispatch a fling to a nested scrolling parent.
+     *
+     * <p>This method should be used to indicate that a nested scrolling child has detected
+     * suitable conditions for a fling. Generally this means that a touch scroll has ended with a
+     * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
+     * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
+     * along a scrollable axis.</p>
+     *
+     * <p>If a nested scrolling child view would normally fling but it is at the edge of
+     * its own content, it can use this method to delegate the fling to its nested scrolling
+     * parent instead. The parent may optionally consume the fling or observe a child fling.</p>
+     *
+     * @param velocityX Horizontal fling velocity in pixels per second
+     * @param velocityY Vertical fling velocity in pixels per second
+     * @param consumed true if the child consumed the fling, false otherwise
+     * @return true if the nested scrolling parent consumed or otherwise reacted to the fling
+     */
+    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
+
+    /**
+     * Dispatch a fling to a nested scrolling parent before it is processed by this view.
+     *
+     * <p>Nested pre-fling events are to nested fling events what touch intercept is to touch
+     * and what nested pre-scroll is to nested scroll. <code>dispatchNestedPreFling</code>
+     * offsets an opportunity for the parent view in a nested fling to fully consume the fling
+     * before the child view consumes it. If this method returns <code>true</code>, a nested
+     * parent view consumed the fling and this view should not scroll as a result.</p>
+     *
+     * <p>For a better user experience, only one view in a nested scrolling chain should consume
+     * the fling at a time. If a parent view consumed the fling this method will return false.
+     * Custom view implementations should account for this in two ways:</p>
+     *
+     * <ul>
+     *     <li>If a custom view is paged and needs to settle to a fixed page-point, do not
+     *     call <code>dispatchNestedPreFling</code>; consume the fling and settle to a valid
+     *     position regardless.</li>
+     *     <li>If a nested parent does consume the fling, this view should not scroll at all,
+     *     even to settle back to a valid idle position.</li>
+     * </ul>
+     *
+     * <p>Views should also not offer fling velocities to nested parent views along an axis
+     * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView}
+     * should not offer a horizontal fling velocity to its parents since scrolling along that
+     * axis is not permitted and carrying velocity along that motion does not make sense.</p>
+     *
+     * @param velocityX Horizontal fling velocity in pixels per second
+     * @param velocityY Vertical fling velocity in pixels per second
+     * @return true if a nested scrolling parent consumed the fling
+     */
+    boolean dispatchNestedPreFling(float velocityX, float velocityY);
+}
diff --git a/com/android/internal/widget/NotificationActionListLayout.java b/com/android/internal/widget/NotificationActionListLayout.java
new file mode 100644
index 0000000..073aac5
--- /dev/null
+++ b/com/android/internal/widget/NotificationActionListLayout.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Layout for notification actions that ensures that no action consumes more than their share of
+ * the remaining available width, and the last action consumes the remaining space.
+ */
[email protected]
+public class NotificationActionListLayout extends LinearLayout {
+
+    private int mTotalWidth = 0;
+    private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>();
+    private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
+    private boolean mMeasureLinearly;
+    private int mDefaultPaddingEnd;
+    private Drawable mDefaultBackground;
+
+    public NotificationActionListLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mMeasureLinearly) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            return;
+        }
+        final int N = getChildCount();
+        int textViews = 0;
+        int otherViews = 0;
+        int notGoneChildren = 0;
+
+        View lastNotGoneChild = null;
+        for (int i = 0; i < N; i++) {
+            View c = getChildAt(i);
+            if (c instanceof TextView) {
+                textViews++;
+            } else {
+                otherViews++;
+            }
+            if (c.getVisibility() != GONE) {
+                notGoneChildren++;
+                lastNotGoneChild = c;
+            }
+        }
+
+        // Rebuild the measure order if the number of children changed or the text length of
+        // any of the children changed.
+        boolean needRebuild = false;
+        if (textViews != mMeasureOrderTextViews.size()
+                || otherViews != mMeasureOrderOther.size()) {
+            needRebuild = true;
+        }
+        if (!needRebuild) {
+            final int size = mMeasureOrderTextViews.size();
+            for (int i = 0; i < size; i++) {
+                Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i);
+                if (pair.first != pair.second.getText().length()) {
+                    needRebuild = true;
+                }
+            }
+        }
+        if (notGoneChildren > 1 && needRebuild) {
+            rebuildMeasureOrder(textViews, otherViews);
+        }
+
+        final boolean constrained =
+                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
+
+        final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
+        final int otherSize = mMeasureOrderOther.size();
+        int usedWidth = 0;
+
+        // Optimization: Don't do this if there's only one child.
+        int measuredChildren = 0;
+        for (int i = 0; i < N && notGoneChildren > 1; i++) {
+            // Measure shortest children first. To avoid measuring twice, we approximate by looking
+            // at the text length.
+            View c;
+            if (i < otherSize) {
+                c = mMeasureOrderOther.get(i);
+            } else {
+                c = mMeasureOrderTextViews.get(i - otherSize).second;
+            }
+            if (c.getVisibility() == GONE) {
+                continue;
+            }
+            MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
+
+            int usedWidthForChild = usedWidth;
+            if (constrained) {
+                // Make sure that this child doesn't consume more than its share of the remaining
+                // total available space. Not used space will benefit subsequent views. Since we
+                // measure in the order of (approx.) size, a large view can still take more than its
+                // share if the others are small.
+                int availableWidth = innerWidth - usedWidth;
+                int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
+
+                usedWidthForChild = innerWidth - maxWidthForChild;
+            }
+
+            measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
+                    heightMeasureSpec, 0 /* usedHeight */);
+
+            usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
+            measuredChildren++;
+        }
+
+        // Make sure to measure the last child full-width if we didn't use up the entire width,
+        // or we didn't measure yet because there's just one child.
+        if (lastNotGoneChild != null && (constrained && usedWidth < innerWidth
+                || notGoneChildren == 1)) {
+            MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams();
+            if (notGoneChildren > 1) {
+                // Need to make room, since we already measured this once.
+                usedWidth -= lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
+            }
+
+            int originalWidth = lp.width;
+            lp.width = LayoutParams.MATCH_PARENT;
+            measureChildWithMargins(lastNotGoneChild, widthMeasureSpec, usedWidth,
+                    heightMeasureSpec, 0 /* usedHeight */);
+            lp.width = originalWidth;
+
+            usedWidth += lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
+        }
+
+        mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
+        setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+                resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
+    }
+
+    private void rebuildMeasureOrder(int capacityText, int capacityOther) {
+        clearMeasureOrder();
+        mMeasureOrderTextViews.ensureCapacity(capacityText);
+        mMeasureOrderOther.ensureCapacity(capacityOther);
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View c = getChildAt(i);
+            if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
+                mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
+                        (TextView)c));
+            } else {
+                mMeasureOrderOther.add(c);
+            }
+        }
+        mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
+    }
+
+    private void clearMeasureOrder() {
+        mMeasureOrderOther.clear();
+        mMeasureOrderTextViews.clear();
+    }
+
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        clearMeasureOrder();
+    }
+
+    @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        clearMeasureOrder();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (mMeasureLinearly) {
+            super.onLayout(changed, left, top, right, bottom);
+            return;
+        }
+        final boolean isLayoutRtl = isLayoutRtl();
+        final int paddingTop = mPaddingTop;
+
+        int childTop;
+        int childLeft;
+
+        // Where bottom of child should go
+        final int height = bottom - top;
+
+        // Space available for child
+        int innerHeight = height - paddingTop - mPaddingBottom;
+
+        final int count = getChildCount();
+
+        final int layoutDirection = getLayoutDirection();
+        switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) {
+            case Gravity.RIGHT:
+                // mTotalWidth contains the padding already
+                childLeft = mPaddingLeft + right - left - mTotalWidth;
+                break;
+
+            case Gravity.LEFT:
+            default:
+                childLeft = mPaddingLeft;
+                break;
+        }
+
+        int start = 0;
+        int dir = 1;
+        //In case of RTL, start drawing from the last child.
+        if (isLayoutRtl) {
+            start = count - 1;
+            dir = -1;
+        }
+
+        for (int i = 0; i < count; i++) {
+            final int childIndex = start + dir * i;
+            final View child = getChildAt(childIndex);
+            if (child.getVisibility() != GONE) {
+                final int childWidth = child.getMeasuredWidth();
+                final int childHeight = child.getMeasuredHeight();
+
+                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+                childTop = paddingTop + ((innerHeight - childHeight) / 2)
+                            + lp.topMargin - lp.bottomMargin;
+
+                childLeft += lp.leftMargin;
+                child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+                childLeft += childWidth + lp.rightMargin;
+            }
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mDefaultPaddingEnd = getPaddingEnd();
+        mDefaultBackground = getBackground();
+    }
+
+    /**
+     * Set whether the list is in a mode where some actions are emphasized. This will trigger an
+     * equal measuring where all actions are full height and change a few parameters like
+     * the padding.
+     */
+    @RemotableViewMethod
+    public void setEmphasizedMode(boolean emphasizedMode) {
+        mMeasureLinearly = emphasizedMode;
+        setPaddingRelative(getPaddingStart(), getPaddingTop(),
+                emphasizedMode ? 0 : mDefaultPaddingEnd, getPaddingBottom());
+        setBackground(emphasizedMode ? null : mDefaultBackground);
+        requestLayout();
+    }
+
+    public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
+            = (a, b) -> a.first.compareTo(b.first);
+}
diff --git a/com/android/internal/widget/NotificationExpandButton.java b/com/android/internal/widget/NotificationExpandButton.java
new file mode 100644
index 0000000..39f82a5
--- /dev/null
+++ b/com/android/internal/widget/NotificationExpandButton.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+/**
+ * An expand button in a notification
+ */
[email protected]
+public class NotificationExpandButton extends ImageView {
+
+    public NotificationExpandButton(Context context) {
+        super(context);
+    }
+
+    public NotificationExpandButton(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NotificationExpandButton(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public NotificationExpandButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
+        super.getBoundsOnScreen(outRect, clipToParent);
+        extendRectToMinTouchSize(outRect);
+    }
+
+    private void extendRectToMinTouchSize(Rect rect) {
+        int touchTargetSize = (int) (getResources().getDisplayMetrics().density * 48);
+        rect.left = rect.centerX() - touchTargetSize / 2;
+        rect.right = rect.left + touchTargetSize;
+        rect.top = rect.centerY() - touchTargetSize / 2;
+        rect.bottom = rect.top + touchTargetSize;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(Button.class.getName());
+    }
+}
diff --git a/com/android/internal/widget/NumericTextView.java b/com/android/internal/widget/NumericTextView.java
new file mode 100644
index 0000000..27c5834
--- /dev/null
+++ b/com/android/internal/widget/NumericTextView.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.StateSet;
+import android.view.KeyEvent;
+import android.widget.TextView;
+
+/**
+ * Extension of TextView that can handle displaying and inputting a range of
+ * numbers.
+ * <p>
+ * Clients of this view should never call {@link #setText(CharSequence)} or
+ * {@link #setHint(CharSequence)} directly. Instead, they should call
+ * {@link #setValue(int)} to modify the currently displayed value.
+ */
+public class NumericTextView extends TextView {
+    private static final int RADIX = 10;
+    private static final double LOG_RADIX = Math.log(RADIX);
+
+    private int mMinValue = 0;
+    private int mMaxValue = 99;
+
+    /** Number of digits in the maximum value. */
+    private int mMaxCount = 2;
+
+    private boolean mShowLeadingZeroes = true;
+
+    private int mValue;
+
+    /** Number of digits entered during editing mode. */
+    private int mCount;
+
+    /** Used to restore the value after an aborted edit. */
+    private int mPreviousValue;
+
+    private OnValueChangedListener mListener;
+
+    public NumericTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // Generate the hint text color based on disabled state.
+        final int textColorDisabled = getTextColors().getColorForState(StateSet.get(0), 0);
+        setHintTextColor(textColorDisabled);
+
+        setFocusable(true);
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+        if (focused) {
+            mPreviousValue = mValue;
+            mValue = 0;
+            mCount = 0;
+
+            // Transfer current text to hint.
+            setHint(getText());
+            setText("");
+        } else {
+            if (mCount == 0) {
+                // No digits were entered, revert to previous value.
+                mValue = mPreviousValue;
+
+                setText(getHint());
+                setHint("");
+            }
+
+            // Ensure the committed value is within range.
+            if (mValue < mMinValue) {
+                mValue = mMinValue;
+            }
+
+            setValue(mValue);
+
+            if (mListener != null) {
+                mListener.onValueChanged(this, mValue, true, true);
+            }
+        }
+    }
+
+    /**
+     * Sets the currently displayed value.
+     * <p>
+     * The specified {@code value} must be within the range specified by
+     * {@link #setRange(int, int)} (e.g. between {@link #getRangeMinimum()}
+     * and {@link #getRangeMaximum()}).
+     *
+     * @param value the value to display
+     */
+    public final void setValue(int value) {
+        if (mValue != value) {
+            mValue = value;
+
+            updateDisplayedValue();
+        }
+    }
+
+    /**
+     * Returns the currently displayed value.
+     * <p>
+     * If the value is currently being edited, returns the live value which may
+     * not be within the range specified by {@link #setRange(int, int)}.
+     *
+     * @return the currently displayed value
+     */
+    public final int getValue() {
+        return mValue;
+    }
+
+    /**
+     * Sets the valid range (inclusive).
+     *
+     * @param minValue the minimum valid value (inclusive)
+     * @param maxValue the maximum valid value (inclusive)
+     */
+    public final void setRange(int minValue, int maxValue) {
+        if (mMinValue != minValue) {
+            mMinValue = minValue;
+        }
+
+        if (mMaxValue != maxValue) {
+            mMaxValue = maxValue;
+            mMaxCount = 1 + (int) (Math.log(maxValue) / LOG_RADIX);
+
+            updateMinimumWidth();
+            updateDisplayedValue();
+        }
+    }
+
+    /**
+     * @return the minimum value value (inclusive)
+     */
+    public final int getRangeMinimum() {
+        return mMinValue;
+    }
+
+    /**
+     * @return the maximum value value (inclusive)
+     */
+    public final int getRangeMaximum() {
+        return mMaxValue;
+    }
+
+    /**
+     * Sets whether this view shows leading zeroes.
+     * <p>
+     * When leading zeroes are shown, the displayed value will be padded
+     * with zeroes to the width of the maximum value as specified by
+     * {@link #setRange(int, int)} (see also {@link #getRangeMaximum()}.
+     * <p>
+     * For example, with leading zeroes shown, a maximum of 99 and value of
+     * 9 would display "09". A maximum of 100 and a value of 9 would display
+     * "009". With leading zeroes hidden, both cases would show "9".
+     *
+     * @param showLeadingZeroes {@code true} to show leading zeroes,
+     *                          {@code false} to hide them
+     */
+    public final void setShowLeadingZeroes(boolean showLeadingZeroes) {
+        if (mShowLeadingZeroes != showLeadingZeroes) {
+            mShowLeadingZeroes = showLeadingZeroes;
+
+            updateDisplayedValue();
+        }
+    }
+
+    public final boolean getShowLeadingZeroes() {
+        return mShowLeadingZeroes;
+    }
+
+    /**
+     * Computes the display value and updates the text of the view.
+     * <p>
+     * This method should be called whenever the current value or display
+     * properties (leading zeroes, max digits) change.
+     */
+    private void updateDisplayedValue() {
+        final String format;
+        if (mShowLeadingZeroes) {
+            format = "%0" + mMaxCount + "d";
+        } else {
+            format = "%d";
+        }
+
+        // Always use String.format() rather than Integer.toString()
+        // to obtain correctly localized values.
+        setText(String.format(format, mValue));
+    }
+
+    /**
+     * Computes the minimum width in pixels required to display all possible
+     * values and updates the minimum width of the view.
+     * <p>
+     * This method should be called whenever the maximum value changes.
+     */
+    private void updateMinimumWidth() {
+        final CharSequence previousText = getText();
+        int maxWidth = 0;
+
+        for (int i = 0; i < mMaxValue; i++) {
+            setText(String.format("%0" + mMaxCount + "d", i));
+            measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+            final int width = getMeasuredWidth();
+            if (width > maxWidth) {
+                maxWidth = width;
+            }
+        }
+
+        setText(previousText);
+        setMinWidth(maxWidth);
+        setMinimumWidth(maxWidth);
+    }
+
+    public final void setOnDigitEnteredListener(OnValueChangedListener listener) {
+        mListener = listener;
+    }
+
+    public final OnValueChangedListener getOnDigitEnteredListener() {
+        return mListener;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return isKeyCodeNumeric(keyCode)
+                || (keyCode == KeyEvent.KEYCODE_DEL)
+                || super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+        return isKeyCodeNumeric(keyCode)
+                || (keyCode == KeyEvent.KEYCODE_DEL)
+                || super.onKeyMultiple(keyCode, repeatCount, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return handleKeyUp(keyCode)
+                || super.onKeyUp(keyCode, event);
+    }
+
+    private boolean handleKeyUp(int keyCode) {
+        if (keyCode == KeyEvent.KEYCODE_DEL) {
+            // Backspace removes the least-significant digit, if available.
+            if (mCount > 0) {
+                mValue /= RADIX;
+                mCount--;
+            }
+        } else if (isKeyCodeNumeric(keyCode)) {
+            if (mCount < mMaxCount) {
+                final int keyValue = numericKeyCodeToInt(keyCode);
+                final int newValue = mValue * RADIX + keyValue;
+                if (newValue <= mMaxValue) {
+                    mValue = newValue;
+                    mCount++;
+                }
+            }
+        } else {
+            return false;
+        }
+
+        final String formattedValue;
+        if (mCount > 0) {
+            // If the user types 01, we should always show the leading 0 even if
+            // getShowLeadingZeroes() is false. Preserve typed leading zeroes by
+            // using the number of digits entered as the format width.
+            formattedValue = String.format("%0" + mCount + "d", mValue);
+        } else {
+            formattedValue = "";
+        }
+
+        setText(formattedValue);
+
+        if (mListener != null) {
+            final boolean isValid = mValue >= mMinValue;
+            final boolean isFinished = mCount >= mMaxCount || mValue * RADIX > mMaxValue;
+            mListener.onValueChanged(this, mValue, isValid, isFinished);
+        }
+
+        return true;
+    }
+
+    private static boolean isKeyCodeNumeric(int keyCode) {
+        return keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1
+                || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3
+                || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5
+                || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
+                || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9;
+    }
+
+    private static int numericKeyCodeToInt(int keyCode) {
+        return keyCode - KeyEvent.KEYCODE_0;
+    }
+
+    public interface OnValueChangedListener {
+        /**
+         * Called when the value displayed by {@code view} changes.
+         *
+         * @param view the view whose value changed
+         * @param value the new value
+         * @param isValid {@code true} if the value is valid (e.g. within the
+         *                range specified by {@link #setRange(int, int)}),
+         *                {@code false} otherwise
+         * @param isFinished {@code true} if the no more digits may be entered,
+         *                   {@code false} if more digits may be entered
+         */
+        void onValueChanged(NumericTextView view, int value, boolean isValid, boolean isFinished);
+    }
+}
diff --git a/com/android/internal/widget/OpReorderer.java b/com/android/internal/widget/OpReorderer.java
new file mode 100644
index 0000000..babb087
--- /dev/null
+++ b/com/android/internal/widget/OpReorderer.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import static com.android.internal.widget.AdapterHelper.UpdateOp.ADD;
+import static com.android.internal.widget.AdapterHelper.UpdateOp.MOVE;
+import static com.android.internal.widget.AdapterHelper.UpdateOp.REMOVE;
+import static com.android.internal.widget.AdapterHelper.UpdateOp.UPDATE;
+
+import com.android.internal.widget.AdapterHelper.UpdateOp;
+
+import java.util.List;
+
+class OpReorderer {
+
+    final Callback mCallback;
+
+    OpReorderer(Callback callback) {
+        mCallback = callback;
+    }
+
+    void reorderOps(List<UpdateOp> ops) {
+        // since move operations breaks continuity, their effects on ADD/RM are hard to handle.
+        // we push them to the end of the list so that they can be handled easily.
+        int badMove;
+        while ((badMove = getLastMoveOutOfOrder(ops)) != -1) {
+            swapMoveOp(ops, badMove, badMove + 1);
+        }
+    }
+
+    private void swapMoveOp(List<UpdateOp> list, int badMove, int next) {
+        final UpdateOp moveOp = list.get(badMove);
+        final UpdateOp nextOp = list.get(next);
+        switch (nextOp.cmd) {
+            case REMOVE:
+                swapMoveRemove(list, badMove, moveOp, next, nextOp);
+                break;
+            case ADD:
+                swapMoveAdd(list, badMove, moveOp, next, nextOp);
+                break;
+            case UPDATE:
+                swapMoveUpdate(list, badMove, moveOp, next, nextOp);
+                break;
+        }
+    }
+
+    void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
+            int removePos, UpdateOp removeOp) {
+        UpdateOp extraRm = null;
+        // check if move is nulled out by remove
+        boolean revertedMove = false;
+        final boolean moveIsBackwards;
+
+        if (moveOp.positionStart < moveOp.itemCount) {
+            moveIsBackwards = false;
+            if (removeOp.positionStart == moveOp.positionStart
+                    && removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
+                revertedMove = true;
+            }
+        } else {
+            moveIsBackwards = true;
+            if (removeOp.positionStart == moveOp.itemCount + 1
+                    && removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
+                revertedMove = true;
+            }
+        }
+
+        // going in reverse, first revert the effect of add
+        if (moveOp.itemCount < removeOp.positionStart) {
+            removeOp.positionStart--;
+        } else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
+            // move is removed.
+            removeOp.itemCount--;
+            moveOp.cmd = REMOVE;
+            moveOp.itemCount = 1;
+            if (removeOp.itemCount == 0) {
+                list.remove(removePos);
+                mCallback.recycleUpdateOp(removeOp);
+            }
+            // no need to swap, it is already a remove
+            return;
+        }
+
+        // now affect of add is consumed. now apply effect of first remove
+        if (moveOp.positionStart <= removeOp.positionStart) {
+            removeOp.positionStart++;
+        } else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
+            final int remaining = removeOp.positionStart + removeOp.itemCount
+                    - moveOp.positionStart;
+            extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining, null);
+            removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
+        }
+
+        // if effects of move is reverted by remove, we are done.
+        if (revertedMove) {
+            list.set(movePos, removeOp);
+            list.remove(removePos);
+            mCallback.recycleUpdateOp(moveOp);
+            return;
+        }
+
+        // now find out the new locations for move actions
+        if (moveIsBackwards) {
+            if (extraRm != null) {
+                if (moveOp.positionStart > extraRm.positionStart) {
+                    moveOp.positionStart -= extraRm.itemCount;
+                }
+                if (moveOp.itemCount > extraRm.positionStart) {
+                    moveOp.itemCount -= extraRm.itemCount;
+                }
+            }
+            if (moveOp.positionStart > removeOp.positionStart) {
+                moveOp.positionStart -= removeOp.itemCount;
+            }
+            if (moveOp.itemCount > removeOp.positionStart) {
+                moveOp.itemCount -= removeOp.itemCount;
+            }
+        } else {
+            if (extraRm != null) {
+                if (moveOp.positionStart >= extraRm.positionStart) {
+                    moveOp.positionStart -= extraRm.itemCount;
+                }
+                if (moveOp.itemCount >= extraRm.positionStart) {
+                    moveOp.itemCount -= extraRm.itemCount;
+                }
+            }
+            if (moveOp.positionStart >= removeOp.positionStart) {
+                moveOp.positionStart -= removeOp.itemCount;
+            }
+            if (moveOp.itemCount >= removeOp.positionStart) {
+                moveOp.itemCount -= removeOp.itemCount;
+            }
+        }
+
+        list.set(movePos, removeOp);
+        if (moveOp.positionStart != moveOp.itemCount) {
+            list.set(removePos, moveOp);
+        } else {
+            list.remove(removePos);
+        }
+        if (extraRm != null) {
+            list.add(movePos, extraRm);
+        }
+    }
+
+    private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
+            UpdateOp addOp) {
+        int offset = 0;
+        // going in reverse, first revert the effect of add
+        if (moveOp.itemCount < addOp.positionStart) {
+            offset--;
+        }
+        if (moveOp.positionStart < addOp.positionStart) {
+            offset++;
+        }
+        if (addOp.positionStart <= moveOp.positionStart) {
+            moveOp.positionStart += addOp.itemCount;
+        }
+        if (addOp.positionStart <= moveOp.itemCount) {
+            moveOp.itemCount += addOp.itemCount;
+        }
+        addOp.positionStart += offset;
+        list.set(move, addOp);
+        list.set(add, moveOp);
+    }
+
+    void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
+            UpdateOp updateOp) {
+        UpdateOp extraUp1 = null;
+        UpdateOp extraUp2 = null;
+        // going in reverse, first revert the effect of add
+        if (moveOp.itemCount < updateOp.positionStart) {
+            updateOp.positionStart--;
+        } else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
+            // moved item is updated. add an update for it
+            updateOp.itemCount--;
+            extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1, updateOp.payload);
+        }
+        // now affect of add is consumed. now apply effect of first remove
+        if (moveOp.positionStart <= updateOp.positionStart) {
+            updateOp.positionStart++;
+        } else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
+            final int remaining = updateOp.positionStart + updateOp.itemCount
+                    - moveOp.positionStart;
+            extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining,
+                    updateOp.payload);
+            updateOp.itemCount -= remaining;
+        }
+        list.set(update, moveOp);
+        if (updateOp.itemCount > 0) {
+            list.set(move, updateOp);
+        } else {
+            list.remove(move);
+            mCallback.recycleUpdateOp(updateOp);
+        }
+        if (extraUp1 != null) {
+            list.add(move, extraUp1);
+        }
+        if (extraUp2 != null) {
+            list.add(move, extraUp2);
+        }
+    }
+
+    private int getLastMoveOutOfOrder(List<UpdateOp> list) {
+        boolean foundNonMove = false;
+        for (int i = list.size() - 1; i >= 0; i--) {
+            final UpdateOp op1 = list.get(i);
+            if (op1.cmd == MOVE) {
+                if (foundNonMove) {
+                    return i;
+                }
+            } else {
+                foundNonMove = true;
+            }
+        }
+        return -1;
+    }
+
+    interface Callback {
+
+        UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload);
+
+        void recycleUpdateOp(UpdateOp op);
+    }
+}
diff --git a/com/android/internal/widget/OrientationHelper.java b/com/android/internal/widget/OrientationHelper.java
new file mode 100644
index 0000000..1b02c88
--- /dev/null
+++ b/com/android/internal/widget/OrientationHelper.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.LinearLayout;
+
+/**
+ * Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
+ * <p>
+ * It is developed to easily support vertical and horizontal orientations in a LayoutManager but
+ * can also be used to abstract calls around view bounds and child measurements with margins and
+ * decorations.
+ *
+ * @see #createHorizontalHelper(RecyclerView.LayoutManager)
+ * @see #createVerticalHelper(RecyclerView.LayoutManager)
+ */
+public abstract class OrientationHelper {
+
+    private static final int INVALID_SIZE = Integer.MIN_VALUE;
+
+    protected final RecyclerView.LayoutManager mLayoutManager;
+
+    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+
+    public static final int VERTICAL = LinearLayout.VERTICAL;
+
+    private int mLastTotalSpace = INVALID_SIZE;
+
+    final Rect mTmpRect = new Rect();
+
+    private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
+        mLayoutManager = layoutManager;
+    }
+
+    /**
+     * Call this method after onLayout method is complete if state is NOT pre-layout.
+     * This method records information like layout bounds that might be useful in the next layout
+     * calculations.
+     */
+    public void onLayoutComplete() {
+        mLastTotalSpace = getTotalSpace();
+    }
+
+    /**
+     * Returns the layout space change between the previous layout pass and current layout pass.
+     * <p>
+     * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's
+     * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler,
+     * RecyclerView.State)} method.
+     *
+     * @return The difference between the current total space and previous layout's total space.
+     * @see #onLayoutComplete()
+     */
+    public int getTotalSpaceChange() {
+        return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace;
+    }
+
+    /**
+     * Returns the start of the view including its decoration and margin.
+     * <p>
+     * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
+     * decoration and 3px left margin, returned value will be 15px.
+     *
+     * @param view The view element to check
+     * @return The first pixel of the element
+     * @see #getDecoratedEnd(android.view.View)
+     */
+    public abstract int getDecoratedStart(View view);
+
+    /**
+     * Returns the end of the view including its decoration and margin.
+     * <p>
+     * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
+     * decoration and 3px right margin, returned value will be 205.
+     *
+     * @param view The view element to check
+     * @return The last pixel of the element
+     * @see #getDecoratedStart(android.view.View)
+     */
+    public abstract int getDecoratedEnd(View view);
+
+    /**
+     * Returns the end of the View after its matrix transformations are applied to its layout
+     * position.
+     * <p>
+     * This method is useful when trying to detect the visible edge of a View.
+     * <p>
+     * It includes the decorations but does not include the margins.
+     *
+     * @param view The view whose transformed end will be returned
+     * @return The end of the View after its decor insets and transformation matrix is applied to
+     * its position
+     *
+     * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
+     */
+    public abstract int getTransformedEndWithDecoration(View view);
+
+    /**
+     * Returns the start of the View after its matrix transformations are applied to its layout
+     * position.
+     * <p>
+     * This method is useful when trying to detect the visible edge of a View.
+     * <p>
+     * It includes the decorations but does not include the margins.
+     *
+     * @param view The view whose transformed start will be returned
+     * @return The start of the View after its decor insets and transformation matrix is applied to
+     * its position
+     *
+     * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
+     */
+    public abstract int getTransformedStartWithDecoration(View view);
+
+    /**
+     * Returns the space occupied by this View in the current orientation including decorations and
+     * margins.
+     *
+     * @param view The view element to check
+     * @return Total space occupied by this view
+     * @see #getDecoratedMeasurementInOther(View)
+     */
+    public abstract int getDecoratedMeasurement(View view);
+
+    /**
+     * Returns the space occupied by this View in the perpendicular orientation including
+     * decorations and margins.
+     *
+     * @param view The view element to check
+     * @return Total space occupied by this view in the perpendicular orientation to current one
+     * @see #getDecoratedMeasurement(View)
+     */
+    public abstract int getDecoratedMeasurementInOther(View view);
+
+    /**
+     * Returns the start position of the layout after the start padding is added.
+     *
+     * @return The very first pixel we can draw.
+     */
+    public abstract int getStartAfterPadding();
+
+    /**
+     * Returns the end position of the layout after the end padding is removed.
+     *
+     * @return The end boundary for this layout.
+     */
+    public abstract int getEndAfterPadding();
+
+    /**
+     * Returns the end position of the layout without taking padding into account.
+     *
+     * @return The end boundary for this layout without considering padding.
+     */
+    public abstract int getEnd();
+
+    /**
+     * Offsets all children's positions by the given amount.
+     *
+     * @param amount Value to add to each child's layout parameters
+     */
+    public abstract void offsetChildren(int amount);
+
+    /**
+     * Returns the total space to layout. This number is the difference between
+     * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
+     *
+     * @return Total space to layout children
+     */
+    public abstract int getTotalSpace();
+
+    /**
+     * Offsets the child in this orientation.
+     *
+     * @param view   View to offset
+     * @param offset offset amount
+     */
+    public abstract void offsetChild(View view, int offset);
+
+    /**
+     * Returns the padding at the end of the layout. For horizontal helper, this is the right
+     * padding and for vertical helper, this is the bottom padding. This method does not check
+     * whether the layout is RTL or not.
+     *
+     * @return The padding at the end of the layout.
+     */
+    public abstract int getEndPadding();
+
+    /**
+     * Returns the MeasureSpec mode for the current orientation from the LayoutManager.
+     *
+     * @return The current measure spec mode.
+     *
+     * @see View.MeasureSpec
+     * @see RecyclerView.LayoutManager#getWidthMode()
+     * @see RecyclerView.LayoutManager#getHeightMode()
+     */
+    public abstract int getMode();
+
+    /**
+     * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager.
+     *
+     * @return The current measure spec mode.
+     *
+     * @see View.MeasureSpec
+     * @see RecyclerView.LayoutManager#getWidthMode()
+     * @see RecyclerView.LayoutManager#getHeightMode()
+     */
+    public abstract int getModeInOther();
+
+    /**
+     * Creates an OrientationHelper for the given LayoutManager and orientation.
+     *
+     * @param layoutManager LayoutManager to attach to
+     * @param orientation   Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
+     * @return A new OrientationHelper
+     */
+    public static OrientationHelper createOrientationHelper(
+            RecyclerView.LayoutManager layoutManager, int orientation) {
+        switch (orientation) {
+            case HORIZONTAL:
+                return createHorizontalHelper(layoutManager);
+            case VERTICAL:
+                return createVerticalHelper(layoutManager);
+        }
+        throw new IllegalArgumentException("invalid orientation");
+    }
+
+    /**
+     * Creates a horizontal OrientationHelper for the given LayoutManager.
+     *
+     * @param layoutManager The LayoutManager to attach to.
+     * @return A new OrientationHelper
+     */
+    public static OrientationHelper createHorizontalHelper(
+            RecyclerView.LayoutManager layoutManager) {
+        return new OrientationHelper(layoutManager) {
+            @Override
+            public int getEndAfterPadding() {
+                return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
+            }
+
+            @Override
+            public int getEnd() {
+                return mLayoutManager.getWidth();
+            }
+
+            @Override
+            public void offsetChildren(int amount) {
+                mLayoutManager.offsetChildrenHorizontal(amount);
+            }
+
+            @Override
+            public int getStartAfterPadding() {
+                return mLayoutManager.getPaddingLeft();
+            }
+
+            @Override
+            public int getDecoratedMeasurement(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+                        + params.rightMargin;
+            }
+
+            @Override
+            public int getDecoratedMeasurementInOther(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+                        + params.bottomMargin;
+            }
+
+            @Override
+            public int getDecoratedEnd(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
+            }
+
+            @Override
+            public int getDecoratedStart(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
+            }
+
+            @Override
+            public int getTransformedEndWithDecoration(View view) {
+                mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+                return mTmpRect.right;
+            }
+
+            @Override
+            public int getTransformedStartWithDecoration(View view) {
+                mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+                return mTmpRect.left;
+            }
+
+            @Override
+            public int getTotalSpace() {
+                return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
+                        - mLayoutManager.getPaddingRight();
+            }
+
+            @Override
+            public void offsetChild(View view, int offset) {
+                view.offsetLeftAndRight(offset);
+            }
+
+            @Override
+            public int getEndPadding() {
+                return mLayoutManager.getPaddingRight();
+            }
+
+            @Override
+            public int getMode() {
+                return mLayoutManager.getWidthMode();
+            }
+
+            @Override
+            public int getModeInOther() {
+                return mLayoutManager.getHeightMode();
+            }
+        };
+    }
+
+    /**
+     * Creates a vertical OrientationHelper for the given LayoutManager.
+     *
+     * @param layoutManager The LayoutManager to attach to.
+     * @return A new OrientationHelper
+     */
+    public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
+        return new OrientationHelper(layoutManager) {
+            @Override
+            public int getEndAfterPadding() {
+                return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
+            }
+
+            @Override
+            public int getEnd() {
+                return mLayoutManager.getHeight();
+            }
+
+            @Override
+            public void offsetChildren(int amount) {
+                mLayoutManager.offsetChildrenVertical(amount);
+            }
+
+            @Override
+            public int getStartAfterPadding() {
+                return mLayoutManager.getPaddingTop();
+            }
+
+            @Override
+            public int getDecoratedMeasurement(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+                        + params.bottomMargin;
+            }
+
+            @Override
+            public int getDecoratedMeasurementInOther(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+                        + params.rightMargin;
+            }
+
+            @Override
+            public int getDecoratedEnd(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
+            }
+
+            @Override
+            public int getDecoratedStart(View view) {
+                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+                        view.getLayoutParams();
+                return mLayoutManager.getDecoratedTop(view) - params.topMargin;
+            }
+
+            @Override
+            public int getTransformedEndWithDecoration(View view) {
+                mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+                return mTmpRect.bottom;
+            }
+
+            @Override
+            public int getTransformedStartWithDecoration(View view) {
+                mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+                return mTmpRect.top;
+            }
+
+            @Override
+            public int getTotalSpace() {
+                return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
+                        - mLayoutManager.getPaddingBottom();
+            }
+
+            @Override
+            public void offsetChild(View view, int offset) {
+                view.offsetTopAndBottom(offset);
+            }
+
+            @Override
+            public int getEndPadding() {
+                return mLayoutManager.getPaddingBottom();
+            }
+
+            @Override
+            public int getMode() {
+                return mLayoutManager.getHeightMode();
+            }
+
+            @Override
+            public int getModeInOther() {
+                return mLayoutManager.getWidthMode();
+            }
+        };
+    }
+}
diff --git a/com/android/internal/widget/PagerAdapter.java b/com/android/internal/widget/PagerAdapter.java
new file mode 100644
index 0000000..910a720
--- /dev/null
+++ b/com/android/internal/widget/PagerAdapter.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Base class providing the adapter to populate pages inside of
+ * a {@link android.support.v4.view.ViewPager}.  You will most likely want to use a more
+ * specific implementation of this, such as
+ * {@link android.support.v4.app.FragmentPagerAdapter} or
+ * {@link android.support.v4.app.FragmentStatePagerAdapter}.
+ *
+ * <p>When you implement a PagerAdapter, you must override the following methods
+ * at minimum:</p>
+ * <ul>
+ * <li>{@link #instantiateItem(android.view.ViewGroup, int)}</li>
+ * <li>{@link #destroyItem(android.view.ViewGroup, int, Object)}</li>
+ * <li>{@link #getCount()}</li>
+ * <li>{@link #isViewFromObject(android.view.View, Object)}</li>
+ * </ul>
+ *
+ * <p>PagerAdapter is more general than the adapters used for
+ * {@link android.widget.AdapterView AdapterViews}. Instead of providing a
+ * View recycling mechanism directly ViewPager uses callbacks to indicate the
+ * steps taken during an update. A PagerAdapter may implement a form of View
+ * recycling if desired or use a more sophisticated method of managing page
+ * Views such as Fragment transactions where each page is represented by its
+ * own Fragment.</p>
+ *
+ * <p>ViewPager associates each page with a key Object instead of working with
+ * Views directly. This key is used to track and uniquely identify a given page
+ * independent of its position in the adapter. A call to the PagerAdapter method
+ * {@link #startUpdate(android.view.ViewGroup)} indicates that the contents of the ViewPager
+ * are about to change. One or more calls to {@link #instantiateItem(android.view.ViewGroup, int)}
+ * and/or {@link #destroyItem(android.view.ViewGroup, int, Object)} will follow, and the end
+ * of an update will be signaled by a call to {@link #finishUpdate(android.view.ViewGroup)}.
+ * By the time {@link #finishUpdate(android.view.ViewGroup) finishUpdate} returns the views
+ * associated with the key objects returned by
+ * {@link #instantiateItem(android.view.ViewGroup, int) instantiateItem} should be added to
+ * the parent ViewGroup passed to these methods and the views associated with
+ * the keys passed to {@link #destroyItem(android.view.ViewGroup, int, Object) destroyItem}
+ * should be removed. The method {@link #isViewFromObject(android.view.View, Object)} identifies
+ * whether a page View is associated with a given key object.</p>
+ *
+ * <p>A very simple PagerAdapter may choose to use the page Views themselves
+ * as key objects, returning them from {@link #instantiateItem(android.view.ViewGroup, int)}
+ * after creation and adding them to the parent ViewGroup. A matching
+ * {@link #destroyItem(android.view.ViewGroup, int, Object)} implementation would remove the
+ * View from the parent ViewGroup and {@link #isViewFromObject(android.view.View, Object)}
+ * could be implemented as <code>return view == object;</code>.</p>
+ *
+ * <p>PagerAdapter supports data set changes. Data set changes must occur on the
+ * main thread and must end with a call to {@link #notifyDataSetChanged()} similar
+ * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
+ * set change may involve pages being added, removed, or changing position. The
+ * ViewPager will keep the current page active provided the adapter implements
+ * the method {@link #getItemPosition(Object)}.</p>
+ */
+public abstract class PagerAdapter {
+    private DataSetObservable mObservable = new DataSetObservable();
+
+    public static final int POSITION_UNCHANGED = -1;
+    public static final int POSITION_NONE = -2;
+
+    /**
+     * Return the number of views available.
+     */
+    public abstract int getCount();
+
+    /**
+     * Called when a change in the shown pages is going to start being made.
+     * @param container The containing View which is displaying this adapter's
+     * page views.
+     */
+    public void startUpdate(ViewGroup container) {
+        startUpdate((View) container);
+    }
+
+    /**
+     * Create the page for the given position.  The adapter is responsible
+     * for adding the view to the container given here, although it only
+     * must ensure this is done by the time it returns from
+     * {@link #finishUpdate(android.view.ViewGroup)}.
+     *
+     * @param container The containing View in which the page will be shown.
+     * @param position The page position to be instantiated.
+     * @return Returns an Object representing the new page.  This does not
+     * need to be a View, but can be some other container of the page.
+     */
+    public Object instantiateItem(ViewGroup container, int position) {
+        return instantiateItem((View) container, position);
+    }
+
+    /**
+     * Remove a page for the given position.  The adapter is responsible
+     * for removing the view from its container, although it only must ensure
+     * this is done by the time it returns from {@link #finishUpdate(android.view.ViewGroup)}.
+     *
+     * @param container The containing View from which the page will be removed.
+     * @param position The page position to be removed.
+     * @param object The same object that was returned by
+     * {@link #instantiateItem(android.view.View, int)}.
+     */
+    public void destroyItem(ViewGroup container, int position, Object object) {
+        destroyItem((View) container, position, object);
+    }
+
+    /**
+     * Called to inform the adapter of which item is currently considered to
+     * be the "primary", that is the one show to the user as the current page.
+     *
+     * @param container The containing View from which the page will be removed.
+     * @param position The page position that is now the primary.
+     * @param object The same object that was returned by
+     * {@link #instantiateItem(android.view.View, int)}.
+     */
+    public void setPrimaryItem(ViewGroup container, int position, Object object) {
+        setPrimaryItem((View) container, position, object);
+    }
+
+    /**
+     * Called when the a change in the shown pages has been completed.  At this
+     * point you must ensure that all of the pages have actually been added or
+     * removed from the container as appropriate.
+     * @param container The containing View which is displaying this adapter's
+     * page views.
+     */
+    public void finishUpdate(ViewGroup container) {
+        finishUpdate((View) container);
+    }
+
+    /**
+     * Called when a change in the shown pages is going to start being made.
+     * @param container The containing View which is displaying this adapter's
+     * page views.
+     *
+     * @deprecated Use {@link #startUpdate(android.view.ViewGroup)}
+     */
+    public void startUpdate(View container) {
+    }
+
+    /**
+     * Create the page for the given position.  The adapter is responsible
+     * for adding the view to the container given here, although it only
+     * must ensure this is done by the time it returns from
+     * {@link #finishUpdate(android.view.ViewGroup)}.
+     *
+     * @param container The containing View in which the page will be shown.
+     * @param position The page position to be instantiated.
+     * @return Returns an Object representing the new page.  This does not
+     * need to be a View, but can be some other container of the page.
+     *
+     * @deprecated Use {@link #instantiateItem(android.view.ViewGroup, int)}
+     */
+    public Object instantiateItem(View container, int position) {
+        throw new UnsupportedOperationException(
+                "Required method instantiateItem was not overridden");
+    }
+
+    /**
+     * Remove a page for the given position.  The adapter is responsible
+     * for removing the view from its container, although it only must ensure
+     * this is done by the time it returns from {@link #finishUpdate(android.view.View)}.
+     *
+     * @param container The containing View from which the page will be removed.
+     * @param position The page position to be removed.
+     * @param object The same object that was returned by
+     * {@link #instantiateItem(android.view.View, int)}.
+     *
+     * @deprecated Use {@link #destroyItem(android.view.ViewGroup, int, Object)}
+     */
+    public void destroyItem(View container, int position, Object object) {
+        throw new UnsupportedOperationException("Required method destroyItem was not overridden");
+    }
+
+    /**
+     * Called to inform the adapter of which item is currently considered to
+     * be the "primary", that is the one show to the user as the current page.
+     *
+     * @param container The containing View from which the page will be removed.
+     * @param position The page position that is now the primary.
+     * @param object The same object that was returned by
+     * {@link #instantiateItem(android.view.View, int)}.
+     *
+     * @deprecated Use {@link #setPrimaryItem(android.view.ViewGroup, int, Object)}
+     */
+    public void setPrimaryItem(View container, int position, Object object) {
+    }
+
+    /**
+     * Called when the a change in the shown pages has been completed.  At this
+     * point you must ensure that all of the pages have actually been added or
+     * removed from the container as appropriate.
+     * @param container The containing View which is displaying this adapter's
+     * page views.
+     *
+     * @deprecated Use {@link #finishUpdate(android.view.ViewGroup)}
+     */
+    public void finishUpdate(View container) {
+    }
+
+    /**
+     * Determines whether a page View is associated with a specific key object
+     * as returned by {@link #instantiateItem(android.view.ViewGroup, int)}. This method is
+     * required for a PagerAdapter to function properly.
+     *
+     * @param view Page View to check for association with <code>object</code>
+     * @param object Object to check for association with <code>view</code>
+     * @return true if <code>view</code> is associated with the key object <code>object</code>
+     */
+    public abstract boolean isViewFromObject(View view, Object object);
+
+    /**
+     * Save any instance state associated with this adapter and its pages that should be
+     * restored if the current UI state needs to be reconstructed.
+     *
+     * @return Saved state for this adapter
+     */
+    public Parcelable saveState() {
+        return null;
+    }
+
+    /**
+     * Restore any instance state associated with this adapter and its pages
+     * that was previously saved by {@link #saveState()}.
+     *
+     * @param state State previously saved by a call to {@link #saveState()}
+     * @param loader A ClassLoader that should be used to instantiate any restored objects
+     */
+    public void restoreState(Parcelable state, ClassLoader loader) {
+    }
+
+    /**
+     * Called when the host view is attempting to determine if an item's position
+     * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
+     * item has not changed or {@link #POSITION_NONE} if the item is no longer present
+     * in the adapter.
+     *
+     * <p>The default implementation assumes that items will never
+     * change position and always returns {@link #POSITION_UNCHANGED}.
+     *
+     * @param object Object representing an item, previously returned by a call to
+     *               {@link #instantiateItem(android.view.View, int)}.
+     * @return object's new position index from [0, {@link #getCount()}),
+     *         {@link #POSITION_UNCHANGED} if the object's position has not changed,
+     *         or {@link #POSITION_NONE} if the item is no longer present.
+     */
+    public int getItemPosition(Object object) {
+        return POSITION_UNCHANGED;
+    }
+
+    /**
+     * This method should be called by the application if the data backing this adapter has changed
+     * and associated views should update.
+     */
+    public void notifyDataSetChanged() {
+        mObservable.notifyChanged();
+    }
+
+    /**
+     * Register an observer to receive callbacks related to the adapter's data changing.
+     *
+     * @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
+     */
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mObservable.registerObserver(observer);
+    }
+
+    /**
+     * Unregister an observer from callbacks related to the adapter's data changing.
+     *
+     * @param observer The {@link android.database.DataSetObserver} which will be unregistered.
+     */
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mObservable.unregisterObserver(observer);
+    }
+
+    /**
+     * This method may be called by the ViewPager to obtain a title string
+     * to describe the specified page. This method may return null
+     * indicating no title for this page. The default implementation returns
+     * null.
+     *
+     * @param position The position of the title requested
+     * @return A title for the requested page
+     */
+    public CharSequence getPageTitle(int position) {
+        return null;
+    }
+
+    /**
+     * Returns the proportional width of a given page as a percentage of the
+     * ViewPager's measured width from (0.f-1.f]
+     *
+     * @param position The position of the page requested
+     * @return Proportional width for the given page position
+     */
+    public float getPageWidth(int position) {
+        return 1.f;
+    }
+}
diff --git a/com/android/internal/widget/PointerLocationView.java b/com/android/internal/widget/PointerLocationView.java
new file mode 100644
index 0000000..e53162c
--- /dev/null
+++ b/com/android/internal/widget/PointerLocationView.java
@@ -0,0 +1,885 @@
+/*
+ * Copyright (C) 2010 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Paint.FontMetricsInt;
+import android.hardware.input.InputManager;
+import android.hardware.input.InputManager.InputDeviceListener;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.Slog;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowManagerPolicy.PointerEventListener;
+import android.view.MotionEvent.PointerCoords;
+
+import java.util.ArrayList;
+
+public class PointerLocationView extends View implements InputDeviceListener,
+        PointerEventListener {
+    private static final String TAG = "Pointer";
+
+    // The system property key used to specify an alternate velocity tracker strategy
+    // to plot alongside the default one.  Useful for testing and comparison purposes.
+    private static final String ALT_STRATEGY_PROPERY_KEY = "debug.velocitytracker.alt";
+
+    public static class PointerState {
+        // Trace of previous points.
+        private float[] mTraceX = new float[32];
+        private float[] mTraceY = new float[32];
+        private boolean[] mTraceCurrent = new boolean[32];
+        private int mTraceCount;
+        
+        // True if the pointer is down.
+        private boolean mCurDown;
+        
+        // Most recent coordinates.
+        private PointerCoords mCoords = new PointerCoords();
+        private int mToolType;
+        
+        // Most recent velocity.
+        private float mXVelocity;
+        private float mYVelocity;
+        private float mAltXVelocity;
+        private float mAltYVelocity;
+
+        // Current bounding box, if any
+        private boolean mHasBoundingBox;
+        private float mBoundingLeft;
+        private float mBoundingTop;
+        private float mBoundingRight;
+        private float mBoundingBottom;
+
+        // Position estimator.
+        private VelocityTracker.Estimator mEstimator = new VelocityTracker.Estimator();
+        private VelocityTracker.Estimator mAltEstimator = new VelocityTracker.Estimator();
+
+        public void clearTrace() {
+            mTraceCount = 0;
+        }
+        
+        public void addTrace(float x, float y, boolean current) {
+            int traceCapacity = mTraceX.length;
+            if (mTraceCount == traceCapacity) {
+                traceCapacity *= 2;
+                float[] newTraceX = new float[traceCapacity];
+                System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
+                mTraceX = newTraceX;
+                
+                float[] newTraceY = new float[traceCapacity];
+                System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
+                mTraceY = newTraceY;
+
+                boolean[] newTraceCurrent = new boolean[traceCapacity];
+                System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount);
+                mTraceCurrent= newTraceCurrent;
+            }
+            
+            mTraceX[mTraceCount] = x;
+            mTraceY[mTraceCount] = y;
+            mTraceCurrent[mTraceCount] = current;
+            mTraceCount += 1;
+        }
+    }
+
+    private final int ESTIMATE_PAST_POINTS = 4;
+    private final int ESTIMATE_FUTURE_POINTS = 2;
+    private final float ESTIMATE_INTERVAL = 0.02f;
+
+    private final InputManager mIm;
+
+    private final ViewConfiguration mVC;
+    private final Paint mTextPaint;
+    private final Paint mTextBackgroundPaint;
+    private final Paint mTextLevelPaint;
+    private final Paint mPaint;
+    private final Paint mCurrentPointPaint;
+    private final Paint mTargetPaint;
+    private final Paint mPathPaint;
+    private final FontMetricsInt mTextMetrics = new FontMetricsInt();
+    private int mHeaderBottom;
+    private boolean mCurDown;
+    private int mCurNumPointers;
+    private int mMaxNumPointers;
+    private int mActivePointerId;
+    private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>();
+    private final PointerCoords mTempCoords = new PointerCoords();
+    
+    private final VelocityTracker mVelocity;
+    private final VelocityTracker mAltVelocity;
+
+    private final FasterStringBuilder mText = new FasterStringBuilder();
+    
+    private boolean mPrintCoords = true;
+    
+    public PointerLocationView(Context c) {
+        super(c);
+        setFocusableInTouchMode(true);
+
+        mIm = c.getSystemService(InputManager.class);
+
+        mVC = ViewConfiguration.get(c);
+        mTextPaint = new Paint();
+        mTextPaint.setAntiAlias(true);
+        mTextPaint.setTextSize(10
+                * getResources().getDisplayMetrics().density);
+        mTextPaint.setARGB(255, 0, 0, 0);
+        mTextBackgroundPaint = new Paint();
+        mTextBackgroundPaint.setAntiAlias(false);
+        mTextBackgroundPaint.setARGB(128, 255, 255, 255);
+        mTextLevelPaint = new Paint();
+        mTextLevelPaint.setAntiAlias(false);
+        mTextLevelPaint.setARGB(192, 255, 0, 0);
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setARGB(255, 255, 255, 255);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(2);
+        mCurrentPointPaint = new Paint();
+        mCurrentPointPaint.setAntiAlias(true);
+        mCurrentPointPaint.setARGB(255, 255, 0, 0);
+        mCurrentPointPaint.setStyle(Paint.Style.STROKE);
+        mCurrentPointPaint.setStrokeWidth(2);
+        mTargetPaint = new Paint();
+        mTargetPaint.setAntiAlias(false);
+        mTargetPaint.setARGB(255, 0, 0, 192);
+        mPathPaint = new Paint();
+        mPathPaint.setAntiAlias(false);
+        mPathPaint.setARGB(255, 0, 96, 255);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(1);
+        
+        PointerState ps = new PointerState();
+        mPointers.add(ps);
+        mActivePointerId = 0;
+        
+        mVelocity = VelocityTracker.obtain();
+
+        String altStrategy = SystemProperties.get(ALT_STRATEGY_PROPERY_KEY);
+        if (altStrategy.length() != 0) {
+            Log.d(TAG, "Comparing default velocity tracker strategy with " + altStrategy);
+            mAltVelocity = VelocityTracker.obtain(altStrategy);
+        } else {
+            mAltVelocity = null;
+        }
+    }
+
+    public void setPrintCoords(boolean state) {
+        mPrintCoords = state;
+    }
+    
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        mTextPaint.getFontMetricsInt(mTextMetrics);
+        mHeaderBottom = -mTextMetrics.ascent+mTextMetrics.descent+2;
+        if (false) {
+            Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
+                    + " descent=" + mTextMetrics.descent
+                    + " leading=" + mTextMetrics.leading
+                    + " top=" + mTextMetrics.top
+                    + " bottom=" + mTextMetrics.bottom);
+        }
+    }
+    
+    // Draw an oval.  When angle is 0 radians, orients the major axis vertically,
+    // angles less than or greater than 0 radians rotate the major axis left or right.
+    private RectF mReusableOvalRect = new RectF();
+    private void drawOval(Canvas canvas, float x, float y, float major, float minor,
+            float angle, Paint paint) {
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.rotate((float) (angle * 180 / Math.PI), x, y);
+        mReusableOvalRect.left = x - minor / 2;
+        mReusableOvalRect.right = x + minor / 2;
+        mReusableOvalRect.top = y - major / 2;
+        mReusableOvalRect.bottom = y + major / 2;
+        canvas.drawOval(mReusableOvalRect, paint);
+        canvas.restore();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        final int w = getWidth();
+        final int itemW = w/7;
+        final int base = -mTextMetrics.ascent+1;
+        final int bottom = mHeaderBottom;
+
+        final int NP = mPointers.size();
+
+        // Labels
+        if (mActivePointerId >= 0) {
+            final PointerState ps = mPointers.get(mActivePointerId);
+            
+            canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
+            canvas.drawText(mText.clear()
+                    .append("P: ").append(mCurNumPointers)
+                    .append(" / ").append(mMaxNumPointers)
+                    .toString(), 1, base, mTextPaint);
+
+            final int N = ps.mTraceCount;
+            if ((mCurDown && ps.mCurDown) || N == 0) {
+                canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
+                canvas.drawText(mText.clear()
+                        .append("X: ").append(ps.mCoords.x, 1)
+                        .toString(), 1 + itemW, base, mTextPaint);
+                canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
+                canvas.drawText(mText.clear()
+                        .append("Y: ").append(ps.mCoords.y, 1)
+                        .toString(), 1 + itemW * 2, base, mTextPaint);
+            } else {
+                float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
+                float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
+                canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
+                        Math.abs(dx) < mVC.getScaledTouchSlop()
+                        ? mTextBackgroundPaint : mTextLevelPaint);
+                canvas.drawText(mText.clear()
+                        .append("dX: ").append(dx, 1)
+                        .toString(), 1 + itemW, base, mTextPaint);
+                canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
+                        Math.abs(dy) < mVC.getScaledTouchSlop()
+                        ? mTextBackgroundPaint : mTextLevelPaint);
+                canvas.drawText(mText.clear()
+                        .append("dY: ").append(dy, 1)
+                        .toString(), 1 + itemW * 2, base, mTextPaint);
+            }
+
+            canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
+            canvas.drawText(mText.clear()
+                    .append("Xv: ").append(ps.mXVelocity, 3)
+                    .toString(), 1 + itemW * 3, base, mTextPaint);
+
+            canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
+            canvas.drawText(mText.clear()
+                    .append("Yv: ").append(ps.mYVelocity, 3)
+                    .toString(), 1 + itemW * 4, base, mTextPaint);
+
+            canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
+            canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
+                    bottom, mTextLevelPaint);
+            canvas.drawText(mText.clear()
+                    .append("Prs: ").append(ps.mCoords.pressure, 2)
+                    .toString(), 1 + itemW * 5, base, mTextPaint);
+
+            canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
+            canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
+                    bottom, mTextLevelPaint);
+            canvas.drawText(mText.clear()
+                    .append("Size: ").append(ps.mCoords.size, 2)
+                    .toString(), 1 + itemW * 6, base, mTextPaint);
+        }
+
+        // Pointer trace.
+        for (int p = 0; p < NP; p++) {
+            final PointerState ps = mPointers.get(p);
+
+            // Draw path.
+            final int N = ps.mTraceCount;
+            float lastX = 0, lastY = 0;
+            boolean haveLast = false;
+            boolean drawn = false;
+            mPaint.setARGB(255, 128, 255, 255);
+            for (int i=0; i < N; i++) {
+                float x = ps.mTraceX[i];
+                float y = ps.mTraceY[i];
+                if (Float.isNaN(x)) {
+                    haveLast = false;
+                    continue;
+                }
+                if (haveLast) {
+                    canvas.drawLine(lastX, lastY, x, y, mPathPaint);
+                    final Paint paint = ps.mTraceCurrent[i] ? mCurrentPointPaint : mPaint;
+                    canvas.drawPoint(lastX, lastY, paint);
+                    drawn = true;
+                }
+                lastX = x;
+                lastY = y;
+                haveLast = true;
+            }
+
+            if (drawn) {
+                // Draw movement estimate curve.
+                mPaint.setARGB(128, 128, 0, 128);
+                float lx = ps.mEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
+                float ly = ps.mEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
+                for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {
+                    float x = ps.mEstimator.estimateX(i * ESTIMATE_INTERVAL);
+                    float y = ps.mEstimator.estimateY(i * ESTIMATE_INTERVAL);
+                    canvas.drawLine(lx, ly, x, y, mPaint);
+                    lx = x;
+                    ly = y;
+                }
+
+                // Draw velocity vector.
+                mPaint.setARGB(255, 255, 64, 128);
+                float xVel = ps.mXVelocity * (1000 / 60);
+                float yVel = ps.mYVelocity * (1000 / 60);
+                canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+
+                // Draw alternate estimate.
+                if (mAltVelocity != null) {
+                    mPaint.setARGB(128, 0, 128, 128);
+                    lx = ps.mAltEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
+                    ly = ps.mAltEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
+                    for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {
+                        float x = ps.mAltEstimator.estimateX(i * ESTIMATE_INTERVAL);
+                        float y = ps.mAltEstimator.estimateY(i * ESTIMATE_INTERVAL);
+                        canvas.drawLine(lx, ly, x, y, mPaint);
+                        lx = x;
+                        ly = y;
+                    }
+
+                    mPaint.setARGB(255, 64, 255, 128);
+                    xVel = ps.mAltXVelocity * (1000 / 60);
+                    yVel = ps.mAltYVelocity * (1000 / 60);
+                    canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+                }
+            }
+
+            if (mCurDown && ps.mCurDown) {
+                // Draw crosshairs.
+                canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
+                canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
+
+                // Draw current point.
+                int pressureLevel = (int)(ps.mCoords.pressure * 255);
+                mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
+                canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
+
+                // Draw current touch ellipse.
+                mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
+                drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
+                        ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
+
+                // Draw current tool ellipse.
+                mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
+                drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
+                        ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
+
+                // Draw the orientation arrow.
+                float arrowSize = ps.mCoords.toolMajor * 0.7f;
+                if (arrowSize < 20) {
+                    arrowSize = 20;
+                }
+                mPaint.setARGB(255, pressureLevel, 255, 0);
+                float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
+                        * arrowSize);
+                float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)
+                        * arrowSize);
+                if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS
+                        || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {
+                    // Show full circle orientation.
+                    canvas.drawLine(ps.mCoords.x, ps.mCoords.y,
+                            ps.mCoords.x + orientationVectorX,
+                            ps.mCoords.y + orientationVectorY,
+                            mPaint);
+                } else {
+                    // Show half circle orientation.
+                    canvas.drawLine(
+                            ps.mCoords.x - orientationVectorX,
+                            ps.mCoords.y - orientationVectorY,
+                            ps.mCoords.x + orientationVectorX,
+                            ps.mCoords.y + orientationVectorY,
+                            mPaint);
+                }
+
+                // Draw the tilt point along the orientation arrow.
+                float tiltScale = (float) Math.sin(
+                        ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));
+                canvas.drawCircle(
+                        ps.mCoords.x + orientationVectorX * tiltScale,
+                        ps.mCoords.y + orientationVectorY * tiltScale,
+                        3.0f, mPaint);
+
+                // Draw the current bounding box
+                if (ps.mHasBoundingBox) {
+                    canvas.drawRect(ps.mBoundingLeft, ps.mBoundingTop,
+                            ps.mBoundingRight, ps.mBoundingBottom, mPaint);
+                }
+            }
+        }
+    }
+
+    private void logMotionEvent(String type, MotionEvent event) {
+        final int action = event.getAction();
+        final int N = event.getHistorySize();
+        final int NI = event.getPointerCount();
+        for (int historyPos = 0; historyPos < N; historyPos++) {
+            for (int i = 0; i < NI; i++) {
+                final int id = event.getPointerId(i);
+                event.getHistoricalPointerCoords(i, historyPos, mTempCoords);
+                logCoords(type, action, i, mTempCoords, id, event);
+            }
+        }
+        for (int i = 0; i < NI; i++) {
+            final int id = event.getPointerId(i);
+            event.getPointerCoords(i, mTempCoords);
+            logCoords(type, action, i, mTempCoords, id, event);
+        }
+    }
+
+    private void logCoords(String type, int action, int index,
+            MotionEvent.PointerCoords coords, int id, MotionEvent event) {
+        final int toolType = event.getToolType(index);
+        final int buttonState = event.getButtonState();
+        final String prefix;
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:
+                prefix = "DOWN";
+                break;
+            case MotionEvent.ACTION_UP:
+                prefix = "UP";
+                break;
+            case MotionEvent.ACTION_MOVE:
+                prefix = "MOVE";
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                prefix = "CANCEL";
+                break;
+            case MotionEvent.ACTION_OUTSIDE:
+                prefix = "OUTSIDE";
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
+                    prefix = "DOWN";
+                } else {
+                    prefix = "MOVE";
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
+                    prefix = "UP";
+                } else {
+                    prefix = "MOVE";
+                }
+                break;
+            case MotionEvent.ACTION_HOVER_MOVE:
+                prefix = "HOVER MOVE";
+                break;
+            case MotionEvent.ACTION_HOVER_ENTER:
+                prefix = "HOVER ENTER";
+                break;
+            case MotionEvent.ACTION_HOVER_EXIT:
+                prefix = "HOVER EXIT";
+                break;
+            case MotionEvent.ACTION_SCROLL:
+                prefix = "SCROLL";
+                break;
+            default:
+                prefix = Integer.toString(action);
+                break;
+        }
+
+        Log.i(TAG, mText.clear()
+                .append(type).append(" id ").append(id + 1)
+                .append(": ")
+                .append(prefix)
+                .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3)
+                .append(") Pressure=").append(coords.pressure, 3)
+                .append(" Size=").append(coords.size, 3)
+                .append(" TouchMajor=").append(coords.touchMajor, 3)
+                .append(" TouchMinor=").append(coords.touchMinor, 3)
+                .append(" ToolMajor=").append(coords.toolMajor, 3)
+                .append(" ToolMinor=").append(coords.toolMinor, 3)
+                .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
+                .append("deg")
+                .append(" Tilt=").append((float)(
+                        coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
+                .append("deg")
+                .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
+                .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1)
+                .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1)
+                .append(" BoundingBox=[(")
+                .append(event.getAxisValue(MotionEvent.AXIS_GENERIC_1), 3)
+                .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_2), 3).append(")")
+                .append(", (").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_3), 3)
+                .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_4), 3)
+                .append(")]")
+                .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType))
+                .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState))
+                .toString());
+    }
+
+    @Override
+    public void onPointerEvent(MotionEvent event) {
+        final int action = event.getAction();
+        int NP = mPointers.size();
+
+        if (action == MotionEvent.ACTION_DOWN
+                || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
+            final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
+            if (action == MotionEvent.ACTION_DOWN) {
+                for (int p=0; p<NP; p++) {
+                    final PointerState ps = mPointers.get(p);
+                    ps.clearTrace();
+                    ps.mCurDown = false;
+                }
+                mCurDown = true;
+                mCurNumPointers = 0;
+                mMaxNumPointers = 0;
+                mVelocity.clear();
+                if (mAltVelocity != null) {
+                    mAltVelocity.clear();
+                }
+            }
+
+            mCurNumPointers += 1;
+            if (mMaxNumPointers < mCurNumPointers) {
+                mMaxNumPointers = mCurNumPointers;
+            }
+
+            final int id = event.getPointerId(index);
+            while (NP <= id) {
+                PointerState ps = new PointerState();
+                mPointers.add(ps);
+                NP++;
+            }
+
+            if (mActivePointerId < 0 ||
+                    !mPointers.get(mActivePointerId).mCurDown) {
+                mActivePointerId = id;
+            }
+
+            final PointerState ps = mPointers.get(id);
+            ps.mCurDown = true;
+            InputDevice device = InputDevice.getDevice(event.getDeviceId());
+            ps.mHasBoundingBox = device != null &&
+                    device.getMotionRange(MotionEvent.AXIS_GENERIC_1) != null;
+        }
+
+        final int NI = event.getPointerCount();
+
+        mVelocity.addMovement(event);
+        mVelocity.computeCurrentVelocity(1);
+        if (mAltVelocity != null) {
+            mAltVelocity.addMovement(event);
+            mAltVelocity.computeCurrentVelocity(1);
+        }
+
+        final int N = event.getHistorySize();
+        for (int historyPos = 0; historyPos < N; historyPos++) {
+            for (int i = 0; i < NI; i++) {
+                final int id = event.getPointerId(i);
+                final PointerState ps = mCurDown ? mPointers.get(id) : null;
+                final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
+                event.getHistoricalPointerCoords(i, historyPos, coords);
+                if (mPrintCoords) {
+                    logCoords("Pointer", action, i, coords, id, event);
+                }
+                if (ps != null) {
+                    ps.addTrace(coords.x, coords.y, false);
+                }
+            }
+        }
+        for (int i = 0; i < NI; i++) {
+            final int id = event.getPointerId(i);
+            final PointerState ps = mCurDown ? mPointers.get(id) : null;
+            final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
+            event.getPointerCoords(i, coords);
+            if (mPrintCoords) {
+                logCoords("Pointer", action, i, coords, id, event);
+            }
+            if (ps != null) {
+                ps.addTrace(coords.x, coords.y, true);
+                ps.mXVelocity = mVelocity.getXVelocity(id);
+                ps.mYVelocity = mVelocity.getYVelocity(id);
+                mVelocity.getEstimator(id, ps.mEstimator);
+                if (mAltVelocity != null) {
+                    ps.mAltXVelocity = mAltVelocity.getXVelocity(id);
+                    ps.mAltYVelocity = mAltVelocity.getYVelocity(id);
+                    mAltVelocity.getEstimator(id, ps.mAltEstimator);
+                }
+                ps.mToolType = event.getToolType(i);
+
+                if (ps.mHasBoundingBox) {
+                    ps.mBoundingLeft = event.getAxisValue(MotionEvent.AXIS_GENERIC_1, i);
+                    ps.mBoundingTop = event.getAxisValue(MotionEvent.AXIS_GENERIC_2, i);
+                    ps.mBoundingRight = event.getAxisValue(MotionEvent.AXIS_GENERIC_3, i);
+                    ps.mBoundingBottom = event.getAxisValue(MotionEvent.AXIS_GENERIC_4, i);
+                }
+            }
+        }
+
+        if (action == MotionEvent.ACTION_UP
+                || action == MotionEvent.ACTION_CANCEL
+                || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
+            final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
+
+            final int id = event.getPointerId(index);
+            if (id >= NP) {
+                Slog.wtf(TAG, "Got pointer ID out of bounds: id=" + id + " arraysize="
+                        + NP + " pointerindex=" + index
+                        + " action=0x" + Integer.toHexString(action));
+                return;
+            }
+            final PointerState ps = mPointers.get(id);
+            ps.mCurDown = false;
+
+            if (action == MotionEvent.ACTION_UP
+                    || action == MotionEvent.ACTION_CANCEL) {
+                mCurDown = false;
+                mCurNumPointers = 0;
+            } else {
+                mCurNumPointers -= 1;
+                if (mActivePointerId == id) {
+                    mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
+                }
+                ps.addTrace(Float.NaN, Float.NaN, false);
+            }
+        }
+
+        invalidate();
+    }
+    
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        onPointerEvent(event);
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) {
+            requestFocus();
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        final int source = event.getSource();
+        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            onPointerEvent(event);
+        } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+            logMotionEvent("Joystick", event);
+        } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) {
+            logMotionEvent("Position", event);
+        } else {
+            logMotionEvent("Generic", event);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (shouldLogKey(keyCode)) {
+            final int repeatCount = event.getRepeatCount();
+            if (repeatCount == 0) {
+                Log.i(TAG, "Key Down: " + event);
+            } else {
+                Log.i(TAG, "Key Repeat #" + repeatCount + ": " + event);
+            }
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (shouldLogKey(keyCode)) {
+            Log.i(TAG, "Key Up: " + event);
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    private static boolean shouldLogKey(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_UP:
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                return true;
+            default:
+                return KeyEvent.isGamepadButton(keyCode)
+                    || KeyEvent.isModifierKey(keyCode);
+        }
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent event) {
+        logMotionEvent("Trackball", event);
+        return true;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mIm.registerInputDeviceListener(this, getHandler());
+        logInputDevices();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mIm.unregisterInputDeviceListener(this);
+    }
+
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+        logInputDeviceState(deviceId, "Device Added");
+    }
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+        logInputDeviceState(deviceId, "Device Changed");
+    }
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+        logInputDeviceState(deviceId, "Device Removed");
+    }
+
+    private void logInputDevices() {
+        int[] deviceIds = InputDevice.getDeviceIds();
+        for (int i = 0; i < deviceIds.length; i++) {
+            logInputDeviceState(deviceIds[i], "Device Enumerated");
+        }
+    }
+
+    private void logInputDeviceState(int deviceId, String state) {
+        InputDevice device = mIm.getInputDevice(deviceId);
+        if (device != null) {
+            Log.i(TAG, state + ": " + device);
+        } else {
+            Log.i(TAG, state + ": " + deviceId);
+        }
+    }
+
+    // HACK
+    // A quick and dirty string builder implementation optimized for GC.
+    // Using String.format causes the application grind to a halt when
+    // more than a couple of pointers are down due to the number of
+    // temporary objects allocated while formatting strings for drawing or logging.
+    private static final class FasterStringBuilder {
+        private char[] mChars;
+        private int mLength;
+        
+        public FasterStringBuilder() {
+            mChars = new char[64];
+        }
+        
+        public FasterStringBuilder clear() {
+            mLength = 0;
+            return this;
+        }
+        
+        public FasterStringBuilder append(String value) {
+            final int valueLength = value.length();
+            final int index = reserve(valueLength);
+            value.getChars(0, valueLength, mChars, index);
+            mLength += valueLength;
+            return this;
+        }
+        
+        public FasterStringBuilder append(int value) {
+            return append(value, 0);
+        }
+        
+        public FasterStringBuilder append(int value, int zeroPadWidth) {
+            final boolean negative = value < 0;
+            if (negative) {
+                value = - value;
+                if (value < 0) {
+                    append("-2147483648");
+                    return this;
+                }
+            }
+            
+            int index = reserve(11);
+            final char[] chars = mChars;
+            
+            if (value == 0) {
+                chars[index++] = '0';
+                mLength += 1;
+                return this;
+            }
+            
+            if (negative) {
+                chars[index++] = '-';
+            }
+
+            int divisor = 1000000000;
+            int numberWidth = 10;
+            while (value < divisor) {
+                divisor /= 10;
+                numberWidth -= 1;
+                if (numberWidth < zeroPadWidth) {
+                    chars[index++] = '0';
+                }
+            }
+            
+            do {
+                int digit = value / divisor;
+                value -= digit * divisor;
+                divisor /= 10;
+                chars[index++] = (char) (digit + '0');
+            } while (divisor != 0);
+            
+            mLength = index;
+            return this;
+        }
+        
+        public FasterStringBuilder append(float value, int precision) {
+            int scale = 1;
+            for (int i = 0; i < precision; i++) {
+                scale *= 10;
+            }
+            value = (float) (Math.rint(value * scale) / scale);
+            
+            append((int) value);
+
+            if (precision != 0) {
+                append(".");
+                value = Math.abs(value);
+                value -= Math.floor(value);
+                append((int) (value * scale), precision);
+            }
+            
+            return this;
+        }
+        
+        @Override
+        public String toString() {
+            return new String(mChars, 0, mLength);
+        }
+        
+        private int reserve(int length) {
+            final int oldLength = mLength;
+            final int newLength = mLength + length;
+            final char[] oldChars = mChars;
+            final int oldCapacity = oldChars.length;
+            if (newLength > oldCapacity) {
+                final int newCapacity = oldCapacity * 2;
+                final char[] newChars = new char[newCapacity];
+                System.arraycopy(oldChars, 0, newChars, 0, oldLength);
+                mChars = newChars;
+            }
+            return oldLength;
+        }
+    }
+}
diff --git a/com/android/internal/widget/PreferenceImageView.java b/com/android/internal/widget/PreferenceImageView.java
new file mode 100644
index 0000000..8730cda
--- /dev/null
+++ b/com/android/internal/widget/PreferenceImageView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Extension of ImageView that correctly applies maxWidth and maxHeight.
+ */
+public class PreferenceImageView extends ImageView {
+
+    public PreferenceImageView(Context context) {
+        this(context, null);
+    }
+
+    public PreferenceImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
+            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+            final int maxWidth = getMaxWidth();
+            if (maxWidth != Integer.MAX_VALUE
+                    && (maxWidth < widthSize || widthMode == MeasureSpec.UNSPECIFIED)) {
+                widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
+            }
+        }
+
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
+            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+            final int maxHeight = getMaxHeight();
+            if (maxHeight != Integer.MAX_VALUE
+                    && (maxHeight < heightSize || heightMode == MeasureSpec.UNSPECIFIED)) {
+                heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
+            }
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}
diff --git a/com/android/internal/widget/RecyclerView.java b/com/android/internal/widget/RecyclerView.java
new file mode 100644
index 0000000..7abc76a
--- /dev/null
+++ b/com/android/internal/widget/RecyclerView.java
@@ -0,0 +1,12248 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Observable;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.AbsSavedState;
+import android.view.Display;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
+import android.widget.EdgeEffect;
+import android.widget.OverScroller;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A flexible view for providing a limited window into a large data set.
+ *
+ * <h3>Glossary of terms:</h3>
+ *
+ * <ul>
+ *     <li><em>Adapter:</em> A subclass of {@link Adapter} responsible for providing views
+ *     that represent items in a data set.</li>
+ *     <li><em>Position:</em> The position of a data item within an <em>Adapter</em>.</li>
+ *     <li><em>Index:</em> The index of an attached child view as used in a call to
+ *     {@link ViewGroup#getChildAt}. Contrast with <em>Position.</em></li>
+ *     <li><em>Binding:</em> The process of preparing a child view to display data corresponding
+ *     to a <em>position</em> within the adapter.</li>
+ *     <li><em>Recycle (view):</em> A view previously used to display data for a specific adapter
+ *     position may be placed in a cache for later reuse to display the same type of data again
+ *     later. This can drastically improve performance by skipping initial layout inflation
+ *     or construction.</li>
+ *     <li><em>Scrap (view):</em> A child view that has entered into a temporarily detached
+ *     state during layout. Scrap views may be reused without becoming fully detached
+ *     from the parent RecyclerView, either unmodified if no rebinding is required or modified
+ *     by the adapter if the view was considered <em>dirty</em>.</li>
+ *     <li><em>Dirty (view):</em> A child view that must be rebound by the adapter before
+ *     being displayed.</li>
+ * </ul>
+ *
+ * <h4>Positions in RecyclerView:</h4>
+ * <p>
+ * RecyclerView introduces an additional level of abstraction between the {@link Adapter} and
+ * {@link LayoutManager} to be able to detect data set changes in batches during a layout
+ * calculation. This saves LayoutManager from tracking adapter changes to calculate animations.
+ * It also helps with performance because all view bindings happen at the same time and unnecessary
+ * bindings are avoided.
+ * <p>
+ * For this reason, there are two types of <code>position</code> related methods in RecyclerView:
+ * <ul>
+ *     <li>layout position: Position of an item in the latest layout calculation. This is the
+ *     position from the LayoutManager's perspective.</li>
+ *     <li>adapter position: Position of an item in the adapter. This is the position from
+ *     the Adapter's perspective.</li>
+ * </ul>
+ * <p>
+ * These two positions are the same except the time between dispatching <code>adapter.notify*
+ * </code> events and calculating the updated layout.
+ * <p>
+ * Methods that return or receive <code>*LayoutPosition*</code> use position as of the latest
+ * layout calculation (e.g. {@link ViewHolder#getLayoutPosition()},
+ * {@link #findViewHolderForLayoutPosition(int)}). These positions include all changes until the
+ * last layout calculation. You can rely on these positions to be consistent with what user is
+ * currently seeing on the screen. For example, if you have a list of items on the screen and user
+ * asks for the 5<sup>th</sup> element, you should use these methods as they'll match what user
+ * is seeing.
+ * <p>
+ * The other set of position related methods are in the form of
+ * <code>*AdapterPosition*</code>. (e.g. {@link ViewHolder#getAdapterPosition()},
+ * {@link #findViewHolderForAdapterPosition(int)}) You should use these methods when you need to
+ * work with up-to-date adapter positions even if they may not have been reflected to layout yet.
+ * For example, if you want to access the item in the adapter on a ViewHolder click, you should use
+ * {@link ViewHolder#getAdapterPosition()}. Beware that these methods may not be able to calculate
+ * adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new layout has
+ * not yet been calculated. For this reasons, you should carefully handle {@link #NO_POSITION} or
+ * <code>null</code> results from these methods.
+ * <p>
+ * When writing a {@link LayoutManager} you almost always want to use layout positions whereas when
+ * writing an {@link Adapter}, you probably want to use adapter positions.
+ */
+public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
+
+    static final String TAG = "RecyclerView";
+
+    static final boolean DEBUG = false;
+
+    private static final int[]  NESTED_SCROLLING_ATTRS = { android.R.attr.nestedScrollingEnabled };
+
+    private static final int[] CLIP_TO_PADDING_ATTR = {android.R.attr.clipToPadding};
+
+    /**
+     * On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if
+     * a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by
+     * setting View's visibility to INVISIBLE when View is detached. On Kitkat and JB MR2, Recycler
+     * recursively traverses itemView and invalidates display list for each ViewGroup that matches
+     * this criteria.
+     */
+    static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 18
+            || Build.VERSION.SDK_INT == 19 || Build.VERSION.SDK_INT == 20;
+    /**
+     * On M+, an unspecified measure spec may include a hint which we can use. On older platforms,
+     * this value might be garbage. To save LayoutManagers from it, RecyclerView sets the size to
+     * 0 when mode is unspecified.
+     */
+    static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23;
+
+    static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16;
+
+    /**
+     * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to
+     * RenderThread but before the next frame begins. We schedule prefetch work in this window.
+     */
+    private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
+
+    /**
+     * FocusFinder#findNextFocus is broken on ICS MR1 and older for View.FOCUS_BACKWARD direction.
+     * We convert it to an absolute direction such as FOCUS_DOWN or FOCUS_LEFT.
+     */
+    private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15;
+
+    /**
+     * on API 15-, a focused child can still be considered a focused child of RV even after
+     * it's being removed or its focusable flag is set to false. This is because when this focused
+     * child is detached, the reference to this child is not removed in clearFocus. API 16 and above
+     * properly handle this case by calling ensureInputFocusOnFirstFocusable or rootViewRequestFocus
+     * to request focus on a new child, which will clear the focus on the old (detached) child as a
+     * side-effect.
+     */
+    private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15;
+
+    static final boolean DISPATCH_TEMP_DETACH = false;
+    public static final int HORIZONTAL = 0;
+    public static final int VERTICAL = 1;
+
+    public static final int NO_POSITION = -1;
+    public static final long NO_ID = -1;
+    public static final int INVALID_TYPE = -1;
+
+    /**
+     * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates
+     * that the RecyclerView should use the standard touch slop for smooth,
+     * continuous scrolling.
+     */
+    public static final int TOUCH_SLOP_DEFAULT = 0;
+
+    /**
+     * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates
+     * that the RecyclerView should use the standard touch slop for scrolling
+     * widgets that snap to a page or other coarse-grained barrier.
+     */
+    public static final int TOUCH_SLOP_PAGING = 1;
+
+    static final int MAX_SCROLL_DURATION = 2000;
+
+    /**
+     * RecyclerView is calculating a scroll.
+     * If there are too many of these in Systrace, some Views inside RecyclerView might be causing
+     * it. Try to avoid using EditText, focusable views or handle them with care.
+     */
+    static final String TRACE_SCROLL_TAG = "RV Scroll";
+
+    /**
+     * OnLayout has been called by the View system.
+     * If this shows up too many times in Systrace, make sure the children of RecyclerView do not
+     * update themselves directly. This will cause a full re-layout but when it happens via the
+     * Adapter notifyItemChanged, RecyclerView can avoid full layout calculation.
+     */
+    private static final String TRACE_ON_LAYOUT_TAG = "RV OnLayout";
+
+    /**
+     * NotifyDataSetChanged or equal has been called.
+     * If this is taking a long time, try sending granular notify adapter changes instead of just
+     * calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter
+     * might help.
+     */
+    private static final String TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG = "RV FullInvalidate";
+
+    /**
+     * RecyclerView is doing a layout for partial adapter updates (we know what has changed)
+     * If this is taking a long time, you may have dispatched too many Adapter updates causing too
+     * many Views being rebind. Make sure all are necessary and also prefer using notify*Range
+     * methods.
+     */
+    private static final String TRACE_HANDLE_ADAPTER_UPDATES_TAG = "RV PartialInvalidate";
+
+    /**
+     * RecyclerView is rebinding a View.
+     * If this is taking a lot of time, consider optimizing your layout or make sure you are not
+     * doing extra operations in onBindViewHolder call.
+     */
+    static final String TRACE_BIND_VIEW_TAG = "RV OnBindView";
+
+    /**
+     * RecyclerView is attempting to pre-populate off screen views.
+     */
+    static final String TRACE_PREFETCH_TAG = "RV Prefetch";
+
+    /**
+     * RecyclerView is attempting to pre-populate off screen itemviews within an off screen
+     * RecyclerView.
+     */
+    static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch";
+
+    /**
+     * RecyclerView is creating a new View.
+     * If too many of these present in Systrace:
+     * - There might be a problem in Recycling (e.g. custom Animations that set transient state and
+     * prevent recycling or ItemAnimator not implementing the contract properly. ({@link
+     * > Adapter#onFailedToRecycleView(ViewHolder)})
+     *
+     * - There might be too many item view types.
+     * > Try merging them
+     *
+     * - There might be too many itemChange animations and not enough space in RecyclerPool.
+     * >Try increasing your pool size and item cache size.
+     */
+    static final String TRACE_CREATE_VIEW_TAG = "RV CreateView";
+    private static final Class<?>[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE =
+            new Class[]{Context.class, AttributeSet.class, int.class, int.class};
+
+    private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
+
+    final Recycler mRecycler = new Recycler();
+
+    private SavedState mPendingSavedState;
+
+    /**
+     * Handles adapter updates
+     */
+    AdapterHelper mAdapterHelper;
+
+    /**
+     * Handles abstraction between LayoutManager children and RecyclerView children
+     */
+    ChildHelper mChildHelper;
+
+    /**
+     * Keeps data about views to be used for animations
+     */
+    final ViewInfoStore mViewInfoStore = new ViewInfoStore();
+
+    /**
+     * Prior to L, there is no way to query this variable which is why we override the setter and
+     * track it here.
+     */
+    boolean mClipToPadding;
+
+    /**
+     * Note: this Runnable is only ever posted if:
+     * 1) We've been through first layout
+     * 2) We know we have a fixed size (mHasFixedSize)
+     * 3) We're attached
+     */
+    final Runnable mUpdateChildViewsRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (!mFirstLayoutComplete || isLayoutRequested()) {
+                // a layout request will happen, we should not do layout here.
+                return;
+            }
+            if (!mIsAttached) {
+                requestLayout();
+                // if we are not attached yet, mark us as requiring layout and skip
+                return;
+            }
+            if (mLayoutFrozen) {
+                mLayoutRequestEaten = true;
+                return; //we'll process updates when ice age ends.
+            }
+            consumePendingUpdateOperations();
+        }
+    };
+
+    final Rect mTempRect = new Rect();
+    private final Rect mTempRect2 = new Rect();
+    final RectF mTempRectF = new RectF();
+    Adapter mAdapter;
+    @VisibleForTesting LayoutManager mLayout;
+    RecyclerListener mRecyclerListener;
+    final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
+    private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
+            new ArrayList<>();
+    private OnItemTouchListener mActiveOnItemTouchListener;
+    boolean mIsAttached;
+    boolean mHasFixedSize;
+    @VisibleForTesting boolean mFirstLayoutComplete;
+
+    // Counting lock to control whether we should ignore requestLayout calls from children or not.
+    private int mEatRequestLayout = 0;
+
+    boolean mLayoutRequestEaten;
+    boolean mLayoutFrozen;
+    private boolean mIgnoreMotionEventTillDown;
+
+    // binary OR of change events that were eaten during a layout or scroll.
+    private int mEatenAccessibilityChangeFlags;
+    boolean mAdapterUpdateDuringMeasure;
+
+    private final AccessibilityManager mAccessibilityManager;
+    private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners;
+
+    /**
+     * Set to true when an adapter data set changed notification is received.
+     * In that case, we cannot run any animations since we don't know what happened until layout.
+     *
+     * Attached items are invalid until next layout, at which point layout will animate/replace
+     * items as necessary, building up content from the (effectively) new adapter from scratch.
+     *
+     * Cached items must be discarded when setting this to true, so that the cache may be freely
+     * used by prefetching until the next layout occurs.
+     *
+     * @see #setDataSetChangedAfterLayout()
+     */
+    boolean mDataSetHasChangedAfterLayout = false;
+
+    /**
+     * This variable is incremented during a dispatchLayout and/or scroll.
+     * Some methods should not be called during these periods (e.g. adapter data change).
+     * Doing so will create hard to find bugs so we better check it and throw an exception.
+     *
+     * @see #assertInLayoutOrScroll(String)
+     * @see #assertNotInLayoutOrScroll(String)
+     */
+    private int mLayoutOrScrollCounter = 0;
+
+    /**
+     * Similar to mLayoutOrScrollCounter but logs a warning instead of throwing an exception
+     * (for API compatibility).
+     * <p>
+     * It is a bad practice for a developer to update the data in a scroll callback since it is
+     * potentially called during a layout.
+     */
+    private int mDispatchScrollCounter = 0;
+
+    private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
+
+    ItemAnimator mItemAnimator = new DefaultItemAnimator();
+
+    private static final int INVALID_POINTER = -1;
+
+    /**
+     * The RecyclerView is not currently scrolling.
+     * @see #getScrollState()
+     */
+    public static final int SCROLL_STATE_IDLE = 0;
+
+    /**
+     * The RecyclerView is currently being dragged by outside input such as user touch input.
+     * @see #getScrollState()
+     */
+    public static final int SCROLL_STATE_DRAGGING = 1;
+
+    /**
+     * The RecyclerView is currently animating to a final position while not under
+     * outside control.
+     * @see #getScrollState()
+     */
+    public static final int SCROLL_STATE_SETTLING = 2;
+
+    static final long FOREVER_NS = Long.MAX_VALUE;
+
+    // Touch/scrolling handling
+
+    private int mScrollState = SCROLL_STATE_IDLE;
+    private int mScrollPointerId = INVALID_POINTER;
+    private VelocityTracker mVelocityTracker;
+    private int mInitialTouchX;
+    private int mInitialTouchY;
+    private int mLastTouchX;
+    private int mLastTouchY;
+    private int mTouchSlop;
+    private OnFlingListener mOnFlingListener;
+    private final int mMinFlingVelocity;
+    private final int mMaxFlingVelocity;
+    // This value is used when handling generic motion events.
+    private float mScrollFactor = Float.MIN_VALUE;
+    private boolean mPreserveFocusAfterLayout = true;
+
+    final ViewFlinger mViewFlinger = new ViewFlinger();
+
+    GapWorker mGapWorker;
+    GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry =
+            ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null;
+
+    final State mState = new State();
+
+    private OnScrollListener mScrollListener;
+    private List<OnScrollListener> mScrollListeners;
+
+    // For use in item animations
+    boolean mItemsAddedOrRemoved = false;
+    boolean mItemsChanged = false;
+    private ItemAnimator.ItemAnimatorListener mItemAnimatorListener =
+            new ItemAnimatorRestoreListener();
+    boolean mPostedAnimatorRunner = false;
+    RecyclerViewAccessibilityDelegate mAccessibilityDelegate;
+    private ChildDrawingOrderCallback mChildDrawingOrderCallback;
+
+    // simple array to keep min and max child position during a layout calculation
+    // preserved not to create a new one in each layout pass
+    private final int[] mMinMaxLayoutPositions = new int[2];
+
+    private final int[] mScrollOffset = new int[2];
+    private final int[] mScrollConsumed = new int[2];
+    private final int[] mNestedOffsets = new int[2];
+
+    /**
+     * These are views that had their a11y importance changed during a layout. We defer these events
+     * until the end of the layout because a11y service may make sync calls back to the RV while
+     * the View's state is undefined.
+     */
+    @VisibleForTesting
+    final List<ViewHolder> mPendingAccessibilityImportanceChange = new ArrayList();
+
+    private Runnable mItemAnimatorRunner = new Runnable() {
+        @Override
+        public void run() {
+            if (mItemAnimator != null) {
+                mItemAnimator.runPendingAnimations();
+            }
+            mPostedAnimatorRunner = false;
+        }
+    };
+
+    static final Interpolator sQuinticInterpolator = new Interpolator() {
+        @Override
+        public float getInterpolation(float t) {
+            t -= 1.0f;
+            return t * t * t * t * t + 1.0f;
+        }
+    };
+
+    /**
+     * The callback to convert view info diffs into animations.
+     */
+    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
+            new ViewInfoStore.ProcessCallback() {
+        @Override
+        public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
+                @Nullable ItemHolderInfo postInfo) {
+            mRecycler.unscrapView(viewHolder);
+            animateDisappearance(viewHolder, info, postInfo);
+        }
+        @Override
+        public void processAppeared(ViewHolder viewHolder,
+                ItemHolderInfo preInfo, ItemHolderInfo info) {
+            animateAppearance(viewHolder, preInfo, info);
+        }
+
+        @Override
+        public void processPersistent(ViewHolder viewHolder,
+                @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+            viewHolder.setIsRecyclable(false);
+            if (mDataSetHasChangedAfterLayout) {
+                // since it was rebound, use change instead as we'll be mapping them from
+                // stable ids. If stable ids were false, we would not be running any
+                // animations
+                if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
+                    postAnimationRunner();
+                }
+            } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
+                postAnimationRunner();
+            }
+        }
+        @Override
+        public void unused(ViewHolder viewHolder) {
+            mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
+        }
+    };
+
+    public RecyclerView(Context context) {
+        this(context, null);
+    }
+
+    public RecyclerView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
+            mClipToPadding = a.getBoolean(0, true);
+            a.recycle();
+        } else {
+            mClipToPadding = true;
+        }
+        setScrollContainer(true);
+        setFocusableInTouchMode(true);
+
+        final ViewConfiguration vc = ViewConfiguration.get(context);
+        mTouchSlop = vc.getScaledTouchSlop();
+        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+        setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
+
+        mItemAnimator.setListener(mItemAnimatorListener);
+        initAdapterManager();
+        initChildrenHelper();
+        // If not explicitly specified this view is important for accessibility.
+        if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+        mAccessibilityManager = (AccessibilityManager) getContext()
+                .getSystemService(Context.ACCESSIBILITY_SERVICE);
+        setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this));
+        // Create the layoutManager if specified.
+
+        boolean nestedScrollingEnabled = true;
+
+        if (attrs != null) {
+            int defStyleRes = 0;
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
+                    defStyle, defStyleRes);
+            String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
+            int descendantFocusability = a.getInt(
+                    R.styleable.RecyclerView_descendantFocusability, -1);
+            if (descendantFocusability == -1) {
+                setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+            }
+            a.recycle();
+            createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);
+
+            if (Build.VERSION.SDK_INT >= 21) {
+                a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
+                        defStyle, defStyleRes);
+                nestedScrollingEnabled = a.getBoolean(0, true);
+                a.recycle();
+            }
+        } else {
+            setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+        }
+
+        // Re-set whether nested scrolling is enabled so that it is set on all API levels
+        setNestedScrollingEnabled(nestedScrollingEnabled);
+    }
+
+    /**
+     * Returns the accessibility delegate compatibility implementation used by the RecyclerView.
+     * @return An instance of AccessibilityDelegateCompat used by RecyclerView
+     */
+    public RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate() {
+        return mAccessibilityDelegate;
+    }
+
+    /**
+     * Sets the accessibility delegate compatibility implementation used by RecyclerView.
+     * @param accessibilityDelegate The accessibility delegate to be used by RecyclerView.
+     */
+    public void setAccessibilityDelegateCompat(
+            RecyclerViewAccessibilityDelegate accessibilityDelegate) {
+        mAccessibilityDelegate = accessibilityDelegate;
+        setAccessibilityDelegate(mAccessibilityDelegate);
+    }
+
+    /**
+     * Instantiate and set a LayoutManager, if specified in the attributes.
+     */
+    private void createLayoutManager(Context context, String className, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        if (className != null) {
+            className = className.trim();
+            if (className.length() != 0) {  // Can't use isEmpty since it was added in API 9.
+                className = getFullClassName(context, className);
+                try {
+                    ClassLoader classLoader;
+                    if (isInEditMode()) {
+                        // Stupid layoutlib cannot handle simple class loaders.
+                        classLoader = this.getClass().getClassLoader();
+                    } else {
+                        classLoader = context.getClassLoader();
+                    }
+                    Class<? extends LayoutManager> layoutManagerClass =
+                            classLoader.loadClass(className).asSubclass(LayoutManager.class);
+                    Constructor<? extends LayoutManager> constructor;
+                    Object[] constructorArgs = null;
+                    try {
+                        constructor = layoutManagerClass
+                                .getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE);
+                        constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes};
+                    } catch (NoSuchMethodException e) {
+                        try {
+                            constructor = layoutManagerClass.getConstructor();
+                        } catch (NoSuchMethodException e1) {
+                            e1.initCause(e);
+                            throw new IllegalStateException(attrs.getPositionDescription()
+                                    + ": Error creating LayoutManager " + className, e1);
+                        }
+                    }
+                    constructor.setAccessible(true);
+                    setLayoutManager(constructor.newInstance(constructorArgs));
+                } catch (ClassNotFoundException e) {
+                    throw new IllegalStateException(attrs.getPositionDescription()
+                            + ": Unable to find LayoutManager " + className, e);
+                } catch (InvocationTargetException e) {
+                    throw new IllegalStateException(attrs.getPositionDescription()
+                            + ": Could not instantiate the LayoutManager: " + className, e);
+                } catch (InstantiationException e) {
+                    throw new IllegalStateException(attrs.getPositionDescription()
+                            + ": Could not instantiate the LayoutManager: " + className, e);
+                } catch (IllegalAccessException e) {
+                    throw new IllegalStateException(attrs.getPositionDescription()
+                            + ": Cannot access non-public constructor " + className, e);
+                } catch (ClassCastException e) {
+                    throw new IllegalStateException(attrs.getPositionDescription()
+                            + ": Class is not a LayoutManager " + className, e);
+                }
+            }
+        }
+    }
+
+    private String getFullClassName(Context context, String className) {
+        if (className.charAt(0) == '.') {
+            return context.getPackageName() + className;
+        }
+        if (className.contains(".")) {
+            return className;
+        }
+        return RecyclerView.class.getPackage().getName() + '.' + className;
+    }
+
+    private void initChildrenHelper() {
+        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
+            @Override
+            public int getChildCount() {
+                return RecyclerView.this.getChildCount();
+            }
+
+            @Override
+            public void addView(View child, int index) {
+                RecyclerView.this.addView(child, index);
+                dispatchChildAttached(child);
+            }
+
+            @Override
+            public int indexOfChild(View view) {
+                return RecyclerView.this.indexOfChild(view);
+            }
+
+            @Override
+            public void removeViewAt(int index) {
+                final View child = RecyclerView.this.getChildAt(index);
+                if (child != null) {
+                    dispatchChildDetached(child);
+                }
+                RecyclerView.this.removeViewAt(index);
+            }
+
+            @Override
+            public View getChildAt(int offset) {
+                return RecyclerView.this.getChildAt(offset);
+            }
+
+            @Override
+            public void removeAllViews() {
+                final int count = getChildCount();
+                for (int i = 0; i < count; i++) {
+                    dispatchChildDetached(getChildAt(i));
+                }
+                RecyclerView.this.removeAllViews();
+            }
+
+            @Override
+            public ViewHolder getChildViewHolder(View view) {
+                return getChildViewHolderInt(view);
+            }
+
+            @Override
+            public void attachViewToParent(View child, int index,
+                    ViewGroup.LayoutParams layoutParams) {
+                final ViewHolder vh = getChildViewHolderInt(child);
+                if (vh != null) {
+                    if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
+                        throw new IllegalArgumentException("Called attach on a child which is not"
+                                + " detached: " + vh);
+                    }
+                    if (DEBUG) {
+                        Log.d(TAG, "reAttach " + vh);
+                    }
+                    vh.clearTmpDetachFlag();
+                }
+                RecyclerView.this.attachViewToParent(child, index, layoutParams);
+            }
+
+            @Override
+            public void detachViewFromParent(int offset) {
+                final View view = getChildAt(offset);
+                if (view != null) {
+                    final ViewHolder vh = getChildViewHolderInt(view);
+                    if (vh != null) {
+                        if (vh.isTmpDetached() && !vh.shouldIgnore()) {
+                            throw new IllegalArgumentException("called detach on an already"
+                                    + " detached child " + vh);
+                        }
+                        if (DEBUG) {
+                            Log.d(TAG, "tmpDetach " + vh);
+                        }
+                        vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
+                    }
+                }
+                RecyclerView.this.detachViewFromParent(offset);
+            }
+
+            @Override
+            public void onEnteredHiddenState(View child) {
+                final ViewHolder vh = getChildViewHolderInt(child);
+                if (vh != null) {
+                    vh.onEnteredHiddenState(RecyclerView.this);
+                }
+            }
+
+            @Override
+            public void onLeftHiddenState(View child) {
+                final ViewHolder vh = getChildViewHolderInt(child);
+                if (vh != null) {
+                    vh.onLeftHiddenState(RecyclerView.this);
+                }
+            }
+        });
+    }
+
+    void initAdapterManager() {
+        mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
+            @Override
+            public ViewHolder findViewHolder(int position) {
+                final ViewHolder vh = findViewHolderForPosition(position, true);
+                if (vh == null) {
+                    return null;
+                }
+                // ensure it is not hidden because for adapter helper, the only thing matter is that
+                // LM thinks view is a child.
+                if (mChildHelper.isHidden(vh.itemView)) {
+                    if (DEBUG) {
+                        Log.d(TAG, "assuming view holder cannot be find because it is hidden");
+                    }
+                    return null;
+                }
+                return vh;
+            }
+
+            @Override
+            public void offsetPositionsForRemovingInvisible(int start, int count) {
+                offsetPositionRecordsForRemove(start, count, true);
+                mItemsAddedOrRemoved = true;
+                mState.mDeletedInvisibleItemCountSincePreviousLayout += count;
+            }
+
+            @Override
+            public void offsetPositionsForRemovingLaidOutOrNewView(
+                    int positionStart, int itemCount) {
+                offsetPositionRecordsForRemove(positionStart, itemCount, false);
+                mItemsAddedOrRemoved = true;
+            }
+
+            @Override
+            public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
+                viewRangeUpdate(positionStart, itemCount, payload);
+                mItemsChanged = true;
+            }
+
+            @Override
+            public void onDispatchFirstPass(AdapterHelper.UpdateOp op) {
+                dispatchUpdate(op);
+            }
+
+            void dispatchUpdate(AdapterHelper.UpdateOp op) {
+                switch (op.cmd) {
+                    case AdapterHelper.UpdateOp.ADD:
+                        mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
+                        break;
+                    case AdapterHelper.UpdateOp.REMOVE:
+                        mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
+                        break;
+                    case AdapterHelper.UpdateOp.UPDATE:
+                        mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
+                                op.payload);
+                        break;
+                    case AdapterHelper.UpdateOp.MOVE:
+                        mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
+                        break;
+                }
+            }
+
+            @Override
+            public void onDispatchSecondPass(AdapterHelper.UpdateOp op) {
+                dispatchUpdate(op);
+            }
+
+            @Override
+            public void offsetPositionsForAdd(int positionStart, int itemCount) {
+                offsetPositionRecordsForInsert(positionStart, itemCount);
+                mItemsAddedOrRemoved = true;
+            }
+
+            @Override
+            public void offsetPositionsForMove(int from, int to) {
+                offsetPositionRecordsForMove(from, to);
+                // should we create mItemsMoved ?
+                mItemsAddedOrRemoved = true;
+            }
+        });
+    }
+
+    /**
+     * RecyclerView can perform several optimizations if it can know in advance that RecyclerView's
+     * size is not affected by the adapter contents. RecyclerView can still change its size based
+     * on other factors (e.g. its parent's size) but this size calculation cannot depend on the
+     * size of its children or contents of its adapter (except the number of items in the adapter).
+     * <p>
+     * If your use of RecyclerView falls into this category, set this to {@code true}. It will allow
+     * RecyclerView to avoid invalidating the whole layout when its adapter contents change.
+     *
+     * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
+     */
+    public void setHasFixedSize(boolean hasFixedSize) {
+        mHasFixedSize = hasFixedSize;
+    }
+
+    /**
+     * @return true if the app has specified that changes in adapter content cannot change
+     * the size of the RecyclerView itself.
+     */
+    public boolean hasFixedSize() {
+        return mHasFixedSize;
+    }
+
+    @Override
+    public void setClipToPadding(boolean clipToPadding) {
+        if (clipToPadding != mClipToPadding) {
+            invalidateGlows();
+        }
+        mClipToPadding = clipToPadding;
+        super.setClipToPadding(clipToPadding);
+        if (mFirstLayoutComplete) {
+            requestLayout();
+        }
+    }
+
+    /**
+     * Returns whether this RecyclerView will clip its children to its padding, and resize (but
+     * not clip) any EdgeEffect to the padded region, if padding is present.
+     * <p>
+     * By default, children are clipped to the padding of their parent
+     * RecyclerView. This clipping behavior is only enabled if padding is non-zero.
+     *
+     * @return true if this RecyclerView clips children to its padding and resizes (but doesn't
+     *         clip) any EdgeEffect to the padded region, false otherwise.
+     *
+     * @attr name android:clipToPadding
+     */
+    @Override
+    public boolean getClipToPadding() {
+        return mClipToPadding;
+    }
+
+    /**
+     * Configure the scrolling touch slop for a specific use case.
+     *
+     * Set up the RecyclerView's scrolling motion threshold based on common usages.
+     * Valid arguments are {@link #TOUCH_SLOP_DEFAULT} and {@link #TOUCH_SLOP_PAGING}.
+     *
+     * @param slopConstant One of the <code>TOUCH_SLOP_</code> constants representing
+     *                     the intended usage of this RecyclerView
+     */
+    public void setScrollingTouchSlop(int slopConstant) {
+        final ViewConfiguration vc = ViewConfiguration.get(getContext());
+        switch (slopConstant) {
+            default:
+                Log.w(TAG, "setScrollingTouchSlop(): bad argument constant "
+                        + slopConstant + "; using default value");
+                // fall-through
+            case TOUCH_SLOP_DEFAULT:
+                mTouchSlop = vc.getScaledTouchSlop();
+                break;
+
+            case TOUCH_SLOP_PAGING:
+                mTouchSlop = vc.getScaledPagingTouchSlop();
+                break;
+        }
+    }
+
+    /**
+     * Swaps the current adapter with the provided one. It is similar to
+     * {@link #setAdapter(Adapter)} but assumes existing adapter and the new adapter uses the same
+     * {@link ViewHolder} and does not clear the RecycledViewPool.
+     * <p>
+     * Note that it still calls onAdapterChanged callbacks.
+     *
+     * @param adapter The new adapter to set, or null to set no adapter.
+     * @param removeAndRecycleExistingViews If set to true, RecyclerView will recycle all existing
+     *                                      Views. If adapters have stable ids and/or you want to
+     *                                      animate the disappearing views, you may prefer to set
+     *                                      this to false.
+     * @see #setAdapter(Adapter)
+     */
+    public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) {
+        // bail out if layout is frozen
+        setLayoutFrozen(false);
+        setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
+        setDataSetChangedAfterLayout();
+        requestLayout();
+    }
+    /**
+     * Set a new adapter to provide child views on demand.
+     * <p>
+     * When adapter is changed, all existing views are recycled back to the pool. If the pool has
+     * only one adapter, it will be cleared.
+     *
+     * @param adapter The new adapter to set, or null to set no adapter.
+     * @see #swapAdapter(Adapter, boolean)
+     */
+    public void setAdapter(Adapter adapter) {
+        // bail out if layout is frozen
+        setLayoutFrozen(false);
+        setAdapterInternal(adapter, false, true);
+        requestLayout();
+    }
+
+    /**
+     * Removes and recycles all views - both those currently attached, and those in the Recycler.
+     */
+    void removeAndRecycleViews() {
+        // end all running animations
+        if (mItemAnimator != null) {
+            mItemAnimator.endAnimations();
+        }
+        // Since animations are ended, mLayout.children should be equal to
+        // recyclerView.children. This may not be true if item animator's end does not work as
+        // expected. (e.g. not release children instantly). It is safer to use mLayout's child
+        // count.
+        if (mLayout != null) {
+            mLayout.removeAndRecycleAllViews(mRecycler);
+            mLayout.removeAndRecycleScrapInt(mRecycler);
+        }
+        // we should clear it here before adapters are swapped to ensure correct callbacks.
+        mRecycler.clear();
+    }
+
+    /**
+     * Replaces the current adapter with the new one and triggers listeners.
+     * @param adapter The new adapter
+     * @param compatibleWithPrevious If true, the new adapter is using the same View Holders and
+     *                               item types with the current adapter (helps us avoid cache
+     *                               invalidation).
+     * @param removeAndRecycleViews  If true, we'll remove and recycle all existing views. If
+     *                               compatibleWithPrevious is false, this parameter is ignored.
+     */
+    private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
+            boolean removeAndRecycleViews) {
+        if (mAdapter != null) {
+            mAdapter.unregisterAdapterDataObserver(mObserver);
+            mAdapter.onDetachedFromRecyclerView(this);
+        }
+        if (!compatibleWithPrevious || removeAndRecycleViews) {
+            removeAndRecycleViews();
+        }
+        mAdapterHelper.reset();
+        final Adapter oldAdapter = mAdapter;
+        mAdapter = adapter;
+        if (adapter != null) {
+            adapter.registerAdapterDataObserver(mObserver);
+            adapter.onAttachedToRecyclerView(this);
+        }
+        if (mLayout != null) {
+            mLayout.onAdapterChanged(oldAdapter, mAdapter);
+        }
+        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
+        mState.mStructureChanged = true;
+        markKnownViewsInvalid();
+    }
+
+    /**
+     * Retrieves the previously set adapter or null if no adapter is set.
+     *
+     * @return The previously set adapter
+     * @see #setAdapter(Adapter)
+     */
+    public Adapter getAdapter() {
+        return mAdapter;
+    }
+
+    /**
+     * Register a listener that will be notified whenever a child view is recycled.
+     *
+     * <p>This listener will be called when a LayoutManager or the RecyclerView decides
+     * that a child view is no longer needed. If an application associates expensive
+     * or heavyweight data with item views, this may be a good place to release
+     * or free those resources.</p>
+     *
+     * @param listener Listener to register, or null to clear
+     */
+    public void setRecyclerListener(RecyclerListener listener) {
+        mRecyclerListener = listener;
+    }
+
+    /**
+     * <p>Return the offset of the RecyclerView's text baseline from the its top
+     * boundary. If the LayoutManager of this RecyclerView does not support baseline alignment,
+     * this method returns -1.</p>
+     *
+     * @return the offset of the baseline within the RecyclerView's bounds or -1
+     *         if baseline alignment is not supported
+     */
+    @Override
+    public int getBaseline() {
+        if (mLayout != null) {
+            return mLayout.getBaseline();
+        } else {
+            return super.getBaseline();
+        }
+    }
+
+    /**
+     * Register a listener that will be notified whenever a child view is attached to or detached
+     * from RecyclerView.
+     *
+     * <p>This listener will be called when a LayoutManager or the RecyclerView decides
+     * that a child view is no longer needed. If an application associates expensive
+     * or heavyweight data with item views, this may be a good place to release
+     * or free those resources.</p>
+     *
+     * @param listener Listener to register
+     */
+    public void addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) {
+        if (mOnChildAttachStateListeners == null) {
+            mOnChildAttachStateListeners = new ArrayList<>();
+        }
+        mOnChildAttachStateListeners.add(listener);
+    }
+
+    /**
+     * Removes the provided listener from child attached state listeners list.
+     *
+     * @param listener Listener to unregister
+     */
+    public void removeOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) {
+        if (mOnChildAttachStateListeners == null) {
+            return;
+        }
+        mOnChildAttachStateListeners.remove(listener);
+    }
+
+    /**
+     * Removes all listeners that were added via
+     * {@link #addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener)}.
+     */
+    public void clearOnChildAttachStateChangeListeners() {
+        if (mOnChildAttachStateListeners != null) {
+            mOnChildAttachStateListeners.clear();
+        }
+    }
+
+    /**
+     * Set the {@link LayoutManager} that this RecyclerView will use.
+     *
+     * <p>In contrast to other adapter-backed views such as {@link android.widget.ListView}
+     * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom
+     * layout arrangements for child views. These arrangements are controlled by the
+     * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p>
+     *
+     * <p>Several default strategies are provided for common uses such as lists and grids.</p>
+     *
+     * @param layout LayoutManager to use
+     */
+    public void setLayoutManager(LayoutManager layout) {
+        if (layout == mLayout) {
+            return;
+        }
+        stopScroll();
+        // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
+        // chance that LayoutManagers will re-use views.
+        if (mLayout != null) {
+            // end all running animations
+            if (mItemAnimator != null) {
+                mItemAnimator.endAnimations();
+            }
+            mLayout.removeAndRecycleAllViews(mRecycler);
+            mLayout.removeAndRecycleScrapInt(mRecycler);
+            mRecycler.clear();
+
+            if (mIsAttached) {
+                mLayout.dispatchDetachedFromWindow(this, mRecycler);
+            }
+            mLayout.setRecyclerView(null);
+            mLayout = null;
+        } else {
+            mRecycler.clear();
+        }
+        // this is just a defensive measure for faulty item animators.
+        mChildHelper.removeAllViewsUnfiltered();
+        mLayout = layout;
+        if (layout != null) {
+            if (layout.mRecyclerView != null) {
+                throw new IllegalArgumentException("LayoutManager " + layout
+                        + " is already attached to a RecyclerView: " + layout.mRecyclerView);
+            }
+            mLayout.setRecyclerView(this);
+            if (mIsAttached) {
+                mLayout.dispatchAttachedToWindow(this);
+            }
+        }
+        mRecycler.updateViewCacheSize();
+        requestLayout();
+    }
+
+    /**
+     * Set a {@link OnFlingListener} for this {@link RecyclerView}.
+     * <p>
+     * If the {@link OnFlingListener} is set then it will receive
+     * calls to {@link #fling(int,int)} and will be able to intercept them.
+     *
+     * @param onFlingListener The {@link OnFlingListener} instance.
+     */
+    public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) {
+        mOnFlingListener = onFlingListener;
+    }
+
+    /**
+     * Get the current {@link OnFlingListener} from this {@link RecyclerView}.
+     *
+     * @return The {@link OnFlingListener} instance currently set (can be null).
+     */
+    @Nullable
+    public OnFlingListener getOnFlingListener() {
+        return mOnFlingListener;
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        SavedState state = new SavedState(super.onSaveInstanceState());
+        if (mPendingSavedState != null) {
+            state.copyFrom(mPendingSavedState);
+        } else if (mLayout != null) {
+            state.mLayoutState = mLayout.onSaveInstanceState();
+        } else {
+            state.mLayoutState = null;
+        }
+
+        return state;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        mPendingSavedState = (SavedState) state;
+        super.onRestoreInstanceState(mPendingSavedState.getSuperState());
+        if (mLayout != null && mPendingSavedState.mLayoutState != null) {
+            mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
+        }
+    }
+
+    /**
+     * Override to prevent freezing of any views created by the adapter.
+     */
+    @Override
+    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+        dispatchFreezeSelfOnly(container);
+    }
+
+    /**
+     * Override to prevent thawing of any views created by the adapter.
+     */
+    @Override
+    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+        dispatchThawSelfOnly(container);
+    }
+
+    /**
+     * Adds a view to the animatingViews list.
+     * mAnimatingViews holds the child views that are currently being kept around
+     * purely for the purpose of being animated out of view. They are drawn as a regular
+     * part of the child list of the RecyclerView, but they are invisible to the LayoutManager
+     * as they are managed separately from the regular child views.
+     * @param viewHolder The ViewHolder to be removed
+     */
+    private void addAnimatingView(ViewHolder viewHolder) {
+        final View view = viewHolder.itemView;
+        final boolean alreadyParented = view.getParent() == this;
+        mRecycler.unscrapView(getChildViewHolder(view));
+        if (viewHolder.isTmpDetached()) {
+            // re-attach
+            mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
+        } else if (!alreadyParented) {
+            mChildHelper.addView(view, true);
+        } else {
+            mChildHelper.hide(view);
+        }
+    }
+
+    /**
+     * Removes a view from the animatingViews list.
+     * @param view The view to be removed
+     * @see #addAnimatingView(RecyclerView.ViewHolder)
+     * @return true if an animating view is removed
+     */
+    boolean removeAnimatingView(View view) {
+        eatRequestLayout();
+        final boolean removed = mChildHelper.removeViewIfHidden(view);
+        if (removed) {
+            final ViewHolder viewHolder = getChildViewHolderInt(view);
+            mRecycler.unscrapView(viewHolder);
+            mRecycler.recycleViewHolderInternal(viewHolder);
+            if (DEBUG) {
+                Log.d(TAG, "after removing animated view: " + view + ", " + this);
+            }
+        }
+        // only clear request eaten flag if we removed the view.
+        resumeRequestLayout(!removed);
+        return removed;
+    }
+
+    /**
+     * Return the {@link LayoutManager} currently responsible for
+     * layout policy for this RecyclerView.
+     *
+     * @return The currently bound LayoutManager
+     */
+    public LayoutManager getLayoutManager() {
+        return mLayout;
+    }
+
+    /**
+     * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
+     * if no pool is set for this view a new one will be created. See
+     * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
+     *
+     * @return The pool used to store recycled item views for reuse.
+     * @see #setRecycledViewPool(RecycledViewPool)
+     */
+    public RecycledViewPool getRecycledViewPool() {
+        return mRecycler.getRecycledViewPool();
+    }
+
+    /**
+     * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
+     * This can be useful if you have multiple RecyclerViews with adapters that use the same
+     * view types, for example if you have several data sets with the same kinds of item views
+     * displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
+     *
+     * @param pool Pool to set. If this parameter is null a new pool will be created and used.
+     */
+    public void setRecycledViewPool(RecycledViewPool pool) {
+        mRecycler.setRecycledViewPool(pool);
+    }
+
+    /**
+     * Sets a new {@link ViewCacheExtension} to be used by the Recycler.
+     *
+     * @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
+     *
+     * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)}
+     */
+    public void setViewCacheExtension(ViewCacheExtension extension) {
+        mRecycler.setViewCacheExtension(extension);
+    }
+
+    /**
+     * Set the number of offscreen views to retain before adding them to the potentially shared
+     * {@link #getRecycledViewPool() recycled view pool}.
+     *
+     * <p>The offscreen view cache stays aware of changes in the attached adapter, allowing
+     * a LayoutManager to reuse those views unmodified without needing to return to the adapter
+     * to rebind them.</p>
+     *
+     * @param size Number of views to cache offscreen before returning them to the general
+     *             recycled view pool
+     */
+    public void setItemViewCacheSize(int size) {
+        mRecycler.setViewCacheSize(size);
+    }
+
+    /**
+     * Return the current scrolling state of the RecyclerView.
+     *
+     * @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
+     * {@link #SCROLL_STATE_SETTLING}
+     */
+    public int getScrollState() {
+        return mScrollState;
+    }
+
+    void setScrollState(int state) {
+        if (state == mScrollState) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
+                    new Exception());
+        }
+        mScrollState = state;
+        if (state != SCROLL_STATE_SETTLING) {
+            stopScrollersInternal();
+        }
+        dispatchOnScrollStateChanged(state);
+    }
+
+    /**
+     * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
+     * affect both measurement and drawing of individual item views.
+     *
+     * <p>Item decorations are ordered. Decorations placed earlier in the list will
+     * be run/queried/drawn first for their effects on item views. Padding added to views
+     * will be nested; a padding added by an earlier decoration will mean further
+     * item decorations in the list will be asked to draw/pad within the previous decoration's
+     * given area.</p>
+     *
+     * @param decor Decoration to add
+     * @param index Position in the decoration chain to insert this decoration at. If this value
+     *              is negative the decoration will be added at the end.
+     */
+    public void addItemDecoration(ItemDecoration decor, int index) {
+        if (mLayout != null) {
+            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
+                    + " layout");
+        }
+        if (mItemDecorations.isEmpty()) {
+            setWillNotDraw(false);
+        }
+        if (index < 0) {
+            mItemDecorations.add(decor);
+        } else {
+            mItemDecorations.add(index, decor);
+        }
+        markItemDecorInsetsDirty();
+        requestLayout();
+    }
+
+    /**
+     * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
+     * affect both measurement and drawing of individual item views.
+     *
+     * <p>Item decorations are ordered. Decorations placed earlier in the list will
+     * be run/queried/drawn first for their effects on item views. Padding added to views
+     * will be nested; a padding added by an earlier decoration will mean further
+     * item decorations in the list will be asked to draw/pad within the previous decoration's
+     * given area.</p>
+     *
+     * @param decor Decoration to add
+     */
+    public void addItemDecoration(ItemDecoration decor) {
+        addItemDecoration(decor, -1);
+    }
+
+    /**
+     * Remove an {@link ItemDecoration} from this RecyclerView.
+     *
+     * <p>The given decoration will no longer impact the measurement and drawing of
+     * item views.</p>
+     *
+     * @param decor Decoration to remove
+     * @see #addItemDecoration(ItemDecoration)
+     */
+    public void removeItemDecoration(ItemDecoration decor) {
+        if (mLayout != null) {
+            mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll  or"
+                    + " layout");
+        }
+        mItemDecorations.remove(decor);
+        if (mItemDecorations.isEmpty()) {
+            setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
+        }
+        markItemDecorInsetsDirty();
+        requestLayout();
+    }
+
+    /**
+     * Sets the {@link ChildDrawingOrderCallback} to be used for drawing children.
+     * <p>
+     * See {@link ViewGroup#getChildDrawingOrder(int, int)} for details. Calling this method will
+     * always call {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)}. The parameter will be
+     * true if childDrawingOrderCallback is not null, false otherwise.
+     * <p>
+     * Note that child drawing order may be overridden by View's elevation.
+     *
+     * @param childDrawingOrderCallback The ChildDrawingOrderCallback to be used by the drawing
+     *                                  system.
+     */
+    public void setChildDrawingOrderCallback(ChildDrawingOrderCallback childDrawingOrderCallback) {
+        if (childDrawingOrderCallback == mChildDrawingOrderCallback) {
+            return;
+        }
+        mChildDrawingOrderCallback = childDrawingOrderCallback;
+        setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null);
+    }
+
+    /**
+     * Set a listener that will be notified of any changes in scroll state or position.
+     *
+     * @param listener Listener to set or null to clear
+     *
+     * @deprecated Use {@link #addOnScrollListener(OnScrollListener)} and
+     *             {@link #removeOnScrollListener(OnScrollListener)}
+     */
+    @Deprecated
+    public void setOnScrollListener(OnScrollListener listener) {
+        mScrollListener = listener;
+    }
+
+    /**
+     * Add a listener that will be notified of any changes in scroll state or position.
+     *
+     * <p>Components that add a listener should take care to remove it when finished.
+     * Other components that take ownership of a view may call {@link #clearOnScrollListeners()}
+     * to remove all attached listeners.</p>
+     *
+     * @param listener listener to set or null to clear
+     */
+    public void addOnScrollListener(OnScrollListener listener) {
+        if (mScrollListeners == null) {
+            mScrollListeners = new ArrayList<>();
+        }
+        mScrollListeners.add(listener);
+    }
+
+    /**
+     * Remove a listener that was notified of any changes in scroll state or position.
+     *
+     * @param listener listener to set or null to clear
+     */
+    public void removeOnScrollListener(OnScrollListener listener) {
+        if (mScrollListeners != null) {
+            mScrollListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Remove all secondary listener that were notified of any changes in scroll state or position.
+     */
+    public void clearOnScrollListeners() {
+        if (mScrollListeners != null) {
+            mScrollListeners.clear();
+        }
+    }
+
+    /**
+     * Convenience method to scroll to a certain position.
+     *
+     * RecyclerView does not implement scrolling logic, rather forwards the call to
+     * {@link com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
+     * @param position Scroll to this adapter position
+     * @see com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int)
+     */
+    public void scrollToPosition(int position) {
+        if (mLayoutFrozen) {
+            return;
+        }
+        stopScroll();
+        if (mLayout == null) {
+            Log.e(TAG, "Cannot scroll to position a LayoutManager set. "
+                    + "Call setLayoutManager with a non-null argument.");
+            return;
+        }
+        mLayout.scrollToPosition(position);
+        awakenScrollBars();
+    }
+
+    void jumpToPositionForSmoothScroller(int position) {
+        if (mLayout == null) {
+            return;
+        }
+        mLayout.scrollToPosition(position);
+        awakenScrollBars();
+    }
+
+    /**
+     * Starts a smooth scroll to an adapter position.
+     * <p>
+     * To support smooth scrolling, you must override
+     * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
+     * {@link SmoothScroller}.
+     * <p>
+     * {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
+     * provide a custom smooth scroll logic, override
+     * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
+     * LayoutManager.
+     *
+     * @param position The adapter position to scroll to
+     * @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
+     */
+    public void smoothScrollToPosition(int position) {
+        if (mLayoutFrozen) {
+            return;
+        }
+        if (mLayout == null) {
+            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+                    + "Call setLayoutManager with a non-null argument.");
+            return;
+        }
+        mLayout.smoothScrollToPosition(this, mState, position);
+    }
+
+    @Override
+    public void scrollTo(int x, int y) {
+        Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. "
+                + "Use scrollToPosition instead");
+    }
+
+    @Override
+    public void scrollBy(int x, int y) {
+        if (mLayout == null) {
+            Log.e(TAG, "Cannot scroll without a LayoutManager set. "
+                    + "Call setLayoutManager with a non-null argument.");
+            return;
+        }
+        if (mLayoutFrozen) {
+            return;
+        }
+        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
+        final boolean canScrollVertical = mLayout.canScrollVertically();
+        if (canScrollHorizontal || canScrollVertical) {
+            scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
+        }
+    }
+
+    /**
+     * Helper method reflect data changes to the state.
+     * <p>
+     * Adapter changes during a scroll may trigger a crash because scroll assumes no data change
+     * but data actually changed.
+     * <p>
+     * This method consumes all deferred changes to avoid that case.
+     */
+    void consumePendingUpdateOperations() {
+        if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) {
+            Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
+            dispatchLayout();
+            Trace.endSection();
+            return;
+        }
+        if (!mAdapterHelper.hasPendingUpdates()) {
+            return;
+        }
+
+        // if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any
+        // of the visible items is affected and if not, just ignore the change.
+        if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper
+                .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
+                        | AdapterHelper.UpdateOp.MOVE)) {
+            Trace.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
+            eatRequestLayout();
+            onEnterLayoutOrScroll();
+            mAdapterHelper.preProcess();
+            if (!mLayoutRequestEaten) {
+                if (hasUpdatedView()) {
+                    dispatchLayout();
+                } else {
+                    // no need to layout, clean state
+                    mAdapterHelper.consumePostponedUpdates();
+                }
+            }
+            resumeRequestLayout(true);
+            onExitLayoutOrScroll();
+            Trace.endSection();
+        } else if (mAdapterHelper.hasPendingUpdates()) {
+            Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
+            dispatchLayout();
+            Trace.endSection();
+        }
+    }
+
+    /**
+     * @return True if an existing view holder needs to be updated
+     */
+    private boolean hasUpdatedView() {
+        final int childCount = mChildHelper.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+            if (holder == null || holder.shouldIgnore()) {
+                continue;
+            }
+            if (holder.isUpdated()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Does not perform bounds checking. Used by internal methods that have already validated input.
+     * <p>
+     * It also reports any unused scroll request to the related EdgeEffect.
+     *
+     * @param x The amount of horizontal scroll request
+     * @param y The amount of vertical scroll request
+     * @param ev The originating MotionEvent, or null if not from a touch event.
+     *
+     * @return Whether any scroll was consumed in either direction.
+     */
+    boolean scrollByInternal(int x, int y, MotionEvent ev) {
+        int unconsumedX = 0, unconsumedY = 0;
+        int consumedX = 0, consumedY = 0;
+
+        consumePendingUpdateOperations();
+        if (mAdapter != null) {
+            eatRequestLayout();
+            onEnterLayoutOrScroll();
+            Trace.beginSection(TRACE_SCROLL_TAG);
+            if (x != 0) {
+                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
+                unconsumedX = x - consumedX;
+            }
+            if (y != 0) {
+                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
+                unconsumedY = y - consumedY;
+            }
+            Trace.endSection();
+            repositionShadowingViews();
+            onExitLayoutOrScroll();
+            resumeRequestLayout(false);
+        }
+        if (!mItemDecorations.isEmpty()) {
+            invalidate();
+        }
+
+        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
+            // Update the last touch co-ords, taking any scroll offset into account
+            mLastTouchX -= mScrollOffset[0];
+            mLastTouchY -= mScrollOffset[1];
+            if (ev != null) {
+                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+            }
+            mNestedOffsets[0] += mScrollOffset[0];
+            mNestedOffsets[1] += mScrollOffset[1];
+        } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+            if (ev != null) {
+                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
+            }
+            considerReleasingGlowsOnScroll(x, y);
+        }
+        if (consumedX != 0 || consumedY != 0) {
+            dispatchOnScrolled(consumedX, consumedY);
+        }
+        if (!awakenScrollBars()) {
+            invalidate();
+        }
+        return consumedX != 0 || consumedY != 0;
+    }
+
+    /**
+     * <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
+     * range. This value is used to compute the length of the thumb within the scrollbar's track.
+     * </p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the units used by
+     * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.</p>
+     *
+     * <p>Default implementation returns 0.</p>
+     *
+     * <p>If you want to support scroll bars, override
+     * {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your
+     * LayoutManager. </p>
+     *
+     * @return The horizontal offset of the scrollbar's thumb
+     * @see com.android.internal.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset
+     * (RecyclerView.State)
+     */
+    @Override
+    public int computeHorizontalScrollOffset() {
+        if (mLayout == null) {
+            return 0;
+        }
+        return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0;
+    }
+
+    /**
+     * <p>Compute the horizontal extent of the horizontal scrollbar's thumb within the
+     * horizontal range. This value is used to compute the length of the thumb within the
+     * scrollbar's track.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the units used by
+     * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.</p>
+     *
+     * <p>Default implementation returns 0.</p>
+     *
+     * <p>If you want to support scroll bars, override
+     * {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your
+     * LayoutManager.</p>
+     *
+     * @return The horizontal extent of the scrollbar's thumb
+     * @see RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)
+     */
+    @Override
+    public int computeHorizontalScrollExtent() {
+        if (mLayout == null) {
+            return 0;
+        }
+        return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mState) : 0;
+    }
+
+    /**
+     * <p>Compute the horizontal range that the horizontal scrollbar represents.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the units used by
+     * {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p>
+     *
+     * <p>Default implementation returns 0.</p>
+     *
+     * <p>If you want to support scroll bars, override
+     * {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your
+     * LayoutManager.</p>
+     *
+     * @return The total horizontal range represented by the vertical scrollbar
+     * @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)
+     */
+    @Override
+    public int computeHorizontalScrollRange() {
+        if (mLayout == null) {
+            return 0;
+        }
+        return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
+    }
+
+    /**
+     * <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
+     * This value is used to compute the length of the thumb within the scrollbar's track. </p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the units used by
+     * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p>
+     *
+     * <p>Default implementation returns 0.</p>
+     *
+     * <p>If you want to support scroll bars, override
+     * {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your
+     * LayoutManager.</p>
+     *
+     * @return The vertical offset of the scrollbar's thumb
+     * @see com.android.internal.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset
+     * (RecyclerView.State)
+     */
+    @Override
+    public int computeVerticalScrollOffset() {
+        if (mLayout == null) {
+            return 0;
+        }
+        return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
+    }
+
+    /**
+     * <p>Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
+     * This value is used to compute the length of the thumb within the scrollbar's track.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the units used by
+     * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.</p>
+     *
+     * <p>Default implementation returns 0.</p>
+     *
+     * <p>If you want to support scroll bars, override
+     * {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your
+     * LayoutManager.</p>
+     *
+     * @return The vertical extent of the scrollbar's thumb
+     * @see RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)
+     */
+    @Override
+    public int computeVerticalScrollExtent() {
+        if (mLayout == null) {
+            return 0;
+        }
+        return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mState) : 0;
+    }
+
+    /**
+     * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the units used by
+     * {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.</p>
+     *
+     * <p>Default implementation returns 0.</p>
+     *
+     * <p>If you want to support scroll bars, override
+     * {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your
+     * LayoutManager.</p>
+     *
+     * @return The total vertical range represented by the vertical scrollbar
+     * @see RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)
+     */
+    @Override
+    public int computeVerticalScrollRange() {
+        if (mLayout == null) {
+            return 0;
+        }
+        return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0;
+    }
+
+
+    void eatRequestLayout() {
+        mEatRequestLayout++;
+        if (mEatRequestLayout == 1 && !mLayoutFrozen) {
+            mLayoutRequestEaten = false;
+        }
+    }
+
+    void resumeRequestLayout(boolean performLayoutChildren) {
+        if (mEatRequestLayout < 1) {
+            //noinspection PointlessBooleanExpression
+            if (DEBUG) {
+                throw new IllegalStateException("invalid eat request layout count");
+            }
+            mEatRequestLayout = 1;
+        }
+        if (!performLayoutChildren) {
+            // Reset the layout request eaten counter.
+            // This is necessary since eatRequest calls can be nested in which case the other
+            // call will override the inner one.
+            // for instance:
+            // eat layout for process adapter updates
+            //   eat layout for dispatchLayout
+            //     a bunch of req layout calls arrive
+
+            mLayoutRequestEaten = false;
+        }
+        if (mEatRequestLayout == 1) {
+            // when layout is frozen we should delay dispatchLayout()
+            if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen
+                    && mLayout != null && mAdapter != null) {
+                dispatchLayout();
+            }
+            if (!mLayoutFrozen) {
+                mLayoutRequestEaten = false;
+            }
+        }
+        mEatRequestLayout--;
+    }
+
+    /**
+     * Enable or disable layout and scroll.  After <code>setLayoutFrozen(true)</code> is called,
+     * Layout requests will be postponed until <code>setLayoutFrozen(false)</code> is called;
+     * child views are not updated when RecyclerView is frozen, {@link #smoothScrollBy(int, int)},
+     * {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and
+     * {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are
+     * dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be
+     * called.
+     *
+     * <p>
+     * <code>setLayoutFrozen(true)</code> does not prevent app from directly calling {@link
+     * LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
+     * RecyclerView, State, int)}.
+     * <p>
+     * {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
+     * stop frozen.
+     * <p>
+     * Note: Running ItemAnimator is not stopped automatically,  it's caller's
+     * responsibility to call ItemAnimator.end().
+     *
+     * @param frozen   true to freeze layout and scroll, false to re-enable.
+     */
+    public void setLayoutFrozen(boolean frozen) {
+        if (frozen != mLayoutFrozen) {
+            assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll");
+            if (!frozen) {
+                mLayoutFrozen = false;
+                if (mLayoutRequestEaten && mLayout != null && mAdapter != null) {
+                    requestLayout();
+                }
+                mLayoutRequestEaten = false;
+            } else {
+                final long now = SystemClock.uptimeMillis();
+                MotionEvent cancelEvent = MotionEvent.obtain(now, now,
+                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+                onTouchEvent(cancelEvent);
+                mLayoutFrozen = true;
+                mIgnoreMotionEventTillDown = true;
+                stopScroll();
+            }
+        }
+    }
+
+    /**
+     * Returns true if layout and scroll are frozen.
+     *
+     * @return true if layout and scroll are frozen
+     * @see #setLayoutFrozen(boolean)
+     */
+    public boolean isLayoutFrozen() {
+        return mLayoutFrozen;
+    }
+
+    /**
+     * Animate a scroll by the given amount of pixels along either axis.
+     *
+     * @param dx Pixels to scroll horizontally
+     * @param dy Pixels to scroll vertically
+     */
+    public void smoothScrollBy(int dx, int dy) {
+        smoothScrollBy(dx, dy, null);
+    }
+
+    /**
+     * Animate a scroll by the given amount of pixels along either axis.
+     *
+     * @param dx Pixels to scroll horizontally
+     * @param dy Pixels to scroll vertically
+     * @param interpolator {@link Interpolator} to be used for scrolling. If it is
+     *                     {@code null}, RecyclerView is going to use the default interpolator.
+     */
+    public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
+        if (mLayout == null) {
+            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+                    + "Call setLayoutManager with a non-null argument.");
+            return;
+        }
+        if (mLayoutFrozen) {
+            return;
+        }
+        if (!mLayout.canScrollHorizontally()) {
+            dx = 0;
+        }
+        if (!mLayout.canScrollVertically()) {
+            dy = 0;
+        }
+        if (dx != 0 || dy != 0) {
+            mViewFlinger.smoothScrollBy(dx, dy, interpolator);
+        }
+    }
+
+    /**
+     * Begin a standard fling with an initial velocity along each axis in pixels per second.
+     * If the velocity given is below the system-defined minimum this method will return false
+     * and no fling will occur.
+     *
+     * @param velocityX Initial horizontal velocity in pixels per second
+     * @param velocityY Initial vertical velocity in pixels per second
+     * @return true if the fling was started, false if the velocity was too low to fling or
+     * LayoutManager does not support scrolling in the axis fling is issued.
+     *
+     * @see LayoutManager#canScrollVertically()
+     * @see LayoutManager#canScrollHorizontally()
+     */
+    public boolean fling(int velocityX, int velocityY) {
+        if (mLayout == null) {
+            Log.e(TAG, "Cannot fling without a LayoutManager set. "
+                    + "Call setLayoutManager with a non-null argument.");
+            return false;
+        }
+        if (mLayoutFrozen) {
+            return false;
+        }
+
+        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
+        final boolean canScrollVertical = mLayout.canScrollVertically();
+
+        if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
+            velocityX = 0;
+        }
+        if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
+            velocityY = 0;
+        }
+        if (velocityX == 0 && velocityY == 0) {
+            // If we don't have any velocity, return false
+            return false;
+        }
+
+        if (!dispatchNestedPreFling(velocityX, velocityY)) {
+            final boolean canScroll = canScrollHorizontal || canScrollVertical;
+            dispatchNestedFling(velocityX, velocityY, canScroll);
+
+            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
+                return true;
+            }
+
+            if (canScroll) {
+                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
+                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
+                mViewFlinger.fling(velocityX, velocityY);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Stop any current scroll in progress, such as one started by
+     * {@link #smoothScrollBy(int, int)}, {@link #fling(int, int)} or a touch-initiated fling.
+     */
+    public void stopScroll() {
+        setScrollState(SCROLL_STATE_IDLE);
+        stopScrollersInternal();
+    }
+
+    /**
+     * Similar to {@link #stopScroll()} but does not set the state.
+     */
+    private void stopScrollersInternal() {
+        mViewFlinger.stop();
+        if (mLayout != null) {
+            mLayout.stopSmoothScroller();
+        }
+    }
+
+    /**
+     * Returns the minimum velocity to start a fling.
+     *
+     * @return The minimum velocity to start a fling
+     */
+    public int getMinFlingVelocity() {
+        return mMinFlingVelocity;
+    }
+
+
+    /**
+     * Returns the maximum fling velocity used by this RecyclerView.
+     *
+     * @return The maximum fling velocity used by this RecyclerView.
+     */
+    public int getMaxFlingVelocity() {
+        return mMaxFlingVelocity;
+    }
+
+    /**
+     * Apply a pull to relevant overscroll glow effects
+     */
+    private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
+        boolean invalidate = false;
+        if (overscrollX < 0) {
+            ensureLeftGlow();
+            mLeftGlow.onPull(-overscrollX / getWidth(), 1f - y  / getHeight());
+            invalidate = true;
+        } else if (overscrollX > 0) {
+            ensureRightGlow();
+            mRightGlow.onPull(overscrollX / getWidth(), y / getHeight());
+            invalidate = true;
+        }
+
+        if (overscrollY < 0) {
+            ensureTopGlow();
+            mTopGlow.onPull(-overscrollY / getHeight(), x / getWidth());
+            invalidate = true;
+        } else if (overscrollY > 0) {
+            ensureBottomGlow();
+            mBottomGlow.onPull(overscrollY / getHeight(), 1f - x / getWidth());
+            invalidate = true;
+        }
+
+        if (invalidate || overscrollX != 0 || overscrollY != 0) {
+            postInvalidateOnAnimation();
+        }
+    }
+
+    private void releaseGlows() {
+        boolean needsInvalidate = false;
+        if (mLeftGlow != null) {
+            mLeftGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (mTopGlow != null) {
+            mTopGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (mRightGlow != null) {
+            mRightGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (mBottomGlow != null) {
+            mBottomGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (needsInvalidate) {
+            postInvalidateOnAnimation();
+        }
+    }
+
+    void considerReleasingGlowsOnScroll(int dx, int dy) {
+        boolean needsInvalidate = false;
+        if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) {
+            mLeftGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) {
+            mRightGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) {
+            mTopGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) {
+            mBottomGlow.onRelease();
+            needsInvalidate = true;
+        }
+        if (needsInvalidate) {
+            postInvalidateOnAnimation();
+        }
+    }
+
+    void absorbGlows(int velocityX, int velocityY) {
+        if (velocityX < 0) {
+            ensureLeftGlow();
+            mLeftGlow.onAbsorb(-velocityX);
+        } else if (velocityX > 0) {
+            ensureRightGlow();
+            mRightGlow.onAbsorb(velocityX);
+        }
+
+        if (velocityY < 0) {
+            ensureTopGlow();
+            mTopGlow.onAbsorb(-velocityY);
+        } else if (velocityY > 0) {
+            ensureBottomGlow();
+            mBottomGlow.onAbsorb(velocityY);
+        }
+
+        if (velocityX != 0 || velocityY != 0) {
+            postInvalidateOnAnimation();
+        }
+    }
+
+    void ensureLeftGlow() {
+        if (mLeftGlow != null) {
+            return;
+        }
+        mLeftGlow = new EdgeEffect(getContext());
+        if (mClipToPadding) {
+            mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+                    getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+        } else {
+            mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth());
+        }
+    }
+
+    void ensureRightGlow() {
+        if (mRightGlow != null) {
+            return;
+        }
+        mRightGlow = new EdgeEffect(getContext());
+        if (mClipToPadding) {
+            mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+                    getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+        } else {
+            mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth());
+        }
+    }
+
+    void ensureTopGlow() {
+        if (mTopGlow != null) {
+            return;
+        }
+        mTopGlow = new EdgeEffect(getContext());
+        if (mClipToPadding) {
+            mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+                    getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+        } else {
+            mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
+        }
+
+    }
+
+    void ensureBottomGlow() {
+        if (mBottomGlow != null) {
+            return;
+        }
+        mBottomGlow = new EdgeEffect(getContext());
+        if (mClipToPadding) {
+            mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+                    getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+        } else {
+            mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
+        }
+    }
+
+    void invalidateGlows() {
+        mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null;
+    }
+
+    /**
+     * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are
+     * in the Adapter but not visible in the UI), it employs a more involved focus search strategy
+     * that differs from other ViewGroups.
+     * <p>
+     * It first does a focus search within the RecyclerView. If this search finds a View that is in
+     * the focus direction with respect to the currently focused View, RecyclerView returns that
+     * child as the next focus target. When it cannot find such child, it calls
+     * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} to layout more Views
+     * in the focus search direction. If LayoutManager adds a View that matches the
+     * focus search criteria, it will be returned as the focus search result. Otherwise,
+     * RecyclerView will call parent to handle the focus search like a regular ViewGroup.
+     * <p>
+     * When the direction is {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}, a View that
+     * is not in the focus direction is still valid focus target which may not be the desired
+     * behavior if the Adapter has more children in the focus direction. To handle this case,
+     * RecyclerView converts the focus direction to an absolute direction and makes a preliminary
+     * focus search in that direction. If there are no Views to gain focus, it will call
+     * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} before running a
+     * focus search with the original (relative) direction. This allows RecyclerView to provide
+     * better candidates to the focus search while still allowing the view system to take focus from
+     * the RecyclerView and give it to a more suitable child if such child exists.
+     *
+     * @param focused The view that currently has focus
+     * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+     * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, {@link View#FOCUS_FORWARD},
+     * {@link View#FOCUS_BACKWARD} or 0 for not applicable.
+     *
+     * @return A new View that can be the next focus after the focused View
+     */
+    @Override
+    public View focusSearch(View focused, int direction) {
+        View result = mLayout.onInterceptFocusSearch(focused, direction);
+        if (result != null) {
+            return result;
+        }
+        final boolean canRunFocusFailure = mAdapter != null && mLayout != null
+                && !isComputingLayout() && !mLayoutFrozen;
+
+        final FocusFinder ff = FocusFinder.getInstance();
+        if (canRunFocusFailure
+                && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) {
+            // convert direction to absolute direction and see if we have a view there and if not
+            // tell LayoutManager to add if it can.
+            boolean needsFocusFailureLayout = false;
+            if (mLayout.canScrollVertically()) {
+                final int absDir =
+                        direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
+                final View found = ff.findNextFocus(this, focused, absDir);
+                needsFocusFailureLayout = found == null;
+                if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
+                    // Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
+                    direction = absDir;
+                }
+            }
+            if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) {
+                boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+                final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
+                        ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+                final View found = ff.findNextFocus(this, focused, absDir);
+                needsFocusFailureLayout = found == null;
+                if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
+                    // Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
+                    direction = absDir;
+                }
+            }
+            if (needsFocusFailureLayout) {
+                consumePendingUpdateOperations();
+                final View focusedItemView = findContainingItemView(focused);
+                if (focusedItemView == null) {
+                    // panic, focused view is not a child anymore, cannot call super.
+                    return null;
+                }
+                eatRequestLayout();
+                mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
+                resumeRequestLayout(false);
+            }
+            result = ff.findNextFocus(this, focused, direction);
+        } else {
+            result = ff.findNextFocus(this, focused, direction);
+            if (result == null && canRunFocusFailure) {
+                consumePendingUpdateOperations();
+                final View focusedItemView = findContainingItemView(focused);
+                if (focusedItemView == null) {
+                    // panic, focused view is not a child anymore, cannot call super.
+                    return null;
+                }
+                eatRequestLayout();
+                result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
+                resumeRequestLayout(false);
+            }
+        }
+        return isPreferredNextFocus(focused, result, direction)
+                ? result : super.focusSearch(focused, direction);
+    }
+
+    /**
+     * Checks if the new focus candidate is a good enough candidate such that RecyclerView will
+     * assign it as the next focus View instead of letting view hierarchy decide.
+     * A good candidate means a View that is aligned in the focus direction wrt the focused View
+     * and is not the RecyclerView itself.
+     * When this method returns false, RecyclerView will let the parent make the decision so the
+     * same View may still get the focus as a result of that search.
+     */
+    private boolean isPreferredNextFocus(View focused, View next, int direction) {
+        if (next == null || next == this) {
+            return false;
+        }
+        if (focused == null) {
+            return true;
+        }
+
+        if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
+            final boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+            final int absHorizontal = (direction == View.FOCUS_FORWARD) ^ rtl
+                    ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+            if (isPreferredNextFocusAbsolute(focused, next, absHorizontal)) {
+                return true;
+            }
+            if (direction == View.FOCUS_FORWARD) {
+                return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_DOWN);
+            } else {
+                return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_UP);
+            }
+        } else {
+            return isPreferredNextFocusAbsolute(focused, next, direction);
+        }
+
+    }
+
+    /**
+     * Logic taken from FocusSearch#isCandidate
+     */
+    private boolean isPreferredNextFocusAbsolute(View focused, View next, int direction) {
+        mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
+        mTempRect2.set(0, 0, next.getWidth(), next.getHeight());
+        offsetDescendantRectToMyCoords(focused, mTempRect);
+        offsetDescendantRectToMyCoords(next, mTempRect2);
+        switch (direction) {
+            case View.FOCUS_LEFT:
+                return (mTempRect.right > mTempRect2.right
+                        || mTempRect.left >= mTempRect2.right)
+                        && mTempRect.left > mTempRect2.left;
+            case View.FOCUS_RIGHT:
+                return (mTempRect.left < mTempRect2.left
+                        || mTempRect.right <= mTempRect2.left)
+                        && mTempRect.right < mTempRect2.right;
+            case View.FOCUS_UP:
+                return (mTempRect.bottom > mTempRect2.bottom
+                        || mTempRect.top >= mTempRect2.bottom)
+                        && mTempRect.top > mTempRect2.top;
+            case View.FOCUS_DOWN:
+                return (mTempRect.top < mTempRect2.top
+                        || mTempRect.bottom <= mTempRect2.top)
+                        && mTempRect.bottom < mTempRect2.bottom;
+        }
+        throw new IllegalArgumentException("direction must be absolute. received:" + direction);
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) {
+            mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
+
+            // get item decor offsets w/o refreshing. If they are invalid, there will be another
+            // layout pass to fix them, then it is LayoutManager's responsibility to keep focused
+            // View in viewport.
+            final ViewGroup.LayoutParams focusedLayoutParams = focused.getLayoutParams();
+            if (focusedLayoutParams instanceof LayoutParams) {
+                // if focused child has item decors, use them. Otherwise, ignore.
+                final LayoutParams lp = (LayoutParams) focusedLayoutParams;
+                if (!lp.mInsetsDirty) {
+                    final Rect insets = lp.mDecorInsets;
+                    mTempRect.left -= insets.left;
+                    mTempRect.right += insets.right;
+                    mTempRect.top -= insets.top;
+                    mTempRect.bottom += insets.bottom;
+                }
+            }
+
+            offsetDescendantRectToMyCoords(focused, mTempRect);
+            offsetRectIntoDescendantCoords(child, mTempRect);
+            requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete);
+        }
+        super.requestChildFocus(child, focused);
+    }
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+        return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate);
+    }
+
+    @Override
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+        if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) {
+            super.addFocusables(views, direction, focusableMode);
+        }
+    }
+
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+        if (isComputingLayout()) {
+            // if we are in the middle of a layout calculation, don't let any child take focus.
+            // RV will handle it after layout calculation is finished.
+            return false;
+        }
+        return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mLayoutOrScrollCounter = 0;
+        mIsAttached = true;
+        mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested();
+        if (mLayout != null) {
+            mLayout.dispatchAttachedToWindow(this);
+        }
+        mPostedAnimatorRunner = false;
+
+        if (ALLOW_THREAD_GAP_WORK) {
+            // Register with gap worker
+            mGapWorker = GapWorker.sGapWorker.get();
+            if (mGapWorker == null) {
+                mGapWorker = new GapWorker();
+
+                // break 60 fps assumption if data from display appears valid
+                // NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
+                Display display = getDisplay();
+                float refreshRate = 60.0f;
+                if (!isInEditMode() && display != null) {
+                    float displayRefreshRate = display.getRefreshRate();
+                    if (displayRefreshRate >= 30.0f) {
+                        refreshRate = displayRefreshRate;
+                    }
+                }
+                mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
+                GapWorker.sGapWorker.set(mGapWorker);
+            }
+            mGapWorker.add(this);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mItemAnimator != null) {
+            mItemAnimator.endAnimations();
+        }
+        stopScroll();
+        mIsAttached = false;
+        if (mLayout != null) {
+            mLayout.dispatchDetachedFromWindow(this, mRecycler);
+        }
+        mPendingAccessibilityImportanceChange.clear();
+        removeCallbacks(mItemAnimatorRunner);
+        mViewInfoStore.onDetach();
+
+        if (ALLOW_THREAD_GAP_WORK) {
+            // Unregister with gap worker
+            mGapWorker.remove(this);
+            mGapWorker = null;
+        }
+    }
+
+    /**
+     * Returns true if RecyclerView is attached to window.
+     */
+    // @override
+    public boolean isAttachedToWindow() {
+        return mIsAttached;
+    }
+
+    /**
+     * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+     * {@link IllegalStateException} if it <b>is not</b>.
+     *
+     * @param message The message for the exception. Can be null.
+     * @see #assertNotInLayoutOrScroll(String)
+     */
+    void assertInLayoutOrScroll(String message) {
+        if (!isComputingLayout()) {
+            if (message == null) {
+                throw new IllegalStateException("Cannot call this method unless RecyclerView is "
+                        + "computing a layout or scrolling");
+            }
+            throw new IllegalStateException(message);
+
+        }
+    }
+
+    /**
+     * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+     * {@link IllegalStateException} if it <b>is</b>.
+     *
+     * @param message The message for the exception. Can be null.
+     * @see #assertInLayoutOrScroll(String)
+     */
+    void assertNotInLayoutOrScroll(String message) {
+        if (isComputingLayout()) {
+            if (message == null) {
+                throw new IllegalStateException("Cannot call this method while RecyclerView is "
+                        + "computing a layout or scrolling");
+            }
+            throw new IllegalStateException(message);
+        }
+        if (mDispatchScrollCounter > 0) {
+            Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might be run"
+                    + " during a measure & layout pass where you cannot change the RecyclerView"
+                    + " data. Any method call that might change the structure of the RecyclerView"
+                    + " or the adapter contents should be postponed to the next frame.",
+                    new IllegalStateException(""));
+        }
+    }
+
+    /**
+     * Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
+     * to child views or this view's standard scrolling behavior.
+     *
+     * <p>Client code may use listeners to implement item manipulation behavior. Once a listener
+     * returns true from
+     * {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its
+     * {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called
+     * for each incoming MotionEvent until the end of the gesture.</p>
+     *
+     * @param listener Listener to add
+     * @see SimpleOnItemTouchListener
+     */
+    public void addOnItemTouchListener(OnItemTouchListener listener) {
+        mOnItemTouchListeners.add(listener);
+    }
+
+    /**
+     * Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events.
+     *
+     * @param listener Listener to remove
+     */
+    public void removeOnItemTouchListener(OnItemTouchListener listener) {
+        mOnItemTouchListeners.remove(listener);
+        if (mActiveOnItemTouchListener == listener) {
+            mActiveOnItemTouchListener = null;
+        }
+    }
+
+    private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
+        final int action = e.getAction();
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
+            mActiveOnItemTouchListener = null;
+        }
+
+        final int listenerCount = mOnItemTouchListeners.size();
+        for (int i = 0; i < listenerCount; i++) {
+            final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+            if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
+                mActiveOnItemTouchListener = listener;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean dispatchOnItemTouch(MotionEvent e) {
+        final int action = e.getAction();
+        if (mActiveOnItemTouchListener != null) {
+            if (action == MotionEvent.ACTION_DOWN) {
+                // Stale state from a previous gesture, we're starting a new one. Clear it.
+                mActiveOnItemTouchListener = null;
+            } else {
+                mActiveOnItemTouchListener.onTouchEvent(this, e);
+                if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+                    // Clean up for the next gesture.
+                    mActiveOnItemTouchListener = null;
+                }
+                return true;
+            }
+        }
+
+        // Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept
+        // as called from onInterceptTouchEvent; skip it.
+        if (action != MotionEvent.ACTION_DOWN) {
+            final int listenerCount = mOnItemTouchListeners.size();
+            for (int i = 0; i < listenerCount; i++) {
+                final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+                if (listener.onInterceptTouchEvent(this, e)) {
+                    mActiveOnItemTouchListener = listener;
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent e) {
+        if (mLayoutFrozen) {
+            // When layout is frozen,  RV does not intercept the motion event.
+            // A child view e.g. a button may still get the click.
+            return false;
+        }
+        if (dispatchOnItemTouchIntercept(e)) {
+            cancelTouch();
+            return true;
+        }
+
+        if (mLayout == null) {
+            return false;
+        }
+
+        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
+        final boolean canScrollVertically = mLayout.canScrollVertically();
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(e);
+
+        final int action = e.getActionMasked();
+        final int actionIndex = e.getActionIndex();
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                if (mIgnoreMotionEventTillDown) {
+                    mIgnoreMotionEventTillDown = false;
+                }
+                mScrollPointerId = e.getPointerId(0);
+                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
+                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+
+                if (mScrollState == SCROLL_STATE_SETTLING) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                }
+
+                // Clear the nested offsets
+                mNestedOffsets[0] = mNestedOffsets[1] = 0;
+
+                int nestedScrollAxis = View.SCROLL_AXIS_NONE;
+                if (canScrollHorizontally) {
+                    nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL;
+                }
+                if (canScrollVertically) {
+                    nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL;
+                }
+                startNestedScroll(nestedScrollAxis);
+                break;
+
+            case MotionEvent.ACTION_POINTER_DOWN:
+                mScrollPointerId = e.getPointerId(actionIndex);
+                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
+                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
+                break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final int index = e.findPointerIndex(mScrollPointerId);
+                if (index < 0) {
+                    Log.e(TAG, "Error processing scroll; pointer index for id "
+                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
+                    return false;
+                }
+
+                final int x = (int) (e.getX(index) + 0.5f);
+                final int y = (int) (e.getY(index) + 0.5f);
+                if (mScrollState != SCROLL_STATE_DRAGGING) {
+                    final int dx = x - mInitialTouchX;
+                    final int dy = y - mInitialTouchY;
+                    boolean startScroll = false;
+                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+                        mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
+                        startScroll = true;
+                    }
+                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+                        mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
+                        startScroll = true;
+                    }
+                    if (startScroll) {
+                        setScrollState(SCROLL_STATE_DRAGGING);
+                    }
+                }
+            } break;
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onPointerUp(e);
+            } break;
+
+            case MotionEvent.ACTION_UP: {
+                mVelocityTracker.clear();
+                stopNestedScroll();
+            } break;
+
+            case MotionEvent.ACTION_CANCEL: {
+                cancelTouch();
+            }
+        }
+        return mScrollState == SCROLL_STATE_DRAGGING;
+    }
+
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        final int listenerCount = mOnItemTouchListeners.size();
+        for (int i = 0; i < listenerCount; i++) {
+            final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+            listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent e) {
+        if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
+            return false;
+        }
+        if (dispatchOnItemTouch(e)) {
+            cancelTouch();
+            return true;
+        }
+
+        if (mLayout == null) {
+            return false;
+        }
+
+        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
+        final boolean canScrollVertically = mLayout.canScrollVertically();
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        boolean eventAddedToVelocityTracker = false;
+
+        final MotionEvent vtev = MotionEvent.obtain(e);
+        final int action = e.getActionMasked();
+        final int actionIndex = e.getActionIndex();
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            mNestedOffsets[0] = mNestedOffsets[1] = 0;
+        }
+        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                mScrollPointerId = e.getPointerId(0);
+                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
+                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+
+                int nestedScrollAxis = View.SCROLL_AXIS_NONE;
+                if (canScrollHorizontally) {
+                    nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL;
+                }
+                if (canScrollVertically) {
+                    nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL;
+                }
+                startNestedScroll(nestedScrollAxis);
+            } break;
+
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                mScrollPointerId = e.getPointerId(actionIndex);
+                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
+                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
+            } break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final int index = e.findPointerIndex(mScrollPointerId);
+                if (index < 0) {
+                    Log.e(TAG, "Error processing scroll; pointer index for id "
+                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
+                    return false;
+                }
+
+                final int x = (int) (e.getX(index) + 0.5f);
+                final int y = (int) (e.getY(index) + 0.5f);
+                int dx = mLastTouchX - x;
+                int dy = mLastTouchY - y;
+
+                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
+                    dx -= mScrollConsumed[0];
+                    dy -= mScrollConsumed[1];
+                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+                    // Updated the nested offsets
+                    mNestedOffsets[0] += mScrollOffset[0];
+                    mNestedOffsets[1] += mScrollOffset[1];
+                }
+
+                if (mScrollState != SCROLL_STATE_DRAGGING) {
+                    boolean startScroll = false;
+                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+                        if (dx > 0) {
+                            dx -= mTouchSlop;
+                        } else {
+                            dx += mTouchSlop;
+                        }
+                        startScroll = true;
+                    }
+                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+                        if (dy > 0) {
+                            dy -= mTouchSlop;
+                        } else {
+                            dy += mTouchSlop;
+                        }
+                        startScroll = true;
+                    }
+                    if (startScroll) {
+                        setScrollState(SCROLL_STATE_DRAGGING);
+                    }
+                }
+
+                if (mScrollState == SCROLL_STATE_DRAGGING) {
+                    mLastTouchX = x - mScrollOffset[0];
+                    mLastTouchY = y - mScrollOffset[1];
+
+                    if (scrollByInternal(
+                            canScrollHorizontally ? dx : 0,
+                            canScrollVertically ? dy : 0,
+                            vtev)) {
+                        getParent().requestDisallowInterceptTouchEvent(true);
+                    }
+                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
+                        mGapWorker.postFromTraversal(this, dx, dy);
+                    }
+                }
+            } break;
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onPointerUp(e);
+            } break;
+
+            case MotionEvent.ACTION_UP: {
+                mVelocityTracker.addMovement(vtev);
+                eventAddedToVelocityTracker = true;
+                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
+                final float xvel = canScrollHorizontally
+                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
+                final float yvel = canScrollVertically
+                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
+                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
+                    setScrollState(SCROLL_STATE_IDLE);
+                }
+                resetTouch();
+            } break;
+
+            case MotionEvent.ACTION_CANCEL: {
+                cancelTouch();
+            } break;
+        }
+
+        if (!eventAddedToVelocityTracker) {
+            mVelocityTracker.addMovement(vtev);
+        }
+        vtev.recycle();
+
+        return true;
+    }
+
+    private void resetTouch() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.clear();
+        }
+        stopNestedScroll();
+        releaseGlows();
+    }
+
+    private void cancelTouch() {
+        resetTouch();
+        setScrollState(SCROLL_STATE_IDLE);
+    }
+
+    private void onPointerUp(MotionEvent e) {
+        final int actionIndex = e.getActionIndex();
+        if (e.getPointerId(actionIndex) == mScrollPointerId) {
+            // Pick a new pointer to pick up the slack.
+            final int newIndex = actionIndex == 0 ? 1 : 0;
+            mScrollPointerId = e.getPointerId(newIndex);
+            mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
+            mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
+        }
+    }
+
+    // @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if (mLayout == null) {
+            return false;
+        }
+        if (mLayoutFrozen) {
+            return false;
+        }
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            if (event.getAction() == MotionEvent.ACTION_SCROLL) {
+                final float vScroll, hScroll;
+                if (mLayout.canScrollVertically()) {
+                    // Inverse the sign of the vertical scroll to align the scroll orientation
+                    // with AbsListView.
+                    vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                } else {
+                    vScroll = 0f;
+                }
+                if (mLayout.canScrollHorizontally()) {
+                    hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                } else {
+                    hScroll = 0f;
+                }
+
+                if (vScroll != 0 || hScroll != 0) {
+                    final float scrollFactor = getScrollFactor();
+                    scrollByInternal((int) (hScroll * scrollFactor),
+                            (int) (vScroll * scrollFactor), event);
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Ported from View.getVerticalScrollFactor.
+     */
+    private float getScrollFactor() {
+        if (mScrollFactor == Float.MIN_VALUE) {
+            TypedValue outValue = new TypedValue();
+            if (getContext().getTheme().resolveAttribute(
+                    android.R.attr.listPreferredItemHeight, outValue, true)) {
+                mScrollFactor = outValue.getDimension(
+                        getContext().getResources().getDisplayMetrics());
+            } else {
+                return 0; //listPreferredItemHeight is not defined, no generic scrolling
+            }
+        }
+        return mScrollFactor;
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        if (mLayout == null) {
+            defaultOnMeasure(widthSpec, heightSpec);
+            return;
+        }
+        if (mLayout.mAutoMeasure) {
+            final int widthMode = MeasureSpec.getMode(widthSpec);
+            final int heightMode = MeasureSpec.getMode(heightSpec);
+            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
+                    && heightMode == MeasureSpec.EXACTLY;
+            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+            if (skipMeasure || mAdapter == null) {
+                return;
+            }
+            if (mState.mLayoutStep == State.STEP_START) {
+                dispatchLayoutStep1();
+            }
+            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
+            // consistency
+            mLayout.setMeasureSpecs(widthSpec, heightSpec);
+            mState.mIsMeasuring = true;
+            dispatchLayoutStep2();
+
+            // now we can get the width and height from the children.
+            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
+
+            // if RecyclerView has non-exact width and height and if there is at least one child
+            // which also has non-exact width & height, we have to re-measure.
+            if (mLayout.shouldMeasureTwice()) {
+                mLayout.setMeasureSpecs(
+                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
+                mState.mIsMeasuring = true;
+                dispatchLayoutStep2();
+                // now we can get the width and height from the children.
+                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
+            }
+        } else {
+            if (mHasFixedSize) {
+                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+                return;
+            }
+            // custom onMeasure
+            if (mAdapterUpdateDuringMeasure) {
+                eatRequestLayout();
+                onEnterLayoutOrScroll();
+                processAdapterUpdatesAndSetAnimationFlags();
+                onExitLayoutOrScroll();
+
+                if (mState.mRunPredictiveAnimations) {
+                    mState.mInPreLayout = true;
+                } else {
+                    // consume remaining updates to provide a consistent state with the layout pass.
+                    mAdapterHelper.consumeUpdatesInOnePass();
+                    mState.mInPreLayout = false;
+                }
+                mAdapterUpdateDuringMeasure = false;
+                resumeRequestLayout(false);
+            }
+
+            if (mAdapter != null) {
+                mState.mItemCount = mAdapter.getItemCount();
+            } else {
+                mState.mItemCount = 0;
+            }
+            eatRequestLayout();
+            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+            resumeRequestLayout(false);
+            mState.mInPreLayout = false; // clear
+        }
+    }
+
+    /**
+     * Used when onMeasure is called before layout manager is set
+     */
+    void defaultOnMeasure(int widthSpec, int heightSpec) {
+        // calling LayoutManager here is not pretty but that API is already public and it is better
+        // than creating another method since this is internal.
+        final int width = LayoutManager.chooseSize(widthSpec,
+                getPaddingLeft() + getPaddingRight(),
+                getMinimumWidth());
+        final int height = LayoutManager.chooseSize(heightSpec,
+                getPaddingTop() + getPaddingBottom(),
+                getMinimumHeight());
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        if (w != oldw || h != oldh) {
+            invalidateGlows();
+            // layout's w/h are updated during measure/layout steps.
+        }
+    }
+
+    /**
+     * Sets the {@link ItemAnimator} that will handle animations involving changes
+     * to the items in this RecyclerView. By default, RecyclerView instantiates and
+     * uses an instance of {@link DefaultItemAnimator}. Whether item animations are
+     * enabled for the RecyclerView depends on the ItemAnimator and whether
+     * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations()
+     * supports item animations}.
+     *
+     * @param animator The ItemAnimator being set. If null, no animations will occur
+     * when changes occur to the items in this RecyclerView.
+     */
+    public void setItemAnimator(ItemAnimator animator) {
+        if (mItemAnimator != null) {
+            mItemAnimator.endAnimations();
+            mItemAnimator.setListener(null);
+        }
+        mItemAnimator = animator;
+        if (mItemAnimator != null) {
+            mItemAnimator.setListener(mItemAnimatorListener);
+        }
+    }
+
+    void onEnterLayoutOrScroll() {
+        mLayoutOrScrollCounter++;
+    }
+
+    void onExitLayoutOrScroll() {
+        mLayoutOrScrollCounter--;
+        if (mLayoutOrScrollCounter < 1) {
+            if (DEBUG && mLayoutOrScrollCounter < 0) {
+                throw new IllegalStateException("layout or scroll counter cannot go below zero."
+                        + "Some calls are not matching");
+            }
+            mLayoutOrScrollCounter = 0;
+            dispatchContentChangedIfNecessary();
+            dispatchPendingImportantForAccessibilityChanges();
+        }
+    }
+
+    boolean isAccessibilityEnabled() {
+        return mAccessibilityManager != null && mAccessibilityManager.isEnabled();
+    }
+
+    private void dispatchContentChangedIfNecessary() {
+        final int flags = mEatenAccessibilityChangeFlags;
+        mEatenAccessibilityChangeFlags = 0;
+        if (flags != 0 && isAccessibilityEnabled()) {
+            final AccessibilityEvent event = AccessibilityEvent.obtain();
+            event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+            event.setContentChangeTypes(flags);
+            sendAccessibilityEventUnchecked(event);
+        }
+    }
+
+    /**
+     * Returns whether RecyclerView is currently computing a layout.
+     * <p>
+     * If this method returns true, it means that RecyclerView is in a lockdown state and any
+     * attempt to update adapter contents will result in an exception because adapter contents
+     * cannot be changed while RecyclerView is trying to compute the layout.
+     * <p>
+     * It is very unlikely that your code will be running during this state as it is
+     * called by the framework when a layout traversal happens or RecyclerView starts to scroll
+     * in response to system events (touch, accessibility etc).
+     * <p>
+     * This case may happen if you have some custom logic to change adapter contents in
+     * response to a View callback (e.g. focus change callback) which might be triggered during a
+     * layout calculation. In these cases, you should just postpone the change using a Handler or a
+     * similar mechanism.
+     *
+     * @return <code>true</code> if RecyclerView is currently computing a layout, <code>false</code>
+     *         otherwise
+     */
+    public boolean isComputingLayout() {
+        return mLayoutOrScrollCounter > 0;
+    }
+
+    /**
+     * Returns true if an accessibility event should not be dispatched now. This happens when an
+     * accessibility request arrives while RecyclerView does not have a stable state which is very
+     * hard to handle for a LayoutManager. Instead, this method records necessary information about
+     * the event and dispatches a window change event after the critical section is finished.
+     *
+     * @return True if the accessibility event should be postponed.
+     */
+    boolean shouldDeferAccessibilityEvent(AccessibilityEvent event) {
+        if (isComputingLayout()) {
+            int type = 0;
+            if (event != null) {
+                type = event.getContentChangeTypes();
+            }
+            if (type == 0) {
+                type = AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+            }
+            mEatenAccessibilityChangeFlags |= type;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+        if (shouldDeferAccessibilityEvent(event)) {
+            return;
+        }
+        super.sendAccessibilityEventUnchecked(event);
+    }
+
+    /**
+     * Gets the current ItemAnimator for this RecyclerView. A null return value
+     * indicates that there is no animator and that item changes will happen without
+     * any animations. By default, RecyclerView instantiates and
+     * uses an instance of {@link DefaultItemAnimator}.
+     *
+     * @return ItemAnimator The current ItemAnimator. If null, no animations will occur
+     * when changes occur to the items in this RecyclerView.
+     */
+    public ItemAnimator getItemAnimator() {
+        return mItemAnimator;
+    }
+
+    /**
+     * Post a runnable to the next frame to run pending item animations. Only the first such
+     * request will be posted, governed by the mPostedAnimatorRunner flag.
+     */
+    void postAnimationRunner() {
+        if (!mPostedAnimatorRunner && mIsAttached) {
+            postOnAnimation(mItemAnimatorRunner);
+            mPostedAnimatorRunner = true;
+        }
+    }
+
+    private boolean predictiveItemAnimationsEnabled() {
+        return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
+    }
+
+    /**
+     * Consumes adapter updates and calculates which type of animations we want to run.
+     * Called in onMeasure and dispatchLayout.
+     * <p>
+     * This method may process only the pre-layout state of updates or all of them.
+     */
+    private void processAdapterUpdatesAndSetAnimationFlags() {
+        if (mDataSetHasChangedAfterLayout) {
+            // Processing these items have no value since data set changed unexpectedly.
+            // Instead, we just reset it.
+            mAdapterHelper.reset();
+            mLayout.onItemsChanged(this);
+        }
+        // simple animations are a subset of advanced animations (which will cause a
+        // pre-layout step)
+        // If layout supports predictive animations, pre-process to decide if we want to run them
+        if (predictiveItemAnimationsEnabled()) {
+            mAdapterHelper.preProcess();
+        } else {
+            mAdapterHelper.consumeUpdatesInOnePass();
+        }
+        boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
+        mState.mRunSimpleAnimations = mFirstLayoutComplete
+                && mItemAnimator != null
+                && (mDataSetHasChangedAfterLayout
+                        || animationTypeSupported
+                        || mLayout.mRequestedSimpleAnimations)
+                && (!mDataSetHasChangedAfterLayout
+                        || mAdapter.hasStableIds());
+        mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
+                && animationTypeSupported
+                && !mDataSetHasChangedAfterLayout
+                && predictiveItemAnimationsEnabled();
+    }
+
+    /**
+     * Wrapper around layoutChildren() that handles animating changes caused by layout.
+     * Animations work on the assumption that there are five different kinds of items
+     * in play:
+     * PERSISTENT: items are visible before and after layout
+     * REMOVED: items were visible before layout and were removed by the app
+     * ADDED: items did not exist before layout and were added by the app
+     * DISAPPEARING: items exist in the data set before/after, but changed from
+     * visible to non-visible in the process of layout (they were moved off
+     * screen as a side-effect of other changes)
+     * APPEARING: items exist in the data set before/after, but changed from
+     * non-visible to visible in the process of layout (they were moved on
+     * screen as a side-effect of other changes)
+     * The overall approach figures out what items exist before/after layout and
+     * infers one of the five above states for each of the items. Then the animations
+     * are set up accordingly:
+     * PERSISTENT views are animated via
+     * {@link ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
+     * DISAPPEARING views are animated via
+     * {@link ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
+     * APPEARING views are animated via
+     * {@link ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
+     * and changed views are animated via
+     * {@link ItemAnimator#animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)}.
+     */
+    void dispatchLayout() {
+        if (mAdapter == null) {
+            Log.e(TAG, "No adapter attached; skipping layout");
+            // leave the state in START
+            return;
+        }
+        if (mLayout == null) {
+            Log.e(TAG, "No layout manager attached; skipping layout");
+            // leave the state in START
+            return;
+        }
+        mState.mIsMeasuring = false;
+        if (mState.mLayoutStep == State.STEP_START) {
+            dispatchLayoutStep1();
+            mLayout.setExactMeasureSpecsFrom(this);
+            dispatchLayoutStep2();
+        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
+                || mLayout.getHeight() != getHeight()) {
+            // First 2 steps are done in onMeasure but looks like we have to run again due to
+            // changed size.
+            mLayout.setExactMeasureSpecsFrom(this);
+            dispatchLayoutStep2();
+        } else {
+            // always make sure we sync them (to ensure mode is exact)
+            mLayout.setExactMeasureSpecsFrom(this);
+        }
+        dispatchLayoutStep3();
+    }
+
+    private void saveFocusInfo() {
+        View child = null;
+        if (mPreserveFocusAfterLayout && hasFocus() && mAdapter != null) {
+            child = getFocusedChild();
+        }
+
+        final ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child);
+        if (focusedVh == null) {
+            resetFocusInfo();
+        } else {
+            mState.mFocusedItemId = mAdapter.hasStableIds() ? focusedVh.getItemId() : NO_ID;
+            // mFocusedItemPosition should hold the current adapter position of the previously
+            // focused item. If the item is removed, we store the previous adapter position of the
+            // removed item.
+            mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION
+                    : (focusedVh.isRemoved() ? focusedVh.mOldPosition
+                            : focusedVh.getAdapterPosition());
+            mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView);
+        }
+    }
+
+    private void resetFocusInfo() {
+        mState.mFocusedItemId = NO_ID;
+        mState.mFocusedItemPosition = NO_POSITION;
+        mState.mFocusedSubChildId = View.NO_ID;
+    }
+
+    /**
+     * Finds the best view candidate to request focus on using mFocusedItemPosition index of the
+     * previously focused item. It first traverses the adapter forward to find a focusable candidate
+     * and if no such candidate is found, it reverses the focus search direction for the items
+     * before the mFocusedItemPosition'th index;
+     * @return The best candidate to request focus on, or null if no such candidate exists. Null
+     * indicates all the existing adapter items are unfocusable.
+     */
+    @Nullable
+    private View findNextViewToFocus() {
+        int startFocusSearchIndex = mState.mFocusedItemPosition != -1 ? mState.mFocusedItemPosition
+                : 0;
+        ViewHolder nextFocus;
+        final int itemCount = mState.getItemCount();
+        for (int i = startFocusSearchIndex; i < itemCount; i++) {
+            nextFocus = findViewHolderForAdapterPosition(i);
+            if (nextFocus == null) {
+                break;
+            }
+            if (nextFocus.itemView.hasFocusable()) {
+                return nextFocus.itemView;
+            }
+        }
+        final int limit = Math.min(itemCount, startFocusSearchIndex);
+        for (int i = limit - 1; i >= 0; i--) {
+            nextFocus = findViewHolderForAdapterPosition(i);
+            if (nextFocus == null) {
+                return null;
+            }
+            if (nextFocus.itemView.hasFocusable()) {
+                return nextFocus.itemView;
+            }
+        }
+        return null;
+    }
+
+    private void recoverFocusFromState() {
+        if (!mPreserveFocusAfterLayout || mAdapter == null || !hasFocus()
+                || getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS
+                || (getDescendantFocusability() == FOCUS_BEFORE_DESCENDANTS && isFocused())) {
+            // No-op if either of these cases happens:
+            // 1. RV has no focus, or 2. RV blocks focus to its children, or 3. RV takes focus
+            // before its children and is focused (i.e. it already stole the focus away from its
+            // descendants).
+            return;
+        }
+        // only recover focus if RV itself has the focus or the focused view is hidden
+        if (!isFocused()) {
+            final View focusedChild = getFocusedChild();
+            if (IGNORE_DETACHED_FOCUSED_CHILD
+                    && (focusedChild.getParent() == null || !focusedChild.hasFocus())) {
+                // Special handling of API 15-. A focused child can be invalid because mFocus is not
+                // cleared when the child is detached (mParent = null),
+                // This happens because clearFocus on API 15- does not invalidate mFocus of its
+                // parent when this child is detached.
+                // For API 16+, this is not an issue because requestFocus takes care of clearing the
+                // prior detached focused child. For API 15- the problem happens in 2 cases because
+                // clearChild does not call clearChildFocus on RV: 1. setFocusable(false) is called
+                // for the current focused item which calls clearChild or 2. when the prior focused
+                // child is removed, removeDetachedView called in layout step 3 which calls
+                // clearChild. We should ignore this invalid focused child in all our calculations
+                // for the next view to receive focus, and apply the focus recovery logic instead.
+                if (mChildHelper.getChildCount() == 0) {
+                    // No children left. Request focus on the RV itself since one of its children
+                    // was holding focus previously.
+                    requestFocus();
+                    return;
+                }
+            } else if (!mChildHelper.isHidden(focusedChild)) {
+                // If the currently focused child is hidden, apply the focus recovery logic.
+                // Otherwise return, i.e. the currently (unhidden) focused child is good enough :/.
+                return;
+            }
+        }
+        ViewHolder focusTarget = null;
+        // RV first attempts to locate the previously focused item to request focus on using
+        // mFocusedItemId. If such an item no longer exists, it then makes a best-effort attempt to
+        // find the next best candidate to request focus on based on mFocusedItemPosition.
+        if (mState.mFocusedItemId != NO_ID && mAdapter.hasStableIds()) {
+            focusTarget = findViewHolderForItemId(mState.mFocusedItemId);
+        }
+        View viewToFocus = null;
+        if (focusTarget == null || mChildHelper.isHidden(focusTarget.itemView)
+                || !focusTarget.itemView.hasFocusable()) {
+            if (mChildHelper.getChildCount() > 0) {
+                // At this point, RV has focus and either of these conditions are true:
+                // 1. There's no previously focused item either because RV received focused before
+                // layout, or the previously focused item was removed, or RV doesn't have stable IDs
+                // 2. Previous focus child is hidden, or 3. Previous focused child is no longer
+                // focusable. In either of these cases, we make sure that RV still passes down the
+                // focus to one of its focusable children using a best-effort algorithm.
+                viewToFocus = findNextViewToFocus();
+            }
+        } else {
+            // looks like the focused item has been replaced with another view that represents the
+            // same item in the adapter. Request focus on that.
+            viewToFocus = focusTarget.itemView;
+        }
+
+        if (viewToFocus != null) {
+            if (mState.mFocusedSubChildId != NO_ID) {
+                View child = viewToFocus.findViewById(mState.mFocusedSubChildId);
+                if (child != null && child.isFocusable()) {
+                    viewToFocus = child;
+                }
+            }
+            viewToFocus.requestFocus();
+        }
+    }
+
+    private int getDeepestFocusedViewWithId(View view) {
+        int lastKnownId = view.getId();
+        while (!view.isFocused() && view instanceof ViewGroup && view.hasFocus()) {
+            view = ((ViewGroup) view).getFocusedChild();
+            final int id = view.getId();
+            if (id != View.NO_ID) {
+                lastKnownId = view.getId();
+            }
+        }
+        return lastKnownId;
+    }
+
+    /**
+     * The first step of a layout where we;
+     * - process adapter updates
+     * - decide which animation should run
+     * - save information about current views
+     * - If necessary, run predictive layout and save its information
+     */
+    private void dispatchLayoutStep1() {
+        mState.assertLayoutStep(State.STEP_START);
+        mState.mIsMeasuring = false;
+        eatRequestLayout();
+        mViewInfoStore.clear();
+        onEnterLayoutOrScroll();
+        processAdapterUpdatesAndSetAnimationFlags();
+        saveFocusInfo();
+        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
+        mItemsAddedOrRemoved = mItemsChanged = false;
+        mState.mInPreLayout = mState.mRunPredictiveAnimations;
+        mState.mItemCount = mAdapter.getItemCount();
+        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
+
+        if (mState.mRunSimpleAnimations) {
+            // Step 0: Find out where all non-removed items are, pre-layout
+            int count = mChildHelper.getChildCount();
+            for (int i = 0; i < count; ++i) {
+                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+                if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
+                    continue;
+                }
+                final ItemHolderInfo animationInfo = mItemAnimator
+                        .recordPreLayoutInformation(mState, holder,
+                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
+                                holder.getUnmodifiedPayloads());
+                mViewInfoStore.addToPreLayout(holder, animationInfo);
+                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
+                        && !holder.shouldIgnore() && !holder.isInvalid()) {
+                    long key = getChangedHolderKey(holder);
+                    // This is NOT the only place where a ViewHolder is added to old change holders
+                    // list. There is another case where:
+                    //    * A VH is currently hidden but not deleted
+                    //    * The hidden item is changed in the adapter
+                    //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
+                    // When this case is detected, RV will un-hide that view and add to the old
+                    // change holders list.
+                    mViewInfoStore.addToOldChangeHolders(key, holder);
+                }
+            }
+        }
+        if (mState.mRunPredictiveAnimations) {
+            // Step 1: run prelayout: This will use the old positions of items. The layout manager
+            // is expected to layout everything, even removed items (though not to add removed
+            // items back to the container). This gives the pre-layout position of APPEARING views
+            // which come into existence as part of the real layout.
+
+            // Save old positions so that LayoutManager can run its mapping logic.
+            saveOldPositions();
+            final boolean didStructureChange = mState.mStructureChanged;
+            mState.mStructureChanged = false;
+            // temporarily disable flag because we are asking for previous layout
+            mLayout.onLayoutChildren(mRecycler, mState);
+            mState.mStructureChanged = didStructureChange;
+
+            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
+                final View child = mChildHelper.getChildAt(i);
+                final ViewHolder viewHolder = getChildViewHolderInt(child);
+                if (viewHolder.shouldIgnore()) {
+                    continue;
+                }
+                if (!mViewInfoStore.isInPreLayout(viewHolder)) {
+                    int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
+                    boolean wasHidden = viewHolder
+                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                    if (!wasHidden) {
+                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    }
+                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
+                            mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
+                    if (wasHidden) {
+                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
+                    } else {
+                        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
+                    }
+                }
+            }
+            // we don't process disappearing list because they may re-appear in post layout pass.
+            clearOldPositions();
+        } else {
+            clearOldPositions();
+        }
+        onExitLayoutOrScroll();
+        resumeRequestLayout(false);
+        mState.mLayoutStep = State.STEP_LAYOUT;
+    }
+
+    /**
+     * The second layout step where we do the actual layout of the views for the final state.
+     * This step might be run multiple times if necessary (e.g. measure).
+     */
+    private void dispatchLayoutStep2() {
+        eatRequestLayout();
+        onEnterLayoutOrScroll();
+        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
+        mAdapterHelper.consumeUpdatesInOnePass();
+        mState.mItemCount = mAdapter.getItemCount();
+        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
+
+        // Step 2: Run layout
+        mState.mInPreLayout = false;
+        mLayout.onLayoutChildren(mRecycler, mState);
+
+        mState.mStructureChanged = false;
+        mPendingSavedState = null;
+
+        // onLayoutChildren may have caused client code to disable item animations; re-check
+        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
+        mState.mLayoutStep = State.STEP_ANIMATIONS;
+        onExitLayoutOrScroll();
+        resumeRequestLayout(false);
+    }
+
+    /**
+     * The final step of the layout where we save the information about views for animations,
+     * trigger animations and do any necessary cleanup.
+     */
+    private void dispatchLayoutStep3() {
+        mState.assertLayoutStep(State.STEP_ANIMATIONS);
+        eatRequestLayout();
+        onEnterLayoutOrScroll();
+        mState.mLayoutStep = State.STEP_START;
+        if (mState.mRunSimpleAnimations) {
+            // Step 3: Find out where things are now, and process change animations.
+            // traverse list in reverse because we may call animateChange in the loop which may
+            // remove the target view holder.
+            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
+                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+                if (holder.shouldIgnore()) {
+                    continue;
+                }
+                long key = getChangedHolderKey(holder);
+                final ItemHolderInfo animationInfo = mItemAnimator
+                        .recordPostLayoutInformation(mState, holder);
+                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
+                if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
+                    // run a change animation
+
+                    // If an Item is CHANGED but the updated version is disappearing, it creates
+                    // a conflicting case.
+                    // Since a view that is marked as disappearing is likely to be going out of
+                    // bounds, we run a change animation. Both views will be cleaned automatically
+                    // once their animations finish.
+                    // On the other hand, if it is the same view holder instance, we run a
+                    // disappearing animation instead because we are not going to rebind the updated
+                    // VH unless it is enforced by the layout manager.
+                    final boolean oldDisappearing = mViewInfoStore.isDisappearing(
+                            oldChangeViewHolder);
+                    final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
+                    if (oldDisappearing && oldChangeViewHolder == holder) {
+                        // run disappear animation instead of change
+                        mViewInfoStore.addToPostLayout(holder, animationInfo);
+                    } else {
+                        final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
+                                oldChangeViewHolder);
+                        // we add and remove so that any post info is merged.
+                        mViewInfoStore.addToPostLayout(holder, animationInfo);
+                        ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
+                        if (preInfo == null) {
+                            handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
+                        } else {
+                            animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
+                                    oldDisappearing, newDisappearing);
+                        }
+                    }
+                } else {
+                    mViewInfoStore.addToPostLayout(holder, animationInfo);
+                }
+            }
+
+            // Step 4: Process view info lists and trigger animations
+            mViewInfoStore.process(mViewInfoProcessCallback);
+        }
+
+        mLayout.removeAndRecycleScrapInt(mRecycler);
+        mState.mPreviousLayoutItemCount = mState.mItemCount;
+        mDataSetHasChangedAfterLayout = false;
+        mState.mRunSimpleAnimations = false;
+
+        mState.mRunPredictiveAnimations = false;
+        mLayout.mRequestedSimpleAnimations = false;
+        if (mRecycler.mChangedScrap != null) {
+            mRecycler.mChangedScrap.clear();
+        }
+        if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
+            // Initial prefetch has expanded cache, so reset until next prefetch.
+            // This prevents initial prefetches from expanding the cache permanently.
+            mLayout.mPrefetchMaxCountObserved = 0;
+            mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
+            mRecycler.updateViewCacheSize();
+        }
+
+        mLayout.onLayoutCompleted(mState);
+        onExitLayoutOrScroll();
+        resumeRequestLayout(false);
+        mViewInfoStore.clear();
+        if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
+            dispatchOnScrolled(0, 0);
+        }
+        recoverFocusFromState();
+        resetFocusInfo();
+    }
+
+    /**
+     * This handles the case where there is an unexpected VH missing in the pre-layout map.
+     * <p>
+     * We might be able to detect the error in the application which will help the developer to
+     * resolve the issue.
+     * <p>
+     * If it is not an expected error, we at least print an error to notify the developer and ignore
+     * the animation.
+     *
+     * https://code.google.com/p/android/issues/detail?id=193958
+     *
+     * @param key The change key
+     * @param holder Current ViewHolder
+     * @param oldChangeViewHolder Changed ViewHolder
+     */
+    private void handleMissingPreInfoForChangeError(long key,
+            ViewHolder holder, ViewHolder oldChangeViewHolder) {
+        // check if two VH have the same key, if so, print that as an error
+        final int childCount = mChildHelper.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View view = mChildHelper.getChildAt(i);
+            ViewHolder other = getChildViewHolderInt(view);
+            if (other == holder) {
+                continue;
+            }
+            final long otherKey = getChangedHolderKey(other);
+            if (otherKey == key) {
+                if (mAdapter != null && mAdapter.hasStableIds()) {
+                    throw new IllegalStateException("Two different ViewHolders have the same stable"
+                            + " ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT"
+                            + " change.\n ViewHolder 1:" + other + " \n View Holder 2:" + holder);
+                } else {
+                    throw new IllegalStateException("Two different ViewHolders have the same change"
+                            + " ID. This might happen due to inconsistent Adapter update events or"
+                            + " if the LayoutManager lays out the same View multiple times."
+                            + "\n ViewHolder 1:" + other + " \n View Holder 2:" + holder);
+                }
+            }
+        }
+        // Very unlikely to happen but if it does, notify the developer.
+        Log.e(TAG, "Problem while matching changed view holders with the new"
+                + "ones. The pre-layout information for the change holder " + oldChangeViewHolder
+                + " cannot be found but it is necessary for " + holder);
+    }
+
+    /**
+     * Records the animation information for a view holder that was bounced from hidden list. It
+     * also clears the bounce back flag.
+     */
+    void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,
+            ItemHolderInfo animationInfo) {
+        // looks like this view bounced back from hidden list!
+        viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+        if (mState.mTrackOldChangeHolders && viewHolder.isUpdated()
+                && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) {
+            long key = getChangedHolderKey(viewHolder);
+            mViewInfoStore.addToOldChangeHolders(key, viewHolder);
+        }
+        mViewInfoStore.addToPreLayout(viewHolder, animationInfo);
+    }
+
+    private void findMinMaxChildLayoutPositions(int[] into) {
+        final int count = mChildHelper.getChildCount();
+        if (count == 0) {
+            into[0] = NO_POSITION;
+            into[1] = NO_POSITION;
+            return;
+        }
+        int minPositionPreLayout = Integer.MAX_VALUE;
+        int maxPositionPreLayout = Integer.MIN_VALUE;
+        for (int i = 0; i < count; ++i) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+            if (holder.shouldIgnore()) {
+                continue;
+            }
+            final int pos = holder.getLayoutPosition();
+            if (pos < minPositionPreLayout) {
+                minPositionPreLayout = pos;
+            }
+            if (pos > maxPositionPreLayout) {
+                maxPositionPreLayout = pos;
+            }
+        }
+        into[0] = minPositionPreLayout;
+        into[1] = maxPositionPreLayout;
+    }
+
+    private boolean didChildRangeChange(int minPositionPreLayout, int maxPositionPreLayout) {
+        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
+        return mMinMaxLayoutPositions[0] != minPositionPreLayout
+                || mMinMaxLayoutPositions[1] != maxPositionPreLayout;
+    }
+
+    @Override
+    protected void removeDetachedView(View child, boolean animate) {
+        ViewHolder vh = getChildViewHolderInt(child);
+        if (vh != null) {
+            if (vh.isTmpDetached()) {
+                vh.clearTmpDetachFlag();
+            } else if (!vh.shouldIgnore()) {
+                throw new IllegalArgumentException("Called removeDetachedView with a view which"
+                        + " is not flagged as tmp detached." + vh);
+            }
+        }
+        dispatchChildDetached(child);
+        super.removeDetachedView(child, animate);
+    }
+
+    /**
+     * Returns a unique key to be used while handling change animations.
+     * It might be child's position or stable id depending on the adapter type.
+     */
+    long getChangedHolderKey(ViewHolder holder) {
+        return mAdapter.hasStableIds() ? holder.getItemId() : holder.mPosition;
+    }
+
+    void animateAppearance(@NonNull ViewHolder itemHolder,
+            @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
+        itemHolder.setIsRecyclable(false);
+        if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
+            postAnimationRunner();
+        }
+    }
+
+    void animateDisappearance(@NonNull ViewHolder holder,
+            @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
+        addAnimatingView(holder);
+        holder.setIsRecyclable(false);
+        if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
+            postAnimationRunner();
+        }
+    }
+
+    private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
+            @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
+            boolean oldHolderDisappearing, boolean newHolderDisappearing) {
+        oldHolder.setIsRecyclable(false);
+        if (oldHolderDisappearing) {
+            addAnimatingView(oldHolder);
+        }
+        if (oldHolder != newHolder) {
+            if (newHolderDisappearing) {
+                addAnimatingView(newHolder);
+            }
+            oldHolder.mShadowedHolder = newHolder;
+            // old holder should disappear after animation ends
+            addAnimatingView(oldHolder);
+            mRecycler.unscrapView(oldHolder);
+            newHolder.setIsRecyclable(false);
+            newHolder.mShadowingHolder = oldHolder;
+        }
+        if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
+            postAnimationRunner();
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        Trace.beginSection(TRACE_ON_LAYOUT_TAG);
+        dispatchLayout();
+        Trace.endSection();
+        mFirstLayoutComplete = true;
+    }
+
+    @Override
+    public void requestLayout() {
+        if (mEatRequestLayout == 0 && !mLayoutFrozen) {
+            super.requestLayout();
+        } else {
+            mLayoutRequestEaten = true;
+        }
+    }
+
+    void markItemDecorInsetsDirty() {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = mChildHelper.getUnfilteredChildAt(i);
+            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
+        }
+        mRecycler.markItemDecorInsetsDirty();
+    }
+
+    @Override
+    public void draw(Canvas c) {
+        super.draw(c);
+
+        final int count = mItemDecorations.size();
+        for (int i = 0; i < count; i++) {
+            mItemDecorations.get(i).onDrawOver(c, this, mState);
+        }
+        // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
+        // need find children closest to edges. Not sure if it is worth the effort.
+        boolean needsInvalidate = false;
+        if (mLeftGlow != null && !mLeftGlow.isFinished()) {
+            final int restore = c.save();
+            final int padding = mClipToPadding ? getPaddingBottom() : 0;
+            c.rotate(270);
+            c.translate(-getHeight() + padding, 0);
+            needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c);
+            c.restoreToCount(restore);
+        }
+        if (mTopGlow != null && !mTopGlow.isFinished()) {
+            final int restore = c.save();
+            if (mClipToPadding) {
+                c.translate(getPaddingLeft(), getPaddingTop());
+            }
+            needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
+            c.restoreToCount(restore);
+        }
+        if (mRightGlow != null && !mRightGlow.isFinished()) {
+            final int restore = c.save();
+            final int width = getWidth();
+            final int padding = mClipToPadding ? getPaddingTop() : 0;
+            c.rotate(90);
+            c.translate(-padding, -width);
+            needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);
+            c.restoreToCount(restore);
+        }
+        if (mBottomGlow != null && !mBottomGlow.isFinished()) {
+            final int restore = c.save();
+            c.rotate(180);
+            if (mClipToPadding) {
+                c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom());
+            } else {
+                c.translate(-getWidth(), -getHeight());
+            }
+            needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c);
+            c.restoreToCount(restore);
+        }
+
+        // If some views are animating, ItemDecorators are likely to move/change with them.
+        // Invalidate RecyclerView to re-draw decorators. This is still efficient because children's
+        // display lists are not invalidated.
+        if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0
+                && mItemAnimator.isRunning()) {
+            needsInvalidate = true;
+        }
+
+        if (needsInvalidate) {
+            postInvalidateOnAnimation();
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas c) {
+        super.onDraw(c);
+
+        final int count = mItemDecorations.size();
+        for (int i = 0; i < count; i++) {
+            mItemDecorations.get(i).onDraw(c, this, mState);
+        }
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        if (mLayout == null) {
+            throw new IllegalStateException("RecyclerView has no LayoutManager");
+        }
+        return mLayout.generateDefaultLayoutParams();
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        if (mLayout == null) {
+            throw new IllegalStateException("RecyclerView has no LayoutManager");
+        }
+        return mLayout.generateLayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        if (mLayout == null) {
+            throw new IllegalStateException("RecyclerView has no LayoutManager");
+        }
+        return mLayout.generateLayoutParams(p);
+    }
+
+    /**
+     * Returns true if RecyclerView is currently running some animations.
+     * <p>
+     * If you want to be notified when animations are finished, use
+     * {@link ItemAnimator#isRunning(ItemAnimator.ItemAnimatorFinishedListener)}.
+     *
+     * @return True if there are some item animations currently running or waiting to be started.
+     */
+    public boolean isAnimating() {
+        return mItemAnimator != null && mItemAnimator.isRunning();
+    }
+
+    void saveOldPositions() {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) {
+                throw new IllegalStateException("view holder cannot have position -1 unless it"
+                        + " is removed");
+            }
+            if (!holder.shouldIgnore()) {
+                holder.saveOldPosition();
+            }
+        }
+    }
+
+    void clearOldPositions() {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (!holder.shouldIgnore()) {
+                holder.clearOldPosition();
+            }
+        }
+        mRecycler.clearOldPositions();
+    }
+
+    void offsetPositionRecordsForMove(int from, int to) {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        final int start, end, inBetweenOffset;
+        if (from < to) {
+            start = from;
+            end = to;
+            inBetweenOffset = -1;
+        } else {
+            start = to;
+            end = from;
+            inBetweenOffset = 1;
+        }
+
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+                continue;
+            }
+            if (DEBUG) {
+                Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder "
+                        + holder);
+            }
+            if (holder.mPosition == from) {
+                holder.offsetPosition(to - from, false);
+            } else {
+                holder.offsetPosition(inBetweenOffset, false);
+            }
+
+            mState.mStructureChanged = true;
+        }
+        mRecycler.offsetPositionRecordsForMove(from, to);
+        requestLayout();
+    }
+
+    void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
+                if (DEBUG) {
+                    Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder "
+                            + holder + " now at position " + (holder.mPosition + itemCount));
+                }
+                holder.offsetPosition(itemCount, false);
+                mState.mStructureChanged = true;
+            }
+        }
+        mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
+        requestLayout();
+    }
+
+    void offsetPositionRecordsForRemove(int positionStart, int itemCount,
+            boolean applyToPreLayout) {
+        final int positionEnd = positionStart + itemCount;
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.shouldIgnore()) {
+                if (holder.mPosition >= positionEnd) {
+                    if (DEBUG) {
+                        Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
+                                + " holder " + holder + " now at position "
+                                + (holder.mPosition - itemCount));
+                    }
+                    holder.offsetPosition(-itemCount, applyToPreLayout);
+                    mState.mStructureChanged = true;
+                } else if (holder.mPosition >= positionStart) {
+                    if (DEBUG) {
+                        Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
+                                + " holder " + holder + " now REMOVED");
+                    }
+                    holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
+                            applyToPreLayout);
+                    mState.mStructureChanged = true;
+                }
+            }
+        }
+        mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
+        requestLayout();
+    }
+
+    /**
+     * Rebind existing views for the given range, or create as needed.
+     *
+     * @param positionStart Adapter position to start at
+     * @param itemCount Number of views that must explicitly be rebound
+     */
+    void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        final int positionEnd = positionStart + itemCount;
+
+        for (int i = 0; i < childCount; i++) {
+            final View child = mChildHelper.getUnfilteredChildAt(i);
+            final ViewHolder holder = getChildViewHolderInt(child);
+            if (holder == null || holder.shouldIgnore()) {
+                continue;
+            }
+            if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
+                // We re-bind these view holders after pre-processing is complete so that
+                // ViewHolders have their final positions assigned.
+                holder.addFlags(ViewHolder.FLAG_UPDATE);
+                holder.addChangePayload(payload);
+                // lp cannot be null since we get ViewHolder from it.
+                ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
+            }
+        }
+        mRecycler.viewRangeUpdate(positionStart, itemCount);
+    }
+
+    boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
+        return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
+                viewHolder.getUnmodifiedPayloads());
+    }
+
+
+    /**
+     * Call this method to signal that *all* adapter content has changed (generally, because of
+     * swapAdapter, or notifyDataSetChanged), and that once layout occurs, all attached items should
+     * be discarded or animated. Note that this work is deferred because RecyclerView requires a
+     * layout to resolve non-incremental changes to the data set.
+     *
+     * Attached items are labeled as position unknown, and may no longer be cached.
+     *
+     * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
+     * so calling this method *must* be associated with marking the cache invalid, so that the
+     * only valid items that remain in the cache, once layout occurs, are prefetched items.
+     */
+    void setDataSetChangedAfterLayout() {
+        if (mDataSetHasChangedAfterLayout) {
+            return;
+        }
+        mDataSetHasChangedAfterLayout = true;
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.shouldIgnore()) {
+                holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
+            }
+        }
+        mRecycler.setAdapterPositionsAsUnknown();
+
+        // immediately mark all views as invalid, so prefetched views can be
+        // differentiated from views bound to previous data set - both in children, and cache
+        markKnownViewsInvalid();
+    }
+
+    /**
+     * Mark all known views as invalid. Used in response to a, "the whole world might have changed"
+     * data change event.
+     */
+    void markKnownViewsInvalid() {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.shouldIgnore()) {
+                holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+            }
+        }
+        markItemDecorInsetsDirty();
+        mRecycler.markKnownViewsInvalid();
+    }
+
+    /**
+     * Invalidates all ItemDecorations. If RecyclerView has item decorations, calling this method
+     * will trigger a {@link #requestLayout()} call.
+     */
+    public void invalidateItemDecorations() {
+        if (mItemDecorations.size() == 0) {
+            return;
+        }
+        if (mLayout != null) {
+            mLayout.assertNotInLayoutOrScroll("Cannot invalidate item decorations during a scroll"
+                    + " or layout");
+        }
+        markItemDecorInsetsDirty();
+        requestLayout();
+    }
+
+    /**
+     * Returns true if the RecyclerView should attempt to preserve currently focused Adapter Item's
+     * focus even if the View representing the Item is replaced during a layout calculation.
+     * <p>
+     * By default, this value is {@code true}.
+     *
+     * @return True if the RecyclerView will try to preserve focused Item after a layout if it loses
+     * focus.
+     *
+     * @see #setPreserveFocusAfterLayout(boolean)
+     */
+    public boolean getPreserveFocusAfterLayout() {
+        return mPreserveFocusAfterLayout;
+    }
+
+    /**
+     * Set whether the RecyclerView should try to keep the same Item focused after a layout
+     * calculation or not.
+     * <p>
+     * Usually, LayoutManagers keep focused views visible before and after layout but sometimes,
+     * views may lose focus during a layout calculation as their state changes or they are replaced
+     * with another view due to type change or animation. In these cases, RecyclerView can request
+     * focus on the new view automatically.
+     *
+     * @param preserveFocusAfterLayout Whether RecyclerView should preserve focused Item during a
+     *                                 layout calculations. Defaults to true.
+     *
+     * @see #getPreserveFocusAfterLayout()
+     */
+    public void setPreserveFocusAfterLayout(boolean preserveFocusAfterLayout) {
+        mPreserveFocusAfterLayout = preserveFocusAfterLayout;
+    }
+
+    /**
+     * Retrieve the {@link ViewHolder} for the given child view.
+     *
+     * @param child Child of this RecyclerView to query for its ViewHolder
+     * @return The child view's ViewHolder
+     */
+    public ViewHolder getChildViewHolder(View child) {
+        final ViewParent parent = child.getParent();
+        if (parent != null && parent != this) {
+            throw new IllegalArgumentException("View " + child + " is not a direct child of "
+                    + this);
+        }
+        return getChildViewHolderInt(child);
+    }
+
+    /**
+     * Traverses the ancestors of the given view and returns the item view that contains it and
+     * also a direct child of the RecyclerView. This returned view can be used to get the
+     * ViewHolder by calling {@link #getChildViewHolder(View)}.
+     *
+     * @param view The view that is a descendant of the RecyclerView.
+     *
+     * @return The direct child of the RecyclerView which contains the given view or null if the
+     * provided view is not a descendant of this RecyclerView.
+     *
+     * @see #getChildViewHolder(View)
+     * @see #findContainingViewHolder(View)
+     */
+    @Nullable
+    public View findContainingItemView(View view) {
+        ViewParent parent = view.getParent();
+        while (parent != null && parent != this && parent instanceof View) {
+            view = (View) parent;
+            parent = view.getParent();
+        }
+        return parent == this ? view : null;
+    }
+
+    /**
+     * Returns the ViewHolder that contains the given view.
+     *
+     * @param view The view that is a descendant of the RecyclerView.
+     *
+     * @return The ViewHolder that contains the given view or null if the provided view is not a
+     * descendant of this RecyclerView.
+     */
+    @Nullable
+    public ViewHolder findContainingViewHolder(View view) {
+        View itemView = findContainingItemView(view);
+        return itemView == null ? null : getChildViewHolder(itemView);
+    }
+
+
+    static ViewHolder getChildViewHolderInt(View child) {
+        if (child == null) {
+            return null;
+        }
+        return ((LayoutParams) child.getLayoutParams()).mViewHolder;
+    }
+
+    /**
+     * @deprecated use {@link #getChildAdapterPosition(View)} or
+     * {@link #getChildLayoutPosition(View)}.
+     */
+    @Deprecated
+    public int getChildPosition(View child) {
+        return getChildAdapterPosition(child);
+    }
+
+    /**
+     * Return the adapter position that the given child view corresponds to.
+     *
+     * @param child Child View to query
+     * @return Adapter position corresponding to the given view or {@link #NO_POSITION}
+     */
+    public int getChildAdapterPosition(View child) {
+        final ViewHolder holder = getChildViewHolderInt(child);
+        return holder != null ? holder.getAdapterPosition() : NO_POSITION;
+    }
+
+    /**
+     * Return the adapter position of the given child view as of the latest completed layout pass.
+     * <p>
+     * This position may not be equal to Item's adapter position if there are pending changes
+     * in the adapter which have not been reflected to the layout yet.
+     *
+     * @param child Child View to query
+     * @return Adapter position of the given View as of last layout pass or {@link #NO_POSITION} if
+     * the View is representing a removed item.
+     */
+    public int getChildLayoutPosition(View child) {
+        final ViewHolder holder = getChildViewHolderInt(child);
+        return holder != null ? holder.getLayoutPosition() : NO_POSITION;
+    }
+
+    /**
+     * Return the stable item id that the given child view corresponds to.
+     *
+     * @param child Child View to query
+     * @return Item id corresponding to the given view or {@link #NO_ID}
+     */
+    public long getChildItemId(View child) {
+        if (mAdapter == null || !mAdapter.hasStableIds()) {
+            return NO_ID;
+        }
+        final ViewHolder holder = getChildViewHolderInt(child);
+        return holder != null ? holder.getItemId() : NO_ID;
+    }
+
+    /**
+     * @deprecated use {@link #findViewHolderForLayoutPosition(int)} or
+     * {@link #findViewHolderForAdapterPosition(int)}
+     */
+    @Deprecated
+    public ViewHolder findViewHolderForPosition(int position) {
+        return findViewHolderForPosition(position, false);
+    }
+
+    /**
+     * Return the ViewHolder for the item in the given position of the data set as of the latest
+     * layout pass.
+     * <p>
+     * This method checks only the children of RecyclerView. If the item at the given
+     * <code>position</code> is not laid out, it <em>will not</em> create a new one.
+     * <p>
+     * Note that when Adapter contents change, ViewHolder positions are not updated until the
+     * next layout calculation. If there are pending adapter updates, the return value of this
+     * method may not match your adapter contents. You can use
+     * #{@link ViewHolder#getAdapterPosition()} to get the current adapter position of a ViewHolder.
+     * <p>
+     * When the ItemAnimator is running a change animation, there might be 2 ViewHolders
+     * with the same layout position representing the same Item. In this case, the updated
+     * ViewHolder will be returned.
+     *
+     * @param position The position of the item in the data set of the adapter
+     * @return The ViewHolder at <code>position</code> or null if there is no such item
+     */
+    public ViewHolder findViewHolderForLayoutPosition(int position) {
+        return findViewHolderForPosition(position, false);
+    }
+
+    /**
+     * Return the ViewHolder for the item in the given position of the data set. Unlike
+     * {@link #findViewHolderForLayoutPosition(int)} this method takes into account any pending
+     * adapter changes that may not be reflected to the layout yet. On the other hand, if
+     * {@link Adapter#notifyDataSetChanged()} has been called but the new layout has not been
+     * calculated yet, this method will return <code>null</code> since the new positions of views
+     * are unknown until the layout is calculated.
+     * <p>
+     * This method checks only the children of RecyclerView. If the item at the given
+     * <code>position</code> is not laid out, it <em>will not</em> create a new one.
+     * <p>
+     * When the ItemAnimator is running a change animation, there might be 2 ViewHolders
+     * representing the same Item. In this case, the updated ViewHolder will be returned.
+     *
+     * @param position The position of the item in the data set of the adapter
+     * @return The ViewHolder at <code>position</code> or null if there is no such item
+     */
+    public ViewHolder findViewHolderForAdapterPosition(int position) {
+        if (mDataSetHasChangedAfterLayout) {
+            return null;
+        }
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        // hidden VHs are not preferred but if that is the only one we find, we rather return it
+        ViewHolder hidden = null;
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.isRemoved()
+                    && getAdapterPositionFor(holder) == position) {
+                if (mChildHelper.isHidden(holder.itemView)) {
+                    hidden = holder;
+                } else {
+                    return holder;
+                }
+            }
+        }
+        return hidden;
+    }
+
+    ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) {
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        ViewHolder hidden = null;
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.isRemoved()) {
+                if (checkNewPosition) {
+                    if (holder.mPosition != position) {
+                        continue;
+                    }
+                } else if (holder.getLayoutPosition() != position) {
+                    continue;
+                }
+                if (mChildHelper.isHidden(holder.itemView)) {
+                    hidden = holder;
+                } else {
+                    return holder;
+                }
+            }
+        }
+        // This method should not query cached views. It creates a problem during adapter updates
+        // when we are dealing with already laid out views. Also, for the public method, it is more
+        // reasonable to return null if position is not laid out.
+        return hidden;
+    }
+
+    /**
+     * Return the ViewHolder for the item with the given id. The RecyclerView must
+     * use an Adapter with {@link Adapter#setHasStableIds(boolean) stableIds} to
+     * return a non-null value.
+     * <p>
+     * This method checks only the children of RecyclerView. If the item with the given
+     * <code>id</code> is not laid out, it <em>will not</em> create a new one.
+     *
+     * When the ItemAnimator is running a change animation, there might be 2 ViewHolders with the
+     * same id. In this case, the updated ViewHolder will be returned.
+     *
+     * @param id The id for the requested item
+     * @return The ViewHolder with the given <code>id</code> or null if there is no such item
+     */
+    public ViewHolder findViewHolderForItemId(long id) {
+        if (mAdapter == null || !mAdapter.hasStableIds()) {
+            return null;
+        }
+        final int childCount = mChildHelper.getUnfilteredChildCount();
+        ViewHolder hidden = null;
+        for (int i = 0; i < childCount; i++) {
+            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+            if (holder != null && !holder.isRemoved() && holder.getItemId() == id) {
+                if (mChildHelper.isHidden(holder.itemView)) {
+                    hidden = holder;
+                } else {
+                    return holder;
+                }
+            }
+        }
+        return hidden;
+    }
+
+    /**
+     * Find the topmost view under the given point.
+     *
+     * @param x Horizontal position in pixels to search
+     * @param y Vertical position in pixels to search
+     * @return The child view under (x, y) or null if no matching child is found
+     */
+    public View findChildViewUnder(float x, float y) {
+        final int count = mChildHelper.getChildCount();
+        for (int i = count - 1; i >= 0; i--) {
+            final View child = mChildHelper.getChildAt(i);
+            final float translationX = child.getTranslationX();
+            final float translationY = child.getTranslationY();
+            if (x >= child.getLeft() + translationX
+                    && x <= child.getRight() + translationX
+                    && y >= child.getTop() + translationY
+                    && y <= child.getBottom() + translationY) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        return super.drawChild(canvas, child, drawingTime);
+    }
+
+    /**
+     * Offset the bounds of all child views by <code>dy</code> pixels.
+     * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
+     *
+     * @param dy Vertical pixel offset to apply to the bounds of all child views
+     */
+    public void offsetChildrenVertical(int dy) {
+        final int childCount = mChildHelper.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
+        }
+    }
+
+    /**
+     * Called when an item view is attached to this RecyclerView.
+     *
+     * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
+     * of child views as they become attached. This will be called before a
+     * {@link LayoutManager} measures or lays out the view and is a good time to perform these
+     * changes.</p>
+     *
+     * @param child Child view that is now attached to this RecyclerView and its associated window
+     */
+    public void onChildAttachedToWindow(View child) {
+    }
+
+    /**
+     * Called when an item view is detached from this RecyclerView.
+     *
+     * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
+     * of child views as they become detached. This will be called as a
+     * {@link LayoutManager} fully detaches the child view from the parent and its window.</p>
+     *
+     * @param child Child view that is now detached from this RecyclerView and its associated window
+     */
+    public void onChildDetachedFromWindow(View child) {
+    }
+
+    /**
+     * Offset the bounds of all child views by <code>dx</code> pixels.
+     * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
+     *
+     * @param dx Horizontal pixel offset to apply to the bounds of all child views
+     */
+    public void offsetChildrenHorizontal(int dx) {
+        final int childCount = mChildHelper.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
+        }
+    }
+
+    /**
+     * Returns the bounds of the view including its decoration and margins.
+     *
+     * @param view The view element to check
+     * @param outBounds A rect that will receive the bounds of the element including its
+     *                  decoration and margins.
+     */
+    public void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
+        getDecoratedBoundsWithMarginsInt(view, outBounds);
+    }
+
+    static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) {
+        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+        final Rect insets = lp.mDecorInsets;
+        outBounds.set(view.getLeft() - insets.left - lp.leftMargin,
+                view.getTop() - insets.top - lp.topMargin,
+                view.getRight() + insets.right + lp.rightMargin,
+                view.getBottom() + insets.bottom + lp.bottomMargin);
+    }
+
+    Rect getItemDecorInsetsForChild(View child) {
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        if (!lp.mInsetsDirty) {
+            return lp.mDecorInsets;
+        }
+
+        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
+            // changed/invalid items should not be updated until they are rebound.
+            return lp.mDecorInsets;
+        }
+        final Rect insets = lp.mDecorInsets;
+        insets.set(0, 0, 0, 0);
+        final int decorCount = mItemDecorations.size();
+        for (int i = 0; i < decorCount; i++) {
+            mTempRect.set(0, 0, 0, 0);
+            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
+            insets.left += mTempRect.left;
+            insets.top += mTempRect.top;
+            insets.right += mTempRect.right;
+            insets.bottom += mTempRect.bottom;
+        }
+        lp.mInsetsDirty = false;
+        return insets;
+    }
+
+    /**
+     * Called when the scroll position of this RecyclerView changes. Subclasses should use
+     * this method to respond to scrolling within the adapter's data set instead of an explicit
+     * listener.
+     *
+     * <p>This method will always be invoked before listeners. If a subclass needs to perform
+     * any additional upkeep or bookkeeping after scrolling but before listeners run,
+     * this is a good place to do so.</p>
+     *
+     * <p>This differs from {@link View#onScrollChanged(int, int, int, int)} in that it receives
+     * the distance scrolled in either direction within the adapter's data set instead of absolute
+     * scroll coordinates. Since RecyclerView cannot compute the absolute scroll position from
+     * any arbitrary point in the data set, <code>onScrollChanged</code> will always receive
+     * the current {@link View#getScrollX()} and {@link View#getScrollY()} values which
+     * do not correspond to the data set scroll position. However, some subclasses may choose
+     * to use these fields as special offsets.</p>
+     *
+     * @param dx horizontal distance scrolled in pixels
+     * @param dy vertical distance scrolled in pixels
+     */
+    public void onScrolled(int dx, int dy) {
+        // Do nothing
+    }
+
+    void dispatchOnScrolled(int hresult, int vresult) {
+        mDispatchScrollCounter++;
+        // Pass the current scrollX/scrollY values; no actual change in these properties occurred
+        // but some general-purpose code may choose to respond to changes this way.
+        final int scrollX = getScrollX();
+        final int scrollY = getScrollY();
+        onScrollChanged(scrollX, scrollY, scrollX, scrollY);
+
+        // Pass the real deltas to onScrolled, the RecyclerView-specific method.
+        onScrolled(hresult, vresult);
+
+        // Invoke listeners last. Subclassed view methods always handle the event first.
+        // All internal state is consistent by the time listeners are invoked.
+        if (mScrollListener != null) {
+            mScrollListener.onScrolled(this, hresult, vresult);
+        }
+        if (mScrollListeners != null) {
+            for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
+                mScrollListeners.get(i).onScrolled(this, hresult, vresult);
+            }
+        }
+        mDispatchScrollCounter--;
+    }
+
+    /**
+     * Called when the scroll state of this RecyclerView changes. Subclasses should use this
+     * method to respond to state changes instead of an explicit listener.
+     *
+     * <p>This method will always be invoked before listeners, but after the LayoutManager
+     * responds to the scroll state change.</p>
+     *
+     * @param state the new scroll state, one of {@link #SCROLL_STATE_IDLE},
+     *              {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}
+     */
+    public void onScrollStateChanged(int state) {
+        // Do nothing
+    }
+
+    void dispatchOnScrollStateChanged(int state) {
+        // Let the LayoutManager go first; this allows it to bring any properties into
+        // a consistent state before the RecyclerView subclass responds.
+        if (mLayout != null) {
+            mLayout.onScrollStateChanged(state);
+        }
+
+        // Let the RecyclerView subclass handle this event next; any LayoutManager property
+        // changes will be reflected by this time.
+        onScrollStateChanged(state);
+
+        // Listeners go last. All other internal state is consistent by this point.
+        if (mScrollListener != null) {
+            mScrollListener.onScrollStateChanged(this, state);
+        }
+        if (mScrollListeners != null) {
+            for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
+                mScrollListeners.get(i).onScrollStateChanged(this, state);
+            }
+        }
+    }
+
+    /**
+     * Returns whether there are pending adapter updates which are not yet applied to the layout.
+     * <p>
+     * If this method returns <code>true</code>, it means that what user is currently seeing may not
+     * reflect them adapter contents (depending on what has changed).
+     * You may use this information to defer or cancel some operations.
+     * <p>
+     * This method returns true if RecyclerView has not yet calculated the first layout after it is
+     * attached to the Window or the Adapter has been replaced.
+     *
+     * @return True if there are some adapter updates which are not yet reflected to layout or false
+     * if layout is up to date.
+     */
+    public boolean hasPendingAdapterUpdates() {
+        return !mFirstLayoutComplete || mDataSetHasChangedAfterLayout
+                || mAdapterHelper.hasPendingUpdates();
+    }
+
+    class ViewFlinger implements Runnable {
+        private int mLastFlingX;
+        private int mLastFlingY;
+        private OverScroller mScroller;
+        Interpolator mInterpolator = sQuinticInterpolator;
+
+
+        // When set to true, postOnAnimation callbacks are delayed until the run method completes
+        private boolean mEatRunOnAnimationRequest = false;
+
+        // Tracks if postAnimationCallback should be re-attached when it is done
+        private boolean mReSchedulePostAnimationCallback = false;
+
+        ViewFlinger() {
+            mScroller = new OverScroller(getContext(), sQuinticInterpolator);
+        }
+
+        @Override
+        public void run() {
+            if (mLayout == null) {
+                stop();
+                return; // no layout, cannot scroll.
+            }
+            disableRunOnAnimationRequests();
+            consumePendingUpdateOperations();
+            // keep a local reference so that if it is changed during onAnimation method, it won't
+            // cause unexpected behaviors
+            final OverScroller scroller = mScroller;
+            final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
+            if (scroller.computeScrollOffset()) {
+                final int x = scroller.getCurrX();
+                final int y = scroller.getCurrY();
+                final int dx = x - mLastFlingX;
+                final int dy = y - mLastFlingY;
+                int hresult = 0;
+                int vresult = 0;
+                mLastFlingX = x;
+                mLastFlingY = y;
+                int overscrollX = 0, overscrollY = 0;
+                if (mAdapter != null) {
+                    eatRequestLayout();
+                    onEnterLayoutOrScroll();
+                    Trace.beginSection(TRACE_SCROLL_TAG);
+                    if (dx != 0) {
+                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
+                        overscrollX = dx - hresult;
+                    }
+                    if (dy != 0) {
+                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
+                        overscrollY = dy - vresult;
+                    }
+                    Trace.endSection();
+                    repositionShadowingViews();
+
+                    onExitLayoutOrScroll();
+                    resumeRequestLayout(false);
+
+                    if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
+                            && smoothScroller.isRunning()) {
+                        final int adapterSize = mState.getItemCount();
+                        if (adapterSize == 0) {
+                            smoothScroller.stop();
+                        } else if (smoothScroller.getTargetPosition() >= adapterSize) {
+                            smoothScroller.setTargetPosition(adapterSize - 1);
+                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+                        } else {
+                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+                        }
+                    }
+                }
+                if (!mItemDecorations.isEmpty()) {
+                    invalidate();
+                }
+                if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+                    considerReleasingGlowsOnScroll(dx, dy);
+                }
+                if (overscrollX != 0 || overscrollY != 0) {
+                    final int vel = (int) scroller.getCurrVelocity();
+
+                    int velX = 0;
+                    if (overscrollX != x) {
+                        velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
+                    }
+
+                    int velY = 0;
+                    if (overscrollY != y) {
+                        velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
+                    }
+
+                    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+                        absorbGlows(velX, velY);
+                    }
+                    if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
+                            && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
+                        scroller.abortAnimation();
+                    }
+                }
+                if (hresult != 0 || vresult != 0) {
+                    dispatchOnScrolled(hresult, vresult);
+                }
+
+                if (!awakenScrollBars()) {
+                    invalidate();
+                }
+
+                final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
+                        && vresult == dy;
+                final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
+                        && hresult == dx;
+                final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
+                        || fullyConsumedVertical;
+
+                if (scroller.isFinished() || !fullyConsumedAny) {
+                    setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
+                    if (ALLOW_THREAD_GAP_WORK) {
+                        mPrefetchRegistry.clearPrefetchPositions();
+                    }
+                } else {
+                    postOnAnimation();
+                    if (mGapWorker != null) {
+                        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
+                    }
+                }
+            }
+            // call this after the onAnimation is complete not to have inconsistent callbacks etc.
+            if (smoothScroller != null) {
+                if (smoothScroller.isPendingInitialRun()) {
+                    smoothScroller.onAnimation(0, 0);
+                }
+                if (!mReSchedulePostAnimationCallback) {
+                    smoothScroller.stop(); //stop if it does not trigger any scroll
+                }
+            }
+            enableRunOnAnimationRequests();
+        }
+
+        private void disableRunOnAnimationRequests() {
+            mReSchedulePostAnimationCallback = false;
+            mEatRunOnAnimationRequest = true;
+        }
+
+        private void enableRunOnAnimationRequests() {
+            mEatRunOnAnimationRequest = false;
+            if (mReSchedulePostAnimationCallback) {
+                postOnAnimation();
+            }
+        }
+
+        void postOnAnimation() {
+            if (mEatRunOnAnimationRequest) {
+                mReSchedulePostAnimationCallback = true;
+            } else {
+                removeCallbacks(this);
+                RecyclerView.this.postOnAnimation(this);
+            }
+        }
+
+        public void fling(int velocityX, int velocityY) {
+            setScrollState(SCROLL_STATE_SETTLING);
+            mLastFlingX = mLastFlingY = 0;
+            mScroller.fling(0, 0, velocityX, velocityY,
+                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+            postOnAnimation();
+        }
+
+        public void smoothScrollBy(int dx, int dy) {
+            smoothScrollBy(dx, dy, 0, 0);
+        }
+
+        public void smoothScrollBy(int dx, int dy, int vx, int vy) {
+            smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, vx, vy));
+        }
+
+        private float distanceInfluenceForSnapDuration(float f) {
+            f -= 0.5f; // center the values about 0.
+            f *= 0.3f * Math.PI / 2.0f;
+            return (float) Math.sin(f);
+        }
+
+        private int computeScrollDuration(int dx, int dy, int vx, int vy) {
+            final int absDx = Math.abs(dx);
+            final int absDy = Math.abs(dy);
+            final boolean horizontal = absDx > absDy;
+            final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
+            final int delta = (int) Math.sqrt(dx * dx + dy * dy);
+            final int containerSize = horizontal ? getWidth() : getHeight();
+            final int halfContainerSize = containerSize / 2;
+            final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
+            final float distance = halfContainerSize + halfContainerSize
+                    * distanceInfluenceForSnapDuration(distanceRatio);
+
+            final int duration;
+            if (velocity > 0) {
+                duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+            } else {
+                float absDelta = (float) (horizontal ? absDx : absDy);
+                duration = (int) (((absDelta / containerSize) + 1) * 300);
+            }
+            return Math.min(duration, MAX_SCROLL_DURATION);
+        }
+
+        public void smoothScrollBy(int dx, int dy, int duration) {
+            smoothScrollBy(dx, dy, duration, sQuinticInterpolator);
+        }
+
+        public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
+            smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, 0, 0),
+                    interpolator == null ? sQuinticInterpolator : interpolator);
+        }
+
+        public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
+            if (mInterpolator != interpolator) {
+                mInterpolator = interpolator;
+                mScroller = new OverScroller(getContext(), interpolator);
+            }
+            setScrollState(SCROLL_STATE_SETTLING);
+            mLastFlingX = mLastFlingY = 0;
+            mScroller.startScroll(0, 0, dx, dy, duration);
+            postOnAnimation();
+        }
+
+        public void stop() {
+            removeCallbacks(this);
+            mScroller.abortAnimation();
+        }
+
+    }
+
+    void repositionShadowingViews() {
+        // Fix up shadow views used by change animations
+        int count = mChildHelper.getChildCount();
+        for (int i = 0; i < count; i++) {
+            View view = mChildHelper.getChildAt(i);
+            ViewHolder holder = getChildViewHolder(view);
+            if (holder != null && holder.mShadowingHolder != null) {
+                View shadowingView = holder.mShadowingHolder.itemView;
+                int left = view.getLeft();
+                int top = view.getTop();
+                if (left != shadowingView.getLeft() ||  top != shadowingView.getTop()) {
+                    shadowingView.layout(left, top,
+                            left + shadowingView.getWidth(),
+                            top + shadowingView.getHeight());
+                }
+            }
+        }
+    }
+
+    private class RecyclerViewDataObserver extends AdapterDataObserver {
+        RecyclerViewDataObserver() {
+        }
+
+        @Override
+        public void onChanged() {
+            assertNotInLayoutOrScroll(null);
+            mState.mStructureChanged = true;
+
+            setDataSetChangedAfterLayout();
+            if (!mAdapterHelper.hasPendingUpdates()) {
+                requestLayout();
+            }
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+            assertNotInLayoutOrScroll(null);
+            if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
+                triggerUpdateProcessor();
+            }
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            assertNotInLayoutOrScroll(null);
+            if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
+                triggerUpdateProcessor();
+            }
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            assertNotInLayoutOrScroll(null);
+            if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
+                triggerUpdateProcessor();
+            }
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            assertNotInLayoutOrScroll(null);
+            if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
+                triggerUpdateProcessor();
+            }
+        }
+
+        void triggerUpdateProcessor() {
+            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
+                RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
+            } else {
+                mAdapterUpdateDuringMeasure = true;
+                requestLayout();
+            }
+        }
+    }
+
+    /**
+     * RecycledViewPool lets you share Views between multiple RecyclerViews.
+     * <p>
+     * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
+     * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
+     * <p>
+     * RecyclerView automatically creates a pool for itself if you don't provide one.
+     *
+     */
+    public static class RecycledViewPool {
+        private static final int DEFAULT_MAX_SCRAP = 5;
+
+        /**
+         * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
+         *
+         * Note that this tracks running averages of create/bind time across all RecyclerViews
+         * (and, indirectly, Adapters) that use this pool.
+         *
+         * 1) This enables us to track average create and bind times across multiple adapters. Even
+         * though create (and especially bind) may behave differently for different Adapter
+         * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
+         *
+         * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
+         * false for all other views of its type for the same deadline. This prevents items
+         * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
+         */
+        static class ScrapData {
+            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
+            int mMaxScrap = DEFAULT_MAX_SCRAP;
+            long mCreateRunningAverageNs = 0;
+            long mBindRunningAverageNs = 0;
+        }
+        SparseArray<ScrapData> mScrap = new SparseArray<>();
+
+        private int mAttachCount = 0;
+
+        public void clear() {
+            for (int i = 0; i < mScrap.size(); i++) {
+                ScrapData data = mScrap.valueAt(i);
+                data.mScrapHeap.clear();
+            }
+        }
+
+        public void setMaxRecycledViews(int viewType, int max) {
+            ScrapData scrapData = getScrapDataForType(viewType);
+            scrapData.mMaxScrap = max;
+            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
+            if (scrapHeap != null) {
+                while (scrapHeap.size() > max) {
+                    scrapHeap.remove(scrapHeap.size() - 1);
+                }
+            }
+        }
+
+        /**
+         * Returns the current number of Views held by the RecycledViewPool of the given view type.
+         */
+        public int getRecycledViewCount(int viewType) {
+            return getScrapDataForType(viewType).mScrapHeap.size();
+        }
+
+        public ViewHolder getRecycledView(int viewType) {
+            final ScrapData scrapData = mScrap.get(viewType);
+            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
+                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
+                return scrapHeap.remove(scrapHeap.size() - 1);
+            }
+            return null;
+        }
+
+        int size() {
+            int count = 0;
+            for (int i = 0; i < mScrap.size(); i++) {
+                ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap;
+                if (viewHolders != null) {
+                    count += viewHolders.size();
+                }
+            }
+            return count;
+        }
+
+        public void putRecycledView(ViewHolder scrap) {
+            final int viewType = scrap.getItemViewType();
+            final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
+            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
+                return;
+            }
+            if (DEBUG && scrapHeap.contains(scrap)) {
+                throw new IllegalArgumentException("this scrap item already exists");
+            }
+            scrap.resetInternal();
+            scrapHeap.add(scrap);
+        }
+
+        long runningAverage(long oldAverage, long newValue) {
+            if (oldAverage == 0) {
+                return newValue;
+            }
+            return (oldAverage / 4 * 3) + (newValue / 4);
+        }
+
+        void factorInCreateTime(int viewType, long createTimeNs) {
+            ScrapData scrapData = getScrapDataForType(viewType);
+            scrapData.mCreateRunningAverageNs = runningAverage(
+                    scrapData.mCreateRunningAverageNs, createTimeNs);
+        }
+
+        void factorInBindTime(int viewType, long bindTimeNs) {
+            ScrapData scrapData = getScrapDataForType(viewType);
+            scrapData.mBindRunningAverageNs = runningAverage(
+                    scrapData.mBindRunningAverageNs, bindTimeNs);
+        }
+
+        boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
+            long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
+            return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
+        }
+
+        boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) {
+            long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs;
+            return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
+        }
+
+        void attach(Adapter adapter) {
+            mAttachCount++;
+        }
+
+        void detach() {
+            mAttachCount--;
+        }
+
+
+        /**
+         * Detaches the old adapter and attaches the new one.
+         * <p>
+         * RecycledViewPool will clear its cache if it has only one adapter attached and the new
+         * adapter uses a different ViewHolder than the oldAdapter.
+         *
+         * @param oldAdapter The previous adapter instance. Will be detached.
+         * @param newAdapter The new adapter instance. Will be attached.
+         * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
+         *                               ViewHolder and view types.
+         */
+        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
+                boolean compatibleWithPrevious) {
+            if (oldAdapter != null) {
+                detach();
+            }
+            if (!compatibleWithPrevious && mAttachCount == 0) {
+                clear();
+            }
+            if (newAdapter != null) {
+                attach(newAdapter);
+            }
+        }
+
+        private ScrapData getScrapDataForType(int viewType) {
+            ScrapData scrapData = mScrap.get(viewType);
+            if (scrapData == null) {
+                scrapData = new ScrapData();
+                mScrap.put(viewType, scrapData);
+            }
+            return scrapData;
+        }
+    }
+
+    /**
+     * Utility method for finding an internal RecyclerView, if present
+     */
+    @Nullable
+    static RecyclerView findNestedRecyclerView(@NonNull View view) {
+        if (!(view instanceof ViewGroup)) {
+            return null;
+        }
+        if (view instanceof RecyclerView) {
+            return (RecyclerView) view;
+        }
+        final ViewGroup parent = (ViewGroup) view;
+        final int count = parent.getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = parent.getChildAt(i);
+            final RecyclerView descendant = findNestedRecyclerView(child);
+            if (descendant != null) {
+                return descendant;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Utility method for clearing holder's internal RecyclerView, if present
+     */
+    static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) {
+        if (holder.mNestedRecyclerView != null) {
+            View item = holder.mNestedRecyclerView.get();
+            while (item != null) {
+                if (item == holder.itemView) {
+                    return; // match found, don't need to clear
+                }
+
+                ViewParent parent = item.getParent();
+                if (parent instanceof View) {
+                    item = (View) parent;
+                } else {
+                    item = null;
+                }
+            }
+            holder.mNestedRecyclerView = null; // not nested
+        }
+    }
+
+    /**
+     * Time base for deadline-aware work scheduling. Overridable for testing.
+     *
+     * Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling
+     * isn't relevant.
+     */
+    long getNanoTime() {
+        if (ALLOW_THREAD_GAP_WORK) {
+            return System.nanoTime();
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * A Recycler is responsible for managing scrapped or detached item views for reuse.
+     *
+     * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
+     * that has been marked for removal or reuse.</p>
+     *
+     * <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
+     * an adapter's data set representing the data at a given position or item ID.
+     * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
+     * If not, the view can be quickly reused by the LayoutManager with no further work.
+     * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
+     * may be repositioned by a LayoutManager without remeasurement.</p>
+     */
+    public final class Recycler {
+        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
+        ArrayList<ViewHolder> mChangedScrap = null;
+
+        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
+
+        private final List<ViewHolder>
+                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
+
+        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
+        int mViewCacheMax = DEFAULT_CACHE_SIZE;
+
+        RecycledViewPool mRecyclerPool;
+
+        private ViewCacheExtension mViewCacheExtension;
+
+        static final int DEFAULT_CACHE_SIZE = 2;
+
+        /**
+         * Clear scrap views out of this recycler. Detached views contained within a
+         * recycled view pool will remain.
+         */
+        public void clear() {
+            mAttachedScrap.clear();
+            recycleAndClearCachedViews();
+        }
+
+        /**
+         * Set the maximum number of detached, valid views we should retain for later use.
+         *
+         * @param viewCount Number of views to keep before sending views to the shared pool
+         */
+        public void setViewCacheSize(int viewCount) {
+            mRequestedCacheMax = viewCount;
+            updateViewCacheSize();
+        }
+
+        void updateViewCacheSize() {
+            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
+            mViewCacheMax = mRequestedCacheMax + extraCache;
+
+            // first, try the views that can be recycled
+            for (int i = mCachedViews.size() - 1;
+                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
+                recycleCachedViewAt(i);
+            }
+        }
+
+        /**
+         * Returns an unmodifiable list of ViewHolders that are currently in the scrap list.
+         *
+         * @return List of ViewHolders in the scrap list.
+         */
+        public List<ViewHolder> getScrapList() {
+            return mUnmodifiableAttachedScrap;
+        }
+
+        /**
+         * Helper method for getViewForPosition.
+         * <p>
+         * Checks whether a given view holder can be used for the provided position.
+         *
+         * @param holder ViewHolder
+         * @return true if ViewHolder matches the provided position, false otherwise
+         */
+        boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
+            // if it is a removed holder, nothing to verify since we cannot ask adapter anymore
+            // if it is not removed, verify the type and id.
+            if (holder.isRemoved()) {
+                if (DEBUG && !mState.isPreLayout()) {
+                    throw new IllegalStateException("should not receive a removed view unless it"
+                            + " is pre layout");
+                }
+                return mState.isPreLayout();
+            }
+            if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
+                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+                        + "adapter position" + holder);
+            }
+            if (!mState.isPreLayout()) {
+                // don't check type if it is pre-layout.
+                final int type = mAdapter.getItemViewType(holder.mPosition);
+                if (type != holder.getItemViewType()) {
+                    return false;
+                }
+            }
+            if (mAdapter.hasStableIds()) {
+                return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
+            }
+            return true;
+        }
+
+        /**
+         * Attempts to bind view, and account for relevant timing information. If
+         * deadlineNs != FOREVER_NS, this method may fail to bind, and return false.
+         *
+         * @param holder Holder to be bound.
+         * @param offsetPosition Position of item to be bound.
+         * @param position Pre-layout position of item to be bound.
+         * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
+         *                   complete. If FOREVER_NS is passed, this method will not fail to
+         *                   bind the holder.
+         * @return
+         */
+        private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
+                int position, long deadlineNs) {
+            holder.mOwnerRecyclerView = RecyclerView.this;
+            final int viewType = holder.getItemViewType();
+            long startBindNs = getNanoTime();
+            if (deadlineNs != FOREVER_NS
+                    && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
+                // abort - we have a deadline we can't meet
+                return false;
+            }
+            mAdapter.bindViewHolder(holder, offsetPosition);
+            long endBindNs = getNanoTime();
+            mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
+            attachAccessibilityDelegate(holder.itemView);
+            if (mState.isPreLayout()) {
+                holder.mPreLayoutPosition = position;
+            }
+            return true;
+        }
+
+        /**
+         * Binds the given View to the position. The View can be a View previously retrieved via
+         * {@link #getViewForPosition(int)} or created by
+         * {@link Adapter#onCreateViewHolder(ViewGroup, int)}.
+         * <p>
+         * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)}
+         * and let the RecyclerView handle caching. This is a helper method for LayoutManager who
+         * wants to handle its own recycling logic.
+         * <p>
+         * Note that, {@link #getViewForPosition(int)} already binds the View to the position so
+         * you don't need to call this method unless you want to bind this View to another position.
+         *
+         * @param view The view to update.
+         * @param position The position of the item to bind to this View.
+         */
+        public void bindViewToPosition(View view, int position) {
+            ViewHolder holder = getChildViewHolderInt(view);
+            if (holder == null) {
+                throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot"
+                        + " pass arbitrary views to this method, they should be created by the "
+                        + "Adapter");
+            }
+            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+            if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
+                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+                        + "position " + position + "(offset:" + offsetPosition + ")."
+                        + "state:" + mState.getItemCount());
+            }
+            tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS);
+
+            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
+            final LayoutParams rvLayoutParams;
+            if (lp == null) {
+                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
+                holder.itemView.setLayoutParams(rvLayoutParams);
+            } else if (!checkLayoutParams(lp)) {
+                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
+                holder.itemView.setLayoutParams(rvLayoutParams);
+            } else {
+                rvLayoutParams = (LayoutParams) lp;
+            }
+
+            rvLayoutParams.mInsetsDirty = true;
+            rvLayoutParams.mViewHolder = holder;
+            rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null;
+        }
+
+        /**
+         * RecyclerView provides artificial position range (item count) in pre-layout state and
+         * automatically maps these positions to {@link Adapter} positions when
+         * {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called.
+         * <p>
+         * Usually, LayoutManager does not need to worry about this. However, in some cases, your
+         * LayoutManager may need to call some custom component with item positions in which
+         * case you need the actual adapter position instead of the pre layout position. You
+         * can use this method to convert a pre-layout position to adapter (post layout) position.
+         * <p>
+         * Note that if the provided position belongs to a deleted ViewHolder, this method will
+         * return -1.
+         * <p>
+         * Calling this method in post-layout state returns the same value back.
+         *
+         * @param position The pre-layout position to convert. Must be greater or equal to 0 and
+         *                 less than {@link State#getItemCount()}.
+         */
+        public int convertPreLayoutPositionToPostLayout(int position) {
+            if (position < 0 || position >= mState.getItemCount()) {
+                throw new IndexOutOfBoundsException("invalid position " + position + ". State "
+                        + "item count is " + mState.getItemCount());
+            }
+            if (!mState.isPreLayout()) {
+                return position;
+            }
+            return mAdapterHelper.findPositionOffset(position);
+        }
+
+        /**
+         * Obtain a view initialized for the given position.
+         *
+         * This method should be used by {@link LayoutManager} implementations to obtain
+         * views to represent data from an {@link Adapter}.
+         * <p>
+         * The Recycler may reuse a scrap or detached view from a shared pool if one is
+         * available for the correct view type. If the adapter has not indicated that the
+         * data at the given position has changed, the Recycler will attempt to hand back
+         * a scrap view that was previously initialized for that data without rebinding.
+         *
+         * @param position Position to obtain a view for
+         * @return A view representing the data at <code>position</code> from <code>adapter</code>
+         */
+        public View getViewForPosition(int position) {
+            return getViewForPosition(position, false);
+        }
+
+        View getViewForPosition(int position, boolean dryRun) {
+            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
+        }
+
+        /**
+         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
+         * cache, the RecycledViewPool, or creating it directly.
+         * <p>
+         * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return
+         * rather than constructing or binding a ViewHolder if it doesn't think it has time.
+         * If a ViewHolder must be constructed and not enough time remains, null is returned. If a
+         * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is
+         * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this.
+         *
+         * @param position Position of ViewHolder to be returned.
+         * @param dryRun True if the ViewHolder should not be removed from scrap/cache/
+         * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
+         *                   complete. If FOREVER_NS is passed, this method will not fail to
+         *                   create/bind the holder if needed.
+         *
+         * @return ViewHolder for requested position
+         */
+        @Nullable
+        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
+                boolean dryRun, long deadlineNs) {
+            if (position < 0 || position >= mState.getItemCount()) {
+                throw new IndexOutOfBoundsException("Invalid item position " + position
+                        + "(" + position + "). Item count:" + mState.getItemCount());
+            }
+            boolean fromScrapOrHiddenOrCache = false;
+            ViewHolder holder = null;
+            // 0) If there is a changed scrap, try to find from there
+            if (mState.isPreLayout()) {
+                holder = getChangedScrapViewForPosition(position);
+                fromScrapOrHiddenOrCache = holder != null;
+            }
+            // 1) Find by position from scrap/hidden list/cache
+            if (holder == null) {
+                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
+                if (holder != null) {
+                    if (!validateViewHolderForOffsetPosition(holder)) {
+                        // recycle holder (and unscrap if relevant) since it can't be used
+                        if (!dryRun) {
+                            // we would like to recycle this but need to make sure it is not used by
+                            // animation logic etc.
+                            holder.addFlags(ViewHolder.FLAG_INVALID);
+                            if (holder.isScrap()) {
+                                removeDetachedView(holder.itemView, false);
+                                holder.unScrap();
+                            } else if (holder.wasReturnedFromScrap()) {
+                                holder.clearReturnedFromScrapFlag();
+                            }
+                            recycleViewHolderInternal(holder);
+                        }
+                        holder = null;
+                    } else {
+                        fromScrapOrHiddenOrCache = true;
+                    }
+                }
+            }
+            if (holder == null) {
+                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
+                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+                            + "position " + position + "(offset:" + offsetPosition + ")."
+                            + "state:" + mState.getItemCount());
+                }
+
+                final int type = mAdapter.getItemViewType(offsetPosition);
+                // 2) Find from scrap/cache via stable ids, if exists
+                if (mAdapter.hasStableIds()) {
+                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
+                            type, dryRun);
+                    if (holder != null) {
+                        // update position
+                        holder.mPosition = offsetPosition;
+                        fromScrapOrHiddenOrCache = true;
+                    }
+                }
+                if (holder == null && mViewCacheExtension != null) {
+                    // We are NOT sending the offsetPosition because LayoutManager does not
+                    // know it.
+                    final View view = mViewCacheExtension
+                            .getViewForPositionAndType(this, position, type);
+                    if (view != null) {
+                        holder = getChildViewHolder(view);
+                        if (holder == null) {
+                            throw new IllegalArgumentException("getViewForPositionAndType returned"
+                                    + " a view which does not have a ViewHolder");
+                        } else if (holder.shouldIgnore()) {
+                            throw new IllegalArgumentException("getViewForPositionAndType returned"
+                                    + " a view that is ignored. You must call stopIgnoring before"
+                                    + " returning this view.");
+                        }
+                    }
+                }
+                if (holder == null) { // fallback to pool
+                    if (DEBUG) {
+                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+                                + position + ") fetching from shared pool");
+                    }
+                    holder = getRecycledViewPool().getRecycledView(type);
+                    if (holder != null) {
+                        holder.resetInternal();
+                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
+                            invalidateDisplayListInt(holder);
+                        }
+                    }
+                }
+                if (holder == null) {
+                    long start = getNanoTime();
+                    if (deadlineNs != FOREVER_NS
+                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
+                        // abort - we have a deadline we can't meet
+                        return null;
+                    }
+                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
+                    if (ALLOW_THREAD_GAP_WORK) {
+                        // only bother finding nested RV if prefetching
+                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
+                        if (innerView != null) {
+                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
+                        }
+                    }
+
+                    long end = getNanoTime();
+                    mRecyclerPool.factorInCreateTime(type, end - start);
+                    if (DEBUG) {
+                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
+                    }
+                }
+            }
+
+            // This is very ugly but the only place we can grab this information
+            // before the View is rebound and returned to the LayoutManager for post layout ops.
+            // We don't need this in pre-layout since the VH is not updated by the LM.
+            if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
+                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
+                holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                if (mState.mRunSimpleAnimations) {
+                    int changeFlags = ItemAnimator
+                            .buildAdapterChangeFlagsForAnimations(holder);
+                    changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
+                            holder, changeFlags, holder.getUnmodifiedPayloads());
+                    recordAnimationInfoIfBouncedHiddenView(holder, info);
+                }
+            }
+
+            boolean bound = false;
+            if (mState.isPreLayout() && holder.isBound()) {
+                // do not update unless we absolutely have to.
+                holder.mPreLayoutPosition = position;
+            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
+                if (DEBUG && holder.isRemoved()) {
+                    throw new IllegalStateException("Removed holder should be bound and it should"
+                            + " come here only in pre-layout. Holder: " + holder);
+                }
+                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
+            }
+
+            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
+            final LayoutParams rvLayoutParams;
+            if (lp == null) {
+                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
+                holder.itemView.setLayoutParams(rvLayoutParams);
+            } else if (!checkLayoutParams(lp)) {
+                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
+                holder.itemView.setLayoutParams(rvLayoutParams);
+            } else {
+                rvLayoutParams = (LayoutParams) lp;
+            }
+            rvLayoutParams.mViewHolder = holder;
+            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
+            return holder;
+        }
+
+        private void attachAccessibilityDelegate(View itemView) {
+            if (isAccessibilityEnabled()) {
+                if (itemView.getImportantForAccessibility()
+                        == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+                    itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+                }
+
+                if (itemView.getAccessibilityDelegate() == null) {
+                    itemView.setAccessibilityDelegate(mAccessibilityDelegate.getItemDelegate());
+                }
+            }
+        }
+
+        private void invalidateDisplayListInt(ViewHolder holder) {
+            if (holder.itemView instanceof ViewGroup) {
+                invalidateDisplayListInt((ViewGroup) holder.itemView, false);
+            }
+        }
+
+        private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) {
+            for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
+                final View view = viewGroup.getChildAt(i);
+                if (view instanceof ViewGroup) {
+                    invalidateDisplayListInt((ViewGroup) view, true);
+                }
+            }
+            if (!invalidateThis) {
+                return;
+            }
+            // we need to force it to become invisible
+            if (viewGroup.getVisibility() == View.INVISIBLE) {
+                viewGroup.setVisibility(View.VISIBLE);
+                viewGroup.setVisibility(View.INVISIBLE);
+            } else {
+                final int visibility = viewGroup.getVisibility();
+                viewGroup.setVisibility(View.INVISIBLE);
+                viewGroup.setVisibility(visibility);
+            }
+        }
+
+        /**
+         * Recycle a detached view. The specified view will be added to a pool of views
+         * for later rebinding and reuse.
+         *
+         * <p>A view must be fully detached (removed from parent) before it may be recycled. If the
+         * View is scrapped, it will be removed from scrap list.</p>
+         *
+         * @param view Removed view for recycling
+         * @see LayoutManager#removeAndRecycleView(View, Recycler)
+         */
+        public void recycleView(View view) {
+            // This public recycle method tries to make view recycle-able since layout manager
+            // intended to recycle this view (e.g. even if it is in scrap or change cache)
+            ViewHolder holder = getChildViewHolderInt(view);
+            if (holder.isTmpDetached()) {
+                removeDetachedView(view, false);
+            }
+            if (holder.isScrap()) {
+                holder.unScrap();
+            } else if (holder.wasReturnedFromScrap()) {
+                holder.clearReturnedFromScrapFlag();
+            }
+            recycleViewHolderInternal(holder);
+        }
+
+        /**
+         * Internally, use this method instead of {@link #recycleView(android.view.View)} to
+         * catch potential bugs.
+         * @param view
+         */
+        void recycleViewInternal(View view) {
+            recycleViewHolderInternal(getChildViewHolderInt(view));
+        }
+
+        void recycleAndClearCachedViews() {
+            final int count = mCachedViews.size();
+            for (int i = count - 1; i >= 0; i--) {
+                recycleCachedViewAt(i);
+            }
+            mCachedViews.clear();
+            if (ALLOW_THREAD_GAP_WORK) {
+                mPrefetchRegistry.clearPrefetchPositions();
+            }
+        }
+
+        /**
+         * Recycles a cached view and removes the view from the list. Views are added to cache
+         * if and only if they are recyclable, so this method does not check it again.
+         * <p>
+         * A small exception to this rule is when the view does not have an animator reference
+         * but transient state is true (due to animations created outside ItemAnimator). In that
+         * case, adapter may choose to recycle it. From RecyclerView's perspective, the view is
+         * still recyclable since Adapter wants to do so.
+         *
+         * @param cachedViewIndex The index of the view in cached views list
+         */
+        void recycleCachedViewAt(int cachedViewIndex) {
+            if (DEBUG) {
+                Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
+            }
+            ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
+            if (DEBUG) {
+                Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
+            }
+            addViewHolderToRecycledViewPool(viewHolder, true);
+            mCachedViews.remove(cachedViewIndex);
+        }
+
+        /**
+         * internal implementation checks if view is scrapped or attached and throws an exception
+         * if so.
+         * Public version un-scraps before calling recycle.
+         */
+        void recycleViewHolderInternal(ViewHolder holder) {
+            if (holder.isScrap() || holder.itemView.getParent() != null) {
+                throw new IllegalArgumentException(
+                        "Scrapped or attached views may not be recycled. isScrap:"
+                                + holder.isScrap() + " isAttached:"
+                                + (holder.itemView.getParent() != null));
+            }
+
+            if (holder.isTmpDetached()) {
+                throw new IllegalArgumentException("Tmp detached view should be removed "
+                        + "from RecyclerView before it can be recycled: " + holder);
+            }
+
+            if (holder.shouldIgnore()) {
+                throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+                        + " should first call stopIgnoringView(view) before calling recycle.");
+            }
+            //noinspection unchecked
+            final boolean transientStatePreventsRecycling = holder
+                    .doesTransientStatePreventRecycling();
+            final boolean forceRecycle = mAdapter != null
+                    && transientStatePreventsRecycling
+                    && mAdapter.onFailedToRecycleView(holder);
+            boolean cached = false;
+            boolean recycled = false;
+            if (DEBUG && mCachedViews.contains(holder)) {
+                throw new IllegalArgumentException("cached view received recycle internal? "
+                        + holder);
+            }
+            if (forceRecycle || holder.isRecyclable()) {
+                if (mViewCacheMax > 0
+                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
+                                | ViewHolder.FLAG_REMOVED
+                                | ViewHolder.FLAG_UPDATE
+                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
+                    // Retire oldest cached view
+                    int cachedViewSize = mCachedViews.size();
+                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
+                        recycleCachedViewAt(0);
+                        cachedViewSize--;
+                    }
+
+                    int targetCacheIndex = cachedViewSize;
+                    if (ALLOW_THREAD_GAP_WORK
+                            && cachedViewSize > 0
+                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
+                        // when adding the view, skip past most recently prefetched views
+                        int cacheIndex = cachedViewSize - 1;
+                        while (cacheIndex >= 0) {
+                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
+                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
+                                break;
+                            }
+                            cacheIndex--;
+                        }
+                        targetCacheIndex = cacheIndex + 1;
+                    }
+                    mCachedViews.add(targetCacheIndex, holder);
+                    cached = true;
+                }
+                if (!cached) {
+                    addViewHolderToRecycledViewPool(holder, true);
+                    recycled = true;
+                }
+            } else {
+                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
+                // runs. In this case, the item is eventually recycled by
+                // ItemAnimatorRestoreListener#onAnimationFinished.
+
+                // TODO: consider cancelling an animation when an item is removed scrollBy,
+                // to return it to the pool faster
+                if (DEBUG) {
+                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+                            + "re-visit here. We are still removing it from animation lists");
+                }
+            }
+            // even if the holder is not removed, we still call this method so that it is removed
+            // from view holder lists.
+            mViewInfoStore.removeViewHolder(holder);
+            if (!cached && !recycled && transientStatePreventsRecycling) {
+                holder.mOwnerRecyclerView = null;
+            }
+        }
+
+        /**
+         * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool.
+         *
+         * Pass false to dispatchRecycled for views that have not been bound.
+         *
+         * @param holder Holder to be added to the pool.
+         * @param dispatchRecycled True to dispatch View recycled callbacks.
+         */
+        void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
+            clearNestedRecyclerViewIfNotNested(holder);
+            holder.itemView.setAccessibilityDelegate(null);
+            if (dispatchRecycled) {
+                dispatchViewRecycled(holder);
+            }
+            holder.mOwnerRecyclerView = null;
+            getRecycledViewPool().putRecycledView(holder);
+        }
+
+        /**
+         * Used as a fast path for unscrapping and recycling a view during a bulk operation.
+         * The caller must call {@link #clearScrap()} when it's done to update the recycler's
+         * internal bookkeeping.
+         */
+        void quickRecycleScrapView(View view) {
+            final ViewHolder holder = getChildViewHolderInt(view);
+            holder.mScrapContainer = null;
+            holder.mInChangeScrap = false;
+            holder.clearReturnedFromScrapFlag();
+            recycleViewHolderInternal(holder);
+        }
+
+        /**
+         * Mark an attached view as scrap.
+         *
+         * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
+         * for rebinding and reuse. Requests for a view for a given position may return a
+         * reused or rebound scrap view instance.</p>
+         *
+         * @param view View to scrap
+         */
+        void scrapView(View view) {
+            final ViewHolder holder = getChildViewHolderInt(view);
+            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
+                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
+                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
+                    throw new IllegalArgumentException("Called scrap view with an invalid view."
+                            + " Invalid views cannot be reused from scrap, they should rebound from"
+                            + " recycler pool.");
+                }
+                holder.setScrapContainer(this, false);
+                mAttachedScrap.add(holder);
+            } else {
+                if (mChangedScrap == null) {
+                    mChangedScrap = new ArrayList<ViewHolder>();
+                }
+                holder.setScrapContainer(this, true);
+                mChangedScrap.add(holder);
+            }
+        }
+
+        /**
+         * Remove a previously scrapped view from the pool of eligible scrap.
+         *
+         * <p>This view will no longer be eligible for reuse until re-scrapped or
+         * until it is explicitly removed and recycled.</p>
+         */
+        void unscrapView(ViewHolder holder) {
+            if (holder.mInChangeScrap) {
+                mChangedScrap.remove(holder);
+            } else {
+                mAttachedScrap.remove(holder);
+            }
+            holder.mScrapContainer = null;
+            holder.mInChangeScrap = false;
+            holder.clearReturnedFromScrapFlag();
+        }
+
+        int getScrapCount() {
+            return mAttachedScrap.size();
+        }
+
+        View getScrapViewAt(int index) {
+            return mAttachedScrap.get(index).itemView;
+        }
+
+        void clearScrap() {
+            mAttachedScrap.clear();
+            if (mChangedScrap != null) {
+                mChangedScrap.clear();
+            }
+        }
+
+        ViewHolder getChangedScrapViewForPosition(int position) {
+            // If pre-layout, check the changed scrap for an exact match.
+            final int changedScrapSize;
+            if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
+                return null;
+            }
+            // find by position
+            for (int i = 0; i < changedScrapSize; i++) {
+                final ViewHolder holder = mChangedScrap.get(i);
+                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
+                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+                    return holder;
+                }
+            }
+            // find by id
+            if (mAdapter.hasStableIds()) {
+                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
+                    final long id = mAdapter.getItemId(offsetPosition);
+                    for (int i = 0; i < changedScrapSize; i++) {
+                        final ViewHolder holder = mChangedScrap.get(i);
+                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
+                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+                            return holder;
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Returns a view for the position either from attach scrap, hidden children, or cache.
+         *
+         * @param position Item position
+         * @param dryRun  Does a dry run, finds the ViewHolder but does not remove
+         * @return a ViewHolder that can be re-used for this position.
+         */
+        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
+            final int scrapCount = mAttachedScrap.size();
+
+            // Try first for an exact, non-invalid match from scrap.
+            for (int i = 0; i < scrapCount; i++) {
+                final ViewHolder holder = mAttachedScrap.get(i);
+                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
+                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
+                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+                    return holder;
+                }
+            }
+
+            if (!dryRun) {
+                View view = mChildHelper.findHiddenNonRemovedView(position);
+                if (view != null) {
+                    // This View is good to be used. We just need to unhide, detach and move to the
+                    // scrap list.
+                    final ViewHolder vh = getChildViewHolderInt(view);
+                    mChildHelper.unhide(view);
+                    int layoutIndex = mChildHelper.indexOfChild(view);
+                    if (layoutIndex == RecyclerView.NO_POSITION) {
+                        throw new IllegalStateException("layout index should not be -1 after "
+                                + "unhiding a view:" + vh);
+                    }
+                    mChildHelper.detachViewFromParent(layoutIndex);
+                    scrapView(view);
+                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
+                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                    return vh;
+                }
+            }
+
+            // Search in our first-level recycled view cache.
+            final int cacheSize = mCachedViews.size();
+            for (int i = 0; i < cacheSize; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                // invalid view holders may be in cache if adapter has stable ids as they can be
+                // retrieved via getScrapOrCachedViewForId
+                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
+                    if (!dryRun) {
+                        mCachedViews.remove(i);
+                    }
+                    if (DEBUG) {
+                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+                                + ") found match in cache: " + holder);
+                    }
+                    return holder;
+                }
+            }
+            return null;
+        }
+
+        ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
+            // Look in our attached views first
+            final int count = mAttachedScrap.size();
+            for (int i = count - 1; i >= 0; i--) {
+                final ViewHolder holder = mAttachedScrap.get(i);
+                if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
+                    if (type == holder.getItemViewType()) {
+                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+                        if (holder.isRemoved()) {
+                            // this might be valid in two cases:
+                            // > item is removed but we are in pre-layout pass
+                            // >> do nothing. return as is. make sure we don't rebind
+                            // > item is removed then added to another position and we are in
+                            // post layout.
+                            // >> remove removed and invalid flags, add update flag to rebind
+                            // because item was invisible to us and we don't know what happened in
+                            // between.
+                            if (!mState.isPreLayout()) {
+                                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
+                                        | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
+                            }
+                        }
+                        return holder;
+                    } else if (!dryRun) {
+                        // if we are running animations, it is actually better to keep it in scrap
+                        // but this would force layout manager to lay it out which would be bad.
+                        // Recycle this scrap. Type mismatch.
+                        mAttachedScrap.remove(i);
+                        removeDetachedView(holder.itemView, false);
+                        quickRecycleScrapView(holder.itemView);
+                    }
+                }
+            }
+
+            // Search the first-level cache
+            final int cacheSize = mCachedViews.size();
+            for (int i = cacheSize - 1; i >= 0; i--) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder.getItemId() == id) {
+                    if (type == holder.getItemViewType()) {
+                        if (!dryRun) {
+                            mCachedViews.remove(i);
+                        }
+                        return holder;
+                    } else if (!dryRun) {
+                        recycleCachedViewAt(i);
+                        return null;
+                    }
+                }
+            }
+            return null;
+        }
+
+        void dispatchViewRecycled(ViewHolder holder) {
+            if (mRecyclerListener != null) {
+                mRecyclerListener.onViewRecycled(holder);
+            }
+            if (mAdapter != null) {
+                mAdapter.onViewRecycled(holder);
+            }
+            if (mState != null) {
+                mViewInfoStore.removeViewHolder(holder);
+            }
+            if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
+        }
+
+        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
+                boolean compatibleWithPrevious) {
+            clear();
+            getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
+        }
+
+        void offsetPositionRecordsForMove(int from, int to) {
+            final int start, end, inBetweenOffset;
+            if (from < to) {
+                start = from;
+                end = to;
+                inBetweenOffset = -1;
+            } else {
+                start = to;
+                end = from;
+                inBetweenOffset = 1;
+            }
+            final int cachedCount = mCachedViews.size();
+            for (int i = 0; i < cachedCount; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+                    continue;
+                }
+                if (holder.mPosition == from) {
+                    holder.offsetPosition(to - from, false);
+                } else {
+                    holder.offsetPosition(inBetweenOffset, false);
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder "
+                            + holder);
+                }
+            }
+        }
+
+        void offsetPositionRecordsForInsert(int insertedAt, int count) {
+            final int cachedCount = mCachedViews.size();
+            for (int i = 0; i < cachedCount; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder != null && holder.mPosition >= insertedAt) {
+                    if (DEBUG) {
+                        Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder "
+                                + holder + " now at position " + (holder.mPosition + count));
+                    }
+                    holder.offsetPosition(count, true);
+                }
+            }
+        }
+
+        /**
+         * @param removedFrom Remove start index
+         * @param count Remove count
+         * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if
+         *                         false, they'll be applied before the second layout pass
+         */
+        void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) {
+            final int removedEnd = removedFrom + count;
+            final int cachedCount = mCachedViews.size();
+            for (int i = cachedCount - 1; i >= 0; i--) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder != null) {
+                    if (holder.mPosition >= removedEnd) {
+                        if (DEBUG) {
+                            Log.d(TAG, "offsetPositionRecordsForRemove cached " + i
+                                    + " holder " + holder + " now at position "
+                                    + (holder.mPosition - count));
+                        }
+                        holder.offsetPosition(-count, applyToPreLayout);
+                    } else if (holder.mPosition >= removedFrom) {
+                        // Item for this view was removed. Dump it from the cache.
+                        holder.addFlags(ViewHolder.FLAG_REMOVED);
+                        recycleCachedViewAt(i);
+                    }
+                }
+            }
+        }
+
+        void setViewCacheExtension(ViewCacheExtension extension) {
+            mViewCacheExtension = extension;
+        }
+
+        void setRecycledViewPool(RecycledViewPool pool) {
+            if (mRecyclerPool != null) {
+                mRecyclerPool.detach();
+            }
+            mRecyclerPool = pool;
+            if (pool != null) {
+                mRecyclerPool.attach(getAdapter());
+            }
+        }
+
+        RecycledViewPool getRecycledViewPool() {
+            if (mRecyclerPool == null) {
+                mRecyclerPool = new RecycledViewPool();
+            }
+            return mRecyclerPool;
+        }
+
+        void viewRangeUpdate(int positionStart, int itemCount) {
+            final int positionEnd = positionStart + itemCount;
+            final int cachedCount = mCachedViews.size();
+            for (int i = cachedCount - 1; i >= 0; i--) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder == null) {
+                    continue;
+                }
+
+                final int pos = holder.getLayoutPosition();
+                if (pos >= positionStart && pos < positionEnd) {
+                    holder.addFlags(ViewHolder.FLAG_UPDATE);
+                    recycleCachedViewAt(i);
+                    // cached views should not be flagged as changed because this will cause them
+                    // to animate when they are returned from cache.
+                }
+            }
+        }
+
+        void setAdapterPositionsAsUnknown() {
+            final int cachedCount = mCachedViews.size();
+            for (int i = 0; i < cachedCount; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder != null) {
+                    holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
+                }
+            }
+        }
+
+        void markKnownViewsInvalid() {
+            if (mAdapter != null && mAdapter.hasStableIds()) {
+                final int cachedCount = mCachedViews.size();
+                for (int i = 0; i < cachedCount; i++) {
+                    final ViewHolder holder = mCachedViews.get(i);
+                    if (holder != null) {
+                        holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+                        holder.addChangePayload(null);
+                    }
+                }
+            } else {
+                // we cannot re-use cached views in this case. Recycle them all
+                recycleAndClearCachedViews();
+            }
+        }
+
+        void clearOldPositions() {
+            final int cachedCount = mCachedViews.size();
+            for (int i = 0; i < cachedCount; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                holder.clearOldPosition();
+            }
+            final int scrapCount = mAttachedScrap.size();
+            for (int i = 0; i < scrapCount; i++) {
+                mAttachedScrap.get(i).clearOldPosition();
+            }
+            if (mChangedScrap != null) {
+                final int changedScrapCount = mChangedScrap.size();
+                for (int i = 0; i < changedScrapCount; i++) {
+                    mChangedScrap.get(i).clearOldPosition();
+                }
+            }
+        }
+
+        void markItemDecorInsetsDirty() {
+            final int cachedCount = mCachedViews.size();
+            for (int i = 0; i < cachedCount; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
+                if (layoutParams != null) {
+                    layoutParams.mInsetsDirty = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * ViewCacheExtension is a helper class to provide an additional layer of view caching that can
+     * be controlled by the developer.
+     * <p>
+     * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
+     * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
+     * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
+     * {@link RecycledViewPool}.
+     * <p>
+     * Note that, Recycler never sends Views to this method to be cached. It is developers
+     * responsibility to decide whether they want to keep their Views in this custom cache or let
+     * the default recycling policy handle it.
+     */
+    public abstract static class ViewCacheExtension {
+
+        /**
+         * Returns a View that can be binded to the given Adapter position.
+         * <p>
+         * This method should <b>not</b> create a new View. Instead, it is expected to return
+         * an already created View that can be re-used for the given type and position.
+         * If the View is marked as ignored, it should first call
+         * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
+         * <p>
+         * RecyclerView will re-bind the returned View to the position if necessary.
+         *
+         * @param recycler The Recycler that can be used to bind the View
+         * @param position The adapter position
+         * @param type     The type of the View, defined by adapter
+         * @return A View that is bound to the given position or NULL if there is no View to re-use
+         * @see LayoutManager#ignoreView(View)
+         */
+        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
+    }
+
+    /**
+     * Base class for an Adapter
+     *
+     * <p>Adapters provide a binding from an app-specific data set to views that are displayed
+     * within a {@link RecyclerView}.</p>
+     *
+     * @param <VH> A class that extends ViewHolder that will be used by the adapter.
+     */
+    public abstract static class Adapter<VH extends ViewHolder> {
+        private final AdapterDataObservable mObservable = new AdapterDataObservable();
+        private boolean mHasStableIds = false;
+
+        /**
+         * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent
+         * an item.
+         * <p>
+         * This new ViewHolder should be constructed with a new View that can represent the items
+         * of the given type. You can either create a new View manually or inflate it from an XML
+         * layout file.
+         * <p>
+         * The new ViewHolder will be used to display items of the adapter using
+         * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display
+         * different items in the data set, it is a good idea to cache references to sub views of
+         * the View to avoid unnecessary {@link View#findViewById(int)} calls.
+         *
+         * @param parent The ViewGroup into which the new View will be added after it is bound to
+         *               an adapter position.
+         * @param viewType The view type of the new View.
+         *
+         * @return A new ViewHolder that holds a View of the given view type.
+         * @see #getItemViewType(int)
+         * @see #onBindViewHolder(ViewHolder, int)
+         */
+        public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
+
+        /**
+         * Called by RecyclerView to display the data at the specified position. This method should
+         * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given
+         * position.
+         * <p>
+         * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
+         * again if the position of the item changes in the data set unless the item itself is
+         * invalidated or the new position cannot be determined. For this reason, you should only
+         * use the <code>position</code> parameter while acquiring the related data item inside
+         * this method and should not keep a copy of it. If you need the position of an item later
+         * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
+         * have the updated adapter position.
+         *
+         * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can
+         * handle efficient partial bind.
+         *
+         * @param holder The ViewHolder which should be updated to represent the contents of the
+         *        item at the given position in the data set.
+         * @param position The position of the item within the adapter's data set.
+         */
+        public abstract void onBindViewHolder(VH holder, int position);
+
+        /**
+         * Called by RecyclerView to display the data at the specified position. This method
+         * should update the contents of the {@link ViewHolder#itemView} to reflect the item at
+         * the given position.
+         * <p>
+         * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
+         * again if the position of the item changes in the data set unless the item itself is
+         * invalidated or the new position cannot be determined. For this reason, you should only
+         * use the <code>position</code> parameter while acquiring the related data item inside
+         * this method and should not keep a copy of it. If you need the position of an item later
+         * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
+         * have the updated adapter position.
+         * <p>
+         * Partial bind vs full bind:
+         * <p>
+         * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
+         * {@link #notifyItemRangeChanged(int, int, Object)}.  If the payloads list is not empty,
+         * the ViewHolder is currently bound to old data and Adapter may run an efficient partial
+         * update using the payload info.  If the payload is empty,  Adapter must run a full bind.
+         * Adapter should not assume that the payload passed in notify methods will be received by
+         * onBindViewHolder().  For example when the view is not attached to the screen, the
+         * payload in notifyItemChange() will be simply dropped.
+         *
+         * @param holder The ViewHolder which should be updated to represent the contents of the
+         *               item at the given position in the data set.
+         * @param position The position of the item within the adapter's data set.
+         * @param payloads A non-null list of merged payloads. Can be empty list if requires full
+         *                 update.
+         */
+        public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
+            onBindViewHolder(holder, position);
+        }
+
+        /**
+         * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new
+         * {@link ViewHolder} and initializes some private fields to be used by RecyclerView.
+         *
+         * @see #onCreateViewHolder(ViewGroup, int)
+         */
+        public final VH createViewHolder(ViewGroup parent, int viewType) {
+            Trace.beginSection(TRACE_CREATE_VIEW_TAG);
+            final VH holder = onCreateViewHolder(parent, viewType);
+            holder.mItemViewType = viewType;
+            Trace.endSection();
+            return holder;
+        }
+
+        /**
+         * This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the
+         * {@link ViewHolder} contents with the item at the given position and also sets up some
+         * private fields to be used by RecyclerView.
+         *
+         * @see #onBindViewHolder(ViewHolder, int)
+         */
+        public final void bindViewHolder(VH holder, int position) {
+            holder.mPosition = position;
+            if (hasStableIds()) {
+                holder.mItemId = getItemId(position);
+            }
+            holder.setFlags(ViewHolder.FLAG_BOUND,
+                    ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
+                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
+            Trace.beginSection(TRACE_BIND_VIEW_TAG);
+            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
+            holder.clearPayload();
+            final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
+            if (layoutParams instanceof RecyclerView.LayoutParams) {
+                ((LayoutParams) layoutParams).mInsetsDirty = true;
+            }
+            Trace.endSection();
+        }
+
+        /**
+         * Return the view type of the item at <code>position</code> for the purposes
+         * of view recycling.
+         *
+         * <p>The default implementation of this method returns 0, making the assumption of
+         * a single view type for the adapter. Unlike ListView adapters, types need not
+         * be contiguous. Consider using id resources to uniquely identify item view types.
+         *
+         * @param position position to query
+         * @return integer value identifying the type of the view needed to represent the item at
+         *                 <code>position</code>. Type codes need not be contiguous.
+         */
+        public int getItemViewType(int position) {
+            return 0;
+        }
+
+        /**
+         * Indicates whether each item in the data set can be represented with a unique identifier
+         * of type {@link java.lang.Long}.
+         *
+         * @param hasStableIds Whether items in data set have unique identifiers or not.
+         * @see #hasStableIds()
+         * @see #getItemId(int)
+         */
+        public void setHasStableIds(boolean hasStableIds) {
+            if (hasObservers()) {
+                throw new IllegalStateException("Cannot change whether this adapter has "
+                        + "stable IDs while the adapter has registered observers.");
+            }
+            mHasStableIds = hasStableIds;
+        }
+
+        /**
+         * Return the stable ID for the item at <code>position</code>. If {@link #hasStableIds()}
+         * would return false this method should return {@link #NO_ID}. The default implementation
+         * of this method returns {@link #NO_ID}.
+         *
+         * @param position Adapter position to query
+         * @return the stable ID of the item at position
+         */
+        public long getItemId(int position) {
+            return NO_ID;
+        }
+
+        /**
+         * Returns the total number of items in the data set held by the adapter.
+         *
+         * @return The total number of items in this adapter.
+         */
+        public abstract int getItemCount();
+
+        /**
+         * Returns true if this adapter publishes a unique <code>long</code> value that can
+         * act as a key for the item at a given position in the data set. If that item is relocated
+         * in the data set, the ID returned for that item should be the same.
+         *
+         * @return true if this adapter's items have stable IDs
+         */
+        public final boolean hasStableIds() {
+            return mHasStableIds;
+        }
+
+        /**
+         * Called when a view created by this adapter has been recycled.
+         *
+         * <p>A view is recycled when a {@link LayoutManager} decides that it no longer
+         * needs to be attached to its parent {@link RecyclerView}. This can be because it has
+         * fallen out of visibility or a set of cached views represented by views still
+         * attached to the parent RecyclerView. If an item view has large or expensive data
+         * bound to it such as large bitmaps, this may be a good place to release those
+         * resources.</p>
+         * <p>
+         * RecyclerView calls this method right before clearing ViewHolder's internal data and
+         * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information
+         * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get
+         * its adapter position.
+         *
+         * @param holder The ViewHolder for the view being recycled
+         */
+        public void onViewRecycled(VH holder) {
+        }
+
+        /**
+         * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled
+         * due to its transient state. Upon receiving this callback, Adapter can clear the
+         * animation(s) that effect the View's transient state and return <code>true</code> so that
+         * the View can be recycled. Keep in mind that the View in question is already removed from
+         * the RecyclerView.
+         * <p>
+         * In some cases, it is acceptable to recycle a View although it has transient state. Most
+         * of the time, this is a case where the transient state will be cleared in
+         * {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position.
+         * For this reason, RecyclerView leaves the decision to the Adapter and uses the return
+         * value of this method to decide whether the View should be recycled or not.
+         * <p>
+         * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you
+         * should never receive this callback because RecyclerView keeps those Views as children
+         * until their animations are complete. This callback is useful when children of the item
+         * views create animations which may not be easy to implement using an {@link ItemAnimator}.
+         * <p>
+         * You should <em>never</em> fix this issue by calling
+         * <code>holder.itemView.setHasTransientState(false);</code> unless you've previously called
+         * <code>holder.itemView.setHasTransientState(true);</code>. Each
+         * <code>View.setHasTransientState(true)</code> call must be matched by a
+         * <code>View.setHasTransientState(false)</code> call, otherwise, the state of the View
+         * may become inconsistent. You should always prefer to end or cancel animations that are
+         * triggering the transient state instead of handling it manually.
+         *
+         * @param holder The ViewHolder containing the View that could not be recycled due to its
+         *               transient state.
+         * @return True if the View should be recycled, false otherwise. Note that if this method
+         * returns <code>true</code>, RecyclerView <em>will ignore</em> the transient state of
+         * the View and recycle it regardless. If this method returns <code>false</code>,
+         * RecyclerView will check the View's transient state again before giving a final decision.
+         * Default implementation returns false.
+         */
+        public boolean onFailedToRecycleView(VH holder) {
+            return false;
+        }
+
+        /**
+         * Called when a view created by this adapter has been attached to a window.
+         *
+         * <p>This can be used as a reasonable signal that the view is about to be seen
+         * by the user. If the adapter previously freed any resources in
+         * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
+         * those resources should be restored here.</p>
+         *
+         * @param holder Holder of the view being attached
+         */
+        public void onViewAttachedToWindow(VH holder) {
+        }
+
+        /**
+         * Called when a view created by this adapter has been detached from its window.
+         *
+         * <p>Becoming detached from the window is not necessarily a permanent condition;
+         * the consumer of an Adapter's views may choose to cache views offscreen while they
+         * are not visible, attaching and detaching them as appropriate.</p>
+         *
+         * @param holder Holder of the view being detached
+         */
+        public void onViewDetachedFromWindow(VH holder) {
+        }
+
+        /**
+         * Returns true if one or more observers are attached to this adapter.
+         *
+         * @return true if this adapter has observers
+         */
+        public final boolean hasObservers() {
+            return mObservable.hasObservers();
+        }
+
+        /**
+         * Register a new observer to listen for data changes.
+         *
+         * <p>The adapter may publish a variety of events describing specific changes.
+         * Not all adapters may support all change types and some may fall back to a generic
+         * {@link com.android.internal.widget.RecyclerView.AdapterDataObserver#onChanged()
+         * "something changed"} event if more specific data is not available.</p>
+         *
+         * <p>Components registering observers with an adapter are responsible for
+         * {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
+         * unregistering} those observers when finished.</p>
+         *
+         * @param observer Observer to register
+         *
+         * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
+         */
+        public void registerAdapterDataObserver(AdapterDataObserver observer) {
+            mObservable.registerObserver(observer);
+        }
+
+        /**
+         * Unregister an observer currently listening for data changes.
+         *
+         * <p>The unregistered observer will no longer receive events about changes
+         * to the adapter.</p>
+         *
+         * @param observer Observer to unregister
+         *
+         * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver)
+         */
+        public void unregisterAdapterDataObserver(AdapterDataObserver observer) {
+            mObservable.unregisterObserver(observer);
+        }
+
+        /**
+         * Called by RecyclerView when it starts observing this Adapter.
+         * <p>
+         * Keep in mind that same adapter may be observed by multiple RecyclerViews.
+         *
+         * @param recyclerView The RecyclerView instance which started observing this adapter.
+         * @see #onDetachedFromRecyclerView(RecyclerView)
+         */
+        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+        }
+
+        /**
+         * Called by RecyclerView when it stops observing this Adapter.
+         *
+         * @param recyclerView The RecyclerView instance which stopped observing this adapter.
+         * @see #onAttachedToRecyclerView(RecyclerView)
+         */
+        public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+        }
+
+        /**
+         * Notify any registered observers that the data set has changed.
+         *
+         * <p>There are two different classes of data change events, item changes and structural
+         * changes. Item changes are when a single item has its data updated but no positional
+         * changes have occurred. Structural changes are when items are inserted, removed or moved
+         * within the data set.</p>
+         *
+         * <p>This event does not specify what about the data set has changed, forcing
+         * any observers to assume that all existing items and structure may no longer be valid.
+         * LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
+         *
+         * <p><code>RecyclerView</code> will attempt to synthesize visible structural change events
+         * for adapters that report that they have {@link #hasStableIds() stable IDs} when
+         * this method is used. This can help for the purposes of animation and visual
+         * object persistence but individual item views will still need to be rebound
+         * and relaid out.</p>
+         *
+         * <p>If you are writing an adapter it will always be more efficient to use the more
+         * specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
+         * as a last resort.</p>
+         *
+         * @see #notifyItemChanged(int)
+         * @see #notifyItemInserted(int)
+         * @see #notifyItemRemoved(int)
+         * @see #notifyItemRangeChanged(int, int)
+         * @see #notifyItemRangeInserted(int, int)
+         * @see #notifyItemRangeRemoved(int, int)
+         */
+        public final void notifyDataSetChanged() {
+            mObservable.notifyChanged();
+        }
+
+        /**
+         * Notify any registered observers that the item at <code>position</code> has changed.
+         * Equivalent to calling <code>notifyItemChanged(position, null);</code>.
+         *
+         * <p>This is an item change event, not a structural change event. It indicates that any
+         * reflection of the data at <code>position</code> is out of date and should be updated.
+         * The item at <code>position</code> retains the same identity.</p>
+         *
+         * @param position Position of the item that has changed
+         *
+         * @see #notifyItemRangeChanged(int, int)
+         */
+        public final void notifyItemChanged(int position) {
+            mObservable.notifyItemRangeChanged(position, 1);
+        }
+
+        /**
+         * Notify any registered observers that the item at <code>position</code> has changed with
+         * an optional payload object.
+         *
+         * <p>This is an item change event, not a structural change event. It indicates that any
+         * reflection of the data at <code>position</code> is out of date and should be updated.
+         * The item at <code>position</code> retains the same identity.
+         * </p>
+         *
+         * <p>
+         * Client can optionally pass a payload for partial change. These payloads will be merged
+         * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
+         * item is already represented by a ViewHolder and it will be rebound to the same
+         * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
+         * payloads on that item and prevent future payload until
+         * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
+         * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
+         * attached, the payload will be simply dropped.
+         *
+         * @param position Position of the item that has changed
+         * @param payload Optional parameter, use null to identify a "full" update
+         *
+         * @see #notifyItemRangeChanged(int, int)
+         */
+        public final void notifyItemChanged(int position, Object payload) {
+            mObservable.notifyItemRangeChanged(position, 1, payload);
+        }
+
+        /**
+         * Notify any registered observers that the <code>itemCount</code> items starting at
+         * position <code>positionStart</code> have changed.
+         * Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
+         *
+         * <p>This is an item change event, not a structural change event. It indicates that
+         * any reflection of the data in the given position range is out of date and should
+         * be updated. The items in the given range retain the same identity.</p>
+         *
+         * @param positionStart Position of the first item that has changed
+         * @param itemCount Number of items that have changed
+         *
+         * @see #notifyItemChanged(int)
+         */
+        public final void notifyItemRangeChanged(int positionStart, int itemCount) {
+            mObservable.notifyItemRangeChanged(positionStart, itemCount);
+        }
+
+        /**
+         * Notify any registered observers that the <code>itemCount</code> items starting at
+         * position <code>positionStart</code> have changed. An optional payload can be
+         * passed to each changed item.
+         *
+         * <p>This is an item change event, not a structural change event. It indicates that any
+         * reflection of the data in the given position range is out of date and should be updated.
+         * The items in the given range retain the same identity.
+         * </p>
+         *
+         * <p>
+         * Client can optionally pass a payload for partial change. These payloads will be merged
+         * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
+         * item is already represented by a ViewHolder and it will be rebound to the same
+         * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
+         * payloads on that item and prevent future payload until
+         * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
+         * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
+         * attached, the payload will be simply dropped.
+         *
+         * @param positionStart Position of the first item that has changed
+         * @param itemCount Number of items that have changed
+         * @param payload  Optional parameter, use null to identify a "full" update
+         *
+         * @see #notifyItemChanged(int)
+         */
+        public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+            mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
+        }
+
+        /**
+         * Notify any registered observers that the item reflected at <code>position</code>
+         * has been newly inserted. The item previously at <code>position</code> is now at
+         * position <code>position + 1</code>.
+         *
+         * <p>This is a structural change event. Representations of other existing items in the
+         * data set are still considered up to date and will not be rebound, though their
+         * positions may be altered.</p>
+         *
+         * @param position Position of the newly inserted item in the data set
+         *
+         * @see #notifyItemRangeInserted(int, int)
+         */
+        public final void notifyItemInserted(int position) {
+            mObservable.notifyItemRangeInserted(position, 1);
+        }
+
+        /**
+         * Notify any registered observers that the item reflected at <code>fromPosition</code>
+         * has been moved to <code>toPosition</code>.
+         *
+         * <p>This is a structural change event. Representations of other existing items in the
+         * data set are still considered up to date and will not be rebound, though their
+         * positions may be altered.</p>
+         *
+         * @param fromPosition Previous position of the item.
+         * @param toPosition New position of the item.
+         */
+        public final void notifyItemMoved(int fromPosition, int toPosition) {
+            mObservable.notifyItemMoved(fromPosition, toPosition);
+        }
+
+        /**
+         * Notify any registered observers that the currently reflected <code>itemCount</code>
+         * items starting at <code>positionStart</code> have been newly inserted. The items
+         * previously located at <code>positionStart</code> and beyond can now be found starting
+         * at position <code>positionStart + itemCount</code>.
+         *
+         * <p>This is a structural change event. Representations of other existing items in the
+         * data set are still considered up to date and will not be rebound, though their positions
+         * may be altered.</p>
+         *
+         * @param positionStart Position of the first item that was inserted
+         * @param itemCount Number of items inserted
+         *
+         * @see #notifyItemInserted(int)
+         */
+        public final void notifyItemRangeInserted(int positionStart, int itemCount) {
+            mObservable.notifyItemRangeInserted(positionStart, itemCount);
+        }
+
+        /**
+         * Notify any registered observers that the item previously located at <code>position</code>
+         * has been removed from the data set. The items previously located at and after
+         * <code>position</code> may now be found at <code>oldPosition - 1</code>.
+         *
+         * <p>This is a structural change event. Representations of other existing items in the
+         * data set are still considered up to date and will not be rebound, though their positions
+         * may be altered.</p>
+         *
+         * @param position Position of the item that has now been removed
+         *
+         * @see #notifyItemRangeRemoved(int, int)
+         */
+        public final void notifyItemRemoved(int position) {
+            mObservable.notifyItemRangeRemoved(position, 1);
+        }
+
+        /**
+         * Notify any registered observers that the <code>itemCount</code> items previously
+         * located at <code>positionStart</code> have been removed from the data set. The items
+         * previously located at and after <code>positionStart + itemCount</code> may now be found
+         * at <code>oldPosition - itemCount</code>.
+         *
+         * <p>This is a structural change event. Representations of other existing items in the data
+         * set are still considered up to date and will not be rebound, though their positions
+         * may be altered.</p>
+         *
+         * @param positionStart Previous position of the first item that was removed
+         * @param itemCount Number of items removed from the data set
+         */
+        public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
+            mObservable.notifyItemRangeRemoved(positionStart, itemCount);
+        }
+    }
+
+    void dispatchChildDetached(View child) {
+        final ViewHolder viewHolder = getChildViewHolderInt(child);
+        onChildDetachedFromWindow(child);
+        if (mAdapter != null && viewHolder != null) {
+            mAdapter.onViewDetachedFromWindow(viewHolder);
+        }
+        if (mOnChildAttachStateListeners != null) {
+            final int cnt = mOnChildAttachStateListeners.size();
+            for (int i = cnt - 1; i >= 0; i--) {
+                mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child);
+            }
+        }
+    }
+
+    void dispatchChildAttached(View child) {
+        final ViewHolder viewHolder = getChildViewHolderInt(child);
+        onChildAttachedToWindow(child);
+        if (mAdapter != null && viewHolder != null) {
+            mAdapter.onViewAttachedToWindow(viewHolder);
+        }
+        if (mOnChildAttachStateListeners != null) {
+            final int cnt = mOnChildAttachStateListeners.size();
+            for (int i = cnt - 1; i >= 0; i--) {
+                mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child);
+            }
+        }
+    }
+
+    /**
+     * A <code>LayoutManager</code> is responsible for measuring and positioning item views
+     * within a <code>RecyclerView</code> as well as determining the policy for when to recycle
+     * item views that are no longer visible to the user. By changing the <code>LayoutManager</code>
+     * a <code>RecyclerView</code> can be used to implement a standard vertically scrolling list,
+     * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock
+     * layout managers are provided for general use.
+     * <p/>
+     * If the LayoutManager specifies a default constructor or one with the signature
+     * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will
+     * instantiate and set the LayoutManager when being inflated. Most used properties can
+     * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case
+     * a LayoutManager specifies both constructors, the non-default constructor will take
+     * precedence.
+     *
+     */
+    public abstract static class LayoutManager {
+        ChildHelper mChildHelper;
+        RecyclerView mRecyclerView;
+
+        @Nullable
+        SmoothScroller mSmoothScroller;
+
+        boolean mRequestedSimpleAnimations = false;
+
+        boolean mIsAttachedToWindow = false;
+
+        boolean mAutoMeasure = false;
+
+        /**
+         * LayoutManager has its own more strict measurement cache to avoid re-measuring a child
+         * if the space that will be given to it is already larger than what it has measured before.
+         */
+        private boolean mMeasurementCacheEnabled = true;
+
+        private boolean mItemPrefetchEnabled = true;
+
+        /**
+         * Written by {@link GapWorker} when prefetches occur to track largest number of view ever
+         * requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or
+         * {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call.
+         *
+         * If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)},
+         * will be reset upon layout to prevent initial prefetches (often large, since they're
+         * proportional to expected child count) from expanding cache permanently.
+         */
+        int mPrefetchMaxCountObserved;
+
+        /**
+         * If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset.
+         */
+        boolean mPrefetchMaxObservedInInitialPrefetch;
+
+        /**
+         * These measure specs might be the measure specs that were passed into RecyclerView's
+         * onMeasure method OR fake measure specs created by the RecyclerView.
+         * For example, when a layout is run, RecyclerView always sets these specs to be
+         * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass.
+         * <p>
+         * Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the
+         * API level and sets the size to 0 pre-M to avoid any issue that might be caused by
+         * corrupt values. Older platforms have no responsibility to provide a size if they set
+         * mode to unspecified.
+         */
+        private int mWidthMode, mHeightMode;
+        private int mWidth, mHeight;
+
+
+        /**
+         * Interface for LayoutManagers to request items to be prefetched, based on position, with
+         * specified distance from viewport, which indicates priority.
+         *
+         * @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)
+         * @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+         */
+        public interface LayoutPrefetchRegistry {
+            /**
+             * Requests an an item to be prefetched, based on position, with a specified distance,
+             * indicating priority.
+             *
+             * @param layoutPosition Position of the item to prefetch.
+             * @param pixelDistance Distance from the current viewport to the bounds of the item,
+             *                      must be non-negative.
+             */
+            void addPosition(int layoutPosition, int pixelDistance);
+        }
+
+        void setRecyclerView(RecyclerView recyclerView) {
+            if (recyclerView == null) {
+                mRecyclerView = null;
+                mChildHelper = null;
+                mWidth = 0;
+                mHeight = 0;
+            } else {
+                mRecyclerView = recyclerView;
+                mChildHelper = recyclerView.mChildHelper;
+                mWidth = recyclerView.getWidth();
+                mHeight = recyclerView.getHeight();
+            }
+            mWidthMode = MeasureSpec.EXACTLY;
+            mHeightMode = MeasureSpec.EXACTLY;
+        }
+
+        void setMeasureSpecs(int wSpec, int hSpec) {
+            mWidth = MeasureSpec.getSize(wSpec);
+            mWidthMode = MeasureSpec.getMode(wSpec);
+            if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
+                mWidth = 0;
+            }
+
+            mHeight = MeasureSpec.getSize(hSpec);
+            mHeightMode = MeasureSpec.getMode(hSpec);
+            if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
+                mHeight = 0;
+            }
+        }
+
+        /**
+         * Called after a layout is calculated during a measure pass when using auto-measure.
+         * <p>
+         * It simply traverses all children to calculate a bounding box then calls
+         * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method
+         * if they need to handle the bounding box differently.
+         * <p>
+         * For example, GridLayoutManager override that method to ensure that even if a column is
+         * empty, the GridLayoutManager still measures wide enough to include it.
+         *
+         * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure
+         * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure
+         */
+        void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
+            final int count = getChildCount();
+            if (count == 0) {
+                mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
+                return;
+            }
+            int minX = Integer.MAX_VALUE;
+            int minY = Integer.MAX_VALUE;
+            int maxX = Integer.MIN_VALUE;
+            int maxY = Integer.MIN_VALUE;
+
+            for (int i = 0; i < count; i++) {
+                View child = getChildAt(i);
+                final Rect bounds = mRecyclerView.mTempRect;
+                getDecoratedBoundsWithMargins(child, bounds);
+                if (bounds.left < minX) {
+                    minX = bounds.left;
+                }
+                if (bounds.right > maxX) {
+                    maxX = bounds.right;
+                }
+                if (bounds.top < minY) {
+                    minY = bounds.top;
+                }
+                if (bounds.bottom > maxY) {
+                    maxY = bounds.bottom;
+                }
+            }
+            mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
+            setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
+        }
+
+        /**
+         * Sets the measured dimensions from the given bounding box of the children and the
+         * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is
+         * called after the RecyclerView calls
+         * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a measurement pass.
+         * <p>
+         * This method should call {@link #setMeasuredDimension(int, int)}.
+         * <p>
+         * The default implementation adds the RecyclerView's padding to the given bounding box
+         * then caps the value to be within the given measurement specs.
+         * <p>
+         * This method is only called if the LayoutManager opted into the auto measurement API.
+         *
+         * @param childrenBounds The bounding box of all children
+         * @param wSpec The widthMeasureSpec that was passed into the RecyclerView.
+         * @param hSpec The heightMeasureSpec that was passed into the RecyclerView.
+         *
+         * @see #setAutoMeasureEnabled(boolean)
+         */
+        public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
+            int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
+            int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
+            int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
+            int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
+            setMeasuredDimension(width, height);
+        }
+
+        /**
+         * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView
+         */
+        public void requestLayout() {
+            if (mRecyclerView != null) {
+                mRecyclerView.requestLayout();
+            }
+        }
+
+        /**
+         * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+         * {@link IllegalStateException} if it <b>is not</b>.
+         *
+         * @param message The message for the exception. Can be null.
+         * @see #assertNotInLayoutOrScroll(String)
+         */
+        public void assertInLayoutOrScroll(String message) {
+            if (mRecyclerView != null) {
+                mRecyclerView.assertInLayoutOrScroll(message);
+            }
+        }
+
+        /**
+         * Chooses a size from the given specs and parameters that is closest to the desired size
+         * and also complies with the spec.
+         *
+         * @param spec The measureSpec
+         * @param desired The preferred measurement
+         * @param min The minimum value
+         *
+         * @return A size that fits to the given specs
+         */
+        public static int chooseSize(int spec, int desired, int min) {
+            final int mode = View.MeasureSpec.getMode(spec);
+            final int size = View.MeasureSpec.getSize(spec);
+            switch (mode) {
+                case View.MeasureSpec.EXACTLY:
+                    return size;
+                case View.MeasureSpec.AT_MOST:
+                    return Math.min(size, Math.max(desired, min));
+                case View.MeasureSpec.UNSPECIFIED:
+                default:
+                    return Math.max(desired, min);
+            }
+        }
+
+        /**
+         * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+         * {@link IllegalStateException} if it <b>is</b>.
+         *
+         * @param message The message for the exception. Can be null.
+         * @see #assertInLayoutOrScroll(String)
+         */
+        public void assertNotInLayoutOrScroll(String message) {
+            if (mRecyclerView != null) {
+                mRecyclerView.assertNotInLayoutOrScroll(message);
+            }
+        }
+
+        /**
+         * Defines whether the layout should be measured by the RecyclerView or the LayoutManager
+         * wants to handle the layout measurements itself.
+         * <p>
+         * This method is usually called by the LayoutManager with value {@code true} if it wants
+         * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize
+         * the measurement logic, you can call this method with {@code false} and override
+         * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic.
+         * <p>
+         * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or
+         * handle various specs provided by the RecyclerView's parent.
+         * It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an
+         * {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based
+         * on children's positions. It does this while supporting all existing animation
+         * capabilities of the RecyclerView.
+         * <p>
+         * AutoMeasure works as follows:
+         * <ol>
+         * <li>LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of
+         * the framework LayoutManagers use {@code auto-measure}.</li>
+         * <li>When {@link RecyclerView#onMeasure(int, int)} is called, if the provided specs are
+         * exact, RecyclerView will only call LayoutManager's {@code onMeasure} and return without
+         * doing any layout calculation.</li>
+         * <li>If one of the layout specs is not {@code EXACT}, the RecyclerView will start the
+         * layout process in {@code onMeasure} call. It will process all pending Adapter updates and
+         * decide whether to run a predictive layout or not. If it decides to do so, it will first
+         * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to
+         * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still
+         * return the width and height of the RecyclerView as of the last layout calculation.
+         * <p>
+         * After handling the predictive case, RecyclerView will call
+         * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
+         * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can
+         * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()},
+         * {@link #getWidth()} and {@link #getWidthMode()}.</li>
+         * <li>After the layout calculation, RecyclerView sets the measured width & height by
+         * calculating the bounding box for the children (+ RecyclerView's padding). The
+         * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose
+         * different values. For instance, GridLayoutManager overrides this value to handle the case
+         * where if it is vertical and has 3 columns but only 2 items, it should still measure its
+         * width to fit 3 items, not 2.</li>
+         * <li>Any following on measure call to the RecyclerView will run
+         * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
+         * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will
+         * take care of which views are actually added / removed / moved / changed for animations so
+         * that the LayoutManager should not worry about them and handle each
+         * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.
+         * </li>
+         * <li>When measure is complete and RecyclerView's
+         * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks
+         * whether it already did layout calculations during the measure pass and if so, it re-uses
+         * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)}
+         * if the last measure spec was different from the final dimensions or adapter contents
+         * have changed between the measure call and the layout call.</li>
+         * <li>Finally, animations are calculated and run as usual.</li>
+         * </ol>
+         *
+         * @param enabled <code>True</code> if the Layout should be measured by the
+         *                             RecyclerView, <code>false</code> if the LayoutManager wants
+         *                             to measure itself.
+         *
+         * @see #setMeasuredDimension(Rect, int, int)
+         * @see #isAutoMeasureEnabled()
+         */
+        public void setAutoMeasureEnabled(boolean enabled) {
+            mAutoMeasure = enabled;
+        }
+
+        /**
+         * Returns whether the LayoutManager uses the automatic measurement API or not.
+         *
+         * @return <code>True</code> if the LayoutManager is measured by the RecyclerView or
+         * <code>false</code> if it measures itself.
+         *
+         * @see #setAutoMeasureEnabled(boolean)
+         */
+        public boolean isAutoMeasureEnabled() {
+            return mAutoMeasure;
+        }
+
+        /**
+         * Returns whether this LayoutManager supports automatic item animations.
+         * A LayoutManager wishing to support item animations should obey certain
+         * rules as outlined in {@link #onLayoutChildren(Recycler, State)}.
+         * The default return value is <code>false</code>, so subclasses of LayoutManager
+         * will not get predictive item animations by default.
+         *
+         * <p>Whether item animations are enabled in a RecyclerView is determined both
+         * by the return value from this method and the
+         * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the
+         * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this
+         * method returns false, then simple item animations will be enabled, in which
+         * views that are moving onto or off of the screen are simply faded in/out. If
+         * the RecyclerView has a non-null ItemAnimator and this method returns true,
+         * then there will be two calls to {@link #onLayoutChildren(Recycler, State)} to
+         * setup up the information needed to more intelligently predict where appearing
+         * and disappearing views should be animated from/to.</p>
+         *
+         * @return true if predictive item animations should be enabled, false otherwise
+         */
+        public boolean supportsPredictiveItemAnimations() {
+            return false;
+        }
+
+        /**
+         * Sets whether the LayoutManager should be queried for views outside of
+         * its viewport while the UI thread is idle between frames.
+         *
+         * <p>If enabled, the LayoutManager will be queried for items to inflate/bind in between
+         * view system traversals on devices running API 21 or greater. Default value is true.</p>
+         *
+         * <p>On platforms API level 21 and higher, the UI thread is idle between passing a frame
+         * to RenderThread and the starting up its next frame at the next VSync pulse. By
+         * prefetching out of window views in this time period, delays from inflation and view
+         * binding are much less likely to cause jank and stuttering during scrolls and flings.</p>
+         *
+         * <p>While prefetch is enabled, it will have the side effect of expanding the effective
+         * size of the View cache to hold prefetched views.</p>
+         *
+         * @param enabled <code>True</code> if items should be prefetched in between traversals.
+         *
+         * @see #isItemPrefetchEnabled()
+         */
+        public final void setItemPrefetchEnabled(boolean enabled) {
+            if (enabled != mItemPrefetchEnabled) {
+                mItemPrefetchEnabled = enabled;
+                mPrefetchMaxCountObserved = 0;
+                if (mRecyclerView != null) {
+                    mRecyclerView.mRecycler.updateViewCacheSize();
+                }
+            }
+        }
+
+        /**
+         * Sets whether the LayoutManager should be queried for views outside of
+         * its viewport while the UI thread is idle between frames.
+         *
+         * @see #setItemPrefetchEnabled(boolean)
+         *
+         * @return true if item prefetch is enabled, false otherwise
+         */
+        public final boolean isItemPrefetchEnabled() {
+            return mItemPrefetchEnabled;
+        }
+
+        /**
+         * Gather all positions from the LayoutManager to be prefetched, given specified momentum.
+         *
+         * <p>If item prefetch is enabled, this method is called in between traversals to gather
+         * which positions the LayoutManager will soon need, given upcoming movement in subsequent
+         * traversals.</p>
+         *
+         * <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for
+         * each item to be prepared, and these positions will have their ViewHolders created and
+         * bound, if there is sufficient time available, in advance of being needed by a
+         * scroll or layout.</p>
+         *
+         * @param dx X movement component.
+         * @param dy Y movement component.
+         * @param state State of RecyclerView
+         * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into.
+         *
+         * @see #isItemPrefetchEnabled()
+         * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+         */
+        public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
+                LayoutPrefetchRegistry layoutPrefetchRegistry) {}
+
+        /**
+         * Gather all positions from the LayoutManager to be prefetched in preperation for its
+         * RecyclerView to come on screen, due to the movement of another, containing RecyclerView.
+         *
+         * <p>This method is only called when a RecyclerView is nested in another RecyclerView.</p>
+         *
+         * <p>If item prefetch is enabled for this LayoutManager, as well in another containing
+         * LayoutManager, this method is called in between draw traversals to gather
+         * which positions this LayoutManager will first need, once it appears on the screen.</p>
+         *
+         * <p>For example, if this LayoutManager represents a horizontally scrolling list within a
+         * vertically scrolling LayoutManager, this method would be called when the horizontal list
+         * is about to come onscreen.</p>
+         *
+         * <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for
+         * each item to be prepared, and these positions will have their ViewHolders created and
+         * bound, if there is sufficient time available, in advance of being needed by a
+         * scroll or layout.</p>
+         *
+         * @param adapterItemCount number of items in the associated adapter.
+         * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into.
+         *
+         * @see #isItemPrefetchEnabled()
+         * @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)
+         */
+        public void collectInitialPrefetchPositions(int adapterItemCount,
+                LayoutPrefetchRegistry layoutPrefetchRegistry) {}
+
+        void dispatchAttachedToWindow(RecyclerView view) {
+            mIsAttachedToWindow = true;
+            onAttachedToWindow(view);
+        }
+
+        void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) {
+            mIsAttachedToWindow = false;
+            onDetachedFromWindow(view, recycler);
+        }
+
+        /**
+         * Returns whether LayoutManager is currently attached to a RecyclerView which is attached
+         * to a window.
+         *
+         * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView
+         * is attached to window.
+         */
+        public boolean isAttachedToWindow() {
+            return mIsAttachedToWindow;
+        }
+
+        /**
+         * Causes the Runnable to execute on the next animation time step.
+         * The runnable will be run on the user interface thread.
+         * <p>
+         * Calling this method when LayoutManager is not attached to a RecyclerView has no effect.
+         *
+         * @param action The Runnable that will be executed.
+         *
+         * @see #removeCallbacks
+         */
+        public void postOnAnimation(Runnable action) {
+            if (mRecyclerView != null) {
+                mRecyclerView.postOnAnimation(action);
+            }
+        }
+
+        /**
+         * Removes the specified Runnable from the message queue.
+         * <p>
+         * Calling this method when LayoutManager is not attached to a RecyclerView has no effect.
+         *
+         * @param action The Runnable to remove from the message handling queue
+         *
+         * @return true if RecyclerView could ask the Handler to remove the Runnable,
+         *         false otherwise. When the returned value is true, the Runnable
+         *         may or may not have been actually removed from the message queue
+         *         (for instance, if the Runnable was not in the queue already.)
+         *
+         * @see #postOnAnimation
+         */
+        public boolean removeCallbacks(Runnable action) {
+            if (mRecyclerView != null) {
+                return mRecyclerView.removeCallbacks(action);
+            }
+            return false;
+        }
+        /**
+         * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView
+         * is attached to a window.
+         * <p>
+         * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
+         * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was
+         * not requested on the RecyclerView while it was detached.
+         * <p>
+         * Subclass implementations should always call through to the superclass implementation.
+         *
+         * @param view The RecyclerView this LayoutManager is bound to
+         *
+         * @see #onDetachedFromWindow(RecyclerView, Recycler)
+         */
+        @CallSuper
+        public void onAttachedToWindow(RecyclerView view) {
+        }
+
+        /**
+         * @deprecated
+         * override {@link #onDetachedFromWindow(RecyclerView, Recycler)}
+         */
+        @Deprecated
+        public void onDetachedFromWindow(RecyclerView view) {
+
+        }
+
+        /**
+         * Called when this LayoutManager is detached from its parent RecyclerView or when
+         * its parent RecyclerView is detached from its window.
+         * <p>
+         * LayoutManager should clear all of its View references as another LayoutManager might be
+         * assigned to the RecyclerView.
+         * <p>
+         * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
+         * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was
+         * not requested on the RecyclerView while it was detached.
+         * <p>
+         * If your LayoutManager has View references that it cleans in on-detach, it should also
+         * call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when
+         * RecyclerView is re-attached.
+         * <p>
+         * Subclass implementations should always call through to the superclass implementation.
+         *
+         * @param view The RecyclerView this LayoutManager is bound to
+         * @param recycler The recycler to use if you prefer to recycle your children instead of
+         *                 keeping them around.
+         *
+         * @see #onAttachedToWindow(RecyclerView)
+         */
+        @CallSuper
+        public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
+            onDetachedFromWindow(view);
+        }
+
+        /**
+         * Check if the RecyclerView is configured to clip child views to its padding.
+         *
+         * @return true if this RecyclerView clips children to its padding, false otherwise
+         */
+        public boolean getClipToPadding() {
+            return mRecyclerView != null && mRecyclerView.mClipToPadding;
+        }
+
+        /**
+         * Lay out all relevant child views from the given adapter.
+         *
+         * The LayoutManager is in charge of the behavior of item animations. By default,
+         * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple
+         * item animations are enabled. This means that add/remove operations on the
+         * adapter will result in animations to add new or appearing items, removed or
+         * disappearing items, and moved items. If a LayoutManager returns false from
+         * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a
+         * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the
+         * RecyclerView will have enough information to run those animations in a simple
+         * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will
+         * simply fade views in and out, whether they are actually added/removed or whether
+         * they are moved on or off the screen due to other add/remove operations.
+         *
+         * <p>A LayoutManager wanting a better item animation experience, where items can be
+         * animated onto and off of the screen according to where the items exist when they
+         * are not on screen, then the LayoutManager should return true from
+         * {@link #supportsPredictiveItemAnimations()} and add additional logic to
+         * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations
+         * means that {@link #onLayoutChildren(Recycler, State)} will be called twice;
+         * once as a "pre" layout step to determine where items would have been prior to
+         * a real layout, and again to do the "real" layout. In the pre-layout phase,
+         * items will remember their pre-layout positions to allow them to be laid out
+         * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will
+         * be returned from the scrap to help determine correct placement of other items.
+         * These removed items should not be added to the child list, but should be used
+         * to help calculate correct positioning of other views, including views that
+         * were not previously onscreen (referred to as APPEARING views), but whose
+         * pre-layout offscreen position can be determined given the extra
+         * information about the pre-layout removed views.</p>
+         *
+         * <p>The second layout pass is the real layout in which only non-removed views
+         * will be used. The only additional requirement during this pass is, if
+         * {@link #supportsPredictiveItemAnimations()} returns true, to note which
+         * views exist in the child list prior to layout and which are not there after
+         * layout (referred to as DISAPPEARING views), and to position/layout those views
+         * appropriately, without regard to the actual bounds of the RecyclerView. This allows
+         * the animation system to know the location to which to animate these disappearing
+         * views.</p>
+         *
+         * <p>The default LayoutManager implementations for RecyclerView handle all of these
+         * requirements for animations already. Clients of RecyclerView can either use one
+         * of these layout managers directly or look at their implementations of
+         * onLayoutChildren() to see how they account for the APPEARING and
+         * DISAPPEARING views.</p>
+         *
+         * @param recycler         Recycler to use for fetching potentially cached views for a
+         *                         position
+         * @param state            Transient state of RecyclerView
+         */
+        public void onLayoutChildren(Recycler recycler, State state) {
+            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
+        }
+
+        /**
+         * Called after a full layout calculation is finished. The layout calculation may include
+         * multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or
+         * layout measurement but it will include only one {@link #onLayoutCompleted(State)} call.
+         * This method will be called at the end of {@link View#layout(int, int, int, int)} call.
+         * <p>
+         * This is a good place for the LayoutManager to do some cleanup like pending scroll
+         * position, saved state etc.
+         *
+         * @param state Transient state of RecyclerView
+         */
+        public void onLayoutCompleted(State state) {
+        }
+
+        /**
+         * Create a default <code>LayoutParams</code> object for a child of the RecyclerView.
+         *
+         * <p>LayoutManagers will often want to use a custom <code>LayoutParams</code> type
+         * to store extra information specific to the layout. Client code should subclass
+         * {@link RecyclerView.LayoutParams} for this purpose.</p>
+         *
+         * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+         * you must also override
+         * {@link #checkLayoutParams(LayoutParams)},
+         * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+         * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+         *
+         * @return A new LayoutParams for a child view
+         */
+        public abstract LayoutParams generateDefaultLayoutParams();
+
+        /**
+         * Determines the validity of the supplied LayoutParams object.
+         *
+         * <p>This should check to make sure that the object is of the correct type
+         * and all values are within acceptable ranges. The default implementation
+         * returns <code>true</code> for non-null params.</p>
+         *
+         * @param lp LayoutParams object to check
+         * @return true if this LayoutParams object is valid, false otherwise
+         */
+        public boolean checkLayoutParams(LayoutParams lp) {
+            return lp != null;
+        }
+
+        /**
+         * Create a LayoutParams object suitable for this LayoutManager, copying relevant
+         * values from the supplied LayoutParams object if possible.
+         *
+         * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+         * you must also override
+         * {@link #checkLayoutParams(LayoutParams)},
+         * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+         * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+         *
+         * @param lp Source LayoutParams object to copy values from
+         * @return a new LayoutParams object
+         */
+        public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+            if (lp instanceof LayoutParams) {
+                return new LayoutParams((LayoutParams) lp);
+            } else if (lp instanceof MarginLayoutParams) {
+                return new LayoutParams((MarginLayoutParams) lp);
+            } else {
+                return new LayoutParams(lp);
+            }
+        }
+
+        /**
+         * Create a LayoutParams object suitable for this LayoutManager from
+         * an inflated layout resource.
+         *
+         * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+         * you must also override
+         * {@link #checkLayoutParams(LayoutParams)},
+         * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+         * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+         *
+         * @param c Context for obtaining styled attributes
+         * @param attrs AttributeSet describing the supplied arguments
+         * @return a new LayoutParams object
+         */
+        public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
+            return new LayoutParams(c, attrs);
+        }
+
+        /**
+         * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.
+         * The default implementation does nothing and returns 0.
+         *
+         * @param dx            distance to scroll by in pixels. X increases as scroll position
+         *                      approaches the right.
+         * @param recycler      Recycler to use for fetching potentially cached views for a
+         *                      position
+         * @param state         Transient state of RecyclerView
+         * @return The actual distance scrolled. The return value will be negative if dx was
+         * negative and scrolling proceeeded in that direction.
+         * <code>Math.abs(result)</code> may be less than dx if a boundary was reached.
+         */
+        public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
+            return 0;
+        }
+
+        /**
+         * Scroll vertically by dy pixels in screen coordinates and return the distance traveled.
+         * The default implementation does nothing and returns 0.
+         *
+         * @param dy            distance to scroll in pixels. Y increases as scroll position
+         *                      approaches the bottom.
+         * @param recycler      Recycler to use for fetching potentially cached views for a
+         *                      position
+         * @param state         Transient state of RecyclerView
+         * @return The actual distance scrolled. The return value will be negative if dy was
+         * negative and scrolling proceeeded in that direction.
+         * <code>Math.abs(result)</code> may be less than dy if a boundary was reached.
+         */
+        public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
+            return 0;
+        }
+
+        /**
+         * Query if horizontal scrolling is currently supported. The default implementation
+         * returns false.
+         *
+         * @return True if this LayoutManager can scroll the current contents horizontally
+         */
+        public boolean canScrollHorizontally() {
+            return false;
+        }
+
+        /**
+         * Query if vertical scrolling is currently supported. The default implementation
+         * returns false.
+         *
+         * @return True if this LayoutManager can scroll the current contents vertically
+         */
+        public boolean canScrollVertically() {
+            return false;
+        }
+
+        /**
+         * Scroll to the specified adapter position.
+         *
+         * Actual position of the item on the screen depends on the LayoutManager implementation.
+         * @param position Scroll to this adapter position.
+         */
+        public void scrollToPosition(int position) {
+            if (DEBUG) {
+                Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract");
+            }
+        }
+
+        /**
+         * <p>Smooth scroll to the specified adapter position.</p>
+         * <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
+         * instance and call {@link #startSmoothScroll(SmoothScroller)}.
+         * </p>
+         * @param recyclerView The RecyclerView to which this layout manager is attached
+         * @param state    Current State of RecyclerView
+         * @param position Scroll to this adapter position.
+         */
+        public void smoothScrollToPosition(RecyclerView recyclerView, State state,
+                int position) {
+            Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
+        }
+
+        /**
+         * <p>Starts a smooth scroll using the provided SmoothScroller.</p>
+         * <p>Calling this method will cancel any previous smooth scroll request.</p>
+         * @param smoothScroller Instance which defines how smooth scroll should be animated
+         */
+        public void startSmoothScroll(SmoothScroller smoothScroller) {
+            if (mSmoothScroller != null && smoothScroller != mSmoothScroller
+                    && mSmoothScroller.isRunning()) {
+                mSmoothScroller.stop();
+            }
+            mSmoothScroller = smoothScroller;
+            mSmoothScroller.start(mRecyclerView, this);
+        }
+
+        /**
+         * @return true if RecycylerView is currently in the state of smooth scrolling.
+         */
+        public boolean isSmoothScrolling() {
+            return mSmoothScroller != null && mSmoothScroller.isRunning();
+        }
+
+
+        /**
+         * Returns the resolved layout direction for this RecyclerView.
+         *
+         * @return {@link android.view.View#LAYOUT_DIRECTION_RTL} if the layout
+         * direction is RTL or returns
+         * {@link android.view.View#LAYOUT_DIRECTION_LTR} if the layout direction
+         * is not RTL.
+         */
+        public int getLayoutDirection() {
+            return mRecyclerView.getLayoutDirection();
+        }
+
+        /**
+         * Ends all animations on the view created by the {@link ItemAnimator}.
+         *
+         * @param view The View for which the animations should be ended.
+         * @see RecyclerView.ItemAnimator#endAnimations()
+         */
+        public void endAnimation(View view) {
+            if (mRecyclerView.mItemAnimator != null) {
+                mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view));
+            }
+        }
+
+        /**
+         * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
+         * to the layout that is known to be going away, either because it has been
+         * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
+         * visible portion of the container but is being laid out in order to inform RecyclerView
+         * in how to animate the item out of view.
+         * <p>
+         * Views added via this method are going to be invisible to LayoutManager after the
+         * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
+         * or won't be included in {@link #getChildCount()} method.
+         *
+         * @param child View to add and then remove with animation.
+         */
+        public void addDisappearingView(View child) {
+            addDisappearingView(child, -1);
+        }
+
+        /**
+         * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
+         * to the layout that is known to be going away, either because it has been
+         * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
+         * visible portion of the container but is being laid out in order to inform RecyclerView
+         * in how to animate the item out of view.
+         * <p>
+         * Views added via this method are going to be invisible to LayoutManager after the
+         * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
+         * or won't be included in {@link #getChildCount()} method.
+         *
+         * @param child View to add and then remove with animation.
+         * @param index Index of the view.
+         */
+        public void addDisappearingView(View child, int index) {
+            addViewInt(child, index, true);
+        }
+
+        /**
+         * Add a view to the currently attached RecyclerView if needed. LayoutManagers should
+         * use this method to add views obtained from a {@link Recycler} using
+         * {@link Recycler#getViewForPosition(int)}.
+         *
+         * @param child View to add
+         */
+        public void addView(View child) {
+            addView(child, -1);
+        }
+
+        /**
+         * Add a view to the currently attached RecyclerView if needed. LayoutManagers should
+         * use this method to add views obtained from a {@link Recycler} using
+         * {@link Recycler#getViewForPosition(int)}.
+         *
+         * @param child View to add
+         * @param index Index to add child at
+         */
+        public void addView(View child, int index) {
+            addViewInt(child, index, false);
+        }
+
+        private void addViewInt(View child, int index, boolean disappearing) {
+            final ViewHolder holder = getChildViewHolderInt(child);
+            if (disappearing || holder.isRemoved()) {
+                // these views will be hidden at the end of the layout pass.
+                mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
+            } else {
+                // This may look like unnecessary but may happen if layout manager supports
+                // predictive layouts and adapter removed then re-added the same item.
+                // In this case, added version will be visible in the post layout (because add is
+                // deferred) but RV will still bind it to the same View.
+                // So if a View re-appears in post layout pass, remove it from disappearing list.
+                mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
+            }
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (holder.wasReturnedFromScrap() || holder.isScrap()) {
+                if (holder.isScrap()) {
+                    holder.unScrap();
+                } else {
+                    holder.clearReturnedFromScrapFlag();
+                }
+                mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
+                if (DISPATCH_TEMP_DETACH) {
+                    child.dispatchFinishTemporaryDetach();
+                }
+            } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
+                // ensure in correct position
+                int currentIndex = mChildHelper.indexOfChild(child);
+                if (index == -1) {
+                    index = mChildHelper.getChildCount();
+                }
+                if (currentIndex == -1) {
+                    throw new IllegalStateException("Added View has RecyclerView as parent but"
+                            + " view is not a real child. Unfiltered index:"
+                            + mRecyclerView.indexOfChild(child));
+                }
+                if (currentIndex != index) {
+                    mRecyclerView.mLayout.moveView(currentIndex, index);
+                }
+            } else {
+                mChildHelper.addView(child, index, false);
+                lp.mInsetsDirty = true;
+                if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
+                    mSmoothScroller.onChildAttachedToWindow(child);
+                }
+            }
+            if (lp.mPendingInvalidate) {
+                if (DEBUG) {
+                    Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
+                }
+                holder.itemView.invalidate();
+                lp.mPendingInvalidate = false;
+            }
+        }
+
+        /**
+         * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
+         * use this method to completely remove a child view that is no longer needed.
+         * LayoutManagers should strongly consider recycling removed views using
+         * {@link Recycler#recycleView(android.view.View)}.
+         *
+         * @param child View to remove
+         */
+        public void removeView(View child) {
+            mChildHelper.removeView(child);
+        }
+
+        /**
+         * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
+         * use this method to completely remove a child view that is no longer needed.
+         * LayoutManagers should strongly consider recycling removed views using
+         * {@link Recycler#recycleView(android.view.View)}.
+         *
+         * @param index Index of the child view to remove
+         */
+        public void removeViewAt(int index) {
+            final View child = getChildAt(index);
+            if (child != null) {
+                mChildHelper.removeViewAt(index);
+            }
+        }
+
+        /**
+         * Remove all views from the currently attached RecyclerView. This will not recycle
+         * any of the affected views; the LayoutManager is responsible for doing so if desired.
+         */
+        public void removeAllViews() {
+            // Only remove non-animating views
+            final int childCount = getChildCount();
+            for (int i = childCount - 1; i >= 0; i--) {
+                mChildHelper.removeViewAt(i);
+            }
+        }
+
+        /**
+         * Returns offset of the RecyclerView's text baseline from the its top boundary.
+         *
+         * @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if
+         * there is no baseline.
+         */
+        public int getBaseline() {
+            return -1;
+        }
+
+        /**
+         * Returns the adapter position of the item represented by the given View. This does not
+         * contain any adapter changes that might have happened after the last layout.
+         *
+         * @param view The view to query
+         * @return The adapter position of the item which is rendered by this View.
+         */
+        public int getPosition(View view) {
+            return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
+        }
+
+        /**
+         * Returns the View type defined by the adapter.
+         *
+         * @param view The view to query
+         * @return The type of the view assigned by the adapter.
+         */
+        public int getItemViewType(View view) {
+            return getChildViewHolderInt(view).getItemViewType();
+        }
+
+        /**
+         * Traverses the ancestors of the given view and returns the item view that contains it
+         * and also a direct child of the LayoutManager.
+         * <p>
+         * Note that this method may return null if the view is a child of the RecyclerView but
+         * not a child of the LayoutManager (e.g. running a disappear animation).
+         *
+         * @param view The view that is a descendant of the LayoutManager.
+         *
+         * @return The direct child of the LayoutManager which contains the given view or null if
+         * the provided view is not a descendant of this LayoutManager.
+         *
+         * @see RecyclerView#getChildViewHolder(View)
+         * @see RecyclerView#findContainingViewHolder(View)
+         */
+        @Nullable
+        public View findContainingItemView(View view) {
+            if (mRecyclerView == null) {
+                return null;
+            }
+            View found = mRecyclerView.findContainingItemView(view);
+            if (found == null) {
+                return null;
+            }
+            if (mChildHelper.isHidden(found)) {
+                return null;
+            }
+            return found;
+        }
+
+        /**
+         * Finds the view which represents the given adapter position.
+         * <p>
+         * This method traverses each child since it has no information about child order.
+         * Override this method to improve performance if your LayoutManager keeps data about
+         * child views.
+         * <p>
+         * If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method.
+         *
+         * @param position Position of the item in adapter
+         * @return The child view that represents the given position or null if the position is not
+         * laid out
+         */
+        public View findViewByPosition(int position) {
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                ViewHolder vh = getChildViewHolderInt(child);
+                if (vh == null) {
+                    continue;
+                }
+                if (vh.getLayoutPosition() == position && !vh.shouldIgnore()
+                        && (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) {
+                    return child;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Temporarily detach a child view.
+         *
+         * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
+         * views currently attached to the RecyclerView. Generally LayoutManager implementations
+         * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
+         * so that the detached view may be rebound and reused.</p>
+         *
+         * <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
+         * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
+         * or {@link #removeDetachedView(android.view.View) fully remove} the detached view
+         * before the LayoutManager entry point method called by RecyclerView returns.</p>
+         *
+         * @param child Child to detach
+         */
+        public void detachView(View child) {
+            final int ind = mChildHelper.indexOfChild(child);
+            if (ind >= 0) {
+                detachViewInternal(ind, child);
+            }
+        }
+
+        /**
+         * Temporarily detach a child view.
+         *
+         * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
+         * views currently attached to the RecyclerView. Generally LayoutManager implementations
+         * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
+         * so that the detached view may be rebound and reused.</p>
+         *
+         * <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
+         * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
+         * or {@link #removeDetachedView(android.view.View) fully remove} the detached view
+         * before the LayoutManager entry point method called by RecyclerView returns.</p>
+         *
+         * @param index Index of the child to detach
+         */
+        public void detachViewAt(int index) {
+            detachViewInternal(index, getChildAt(index));
+        }
+
+        private void detachViewInternal(int index, View view) {
+            if (DISPATCH_TEMP_DETACH) {
+                view.dispatchStartTemporaryDetach();
+            }
+            mChildHelper.detachViewFromParent(index);
+        }
+
+        /**
+         * Reattach a previously {@link #detachView(android.view.View) detached} view.
+         * This method should not be used to reattach views that were previously
+         * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}  scrapped}.
+         *
+         * @param child Child to reattach
+         * @param index Intended child index for child
+         * @param lp LayoutParams for child
+         */
+        public void attachView(View child, int index, LayoutParams lp) {
+            ViewHolder vh = getChildViewHolderInt(child);
+            if (vh.isRemoved()) {
+                mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh);
+            } else {
+                mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh);
+            }
+            mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved());
+            if (DISPATCH_TEMP_DETACH)  {
+                child.dispatchFinishTemporaryDetach();
+            }
+        }
+
+        /**
+         * Reattach a previously {@link #detachView(android.view.View) detached} view.
+         * This method should not be used to reattach views that were previously
+         * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}  scrapped}.
+         *
+         * @param child Child to reattach
+         * @param index Intended child index for child
+         */
+        public void attachView(View child, int index) {
+            attachView(child, index, (LayoutParams) child.getLayoutParams());
+        }
+
+        /**
+         * Reattach a previously {@link #detachView(android.view.View) detached} view.
+         * This method should not be used to reattach views that were previously
+         * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}  scrapped}.
+         *
+         * @param child Child to reattach
+         */
+        public void attachView(View child) {
+            attachView(child, -1);
+        }
+
+        /**
+         * Finish removing a view that was previously temporarily
+         * {@link #detachView(android.view.View) detached}.
+         *
+         * @param child Detached child to remove
+         */
+        public void removeDetachedView(View child) {
+            mRecyclerView.removeDetachedView(child, false);
+        }
+
+        /**
+         * Moves a View from one position to another.
+         *
+         * @param fromIndex The View's initial index
+         * @param toIndex The View's target index
+         */
+        public void moveView(int fromIndex, int toIndex) {
+            View view = getChildAt(fromIndex);
+            if (view == null) {
+                throw new IllegalArgumentException("Cannot move a child from non-existing index:"
+                        + fromIndex);
+            }
+            detachViewAt(fromIndex);
+            attachView(view, toIndex);
+        }
+
+        /**
+         * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
+         *
+         * <p>Scrapping a view allows it to be rebound and reused to show updated or
+         * different data.</p>
+         *
+         * @param child Child to detach and scrap
+         * @param recycler Recycler to deposit the new scrap view into
+         */
+        public void detachAndScrapView(View child, Recycler recycler) {
+            int index = mChildHelper.indexOfChild(child);
+            scrapOrRecycleView(recycler, index, child);
+        }
+
+        /**
+         * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
+         *
+         * <p>Scrapping a view allows it to be rebound and reused to show updated or
+         * different data.</p>
+         *
+         * @param index Index of child to detach and scrap
+         * @param recycler Recycler to deposit the new scrap view into
+         */
+        public void detachAndScrapViewAt(int index, Recycler recycler) {
+            final View child = getChildAt(index);
+            scrapOrRecycleView(recycler, index, child);
+        }
+
+        /**
+         * Remove a child view and recycle it using the given Recycler.
+         *
+         * @param child Child to remove and recycle
+         * @param recycler Recycler to use to recycle child
+         */
+        public void removeAndRecycleView(View child, Recycler recycler) {
+            removeView(child);
+            recycler.recycleView(child);
+        }
+
+        /**
+         * Remove a child view and recycle it using the given Recycler.
+         *
+         * @param index Index of child to remove and recycle
+         * @param recycler Recycler to use to recycle child
+         */
+        public void removeAndRecycleViewAt(int index, Recycler recycler) {
+            final View view = getChildAt(index);
+            removeViewAt(index);
+            recycler.recycleView(view);
+        }
+
+        /**
+         * Return the current number of child views attached to the parent RecyclerView.
+         * This does not include child views that were temporarily detached and/or scrapped.
+         *
+         * @return Number of attached children
+         */
+        public int getChildCount() {
+            return mChildHelper != null ? mChildHelper.getChildCount() : 0;
+        }
+
+        /**
+         * Return the child view at the given index
+         * @param index Index of child to return
+         * @return Child view at index
+         */
+        public View getChildAt(int index) {
+            return mChildHelper != null ? mChildHelper.getChildAt(index) : null;
+        }
+
+        /**
+         * Return the width measurement spec mode of the RecyclerView.
+         * <p>
+         * This value is set only if the LayoutManager opts into the auto measure api via
+         * {@link #setAutoMeasureEnabled(boolean)}.
+         * <p>
+         * When RecyclerView is running a layout, this value is always set to
+         * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
+         *
+         * @return Width measure spec mode.
+         *
+         * @see View.MeasureSpec#getMode(int)
+         * @see View#onMeasure(int, int)
+         */
+        public int getWidthMode() {
+            return mWidthMode;
+        }
+
+        /**
+         * Return the height measurement spec mode of the RecyclerView.
+         * <p>
+         * This value is set only if the LayoutManager opts into the auto measure api via
+         * {@link #setAutoMeasureEnabled(boolean)}.
+         * <p>
+         * When RecyclerView is running a layout, this value is always set to
+         * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
+         *
+         * @return Height measure spec mode.
+         *
+         * @see View.MeasureSpec#getMode(int)
+         * @see View#onMeasure(int, int)
+         */
+        public int getHeightMode() {
+            return mHeightMode;
+        }
+
+        /**
+         * Return the width of the parent RecyclerView
+         *
+         * @return Width in pixels
+         */
+        public int getWidth() {
+            return mWidth;
+        }
+
+        /**
+         * Return the height of the parent RecyclerView
+         *
+         * @return Height in pixels
+         */
+        public int getHeight() {
+            return mHeight;
+        }
+
+        /**
+         * Return the left padding of the parent RecyclerView
+         *
+         * @return Padding in pixels
+         */
+        public int getPaddingLeft() {
+            return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0;
+        }
+
+        /**
+         * Return the top padding of the parent RecyclerView
+         *
+         * @return Padding in pixels
+         */
+        public int getPaddingTop() {
+            return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0;
+        }
+
+        /**
+         * Return the right padding of the parent RecyclerView
+         *
+         * @return Padding in pixels
+         */
+        public int getPaddingRight() {
+            return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0;
+        }
+
+        /**
+         * Return the bottom padding of the parent RecyclerView
+         *
+         * @return Padding in pixels
+         */
+        public int getPaddingBottom() {
+            return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0;
+        }
+
+        /**
+         * Return the start padding of the parent RecyclerView
+         *
+         * @return Padding in pixels
+         */
+        public int getPaddingStart() {
+            return mRecyclerView != null ? mRecyclerView.getPaddingStart() : 0;
+        }
+
+        /**
+         * Return the end padding of the parent RecyclerView
+         *
+         * @return Padding in pixels
+         */
+        public int getPaddingEnd() {
+            return mRecyclerView != null ? mRecyclerView.getPaddingEnd() : 0;
+        }
+
+        /**
+         * Returns true if the RecyclerView this LayoutManager is bound to has focus.
+         *
+         * @return True if the RecyclerView has focus, false otherwise.
+         * @see View#isFocused()
+         */
+        public boolean isFocused() {
+            return mRecyclerView != null && mRecyclerView.isFocused();
+        }
+
+        /**
+         * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus.
+         *
+         * @return true if the RecyclerView has or contains focus
+         * @see View#hasFocus()
+         */
+        public boolean hasFocus() {
+            return mRecyclerView != null && mRecyclerView.hasFocus();
+        }
+
+        /**
+         * Returns the item View which has or contains focus.
+         *
+         * @return A direct child of RecyclerView which has focus or contains the focused child.
+         */
+        public View getFocusedChild() {
+            if (mRecyclerView == null) {
+                return null;
+            }
+            final View focused = mRecyclerView.getFocusedChild();
+            if (focused == null || mChildHelper.isHidden(focused)) {
+                return null;
+            }
+            return focused;
+        }
+
+        /**
+         * Returns the number of items in the adapter bound to the parent RecyclerView.
+         * <p>
+         * Note that this number is not necessarily equal to
+         * {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is
+         * available, you should use {@link State#getItemCount() State#getItemCount()} instead.
+         * For more details, check the documentation for
+         * {@link State#getItemCount() State#getItemCount()}.
+         *
+         * @return The number of items in the bound adapter
+         * @see State#getItemCount()
+         */
+        public int getItemCount() {
+            final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null;
+            return a != null ? a.getItemCount() : 0;
+        }
+
+        /**
+         * Offset all child views attached to the parent RecyclerView by dx pixels along
+         * the horizontal axis.
+         *
+         * @param dx Pixels to offset by
+         */
+        public void offsetChildrenHorizontal(int dx) {
+            if (mRecyclerView != null) {
+                mRecyclerView.offsetChildrenHorizontal(dx);
+            }
+        }
+
+        /**
+         * Offset all child views attached to the parent RecyclerView by dy pixels along
+         * the vertical axis.
+         *
+         * @param dy Pixels to offset by
+         */
+        public void offsetChildrenVertical(int dy) {
+            if (mRecyclerView != null) {
+                mRecyclerView.offsetChildrenVertical(dy);
+            }
+        }
+
+        /**
+         * Flags a view so that it will not be scrapped or recycled.
+         * <p>
+         * Scope of ignoring a child is strictly restricted to position tracking, scrapping and
+         * recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child
+         * whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not
+         * ignore the child.
+         * <p>
+         * Before this child can be recycled again, you have to call
+         * {@link #stopIgnoringView(View)}.
+         * <p>
+         * You can call this method only if your LayoutManger is in onLayout or onScroll callback.
+         *
+         * @param view View to ignore.
+         * @see #stopIgnoringView(View)
+         */
+        public void ignoreView(View view) {
+            if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) {
+                // checking this because calling this method on a recycled or detached view may
+                // cause loss of state.
+                throw new IllegalArgumentException("View should be fully attached to be ignored");
+            }
+            final ViewHolder vh = getChildViewHolderInt(view);
+            vh.addFlags(ViewHolder.FLAG_IGNORE);
+            mRecyclerView.mViewInfoStore.removeViewHolder(vh);
+        }
+
+        /**
+         * View can be scrapped and recycled again.
+         * <p>
+         * Note that calling this method removes all information in the view holder.
+         * <p>
+         * You can call this method only if your LayoutManger is in onLayout or onScroll callback.
+         *
+         * @param view View to ignore.
+         */
+        public void stopIgnoringView(View view) {
+            final ViewHolder vh = getChildViewHolderInt(view);
+            vh.stopIgnoring();
+            vh.resetInternal();
+            vh.addFlags(ViewHolder.FLAG_INVALID);
+        }
+
+        /**
+         * Temporarily detach and scrap all currently attached child views. Views will be scrapped
+         * into the given Recycler. The Recycler may prefer to reuse scrap views before
+         * other views that were previously recycled.
+         *
+         * @param recycler Recycler to scrap views into
+         */
+        public void detachAndScrapAttachedViews(Recycler recycler) {
+            final int childCount = getChildCount();
+            for (int i = childCount - 1; i >= 0; i--) {
+                final View v = getChildAt(i);
+                scrapOrRecycleView(recycler, i, v);
+            }
+        }
+
+        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
+            final ViewHolder viewHolder = getChildViewHolderInt(view);
+            if (viewHolder.shouldIgnore()) {
+                if (DEBUG) {
+                    Log.d(TAG, "ignoring view " + viewHolder);
+                }
+                return;
+            }
+            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
+                    && !mRecyclerView.mAdapter.hasStableIds()) {
+                removeViewAt(index);
+                recycler.recycleViewHolderInternal(viewHolder);
+            } else {
+                detachViewAt(index);
+                recycler.scrapView(view);
+                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
+            }
+        }
+
+        /**
+         * Recycles the scrapped views.
+         * <p>
+         * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is
+         * the expected behavior if scrapped views are used for animations. Otherwise, we need to
+         * call remove and invalidate RecyclerView to ensure UI update.
+         *
+         * @param recycler Recycler
+         */
+        void removeAndRecycleScrapInt(Recycler recycler) {
+            final int scrapCount = recycler.getScrapCount();
+            // Loop backward, recycler might be changed by removeDetachedView()
+            for (int i = scrapCount - 1; i >= 0; i--) {
+                final View scrap = recycler.getScrapViewAt(i);
+                final ViewHolder vh = getChildViewHolderInt(scrap);
+                if (vh.shouldIgnore()) {
+                    continue;
+                }
+                // If the scrap view is animating, we need to cancel them first. If we cancel it
+                // here, ItemAnimator callback may recycle it which will cause double recycling.
+                // To avoid this, we mark it as not recycleable before calling the item animator.
+                // Since removeDetachedView calls a user API, a common mistake (ending animations on
+                // the view) may recycle it too, so we guard it before we call user APIs.
+                vh.setIsRecyclable(false);
+                if (vh.isTmpDetached()) {
+                    mRecyclerView.removeDetachedView(scrap, false);
+                }
+                if (mRecyclerView.mItemAnimator != null) {
+                    mRecyclerView.mItemAnimator.endAnimation(vh);
+                }
+                vh.setIsRecyclable(true);
+                recycler.quickRecycleScrapView(scrap);
+            }
+            recycler.clearScrap();
+            if (scrapCount > 0) {
+                mRecyclerView.invalidate();
+            }
+        }
+
+
+        /**
+         * Measure a child view using standard measurement policy, taking the padding
+         * of the parent RecyclerView and any added item decorations into account.
+         *
+         * <p>If the RecyclerView can be scrolled in either dimension the caller may
+         * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
+         *
+         * @param child Child view to measure
+         * @param widthUsed Width in pixels currently consumed by other views, if relevant
+         * @param heightUsed Height in pixels currently consumed by other views, if relevant
+         */
+        public void measureChild(View child, int widthUsed, int heightUsed) {
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+            widthUsed += insets.left + insets.right;
+            heightUsed += insets.top + insets.bottom;
+            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
+                    getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
+                    canScrollHorizontally());
+            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
+                    getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
+                    canScrollVertically());
+            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
+                child.measure(widthSpec, heightSpec);
+            }
+        }
+
+        /**
+         * RecyclerView internally does its own View measurement caching which should help with
+         * WRAP_CONTENT.
+         * <p>
+         * Use this method if the View is already measured once in this layout pass.
+         */
+        boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
+            return !mMeasurementCacheEnabled
+                    || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width)
+                    || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height);
+        }
+
+        // we may consider making this public
+        /**
+         * RecyclerView internally does its own View measurement caching which should help with
+         * WRAP_CONTENT.
+         * <p>
+         * Use this method if the View is not yet measured and you need to decide whether to
+         * measure this View or not.
+         */
+        boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
+            return child.isLayoutRequested()
+                    || !mMeasurementCacheEnabled
+                    || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width)
+                    || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height);
+        }
+
+        /**
+         * In addition to the View Framework's measurement cache, RecyclerView uses its own
+         * additional measurement cache for its children to avoid re-measuring them when not
+         * necessary. It is on by default but it can be turned off via
+         * {@link #setMeasurementCacheEnabled(boolean)}.
+         *
+         * @return True if measurement cache is enabled, false otherwise.
+         *
+         * @see #setMeasurementCacheEnabled(boolean)
+         */
+        public boolean isMeasurementCacheEnabled() {
+            return mMeasurementCacheEnabled;
+        }
+
+        /**
+         * Sets whether RecyclerView should use its own measurement cache for the children. This is
+         * a more aggressive cache than the framework uses.
+         *
+         * @param measurementCacheEnabled True to enable the measurement cache, false otherwise.
+         *
+         * @see #isMeasurementCacheEnabled()
+         */
+        public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) {
+            mMeasurementCacheEnabled = measurementCacheEnabled;
+        }
+
+        private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) {
+            final int specMode = MeasureSpec.getMode(spec);
+            final int specSize = MeasureSpec.getSize(spec);
+            if (dimension > 0 && childSize != dimension) {
+                return false;
+            }
+            switch (specMode) {
+                case MeasureSpec.UNSPECIFIED:
+                    return true;
+                case MeasureSpec.AT_MOST:
+                    return specSize >= childSize;
+                case MeasureSpec.EXACTLY:
+                    return  specSize == childSize;
+            }
+            return false;
+        }
+
+        /**
+         * Measure a child view using standard measurement policy, taking the padding
+         * of the parent RecyclerView, any added item decorations and the child margins
+         * into account.
+         *
+         * <p>If the RecyclerView can be scrolled in either dimension the caller may
+         * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
+         *
+         * @param child Child view to measure
+         * @param widthUsed Width in pixels currently consumed by other views, if relevant
+         * @param heightUsed Height in pixels currently consumed by other views, if relevant
+         */
+        public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+            widthUsed += insets.left + insets.right;
+            heightUsed += insets.top + insets.bottom;
+
+            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
+                    getPaddingLeft() + getPaddingRight()
+                            + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
+                    canScrollHorizontally());
+            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
+                    getPaddingTop() + getPaddingBottom()
+                            + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
+                    canScrollVertically());
+            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
+                child.measure(widthSpec, heightSpec);
+            }
+        }
+
+        /**
+         * Calculate a MeasureSpec value for measuring a child view in one dimension.
+         *
+         * @param parentSize Size of the parent view where the child will be placed
+         * @param padding Total space currently consumed by other elements of the parent
+         * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
+         *                       Generally obtained from the child view's LayoutParams
+         * @param canScroll true if the parent RecyclerView can scroll in this dimension
+         *
+         * @return a MeasureSpec value for the child view
+         * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)}
+         */
+        @Deprecated
+        public static int getChildMeasureSpec(int parentSize, int padding, int childDimension,
+                boolean canScroll) {
+            int size = Math.max(0, parentSize - padding);
+            int resultSize = 0;
+            int resultMode = 0;
+            if (canScroll) {
+                if (childDimension >= 0) {
+                    resultSize = childDimension;
+                    resultMode = MeasureSpec.EXACTLY;
+                } else {
+                    // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
+                    // instead using UNSPECIFIED.
+                    resultSize = 0;
+                    resultMode = MeasureSpec.UNSPECIFIED;
+                }
+            } else {
+                if (childDimension >= 0) {
+                    resultSize = childDimension;
+                    resultMode = MeasureSpec.EXACTLY;
+                } else if (childDimension == LayoutParams.MATCH_PARENT) {
+                    resultSize = size;
+                    // TODO this should be my spec.
+                    resultMode = MeasureSpec.EXACTLY;
+                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+                    resultSize = size;
+                    resultMode = MeasureSpec.AT_MOST;
+                }
+            }
+            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+        }
+
+        /**
+         * Calculate a MeasureSpec value for measuring a child view in one dimension.
+         *
+         * @param parentSize Size of the parent view where the child will be placed
+         * @param parentMode The measurement spec mode of the parent
+         * @param padding Total space currently consumed by other elements of parent
+         * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
+         *                       Generally obtained from the child view's LayoutParams
+         * @param canScroll true if the parent RecyclerView can scroll in this dimension
+         *
+         * @return a MeasureSpec value for the child view
+         */
+        public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
+                int childDimension, boolean canScroll) {
+            int size = Math.max(0, parentSize - padding);
+            int resultSize = 0;
+            int resultMode = 0;
+            if (canScroll) {
+                if (childDimension >= 0) {
+                    resultSize = childDimension;
+                    resultMode = MeasureSpec.EXACTLY;
+                } else if (childDimension == LayoutParams.MATCH_PARENT) {
+                    switch (parentMode) {
+                        case MeasureSpec.AT_MOST:
+                        case MeasureSpec.EXACTLY:
+                            resultSize = size;
+                            resultMode = parentMode;
+                            break;
+                        case MeasureSpec.UNSPECIFIED:
+                            resultSize = 0;
+                            resultMode = MeasureSpec.UNSPECIFIED;
+                            break;
+                    }
+                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+                    resultSize = 0;
+                    resultMode = MeasureSpec.UNSPECIFIED;
+                }
+            } else {
+                if (childDimension >= 0) {
+                    resultSize = childDimension;
+                    resultMode = MeasureSpec.EXACTLY;
+                } else if (childDimension == LayoutParams.MATCH_PARENT) {
+                    resultSize = size;
+                    resultMode = parentMode;
+                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+                    resultSize = size;
+                    if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
+                        resultMode = MeasureSpec.AT_MOST;
+                    } else {
+                        resultMode = MeasureSpec.UNSPECIFIED;
+                    }
+
+                }
+            }
+            //noinspection WrongConstant
+            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+        }
+
+        /**
+         * Returns the measured width of the given child, plus the additional size of
+         * any insets applied by {@link ItemDecoration ItemDecorations}.
+         *
+         * @param child Child view to query
+         * @return child's measured width plus <code>ItemDecoration</code> insets
+         *
+         * @see View#getMeasuredWidth()
+         */
+        public int getDecoratedMeasuredWidth(View child) {
+            final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+            return child.getMeasuredWidth() + insets.left + insets.right;
+        }
+
+        /**
+         * Returns the measured height of the given child, plus the additional size of
+         * any insets applied by {@link ItemDecoration ItemDecorations}.
+         *
+         * @param child Child view to query
+         * @return child's measured height plus <code>ItemDecoration</code> insets
+         *
+         * @see View#getMeasuredHeight()
+         */
+        public int getDecoratedMeasuredHeight(View child) {
+            final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+            return child.getMeasuredHeight() + insets.top + insets.bottom;
+        }
+
+        /**
+         * Lay out the given child view within the RecyclerView using coordinates that
+         * include any current {@link ItemDecoration ItemDecorations}.
+         *
+         * <p>LayoutManagers should prefer working in sizes and coordinates that include
+         * item decoration insets whenever possible. This allows the LayoutManager to effectively
+         * ignore decoration insets within measurement and layout code. See the following
+         * methods:</p>
+         * <ul>
+         *     <li>{@link #layoutDecoratedWithMargins(View, int, int, int, int)}</li>
+         *     <li>{@link #getDecoratedBoundsWithMargins(View, Rect)}</li>
+         *     <li>{@link #measureChild(View, int, int)}</li>
+         *     <li>{@link #measureChildWithMargins(View, int, int)}</li>
+         *     <li>{@link #getDecoratedLeft(View)}</li>
+         *     <li>{@link #getDecoratedTop(View)}</li>
+         *     <li>{@link #getDecoratedRight(View)}</li>
+         *     <li>{@link #getDecoratedBottom(View)}</li>
+         *     <li>{@link #getDecoratedMeasuredWidth(View)}</li>
+         *     <li>{@link #getDecoratedMeasuredHeight(View)}</li>
+         * </ul>
+         *
+         * @param child Child to lay out
+         * @param left Left edge, with item decoration insets included
+         * @param top Top edge, with item decoration insets included
+         * @param right Right edge, with item decoration insets included
+         * @param bottom Bottom edge, with item decoration insets included
+         *
+         * @see View#layout(int, int, int, int)
+         * @see #layoutDecoratedWithMargins(View, int, int, int, int)
+         */
+        public void layoutDecorated(View child, int left, int top, int right, int bottom) {
+            final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+            child.layout(left + insets.left, top + insets.top, right - insets.right,
+                    bottom - insets.bottom);
+        }
+
+        /**
+         * Lay out the given child view within the RecyclerView using coordinates that
+         * include any current {@link ItemDecoration ItemDecorations} and margins.
+         *
+         * <p>LayoutManagers should prefer working in sizes and coordinates that include
+         * item decoration insets whenever possible. This allows the LayoutManager to effectively
+         * ignore decoration insets within measurement and layout code. See the following
+         * methods:</p>
+         * <ul>
+         *     <li>{@link #layoutDecorated(View, int, int, int, int)}</li>
+         *     <li>{@link #measureChild(View, int, int)}</li>
+         *     <li>{@link #measureChildWithMargins(View, int, int)}</li>
+         *     <li>{@link #getDecoratedLeft(View)}</li>
+         *     <li>{@link #getDecoratedTop(View)}</li>
+         *     <li>{@link #getDecoratedRight(View)}</li>
+         *     <li>{@link #getDecoratedBottom(View)}</li>
+         *     <li>{@link #getDecoratedMeasuredWidth(View)}</li>
+         *     <li>{@link #getDecoratedMeasuredHeight(View)}</li>
+         * </ul>
+         *
+         * @param child Child to lay out
+         * @param left Left edge, with item decoration insets and left margin included
+         * @param top Top edge, with item decoration insets and top margin included
+         * @param right Right edge, with item decoration insets and right margin included
+         * @param bottom Bottom edge, with item decoration insets and bottom margin included
+         *
+         * @see View#layout(int, int, int, int)
+         * @see #layoutDecorated(View, int, int, int, int)
+         */
+        public void layoutDecoratedWithMargins(View child, int left, int top, int right,
+                int bottom) {
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            final Rect insets = lp.mDecorInsets;
+            child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
+                    right - insets.right - lp.rightMargin,
+                    bottom - insets.bottom - lp.bottomMargin);
+        }
+
+        /**
+         * Calculates the bounding box of the View while taking into account its matrix changes
+         * (translation, scale etc) with respect to the RecyclerView.
+         * <p>
+         * If {@code includeDecorInsets} is {@code true}, they are applied first before applying
+         * the View's matrix so that the decor offsets also go through the same transformation.
+         *
+         * @param child The ItemView whose bounding box should be calculated.
+         * @param includeDecorInsets True if the decor insets should be included in the bounding box
+         * @param out The rectangle into which the output will be written.
+         */
+        public void getTransformedBoundingBox(View child, boolean includeDecorInsets, Rect out) {
+            if (includeDecorInsets) {
+                Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+                out.set(-insets.left, -insets.top,
+                        child.getWidth() + insets.right, child.getHeight() + insets.bottom);
+            } else {
+                out.set(0, 0, child.getWidth(), child.getHeight());
+            }
+
+            if (mRecyclerView != null) {
+                final Matrix childMatrix = child.getMatrix();
+                if (childMatrix != null && !childMatrix.isIdentity()) {
+                    final RectF tempRectF = mRecyclerView.mTempRectF;
+                    tempRectF.set(out);
+                    childMatrix.mapRect(tempRectF);
+                    out.set(
+                            (int) Math.floor(tempRectF.left),
+                            (int) Math.floor(tempRectF.top),
+                            (int) Math.ceil(tempRectF.right),
+                            (int) Math.ceil(tempRectF.bottom)
+                    );
+                }
+            }
+            out.offset(child.getLeft(), child.getTop());
+        }
+
+        /**
+         * Returns the bounds of the view including its decoration and margins.
+         *
+         * @param view The view element to check
+         * @param outBounds A rect that will receive the bounds of the element including its
+         *                  decoration and margins.
+         */
+        public void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
+            RecyclerView.getDecoratedBoundsWithMarginsInt(view, outBounds);
+        }
+
+        /**
+         * Returns the left edge of the given child view within its parent, offset by any applied
+         * {@link ItemDecoration ItemDecorations}.
+         *
+         * @param child Child to query
+         * @return Child left edge with offsets applied
+         * @see #getLeftDecorationWidth(View)
+         */
+        public int getDecoratedLeft(View child) {
+            return child.getLeft() - getLeftDecorationWidth(child);
+        }
+
+        /**
+         * Returns the top edge of the given child view within its parent, offset by any applied
+         * {@link ItemDecoration ItemDecorations}.
+         *
+         * @param child Child to query
+         * @return Child top edge with offsets applied
+         * @see #getTopDecorationHeight(View)
+         */
+        public int getDecoratedTop(View child) {
+            return child.getTop() - getTopDecorationHeight(child);
+        }
+
+        /**
+         * Returns the right edge of the given child view within its parent, offset by any applied
+         * {@link ItemDecoration ItemDecorations}.
+         *
+         * @param child Child to query
+         * @return Child right edge with offsets applied
+         * @see #getRightDecorationWidth(View)
+         */
+        public int getDecoratedRight(View child) {
+            return child.getRight() + getRightDecorationWidth(child);
+        }
+
+        /**
+         * Returns the bottom edge of the given child view within its parent, offset by any applied
+         * {@link ItemDecoration ItemDecorations}.
+         *
+         * @param child Child to query
+         * @return Child bottom edge with offsets applied
+         * @see #getBottomDecorationHeight(View)
+         */
+        public int getDecoratedBottom(View child) {
+            return child.getBottom() + getBottomDecorationHeight(child);
+        }
+
+        /**
+         * Calculates the item decor insets applied to the given child and updates the provided
+         * Rect instance with the inset values.
+         * <ul>
+         *     <li>The Rect's left is set to the total width of left decorations.</li>
+         *     <li>The Rect's top is set to the total height of top decorations.</li>
+         *     <li>The Rect's right is set to the total width of right decorations.</li>
+         *     <li>The Rect's bottom is set to total height of bottom decorations.</li>
+         * </ul>
+         * <p>
+         * Note that item decorations are automatically calculated when one of the LayoutManager's
+         * measure child methods is called. If you need to measure the child with custom specs via
+         * {@link View#measure(int, int)}, you can use this method to get decorations.
+         *
+         * @param child The child view whose decorations should be calculated
+         * @param outRect The Rect to hold result values
+         */
+        public void calculateItemDecorationsForChild(View child, Rect outRect) {
+            if (mRecyclerView == null) {
+                outRect.set(0, 0, 0, 0);
+                return;
+            }
+            Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+            outRect.set(insets);
+        }
+
+        /**
+         * Returns the total height of item decorations applied to child's top.
+         * <p>
+         * Note that this value is not updated until the View is measured or
+         * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+         *
+         * @param child Child to query
+         * @return The total height of item decorations applied to the child's top.
+         * @see #getDecoratedTop(View)
+         * @see #calculateItemDecorationsForChild(View, Rect)
+         */
+        public int getTopDecorationHeight(View child) {
+            return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top;
+        }
+
+        /**
+         * Returns the total height of item decorations applied to child's bottom.
+         * <p>
+         * Note that this value is not updated until the View is measured or
+         * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+         *
+         * @param child Child to query
+         * @return The total height of item decorations applied to the child's bottom.
+         * @see #getDecoratedBottom(View)
+         * @see #calculateItemDecorationsForChild(View, Rect)
+         */
+        public int getBottomDecorationHeight(View child) {
+            return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom;
+        }
+
+        /**
+         * Returns the total width of item decorations applied to child's left.
+         * <p>
+         * Note that this value is not updated until the View is measured or
+         * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+         *
+         * @param child Child to query
+         * @return The total width of item decorations applied to the child's left.
+         * @see #getDecoratedLeft(View)
+         * @see #calculateItemDecorationsForChild(View, Rect)
+         */
+        public int getLeftDecorationWidth(View child) {
+            return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left;
+        }
+
+        /**
+         * Returns the total width of item decorations applied to child's right.
+         * <p>
+         * Note that this value is not updated until the View is measured or
+         * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+         *
+         * @param child Child to query
+         * @return The total width of item decorations applied to the child's right.
+         * @see #getDecoratedRight(View)
+         * @see #calculateItemDecorationsForChild(View, Rect)
+         */
+        public int getRightDecorationWidth(View child) {
+            return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right;
+        }
+
+        /**
+         * Called when searching for a focusable view in the given direction has failed
+         * for the current content of the RecyclerView.
+         *
+         * <p>This is the LayoutManager's opportunity to populate views in the given direction
+         * to fulfill the request if it can. The LayoutManager should attach and return
+         * the view to be focused. The default implementation returns null.</p>
+         *
+         * @param focused   The currently focused view
+         * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+         *                  {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+         *                  {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+         *                  or 0 for not applicable
+         * @param recycler  The recycler to use for obtaining views for currently offscreen items
+         * @param state     Transient state of RecyclerView
+         * @return The chosen view to be focused
+         */
+        @Nullable
+        public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
+                State state) {
+            return null;
+        }
+
+        /**
+         * This method gives a LayoutManager an opportunity to intercept the initial focus search
+         * before the default behavior of {@link FocusFinder} is used. If this method returns
+         * null FocusFinder will attempt to find a focusable child view. If it fails
+         * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)}
+         * will be called to give the LayoutManager an opportunity to add new views for items
+         * that did not have attached views representing them. The LayoutManager should not add
+         * or remove views from this method.
+         *
+         * @param focused The currently focused view
+         * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+         *                  {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+         *                  {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+         * @return A descendant view to focus or null to fall back to default behavior.
+         *         The default implementation returns null.
+         */
+        public View onInterceptFocusSearch(View focused, int direction) {
+            return null;
+        }
+
+        /**
+         * Called when a child of the RecyclerView wants a particular rectangle to be positioned
+         * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View,
+         * android.graphics.Rect, boolean)} for more details.
+         *
+         * <p>The base implementation will attempt to perform a standard programmatic scroll
+         * to bring the given rect into view, within the padded area of the RecyclerView.</p>
+         *
+         * @param child The direct child making the request.
+         * @param rect  The rectangle in the child's coordinates the child
+         *              wishes to be on the screen.
+         * @param immediate True to forbid animated or delayed scrolling,
+         *                  false otherwise
+         * @return Whether the group scrolled to handle the operation
+         */
+        public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
+                boolean immediate) {
+            final int parentLeft = getPaddingLeft();
+            final int parentTop = getPaddingTop();
+            final int parentRight = getWidth() - getPaddingRight();
+            final int parentBottom = getHeight() - getPaddingBottom();
+            final int childLeft = child.getLeft() + rect.left - child.getScrollX();
+            final int childTop = child.getTop() + rect.top - child.getScrollY();
+            final int childRight = childLeft + rect.width();
+            final int childBottom = childTop + rect.height();
+
+            final int offScreenLeft = Math.min(0, childLeft - parentLeft);
+            final int offScreenTop = Math.min(0, childTop - parentTop);
+            final int offScreenRight = Math.max(0, childRight - parentRight);
+            final int offScreenBottom = Math.max(0, childBottom - parentBottom);
+
+            // Favor the "start" layout direction over the end when bringing one side or the other
+            // of a large rect into view. If we decide to bring in end because start is already
+            // visible, limit the scroll such that start won't go out of bounds.
+            final int dx;
+            if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+                dx = offScreenRight != 0 ? offScreenRight
+                        : Math.max(offScreenLeft, childRight - parentRight);
+            } else {
+                dx = offScreenLeft != 0 ? offScreenLeft
+                        : Math.min(childLeft - parentLeft, offScreenRight);
+            }
+
+            // Favor bringing the top into view over the bottom. If top is already visible and
+            // we should scroll to make bottom visible, make sure top does not go out of bounds.
+            final int dy = offScreenTop != 0 ? offScreenTop
+                    : Math.min(childTop - parentTop, offScreenBottom);
+
+            if (dx != 0 || dy != 0) {
+                if (immediate) {
+                    parent.scrollBy(dx, dy);
+                } else {
+                    parent.smoothScrollBy(dx, dy);
+                }
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)}
+         */
+        @Deprecated
+        public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
+            // eat the request if we are in the middle of a scroll or layout
+            return isSmoothScrolling() || parent.isComputingLayout();
+        }
+
+        /**
+         * Called when a descendant view of the RecyclerView requests focus.
+         *
+         * <p>A LayoutManager wishing to keep focused views aligned in a specific
+         * portion of the view may implement that behavior in an override of this method.</p>
+         *
+         * <p>If the LayoutManager executes different behavior that should override the default
+         * behavior of scrolling the focused child on screen instead of running alongside it,
+         * this method should return true.</p>
+         *
+         * @param parent  The RecyclerView hosting this LayoutManager
+         * @param state   Current state of RecyclerView
+         * @param child   Direct child of the RecyclerView containing the newly focused view
+         * @param focused The newly focused view. This may be the same view as child or it may be
+         *                null
+         * @return true if the default scroll behavior should be suppressed
+         */
+        public boolean onRequestChildFocus(RecyclerView parent, State state, View child,
+                View focused) {
+            return onRequestChildFocus(parent, child, focused);
+        }
+
+        /**
+         * Called if the RecyclerView this LayoutManager is bound to has a different adapter set.
+         * The LayoutManager may use this opportunity to clear caches and configure state such
+         * that it can relayout appropriately with the new data and potentially new view types.
+         *
+         * <p>The default implementation removes all currently attached views.</p>
+         *
+         * @param oldAdapter The previous adapter instance. Will be null if there was previously no
+         *                   adapter.
+         * @param newAdapter The new adapter instance. Might be null if
+         *                   {@link #setAdapter(RecyclerView.Adapter)} is called with {@code null}.
+         */
+        public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
+        }
+
+        /**
+         * Called to populate focusable views within the RecyclerView.
+         *
+         * <p>The LayoutManager implementation should return <code>true</code> if the default
+         * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be
+         * suppressed.</p>
+         *
+         * <p>The default implementation returns <code>false</code> to trigger RecyclerView
+         * to fall back to the default ViewGroup behavior.</p>
+         *
+         * @param recyclerView The RecyclerView hosting this LayoutManager
+         * @param views List of output views. This method should add valid focusable views
+         *              to this list.
+         * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+         *                  {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+         *                  {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+         * @param focusableMode The type of focusables to be added.
+         *
+         * @return true to suppress the default behavior, false to add default focusables after
+         *         this method returns.
+         *
+         * @see #FOCUSABLES_ALL
+         * @see #FOCUSABLES_TOUCH_MODE
+         */
+        public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views,
+                int direction, int focusableMode) {
+            return false;
+        }
+
+        /**
+         * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
+         * detailed information on what has actually changed.
+         *
+         * @param recyclerView
+         */
+        public void onItemsChanged(RecyclerView recyclerView) {
+        }
+
+        /**
+         * Called when items have been added to the adapter. The LayoutManager may choose to
+         * requestLayout if the inserted items would require refreshing the currently visible set
+         * of child views. (e.g. currently empty space would be filled by appended items, etc.)
+         *
+         * @param recyclerView
+         * @param positionStart
+         * @param itemCount
+         */
+        public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+        }
+
+        /**
+         * Called when items have been removed from the adapter.
+         *
+         * @param recyclerView
+         * @param positionStart
+         * @param itemCount
+         */
+        public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+        }
+
+        /**
+         * Called when items have been changed in the adapter.
+         * To receive payload,  override {@link #onItemsUpdated(RecyclerView, int, int, Object)}
+         * instead, then this callback will not be invoked.
+         *
+         * @param recyclerView
+         * @param positionStart
+         * @param itemCount
+         */
+        public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
+        }
+
+        /**
+         * Called when items have been changed in the adapter and with optional payload.
+         * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}.
+         *
+         * @param recyclerView
+         * @param positionStart
+         * @param itemCount
+         * @param payload
+         */
+        public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
+                Object payload) {
+            onItemsUpdated(recyclerView, positionStart, itemCount);
+        }
+
+        /**
+         * Called when an item is moved withing the adapter.
+         * <p>
+         * Note that, an item may also change position in response to another ADD/REMOVE/MOVE
+         * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved}
+         * is called.
+         *
+         * @param recyclerView
+         * @param from
+         * @param to
+         * @param itemCount
+         */
+        public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
+
+        }
+
+
+        /**
+         * <p>Override this method if you want to support scroll bars.</p>
+         *
+         * <p>Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.</p>
+         *
+         * <p>Default implementation returns 0.</p>
+         *
+         * @param state Current state of RecyclerView
+         * @return The horizontal extent of the scrollbar's thumb
+         * @see RecyclerView#computeHorizontalScrollExtent()
+         */
+        public int computeHorizontalScrollExtent(State state) {
+            return 0;
+        }
+
+        /**
+         * <p>Override this method if you want to support scroll bars.</p>
+         *
+         * <p>Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.</p>
+         *
+         * <p>Default implementation returns 0.</p>
+         *
+         * @param state Current State of RecyclerView where you can find total item count
+         * @return The horizontal offset of the scrollbar's thumb
+         * @see RecyclerView#computeHorizontalScrollOffset()
+         */
+        public int computeHorizontalScrollOffset(State state) {
+            return 0;
+        }
+
+        /**
+         * <p>Override this method if you want to support scroll bars.</p>
+         *
+         * <p>Read {@link RecyclerView#computeHorizontalScrollRange()} for details.</p>
+         *
+         * <p>Default implementation returns 0.</p>
+         *
+         * @param state Current State of RecyclerView where you can find total item count
+         * @return The total horizontal range represented by the vertical scrollbar
+         * @see RecyclerView#computeHorizontalScrollRange()
+         */
+        public int computeHorizontalScrollRange(State state) {
+            return 0;
+        }
+
+        /**
+         * <p>Override this method if you want to support scroll bars.</p>
+         *
+         * <p>Read {@link RecyclerView#computeVerticalScrollExtent()} for details.</p>
+         *
+         * <p>Default implementation returns 0.</p>
+         *
+         * @param state Current state of RecyclerView
+         * @return The vertical extent of the scrollbar's thumb
+         * @see RecyclerView#computeVerticalScrollExtent()
+         */
+        public int computeVerticalScrollExtent(State state) {
+            return 0;
+        }
+
+        /**
+         * <p>Override this method if you want to support scroll bars.</p>
+         *
+         * <p>Read {@link RecyclerView#computeVerticalScrollOffset()} for details.</p>
+         *
+         * <p>Default implementation returns 0.</p>
+         *
+         * @param state Current State of RecyclerView where you can find total item count
+         * @return The vertical offset of the scrollbar's thumb
+         * @see RecyclerView#computeVerticalScrollOffset()
+         */
+        public int computeVerticalScrollOffset(State state) {
+            return 0;
+        }
+
+        /**
+         * <p>Override this method if you want to support scroll bars.</p>
+         *
+         * <p>Read {@link RecyclerView#computeVerticalScrollRange()} for details.</p>
+         *
+         * <p>Default implementation returns 0.</p>
+         *
+         * @param state Current State of RecyclerView where you can find total item count
+         * @return The total vertical range represented by the vertical scrollbar
+         * @see RecyclerView#computeVerticalScrollRange()
+         */
+        public int computeVerticalScrollRange(State state) {
+            return 0;
+        }
+
+        /**
+         * Measure the attached RecyclerView. Implementations must call
+         * {@link #setMeasuredDimension(int, int)} before returning.
+         *
+         * <p>The default implementation will handle EXACTLY measurements and respect
+         * the minimum width and height properties of the host RecyclerView if measured
+         * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView
+         * will consume all available space.</p>
+         *
+         * @param recycler Recycler
+         * @param state Transient state of RecyclerView
+         * @param widthSpec Width {@link android.view.View.MeasureSpec}
+         * @param heightSpec Height {@link android.view.View.MeasureSpec}
+         */
+        public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
+            mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
+        }
+
+        /**
+         * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the
+         * host RecyclerView.
+         *
+         * @param widthSize Measured width
+         * @param heightSize Measured height
+         */
+        public void setMeasuredDimension(int widthSize, int heightSize) {
+            mRecyclerView.setMeasuredDimension(widthSize, heightSize);
+        }
+
+        /**
+         * @return The host RecyclerView's {@link View#getMinimumWidth()}
+         */
+        public int getMinimumWidth() {
+            return mRecyclerView.getMinimumWidth();
+        }
+
+        /**
+         * @return The host RecyclerView's {@link View#getMinimumHeight()}
+         */
+        public int getMinimumHeight() {
+            return mRecyclerView.getMinimumHeight();
+        }
+        /**
+         * <p>Called when the LayoutManager should save its state. This is a good time to save your
+         * scroll position, configuration and anything else that may be required to restore the same
+         * layout state if the LayoutManager is recreated.</p>
+         * <p>RecyclerView does NOT verify if the LayoutManager has changed between state save and
+         * restore. This will let you share information between your LayoutManagers but it is also
+         * your responsibility to make sure they use the same parcelable class.</p>
+         *
+         * @return Necessary information for LayoutManager to be able to restore its state
+         */
+        public Parcelable onSaveInstanceState() {
+            return null;
+        }
+
+
+        public void onRestoreInstanceState(Parcelable state) {
+
+        }
+
+        void stopSmoothScroller() {
+            if (mSmoothScroller != null) {
+                mSmoothScroller.stop();
+            }
+        }
+
+        private void onSmoothScrollerStopped(SmoothScroller smoothScroller) {
+            if (mSmoothScroller == smoothScroller) {
+                mSmoothScroller = null;
+            }
+        }
+
+        /**
+         * RecyclerView calls this method to notify LayoutManager that scroll state has changed.
+         *
+         * @param state The new scroll state for RecyclerView
+         */
+        public void onScrollStateChanged(int state) {
+        }
+
+        /**
+         * Removes all views and recycles them using the given recycler.
+         * <p>
+         * If you want to clean cached views as well, you should call {@link Recycler#clear()} too.
+         * <p>
+         * If a View is marked as "ignored", it is not removed nor recycled.
+         *
+         * @param recycler Recycler to use to recycle children
+         * @see #removeAndRecycleView(View, Recycler)
+         * @see #removeAndRecycleViewAt(int, Recycler)
+         * @see #ignoreView(View)
+         */
+        public void removeAndRecycleAllViews(Recycler recycler) {
+            for (int i = getChildCount() - 1; i >= 0; i--) {
+                final View view = getChildAt(i);
+                if (!getChildViewHolderInt(view).shouldIgnore()) {
+                    removeAndRecycleViewAt(i, recycler);
+                }
+            }
+        }
+
+        // called by accessibility delegate
+        void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+            onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info);
+        }
+
+        /**
+         * Called by the AccessibilityDelegate when the information about the current layout should
+         * be populated.
+         * <p>
+         * Default implementation adds a {@link
+         * android.view.accessibility.AccessibilityNodeInfo.CollectionInfo}.
+         * <p>
+         * You should override
+         * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)},
+         * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)},
+         * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and
+         * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for
+         * more accurate accessibility information.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @param info     The info that should be filled by the LayoutManager
+         * @see View#onInitializeAccessibilityNodeInfo(
+         *android.view.accessibility.AccessibilityNodeInfo)
+         * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)
+         * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)
+         * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)
+         * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)
+         */
+        public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
+                AccessibilityNodeInfo info) {
+            if (mRecyclerView.canScrollVertically(-1)
+                    || mRecyclerView.canScrollHorizontally(-1)) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+                info.setScrollable(true);
+            }
+            if (mRecyclerView.canScrollVertically(1)
+                    || mRecyclerView.canScrollHorizontally(1)) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+                info.setScrollable(true);
+            }
+            final AccessibilityNodeInfo.CollectionInfo collectionInfo =
+                    AccessibilityNodeInfo.CollectionInfo
+                            .obtain(getRowCountForAccessibility(recycler, state),
+                                    getColumnCountForAccessibility(recycler, state),
+                                    isLayoutHierarchical(recycler, state),
+                                    getSelectionModeForAccessibility(recycler, state));
+            info.setCollectionInfo(collectionInfo);
+        }
+
+        // called by accessibility delegate
+        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+            onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event);
+        }
+
+        /**
+         * Called by the accessibility delegate to initialize an accessibility event.
+         * <p>
+         * Default implementation adds item count and scroll information to the event.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @param event    The event instance to initialize
+         * @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent)
+         */
+        public void onInitializeAccessibilityEvent(Recycler recycler, State state,
+                AccessibilityEvent event) {
+            if (mRecyclerView == null || event == null) {
+                return;
+            }
+            event.setScrollable(mRecyclerView.canScrollVertically(1)
+                    || mRecyclerView.canScrollVertically(-1)
+                    || mRecyclerView.canScrollHorizontally(-1)
+                    || mRecyclerView.canScrollHorizontally(1));
+
+            if (mRecyclerView.mAdapter != null) {
+                event.setItemCount(mRecyclerView.mAdapter.getItemCount());
+            }
+        }
+
+        // called by accessibility delegate
+        void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfo info) {
+            final ViewHolder vh = getChildViewHolderInt(host);
+            // avoid trying to create accessibility node info for removed children
+            if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) {
+                onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler,
+                        mRecyclerView.mState, host, info);
+            }
+        }
+
+        /**
+         * Called by the AccessibilityDelegate when the accessibility information for a specific
+         * item should be populated.
+         * <p>
+         * Default implementation adds basic positioning information about the item.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @param host     The child for which accessibility node info should be populated
+         * @param info     The info to fill out about the item
+         * @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int,
+         * android.view.accessibility.AccessibilityNodeInfo)
+         */
+        public void onInitializeAccessibilityNodeInfoForItem(Recycler recycler, State state,
+                View host, AccessibilityNodeInfo info) {
+            int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0;
+            int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0;
+            final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
+                    AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndexGuess, 1,
+                            columnIndexGuess, 1, false, false);
+            info.setCollectionItemInfo(itemInfo);
+        }
+
+        /**
+         * A LayoutManager can call this method to force RecyclerView to run simple animations in
+         * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data
+         * change).
+         * <p>
+         * Note that, calling this method will not guarantee that RecyclerView will run animations
+         * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will
+         * not run any animations but will still clear this flag after the layout is complete.
+         *
+         */
+        public void requestSimpleAnimationsInNextLayout() {
+            mRequestedSimpleAnimations = true;
+        }
+
+        /**
+         * Returns the selection mode for accessibility. Should be
+         * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE},
+         * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_SINGLE} or
+         * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_MULTIPLE}.
+         * <p>
+         * Default implementation returns
+         * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @return Selection mode for accessibility. Default implementation returns
+         * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}.
+         */
+        public int getSelectionModeForAccessibility(Recycler recycler, State state) {
+            return AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE;
+        }
+
+        /**
+         * Returns the number of rows for accessibility.
+         * <p>
+         * Default implementation returns the number of items in the adapter if LayoutManager
+         * supports vertical scrolling or 1 if LayoutManager does not support vertical
+         * scrolling.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @return The number of rows in LayoutManager for accessibility.
+         */
+        public int getRowCountForAccessibility(Recycler recycler, State state) {
+            if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
+                return 1;
+            }
+            return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 1;
+        }
+
+        /**
+         * Returns the number of columns for accessibility.
+         * <p>
+         * Default implementation returns the number of items in the adapter if LayoutManager
+         * supports horizontal scrolling or 1 if LayoutManager does not support horizontal
+         * scrolling.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @return The number of rows in LayoutManager for accessibility.
+         */
+        public int getColumnCountForAccessibility(Recycler recycler, State state) {
+            if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
+                return 1;
+            }
+            return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1;
+        }
+
+        /**
+         * Returns whether layout is hierarchical or not to be used for accessibility.
+         * <p>
+         * Default implementation returns false.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @return True if layout is hierarchical.
+         */
+        public boolean isLayoutHierarchical(Recycler recycler, State state) {
+            return false;
+        }
+
+        // called by accessibility delegate
+        boolean performAccessibilityAction(int action, Bundle args) {
+            return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState,
+                    action, args);
+        }
+
+        /**
+         * Called by AccessibilityDelegate when an action is requested from the RecyclerView.
+         *
+         * @param recycler  The Recycler that can be used to convert view positions into adapter
+         *                  positions
+         * @param state     The current state of RecyclerView
+         * @param action    The action to perform
+         * @param args      Optional action arguments
+         * @see View#performAccessibilityAction(int, android.os.Bundle)
+         */
+        public boolean performAccessibilityAction(Recycler recycler, State state, int action,
+                Bundle args) {
+            if (mRecyclerView == null) {
+                return false;
+            }
+            int vScroll = 0, hScroll = 0;
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+                    if (mRecyclerView.canScrollVertically(-1)) {
+                        vScroll = -(getHeight() - getPaddingTop() - getPaddingBottom());
+                    }
+                    if (mRecyclerView.canScrollHorizontally(-1)) {
+                        hScroll = -(getWidth() - getPaddingLeft() - getPaddingRight());
+                    }
+                    break;
+                case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+                    if (mRecyclerView.canScrollVertically(1)) {
+                        vScroll = getHeight() - getPaddingTop() - getPaddingBottom();
+                    }
+                    if (mRecyclerView.canScrollHorizontally(1)) {
+                        hScroll = getWidth() - getPaddingLeft() - getPaddingRight();
+                    }
+                    break;
+            }
+            if (vScroll == 0 && hScroll == 0) {
+                return false;
+            }
+            mRecyclerView.scrollBy(hScroll, vScroll);
+            return true;
+        }
+
+        // called by accessibility delegate
+        boolean performAccessibilityActionForItem(View view, int action, Bundle args) {
+            return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState,
+                    view, action, args);
+        }
+
+        /**
+         * Called by AccessibilityDelegate when an accessibility action is requested on one of the
+         * children of LayoutManager.
+         * <p>
+         * Default implementation does not do anything.
+         *
+         * @param recycler The Recycler that can be used to convert view positions into adapter
+         *                 positions
+         * @param state    The current state of RecyclerView
+         * @param view     The child view on which the action is performed
+         * @param action   The action to perform
+         * @param args     Optional action arguments
+         * @return true if action is handled
+         * @see View#performAccessibilityAction(int, android.os.Bundle)
+         */
+        public boolean performAccessibilityActionForItem(Recycler recycler, State state, View view,
+                int action, Bundle args) {
+            return false;
+        }
+
+        /**
+         * Parse the xml attributes to get the most common properties used by layout managers.
+         *
+         * @return an object containing the properties as specified in the attrs.
+         */
+        public static Properties getProperties(Context context, AttributeSet attrs,
+                int defStyleAttr, int defStyleRes) {
+            Properties properties = new Properties();
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
+                    defStyleAttr, defStyleRes);
+            properties.orientation = a.getInt(R.styleable.RecyclerView_orientation, VERTICAL);
+            properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1);
+            properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false);
+            properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false);
+            a.recycle();
+            return properties;
+        }
+
+        void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
+            setMeasureSpecs(
+                    MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY)
+            );
+        }
+
+        /**
+         * Internal API to allow LayoutManagers to be measured twice.
+         * <p>
+         * This is not public because LayoutManagers should be able to handle their layouts in one
+         * pass but it is very convenient to make existing LayoutManagers support wrapping content
+         * when both orientations are undefined.
+         * <p>
+         * This API will be removed after default LayoutManagers properly implement wrap content in
+         * non-scroll orientation.
+         */
+        boolean shouldMeasureTwice() {
+            return false;
+        }
+
+        boolean hasFlexibleChildInBothOrientations() {
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final ViewGroup.LayoutParams lp = child.getLayoutParams();
+                if (lp.width < 0 && lp.height < 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Some general properties that a LayoutManager may want to use.
+         */
+        public static class Properties {
+            /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation */
+            public int orientation;
+            /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount */
+            public int spanCount;
+            /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout */
+            public boolean reverseLayout;
+            /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd */
+            public boolean stackFromEnd;
+        }
+    }
+
+    /**
+     * An ItemDecoration allows the application to add a special drawing and layout offset
+     * to specific item views from the adapter's data set. This can be useful for drawing dividers
+     * between items, highlights, visual grouping boundaries and more.
+     *
+     * <p>All ItemDecorations are drawn in the order they were added, before the item
+     * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
+     * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
+     * RecyclerView.State)}.</p>
+     */
+    public abstract static class ItemDecoration {
+        /**
+         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
+         * Any content drawn by this method will be drawn before the item views are drawn,
+         * and will thus appear underneath the views.
+         *
+         * @param c Canvas to draw into
+         * @param parent RecyclerView this ItemDecoration is drawing into
+         * @param state The current state of RecyclerView
+         */
+        public void onDraw(Canvas c, RecyclerView parent, State state) {
+            onDraw(c, parent);
+        }
+
+        /**
+         * @deprecated
+         * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
+         */
+        @Deprecated
+        public void onDraw(Canvas c, RecyclerView parent) {
+        }
+
+        /**
+         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
+         * Any content drawn by this method will be drawn after the item views are drawn
+         * and will thus appear over the views.
+         *
+         * @param c Canvas to draw into
+         * @param parent RecyclerView this ItemDecoration is drawing into
+         * @param state The current state of RecyclerView.
+         */
+        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
+            onDrawOver(c, parent);
+        }
+
+        /**
+         * @deprecated
+         * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
+         */
+        @Deprecated
+        public void onDrawOver(Canvas c, RecyclerView parent) {
+        }
+
+
+        /**
+         * @deprecated
+         * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
+         */
+        @Deprecated
+        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
+            outRect.set(0, 0, 0, 0);
+        }
+
+        /**
+         * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
+         * the number of pixels that the item view should be inset by, similar to padding or margin.
+         * The default implementation sets the bounds of outRect to 0 and returns.
+         *
+         * <p>
+         * If this ItemDecoration does not affect the positioning of item views, it should set
+         * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
+         * before returning.
+         *
+         * <p>
+         * If you need to access Adapter for additional data, you can call
+         * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
+         * View.
+         *
+         * @param outRect Rect to receive the output.
+         * @param view    The child view to decorate
+         * @param parent  RecyclerView this ItemDecoration is decorating
+         * @param state   The current state of RecyclerView.
+         */
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
+            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
+                    parent);
+        }
+    }
+
+    /**
+     * An OnItemTouchListener allows the application to intercept touch events in progress at the
+     * view hierarchy level of the RecyclerView before those touch events are considered for
+     * RecyclerView's own scrolling behavior.
+     *
+     * <p>This can be useful for applications that wish to implement various forms of gestural
+     * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept
+     * a touch interaction already in progress even if the RecyclerView is already handling that
+     * gesture stream itself for the purposes of scrolling.</p>
+     *
+     * @see SimpleOnItemTouchListener
+     */
+    public interface OnItemTouchListener {
+        /**
+         * Silently observe and/or take over touch events sent to the RecyclerView
+         * before they are handled by either the RecyclerView itself or its child views.
+         *
+         * <p>The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run
+         * in the order in which each listener was added, before any other touch processing
+         * by the RecyclerView itself or child views occurs.</p>
+         *
+         * @param e MotionEvent describing the touch event. All coordinates are in
+         *          the RecyclerView's coordinate system.
+         * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false
+         *         to continue with the current behavior and continue observing future events in
+         *         the gesture.
+         */
+        boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
+
+        /**
+         * Process a touch event as part of a gesture that was claimed by returning true from
+         * a previous call to {@link #onInterceptTouchEvent}.
+         *
+         * @param e MotionEvent describing the touch event. All coordinates are in
+         *          the RecyclerView's coordinate system.
+         */
+        void onTouchEvent(RecyclerView rv, MotionEvent e);
+
+        /**
+         * Called when a child of RecyclerView does not want RecyclerView and its ancestors to
+         * intercept touch events with
+         * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
+         *
+         * @param disallowIntercept True if the child does not want the parent to
+         *            intercept touch events.
+         * @see ViewParent#requestDisallowInterceptTouchEvent(boolean)
+         */
+        void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
+    }
+
+    /**
+     * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies
+     * and default return values.
+     * <p>
+     * You may prefer to extend this class if you don't need to override all methods. Another
+     * benefit of using this class is future compatibility. As the interface may change, we'll
+     * always provide a default implementation on this class so that your code won't break when
+     * you update to a new version of the support library.
+     */
+    public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener {
+        @Override
+        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+            return false;
+        }
+
+        @Override
+        public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+        }
+
+        @Override
+        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        }
+    }
+
+
+    /**
+     * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event
+     * has occurred on that RecyclerView.
+     * <p>
+     * @see RecyclerView#addOnScrollListener(OnScrollListener)
+     * @see RecyclerView#clearOnChildAttachStateChangeListeners()
+     *
+     */
+    public abstract static class OnScrollListener {
+        /**
+         * Callback method to be invoked when RecyclerView's scroll state changes.
+         *
+         * @param recyclerView The RecyclerView whose scroll state has changed.
+         * @param newState     The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
+         *                     {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
+         */
+        public void onScrollStateChanged(RecyclerView recyclerView, int newState){}
+
+        /**
+         * Callback method to be invoked when the RecyclerView has been scrolled. This will be
+         * called after the scroll has completed.
+         * <p>
+         * This callback will also be called if visible item range changes after a layout
+         * calculation. In that case, dx and dy will be 0.
+         *
+         * @param recyclerView The RecyclerView which scrolled.
+         * @param dx The amount of horizontal scroll.
+         * @param dy The amount of vertical scroll.
+         */
+        public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
+    }
+
+    /**
+     * A RecyclerListener can be set on a RecyclerView to receive messages whenever
+     * a view is recycled.
+     *
+     * @see RecyclerView#setRecyclerListener(RecyclerListener)
+     */
+    public interface RecyclerListener {
+
+        /**
+         * This method is called whenever the view in the ViewHolder is recycled.
+         *
+         * RecyclerView calls this method right before clearing ViewHolder's internal data and
+         * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information
+         * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get
+         * its adapter position.
+         *
+         * @param holder The ViewHolder containing the view that was recycled
+         */
+        void onViewRecycled(ViewHolder holder);
+    }
+
+    /**
+     * A Listener interface that can be attached to a RecylcerView to get notified
+     * whenever a ViewHolder is attached to or detached from RecyclerView.
+     */
+    public interface OnChildAttachStateChangeListener {
+
+        /**
+         * Called when a view is attached to the RecyclerView.
+         *
+         * @param view The View which is attached to the RecyclerView
+         */
+        void onChildViewAttachedToWindow(View view);
+
+        /**
+         * Called when a view is detached from RecyclerView.
+         *
+         * @param view The View which is being detached from the RecyclerView
+         */
+        void onChildViewDetachedFromWindow(View view);
+    }
+
+    /**
+     * A ViewHolder describes an item view and metadata about its place within the RecyclerView.
+     *
+     * <p>{@link Adapter} implementations should subclass ViewHolder and add fields for caching
+     * potentially expensive {@link View#findViewById(int)} results.</p>
+     *
+     * <p>While {@link LayoutParams} belong to the {@link LayoutManager},
+     * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use
+     * their own custom ViewHolder implementations to store data that makes binding view contents
+     * easier. Implementations should assume that individual item views will hold strong references
+     * to <code>ViewHolder</code> objects and that <code>RecyclerView</code> instances may hold
+     * strong references to extra off-screen item views for caching purposes</p>
+     */
+    public abstract static class ViewHolder {
+        public final View itemView;
+        WeakReference<RecyclerView> mNestedRecyclerView;
+        int mPosition = NO_POSITION;
+        int mOldPosition = NO_POSITION;
+        long mItemId = NO_ID;
+        int mItemViewType = INVALID_TYPE;
+        int mPreLayoutPosition = NO_POSITION;
+
+        // The item that this holder is shadowing during an item change event/animation
+        ViewHolder mShadowedHolder = null;
+        // The item that is shadowing this holder during an item change event/animation
+        ViewHolder mShadowingHolder = null;
+
+        /**
+         * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
+         * are all valid.
+         */
+        static final int FLAG_BOUND = 1 << 0;
+
+        /**
+         * The data this ViewHolder's view reflects is stale and needs to be rebound
+         * by the adapter. mPosition and mItemId are consistent.
+         */
+        static final int FLAG_UPDATE = 1 << 1;
+
+        /**
+         * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
+         * are not to be trusted and may no longer match the item view type.
+         * This ViewHolder must be fully rebound to different data.
+         */
+        static final int FLAG_INVALID = 1 << 2;
+
+        /**
+         * This ViewHolder points at data that represents an item previously removed from the
+         * data set. Its view may still be used for things like outgoing animations.
+         */
+        static final int FLAG_REMOVED = 1 << 3;
+
+        /**
+         * This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
+         * and is intended to keep views around during animations.
+         */
+        static final int FLAG_NOT_RECYCLABLE = 1 << 4;
+
+        /**
+         * This ViewHolder is returned from scrap which means we are expecting an addView call
+         * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until
+         * the end of the layout pass and then recycled by RecyclerView if it is not added back to
+         * the RecyclerView.
+         */
+        static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5;
+
+        /**
+         * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove
+         * it unless LayoutManager is replaced.
+         * It is still fully visible to the LayoutManager.
+         */
+        static final int FLAG_IGNORE = 1 << 7;
+
+        /**
+         * When the View is detached form the parent, we set this flag so that we can take correct
+         * action when we need to remove it or add it back.
+         */
+        static final int FLAG_TMP_DETACHED = 1 << 8;
+
+        /**
+         * Set when we can no longer determine the adapter position of this ViewHolder until it is
+         * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is
+         * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon
+         * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is
+         * re-calculated.
+         */
+        static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9;
+
+        /**
+         * Set when a addChangePayload(null) is called
+         */
+        static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10;
+
+        /**
+         * Used by ItemAnimator when a ViewHolder's position changes
+         */
+        static final int FLAG_MOVED = 1 << 11;
+
+        /**
+         * Used by ItemAnimator when a ViewHolder appears in pre-layout
+         */
+        static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
+
+        static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1;
+
+        /**
+         * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
+         * hidden list (as if it was scrap) without being recycled in between.
+         *
+         * When a ViewHolder is hidden, there are 2 paths it can be re-used:
+         *   a) Animation ends, view is recycled and used from the recycle pool.
+         *   b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
+         *
+         * This flag is used to represent "case b" where the ViewHolder is reused without being
+         * recycled (thus "bounced" from the hidden list). This state requires special handling
+         * because the ViewHolder must be added to pre layout maps for animations as if it was
+         * already there.
+         */
+        static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
+
+        private int mFlags;
+
+        private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
+
+        List<Object> mPayloads = null;
+        List<Object> mUnmodifiedPayloads = null;
+
+        private int mIsRecyclableCount = 0;
+
+        // If non-null, view is currently considered scrap and may be reused for other data by the
+        // scrap container.
+        private Recycler mScrapContainer = null;
+        // Keeps whether this ViewHolder lives in Change scrap or Attached scrap
+        private boolean mInChangeScrap = false;
+
+        // Saves isImportantForAccessibility value for the view item while it's in hidden state and
+        // marked as unimportant for accessibility.
+        private int mWasImportantForAccessibilityBeforeHidden =
+                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+        // set if we defer the accessibility state change of the view holder
+        @VisibleForTesting
+        int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
+
+        /**
+         * Is set when VH is bound from the adapter and cleaned right before it is sent to
+         * {@link RecycledViewPool}.
+         */
+        RecyclerView mOwnerRecyclerView;
+
+        public ViewHolder(View itemView) {
+            if (itemView == null) {
+                throw new IllegalArgumentException("itemView may not be null");
+            }
+            this.itemView = itemView;
+        }
+
+        void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
+            addFlags(ViewHolder.FLAG_REMOVED);
+            offsetPosition(offset, applyToPreLayout);
+            mPosition = mNewPosition;
+        }
+
+        void offsetPosition(int offset, boolean applyToPreLayout) {
+            if (mOldPosition == NO_POSITION) {
+                mOldPosition = mPosition;
+            }
+            if (mPreLayoutPosition == NO_POSITION) {
+                mPreLayoutPosition = mPosition;
+            }
+            if (applyToPreLayout) {
+                mPreLayoutPosition += offset;
+            }
+            mPosition += offset;
+            if (itemView.getLayoutParams() != null) {
+                ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
+            }
+        }
+
+        void clearOldPosition() {
+            mOldPosition = NO_POSITION;
+            mPreLayoutPosition = NO_POSITION;
+        }
+
+        void saveOldPosition() {
+            if (mOldPosition == NO_POSITION) {
+                mOldPosition = mPosition;
+            }
+        }
+
+        boolean shouldIgnore() {
+            return (mFlags & FLAG_IGNORE) != 0;
+        }
+
+        /**
+         * @deprecated This method is deprecated because its meaning is ambiguous due to the async
+         * handling of adapter updates. Please use {@link #getLayoutPosition()} or
+         * {@link #getAdapterPosition()} depending on your use case.
+         *
+         * @see #getLayoutPosition()
+         * @see #getAdapterPosition()
+         */
+        @Deprecated
+        public final int getPosition() {
+            return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
+        }
+
+        /**
+         * Returns the position of the ViewHolder in terms of the latest layout pass.
+         * <p>
+         * This position is mostly used by RecyclerView components to be consistent while
+         * RecyclerView lazily processes adapter updates.
+         * <p>
+         * For performance and animation reasons, RecyclerView batches all adapter updates until the
+         * next layout pass. This may cause mismatches between the Adapter position of the item and
+         * the position it had in the latest layout calculations.
+         * <p>
+         * LayoutManagers should always call this method while doing calculations based on item
+         * positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State},
+         * {@link RecyclerView.Recycler} that receive a position expect it to be the layout position
+         * of the item.
+         * <p>
+         * If LayoutManager needs to call an external method that requires the adapter position of
+         * the item, it can use {@link #getAdapterPosition()} or
+         * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}.
+         *
+         * @return Returns the adapter position of the ViewHolder in the latest layout pass.
+         * @see #getAdapterPosition()
+         */
+        public final int getLayoutPosition() {
+            return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
+        }
+
+        /**
+         * Returns the Adapter position of the item represented by this ViewHolder.
+         * <p>
+         * Note that this might be different than the {@link #getLayoutPosition()} if there are
+         * pending adapter updates but a new layout pass has not happened yet.
+         * <p>
+         * RecyclerView does not handle any adapter updates until the next layout traversal. This
+         * may create temporary inconsistencies between what user sees on the screen and what
+         * adapter contents have. This inconsistency is not important since it will be less than
+         * 16ms but it might be a problem if you want to use ViewHolder position to access the
+         * adapter. Sometimes, you may need to get the exact adapter position to do
+         * some actions in response to user events. In that case, you should use this method which
+         * will calculate the Adapter position of the ViewHolder.
+         * <p>
+         * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the
+         * next layout pass, the return value of this method will be {@link #NO_POSITION}.
+         *
+         * @return The adapter position of the item if it still exists in the adapter.
+         * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter,
+         * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last
+         * layout pass or the ViewHolder has already been recycled.
+         */
+        public final int getAdapterPosition() {
+            if (mOwnerRecyclerView == null) {
+                return NO_POSITION;
+            }
+            return mOwnerRecyclerView.getAdapterPositionFor(this);
+        }
+
+        /**
+         * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders
+         * to perform animations.
+         * <p>
+         * If a ViewHolder was laid out in the previous onLayout call, old position will keep its
+         * adapter index in the previous layout.
+         *
+         * @return The previous adapter index of the Item represented by this ViewHolder or
+         * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is
+         * complete).
+         */
+        public final int getOldPosition() {
+            return mOldPosition;
+        }
+
+        /**
+         * Returns The itemId represented by this ViewHolder.
+         *
+         * @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID}
+         * otherwise
+         */
+        public final long getItemId() {
+            return mItemId;
+        }
+
+        /**
+         * @return The view type of this ViewHolder.
+         */
+        public final int getItemViewType() {
+            return mItemViewType;
+        }
+
+        boolean isScrap() {
+            return mScrapContainer != null;
+        }
+
+        void unScrap() {
+            mScrapContainer.unscrapView(this);
+        }
+
+        boolean wasReturnedFromScrap() {
+            return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
+        }
+
+        void clearReturnedFromScrapFlag() {
+            mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP;
+        }
+
+        void clearTmpDetachFlag() {
+            mFlags = mFlags & ~FLAG_TMP_DETACHED;
+        }
+
+        void stopIgnoring() {
+            mFlags = mFlags & ~FLAG_IGNORE;
+        }
+
+        void setScrapContainer(Recycler recycler, boolean isChangeScrap) {
+            mScrapContainer = recycler;
+            mInChangeScrap = isChangeScrap;
+        }
+
+        boolean isInvalid() {
+            return (mFlags & FLAG_INVALID) != 0;
+        }
+
+        boolean needsUpdate() {
+            return (mFlags & FLAG_UPDATE) != 0;
+        }
+
+        boolean isBound() {
+            return (mFlags & FLAG_BOUND) != 0;
+        }
+
+        boolean isRemoved() {
+            return (mFlags & FLAG_REMOVED) != 0;
+        }
+
+        boolean hasAnyOfTheFlags(int flags) {
+            return (mFlags & flags) != 0;
+        }
+
+        boolean isTmpDetached() {
+            return (mFlags & FLAG_TMP_DETACHED) != 0;
+        }
+
+        boolean isAdapterPositionUnknown() {
+            return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid();
+        }
+
+        void setFlags(int flags, int mask) {
+            mFlags = (mFlags & ~mask) | (flags & mask);
+        }
+
+        void addFlags(int flags) {
+            mFlags |= flags;
+        }
+
+        void addChangePayload(Object payload) {
+            if (payload == null) {
+                addFlags(FLAG_ADAPTER_FULLUPDATE);
+            } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
+                createPayloadsIfNeeded();
+                mPayloads.add(payload);
+            }
+        }
+
+        private void createPayloadsIfNeeded() {
+            if (mPayloads == null) {
+                mPayloads = new ArrayList<Object>();
+                mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads);
+            }
+        }
+
+        void clearPayload() {
+            if (mPayloads != null) {
+                mPayloads.clear();
+            }
+            mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE;
+        }
+
+        List<Object> getUnmodifiedPayloads() {
+            if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
+                if (mPayloads == null || mPayloads.size() == 0) {
+                    // Initial state,  no update being called.
+                    return FULLUPDATE_PAYLOADS;
+                }
+                // there are none-null payloads
+                return mUnmodifiedPayloads;
+            } else {
+                // a full update has been called.
+                return FULLUPDATE_PAYLOADS;
+            }
+        }
+
+        void resetInternal() {
+            mFlags = 0;
+            mPosition = NO_POSITION;
+            mOldPosition = NO_POSITION;
+            mItemId = NO_ID;
+            mPreLayoutPosition = NO_POSITION;
+            mIsRecyclableCount = 0;
+            mShadowedHolder = null;
+            mShadowingHolder = null;
+            clearPayload();
+            mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+            mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
+            clearNestedRecyclerViewIfNotNested(this);
+        }
+
+        /**
+         * Called when the child view enters the hidden state
+         */
+        private void onEnteredHiddenState(RecyclerView parent) {
+            // While the view item is in hidden state, make it invisible for the accessibility.
+            mWasImportantForAccessibilityBeforeHidden =
+                    itemView.getImportantForAccessibility();
+            parent.setChildImportantForAccessibilityInternal(this,
+                    View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+        }
+
+        /**
+         * Called when the child view leaves the hidden state
+         */
+        private void onLeftHiddenState(RecyclerView parent) {
+            parent.setChildImportantForAccessibilityInternal(this,
+                    mWasImportantForAccessibilityBeforeHidden);
+            mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder("ViewHolder{"
+                    + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId
+                    + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition);
+            if (isScrap()) {
+                sb.append(" scrap ")
+                        .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]");
+            }
+            if (isInvalid()) sb.append(" invalid");
+            if (!isBound()) sb.append(" unbound");
+            if (needsUpdate()) sb.append(" update");
+            if (isRemoved()) sb.append(" removed");
+            if (shouldIgnore()) sb.append(" ignored");
+            if (isTmpDetached()) sb.append(" tmpDetached");
+            if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")");
+            if (isAdapterPositionUnknown()) sb.append(" undefined adapter position");
+
+            if (itemView.getParent() == null) sb.append(" no parent");
+            sb.append("}");
+            return sb.toString();
+        }
+
+        /**
+         * Informs the recycler whether this item can be recycled. Views which are not
+         * recyclable will not be reused for other items until setIsRecyclable() is
+         * later set to true. Calls to setIsRecyclable() should always be paired (one
+         * call to setIsRecyclabe(false) should always be matched with a later call to
+         * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally
+         * reference-counted.
+         *
+         * @param recyclable Whether this item is available to be recycled. Default value
+         * is true.
+         *
+         * @see #isRecyclable()
+         */
+        public final void setIsRecyclable(boolean recyclable) {
+            mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1;
+            if (mIsRecyclableCount < 0) {
+                mIsRecyclableCount = 0;
+                if (DEBUG) {
+                    throw new RuntimeException("isRecyclable decremented below 0: "
+                            + "unmatched pair of setIsRecyable() calls for " + this);
+                }
+                Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: "
+                        + "unmatched pair of setIsRecyable() calls for " + this);
+            } else if (!recyclable && mIsRecyclableCount == 1) {
+                mFlags |= FLAG_NOT_RECYCLABLE;
+            } else if (recyclable && mIsRecyclableCount == 0) {
+                mFlags &= ~FLAG_NOT_RECYCLABLE;
+            }
+            if (DEBUG) {
+                Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this);
+            }
+        }
+
+        /**
+         * @return true if this item is available to be recycled, false otherwise.
+         *
+         * @see #setIsRecyclable(boolean)
+         */
+        public final boolean isRecyclable() {
+            return (mFlags & FLAG_NOT_RECYCLABLE) == 0
+                    && !itemView.hasTransientState();
+        }
+
+        /**
+         * Returns whether we have animations referring to this view holder or not.
+         * This is similar to isRecyclable flag but does not check transient state.
+         */
+        private boolean shouldBeKeptAsChild() {
+            return (mFlags & FLAG_NOT_RECYCLABLE) != 0;
+        }
+
+        /**
+         * @return True if ViewHolder is not referenced by RecyclerView animations but has
+         * transient state which will prevent it from being recycled.
+         */
+        private boolean doesTransientStatePreventRecycling() {
+            return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && itemView.hasTransientState();
+        }
+
+        boolean isUpdated() {
+            return (mFlags & FLAG_UPDATE) != 0;
+        }
+    }
+
+    /**
+     * This method is here so that we can control the important for a11y changes and test it.
+     */
+    @VisibleForTesting
+    boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder,
+            int importantForAccessibility) {
+        if (isComputingLayout()) {
+            viewHolder.mPendingAccessibilityState = importantForAccessibility;
+            mPendingAccessibilityImportanceChange.add(viewHolder);
+            return false;
+        }
+        viewHolder.itemView.setImportantForAccessibility(importantForAccessibility);
+        return true;
+    }
+
+    void dispatchPendingImportantForAccessibilityChanges() {
+        for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) {
+            ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i);
+            if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) {
+                continue;
+            }
+            int state = viewHolder.mPendingAccessibilityState;
+            if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) {
+                //noinspection WrongConstant
+                viewHolder.itemView.setImportantForAccessibility(state);
+                viewHolder.mPendingAccessibilityState =
+                        ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET;
+            }
+        }
+        mPendingAccessibilityImportanceChange.clear();
+    }
+
+    int getAdapterPositionFor(ViewHolder viewHolder) {
+        if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
+                | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)
+                || !viewHolder.isBound()) {
+            return RecyclerView.NO_POSITION;
+        }
+        return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition);
+    }
+
+    /**
+     * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of
+     * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged
+     * to create their own subclass of this <code>LayoutParams</code> class
+     * to store any additional required per-child view metadata about the layout.
+     */
+    public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+        ViewHolder mViewHolder;
+        final Rect mDecorInsets = new Rect();
+        boolean mInsetsDirty = true;
+        // Flag is set to true if the view is bound while it is detached from RV.
+        // In this case, we need to manually call invalidate after view is added to guarantee that
+        // invalidation is populated through the View hierarchy
+        boolean mPendingInvalidate = false;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(LayoutParams source) {
+            super((ViewGroup.LayoutParams) source);
+        }
+
+        /**
+         * Returns true if the view this LayoutParams is attached to needs to have its content
+         * updated from the corresponding adapter.
+         *
+         * @return true if the view should have its content updated
+         */
+        public boolean viewNeedsUpdate() {
+            return mViewHolder.needsUpdate();
+        }
+
+        /**
+         * Returns true if the view this LayoutParams is attached to is now representing
+         * potentially invalid data. A LayoutManager should scrap/recycle it.
+         *
+         * @return true if the view is invalid
+         */
+        public boolean isViewInvalid() {
+            return mViewHolder.isInvalid();
+        }
+
+        /**
+         * Returns true if the adapter data item corresponding to the view this LayoutParams
+         * is attached to has been removed from the data set. A LayoutManager may choose to
+         * treat it differently in order to animate its outgoing or disappearing state.
+         *
+         * @return true if the item the view corresponds to was removed from the data set
+         */
+        public boolean isItemRemoved() {
+            return mViewHolder.isRemoved();
+        }
+
+        /**
+         * Returns true if the adapter data item corresponding to the view this LayoutParams
+         * is attached to has been changed in the data set. A LayoutManager may choose to
+         * treat it differently in order to animate its changing state.
+         *
+         * @return true if the item the view corresponds to was changed in the data set
+         */
+        public boolean isItemChanged() {
+            return mViewHolder.isUpdated();
+        }
+
+        /**
+         * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()}
+         */
+        @Deprecated
+        public int getViewPosition() {
+            return mViewHolder.getPosition();
+        }
+
+        /**
+         * Returns the adapter position that the view this LayoutParams is attached to corresponds
+         * to as of latest layout calculation.
+         *
+         * @return the adapter position this view as of latest layout pass
+         */
+        public int getViewLayoutPosition() {
+            return mViewHolder.getLayoutPosition();
+        }
+
+        /**
+         * Returns the up-to-date adapter position that the view this LayoutParams is attached to
+         * corresponds to.
+         *
+         * @return the up-to-date adapter position this view. It may return
+         * {@link RecyclerView#NO_POSITION} if item represented by this View has been removed or
+         * its up-to-date position cannot be calculated.
+         */
+        public int getViewAdapterPosition() {
+            return mViewHolder.getAdapterPosition();
+        }
+    }
+
+    /**
+     * Observer base class for watching changes to an {@link Adapter}.
+     * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}.
+     */
+    public abstract static class AdapterDataObserver {
+        public void onChanged() {
+            // Do nothing
+        }
+
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            // do nothing
+        }
+
+        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+            // fallback to onItemRangeChanged(positionStart, itemCount) if app
+            // does not override this method.
+            onItemRangeChanged(positionStart, itemCount);
+        }
+
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            // do nothing
+        }
+
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            // do nothing
+        }
+
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            // do nothing
+        }
+    }
+
+    /**
+     * <p>Base class for smooth scrolling. Handles basic tracking of the target view position and
+     * provides methods to trigger a programmatic scroll.</p>
+     *
+     * @see LinearSmoothScroller
+     */
+    public abstract static class SmoothScroller {
+
+        private int mTargetPosition = RecyclerView.NO_POSITION;
+
+        private RecyclerView mRecyclerView;
+
+        private LayoutManager mLayoutManager;
+
+        private boolean mPendingInitialRun;
+
+        private boolean mRunning;
+
+        private View mTargetView;
+
+        private final Action mRecyclingAction;
+
+        public SmoothScroller() {
+            mRecyclingAction = new Action(0, 0);
+        }
+
+        /**
+         * Starts a smooth scroll for the given target position.
+         * <p>In each animation step, {@link RecyclerView} will check
+         * for the target view and call either
+         * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
+         * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
+         * SmoothScroller is stopped.</p>
+         *
+         * <p>Note that if RecyclerView finds the target view, it will automatically stop the
+         * SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will
+         * stop calling SmoothScroller in each animation step.</p>
+         */
+        void start(RecyclerView recyclerView, LayoutManager layoutManager) {
+            mRecyclerView = recyclerView;
+            mLayoutManager = layoutManager;
+            if (mTargetPosition == RecyclerView.NO_POSITION) {
+                throw new IllegalArgumentException("Invalid target position");
+            }
+            mRecyclerView.mState.mTargetPosition = mTargetPosition;
+            mRunning = true;
+            mPendingInitialRun = true;
+            mTargetView = findViewByPosition(getTargetPosition());
+            onStart();
+            mRecyclerView.mViewFlinger.postOnAnimation();
+        }
+
+        public void setTargetPosition(int targetPosition) {
+            mTargetPosition = targetPosition;
+        }
+
+        /**
+         * @return The LayoutManager to which this SmoothScroller is attached. Will return
+         * <code>null</code> after the SmoothScroller is stopped.
+         */
+        @Nullable
+        public LayoutManager getLayoutManager() {
+            return mLayoutManager;
+        }
+
+        /**
+         * Stops running the SmoothScroller in each animation callback. Note that this does not
+         * cancel any existing {@link Action} updated by
+         * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
+         * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}.
+         */
+        protected final void stop() {
+            if (!mRunning) {
+                return;
+            }
+            onStop();
+            mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION;
+            mTargetView = null;
+            mTargetPosition = RecyclerView.NO_POSITION;
+            mPendingInitialRun = false;
+            mRunning = false;
+            // trigger a cleanup
+            mLayoutManager.onSmoothScrollerStopped(this);
+            // clear references to avoid any potential leak by a custom smooth scroller
+            mLayoutManager = null;
+            mRecyclerView = null;
+        }
+
+        /**
+         * Returns true if SmoothScroller has been started but has not received the first
+         * animation
+         * callback yet.
+         *
+         * @return True if this SmoothScroller is waiting to start
+         */
+        public boolean isPendingInitialRun() {
+            return mPendingInitialRun;
+        }
+
+
+        /**
+         * @return True if SmoothScroller is currently active
+         */
+        public boolean isRunning() {
+            return mRunning;
+        }
+
+        /**
+         * Returns the adapter position of the target item
+         *
+         * @return Adapter position of the target item or
+         * {@link RecyclerView#NO_POSITION} if no target view is set.
+         */
+        public int getTargetPosition() {
+            return mTargetPosition;
+        }
+
+        private void onAnimation(int dx, int dy) {
+            final RecyclerView recyclerView = mRecyclerView;
+            if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
+                stop();
+            }
+            mPendingInitialRun = false;
+            if (mTargetView != null) {
+                // verify target position
+                if (getChildPosition(mTargetView) == mTargetPosition) {
+                    onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
+                    mRecyclingAction.runIfNecessary(recyclerView);
+                    stop();
+                } else {
+                    Log.e(TAG, "Passed over target position while smooth scrolling.");
+                    mTargetView = null;
+                }
+            }
+            if (mRunning) {
+                onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
+                boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
+                mRecyclingAction.runIfNecessary(recyclerView);
+                if (hadJumpTarget) {
+                    // It is not stopped so needs to be restarted
+                    if (mRunning) {
+                        mPendingInitialRun = true;
+                        recyclerView.mViewFlinger.postOnAnimation();
+                    } else {
+                        stop(); // done
+                    }
+                }
+            }
+        }
+
+        /**
+         * @see RecyclerView#getChildLayoutPosition(android.view.View)
+         */
+        public int getChildPosition(View view) {
+            return mRecyclerView.getChildLayoutPosition(view);
+        }
+
+        /**
+         * @see RecyclerView.LayoutManager#getChildCount()
+         */
+        public int getChildCount() {
+            return mRecyclerView.mLayout.getChildCount();
+        }
+
+        /**
+         * @see RecyclerView.LayoutManager#findViewByPosition(int)
+         */
+        public View findViewByPosition(int position) {
+            return mRecyclerView.mLayout.findViewByPosition(position);
+        }
+
+        /**
+         * @see RecyclerView#scrollToPosition(int)
+         * @deprecated Use {@link Action#jumpTo(int)}.
+         */
+        @Deprecated
+        public void instantScrollToPosition(int position) {
+            mRecyclerView.scrollToPosition(position);
+        }
+
+        protected void onChildAttachedToWindow(View child) {
+            if (getChildPosition(child) == getTargetPosition()) {
+                mTargetView = child;
+                if (DEBUG) {
+                    Log.d(TAG, "smooth scroll target view has been attached");
+                }
+            }
+        }
+
+        /**
+         * Normalizes the vector.
+         * @param scrollVector The vector that points to the target scroll position
+         */
+        protected void normalize(PointF scrollVector) {
+            final double magnitude = Math.sqrt(scrollVector.x * scrollVector.x + scrollVector.y
+                    * scrollVector.y);
+            scrollVector.x /= magnitude;
+            scrollVector.y /= magnitude;
+        }
+
+        /**
+         * Called when smooth scroll is started. This might be a good time to do setup.
+         */
+        protected abstract void onStart();
+
+        /**
+         * Called when smooth scroller is stopped. This is a good place to cleanup your state etc.
+         * @see #stop()
+         */
+        protected abstract void onStop();
+
+        /**
+         * <p>RecyclerView will call this method each time it scrolls until it can find the target
+         * position in the layout.</p>
+         * <p>SmoothScroller should check dx, dy and if scroll should be changed, update the
+         * provided {@link Action} to define the next scroll.</p>
+         *
+         * @param dx        Last scroll amount horizontally
+         * @param dy        Last scroll amount vertically
+         * @param state     Transient state of RecyclerView
+         * @param action    If you want to trigger a new smooth scroll and cancel the previous one,
+         *                  update this object.
+         */
+        protected abstract void onSeekTargetStep(int dx, int dy, State state, Action action);
+
+        /**
+         * Called when the target position is laid out. This is the last callback SmoothScroller
+         * will receive and it should update the provided {@link Action} to define the scroll
+         * details towards the target view.
+         * @param targetView    The view element which render the target position.
+         * @param state         Transient state of RecyclerView
+         * @param action        Action instance that you should update to define final scroll action
+         *                      towards the targetView
+         */
+        protected abstract void onTargetFound(View targetView, State state, Action action);
+
+        /**
+         * Holds information about a smooth scroll request by a {@link SmoothScroller}.
+         */
+        public static class Action {
+
+            public static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
+
+            private int mDx;
+
+            private int mDy;
+
+            private int mDuration;
+
+            private int mJumpToPosition = NO_POSITION;
+
+            private Interpolator mInterpolator;
+
+            private boolean mChanged = false;
+
+            // we track this variable to inform custom implementer if they are updating the action
+            // in every animation callback
+            private int mConsecutiveUpdates = 0;
+
+            /**
+             * @param dx Pixels to scroll horizontally
+             * @param dy Pixels to scroll vertically
+             */
+            public Action(int dx, int dy) {
+                this(dx, dy, UNDEFINED_DURATION, null);
+            }
+
+            /**
+             * @param dx       Pixels to scroll horizontally
+             * @param dy       Pixels to scroll vertically
+             * @param duration Duration of the animation in milliseconds
+             */
+            public Action(int dx, int dy, int duration) {
+                this(dx, dy, duration, null);
+            }
+
+            /**
+             * @param dx           Pixels to scroll horizontally
+             * @param dy           Pixels to scroll vertically
+             * @param duration     Duration of the animation in milliseconds
+             * @param interpolator Interpolator to be used when calculating scroll position in each
+             *                     animation step
+             */
+            public Action(int dx, int dy, int duration, Interpolator interpolator) {
+                mDx = dx;
+                mDy = dy;
+                mDuration = duration;
+                mInterpolator = interpolator;
+            }
+
+            /**
+             * Instead of specifying pixels to scroll, use the target position to jump using
+             * {@link RecyclerView#scrollToPosition(int)}.
+             * <p>
+             * You may prefer using this method if scroll target is really far away and you prefer
+             * to jump to a location and smooth scroll afterwards.
+             * <p>
+             * Note that calling this method takes priority over other update methods such as
+             * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)},
+             * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call
+             * {@link #jumpTo(int)}, the other changes will not be considered for this animation
+             * frame.
+             *
+             * @param targetPosition The target item position to scroll to using instant scrolling.
+             */
+            public void jumpTo(int targetPosition) {
+                mJumpToPosition = targetPosition;
+            }
+
+            boolean hasJumpTarget() {
+                return mJumpToPosition >= 0;
+            }
+
+            void runIfNecessary(RecyclerView recyclerView) {
+                if (mJumpToPosition >= 0) {
+                    final int position = mJumpToPosition;
+                    mJumpToPosition = NO_POSITION;
+                    recyclerView.jumpToPositionForSmoothScroller(position);
+                    mChanged = false;
+                    return;
+                }
+                if (mChanged) {
+                    validate();
+                    if (mInterpolator == null) {
+                        if (mDuration == UNDEFINED_DURATION) {
+                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
+                        } else {
+                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
+                        }
+                    } else {
+                        recyclerView.mViewFlinger.smoothScrollBy(
+                                mDx, mDy, mDuration, mInterpolator);
+                    }
+                    mConsecutiveUpdates++;
+                    if (mConsecutiveUpdates > 10) {
+                        // A new action is being set in every animation step. This looks like a bad
+                        // implementation. Inform developer.
+                        Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure"
+                                + " you are not changing it unless necessary");
+                    }
+                    mChanged = false;
+                } else {
+                    mConsecutiveUpdates = 0;
+                }
+            }
+
+            private void validate() {
+                if (mInterpolator != null && mDuration < 1) {
+                    throw new IllegalStateException("If you provide an interpolator, you must"
+                            + " set a positive duration");
+                } else if (mDuration < 1) {
+                    throw new IllegalStateException("Scroll duration must be a positive number");
+                }
+            }
+
+            public int getDx() {
+                return mDx;
+            }
+
+            public void setDx(int dx) {
+                mChanged = true;
+                mDx = dx;
+            }
+
+            public int getDy() {
+                return mDy;
+            }
+
+            public void setDy(int dy) {
+                mChanged = true;
+                mDy = dy;
+            }
+
+            public int getDuration() {
+                return mDuration;
+            }
+
+            public void setDuration(int duration) {
+                mChanged = true;
+                mDuration = duration;
+            }
+
+            public Interpolator getInterpolator() {
+                return mInterpolator;
+            }
+
+            /**
+             * Sets the interpolator to calculate scroll steps
+             * @param interpolator The interpolator to use. If you specify an interpolator, you must
+             *                     also set the duration.
+             * @see #setDuration(int)
+             */
+            public void setInterpolator(Interpolator interpolator) {
+                mChanged = true;
+                mInterpolator = interpolator;
+            }
+
+            /**
+             * Updates the action with given parameters.
+             * @param dx Pixels to scroll horizontally
+             * @param dy Pixels to scroll vertically
+             * @param duration Duration of the animation in milliseconds
+             * @param interpolator Interpolator to be used when calculating scroll position in each
+             *                     animation step
+             */
+            public void update(int dx, int dy, int duration, Interpolator interpolator) {
+                mDx = dx;
+                mDy = dy;
+                mDuration = duration;
+                mInterpolator = interpolator;
+                mChanged = true;
+            }
+        }
+
+        /**
+         * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager}
+         * to provide a hint to a {@link SmoothScroller} about the location of the target position.
+         */
+        public interface ScrollVectorProvider {
+            /**
+             * Should calculate the vector that points to the direction where the target position
+             * can be found.
+             * <p>
+             * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards
+             * the target position.
+             * <p>
+             * The magnitude of the vector is not important. It is always normalized before being
+             * used by the {@link LinearSmoothScroller}.
+             * <p>
+             * LayoutManager should not check whether the position exists in the adapter or not.
+             *
+             * @param targetPosition the target position to which the returned vector should point
+             *
+             * @return the scroll vector for a given position.
+             */
+            PointF computeScrollVectorForPosition(int targetPosition);
+        }
+    }
+
+    static class AdapterDataObservable extends Observable<AdapterDataObserver> {
+        public boolean hasObservers() {
+            return !mObservers.isEmpty();
+        }
+
+        public void notifyChanged() {
+            // since onChanged() is implemented by the app, it could do anything, including
+            // removing itself from {@link mObservers} - and that could cause problems if
+            // an iterator is used on the ArrayList {@link mObservers}.
+            // to avoid such problems, just march thru the list in the reverse order.
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onChanged();
+            }
+        }
+
+        public void notifyItemRangeChanged(int positionStart, int itemCount) {
+            notifyItemRangeChanged(positionStart, itemCount, null);
+        }
+
+        public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+            // since onItemRangeChanged() is implemented by the app, it could do anything, including
+            // removing itself from {@link mObservers} - and that could cause problems if
+            // an iterator is used on the ArrayList {@link mObservers}.
+            // to avoid such problems, just march thru the list in the reverse order.
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
+            }
+        }
+
+        public void notifyItemRangeInserted(int positionStart, int itemCount) {
+            // since onItemRangeInserted() is implemented by the app, it could do anything,
+            // including removing itself from {@link mObservers} - and that could cause problems if
+            // an iterator is used on the ArrayList {@link mObservers}.
+            // to avoid such problems, just march thru the list in the reverse order.
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
+            }
+        }
+
+        public void notifyItemRangeRemoved(int positionStart, int itemCount) {
+            // since onItemRangeRemoved() is implemented by the app, it could do anything, including
+            // removing itself from {@link mObservers} - and that could cause problems if
+            // an iterator is used on the ArrayList {@link mObservers}.
+            // to avoid such problems, just march thru the list in the reverse order.
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
+            }
+        }
+
+        public void notifyItemMoved(int fromPosition, int toPosition) {
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1);
+            }
+        }
+    }
+
+    /**
+     * This is public so that the CREATOR can be access on cold launch.
+     * @hide
+     */
+    public static class SavedState extends AbsSavedState {
+
+        Parcelable mLayoutState;
+
+        /**
+         * called by CREATOR
+         */
+        SavedState(Parcel in) {
+            super(in);
+            mLayoutState = in.readParcelable(LayoutManager.class.getClassLoader());
+        }
+
+        /**
+         * Called by onSaveInstanceState
+         */
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeParcelable(mLayoutState, 0);
+        }
+
+        void copyFrom(SavedState other) {
+            mLayoutState = other.mLayoutState;
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel in) {
+                        return new SavedState(in);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+    }
+    /**
+     * <p>Contains useful information about the current RecyclerView state like target scroll
+     * position or view focus. State object can also keep arbitrary data, identified by resource
+     * ids.</p>
+     * <p>Often times, RecyclerView components will need to pass information between each other.
+     * To provide a well defined data bus between components, RecyclerView passes the same State
+     * object to component callbacks and these components can use it to exchange data.</p>
+     * <p>If you implement custom components, you can use State's put/get/remove methods to pass
+     * data between your components without needing to manage their lifecycles.</p>
+     */
+    public static class State {
+        static final int STEP_START = 1;
+        static final int STEP_LAYOUT = 1 << 1;
+        static final int STEP_ANIMATIONS = 1 << 2;
+
+        void assertLayoutStep(int accepted) {
+            if ((accepted & mLayoutStep) == 0) {
+                throw new IllegalStateException("Layout state should be one of "
+                        + Integer.toBinaryString(accepted) + " but it is "
+                        + Integer.toBinaryString(mLayoutStep));
+            }
+        }
+
+
+        /** Owned by SmoothScroller */
+        private int mTargetPosition = RecyclerView.NO_POSITION;
+
+        private SparseArray<Object> mData;
+
+        ////////////////////////////////////////////////////////////////////////////////////////////
+        // Fields below are carried from one layout pass to the next
+        ////////////////////////////////////////////////////////////////////////////////////////////
+
+        /**
+         * Number of items adapter had in the previous layout.
+         */
+        int mPreviousLayoutItemCount = 0;
+
+        /**
+         * Number of items that were NOT laid out but has been deleted from the adapter after the
+         * previous layout.
+         */
+        int mDeletedInvisibleItemCountSincePreviousLayout = 0;
+
+        ////////////////////////////////////////////////////////////////////////////////////////////
+        // Fields below must be updated or cleared before they are used (generally before a pass)
+        ////////////////////////////////////////////////////////////////////////////////////////////
+
+        @IntDef(flag = true, value = {
+                STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        @interface LayoutState {}
+
+        @LayoutState
+        int mLayoutStep = STEP_START;
+
+        /**
+         * Number of items adapter has.
+         */
+        int mItemCount = 0;
+
+        boolean mStructureChanged = false;
+
+        boolean mInPreLayout = false;
+
+        boolean mTrackOldChangeHolders = false;
+
+        boolean mIsMeasuring = false;
+
+        ////////////////////////////////////////////////////////////////////////////////////////////
+        // Fields below are always reset outside of the pass (or passes) that use them
+        ////////////////////////////////////////////////////////////////////////////////////////////
+
+        boolean mRunSimpleAnimations = false;
+
+        boolean mRunPredictiveAnimations = false;
+
+        /**
+         * This data is saved before a layout calculation happens. After the layout is finished,
+         * if the previously focused view has been replaced with another view for the same item, we
+         * move the focus to the new item automatically.
+         */
+        int mFocusedItemPosition;
+        long mFocusedItemId;
+        // when a sub child has focus, record its id and see if we can directly request focus on
+        // that one instead
+        int mFocusedSubChildId;
+
+        ////////////////////////////////////////////////////////////////////////////////////////////
+
+        State reset() {
+            mTargetPosition = RecyclerView.NO_POSITION;
+            if (mData != null) {
+                mData.clear();
+            }
+            mItemCount = 0;
+            mStructureChanged = false;
+            mIsMeasuring = false;
+            return this;
+        }
+
+        /**
+         * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially
+         * prior to any layout passes.
+         *
+         * <p>Don't touch any state stored between layout passes, only reset per-layout state, so
+         * that Recycler#getViewForPosition() can function safely.</p>
+         */
+        void prepareForNestedPrefetch(Adapter adapter) {
+            mLayoutStep = STEP_START;
+            mItemCount = adapter.getItemCount();
+            mStructureChanged = false;
+            mInPreLayout = false;
+            mTrackOldChangeHolders = false;
+            mIsMeasuring = false;
+        }
+
+        /**
+         * Returns true if the RecyclerView is currently measuring the layout. This value is
+         * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView
+         * has non-exact measurement specs.
+         * <p>
+         * Note that if the LayoutManager supports predictive animations and it is calculating the
+         * pre-layout step, this value will be {@code false} even if the RecyclerView is in
+         * {@code onMeasure} call. This is because pre-layout means the previous state of the
+         * RecyclerView and measurements made for that state cannot change the RecyclerView's size.
+         * LayoutManager is always guaranteed to receive another call to
+         * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens.
+         *
+         * @return True if the RecyclerView is currently calculating its bounds, false otherwise.
+         */
+        public boolean isMeasuring() {
+            return mIsMeasuring;
+        }
+
+        /**
+         * Returns true if
+         * @return
+         */
+        public boolean isPreLayout() {
+            return mInPreLayout;
+        }
+
+        /**
+         * Returns whether RecyclerView will run predictive animations in this layout pass
+         * or not.
+         *
+         * @return true if RecyclerView is calculating predictive animations to be run at the end
+         *         of the layout pass.
+         */
+        public boolean willRunPredictiveAnimations() {
+            return mRunPredictiveAnimations;
+        }
+
+        /**
+         * Returns whether RecyclerView will run simple animations in this layout pass
+         * or not.
+         *
+         * @return true if RecyclerView is calculating simple animations to be run at the end of
+         *         the layout pass.
+         */
+        public boolean willRunSimpleAnimations() {
+            return mRunSimpleAnimations;
+        }
+
+        /**
+         * Removes the mapping from the specified id, if there was any.
+         * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to
+         *                   preserve cross functionality and avoid conflicts.
+         */
+        public void remove(int resourceId) {
+            if (mData == null) {
+                return;
+            }
+            mData.remove(resourceId);
+        }
+
+        /**
+         * Gets the Object mapped from the specified id, or <code>null</code>
+         * if no such data exists.
+         *
+         * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.*
+         *                   to
+         *                   preserve cross functionality and avoid conflicts.
+         */
+        public <T> T get(int resourceId) {
+            if (mData == null) {
+                return null;
+            }
+            return (T) mData.get(resourceId);
+        }
+
+        /**
+         * Adds a mapping from the specified id to the specified value, replacing the previous
+         * mapping from the specified key if there was one.
+         *
+         * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to
+         *                   preserve cross functionality and avoid conflicts.
+         * @param data       The data you want to associate with the resourceId.
+         */
+        public void put(int resourceId, Object data) {
+            if (mData == null) {
+                mData = new SparseArray<Object>();
+            }
+            mData.put(resourceId, data);
+        }
+
+        /**
+         * If scroll is triggered to make a certain item visible, this value will return the
+         * adapter index of that item.
+         * @return Adapter index of the target item or
+         * {@link RecyclerView#NO_POSITION} if there is no target
+         * position.
+         */
+        public int getTargetScrollPosition() {
+            return mTargetPosition;
+        }
+
+        /**
+         * Returns if current scroll has a target position.
+         * @return true if scroll is being triggered to make a certain position visible
+         * @see #getTargetScrollPosition()
+         */
+        public boolean hasTargetScrollPosition() {
+            return mTargetPosition != RecyclerView.NO_POSITION;
+        }
+
+        /**
+         * @return true if the structure of the data set has changed since the last call to
+         *         onLayoutChildren, false otherwise
+         */
+        public boolean didStructureChange() {
+            return mStructureChanged;
+        }
+
+        /**
+         * Returns the total number of items that can be laid out. Note that this number is not
+         * necessarily equal to the number of items in the adapter, so you should always use this
+         * number for your position calculations and never access the adapter directly.
+         * <p>
+         * RecyclerView listens for Adapter's notify events and calculates the effects of adapter
+         * data changes on existing Views. These calculations are used to decide which animations
+         * should be run.
+         * <p>
+         * To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to
+         * present the correct state to LayoutManager in pre-layout pass.
+         * <p>
+         * For example, a newly added item is not included in pre-layout item count because
+         * pre-layout reflects the contents of the adapter before the item is added. Behind the
+         * scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that
+         * LayoutManager does not know about the new item's existence in pre-layout. The item will
+         * be available in second layout pass and will be included in the item count. Similar
+         * adjustments are made for moved and removed items as well.
+         * <p>
+         * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method.
+         *
+         * @return The number of items currently available
+         * @see LayoutManager#getItemCount()
+         */
+        public int getItemCount() {
+            return mInPreLayout
+                    ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout)
+                    : mItemCount;
+        }
+
+        @Override
+        public String toString() {
+            return "State{"
+                    + "mTargetPosition=" + mTargetPosition
+                    + ", mData=" + mData
+                    + ", mItemCount=" + mItemCount
+                    + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount
+                    + ", mDeletedInvisibleItemCountSincePreviousLayout="
+                    + mDeletedInvisibleItemCountSincePreviousLayout
+                    + ", mStructureChanged=" + mStructureChanged
+                    + ", mInPreLayout=" + mInPreLayout
+                    + ", mRunSimpleAnimations=" + mRunSimpleAnimations
+                    + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations
+                    + '}';
+        }
+    }
+
+    /**
+     * This class defines the behavior of fling if the developer wishes to handle it.
+     * <p>
+     * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior.
+     *
+     * @see #setOnFlingListener(OnFlingListener)
+     */
+    public abstract static class OnFlingListener {
+
+        /**
+         * Override this to handle a fling given the velocities in both x and y directions.
+         * Note that this method will only be called if the associated {@link LayoutManager}
+         * supports scrolling and the fling is not handled by nested scrolls first.
+         *
+         * @param velocityX the fling velocity on the X axis
+         * @param velocityY the fling velocity on the Y axis
+         *
+         * @return true if the fling washandled, false otherwise.
+         */
+        public abstract boolean onFling(int velocityX, int velocityY);
+    }
+
+    /**
+     * Internal listener that manages items after animations finish. This is how items are
+     * retained (not recycled) during animations, but allowed to be recycled afterwards.
+     * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished()
+     * method on the animator's listener when it is done animating any item.
+     */
+    private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {
+
+        ItemAnimatorRestoreListener() {
+        }
+
+        @Override
+        public void onAnimationFinished(ViewHolder item) {
+            item.setIsRecyclable(true);
+            if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh
+                item.mShadowedHolder = null;
+            }
+            // always null this because an OldViewHolder can never become NewViewHolder w/o being
+            // recycled.
+            item.mShadowingHolder = null;
+            if (!item.shouldBeKeptAsChild()) {
+                if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
+                    removeDetachedView(item.itemView, false);
+                }
+            }
+        }
+    }
+
+    /**
+     * This class defines the animations that take place on items as changes are made
+     * to the adapter.
+     *
+     * Subclasses of ItemAnimator can be used to implement custom animations for actions on
+     * ViewHolder items. The RecyclerView will manage retaining these items while they
+     * are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)}
+     * when a ViewHolder's animation is finished. In other words, there must be a matching
+     * {@link #dispatchAnimationFinished(ViewHolder)} call for each
+     * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()},
+     * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+     * animateChange()}
+     * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()},
+     * and
+     * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+     * animateDisappearance()} call.
+     *
+     * <p>By default, RecyclerView uses {@link DefaultItemAnimator}.</p>
+     *
+     * @see #setItemAnimator(ItemAnimator)
+     */
+    @SuppressWarnings("UnusedParameters")
+    public abstract static class ItemAnimator {
+
+        /**
+         * The Item represented by this ViewHolder is updated.
+         * <p>
+         * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+         */
+        public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE;
+
+        /**
+         * The Item represented by this ViewHolder is removed from the adapter.
+         * <p>
+         * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+         */
+        public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED;
+
+        /**
+         * Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content
+         * represented by this ViewHolder is invalid.
+         * <p>
+         * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+         */
+        public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID;
+
+        /**
+         * The position of the Item represented by this ViewHolder has been changed. This flag is
+         * not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to
+         * any adapter change that may have a side effect on this item. (e.g. The item before this
+         * one has been removed from the Adapter).
+         * <p>
+         * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+         */
+        public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED;
+
+        /**
+         * This ViewHolder was not laid out but has been added to the layout in pre-layout state
+         * by the {@link LayoutManager}. This means that the item was already in the Adapter but
+         * invisible and it may become visible in the post layout phase. LayoutManagers may prefer
+         * to add new items in pre-layout to specify their virtual location when they are invisible
+         * (e.g. to specify the item should <i>animate in</i> from below the visible area).
+         * <p>
+         * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+         */
+        public static final int FLAG_APPEARED_IN_PRE_LAYOUT =
+                ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT;
+
+        /**
+         * The set of flags that might be passed to
+         * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         */
+        @IntDef(flag = true, value = {
+                FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED,
+                FLAG_APPEARED_IN_PRE_LAYOUT
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface AdapterChanges {}
+        private ItemAnimatorListener mListener = null;
+        private ArrayList<ItemAnimatorFinishedListener> mFinishedListeners =
+                new ArrayList<ItemAnimatorFinishedListener>();
+
+        private long mAddDuration = 120;
+        private long mRemoveDuration = 120;
+        private long mMoveDuration = 250;
+        private long mChangeDuration = 250;
+
+        /**
+         * Gets the current duration for which all move animations will run.
+         *
+         * @return The current move duration
+         */
+        public long getMoveDuration() {
+            return mMoveDuration;
+        }
+
+        /**
+         * Sets the duration for which all move animations will run.
+         *
+         * @param moveDuration The move duration
+         */
+        public void setMoveDuration(long moveDuration) {
+            mMoveDuration = moveDuration;
+        }
+
+        /**
+         * Gets the current duration for which all add animations will run.
+         *
+         * @return The current add duration
+         */
+        public long getAddDuration() {
+            return mAddDuration;
+        }
+
+        /**
+         * Sets the duration for which all add animations will run.
+         *
+         * @param addDuration The add duration
+         */
+        public void setAddDuration(long addDuration) {
+            mAddDuration = addDuration;
+        }
+
+        /**
+         * Gets the current duration for which all remove animations will run.
+         *
+         * @return The current remove duration
+         */
+        public long getRemoveDuration() {
+            return mRemoveDuration;
+        }
+
+        /**
+         * Sets the duration for which all remove animations will run.
+         *
+         * @param removeDuration The remove duration
+         */
+        public void setRemoveDuration(long removeDuration) {
+            mRemoveDuration = removeDuration;
+        }
+
+        /**
+         * Gets the current duration for which all change animations will run.
+         *
+         * @return The current change duration
+         */
+        public long getChangeDuration() {
+            return mChangeDuration;
+        }
+
+        /**
+         * Sets the duration for which all change animations will run.
+         *
+         * @param changeDuration The change duration
+         */
+        public void setChangeDuration(long changeDuration) {
+            mChangeDuration = changeDuration;
+        }
+
+        /**
+         * Internal only:
+         * Sets the listener that must be called when the animator is finished
+         * animating the item (or immediately if no animation happens). This is set
+         * internally and is not intended to be set by external code.
+         *
+         * @param listener The listener that must be called.
+         */
+        void setListener(ItemAnimatorListener listener) {
+            mListener = listener;
+        }
+
+        /**
+         * Called by the RecyclerView before the layout begins. Item animator should record
+         * necessary information about the View before it is potentially rebound, moved or removed.
+         * <p>
+         * The data returned from this method will be passed to the related <code>animate**</code>
+         * methods.
+         * <p>
+         * Note that this method may be called after pre-layout phase if LayoutManager adds new
+         * Views to the layout in pre-layout pass.
+         * <p>
+         * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of
+         * the View and the adapter change flags.
+         *
+         * @param state       The current State of RecyclerView which includes some useful data
+         *                    about the layout that will be calculated.
+         * @param viewHolder  The ViewHolder whose information should be recorded.
+         * @param changeFlags Additional information about what changes happened in the Adapter
+         *                    about the Item represented by this ViewHolder. For instance, if
+         *                    item is deleted from the adapter, {@link #FLAG_REMOVED} will be set.
+         * @param payloads    The payload list that was previously passed to
+         *                    {@link Adapter#notifyItemChanged(int, Object)} or
+         *                    {@link Adapter#notifyItemRangeChanged(int, int, Object)}.
+         *
+         * @return An ItemHolderInfo instance that preserves necessary information about the
+         * ViewHolder. This object will be passed back to related <code>animate**</code> methods
+         * after layout is complete.
+         *
+         * @see #recordPostLayoutInformation(State, ViewHolder)
+         * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         */
+        public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
+                @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
+                @NonNull List<Object> payloads) {
+            return obtainHolderInfo().setFrom(viewHolder);
+        }
+
+        /**
+         * Called by the RecyclerView after the layout is complete. Item animator should record
+         * necessary information about the View's final state.
+         * <p>
+         * The data returned from this method will be passed to the related <code>animate**</code>
+         * methods.
+         * <p>
+         * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of
+         * the View.
+         *
+         * @param state      The current State of RecyclerView which includes some useful data about
+         *                   the layout that will be calculated.
+         * @param viewHolder The ViewHolder whose information should be recorded.
+         *
+         * @return An ItemHolderInfo that preserves necessary information about the ViewHolder.
+         * This object will be passed back to related <code>animate**</code> methods when
+         * RecyclerView decides how items should be animated.
+         *
+         * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+         * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         */
+        public @NonNull ItemHolderInfo recordPostLayoutInformation(@NonNull State state,
+                @NonNull ViewHolder viewHolder) {
+            return obtainHolderInfo().setFrom(viewHolder);
+        }
+
+        /**
+         * Called by the RecyclerView when a ViewHolder has disappeared from the layout.
+         * <p>
+         * This means that the View was a child of the LayoutManager when layout started but has
+         * been removed by the LayoutManager. It might have been removed from the adapter or simply
+         * become invisible due to other factors. You can distinguish these two cases by checking
+         * the change flags that were passed to
+         * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         * <p>
+         * Note that when a ViewHolder both changes and disappears in the same layout pass, the
+         * animation callback method which will be called by the RecyclerView depends on the
+         * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the
+         * LayoutManager's decision whether to layout the changed version of a disappearing
+         * ViewHolder or not. RecyclerView will call
+         * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator
+         * returns {@code false} from
+         * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the
+         * LayoutManager lays out a new disappearing view that holds the updated information.
+         * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views.
+         * <p>
+         * If LayoutManager supports predictive animations, it might provide a target disappear
+         * location for the View by laying it out in that location. When that happens,
+         * RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the
+         * response of that call will be passed to this method as the <code>postLayoutInfo</code>.
+         * <p>
+         * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
+         * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
+         * decides not to animate the view).
+         *
+         * @param viewHolder    The ViewHolder which should be animated
+         * @param preLayoutInfo The information that was returned from
+         *                      {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         * @param postLayoutInfo The information that was returned from
+         *                       {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be
+         *                       null if the LayoutManager did not layout the item.
+         *
+         * @return true if a later call to {@link #runPendingAnimations()} is requested,
+         * false otherwise.
+         */
+        public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder,
+                @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo);
+
+        /**
+         * Called by the RecyclerView when a ViewHolder is added to the layout.
+         * <p>
+         * In detail, this means that the ViewHolder was <b>not</b> a child when the layout started
+         * but has  been added by the LayoutManager. It might be newly added to the adapter or
+         * simply become visible due to other factors.
+         * <p>
+         * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
+         * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
+         * decides not to animate the view).
+         *
+         * @param viewHolder     The ViewHolder which should be animated
+         * @param preLayoutInfo  The information that was returned from
+         *                       {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         *                       Might be null if Item was just added to the adapter or
+         *                       LayoutManager does not support predictive animations or it could
+         *                       not predict that this ViewHolder will become visible.
+         * @param postLayoutInfo The information that was returned from {@link
+         *                       #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         *
+         * @return true if a later call to {@link #runPendingAnimations()} is requested,
+         * false otherwise.
+         */
+        public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder,
+                @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
+
+        /**
+         * Called by the RecyclerView when a ViewHolder is present in both before and after the
+         * layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call
+         * for it or a {@link Adapter#notifyDataSetChanged()} call.
+         * <p>
+         * This ViewHolder still represents the same data that it was representing when the layout
+         * started but its position / size may be changed by the LayoutManager.
+         * <p>
+         * If the Item's layout position didn't change, RecyclerView still calls this method because
+         * it does not track this information (or does not necessarily know that an animation is
+         * not required). Your ItemAnimator should handle this case and if there is nothing to
+         * animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return
+         * <code>false</code>.
+         * <p>
+         * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
+         * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
+         * decides not to animate the view).
+         *
+         * @param viewHolder     The ViewHolder which should be animated
+         * @param preLayoutInfo  The information that was returned from
+         *                       {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         * @param postLayoutInfo The information that was returned from {@link
+         *                       #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         *
+         * @return true if a later call to {@link #runPendingAnimations()} is requested,
+         * false otherwise.
+         */
+        public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder,
+                @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
+
+        /**
+         * Called by the RecyclerView when an adapter item is present both before and after the
+         * layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call
+         * for it. This method may also be called when
+         * {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that
+         * RecyclerView could still rebind views to the same ViewHolders. If viewType changes when
+         * {@link Adapter#notifyDataSetChanged()} is called, this method <b>will not</b> be called,
+         * instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be
+         * called for the new ViewHolder and the old one will be recycled.
+         * <p>
+         * If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is
+         * a good possibility that item contents didn't really change but it is rebound from the
+         * adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the
+         * screen didn't change and your animator should handle this case as well and avoid creating
+         * unnecessary animations.
+         * <p>
+         * When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the
+         * previous presentation of the item as-is and supply a new ViewHolder for the updated
+         * presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}.
+         * This is useful if you don't know the contents of the Item and would like
+         * to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique).
+         * <p>
+         * When you are writing a custom item animator for your layout, it might be more performant
+         * and elegant to re-use the same ViewHolder and animate the content changes manually.
+         * <p>
+         * When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change.
+         * If the Item's view type has changed or ItemAnimator returned <code>false</code> for
+         * this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the
+         * <code>oldHolder</code> and <code>newHolder</code> will be different ViewHolder instances
+         * which represent the same Item. In that case, only the new ViewHolder is visible
+         * to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations.
+         * <p>
+         * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct
+         * ViewHolder when their animation is complete
+         * (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to
+         * animate the view).
+         * <p>
+         *  If oldHolder and newHolder are the same instance, you should call
+         * {@link #dispatchAnimationFinished(ViewHolder)} <b>only once</b>.
+         * <p>
+         * Note that when a ViewHolder both changes and disappears in the same layout pass, the
+         * animation callback method which will be called by the RecyclerView depends on the
+         * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the
+         * LayoutManager's decision whether to layout the changed version of a disappearing
+         * ViewHolder or not. RecyclerView will call
+         * {@code animateChange} instead of
+         * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateDisappearance} if and only if the ItemAnimator returns {@code false} from
+         * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the
+         * LayoutManager lays out a new disappearing view that holds the updated information.
+         * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views.
+         *
+         * @param oldHolder     The ViewHolder before the layout is started, might be the same
+         *                      instance with newHolder.
+         * @param newHolder     The ViewHolder after the layout is finished, might be the same
+         *                      instance with oldHolder.
+         * @param preLayoutInfo  The information that was returned from
+         *                       {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         * @param postLayoutInfo The information that was returned from {@link
+         *                       #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+         *
+         * @return true if a later call to {@link #runPendingAnimations()} is requested,
+         * false otherwise.
+         */
+        public abstract boolean animateChange(@NonNull ViewHolder oldHolder,
+                @NonNull ViewHolder newHolder,
+                @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
+
+        @AdapterChanges static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) {
+            int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED);
+            if (viewHolder.isInvalid()) {
+                return FLAG_INVALIDATED;
+            }
+            if ((flags & FLAG_INVALIDATED) == 0) {
+                final int oldPos = viewHolder.getOldPosition();
+                final int pos = viewHolder.getAdapterPosition();
+                if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) {
+                    flags |= FLAG_MOVED;
+                }
+            }
+            return flags;
+        }
+
+        /**
+         * Called when there are pending animations waiting to be started. This state
+         * is governed by the return values from
+         * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateAppearance()},
+         * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateChange()}
+         * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animatePersistence()}, and
+         * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be
+         * called later to start the associated animations. runPendingAnimations() will be scheduled
+         * to be run on the next frame.
+         */
+        public abstract void runPendingAnimations();
+
+        /**
+         * Method called when an animation on a view should be ended immediately.
+         * This could happen when other events, like scrolling, occur, so that
+         * animating views can be quickly put into their proper end locations.
+         * Implementations should ensure that any animations running on the item
+         * are canceled and affected properties are set to their end values.
+         * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished
+         * animation since the animations are effectively done when this method is called.
+         *
+         * @param item The item for which an animation should be stopped.
+         */
+        public abstract void endAnimation(ViewHolder item);
+
+        /**
+         * Method called when all item animations should be ended immediately.
+         * This could happen when other events, like scrolling, occur, so that
+         * animating views can be quickly put into their proper end locations.
+         * Implementations should ensure that any animations running on any items
+         * are canceled and affected properties are set to their end values.
+         * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished
+         * animation since the animations are effectively done when this method is called.
+         */
+        public abstract void endAnimations();
+
+        /**
+         * Method which returns whether there are any item animations currently running.
+         * This method can be used to determine whether to delay other actions until
+         * animations end.
+         *
+         * @return true if there are any item animations currently running, false otherwise.
+         */
+        public abstract boolean isRunning();
+
+        /**
+         * Method to be called by subclasses when an animation is finished.
+         * <p>
+         * For each call RecyclerView makes to
+         * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateAppearance()},
+         * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animatePersistence()}, or
+         * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateDisappearance()}, there
+         * should
+         * be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass.
+         * <p>
+         * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateChange()}, subclass should call this method for both the <code>oldHolder</code>
+         * and <code>newHolder</code>  (if they are not the same instance).
+         *
+         * @param viewHolder The ViewHolder whose animation is finished.
+         * @see #onAnimationFinished(ViewHolder)
+         */
+        public final void dispatchAnimationFinished(ViewHolder viewHolder) {
+            onAnimationFinished(viewHolder);
+            if (mListener != null) {
+                mListener.onAnimationFinished(viewHolder);
+            }
+        }
+
+        /**
+         * Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the
+         * ItemAnimator.
+         *
+         * @param viewHolder The ViewHolder whose animation is finished. There might still be other
+         *                   animations running on this ViewHolder.
+         * @see #dispatchAnimationFinished(ViewHolder)
+         */
+        public void onAnimationFinished(ViewHolder viewHolder) {
+        }
+
+        /**
+         * Method to be called by subclasses when an animation is started.
+         * <p>
+         * For each call RecyclerView makes to
+         * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateAppearance()},
+         * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animatePersistence()}, or
+         * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateDisappearance()}, there should be a matching
+         * {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass.
+         * <p>
+         * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+         * animateChange()}, subclass should call this method for both the <code>oldHolder</code>
+         * and <code>newHolder</code> (if they are not the same instance).
+         * <p>
+         * If your ItemAnimator decides not to animate a ViewHolder, it should call
+         * {@link #dispatchAnimationFinished(ViewHolder)} <b>without</b> calling
+         * {@link #dispatchAnimationStarted(ViewHolder)}.
+         *
+         * @param viewHolder The ViewHolder whose animation is starting.
+         * @see #onAnimationStarted(ViewHolder)
+         */
+        public final void dispatchAnimationStarted(ViewHolder viewHolder) {
+            onAnimationStarted(viewHolder);
+        }
+
+        /**
+         * Called when a new animation is started on the given ViewHolder.
+         *
+         * @param viewHolder The ViewHolder which started animating. Note that the ViewHolder
+         *                   might already be animating and this might be another animation.
+         * @see #dispatchAnimationStarted(ViewHolder)
+         */
+        public void onAnimationStarted(ViewHolder viewHolder) {
+
+        }
+
+        /**
+         * Like {@link #isRunning()}, this method returns whether there are any item
+         * animations currently running. Additionally, the listener passed in will be called
+         * when there are no item animations running, either immediately (before the method
+         * returns) if no animations are currently running, or when the currently running
+         * animations are {@link #dispatchAnimationsFinished() finished}.
+         *
+         * <p>Note that the listener is transient - it is either called immediately and not
+         * stored at all, or stored only until it is called when running animations
+         * are finished sometime later.</p>
+         *
+         * @param listener A listener to be called immediately if no animations are running
+         * or later when currently-running animations have finished. A null listener is
+         * equivalent to calling {@link #isRunning()}.
+         * @return true if there are any item animations currently running, false otherwise.
+         */
+        public final boolean isRunning(ItemAnimatorFinishedListener listener) {
+            boolean running = isRunning();
+            if (listener != null) {
+                if (!running) {
+                    listener.onAnimationsFinished();
+                } else {
+                    mFinishedListeners.add(listener);
+                }
+            }
+            return running;
+        }
+
+        /**
+         * When an item is changed, ItemAnimator can decide whether it wants to re-use
+         * the same ViewHolder for animations or RecyclerView should create a copy of the
+         * item and ItemAnimator will use both to run the animation (e.g. cross-fade).
+         * <p>
+         * Note that this method will only be called if the {@link ViewHolder} still has the same
+         * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive
+         * both {@link ViewHolder}s in the
+         * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method.
+         * <p>
+         * If your application is using change payloads, you can override
+         * {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads.
+         *
+         * @param viewHolder The ViewHolder which represents the changed item's old content.
+         *
+         * @return True if RecyclerView should just rebind to the same ViewHolder or false if
+         *         RecyclerView should create a new ViewHolder and pass this ViewHolder to the
+         *         ItemAnimator to animate. Default implementation returns <code>true</code>.
+         *
+         * @see #canReuseUpdatedViewHolder(ViewHolder, List)
+         */
+        public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) {
+            return true;
+        }
+
+        /**
+         * When an item is changed, ItemAnimator can decide whether it wants to re-use
+         * the same ViewHolder for animations or RecyclerView should create a copy of the
+         * item and ItemAnimator will use both to run the animation (e.g. cross-fade).
+         * <p>
+         * Note that this method will only be called if the {@link ViewHolder} still has the same
+         * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive
+         * both {@link ViewHolder}s in the
+         * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method.
+         *
+         * @param viewHolder The ViewHolder which represents the changed item's old content.
+         * @param payloads A non-null list of merged payloads that were sent with change
+         *                 notifications. Can be empty if the adapter is invalidated via
+         *                 {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of
+         *                 payloads will be passed into
+         *                 {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)}
+         *                 method <b>if</b> this method returns <code>true</code>.
+         *
+         * @return True if RecyclerView should just rebind to the same ViewHolder or false if
+         *         RecyclerView should create a new ViewHolder and pass this ViewHolder to the
+         *         ItemAnimator to animate. Default implementation calls
+         *         {@link #canReuseUpdatedViewHolder(ViewHolder)}.
+         *
+         * @see #canReuseUpdatedViewHolder(ViewHolder)
+         */
+        public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+                @NonNull List<Object> payloads) {
+            return canReuseUpdatedViewHolder(viewHolder);
+        }
+
+        /**
+         * This method should be called by ItemAnimator implementations to notify
+         * any listeners that all pending and active item animations are finished.
+         */
+        public final void dispatchAnimationsFinished() {
+            final int count = mFinishedListeners.size();
+            for (int i = 0; i < count; ++i) {
+                mFinishedListeners.get(i).onAnimationsFinished();
+            }
+            mFinishedListeners.clear();
+        }
+
+        /**
+         * Returns a new {@link ItemHolderInfo} which will be used to store information about the
+         * ViewHolder. This information will later be passed into <code>animate**</code> methods.
+         * <p>
+         * You can override this method if you want to extend {@link ItemHolderInfo} and provide
+         * your own instances.
+         *
+         * @return A new {@link ItemHolderInfo}.
+         */
+        public ItemHolderInfo obtainHolderInfo() {
+            return new ItemHolderInfo();
+        }
+
+        /**
+         * The interface to be implemented by listeners to animation events from this
+         * ItemAnimator. This is used internally and is not intended for developers to
+         * create directly.
+         */
+        interface ItemAnimatorListener {
+            void onAnimationFinished(ViewHolder item);
+        }
+
+        /**
+         * This interface is used to inform listeners when all pending or running animations
+         * in an ItemAnimator are finished. This can be used, for example, to delay an action
+         * in a data set until currently-running animations are complete.
+         *
+         * @see #isRunning(ItemAnimatorFinishedListener)
+         */
+        public interface ItemAnimatorFinishedListener {
+            /**
+             * Notifies when all pending or running animations in an ItemAnimator are finished.
+             */
+            void onAnimationsFinished();
+        }
+
+        /**
+         * A simple data structure that holds information about an item's bounds.
+         * This information is used in calculating item animations. Default implementation of
+         * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and
+         * {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data
+         * structure. You can extend this class if you would like to keep more information about
+         * the Views.
+         * <p>
+         * If you want to provide your own implementation but still use `super` methods to record
+         * basic information, you can override {@link #obtainHolderInfo()} to provide your own
+         * instances.
+         */
+        public static class ItemHolderInfo {
+
+            /**
+             * The left edge of the View (excluding decorations)
+             */
+            public int left;
+
+            /**
+             * The top edge of the View (excluding decorations)
+             */
+            public int top;
+
+            /**
+             * The right edge of the View (excluding decorations)
+             */
+            public int right;
+
+            /**
+             * The bottom edge of the View (excluding decorations)
+             */
+            public int bottom;
+
+            /**
+             * The change flags that were passed to
+             * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}.
+             */
+            @AdapterChanges
+            public int changeFlags;
+
+            public ItemHolderInfo() {
+            }
+
+            /**
+             * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from
+             * the given ViewHolder. Clears all {@link #changeFlags}.
+             *
+             * @param holder The ViewHolder whose bounds should be copied.
+             * @return This {@link ItemHolderInfo}
+             */
+            public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder) {
+                return setFrom(holder, 0);
+            }
+
+            /**
+             * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from
+             * the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter.
+             *
+             * @param holder The ViewHolder whose bounds should be copied.
+             * @param flags  The adapter change flags that were passed into
+             *               {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int,
+             *               List)}.
+             * @return This {@link ItemHolderInfo}
+             */
+            public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,
+                    @AdapterChanges int flags) {
+                final View view = holder.itemView;
+                this.left = view.getLeft();
+                this.top = view.getTop();
+                this.right = view.getRight();
+                this.bottom = view.getBottom();
+                return this;
+            }
+        }
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        if (mChildDrawingOrderCallback == null) {
+            return super.getChildDrawingOrder(childCount, i);
+        } else {
+            return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i);
+        }
+    }
+
+    /**
+     * A callback interface that can be used to alter the drawing order of RecyclerView children.
+     * <p>
+     * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case
+     * that applies to that method also applies to this callback. For example, changing the drawing
+     * order of two views will not have any effect if their elevation values are different since
+     * elevation overrides the result of this callback.
+     */
+    public interface ChildDrawingOrderCallback {
+        /**
+         * Returns the index of the child to draw for this iteration. Override this
+         * if you want to change the drawing order of children. By default, it
+         * returns i.
+         *
+         * @param i The current iteration.
+         * @return The index of the child to draw this iteration.
+         *
+         * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback)
+         */
+        int onGetChildDrawingOrder(int childCount, int i);
+    }
+}
diff --git a/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java b/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java
new file mode 100644
index 0000000..282da64
--- /dev/null
+++ b/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * The AccessibilityDelegate used by RecyclerView.
+ * <p>
+ * This class handles basic accessibility actions and delegates them to LayoutManager.
+ */
+public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegate {
+    final RecyclerView mRecyclerView;
+
+
+    public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
+        mRecyclerView = recyclerView;
+    }
+
+    boolean shouldIgnore() {
+        return mRecyclerView.hasPendingAdapterUpdates();
+    }
+
+    @Override
+    public boolean performAccessibilityAction(View host, int action, Bundle args) {
+        if (super.performAccessibilityAction(host, action, args)) {
+            return true;
+        }
+        if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+            return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
+        }
+
+        return false;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(host, info);
+        info.setClassName(RecyclerView.class.getName());
+        if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+            mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(host, event);
+        event.setClassName(RecyclerView.class.getName());
+        if (host instanceof RecyclerView && !shouldIgnore()) {
+            RecyclerView rv = (RecyclerView) host;
+            if (rv.getLayoutManager() != null) {
+                rv.getLayoutManager().onInitializeAccessibilityEvent(event);
+            }
+        }
+    }
+
+    /**
+     * Gets the AccessibilityDelegate for an individual item in the RecyclerView.
+     * A basic item delegate is provided by default, but you can override this
+     * method to provide a custom per-item delegate.
+     */
+    public AccessibilityDelegate getItemDelegate() {
+        return mItemDelegate;
+    }
+
+    final AccessibilityDelegate mItemDelegate = new AccessibilityDelegate() {
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+                mRecyclerView.getLayoutManager()
+                        .onInitializeAccessibilityNodeInfoForItem(host, info);
+            }
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle args) {
+            if (super.performAccessibilityAction(host, action, args)) {
+                return true;
+            }
+            if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+                return mRecyclerView.getLayoutManager()
+                        .performAccessibilityActionForItem(host, action, args);
+            }
+            return false;
+        }
+    };
+}
+
diff --git a/com/android/internal/widget/ResolverDrawerLayout.java b/com/android/internal/widget/ResolverDrawerLayout.java
new file mode 100644
index 0000000..17c7ebd
--- /dev/null
+++ b/com/android/internal/widget/ResolverDrawerLayout.java
@@ -0,0 +1,973 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView;
+import android.widget.OverScroller;
+
+public class ResolverDrawerLayout extends ViewGroup {
+    private static final String TAG = "ResolverDrawerLayout";
+
+    /**
+     * Max width of the whole drawer layout
+     */
+    private int mMaxWidth;
+
+    /**
+     * Max total visible height of views not marked always-show when in the closed/initial state
+     */
+    private int mMaxCollapsedHeight;
+
+    /**
+     * Max total visible height of views not marked always-show when in the closed/initial state
+     * when a default option is present
+     */
+    private int mMaxCollapsedHeightSmall;
+
+    private boolean mSmallCollapsed;
+
+    /**
+     * Move views down from the top by this much in px
+     */
+    private float mCollapseOffset;
+
+    private int mCollapsibleHeight;
+    private int mUncollapsibleHeight;
+
+    /**
+     * The height in pixels of reserved space added to the top of the collapsed UI;
+     * e.g. chooser targets
+     */
+    private int mCollapsibleHeightReserved;
+
+    private int mTopOffset;
+
+    private boolean mIsDragging;
+    private boolean mOpenOnClick;
+    private boolean mOpenOnLayout;
+    private boolean mDismissOnScrollerFinished;
+    private final int mTouchSlop;
+    private final float mMinFlingVelocity;
+    private final OverScroller mScroller;
+    private final VelocityTracker mVelocityTracker;
+
+    private Drawable mScrollIndicatorDrawable;
+
+    private OnDismissedListener mOnDismissedListener;
+    private RunOnDismissedListener mRunOnDismissedListener;
+
+    private boolean mDismissLocked;
+
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private float mLastTouchY;
+    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+    private final Rect mTempRect = new Rect();
+
+    private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
+            new ViewTreeObserver.OnTouchModeChangeListener() {
+                @Override
+                public void onTouchModeChanged(boolean isInTouchMode) {
+                    if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
+                        smoothScrollTo(0, 0);
+                    }
+                }
+            };
+
+    public ResolverDrawerLayout(Context context) {
+        this(context, null);
+    }
+
+    public ResolverDrawerLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
+                defStyleAttr, 0);
+        mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
+        mMaxCollapsedHeight = a.getDimensionPixelSize(
+                R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
+        mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
+                R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
+                mMaxCollapsedHeight);
+        a.recycle();
+
+        mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
+
+        mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
+                android.R.interpolator.decelerate_quint));
+        mVelocityTracker = VelocityTracker.obtain();
+
+        final ViewConfiguration vc = ViewConfiguration.get(context);
+        mTouchSlop = vc.getScaledTouchSlop();
+        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+
+        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+    }
+
+    public void setSmallCollapsed(boolean smallCollapsed) {
+        mSmallCollapsed = smallCollapsed;
+        requestLayout();
+    }
+
+    public boolean isSmallCollapsed() {
+        return mSmallCollapsed;
+    }
+
+    public boolean isCollapsed() {
+        return mCollapseOffset > 0;
+    }
+
+    public void setCollapsed(boolean collapsed) {
+        if (!isLaidOut()) {
+            mOpenOnLayout = collapsed;
+        } else {
+            smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
+        }
+    }
+
+    public void setCollapsibleHeightReserved(int heightPixels) {
+        final int oldReserved = mCollapsibleHeightReserved;
+        mCollapsibleHeightReserved = heightPixels;
+
+        final int dReserved = mCollapsibleHeightReserved - oldReserved;
+        if (dReserved != 0 && mIsDragging) {
+            mLastTouchY -= dReserved;
+        }
+
+        final int oldCollapsibleHeight = mCollapsibleHeight;
+        mCollapsibleHeight = Math.max(mCollapsibleHeight, getMaxCollapsedHeight());
+
+        if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
+            return;
+        }
+
+        invalidate();
+    }
+
+    public void setDismissLocked(boolean locked) {
+        mDismissLocked = locked;
+    }
+
+    private boolean isMoving() {
+        return mIsDragging || !mScroller.isFinished();
+    }
+
+    private boolean isDragging() {
+        return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
+    }
+
+    private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
+        if (oldCollapsibleHeight == mCollapsibleHeight) {
+            return false;
+        }
+
+        if (isLaidOut()) {
+            final boolean isCollapsedOld = mCollapseOffset != 0;
+            if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
+                    && mCollapseOffset == oldCollapsibleHeight)) {
+                // Stay closed even at the new height.
+                mCollapseOffset = mCollapsibleHeight;
+            } else {
+                mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
+            }
+            final boolean isCollapsedNew = mCollapseOffset != 0;
+            if (isCollapsedOld != isCollapsedNew) {
+                onCollapsedChanged(isCollapsedNew);
+            }
+        } else {
+            // Start out collapsed at first unless we restored state for otherwise
+            mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
+        }
+        return true;
+    }
+
+    private int getMaxCollapsedHeight() {
+        return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
+                + mCollapsibleHeightReserved;
+    }
+
+    public void setOnDismissedListener(OnDismissedListener listener) {
+        mOnDismissedListener = listener;
+    }
+
+    private boolean isDismissable() {
+        return mOnDismissedListener != null && !mDismissLocked;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int action = ev.getActionMasked();
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            mVelocityTracker.clear();
+        }
+
+        mVelocityTracker.addMovement(ev);
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                mInitialTouchX = x;
+                mInitialTouchY = mLastTouchY = y;
+                mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
+            }
+            break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                final float dy = y - mInitialTouchY;
+                if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
+                        (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+                    mActivePointerId = ev.getPointerId(0);
+                    mIsDragging = true;
+                    mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+                            Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+                }
+            }
+            break;
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+            }
+            break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                resetTouch();
+            }
+            break;
+        }
+
+        if (mIsDragging) {
+            abortAnimation();
+        }
+        return mIsDragging || mOpenOnClick;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        final int action = ev.getActionMasked();
+
+        mVelocityTracker.addMovement(ev);
+
+        boolean handled = false;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                mInitialTouchX = x;
+                mInitialTouchY = mLastTouchY = y;
+                mActivePointerId = ev.getPointerId(0);
+                final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
+                handled = isDismissable() || mCollapsibleHeight > 0;
+                mIsDragging = hitView && handled;
+                abortAnimation();
+            }
+            break;
+
+            case MotionEvent.ACTION_MOVE: {
+                int index = ev.findPointerIndex(mActivePointerId);
+                if (index < 0) {
+                    Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
+                    index = 0;
+                    mActivePointerId = ev.getPointerId(0);
+                    mInitialTouchX = ev.getX();
+                    mInitialTouchY = mLastTouchY = ev.getY();
+                }
+                final float x = ev.getX(index);
+                final float y = ev.getY(index);
+                if (!mIsDragging) {
+                    final float dy = y - mInitialTouchY;
+                    if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
+                        handled = mIsDragging = true;
+                        mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+                                Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+                    }
+                }
+                if (mIsDragging) {
+                    final float dy = y - mLastTouchY;
+                    performDrag(dy);
+                }
+                mLastTouchY = y;
+            }
+            break;
+
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int pointerIndex = ev.getActionIndex();
+                final int pointerId = ev.getPointerId(pointerIndex);
+                mActivePointerId = pointerId;
+                mInitialTouchX = ev.getX(pointerIndex);
+                mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
+            }
+            break;
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+            }
+            break;
+
+            case MotionEvent.ACTION_UP: {
+                final boolean wasDragging = mIsDragging;
+                mIsDragging = false;
+                if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
+                        findChildUnder(ev.getX(), ev.getY()) == null) {
+                    if (isDismissable()) {
+                        dispatchOnDismissed();
+                        resetTouch();
+                        return true;
+                    }
+                }
+                if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
+                        Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
+                    smoothScrollTo(0, 0);
+                    return true;
+                }
+                mVelocityTracker.computeCurrentVelocity(1000);
+                final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
+                if (Math.abs(yvel) > mMinFlingVelocity) {
+                    if (isDismissable()
+                            && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
+                        smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
+                        mDismissOnScrollerFinished = true;
+                    } else {
+                        smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+                    }
+                } else {
+                    smoothScrollTo(
+                            mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+                }
+                resetTouch();
+            }
+            break;
+
+            case MotionEvent.ACTION_CANCEL: {
+                if (mIsDragging) {
+                    smoothScrollTo(
+                            mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+                }
+                resetTouch();
+                return true;
+            }
+        }
+
+        return handled;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = ev.getActionIndex();
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mInitialTouchX = ev.getX(newPointerIndex);
+            mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+        }
+    }
+
+    private void resetTouch() {
+        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+        mIsDragging = false;
+        mOpenOnClick = false;
+        mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
+        mVelocityTracker.clear();
+    }
+
+    @Override
+    public void computeScroll() {
+        super.computeScroll();
+        if (mScroller.computeScrollOffset()) {
+            final boolean keepGoing = !mScroller.isFinished();
+            performDrag(mScroller.getCurrY() - mCollapseOffset);
+            if (keepGoing) {
+                postInvalidateOnAnimation();
+            } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
+                mRunOnDismissedListener = new RunOnDismissedListener();
+                post(mRunOnDismissedListener);
+            }
+        }
+    }
+
+    private void abortAnimation() {
+        mScroller.abortAnimation();
+        mRunOnDismissedListener = null;
+        mDismissOnScrollerFinished = false;
+    }
+
+    private float performDrag(float dy) {
+        final float newPos = Math.max(0, Math.min(mCollapseOffset + dy,
+                mCollapsibleHeight + mUncollapsibleHeight));
+        if (newPos != mCollapseOffset) {
+            dy = newPos - mCollapseOffset;
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.ignoreOffset) {
+                    child.offsetTopAndBottom((int) dy);
+                }
+            }
+            final boolean isCollapsedOld = mCollapseOffset != 0;
+            mCollapseOffset = newPos;
+            mTopOffset += dy;
+            final boolean isCollapsedNew = newPos != 0;
+            if (isCollapsedOld != isCollapsedNew) {
+                onCollapsedChanged(isCollapsedNew);
+            }
+            postInvalidateOnAnimation();
+            return dy;
+        }
+        return 0;
+    }
+
+    private void onCollapsedChanged(boolean isCollapsed) {
+        notifyViewAccessibilityStateChangedIfNeeded(
+                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+        if (mScrollIndicatorDrawable != null) {
+            setWillNotDraw(!isCollapsed);
+        }
+    }
+
+    void dispatchOnDismissed() {
+        if (mOnDismissedListener != null) {
+            mOnDismissedListener.onDismissed();
+        }
+        if (mRunOnDismissedListener != null) {
+            removeCallbacks(mRunOnDismissedListener);
+            mRunOnDismissedListener = null;
+        }
+    }
+
+    private void smoothScrollTo(int yOffset, float velocity) {
+        abortAnimation();
+        final int sy = (int) mCollapseOffset;
+        int dy = yOffset - sy;
+        if (dy == 0) {
+            return;
+        }
+
+        final int height = getHeight();
+        final int halfHeight = height / 2;
+        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
+        final float distance = halfHeight + halfHeight *
+                distanceInfluenceForSnapDuration(distanceRatio);
+
+        int duration = 0;
+        velocity = Math.abs(velocity);
+        if (velocity > 0) {
+            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+        } else {
+            final float pageDelta = (float) Math.abs(dy) / height;
+            duration = (int) ((pageDelta + 1) * 100);
+        }
+        duration = Math.min(duration, 300);
+
+        mScroller.startScroll(0, sy, 0, dy, duration);
+        postInvalidateOnAnimation();
+    }
+
+    private float distanceInfluenceForSnapDuration(float f) {
+        f -= 0.5f; // center the values about 0.
+        f *= 0.3f * Math.PI / 2.0f;
+        return (float) Math.sin(f);
+    }
+
+    /**
+     * Note: this method doesn't take Z into account for overlapping views
+     * since it is only used in contexts where this doesn't affect the outcome.
+     */
+    private View findChildUnder(float x, float y) {
+        return findChildUnder(this, x, y);
+    }
+
+    private static View findChildUnder(ViewGroup parent, float x, float y) {
+        final int childCount = parent.getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            final View child = parent.getChildAt(i);
+            if (isChildUnder(child, x, y)) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    private View findListChildUnder(float x, float y) {
+        View v = findChildUnder(x, y);
+        while (v != null) {
+            x -= v.getX();
+            y -= v.getY();
+            if (v instanceof AbsListView) {
+                // One more after this.
+                return findChildUnder((ViewGroup) v, x, y);
+            }
+            v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
+        }
+        return v;
+    }
+
+    /**
+     * This only checks clipping along the bottom edge.
+     */
+    private boolean isListChildUnderClipped(float x, float y) {
+        final View listChild = findListChildUnder(x, y);
+        return listChild != null && isDescendantClipped(listChild);
+    }
+
+    private boolean isDescendantClipped(View child) {
+        mTempRect.set(0, 0, child.getWidth(), child.getHeight());
+        offsetDescendantRectToMyCoords(child, mTempRect);
+        View directChild;
+        if (child.getParent() == this) {
+            directChild = child;
+        } else {
+            View v = child;
+            ViewParent p = child.getParent();
+            while (p != this) {
+                v = (View) p;
+                p = v.getParent();
+            }
+            directChild = v;
+        }
+
+        // ResolverDrawerLayout lays out vertically in child order;
+        // the next view and forward is what to check against.
+        int clipEdge = getHeight() - getPaddingBottom();
+        final int childCount = getChildCount();
+        for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
+            final View nextChild = getChildAt(i);
+            if (nextChild.getVisibility() == GONE) {
+                continue;
+            }
+            clipEdge = Math.min(clipEdge, nextChild.getTop());
+        }
+        return mTempRect.bottom > clipEdge;
+    }
+
+    private static boolean isChildUnder(View child, float x, float y) {
+        final float left = child.getX();
+        final float top = child.getY();
+        final float right = left + child.getWidth();
+        final float bottom = top + child.getHeight();
+        return x >= left && y >= top && x < right && y < bottom;
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        super.requestChildFocus(child, focused);
+        if (!isInTouchMode() && isDescendantClipped(focused)) {
+            smoothScrollTo(0, 0);
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
+        abortAnimation();
+    }
+
+    @Override
+    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+        return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0;
+    }
+
+    @Override
+    public void onNestedScrollAccepted(View child, View target, int axes) {
+        super.onNestedScrollAccepted(child, target, axes);
+    }
+
+    @Override
+    public void onStopNestedScroll(View child) {
+        super.onStopNestedScroll(child);
+        if (mScroller.isFinished()) {
+            smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+        }
+    }
+
+    @Override
+    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed) {
+        if (dyUnconsumed < 0) {
+            performDrag(-dyUnconsumed);
+        }
+    }
+
+    @Override
+    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+        if (dy > 0) {
+            consumed[1] = (int) -performDrag(-dy);
+        }
+    }
+
+    @Override
+    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+        if (velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
+            smoothScrollTo(0, velocityY);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+        if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
+            if (isDismissable()
+                    && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+                smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
+                mDismissOnScrollerFinished = true;
+            } else {
+                smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+        if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
+            return true;
+        }
+
+        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
+            smoothScrollTo(0, 0);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        // Since we support scrolling, make this ViewGroup look like a
+        // ScrollView. This is kind of a hack until we have support for
+        // specifying auto-scroll behavior.
+        return android.widget.ScrollView.class.getName();
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (isEnabled()) {
+            if (mCollapseOffset != 0) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+                info.setScrollable(true);
+            }
+        }
+
+        // This view should never get accessibility focus, but it's interactive
+        // via nested scrolling, so we can't hide it completely.
+        info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
+            // This view should never get accessibility focus.
+            return false;
+        }
+
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+
+        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
+            smoothScrollTo(0, 0);
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void onDrawForeground(Canvas canvas) {
+        if (mScrollIndicatorDrawable != null) {
+            mScrollIndicatorDrawable.draw(canvas);
+        }
+
+        super.onDrawForeground(canvas);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int widthSize = sourceWidth;
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        // Single-use layout; just ignore the mode and use available space.
+        // Clamp to maxWidth.
+        if (mMaxWidth >= 0) {
+            widthSize = Math.min(widthSize, mMaxWidth);
+        }
+
+        final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+        final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+        final int widthPadding = getPaddingLeft() + getPaddingRight();
+
+        // Currently we allot more height than is really needed so that the entirety of the
+        // sheet may be pulled up.
+        // TODO: Restrict the height here to be the right value.
+        int heightUsed = getPaddingTop() + getPaddingBottom();
+
+        // Measure always-show children first.
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.alwaysShow && child.getVisibility() != GONE) {
+                measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed);
+                heightUsed += child.getMeasuredHeight();
+            }
+        }
+
+        final int alwaysShowHeight = heightUsed;
+
+        // And now the rest.
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (!lp.alwaysShow && child.getVisibility() != GONE) {
+                measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed);
+                heightUsed += child.getMeasuredHeight();
+            }
+        }
+
+        final int oldCollapsibleHeight = mCollapsibleHeight;
+        mCollapsibleHeight = Math.max(0,
+                heightUsed - alwaysShowHeight - getMaxCollapsedHeight());
+        mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
+
+        updateCollapseOffset(oldCollapsibleHeight, !isDragging());
+
+        mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
+
+        setMeasuredDimension(sourceWidth, heightSize);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = getWidth();
+
+        View indicatorHost = null;
+
+        int ypos = mTopOffset;
+        int leftEdge = getPaddingLeft();
+        int rightEdge = width - getPaddingRight();
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.hasNestedScrollIndicator) {
+                indicatorHost = child;
+            }
+
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+
+            int top = ypos + lp.topMargin;
+            if (lp.ignoreOffset) {
+                top -= mCollapseOffset;
+            }
+            final int bottom = top + child.getMeasuredHeight();
+
+            final int childWidth = child.getMeasuredWidth();
+            final int widthAvailable = rightEdge - leftEdge;
+            final int left = leftEdge + (widthAvailable - childWidth) / 2;
+            final int right = left + childWidth;
+
+            child.layout(left, top, right, bottom);
+
+            ypos = bottom + lp.bottomMargin;
+        }
+
+        if (mScrollIndicatorDrawable != null) {
+            if (indicatorHost != null) {
+                final int left = indicatorHost.getLeft();
+                final int right = indicatorHost.getRight();
+                final int bottom = indicatorHost.getTop();
+                final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
+                mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
+                setWillNotDraw(!isCollapsed());
+            } else {
+                mScrollIndicatorDrawable = null;
+                setWillNotDraw(true);
+            }
+        }
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        if (p instanceof LayoutParams) {
+            return new LayoutParams((LayoutParams) p);
+        } else if (p instanceof MarginLayoutParams) {
+            return new LayoutParams((MarginLayoutParams) p);
+        }
+        return new LayoutParams(p);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        final SavedState ss = new SavedState(super.onSaveInstanceState());
+        ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
+        return ss;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        final SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        mOpenOnLayout = ss.open;
+    }
+
+    public static class LayoutParams extends MarginLayoutParams {
+        public boolean alwaysShow;
+        public boolean ignoreOffset;
+        public boolean hasNestedScrollIndicator;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+
+            final TypedArray a = c.obtainStyledAttributes(attrs,
+                    R.styleable.ResolverDrawerLayout_LayoutParams);
+            alwaysShow = a.getBoolean(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
+                    false);
+            ignoreOffset = a.getBoolean(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
+                    false);
+            hasNestedScrollIndicator = a.getBoolean(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
+                    false);
+            a.recycle();
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(LayoutParams source) {
+            super(source);
+            this.alwaysShow = source.alwaysShow;
+            this.ignoreOffset = source.ignoreOffset;
+            this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
+        }
+
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        boolean open;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            open = in.readInt() != 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(open ? 1 : 0);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    public interface OnDismissedListener {
+        public void onDismissed();
+    }
+
+    private class RunOnDismissedListener implements Runnable {
+        @Override
+        public void run() {
+            dispatchOnDismissed();
+        }
+    }
+}
diff --git a/com/android/internal/widget/ScrollBarUtils.java b/com/android/internal/widget/ScrollBarUtils.java
new file mode 100644
index 0000000..0ae9f74
--- /dev/null
+++ b/com/android/internal/widget/ScrollBarUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+public class ScrollBarUtils {
+
+    public static int getThumbLength(int size, int thickness, int extent, int range) {
+        // Avoid the tiny thumb.
+        final int minLength = thickness * 2;
+        int length = Math.round((float) size * extent / range);
+        if (length < minLength) {
+            length = minLength;
+        }
+        return length;
+    }
+
+    public static int getThumbOffset(int size, int thumbLength, int extent, int range, int offset) {
+        // Avoid the too-big thumb.
+        int thumbOffset = Math.round((float) (size - thumbLength) * offset / (range - extent));
+        if (thumbOffset > size - thumbLength) {
+            thumbOffset = size - thumbLength;
+        }
+        return thumbOffset;
+    }
+}
diff --git a/com/android/internal/widget/ScrollbarHelper.java b/com/android/internal/widget/ScrollbarHelper.java
new file mode 100644
index 0000000..ae34e4c
--- /dev/null
+++ b/com/android/internal/widget/ScrollbarHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.view.View;
+
+/**
+ * A helper class to do scroll offset calculations.
+ */
+class ScrollbarHelper {
+
+    /**
+     * @param startChild View closest to start of the list. (top or left)
+     * @param endChild   View closest to end of the list (bottom or right)
+     */
+    static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
+            View startChild, View endChild, RecyclerView.LayoutManager lm,
+            boolean smoothScrollbarEnabled, boolean reverseLayout) {
+        if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
+                || endChild == null) {
+            return 0;
+        }
+        final int minPosition = Math.min(lm.getPosition(startChild),
+                lm.getPosition(endChild));
+        final int maxPosition = Math.max(lm.getPosition(startChild),
+                lm.getPosition(endChild));
+        final int itemsBefore = reverseLayout
+                ? Math.max(0, state.getItemCount() - maxPosition - 1)
+                : Math.max(0, minPosition);
+        if (!smoothScrollbarEnabled) {
+            return itemsBefore;
+        }
+        final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
+                - orientation.getDecoratedStart(startChild));
+        final int itemRange = Math.abs(lm.getPosition(startChild)
+                - lm.getPosition(endChild)) + 1;
+        final float avgSizePerRow = (float) laidOutArea / itemRange;
+
+        return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
+                - orientation.getDecoratedStart(startChild)));
+    }
+
+    /**
+     * @param startChild View closest to start of the list. (top or left)
+     * @param endChild   View closest to end of the list (bottom or right)
+     */
+    static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
+            View startChild, View endChild, RecyclerView.LayoutManager lm,
+            boolean smoothScrollbarEnabled) {
+        if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
+                || endChild == null) {
+            return 0;
+        }
+        if (!smoothScrollbarEnabled) {
+            return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
+        }
+        final int extend = orientation.getDecoratedEnd(endChild)
+                - orientation.getDecoratedStart(startChild);
+        return Math.min(orientation.getTotalSpace(), extend);
+    }
+
+    /**
+     * @param startChild View closest to start of the list. (top or left)
+     * @param endChild   View closest to end of the list (bottom or right)
+     */
+    static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
+            View startChild, View endChild, RecyclerView.LayoutManager lm,
+            boolean smoothScrollbarEnabled) {
+        if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
+                || endChild == null) {
+            return 0;
+        }
+        if (!smoothScrollbarEnabled) {
+            return state.getItemCount();
+        }
+        // smooth scrollbar enabled. try to estimate better.
+        final int laidOutArea = orientation.getDecoratedEnd(endChild)
+                - orientation.getDecoratedStart(startChild);
+        final int laidOutRange = Math.abs(lm.getPosition(startChild)
+                - lm.getPosition(endChild))
+                + 1;
+        // estimate a size for full list.
+        return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
+    }
+}
diff --git a/com/android/internal/widget/ScrollingTabContainerView.java b/com/android/internal/widget/ScrollingTabContainerView.java
new file mode 100644
index 0000000..311bfac
--- /dev/null
+++ b/com/android/internal/widget/ScrollingTabContainerView.java
@@ -0,0 +1,576 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+import com.android.internal.view.ActionBarPolicy;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+/**
+ * This widget implements the dynamic action bar tab behavior that can change
+ * across different configurations or circumstances.
+ */
+public class ScrollingTabContainerView extends HorizontalScrollView
+        implements AdapterView.OnItemClickListener {
+    private static final String TAG = "ScrollingTabContainerView";
+    Runnable mTabSelector;
+    private TabClickListener mTabClickListener;
+
+    private LinearLayout mTabLayout;
+    private Spinner mTabSpinner;
+    private boolean mAllowCollapse;
+
+    int mMaxTabWidth;
+    int mStackedTabMaxWidth;
+    private int mContentHeight;
+    private int mSelectedTabIndex;
+
+    protected Animator mVisibilityAnim;
+    protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener();
+
+    private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator();
+
+    private static final int FADE_DURATION = 200;
+
+    public ScrollingTabContainerView(Context context) {
+        super(context);
+        setHorizontalScrollBarEnabled(false);
+
+        ActionBarPolicy abp = ActionBarPolicy.get(context);
+        setContentHeight(abp.getTabContainerHeight());
+        mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
+
+        mTabLayout = createTabLayout();
+        addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY;
+        setFillViewport(lockedExpanded);
+
+        final int childCount = mTabLayout.getChildCount();
+        if (childCount > 1 &&
+                (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) {
+            if (childCount > 2) {
+                mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f);
+            } else {
+                mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
+            }
+            mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth);
+        } else {
+            mMaxTabWidth = -1;
+        }
+
+        heightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
+
+        final boolean canCollapse = !lockedExpanded && mAllowCollapse;
+
+        if (canCollapse) {
+            // See if we should expand
+            mTabLayout.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec);
+            if (mTabLayout.getMeasuredWidth() > MeasureSpec.getSize(widthMeasureSpec)) {
+                performCollapse();
+            } else {
+                performExpand();
+            }
+        } else {
+            performExpand();
+        }
+
+        final int oldWidth = getMeasuredWidth();
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        final int newWidth = getMeasuredWidth();
+
+        if (lockedExpanded && oldWidth != newWidth) {
+            // Recenter the tab display if we're at a new (scrollable) size.
+            setTabSelected(mSelectedTabIndex);
+        }
+    }
+
+    /**
+     * Indicates whether this view is collapsed into a dropdown menu instead
+     * of traditional tabs.
+     * @return true if showing as a spinner
+     */
+    private boolean isCollapsed() {
+        return mTabSpinner != null && mTabSpinner.getParent() == this;
+    }
+
+    public void setAllowCollapse(boolean allowCollapse) {
+        mAllowCollapse = allowCollapse;
+    }
+
+    private void performCollapse() {
+        if (isCollapsed()) return;
+
+        if (mTabSpinner == null) {
+            mTabSpinner = createSpinner();
+        }
+        removeView(mTabLayout);
+        addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+        if (mTabSpinner.getAdapter() == null) {
+            final TabAdapter adapter = new TabAdapter(mContext);
+            adapter.setDropDownViewContext(mTabSpinner.getPopupContext());
+            mTabSpinner.setAdapter(adapter);
+        }
+        if (mTabSelector != null) {
+            removeCallbacks(mTabSelector);
+            mTabSelector = null;
+        }
+        mTabSpinner.setSelection(mSelectedTabIndex);
+    }
+
+    private boolean performExpand() {
+        if (!isCollapsed()) return false;
+
+        removeView(mTabSpinner);
+        addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+        setTabSelected(mTabSpinner.getSelectedItemPosition());
+        return false;
+    }
+
+    public void setTabSelected(int position) {
+        mSelectedTabIndex = position;
+        final int tabCount = mTabLayout.getChildCount();
+        for (int i = 0; i < tabCount; i++) {
+            final View child = mTabLayout.getChildAt(i);
+            final boolean isSelected = i == position;
+            child.setSelected(isSelected);
+            if (isSelected) {
+                animateToTab(position);
+            }
+        }
+        if (mTabSpinner != null && position >= 0) {
+            mTabSpinner.setSelection(position);
+        }
+    }
+
+    public void setContentHeight(int contentHeight) {
+        mContentHeight = contentHeight;
+        requestLayout();
+    }
+
+    private LinearLayout createTabLayout() {
+        final LinearLayout tabLayout = new LinearLayout(getContext(), null,
+                com.android.internal.R.attr.actionBarTabBarStyle);
+        tabLayout.setMeasureWithLargestChildEnabled(true);
+        tabLayout.setGravity(Gravity.CENTER);
+        tabLayout.setLayoutParams(new LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
+        return tabLayout;
+    }
+
+    private Spinner createSpinner() {
+        final Spinner spinner = new Spinner(getContext(), null,
+                com.android.internal.R.attr.actionDropDownStyle);
+        spinner.setLayoutParams(new LinearLayout.LayoutParams(
+                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
+        spinner.setOnItemClickListenerInt(this);
+        return spinner;
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        ActionBarPolicy abp = ActionBarPolicy.get(getContext());
+        // Action bar can change size on configuration changes.
+        // Reread the desired height from the theme-specified style.
+        setContentHeight(abp.getTabContainerHeight());
+        mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
+    }
+
+    public void animateToVisibility(int visibility) {
+        if (mVisibilityAnim != null) {
+            mVisibilityAnim.cancel();
+        }
+        if (visibility == VISIBLE) {
+            if (getVisibility() != VISIBLE) {
+                setAlpha(0);
+            }
+            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1);
+            anim.setDuration(FADE_DURATION);
+            anim.setInterpolator(sAlphaInterpolator);
+
+            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+            anim.start();
+        } else {
+            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0);
+            anim.setDuration(FADE_DURATION);
+            anim.setInterpolator(sAlphaInterpolator);
+
+            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+            anim.start();
+        }
+    }
+
+    public void animateToTab(final int position) {
+        final View tabView = mTabLayout.getChildAt(position);
+        if (mTabSelector != null) {
+            removeCallbacks(mTabSelector);
+        }
+        mTabSelector = new Runnable() {
+            public void run() {
+                final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;
+                smoothScrollTo(scrollPos, 0);
+                mTabSelector = null;
+            }
+        };
+        post(mTabSelector);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (mTabSelector != null) {
+            // Re-post the selector we saved
+            post(mTabSelector);
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mTabSelector != null) {
+            removeCallbacks(mTabSelector);
+        }
+    }
+
+    private TabView createTabView(Context context, ActionBar.Tab tab, boolean forAdapter) {
+        final TabView tabView = new TabView(context, tab, forAdapter);
+        if (forAdapter) {
+            tabView.setBackgroundDrawable(null);
+            tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,
+                    mContentHeight));
+        } else {
+            tabView.setFocusable(true);
+
+            if (mTabClickListener == null) {
+                mTabClickListener = new TabClickListener();
+            }
+            tabView.setOnClickListener(mTabClickListener);
+        }
+        return tabView;
+    }
+
+    public void addTab(ActionBar.Tab tab, boolean setSelected) {
+        TabView tabView = createTabView(mContext, tab, false);
+        mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0,
+                LayoutParams.MATCH_PARENT, 1));
+        if (mTabSpinner != null) {
+            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+        }
+        if (setSelected) {
+            tabView.setSelected(true);
+        }
+        if (mAllowCollapse) {
+            requestLayout();
+        }
+    }
+
+    public void addTab(ActionBar.Tab tab, int position, boolean setSelected) {
+        final TabView tabView = createTabView(mContext, tab, false);
+        mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams(
+                0, LayoutParams.MATCH_PARENT, 1));
+        if (mTabSpinner != null) {
+            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+        }
+        if (setSelected) {
+            tabView.setSelected(true);
+        }
+        if (mAllowCollapse) {
+            requestLayout();
+        }
+    }
+
+    public void updateTab(int position) {
+        ((TabView) mTabLayout.getChildAt(position)).update();
+        if (mTabSpinner != null) {
+            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+        }
+        if (mAllowCollapse) {
+            requestLayout();
+        }
+    }
+
+    public void removeTabAt(int position) {
+        mTabLayout.removeViewAt(position);
+        if (mTabSpinner != null) {
+            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+        }
+        if (mAllowCollapse) {
+            requestLayout();
+        }
+    }
+
+    public void removeAllTabs() {
+        mTabLayout.removeAllViews();
+        if (mTabSpinner != null) {
+            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+        }
+        if (mAllowCollapse) {
+            requestLayout();
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        TabView tabView = (TabView) view;
+        tabView.getTab().select();
+    }
+
+    private class TabView extends LinearLayout {
+        private ActionBar.Tab mTab;
+        private TextView mTextView;
+        private ImageView mIconView;
+        private View mCustomView;
+
+        public TabView(Context context, ActionBar.Tab tab, boolean forList) {
+            super(context, null, com.android.internal.R.attr.actionBarTabStyle);
+            mTab = tab;
+
+            if (forList) {
+                setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+            }
+
+            update();
+        }
+
+        public void bindTab(ActionBar.Tab tab) {
+            mTab = tab;
+            update();
+        }
+
+        @Override
+        public void setSelected(boolean selected) {
+            final boolean changed = (isSelected() != selected);
+            super.setSelected(selected);
+            if (changed && selected) {
+                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+            }
+        }
+
+        @Override
+        public CharSequence getAccessibilityClassName() {
+            // This view masquerades as an action bar tab.
+            return ActionBar.Tab.class.getName();
+        }
+
+        @Override
+        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+            // Re-measure if we went beyond our maximum size.
+            if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) {
+                super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY),
+                        heightMeasureSpec);
+            }
+        }
+
+        public void update() {
+            final ActionBar.Tab tab = mTab;
+            final View custom = tab.getCustomView();
+            if (custom != null) {
+                final ViewParent customParent = custom.getParent();
+                if (customParent != this) {
+                    if (customParent != null) ((ViewGroup) customParent).removeView(custom);
+                    addView(custom);
+                }
+                mCustomView = custom;
+                if (mTextView != null) mTextView.setVisibility(GONE);
+                if (mIconView != null) {
+                    mIconView.setVisibility(GONE);
+                    mIconView.setImageDrawable(null);
+                }
+            } else {
+                if (mCustomView != null) {
+                    removeView(mCustomView);
+                    mCustomView = null;
+                }
+
+                final Drawable icon = tab.getIcon();
+                final CharSequence text = tab.getText();
+
+                if (icon != null) {
+                    if (mIconView == null) {
+                        ImageView iconView = new ImageView(getContext());
+                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                                LayoutParams.WRAP_CONTENT);
+                        lp.gravity = Gravity.CENTER_VERTICAL;
+                        iconView.setLayoutParams(lp);
+                        addView(iconView, 0);
+                        mIconView = iconView;
+                    }
+                    mIconView.setImageDrawable(icon);
+                    mIconView.setVisibility(VISIBLE);
+                } else if (mIconView != null) {
+                    mIconView.setVisibility(GONE);
+                    mIconView.setImageDrawable(null);
+                }
+
+                final boolean hasText = !TextUtils.isEmpty(text);
+                if (hasText) {
+                    if (mTextView == null) {
+                        TextView textView = new TextView(getContext(), null,
+                                com.android.internal.R.attr.actionBarTabTextStyle);
+                        textView.setEllipsize(TruncateAt.END);
+                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                                LayoutParams.WRAP_CONTENT);
+                        lp.gravity = Gravity.CENTER_VERTICAL;
+                        textView.setLayoutParams(lp);
+                        addView(textView);
+                        mTextView = textView;
+                    }
+                    mTextView.setText(text);
+                    mTextView.setVisibility(VISIBLE);
+                } else if (mTextView != null) {
+                    mTextView.setVisibility(GONE);
+                    mTextView.setText(null);
+                }
+
+                if (mIconView != null) {
+                    mIconView.setContentDescription(tab.getContentDescription());
+                }
+                setTooltipText(hasText? null : tab.getContentDescription());
+            }
+        }
+
+        public ActionBar.Tab getTab() {
+            return mTab;
+        }
+    }
+
+    private class TabAdapter extends BaseAdapter {
+        private Context mDropDownContext;
+
+        public TabAdapter(Context context) {
+            setDropDownViewContext(context);
+        }
+
+        public void setDropDownViewContext(Context context) {
+            mDropDownContext = context;
+        }
+
+        @Override
+        public int getCount() {
+            return mTabLayout.getChildCount();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return ((TabView) mTabLayout.getChildAt(position)).getTab();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = createTabView(mContext, (ActionBar.Tab) getItem(position), true);
+            } else {
+                ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
+            }
+            return convertView;
+        }
+
+        @Override
+        public View getDropDownView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = createTabView(mDropDownContext,
+                        (ActionBar.Tab) getItem(position), true);
+            } else {
+                ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
+            }
+            return convertView;
+        }
+    }
+
+    private class TabClickListener implements OnClickListener {
+        public void onClick(View view) {
+            TabView tabView = (TabView) view;
+            tabView.getTab().select();
+            final int tabCount = mTabLayout.getChildCount();
+            for (int i = 0; i < tabCount; i++) {
+                final View child = mTabLayout.getChildAt(i);
+                child.setSelected(child == view);
+            }
+        }
+    }
+
+    protected class VisibilityAnimListener implements Animator.AnimatorListener {
+        private boolean mCanceled = false;
+        private int mFinalVisibility;
+
+        public VisibilityAnimListener withFinalVisibility(int visibility) {
+            mFinalVisibility = visibility;
+            return this;
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+            setVisibility(VISIBLE);
+            mVisibilityAnim = animation;
+            mCanceled = false;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (mCanceled) return;
+
+            mVisibilityAnim = null;
+            setVisibility(mFinalVisibility);
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mCanceled = true;
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+        }
+    }
+}
diff --git a/com/android/internal/widget/ScrollingView.java b/com/android/internal/widget/ScrollingView.java
new file mode 100644
index 0000000..a0205e7
--- /dev/null
+++ b/com/android/internal/widget/ScrollingView.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+/**
+ * An interface that can be implemented by Views to provide scroll related APIs.
+ */
+public interface ScrollingView {
+    /**
+     * <p>Compute the horizontal range that the horizontal scrollbar
+     * represents.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the
+     * units used by {@link #computeHorizontalScrollExtent()} and
+     * {@link #computeHorizontalScrollOffset()}.</p>
+     *
+     * <p>The default range is the drawing width of this view.</p>
+     *
+     * @return the total horizontal range represented by the horizontal
+     *         scrollbar
+     *
+     * @see #computeHorizontalScrollExtent()
+     * @see #computeHorizontalScrollOffset()
+     * @see android.widget.ScrollBarDrawable
+     */
+    int computeHorizontalScrollRange();
+
+    /**
+     * <p>Compute the horizontal offset of the horizontal scrollbar's thumb
+     * within the horizontal range. This value is used to compute the position
+     * of the thumb within the scrollbar's track.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the
+     * units used by {@link #computeHorizontalScrollRange()} and
+     * {@link #computeHorizontalScrollExtent()}.</p>
+     *
+     * <p>The default offset is the scroll offset of this view.</p>
+     *
+     * @return the horizontal offset of the scrollbar's thumb
+     *
+     * @see #computeHorizontalScrollRange()
+     * @see #computeHorizontalScrollExtent()
+     * @see android.widget.ScrollBarDrawable
+     */
+    int computeHorizontalScrollOffset();
+
+    /**
+     * <p>Compute the horizontal extent of the horizontal scrollbar's thumb
+     * within the horizontal range. This value is used to compute the length
+     * of the thumb within the scrollbar's track.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the
+     * units used by {@link #computeHorizontalScrollRange()} and
+     * {@link #computeHorizontalScrollOffset()}.</p>
+     *
+     * <p>The default extent is the drawing width of this view.</p>
+     *
+     * @return the horizontal extent of the scrollbar's thumb
+     *
+     * @see #computeHorizontalScrollRange()
+     * @see #computeHorizontalScrollOffset()
+     * @see android.widget.ScrollBarDrawable
+     */
+    int computeHorizontalScrollExtent();
+
+    /**
+     * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the
+     * units used by {@link #computeVerticalScrollExtent()} and
+     * {@link #computeVerticalScrollOffset()}.</p>
+     *
+     * @return the total vertical range represented by the vertical scrollbar
+     *
+     * <p>The default range is the drawing height of this view.</p>
+     *
+     * @see #computeVerticalScrollExtent()
+     * @see #computeVerticalScrollOffset()
+     * @see android.widget.ScrollBarDrawable
+     */
+    int computeVerticalScrollRange();
+
+    /**
+     * <p>Compute the vertical offset of the vertical scrollbar's thumb
+     * within the horizontal range. This value is used to compute the position
+     * of the thumb within the scrollbar's track.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the
+     * units used by {@link #computeVerticalScrollRange()} and
+     * {@link #computeVerticalScrollExtent()}.</p>
+     *
+     * <p>The default offset is the scroll offset of this view.</p>
+     *
+     * @return the vertical offset of the scrollbar's thumb
+     *
+     * @see #computeVerticalScrollRange()
+     * @see #computeVerticalScrollExtent()
+     * @see android.widget.ScrollBarDrawable
+     */
+    int computeVerticalScrollOffset();
+
+    /**
+     * <p>Compute the vertical extent of the vertical scrollbar's thumb
+     * within the vertical range. This value is used to compute the length
+     * of the thumb within the scrollbar's track.</p>
+     *
+     * <p>The range is expressed in arbitrary units that must be the same as the
+     * units used by {@link #computeVerticalScrollRange()} and
+     * {@link #computeVerticalScrollOffset()}.</p>
+     *
+     * <p>The default extent is the drawing height of this view.</p>
+     *
+     * @return the vertical extent of the scrollbar's thumb
+     *
+     * @see #computeVerticalScrollRange()
+     * @see #computeVerticalScrollOffset()
+     * @see android.widget.ScrollBarDrawable
+     */
+    int computeVerticalScrollExtent();
+}
diff --git a/com/android/internal/widget/SimpleItemAnimator.java b/com/android/internal/widget/SimpleItemAnimator.java
new file mode 100644
index 0000000..f4cc753
--- /dev/null
+++ b/com/android/internal/widget/SimpleItemAnimator.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+import android.view.View;
+
+import com.android.internal.widget.RecyclerView.Adapter;
+import com.android.internal.widget.RecyclerView.ViewHolder;
+
+/**
+ * A wrapper class for ItemAnimator that records View bounds and decides whether it should run
+ * move, change, add or remove animations. This class also replicates the original ItemAnimator
+ * API.
+ * <p>
+ * It uses {@link ItemHolderInfo} to track the bounds information of the Views. If you would like
+ * to
+ * extend this class, you can override {@link #obtainHolderInfo()} method to provide your own info
+ * class that extends {@link ItemHolderInfo}.
+ */
+public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
+
+    private static final boolean DEBUG = false;
+
+    private static final String TAG = "SimpleItemAnimator";
+
+    boolean mSupportsChangeAnimations = true;
+
+    /**
+     * Returns whether this ItemAnimator supports animations of change events.
+     *
+     * @return true if change animations are supported, false otherwise
+     */
+    @SuppressWarnings("unused")
+    public boolean getSupportsChangeAnimations() {
+        return mSupportsChangeAnimations;
+    }
+
+    /**
+     * Sets whether this ItemAnimator supports animations of item change events.
+     * If you set this property to false, actions on the data set which change the
+     * contents of items will not be animated. What those animations do is left
+     * up to the discretion of the ItemAnimator subclass, in its
+     * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} implementation.
+     * The value of this property is true by default.
+     *
+     * @param supportsChangeAnimations true if change animations are supported by
+     *                                 this ItemAnimator, false otherwise. If the property is false,
+     *                                 the ItemAnimator
+     *                                 will not receive a call to
+     *                                 {@link #animateChange(ViewHolder, ViewHolder, int, int, int,
+     *                                 int)} when changes occur.
+     * @see Adapter#notifyItemChanged(int)
+     * @see Adapter#notifyItemRangeChanged(int, int)
+     */
+    public void setSupportsChangeAnimations(boolean supportsChangeAnimations) {
+        mSupportsChangeAnimations = supportsChangeAnimations;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return True if change animations are not supported or the ViewHolder is invalid,
+     * false otherwise.
+     *
+     * @see #setSupportsChangeAnimations(boolean)
+     */
+    @Override
+    public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
+        return !mSupportsChangeAnimations || viewHolder.isInvalid();
+    }
+
+    @Override
+    public boolean animateDisappearance(@NonNull ViewHolder viewHolder,
+            @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
+        int oldLeft = preLayoutInfo.left;
+        int oldTop = preLayoutInfo.top;
+        View disappearingItemView = viewHolder.itemView;
+        int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
+        int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
+        if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
+            disappearingItemView.layout(newLeft, newTop,
+                    newLeft + disappearingItemView.getWidth(),
+                    newTop + disappearingItemView.getHeight());
+            if (DEBUG) {
+                Log.d(TAG, "DISAPPEARING: " + viewHolder + " with view " + disappearingItemView);
+            }
+            return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "REMOVED: " + viewHolder + " with view " + disappearingItemView);
+            }
+            return animateRemove(viewHolder);
+        }
+    }
+
+    @Override
+    public boolean animateAppearance(@NonNull ViewHolder viewHolder,
+            @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
+        if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
+                || preLayoutInfo.top != postLayoutInfo.top)) {
+            // slide items in if before/after locations differ
+            if (DEBUG) {
+                Log.d(TAG, "APPEARING: " + viewHolder + " with view " + viewHolder);
+            }
+            return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
+                    postLayoutInfo.left, postLayoutInfo.top);
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "ADDED: " + viewHolder + " with view " + viewHolder);
+            }
+            return animateAdd(viewHolder);
+        }
+    }
+
+    @Override
+    public boolean animatePersistence(@NonNull ViewHolder viewHolder,
+            @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+        if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
+            if (DEBUG) {
+                Log.d(TAG, "PERSISTENT: " + viewHolder
+                        + " with view " + viewHolder.itemView);
+            }
+            return animateMove(viewHolder,
+                    preInfo.left, preInfo.top, postInfo.left, postInfo.top);
+        }
+        dispatchMoveFinished(viewHolder);
+        return false;
+    }
+
+    @Override
+    public boolean animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
+            @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+        if (DEBUG) {
+            Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView);
+        }
+        final int fromLeft = preInfo.left;
+        final int fromTop = preInfo.top;
+        final int toLeft, toTop;
+        if (newHolder.shouldIgnore()) {
+            toLeft = preInfo.left;
+            toTop = preInfo.top;
+        } else {
+            toLeft = postInfo.left;
+            toTop = postInfo.top;
+        }
+        return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop);
+    }
+
+    /**
+     * Called when an item is removed from the RecyclerView. Implementors can choose
+     * whether and how to animate that change, but must always call
+     * {@link #dispatchRemoveFinished(ViewHolder)} when done, either
+     * immediately (if no animation will occur) or after the animation actually finishes.
+     * The return value indicates whether an animation has been set up and whether the
+     * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+     * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+     * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+     * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+     * {@link #animateRemove(ViewHolder) animateRemove()}, and
+     * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+     * then start the animations together in the later call to {@link #runPendingAnimations()}.
+     *
+     * <p>This method may also be called for disappearing items which continue to exist in the
+     * RecyclerView, but for which the system does not have enough information to animate
+     * them out of view. In that case, the default animation for removing items is run
+     * on those items as well.</p>
+     *
+     * @param holder The item that is being removed.
+     * @return true if a later call to {@link #runPendingAnimations()} is requested,
+     * false otherwise.
+     */
+    public abstract boolean animateRemove(ViewHolder holder);
+
+    /**
+     * Called when an item is added to the RecyclerView. Implementors can choose
+     * whether and how to animate that change, but must always call
+     * {@link #dispatchAddFinished(ViewHolder)} when done, either
+     * immediately (if no animation will occur) or after the animation actually finishes.
+     * The return value indicates whether an animation has been set up and whether the
+     * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+     * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+     * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+     * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+     * {@link #animateRemove(ViewHolder) animateRemove()}, and
+     * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+     * then start the animations together in the later call to {@link #runPendingAnimations()}.
+     *
+     * <p>This method may also be called for appearing items which were already in the
+     * RecyclerView, but for which the system does not have enough information to animate
+     * them into view. In that case, the default animation for adding items is run
+     * on those items as well.</p>
+     *
+     * @param holder The item that is being added.
+     * @return true if a later call to {@link #runPendingAnimations()} is requested,
+     * false otherwise.
+     */
+    public abstract boolean animateAdd(ViewHolder holder);
+
+    /**
+     * Called when an item is moved in the RecyclerView. Implementors can choose
+     * whether and how to animate that change, but must always call
+     * {@link #dispatchMoveFinished(ViewHolder)} when done, either
+     * immediately (if no animation will occur) or after the animation actually finishes.
+     * The return value indicates whether an animation has been set up and whether the
+     * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+     * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+     * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+     * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+     * {@link #animateRemove(ViewHolder) animateRemove()}, and
+     * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+     * then start the animations together in the later call to {@link #runPendingAnimations()}.
+     *
+     * @param holder The item that is being moved.
+     * @return true if a later call to {@link #runPendingAnimations()} is requested,
+     * false otherwise.
+     */
+    public abstract boolean animateMove(ViewHolder holder, int fromX, int fromY,
+            int toX, int toY);
+
+    /**
+     * Called when an item is changed in the RecyclerView, as indicated by a call to
+     * {@link Adapter#notifyItemChanged(int)} or
+     * {@link Adapter#notifyItemRangeChanged(int, int)}.
+     * <p>
+     * Implementers can choose whether and how to animate changes, but must always call
+     * {@link #dispatchChangeFinished(ViewHolder, boolean)} for each non-null distinct ViewHolder,
+     * either immediately (if no animation will occur) or after the animation actually finishes.
+     * If the {@code oldHolder} is the same ViewHolder as the {@code newHolder}, you must call
+     * {@link #dispatchChangeFinished(ViewHolder, boolean)} once and only once. In that case, the
+     * second parameter of {@code dispatchChangeFinished} is ignored.
+     * <p>
+     * The return value indicates whether an animation has been set up and whether the
+     * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+     * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+     * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+     * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+     * {@link #animateRemove(ViewHolder) animateRemove()}, and
+     * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+     * then start the animations together in the later call to {@link #runPendingAnimations()}.
+     *
+     * @param oldHolder The original item that changed.
+     * @param newHolder The new item that was created with the changed content. Might be null
+     * @param fromLeft  Left of the old view holder
+     * @param fromTop   Top of the old view holder
+     * @param toLeft    Left of the new view holder
+     * @param toTop     Top of the new view holder
+     * @return true if a later call to {@link #runPendingAnimations()} is requested,
+     * false otherwise.
+     */
+    public abstract boolean animateChange(ViewHolder oldHolder,
+            ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop);
+
+    /**
+     * Method to be called by subclasses when a remove animation is done.
+     *
+     * @param item The item which has been removed
+     * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo,
+     * ItemHolderInfo)
+     */
+    public final void dispatchRemoveFinished(ViewHolder item) {
+        onRemoveFinished(item);
+        dispatchAnimationFinished(item);
+    }
+
+    /**
+     * Method to be called by subclasses when a move animation is done.
+     *
+     * @param item The item which has been moved
+     * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo,
+     * ItemHolderInfo)
+     * @see RecyclerView.ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+     * @see RecyclerView.ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+     */
+    public final void dispatchMoveFinished(ViewHolder item) {
+        onMoveFinished(item);
+        dispatchAnimationFinished(item);
+    }
+
+    /**
+     * Method to be called by subclasses when an add animation is done.
+     *
+     * @param item The item which has been added
+     */
+    public final void dispatchAddFinished(ViewHolder item) {
+        onAddFinished(item);
+        dispatchAnimationFinished(item);
+    }
+
+    /**
+     * Method to be called by subclasses when a change animation is done.
+     *
+     * @param item    The item which has been changed (this method must be called for
+     *                each non-null ViewHolder passed into
+     *                {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}).
+     * @param oldItem true if this is the old item that was changed, false if
+     *                it is the new item that replaced the old item.
+     * @see #animateChange(ViewHolder, ViewHolder, int, int, int, int)
+     */
+    public final void dispatchChangeFinished(ViewHolder item, boolean oldItem) {
+        onChangeFinished(item, oldItem);
+        dispatchAnimationFinished(item);
+    }
+
+    /**
+     * Method to be called by subclasses when a remove animation is being started.
+     *
+     * @param item The item being removed
+     */
+    public final void dispatchRemoveStarting(ViewHolder item) {
+        onRemoveStarting(item);
+    }
+
+    /**
+     * Method to be called by subclasses when a move animation is being started.
+     *
+     * @param item The item being moved
+     */
+    public final void dispatchMoveStarting(ViewHolder item) {
+        onMoveStarting(item);
+    }
+
+    /**
+     * Method to be called by subclasses when an add animation is being started.
+     *
+     * @param item The item being added
+     */
+    public final void dispatchAddStarting(ViewHolder item) {
+        onAddStarting(item);
+    }
+
+    /**
+     * Method to be called by subclasses when a change animation is being started.
+     *
+     * @param item    The item which has been changed (this method must be called for
+     *                each non-null ViewHolder passed into
+     *                {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}).
+     * @param oldItem true if this is the old item that was changed, false if
+     *                it is the new item that replaced the old item.
+     */
+    public final void dispatchChangeStarting(ViewHolder item, boolean oldItem) {
+        onChangeStarting(item, oldItem);
+    }
+
+    /**
+     * Called when a remove animation is being started on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item The ViewHolder being animated.
+     */
+    @SuppressWarnings("UnusedParameters")
+    public void onRemoveStarting(ViewHolder item) {
+    }
+
+    /**
+     * Called when a remove animation has ended on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item The ViewHolder being animated.
+     */
+    public void onRemoveFinished(ViewHolder item) {
+    }
+
+    /**
+     * Called when an add animation is being started on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item The ViewHolder being animated.
+     */
+    @SuppressWarnings("UnusedParameters")
+    public void onAddStarting(ViewHolder item) {
+    }
+
+    /**
+     * Called when an add animation has ended on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item The ViewHolder being animated.
+     */
+    public void onAddFinished(ViewHolder item) {
+    }
+
+    /**
+     * Called when a move animation is being started on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item The ViewHolder being animated.
+     */
+    @SuppressWarnings("UnusedParameters")
+    public void onMoveStarting(ViewHolder item) {
+    }
+
+    /**
+     * Called when a move animation has ended on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item The ViewHolder being animated.
+     */
+    public void onMoveFinished(ViewHolder item) {
+    }
+
+    /**
+     * Called when a change animation is being started on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item    The ViewHolder being animated.
+     * @param oldItem true if this is the old item that was changed, false if
+     *                it is the new item that replaced the old item.
+     */
+    @SuppressWarnings("UnusedParameters")
+    public void onChangeStarting(ViewHolder item, boolean oldItem) {
+    }
+
+    /**
+     * Called when a change animation has ended on the given ViewHolder.
+     * The default implementation does nothing. Subclasses may wish to override
+     * this method to handle any ViewHolder-specific operations linked to animation
+     * lifecycles.
+     *
+     * @param item    The ViewHolder being animated.
+     * @param oldItem true if this is the old item that was changed, false if
+     *                it is the new item that replaced the old item.
+     */
+    public void onChangeFinished(ViewHolder item, boolean oldItem) {
+    }
+}
+
diff --git a/com/android/internal/widget/SlidingTab.java b/com/android/internal/widget/SlidingTab.java
new file mode 100644
index 0000000..79adada
--- /dev/null
+++ b/com/android/internal/widget/SlidingTab.java
@@ -0,0 +1,883 @@
+/*
+ * 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.os.UserHandle;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ImageView.ScaleType;
+
+import com.android.internal.R;
+
+/**
+ * A special widget containing two Sliders and a threshold for each.  Moving either slider beyond
+ * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
+ * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
+ * Equivalently, selecting a tab will result in a call to
+ * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
+ * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
+ *
+ */
+public class SlidingTab extends ViewGroup {
+    private static final String LOG_TAG = "SlidingTab";
+    private static final boolean DBG = false;
+    private static final int HORIZONTAL = 0; // as defined in attrs.xml
+    private static final int VERTICAL = 1;
+
+    // TODO: Make these configurable
+    private static final float THRESHOLD = 2.0f / 3.0f;
+    private static final long VIBRATE_SHORT = 30;
+    private static final long VIBRATE_LONG = 40;
+    private static final int TRACKING_MARGIN = 50;
+    private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
+    private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
+    private boolean mHoldLeftOnTransition = true;
+    private boolean mHoldRightOnTransition = true;
+
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build();
+
+    private OnTriggerListener mOnTriggerListener;
+    private int mGrabbedState = OnTriggerListener.NO_HANDLE;
+    private boolean mTriggered = false;
+    private Vibrator mVibrator;
+    private final float mDensity; // used to scale dimensions for bitmaps.
+
+    /**
+     * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
+     */
+    private final int mOrientation;
+
+    private final Slider mLeftSlider;
+    private final Slider mRightSlider;
+    private Slider mCurrentSlider;
+    private boolean mTracking;
+    private float mThreshold;
+    private Slider mOtherSlider;
+    private boolean mAnimating;
+    private final Rect mTmpRect;
+
+    /**
+     * Listener used to reset the view when the current animation completes.
+     */
+    private final AnimationListener mAnimationDoneListener = new AnimationListener() {
+        public void onAnimationStart(Animation animation) {
+
+        }
+
+        public void onAnimationRepeat(Animation animation) {
+
+        }
+
+        public void onAnimationEnd(Animation animation) {
+            onAnimationDone();
+        }
+    };
+
+    /**
+     * Interface definition for a callback to be invoked when a tab is triggered
+     * by moving it beyond a threshold.
+     */
+    public interface OnTriggerListener {
+        /**
+         * The interface was triggered because the user let go of the handle without reaching the
+         * threshold.
+         */
+        public static final int NO_HANDLE = 0;
+
+        /**
+         * The interface was triggered because the user grabbed the left handle and moved it past
+         * the threshold.
+         */
+        public static final int LEFT_HANDLE = 1;
+
+        /**
+         * The interface was triggered because the user grabbed the right handle and moved it past
+         * the threshold.
+         */
+        public static final int RIGHT_HANDLE = 2;
+
+        /**
+         * Called when the user moves a handle beyond the threshold.
+         *
+         * @param v The view that was triggered.
+         * @param whichHandle  Which "dial handle" the user grabbed,
+         *        either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
+         */
+        void onTrigger(View v, int whichHandle);
+
+        /**
+         * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
+         * one of the handles.)
+         *
+         * @param v the view that was triggered
+         * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
+         * or {@link #RIGHT_HANDLE}.
+         */
+        void onGrabbedStateChange(View v, int grabbedState);
+    }
+
+    /**
+     * Simple container class for all things pertinent to a slider.
+     * A slider consists of 3 Views:
+     *
+     * {@link #tab} is the tab shown on the screen in the default state.
+     * {@link #text} is the view revealed as the user slides the tab out.
+     * {@link #target} is the target the user must drag the slider past to trigger the slider.
+     *
+     */
+    private static class Slider {
+        /**
+         * Tab alignment - determines which side the tab should be drawn on
+         */
+        public static final int ALIGN_LEFT = 0;
+        public static final int ALIGN_RIGHT = 1;
+        public static final int ALIGN_TOP = 2;
+        public static final int ALIGN_BOTTOM = 3;
+        public static final int ALIGN_UNKNOWN = 4;
+
+        /**
+         * States for the view.
+         */
+        private static final int STATE_NORMAL = 0;
+        private static final int STATE_PRESSED = 1;
+        private static final int STATE_ACTIVE = 2;
+
+        private final ImageView tab;
+        private final TextView text;
+        private final ImageView target;
+        private int currentState = STATE_NORMAL;
+        private int alignment = ALIGN_UNKNOWN;
+        private int alignment_value;
+
+        /**
+         * Constructor
+         *
+         * @param parent the container view of this one
+         * @param tabId drawable for the tab
+         * @param barId drawable for the bar
+         * @param targetId drawable for the target
+         */
+        Slider(ViewGroup parent, int tabId, int barId, int targetId) {
+            // Create tab
+            tab = new ImageView(parent.getContext());
+            tab.setBackgroundResource(tabId);
+            tab.setScaleType(ScaleType.CENTER);
+            tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                    LayoutParams.WRAP_CONTENT));
+
+            // Create hint TextView
+            text = new TextView(parent.getContext());
+            text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                    LayoutParams.MATCH_PARENT));
+            text.setBackgroundResource(barId);
+            text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
+            // hint.setSingleLine();  // Hmm.. this causes the text to disappear off-screen
+
+            // Create target
+            target = new ImageView(parent.getContext());
+            target.setImageResource(targetId);
+            target.setScaleType(ScaleType.CENTER);
+            target.setLayoutParams(
+                    new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+            target.setVisibility(View.INVISIBLE);
+
+            parent.addView(target); // this needs to be first - relies on painter's algorithm
+            parent.addView(tab);
+            parent.addView(text);
+        }
+
+        void setIcon(int iconId) {
+            tab.setImageResource(iconId);
+        }
+
+        void setTabBackgroundResource(int tabId) {
+            tab.setBackgroundResource(tabId);
+        }
+
+        void setBarBackgroundResource(int barId) {
+            text.setBackgroundResource(barId);
+        }
+
+        void setHintText(int resId) {
+            text.setText(resId);
+        }
+
+        void hide() {
+            boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+            int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
+                    : alignment_value - tab.getLeft()) : 0;
+            int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
+                    : alignment_value - tab.getTop());
+
+            Animation trans = new TranslateAnimation(0, dx, 0, dy);
+            trans.setDuration(ANIM_DURATION);
+            trans.setFillAfter(true);
+            tab.startAnimation(trans);
+            text.startAnimation(trans);
+            target.setVisibility(View.INVISIBLE);
+        }
+
+        void show(boolean animate) {
+            text.setVisibility(View.VISIBLE);
+            tab.setVisibility(View.VISIBLE);
+            //target.setVisibility(View.INVISIBLE);
+            if (animate) {
+                boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+                int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
+                int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
+
+                Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
+                trans.setDuration(ANIM_DURATION);
+                tab.startAnimation(trans);
+                text.startAnimation(trans);
+            }
+        }
+
+        void setState(int state) {
+            text.setPressed(state == STATE_PRESSED);
+            tab.setPressed(state == STATE_PRESSED);
+            if (state == STATE_ACTIVE) {
+                final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
+                if (text.getBackground().isStateful()) {
+                    text.getBackground().setState(activeState);
+                }
+                if (tab.getBackground().isStateful()) {
+                    tab.getBackground().setState(activeState);
+                }
+                text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
+            } else {
+                text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
+            }
+            currentState = state;
+        }
+
+        void showTarget() {
+            AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
+            alphaAnim.setDuration(ANIM_TARGET_TIME);
+            target.startAnimation(alphaAnim);
+            target.setVisibility(View.VISIBLE);
+        }
+
+        void reset(boolean animate) {
+            setState(STATE_NORMAL);
+            text.setVisibility(View.VISIBLE);
+            text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
+            tab.setVisibility(View.VISIBLE);
+            target.setVisibility(View.INVISIBLE);
+            final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+            int dx = horiz ? (alignment == ALIGN_LEFT ?  alignment_value - tab.getLeft()
+                    : alignment_value - tab.getRight()) : 0;
+            int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
+                    : alignment_value - tab.getBottom());
+            if (animate) {
+                TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
+                trans.setDuration(ANIM_DURATION);
+                trans.setFillAfter(false);
+                text.startAnimation(trans);
+                tab.startAnimation(trans);
+            } else {
+                if (horiz) {
+                    text.offsetLeftAndRight(dx);
+                    tab.offsetLeftAndRight(dx);
+                } else {
+                    text.offsetTopAndBottom(dy);
+                    tab.offsetTopAndBottom(dy);
+                }
+                text.clearAnimation();
+                tab.clearAnimation();
+                target.clearAnimation();
+            }
+        }
+
+        void setTarget(int targetId) {
+            target.setImageResource(targetId);
+        }
+
+        /**
+         * Layout the given widgets within the parent.
+         *
+         * @param l the parent's left border
+         * @param t the parent's top border
+         * @param r the parent's right border
+         * @param b the parent's bottom border
+         * @param alignment which side to align the widget to
+         */
+        void layout(int l, int t, int r, int b, int alignment) {
+            this.alignment = alignment;
+            final Drawable tabBackground = tab.getBackground();
+            final int handleWidth = tabBackground.getIntrinsicWidth();
+            final int handleHeight = tabBackground.getIntrinsicHeight();
+            final Drawable targetDrawable = target.getDrawable();
+            final int targetWidth = targetDrawable.getIntrinsicWidth();
+            final int targetHeight = targetDrawable.getIntrinsicHeight();
+            final int parentWidth = r - l;
+            final int parentHeight = b - t;
+
+            final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
+            final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
+            final int left = (parentWidth - handleWidth) / 2;
+            final int right = left + handleWidth;
+
+            if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
+                // horizontal
+                final int targetTop = (parentHeight - targetHeight) / 2;
+                final int targetBottom = targetTop + targetHeight;
+                final int top = (parentHeight - handleHeight) / 2;
+                final int bottom = (parentHeight + handleHeight) / 2;
+                if (alignment == ALIGN_LEFT) {
+                    tab.layout(0, top, handleWidth, bottom);
+                    text.layout(0 - parentWidth, top, 0, bottom);
+                    text.setGravity(Gravity.RIGHT);
+                    target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
+                    alignment_value = l;
+                } else {
+                    tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
+                    text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
+                    target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
+                    text.setGravity(Gravity.TOP);
+                    alignment_value = r;
+                }
+            } else {
+                // vertical
+                final int targetLeft = (parentWidth - targetWidth) / 2;
+                final int targetRight = (parentWidth + targetWidth) / 2;
+                final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
+                final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
+                if (alignment == ALIGN_TOP) {
+                    tab.layout(left, 0, right, handleHeight);
+                    text.layout(left, 0 - parentHeight, right, 0);
+                    target.layout(targetLeft, top, targetRight, top + targetHeight);
+                    alignment_value = t;
+                } else {
+                    tab.layout(left, parentHeight - handleHeight, right, parentHeight);
+                    text.layout(left, parentHeight, right, parentHeight + parentHeight);
+                    target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
+                    alignment_value = b;
+                }
+            }
+        }
+
+        public void updateDrawableStates() {
+            setState(currentState);
+        }
+
+        /**
+         * Ensure all the dependent widgets are measured.
+         */
+        public void measure(int widthMeasureSpec, int heightMeasureSpec) {
+            int width = MeasureSpec.getSize(widthMeasureSpec);
+            int height = MeasureSpec.getSize(heightMeasureSpec);
+            tab.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
+                    View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
+            text.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
+                    View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
+        }
+
+        /**
+         * Get the measured tab width. Must be called after {@link Slider#measure()}.
+         * @return
+         */
+        public int getTabWidth() {
+            return tab.getMeasuredWidth();
+        }
+
+        /**
+         * Get the measured tab width. Must be called after {@link Slider#measure()}.
+         * @return
+         */
+        public int getTabHeight() {
+            return tab.getMeasuredHeight();
+        }
+
+        /**
+         * Start animating the slider. Note we need two animations since a ValueAnimator
+         * keeps internal state of the invalidation region which is just the view being animated.
+         *
+         * @param anim1
+         * @param anim2
+         */
+        public void startAnimation(Animation anim1, Animation anim2) {
+            tab.startAnimation(anim1);
+            text.startAnimation(anim2);
+        }
+
+        public void hideTarget() {
+            target.clearAnimation();
+            target.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    public SlidingTab(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Constructor used when this widget is created from a layout file.
+     */
+    public SlidingTab(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // Allocate a temporary once that can be used everywhere.
+        mTmpRect = new Rect();
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
+        mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
+        a.recycle();
+
+        Resources r = getResources();
+        mDensity = r.getDisplayMetrics().density;
+        if (DBG) log("- Density: " + mDensity);
+
+        mLeftSlider = new Slider(this,
+                R.drawable.jog_tab_left_generic,
+                R.drawable.jog_tab_bar_left_generic,
+                R.drawable.jog_tab_target_gray);
+        mRightSlider = new Slider(this,
+                R.drawable.jog_tab_right_generic,
+                R.drawable.jog_tab_bar_right_generic,
+                R.drawable.jog_tab_target_gray);
+
+        // setBackgroundColor(0x80808080);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+        int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
+
+        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
+
+        if (DBG) {
+            if (widthSpecMode == MeasureSpec.UNSPECIFIED 
+                    || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+                Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
+                        +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
+                        new RuntimeException(LOG_TAG + "stack:"));
+            }
+        }
+
+        mLeftSlider.measure(widthMeasureSpec, heightMeasureSpec);
+        mRightSlider.measure(widthMeasureSpec, heightMeasureSpec);
+        final int leftTabWidth = mLeftSlider.getTabWidth();
+        final int rightTabWidth = mRightSlider.getTabWidth();
+        final int leftTabHeight = mLeftSlider.getTabHeight();
+        final int rightTabHeight = mRightSlider.getTabHeight();
+        final int width;
+        final int height;
+        if (isHorizontal()) {
+            width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
+            height = Math.max(leftTabHeight, rightTabHeight);
+        } else {
+            width = Math.max(leftTabWidth, rightTabHeight);
+            height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
+        }
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        final int action = event.getAction();
+        final float x = event.getX();
+        final float y = event.getY();
+
+        if (mAnimating) {
+            return false;
+        }
+
+        View leftHandle = mLeftSlider.tab;
+        leftHandle.getHitRect(mTmpRect);
+        boolean leftHit = mTmpRect.contains((int) x, (int) y);
+
+        View rightHandle = mRightSlider.tab;
+        rightHandle.getHitRect(mTmpRect);
+        boolean rightHit = mTmpRect.contains((int)x, (int) y);
+
+        if (!mTracking && !(leftHit || rightHit)) {
+            return false;
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                mTracking = true;
+                mTriggered = false;
+                vibrate(VIBRATE_SHORT);
+                if (leftHit) {
+                    mCurrentSlider = mLeftSlider;
+                    mOtherSlider = mRightSlider;
+                    mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
+                    setGrabbedState(OnTriggerListener.LEFT_HANDLE);
+                } else {
+                    mCurrentSlider = mRightSlider;
+                    mOtherSlider = mLeftSlider;
+                    mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
+                    setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
+                }
+                mCurrentSlider.setState(Slider.STATE_PRESSED);
+                mCurrentSlider.showTarget();
+                mOtherSlider.hide();
+                break;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Reset the tabs to their original state and stop any existing animation.
+     * Animate them back into place if animate is true.
+     *
+     * @param animate
+     */
+    public void reset(boolean animate) {
+        mLeftSlider.reset(animate);
+        mRightSlider.reset(animate);
+        if (!animate) {
+            mAnimating = false;
+        }
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        // Clear animations so sliders don't continue to animate when we show the widget again.
+        if (visibility != getVisibility() && visibility == View.INVISIBLE) {
+           reset(false);
+        }
+        super.setVisibility(visibility);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mTracking) {
+            final int action = event.getAction();
+            final float x = event.getX();
+            final float y = event.getY();
+
+            switch (action) {
+                case MotionEvent.ACTION_MOVE:
+                    if (withinView(x, y, this) ) {
+                        moveHandle(x, y);
+                        float position = isHorizontal() ? x : y;
+                        float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
+                        boolean thresholdReached;
+                        if (isHorizontal()) {
+                            thresholdReached = mCurrentSlider == mLeftSlider ?
+                                    position > target : position < target;
+                        } else {
+                            thresholdReached = mCurrentSlider == mLeftSlider ?
+                                    position < target : position > target;
+                        }
+                        if (!mTriggered && thresholdReached) {
+                            mTriggered = true;
+                            mTracking = false;
+                            mCurrentSlider.setState(Slider.STATE_ACTIVE);
+                            boolean isLeft = mCurrentSlider == mLeftSlider;
+                            dispatchTriggerEvent(isLeft ?
+                                OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
+
+                            startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
+                            setGrabbedState(OnTriggerListener.NO_HANDLE);
+                        }
+                        break;
+                    }
+                    // Intentionally fall through - we're outside tracking rectangle
+
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    cancelGrab();
+                    break;
+            }
+        }
+
+        return mTracking || super.onTouchEvent(event);
+    }
+
+    private void cancelGrab() {
+        mTracking = false;
+        mTriggered = false;
+        mOtherSlider.show(true);
+        mCurrentSlider.reset(false);
+        mCurrentSlider.hideTarget();
+        mCurrentSlider = null;
+        mOtherSlider = null;
+        setGrabbedState(OnTriggerListener.NO_HANDLE);
+    }
+
+    void startAnimating(final boolean holdAfter) {
+        mAnimating = true;
+        final Animation trans1;
+        final Animation trans2;
+        final Slider slider = mCurrentSlider;
+        final Slider other = mOtherSlider;
+        final int dx;
+        final int dy;
+        if (isHorizontal()) {
+            int right = slider.tab.getRight();
+            int width = slider.tab.getWidth();
+            int left = slider.tab.getLeft();
+            int viewWidth = getWidth();
+            int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
+            dx =  slider == mRightSlider ? - (right + viewWidth - holdOffset)
+                    : (viewWidth - left) + viewWidth - holdOffset;
+            dy = 0;
+        } else {
+            int top = slider.tab.getTop();
+            int bottom = slider.tab.getBottom();
+            int height = slider.tab.getHeight();
+            int viewHeight = getHeight();
+            int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
+            dx = 0;
+            dy =  slider == mRightSlider ? (top + viewHeight - holdOffset)
+                    : - ((viewHeight - bottom) + viewHeight - holdOffset);
+        }
+        trans1 = new TranslateAnimation(0, dx, 0, dy);
+        trans1.setDuration(ANIM_DURATION);
+        trans1.setInterpolator(new LinearInterpolator());
+        trans1.setFillAfter(true);
+        trans2 = new TranslateAnimation(0, dx, 0, dy);
+        trans2.setDuration(ANIM_DURATION);
+        trans2.setInterpolator(new LinearInterpolator());
+        trans2.setFillAfter(true);
+
+        trans1.setAnimationListener(new AnimationListener() {
+            public void onAnimationEnd(Animation animation) {
+                Animation anim;
+                if (holdAfter) {
+                    anim = new TranslateAnimation(dx, dx, dy, dy);
+                    anim.setDuration(1000); // plenty of time for transitions
+                    mAnimating = false;
+                } else {
+                    anim = new AlphaAnimation(0.5f, 1.0f);
+                    anim.setDuration(ANIM_DURATION);
+                    resetView();
+                }
+                anim.setAnimationListener(mAnimationDoneListener);
+
+                /* Animation can be the same for these since the animation just holds */
+                mLeftSlider.startAnimation(anim, anim);
+                mRightSlider.startAnimation(anim, anim);
+            }
+
+            public void onAnimationRepeat(Animation animation) {
+
+            }
+
+            public void onAnimationStart(Animation animation) {
+
+            }
+
+        });
+
+        slider.hideTarget();
+        slider.startAnimation(trans1, trans2);
+    }
+
+    private void onAnimationDone() {
+        resetView();
+        mAnimating = false;
+    }
+
+    private boolean withinView(final float x, final float y, final View view) {
+        return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
+            || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
+    }
+
+    private boolean isHorizontal() {
+        return mOrientation == HORIZONTAL;
+    }
+
+    private void resetView() {
+        mLeftSlider.reset(false);
+        mRightSlider.reset(false);
+        // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (!changed) return;
+
+        // Center the widgets in the view
+        mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
+        mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
+    }
+
+    private void moveHandle(float x, float y) {
+        final View handle = mCurrentSlider.tab;
+        final View content = mCurrentSlider.text;
+        if (isHorizontal()) {
+            int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
+            handle.offsetLeftAndRight(deltaX);
+            content.offsetLeftAndRight(deltaX);
+        } else {
+            int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
+            handle.offsetTopAndBottom(deltaY);
+            content.offsetTopAndBottom(deltaY);
+        }
+        invalidate(); // TODO: be more conservative about what we're invalidating
+    }
+
+    /**
+     * Sets the left handle icon to a given resource.
+     *
+     * The resource should refer to a Drawable object, or use 0 to remove
+     * the icon.
+     *
+     * @param iconId the resource ID of the icon drawable
+     * @param targetId the resource of the target drawable
+     * @param barId the resource of the bar drawable (stateful)
+     * @param tabId the resource of the
+     */
+    public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
+        mLeftSlider.setIcon(iconId);
+        mLeftSlider.setTarget(targetId);
+        mLeftSlider.setBarBackgroundResource(barId);
+        mLeftSlider.setTabBackgroundResource(tabId);
+        mLeftSlider.updateDrawableStates();
+    }
+
+    /**
+     * Sets the left handle hint text to a given resource string.
+     *
+     * @param resId
+     */
+    public void setLeftHintText(int resId) {
+        if (isHorizontal()) {
+            mLeftSlider.setHintText(resId);
+        }
+    }
+
+    /**
+     * Sets the right handle icon to a given resource.
+     *
+     * The resource should refer to a Drawable object, or use 0 to remove
+     * the icon.
+     *
+     * @param iconId the resource ID of the icon drawable
+     * @param targetId the resource of the target drawable
+     * @param barId the resource of the bar drawable (stateful)
+     * @param tabId the resource of the
+     */
+    public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
+        mRightSlider.setIcon(iconId);
+        mRightSlider.setTarget(targetId);
+        mRightSlider.setBarBackgroundResource(barId);
+        mRightSlider.setTabBackgroundResource(tabId);
+        mRightSlider.updateDrawableStates();
+    }
+
+    /**
+     * Sets the left handle hint text to a given resource string.
+     *
+     * @param resId
+     */
+    public void setRightHintText(int resId) {
+        if (isHorizontal()) {
+            mRightSlider.setHintText(resId);
+        }
+    }
+
+    public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
+        mHoldLeftOnTransition = holdLeft;
+        mHoldRightOnTransition = holdRight;
+    }
+
+    /**
+     * Triggers haptic feedback.
+     */
+    private synchronized void vibrate(long duration) {
+        final boolean hapticEnabled = Settings.System.getIntForUser(
+                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
+                UserHandle.USER_CURRENT) != 0;
+        if (hapticEnabled) {
+            if (mVibrator == null) {
+                mVibrator = (android.os.Vibrator) getContext()
+                        .getSystemService(Context.VIBRATOR_SERVICE);
+            }
+            mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
+        }
+    }
+
+    /**
+     * Registers a callback to be invoked when the user triggers an event.
+     *
+     * @param listener the OnDialTriggerListener to attach to this view
+     */
+    public void setOnTriggerListener(OnTriggerListener listener) {
+        mOnTriggerListener = listener;
+    }
+
+    /**
+     * Dispatches a trigger event to listener. Ignored if a listener is not set.
+     * @param whichHandle the handle that triggered the event.
+     */
+    private void dispatchTriggerEvent(int whichHandle) {
+        vibrate(VIBRATE_LONG);
+        if (mOnTriggerListener != null) {
+            mOnTriggerListener.onTrigger(this, whichHandle);
+        }
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        // When visibility changes and the user has a tab selected, unselect it and
+        // make sure their callback gets called.
+        if (changedView == this && visibility != VISIBLE
+                && mGrabbedState != OnTriggerListener.NO_HANDLE) {
+            cancelGrab();
+        }
+    }
+
+    /**
+     * Sets the current grabbed state, and dispatches a grabbed state change
+     * event to our listener.
+     */
+    private void setGrabbedState(int newState) {
+        if (newState != mGrabbedState) {
+            mGrabbedState = newState;
+            if (mOnTriggerListener != null) {
+                mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
+            }
+        }
+    }
+
+    private void log(String msg) {
+        Log.d(LOG_TAG, msg);
+    }
+}
diff --git a/com/android/internal/widget/SubtitleView.java b/com/android/internal/widget/SubtitleView.java
new file mode 100644
index 0000000..1107828
--- /dev/null
+++ b/com/android/internal/widget/SubtitleView.java
@@ -0,0 +1,367 @@
+/*
+ * 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.internal.widget;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Join;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableStringBuilder;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+public class SubtitleView extends View {
+    // Ratio of inner padding to font size.
+    private static final float INNER_PADDING_RATIO = 0.125f;
+
+    /** Color used for the shadowed edge of a bevel. */
+    private static final int COLOR_BEVEL_DARK = 0x80000000;
+
+    /** Color used for the illuminated edge of a bevel. */
+    private static final int COLOR_BEVEL_LIGHT = 0x80FFFFFF;
+
+    // Styled dimensions.
+    private final float mCornerRadius;
+    private final float mOutlineWidth;
+    private final float mShadowRadius;
+    private final float mShadowOffsetX;
+    private final float mShadowOffsetY;
+
+    /** Temporary rectangle used for computing line bounds. */
+    private final RectF mLineBounds = new RectF();
+
+    /** Reusable spannable string builder used for holding text. */
+    private final SpannableStringBuilder mText = new SpannableStringBuilder();
+
+    private Alignment mAlignment;
+    private TextPaint mTextPaint;
+    private Paint mPaint;
+
+    private int mForegroundColor;
+    private int mBackgroundColor;
+    private int mEdgeColor;
+    private int mEdgeType;
+
+    private boolean mHasMeasurements;
+    private int mLastMeasuredWidth;
+    private StaticLayout mLayout;
+
+    private float mSpacingMult = 1;
+    private float mSpacingAdd = 0;
+    private int mInnerPaddingX = 0;
+
+    public SubtitleView(Context context) {
+        this(context, null);
+    }
+
+    public SubtitleView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                    attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
+
+        CharSequence text = "";
+        int textSize = 15;
+
+        final int n = a.getIndexCount();
+        for (int i = 0; i < n; i++) {
+            int attr = a.getIndex(i);
+
+            switch (attr) {
+                case android.R.styleable.TextView_text:
+                    text = a.getText(attr);
+                    break;
+                case android.R.styleable.TextView_lineSpacingExtra:
+                    mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
+                    break;
+                case android.R.styleable.TextView_lineSpacingMultiplier:
+                    mSpacingMult = a.getFloat(attr, mSpacingMult);
+                    break;
+                case android.R.styleable.TextAppearance_textSize:
+                    textSize = a.getDimensionPixelSize(attr, textSize);
+                    break;
+            }
+        }
+
+        // Set up density-dependent properties.
+        // TODO: Move these to a default style.
+        final Resources res = getContext().getResources();
+        mCornerRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_corner_radius);
+        mOutlineWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_outline_width);
+        mShadowRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_radius);
+        mShadowOffsetX = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_offset);
+        mShadowOffsetY = mShadowOffsetX;
+
+        mTextPaint = new TextPaint();
+        mTextPaint.setAntiAlias(true);
+        mTextPaint.setSubpixelText(true);
+
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+
+        setText(text);
+        setTextSize(textSize);
+    }
+
+    public void setText(int resId) {
+        final CharSequence text = getContext().getText(resId);
+        setText(text);
+    }
+
+    public void setText(CharSequence text) {
+        mText.clear();
+        mText.append(text);
+
+        mHasMeasurements = false;
+
+        requestLayout();
+        invalidate();
+    }
+
+    public void setForegroundColor(int color) {
+        mForegroundColor = color;
+
+        invalidate();
+    }
+
+    @Override
+    public void setBackgroundColor(int color) {
+        mBackgroundColor = color;
+
+        invalidate();
+    }
+
+    public void setEdgeType(int edgeType) {
+        mEdgeType = edgeType;
+
+        invalidate();
+    }
+
+    public void setEdgeColor(int color) {
+        mEdgeColor = color;
+
+        invalidate();
+    }
+
+    /**
+     * Sets the text size in pixels.
+     *
+     * @param size the text size in pixels
+     */
+    public void setTextSize(float size) {
+        if (mTextPaint.getTextSize() != size) {
+            mTextPaint.setTextSize(size);
+            mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
+
+            mHasMeasurements = false;
+
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    public void setTypeface(Typeface typeface) {
+        if (mTextPaint.getTypeface() != typeface) {
+            mTextPaint.setTypeface(typeface);
+
+            mHasMeasurements = false;
+
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    public void setAlignment(Alignment textAlignment) {
+        if (mAlignment != textAlignment) {
+            mAlignment = textAlignment;
+
+            mHasMeasurements = false;
+
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
+
+        if (computeMeasurements(widthSpec)) {
+            final StaticLayout layout = mLayout;
+
+            // Account for padding.
+            final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
+            final int width = layout.getWidth() + paddingX;
+            final int height = layout.getHeight() + mPaddingTop + mPaddingBottom;
+            setMeasuredDimension(width, height);
+        } else {
+            setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
+        }
+    }
+
+    @Override
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = r - l;
+
+        computeMeasurements(width);
+    }
+
+    private boolean computeMeasurements(int maxWidth) {
+        if (mHasMeasurements && maxWidth == mLastMeasuredWidth) {
+            return true;
+        }
+
+        // Account for padding.
+        final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
+        maxWidth -= paddingX;
+        if (maxWidth <= 0) {
+            return false;
+        }
+
+        // TODO: Implement minimum-difference line wrapping. Adding the results
+        // of Paint.getTextWidths() seems to return different values than
+        // StaticLayout.getWidth(), so this is non-trivial.
+        mHasMeasurements = true;
+        mLastMeasuredWidth = maxWidth;
+        mLayout = StaticLayout.Builder.obtain(mText, 0, mText.length(), mTextPaint, maxWidth)
+                .setAlignment(mAlignment)
+                .setLineSpacing(mSpacingAdd, mSpacingMult)
+                .setUseLineSpacingFromFallbacks(true)
+                .build();
+
+        return true;
+    }
+
+    public void setStyle(int styleId) {
+        final Context context = mContext;
+        final ContentResolver cr = context.getContentResolver();
+        final CaptionStyle style;
+        if (styleId == CaptionStyle.PRESET_CUSTOM) {
+            style = CaptionStyle.getCustomStyle(cr);
+        } else {
+            style = CaptionStyle.PRESETS[styleId];
+        }
+
+        final CaptionStyle defStyle = CaptionStyle.DEFAULT;
+        mForegroundColor = style.hasForegroundColor() ?
+                style.foregroundColor : defStyle.foregroundColor;
+        mBackgroundColor = style.hasBackgroundColor() ?
+                style.backgroundColor : defStyle.backgroundColor;
+        mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
+        mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
+        mHasMeasurements = false;
+
+        final Typeface typeface = style.getTypeface();
+        setTypeface(typeface);
+
+        requestLayout();
+    }
+
+    @Override
+    protected void onDraw(Canvas c) {
+        final StaticLayout layout = mLayout;
+        if (layout == null) {
+            return;
+        }
+
+        final int saveCount = c.save();
+        final int innerPaddingX = mInnerPaddingX;
+        c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
+
+        final int lineCount = layout.getLineCount();
+        final Paint textPaint = mTextPaint;
+        final Paint paint = mPaint;
+        final RectF bounds = mLineBounds;
+
+        if (Color.alpha(mBackgroundColor) > 0) {
+            final float cornerRadius = mCornerRadius;
+            float previousBottom = layout.getLineTop(0);
+
+            paint.setColor(mBackgroundColor);
+            paint.setStyle(Style.FILL);
+
+            for (int i = 0; i < lineCount; i++) {
+                bounds.left = layout.getLineLeft(i) -innerPaddingX;
+                bounds.right = layout.getLineRight(i) + innerPaddingX;
+                bounds.top = previousBottom;
+                bounds.bottom = layout.getLineBottom(i);
+                previousBottom = bounds.bottom;
+
+                c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
+            }
+        }
+
+        final int edgeType = mEdgeType;
+        if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
+            textPaint.setStrokeJoin(Join.ROUND);
+            textPaint.setStrokeWidth(mOutlineWidth);
+            textPaint.setColor(mEdgeColor);
+            textPaint.setStyle(Style.FILL_AND_STROKE);
+
+            for (int i = 0; i < lineCount; i++) {
+                layout.drawText(c, i, i);
+            }
+        } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+            textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
+        } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
+                || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
+            final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
+            final int colorUp = raised ? Color.WHITE : mEdgeColor;
+            final int colorDown = raised ? mEdgeColor : Color.WHITE;
+            final float offset = mShadowRadius / 2f;
+
+            textPaint.setColor(mForegroundColor);
+            textPaint.setStyle(Style.FILL);
+            textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+
+            for (int i = 0; i < lineCount; i++) {
+                layout.drawText(c, i, i);
+            }
+
+            textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
+        }
+
+        textPaint.setColor(mForegroundColor);
+        textPaint.setStyle(Style.FILL);
+
+        for (int i = 0; i < lineCount; i++) {
+            layout.drawText(c, i, i);
+        }
+
+        textPaint.setShadowLayer(0, 0, 0, 0);
+        c.restoreToCount(saveCount);
+    }
+}
diff --git a/com/android/internal/widget/SwipeDismissLayout.java b/com/android/internal/widget/SwipeDismissLayout.java
new file mode 100644
index 0000000..d2a9072
--- /dev/null
+++ b/com/android/internal/widget/SwipeDismissLayout.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ReceiverCallNotAllowedException;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+
+/**
+ * Special layout that finishes its activity when swiped away.
+ */
+public class SwipeDismissLayout extends FrameLayout {
+    private static final String TAG = "SwipeDismissLayout";
+
+    private static final float MAX_DIST_THRESHOLD = .33f;
+    private static final float MIN_DIST_THRESHOLD = .1f;
+
+    public interface OnDismissedListener {
+        void onDismissed(SwipeDismissLayout layout);
+    }
+
+    public interface OnSwipeProgressChangedListener {
+        /**
+         * Called when the layout has been swiped and the position of the window should change.
+         *
+         * @param alpha A number in [0, 1] representing what the alpha transparency of the window
+         * should be.
+         * @param translate A number in [0, w], where w is the width of the
+         * layout. This is equivalent to progress * layout.getWidth().
+         */
+        void onSwipeProgressChanged(SwipeDismissLayout layout, float alpha, float translate);
+
+        void onSwipeCancelled(SwipeDismissLayout layout);
+    }
+
+    private boolean mIsWindowNativelyTranslucent;
+
+    // Cached ViewConfiguration and system-wide constant values
+    private int mSlop;
+    private int mMinFlingVelocity;
+
+    // Transient properties
+    private int mActiveTouchId;
+    private float mDownX;
+    private float mDownY;
+    private float mLastX;
+    private boolean mSwiping;
+    private boolean mDismissed;
+    private boolean mDiscardIntercept;
+    private VelocityTracker mVelocityTracker;
+    private boolean mBlockGesture = false;
+    private boolean mActivityTranslucencyConverted = false;
+
+    private final DismissAnimator mDismissAnimator = new DismissAnimator();
+
+    private OnDismissedListener mDismissedListener;
+    private OnSwipeProgressChangedListener mProgressListener;
+    private BroadcastReceiver mScreenOffReceiver;
+    private IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+
+
+    private boolean mDismissable = true;
+
+    public SwipeDismissLayout(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public SwipeDismissLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    private void init(Context context) {
+        ViewConfiguration vc = ViewConfiguration.get(context);
+        mSlop = vc.getScaledTouchSlop();
+        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+        TypedArray a = context.getTheme().obtainStyledAttributes(
+                com.android.internal.R.styleable.Theme);
+        mIsWindowNativelyTranslucent = a.getBoolean(
+                com.android.internal.R.styleable.Window_windowIsTranslucent, false);
+        a.recycle();
+    }
+
+    public void setOnDismissedListener(OnDismissedListener listener) {
+        mDismissedListener = listener;
+    }
+
+    public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
+        mProgressListener = listener;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        try {
+            mScreenOffReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    post(() -> {
+                        if (mDismissed) {
+                            dismiss();
+                        } else {
+                            cancel();
+                        }
+                        resetMembers();
+                    });
+                }
+            };
+            getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter);
+        } catch (ReceiverCallNotAllowedException e) {
+            /* Exception is thrown if the context is a ReceiverRestrictedContext object. As
+             * ReceiverRestrictedContext is not public, the context type cannot be checked before
+             * calling registerReceiver. The most likely scenario in which the exception would be
+             * thrown would be when a BroadcastReceiver creates a dialog to show the user. */
+            mScreenOffReceiver = null; // clear receiver since it was not used.
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        if (mScreenOffReceiver != null) {
+            getContext().unregisterReceiver(mScreenOffReceiver);
+            mScreenOffReceiver = null;
+        }
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        checkGesture((ev));
+        if (mBlockGesture) {
+            return true;
+        }
+        if (!mDismissable) {
+            return super.onInterceptTouchEvent(ev);
+        }
+
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        ev.offsetLocation(ev.getRawX() - ev.getX(), 0);
+
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                resetMembers();
+                mDownX = ev.getRawX();
+                mDownY = ev.getRawY();
+                mActiveTouchId = ev.getPointerId(0);
+                mVelocityTracker = VelocityTracker.obtain("int1");
+                mVelocityTracker.addMovement(ev);
+                break;
+
+            case MotionEvent.ACTION_POINTER_DOWN:
+                int actionIndex = ev.getActionIndex();
+                mActiveTouchId = ev.getPointerId(actionIndex);
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                actionIndex = ev.getActionIndex();
+                int pointerId = ev.getPointerId(actionIndex);
+                if (pointerId == mActiveTouchId) {
+                    // This was our active pointer going up. Choose a new active pointer.
+                    int newActionIndex = actionIndex == 0 ? 1 : 0;
+                    mActiveTouchId = ev.getPointerId(newActionIndex);
+                }
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                resetMembers();
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                if (mVelocityTracker == null || mDiscardIntercept) {
+                    break;
+                }
+
+                int pointerIndex = ev.findPointerIndex(mActiveTouchId);
+                if (pointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointer index: ignoring.");
+                    mDiscardIntercept = true;
+                    break;
+                }
+                float dx = ev.getRawX() - mDownX;
+                float x = ev.getX(pointerIndex);
+                float y = ev.getY(pointerIndex);
+                if (dx != 0 && canScroll(this, false, dx, x, y)) {
+                    mDiscardIntercept = true;
+                    break;
+                }
+                updateSwiping(ev);
+                break;
+        }
+
+        return !mDiscardIntercept && mSwiping;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        checkGesture((ev));
+        if (mBlockGesture) {
+            return true;
+        }
+        if (mVelocityTracker == null || !mDismissable) {
+            return super.onTouchEvent(ev);
+        }
+
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        ev.offsetLocation(ev.getRawX() - ev.getX(), 0);
+
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_UP:
+                updateDismiss(ev);
+                if (mDismissed) {
+                    mDismissAnimator.animateDismissal(ev.getRawX() - mDownX);
+                } else if (mSwiping
+                        // Only trigger animation if we had a MOVE event that would shift the
+                        // underlying view, otherwise the animation would be janky.
+                        && mLastX != Integer.MIN_VALUE) {
+                    mDismissAnimator.animateRecovery(ev.getRawX() - mDownX);
+                }
+                resetMembers();
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+                cancel();
+                resetMembers();
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                mVelocityTracker.addMovement(ev);
+                mLastX = ev.getRawX();
+                updateSwiping(ev);
+                if (mSwiping) {
+                    setProgress(ev.getRawX() - mDownX);
+                    break;
+                }
+        }
+        return true;
+    }
+
+    private void setProgress(float deltaX) {
+        if (mProgressListener != null && deltaX >= 0)  {
+            mProgressListener.onSwipeProgressChanged(
+                    this, progressToAlpha(deltaX / getWidth()), deltaX);
+        }
+    }
+
+    private void dismiss() {
+        if (mDismissedListener != null) {
+            mDismissedListener.onDismissed(this);
+        }
+    }
+
+    protected void cancel() {
+        if (!mIsWindowNativelyTranslucent) {
+            Activity activity = findActivity();
+            if (activity != null && mActivityTranslucencyConverted) {
+                activity.convertFromTranslucent();
+                mActivityTranslucencyConverted = false;
+            }
+        }
+        if (mProgressListener != null) {
+            mProgressListener.onSwipeCancelled(this);
+        }
+    }
+
+    /**
+     * Resets internal members when canceling.
+     */
+    private void resetMembers() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+        }
+        mVelocityTracker = null;
+        mDownX = 0;
+        mLastX = Integer.MIN_VALUE;
+        mDownY = 0;
+        mSwiping = false;
+        mDismissed = false;
+        mDiscardIntercept = false;
+    }
+
+    private void updateSwiping(MotionEvent ev) {
+        boolean oldSwiping = mSwiping;
+        if (!mSwiping) {
+            float deltaX = ev.getRawX() - mDownX;
+            float deltaY = ev.getRawY() - mDownY;
+            if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) {
+                mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < Math.abs(deltaX);
+            } else {
+                mSwiping = false;
+            }
+        }
+
+        if (mSwiping && !oldSwiping) {
+            // Swiping has started
+            if (!mIsWindowNativelyTranslucent) {
+                Activity activity = findActivity();
+                if (activity != null) {
+                    mActivityTranslucencyConverted = activity.convertToTranslucent(null, null);
+                }
+            }
+        }
+    }
+
+    private void updateDismiss(MotionEvent ev) {
+        float deltaX = ev.getRawX() - mDownX;
+        // Don't add the motion event as an UP event would clear the velocity tracker
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float xVelocity = mVelocityTracker.getXVelocity();
+        if (mLastX == Integer.MIN_VALUE) {
+            // If there's no changes to mLastX, we have only one point of data, and therefore no
+            // velocity. Estimate velocity from just the up and down event in that case.
+            xVelocity = deltaX / ((ev.getEventTime() - ev.getDownTime()) / 1000);
+        }
+        if (!mDismissed) {
+            // Adjust the distance threshold linearly between the min and max threshold based on the
+            // x-velocity scaled with the the fling threshold speed
+            float distanceThreshold = getWidth() * Math.max(
+                    Math.min((MIN_DIST_THRESHOLD - MAX_DIST_THRESHOLD)
+                            * xVelocity / mMinFlingVelocity // scale x-velocity with fling velocity
+                            + MAX_DIST_THRESHOLD, // offset to start at max threshold
+                            MAX_DIST_THRESHOLD), // cap at max threshold
+                    MIN_DIST_THRESHOLD); // bottom out at min threshold
+            if ((deltaX > distanceThreshold && ev.getRawX() >= mLastX)
+                    || xVelocity >= mMinFlingVelocity) {
+                mDismissed = true;
+            }
+        }
+        // Check if the user tried to undo this.
+        if (mDismissed && mSwiping) {
+            // Check if the user's finger is actually flinging back to left
+            if (xVelocity < -mMinFlingVelocity) {
+                mDismissed = false;
+            }
+        }
+    }
+
+    /**
+     * Tests scrollability within child views of v in the direction of dx.
+     *
+     * @param v View to test for horizontal scrollability
+     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+     *               or just its children (false).
+     * @param dx Delta scrolled in pixels. Only the sign of this is used.
+     * @param x X coordinate of the active touch point
+     * @param y Y coordinate of the active touch point
+     * @return true if child views of v can be scrolled by delta of dx.
+     */
+    protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
+        if (v instanceof ViewGroup) {
+            final ViewGroup group = (ViewGroup) v;
+            final int scrollX = v.getScrollX();
+            final int scrollY = v.getScrollY();
+            final int count = group.getChildCount();
+            for (int i = count - 1; i >= 0; i--) {
+                final View child = group.getChildAt(i);
+                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
+                                y + scrollY - child.getTop())) {
+                    return true;
+                }
+            }
+        }
+
+        return checkV && v.canScrollHorizontally((int) -dx);
+    }
+
+    public void setDismissable(boolean dismissable) {
+        if (!dismissable && mDismissable) {
+            cancel();
+            resetMembers();
+        }
+
+        mDismissable = dismissable;
+    }
+
+    private void checkGesture(MotionEvent ev) {
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mBlockGesture = mDismissAnimator.isAnimating();
+        }
+    }
+
+    private float progressToAlpha(float progress) {
+        return 1 - progress * progress * progress;
+    }
+
+    private Activity findActivity() {
+        Context context = getContext();
+        while (context instanceof ContextWrapper) {
+            if (context instanceof Activity) {
+                return (Activity) context;
+            }
+            context = ((ContextWrapper) context).getBaseContext();
+        }
+        return null;
+    }
+
+    private class DismissAnimator implements AnimatorUpdateListener, Animator.AnimatorListener {
+        private final TimeInterpolator DISMISS_INTERPOLATOR = new DecelerateInterpolator(1.5f);
+        private final long DISMISS_DURATION = 250;
+
+        private final ValueAnimator mDismissAnimator = new ValueAnimator();
+        private boolean mWasCanceled = false;
+        private boolean mDismissOnComplete = false;
+
+        /* package */ DismissAnimator() {
+            mDismissAnimator.addUpdateListener(this);
+            mDismissAnimator.addListener(this);
+        }
+
+        /* package */ void animateDismissal(float currentTranslation) {
+            animate(
+                    currentTranslation / getWidth(),
+                    1,
+                    DISMISS_DURATION,
+                    DISMISS_INTERPOLATOR,
+                    true /* dismiss */);
+        }
+
+        /* package */ void animateRecovery(float currentTranslation) {
+            animate(
+                    currentTranslation / getWidth(),
+                    0,
+                    DISMISS_DURATION,
+                    DISMISS_INTERPOLATOR,
+                    false /* don't dismiss */);
+        }
+
+        /* package */ boolean isAnimating() {
+            return mDismissAnimator.isStarted();
+        }
+
+        private void animate(float from, float to, long duration, TimeInterpolator interpolator,
+                boolean dismissOnComplete) {
+            mDismissAnimator.cancel();
+            mDismissOnComplete = dismissOnComplete;
+            mDismissAnimator.setFloatValues(from, to);
+            mDismissAnimator.setDuration(duration);
+            mDismissAnimator.setInterpolator(interpolator);
+            mDismissAnimator.start();
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            float value = (Float) animation.getAnimatedValue();
+            setProgress(value * getWidth());
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+            mWasCanceled = false;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mWasCanceled = true;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (!mWasCanceled) {
+                if (mDismissOnComplete) {
+                    dismiss();
+                } else {
+                    cancel();
+                }
+            }
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+        }
+    }
+}
diff --git a/com/android/internal/widget/TextProgressBar.java b/com/android/internal/widget/TextProgressBar.java
new file mode 100644
index 0000000..7ca07d4
--- /dev/null
+++ b/com/android/internal/widget/TextProgressBar.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2008 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.internal.widget;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Chronometer;
+import android.widget.Chronometer.OnChronometerTickListener;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * Container that links together a {@link ProgressBar} and {@link Chronometer}
+ * as children. It subscribes to {@link Chronometer#OnChronometerTickListener}
+ * and updates the {@link ProgressBar} based on a preset finishing time.
+ * <p>
+ * This widget expects to contain two children with specific ids
+ * {@link android.R.id.progress} and {@link android.R.id.text1}.
+ * <p>
+ * If the {@link Chronometer} {@link android.R.attr#layout_width} is
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, then the
+ * {@link android.R.attr#gravity} will be used to automatically move it with
+ * respect to the {@link ProgressBar} position. For example, if
+ * {@link android.view.Gravity#LEFT} then the {@link Chronometer} will be placed
+ * just ahead of the leading edge of the {@link ProgressBar} position.
+ */
+@RemoteView
+public class TextProgressBar extends RelativeLayout implements OnChronometerTickListener {
+    public static final String TAG = "TextProgressBar"; 
+    
+    static final int CHRONOMETER_ID = android.R.id.text1;
+    static final int PROGRESSBAR_ID = android.R.id.progress;
+    
+    Chronometer mChronometer = null;
+    ProgressBar mProgressBar = null;
+    
+    long mDurationBase = -1;
+    int mDuration = -1;
+
+    boolean mChronometerFollow = false;
+    int mChronometerGravity = Gravity.NO_GRAVITY;
+
+    public TextProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+    
+    public TextProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public TextProgressBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public TextProgressBar(Context context) {
+        super(context);
+    }
+
+    /**
+     * Catch any interesting children when they are added.
+     */
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        super.addView(child, index, params);
+        
+        int childId = child.getId();
+        if (childId == CHRONOMETER_ID && child instanceof Chronometer) {
+            mChronometer = (Chronometer) child;
+            mChronometer.setOnChronometerTickListener(this);
+            
+            // Check if Chronometer should move with with ProgressBar 
+            mChronometerFollow = (params.width == ViewGroup.LayoutParams.WRAP_CONTENT);
+            mChronometerGravity = (mChronometer.getGravity() &
+                    Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK);
+            
+        } else if (childId == PROGRESSBAR_ID && child instanceof ProgressBar) {
+            mProgressBar = (ProgressBar) child;
+        }
+    }
+
+    /**
+     * Set the expected termination time of the running {@link Chronometer}.
+     * This value is used to adjust the {@link ProgressBar} against the elapsed
+     * time.
+     * <p>
+     * Call this <b>after</b> adjusting the {@link Chronometer} base, if
+     * necessary.
+     * 
+     * @param durationBase Use the {@link SystemClock#elapsedRealtime} time
+     *            base.
+     */
+    @android.view.RemotableViewMethod
+    public void setDurationBase(long durationBase) {
+        mDurationBase = durationBase;
+        
+        if (mProgressBar == null || mChronometer == null) {
+            throw new RuntimeException("Expecting child ProgressBar with id " +
+                    "'android.R.id.progress' and Chronometer id 'android.R.id.text1'");
+        }
+        
+        // Update the ProgressBar maximum relative to Chronometer base
+        mDuration = (int) (durationBase - mChronometer.getBase());
+        if (mDuration <= 0) {
+            mDuration = 1;
+        }
+        mProgressBar.setMax(mDuration);
+    }
+    
+    /**
+     * Callback when {@link Chronometer} changes, indicating that we should
+     * update the {@link ProgressBar} and change the layout if necessary.
+     */
+    public void onChronometerTick(Chronometer chronometer) {
+        if (mProgressBar == null) {
+            throw new RuntimeException(
+                "Expecting child ProgressBar with id 'android.R.id.progress'");
+        }
+        
+        // Stop Chronometer if we're past duration
+        long now = SystemClock.elapsedRealtime();
+        if (now >= mDurationBase) {
+            mChronometer.stop();
+        }
+
+        // Update the ProgressBar status
+        int remaining = (int) (mDurationBase - now);
+        mProgressBar.setProgress(mDuration - remaining);
+        
+        // Move the Chronometer if gravity is set correctly
+        if (mChronometerFollow) {
+            RelativeLayout.LayoutParams params;
+            
+            // Calculate estimate of ProgressBar leading edge position
+            params = (RelativeLayout.LayoutParams) mProgressBar.getLayoutParams();
+            int contentWidth = mProgressBar.getWidth() - (params.leftMargin + params.rightMargin);
+            int leadingEdge = ((contentWidth * mProgressBar.getProgress()) /
+                    mProgressBar.getMax()) + params.leftMargin;
+            
+            // Calculate any adjustment based on gravity
+            int adjustLeft = 0;
+            int textWidth = mChronometer.getWidth();
+            if (mChronometerGravity == Gravity.END) {
+                adjustLeft = -textWidth;
+            } else if (mChronometerGravity == Gravity.CENTER_HORIZONTAL) {
+                adjustLeft = -(textWidth / 2);
+            }
+            
+            // Limit margin to keep text inside ProgressBar bounds
+            leadingEdge += adjustLeft;
+            int rightLimit = contentWidth - params.rightMargin - textWidth;
+            if (leadingEdge < params.leftMargin) {
+                leadingEdge = params.leftMargin;
+            } else if (leadingEdge > rightLimit) {
+                leadingEdge = rightLimit;
+            }
+            
+            params = (RelativeLayout.LayoutParams) mChronometer.getLayoutParams();
+            params.leftMargin = leadingEdge;
+            
+            // Request layout to move Chronometer
+            mChronometer.requestLayout();
+            
+        }
+    }
+}
diff --git a/com/android/internal/widget/TextViewInputDisabler.java b/com/android/internal/widget/TextViewInputDisabler.java
new file mode 100644
index 0000000..fb0b3b9
--- /dev/null
+++ b/com/android/internal/widget/TextViewInputDisabler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.widget.TextView;
+
+/**
+ * Helper class to disable input on a TextView. The input is disabled by swapping in an InputFilter
+ * that discards all changes. Use with care if you have customized InputFilter on the target
+ * TextView.
+ */
+public class TextViewInputDisabler {
+    private TextView mTextView;
+    private InputFilter[] mDefaultFilters;
+    private InputFilter[] mNoInputFilters = new InputFilter[] {
+            new InputFilter () {
+                @Override
+                public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
+                        int dstart, int dend) {
+                    return "";
+                }
+            }
+    };
+
+    public TextViewInputDisabler(TextView textView) {
+        mTextView = textView;
+        mDefaultFilters = mTextView.getFilters();
+    }
+
+    public void setInputEnabled(boolean enabled) {
+        mTextView.setFilters(enabled ? mDefaultFilters : mNoInputFilters);
+    }
+}
diff --git a/com/android/internal/widget/ToolbarWidgetWrapper.java b/com/android/internal/widget/ToolbarWidgetWrapper.java
new file mode 100644
index 0000000..32aae72
--- /dev/null
+++ b/com/android/internal/widget/ToolbarWidgetWrapper.java
@@ -0,0 +1,708 @@
+/*
+ * Copyright (C) 2014 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.ActionMenuPresenter;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.Toolbar;
+import com.android.internal.R;
+import com.android.internal.view.menu.ActionMenuItem;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Internal class used to interact with the Toolbar widget without
+ * exposing interface methods to the public API.
+ *
+ * <p>ToolbarWidgetWrapper manages the differences between Toolbar and ActionBarView
+ * so that either variant acting as a
+ * {@link com.android.internal.app.WindowDecorActionBar WindowDecorActionBar} can behave
+ * in the same way.</p>
+ *
+ * @hide
+ */
+public class ToolbarWidgetWrapper implements DecorToolbar {
+    private static final String TAG = "ToolbarWidgetWrapper";
+
+    private static final int AFFECTS_LOGO_MASK =
+            ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_USE_LOGO;
+    // Default fade duration for fading in/out tool bar.
+    private static final long DEFAULT_FADE_DURATION_MS = 200;
+
+    private Toolbar mToolbar;
+
+    private int mDisplayOpts;
+    private View mTabView;
+    private Spinner mSpinner;
+    private View mCustomView;
+
+    private Drawable mIcon;
+    private Drawable mLogo;
+    private Drawable mNavIcon;
+
+    private boolean mTitleSet;
+    private CharSequence mTitle;
+    private CharSequence mSubtitle;
+    private CharSequence mHomeDescription;
+
+    private Window.Callback mWindowCallback;
+    private boolean mMenuPrepared;
+    private ActionMenuPresenter mActionMenuPresenter;
+
+    private int mNavigationMode = ActionBar.NAVIGATION_MODE_STANDARD;
+    private int mDefaultNavigationContentDescription = 0;
+    private Drawable mDefaultNavigationIcon;
+
+    public ToolbarWidgetWrapper(Toolbar toolbar, boolean style) {
+        this(toolbar, style, R.string.action_bar_up_description);
+    }
+
+    public ToolbarWidgetWrapper(Toolbar toolbar, boolean style,
+            int defaultNavigationContentDescription) {
+        mToolbar = toolbar;
+
+        mTitle = toolbar.getTitle();
+        mSubtitle = toolbar.getSubtitle();
+        mTitleSet = mTitle != null;
+        mNavIcon = mToolbar.getNavigationIcon();
+        final TypedArray a = toolbar.getContext().obtainStyledAttributes(null,
+                R.styleable.ActionBar, R.attr.actionBarStyle, 0);
+        mDefaultNavigationIcon = a.getDrawable(R.styleable.ActionBar_homeAsUpIndicator);
+        if (style) {
+            final CharSequence title = a.getText(R.styleable.ActionBar_title);
+            if (!TextUtils.isEmpty(title)) {
+                setTitle(title);
+            }
+
+            final CharSequence subtitle = a.getText(R.styleable.ActionBar_subtitle);
+            if (!TextUtils.isEmpty(subtitle)) {
+                setSubtitle(subtitle);
+            }
+
+            final Drawable logo = a.getDrawable(R.styleable.ActionBar_logo);
+            if (logo != null) {
+                setLogo(logo);
+            }
+
+            final Drawable icon = a.getDrawable(R.styleable.ActionBar_icon);
+            if (icon != null) {
+                setIcon(icon);
+            }
+            if (mNavIcon == null && mDefaultNavigationIcon != null) {
+                setNavigationIcon(mDefaultNavigationIcon);
+            }
+            setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, 0));
+
+            final int customNavId = a.getResourceId(
+                    R.styleable.ActionBar_customNavigationLayout, 0);
+            if (customNavId != 0) {
+                setCustomView(LayoutInflater.from(mToolbar.getContext()).inflate(customNavId,
+                        mToolbar, false));
+                setDisplayOptions(mDisplayOpts | ActionBar.DISPLAY_SHOW_CUSTOM);
+            }
+
+            final int height = a.getLayoutDimension(R.styleable.ActionBar_height, 0);
+            if (height > 0) {
+                final ViewGroup.LayoutParams lp = mToolbar.getLayoutParams();
+                lp.height = height;
+                mToolbar.setLayoutParams(lp);
+            }
+
+            final int contentInsetStart = a.getDimensionPixelOffset(
+                    R.styleable.ActionBar_contentInsetStart, -1);
+            final int contentInsetEnd = a.getDimensionPixelOffset(
+                    R.styleable.ActionBar_contentInsetEnd, -1);
+            if (contentInsetStart >= 0 || contentInsetEnd >= 0) {
+                mToolbar.setContentInsetsRelative(Math.max(contentInsetStart, 0),
+                        Math.max(contentInsetEnd, 0));
+            }
+
+            final int titleTextStyle = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0);
+            if (titleTextStyle != 0) {
+                mToolbar.setTitleTextAppearance(mToolbar.getContext(), titleTextStyle);
+            }
+
+            final int subtitleTextStyle = a.getResourceId(
+                    R.styleable.ActionBar_subtitleTextStyle, 0);
+            if (subtitleTextStyle != 0) {
+                mToolbar.setSubtitleTextAppearance(mToolbar.getContext(), subtitleTextStyle);
+            }
+
+            final int popupTheme = a.getResourceId(R.styleable.ActionBar_popupTheme, 0);
+            if (popupTheme != 0) {
+                mToolbar.setPopupTheme(popupTheme);
+            }
+        } else {
+            mDisplayOpts = detectDisplayOptions();
+        }
+        a.recycle();
+
+        setDefaultNavigationContentDescription(defaultNavigationContentDescription);
+        mHomeDescription = mToolbar.getNavigationContentDescription();
+
+        mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+            final ActionMenuItem mNavItem = new ActionMenuItem(mToolbar.getContext(),
+                    0, android.R.id.home, 0, 0, mTitle);
+            @Override
+            public void onClick(View v) {
+                if (mWindowCallback != null && mMenuPrepared) {
+                    mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, mNavItem);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setDefaultNavigationContentDescription(int defaultNavigationContentDescription) {
+        if (defaultNavigationContentDescription == mDefaultNavigationContentDescription) {
+            return;
+        }
+        mDefaultNavigationContentDescription = defaultNavigationContentDescription;
+        if (TextUtils.isEmpty(mToolbar.getNavigationContentDescription())) {
+            setNavigationContentDescription(mDefaultNavigationContentDescription);
+        }
+    }
+
+    private int detectDisplayOptions() {
+        int opts = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME |
+                ActionBar.DISPLAY_USE_LOGO;
+        if (mToolbar.getNavigationIcon() != null) {
+            opts |= ActionBar.DISPLAY_HOME_AS_UP;
+            mDefaultNavigationIcon = mToolbar.getNavigationIcon();
+        }
+        return opts;
+    }
+
+    @Override
+    public ViewGroup getViewGroup() {
+        return mToolbar;
+    }
+
+    @Override
+    public Context getContext() {
+        return mToolbar.getContext();
+    }
+
+    @Override
+    public boolean isSplit() {
+        return false;
+    }
+
+    @Override
+    public boolean hasExpandedActionView() {
+        return mToolbar.hasExpandedActionView();
+    }
+
+    @Override
+    public void collapseActionView() {
+        mToolbar.collapseActionView();
+    }
+
+    @Override
+    public void setWindowCallback(Window.Callback cb) {
+        mWindowCallback = cb;
+    }
+
+    @Override
+    public void setWindowTitle(CharSequence title) {
+        // "Real" title always trumps window title.
+        if (!mTitleSet) {
+            setTitleInt(title);
+        }
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        return mToolbar.getTitle();
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mTitleSet = true;
+        setTitleInt(title);
+    }
+
+    private void setTitleInt(CharSequence title) {
+        mTitle = title;
+        if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+            mToolbar.setTitle(title);
+        }
+    }
+
+    @Override
+    public CharSequence getSubtitle() {
+        return mToolbar.getSubtitle();
+    }
+
+    @Override
+    public void setSubtitle(CharSequence subtitle) {
+        mSubtitle = subtitle;
+        if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+            mToolbar.setSubtitle(subtitle);
+        }
+    }
+
+    @Override
+    public void initProgress() {
+        Log.i(TAG, "Progress display unsupported");
+    }
+
+    @Override
+    public void initIndeterminateProgress() {
+        Log.i(TAG, "Progress display unsupported");
+    }
+
+    @Override
+    public boolean canSplit() {
+        return false;
+    }
+
+    @Override
+    public void setSplitView(ViewGroup splitView) {
+    }
+
+    @Override
+    public void setSplitToolbar(boolean split) {
+        if (split) {
+            throw new UnsupportedOperationException("Cannot split an android.widget.Toolbar");
+        }
+    }
+
+    @Override
+    public void setSplitWhenNarrow(boolean splitWhenNarrow) {
+        // Ignore.
+    }
+
+    @Override
+    public boolean hasIcon() {
+        return mIcon != null;
+    }
+
+    @Override
+    public boolean hasLogo() {
+        return mLogo != null;
+    }
+
+    @Override
+    public void setIcon(int resId) {
+        setIcon(resId != 0 ? getContext().getDrawable(resId) : null);
+    }
+
+    @Override
+    public void setIcon(Drawable d) {
+        mIcon = d;
+        updateToolbarLogo();
+    }
+
+    @Override
+    public void setLogo(int resId) {
+        setLogo(resId != 0 ? getContext().getDrawable(resId) : null);
+    }
+
+    @Override
+    public void setLogo(Drawable d) {
+        mLogo = d;
+        updateToolbarLogo();
+    }
+
+    private void updateToolbarLogo() {
+        Drawable logo = null;
+        if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_HOME) != 0) {
+            if ((mDisplayOpts & ActionBar.DISPLAY_USE_LOGO) != 0) {
+                logo = mLogo != null ? mLogo : mIcon;
+            } else {
+                logo = mIcon;
+            }
+        }
+        mToolbar.setLogo(logo);
+    }
+
+    @Override
+    public boolean canShowOverflowMenu() {
+        return mToolbar.canShowOverflowMenu();
+    }
+
+    @Override
+    public boolean isOverflowMenuShowing() {
+        return mToolbar.isOverflowMenuShowing();
+    }
+
+    @Override
+    public boolean isOverflowMenuShowPending() {
+        return mToolbar.isOverflowMenuShowPending();
+    }
+
+    @Override
+    public boolean showOverflowMenu() {
+        return mToolbar.showOverflowMenu();
+    }
+
+    @Override
+    public boolean hideOverflowMenu() {
+        return mToolbar.hideOverflowMenu();
+    }
+
+    @Override
+    public void setMenuPrepared() {
+        mMenuPrepared = true;
+    }
+
+    @Override
+    public void setMenu(Menu menu, MenuPresenter.Callback cb) {
+        if (mActionMenuPresenter == null) {
+            mActionMenuPresenter = new ActionMenuPresenter(mToolbar.getContext());
+            mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
+        }
+        mActionMenuPresenter.setCallback(cb);
+        mToolbar.setMenu((MenuBuilder) menu, mActionMenuPresenter);
+    }
+
+    @Override
+    public void dismissPopupMenus() {
+        mToolbar.dismissPopupMenus();
+    }
+
+    @Override
+    public int getDisplayOptions() {
+        return mDisplayOpts;
+    }
+
+    @Override
+    public void setDisplayOptions(int newOpts) {
+        final int oldOpts = mDisplayOpts;
+        final int changed = oldOpts ^ newOpts;
+        mDisplayOpts = newOpts;
+        if (changed != 0) {
+            if ((changed & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+                if ((newOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+                    updateHomeAccessibility();
+                }
+                updateNavigationIcon();
+            }
+
+            if ((changed & AFFECTS_LOGO_MASK) != 0) {
+                updateToolbarLogo();
+            }
+
+            if ((changed & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+                if ((newOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+                    mToolbar.setTitle(mTitle);
+                    mToolbar.setSubtitle(mSubtitle);
+                } else {
+                    mToolbar.setTitle(null);
+                    mToolbar.setSubtitle(null);
+                }
+            }
+
+            if ((changed & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && mCustomView != null) {
+                if ((newOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+                    mToolbar.addView(mCustomView);
+                } else {
+                    mToolbar.removeView(mCustomView);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setEmbeddedTabView(ScrollingTabContainerView tabView) {
+        if (mTabView != null && mTabView.getParent() == mToolbar) {
+            mToolbar.removeView(mTabView);
+        }
+        mTabView = tabView;
+        if (tabView != null && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) {
+            mToolbar.addView(mTabView, 0);
+            Toolbar.LayoutParams lp = (Toolbar.LayoutParams) mTabView.getLayoutParams();
+            lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+            lp.gravity = Gravity.START | Gravity.BOTTOM;
+            tabView.setAllowCollapse(true);
+        }
+    }
+
+    @Override
+    public boolean hasEmbeddedTabs() {
+        return mTabView != null;
+    }
+
+    @Override
+    public boolean isTitleTruncated() {
+        return mToolbar.isTitleTruncated();
+    }
+
+    @Override
+    public void setCollapsible(boolean collapsible) {
+        mToolbar.setCollapsible(collapsible);
+    }
+
+    @Override
+    public void setHomeButtonEnabled(boolean enable) {
+        // Ignore
+    }
+
+    @Override
+    public int getNavigationMode() {
+        return mNavigationMode;
+    }
+
+    @Override
+    public void setNavigationMode(int mode) {
+        final int oldMode = mNavigationMode;
+        if (mode != oldMode) {
+            switch (oldMode) {
+                case ActionBar.NAVIGATION_MODE_LIST:
+                    if (mSpinner != null && mSpinner.getParent() == mToolbar) {
+                        mToolbar.removeView(mSpinner);
+                    }
+                    break;
+                case ActionBar.NAVIGATION_MODE_TABS:
+                    if (mTabView != null && mTabView.getParent() == mToolbar) {
+                        mToolbar.removeView(mTabView);
+                    }
+                    break;
+            }
+
+            mNavigationMode = mode;
+
+            switch (mode) {
+                case ActionBar.NAVIGATION_MODE_STANDARD:
+                    break;
+                case ActionBar.NAVIGATION_MODE_LIST:
+                    ensureSpinner();
+                    mToolbar.addView(mSpinner, 0);
+                    break;
+                case ActionBar.NAVIGATION_MODE_TABS:
+                    if (mTabView != null) {
+                        mToolbar.addView(mTabView, 0);
+                        Toolbar.LayoutParams lp = (Toolbar.LayoutParams) mTabView.getLayoutParams();
+                        lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+                        lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+                        lp.gravity = Gravity.START | Gravity.BOTTOM;
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid navigation mode " + mode);
+            }
+        }
+    }
+
+    private void ensureSpinner() {
+        if (mSpinner == null) {
+            mSpinner = new Spinner(getContext(), null, R.attr.actionDropDownStyle);
+            Toolbar.LayoutParams lp = new Toolbar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.CENTER_VERTICAL);
+            mSpinner.setLayoutParams(lp);
+        }
+    }
+
+    @Override
+    public void setDropdownParams(SpinnerAdapter adapter,
+            AdapterView.OnItemSelectedListener listener) {
+        ensureSpinner();
+        mSpinner.setAdapter(adapter);
+        mSpinner.setOnItemSelectedListener(listener);
+    }
+
+    @Override
+    public void setDropdownSelectedPosition(int position) {
+        if (mSpinner == null) {
+            throw new IllegalStateException(
+                    "Can't set dropdown selected position without an adapter");
+        }
+        mSpinner.setSelection(position);
+    }
+
+    @Override
+    public int getDropdownSelectedPosition() {
+        return mSpinner != null ? mSpinner.getSelectedItemPosition() : 0;
+    }
+
+    @Override
+    public int getDropdownItemCount() {
+        return mSpinner != null ? mSpinner.getCount() : 0;
+    }
+
+    @Override
+    public void setCustomView(View view) {
+        if (mCustomView != null && (mDisplayOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+            mToolbar.removeView(mCustomView);
+        }
+        mCustomView = view;
+        if (view != null && (mDisplayOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+            mToolbar.addView(mCustomView);
+        }
+    }
+
+    @Override
+    public View getCustomView() {
+        return mCustomView;
+    }
+
+    @Override
+    public void animateToVisibility(int visibility) {
+        Animator anim = setupAnimatorToVisibility(visibility, DEFAULT_FADE_DURATION_MS);
+        if (anim != null) {
+            anim.start();
+        }
+    }
+
+    @Override
+    public Animator setupAnimatorToVisibility(int visibility, long duration) {
+
+        if (visibility == View.GONE) {
+            ObjectAnimator anim = ObjectAnimator.ofFloat(mToolbar, View.ALPHA, 1, 0);
+            anim.setDuration(duration);
+            anim.addListener(new AnimatorListenerAdapter() {
+                        private boolean mCanceled = false;
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            if (!mCanceled) {
+                                mToolbar.setVisibility(View.GONE);
+                            }
+                        }
+
+                        @Override
+                        public void onAnimationCancel(Animator animation) {
+                            mCanceled = true;
+                        }
+                    });
+            return anim;
+        } else if (visibility == View.VISIBLE) {
+            ObjectAnimator anim = ObjectAnimator.ofFloat(mToolbar, View.ALPHA, 0, 1);
+            anim.setDuration(duration);
+            anim.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationStart(Animator animation) {
+                            mToolbar.setVisibility(View.VISIBLE);
+                        }
+                    });
+            return anim;
+        }
+        return null;
+    }
+
+    @Override
+    public void setNavigationIcon(Drawable icon) {
+        mNavIcon = icon;
+        updateNavigationIcon();
+    }
+
+    @Override
+    public void setNavigationIcon(int resId) {
+        setNavigationIcon(resId != 0 ? mToolbar.getContext().getDrawable(resId) : null);
+    }
+
+    @Override
+    public void setDefaultNavigationIcon(Drawable defaultNavigationIcon) {
+        if (mDefaultNavigationIcon != defaultNavigationIcon) {
+            mDefaultNavigationIcon = defaultNavigationIcon;
+            updateNavigationIcon();
+        }
+    }
+
+    private void updateNavigationIcon() {
+        if ((mDisplayOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+            mToolbar.setNavigationIcon(mNavIcon != null ? mNavIcon : mDefaultNavigationIcon);
+        } else {
+            mToolbar.setNavigationIcon(null);
+        }
+    }
+
+    @Override
+    public void setNavigationContentDescription(CharSequence description) {
+        mHomeDescription = description;
+        updateHomeAccessibility();
+    }
+
+    @Override
+    public void setNavigationContentDescription(int resId) {
+        setNavigationContentDescription(resId == 0 ? null : getContext().getString(resId));
+    }
+
+    private void updateHomeAccessibility() {
+        if ((mDisplayOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+            if (TextUtils.isEmpty(mHomeDescription)) {
+                mToolbar.setNavigationContentDescription(mDefaultNavigationContentDescription);
+            } else {
+                mToolbar.setNavigationContentDescription(mHomeDescription);
+            }
+        }
+    }
+
+    @Override
+    public void saveHierarchyState(SparseArray<Parcelable> toolbarStates) {
+        mToolbar.saveHierarchyState(toolbarStates);
+    }
+
+    @Override
+    public void restoreHierarchyState(SparseArray<Parcelable> toolbarStates) {
+        mToolbar.restoreHierarchyState(toolbarStates);
+    }
+
+    @Override
+    public void setBackgroundDrawable(Drawable d) {
+        //noinspection deprecation
+        mToolbar.setBackgroundDrawable(d);
+    }
+
+    @Override
+    public int getHeight() {
+        return mToolbar.getHeight();
+    }
+
+    @Override
+    public void setVisibility(int visible) {
+        mToolbar.setVisibility(visible);
+    }
+
+    @Override
+    public int getVisibility() {
+        return mToolbar.getVisibility();
+    }
+
+    @Override
+    public void setMenuCallbacks(MenuPresenter.Callback presenterCallback,
+            MenuBuilder.Callback menuBuilderCallback) {
+        mToolbar.setMenuCallbacks(presenterCallback, menuBuilderCallback);
+    }
+
+    @Override
+    public Menu getMenu() {
+        return mToolbar.getMenu();
+    }
+
+}
diff --git a/com/android/internal/widget/VerifyCredentialResponse.java b/com/android/internal/widget/VerifyCredentialResponse.java
new file mode 100644
index 0000000..ad6020c
--- /dev/null
+++ b/com/android/internal/widget/VerifyCredentialResponse.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.gatekeeper.GateKeeperResponse;
+import android.util.Slog;
+
+/**
+ * Response object for a ILockSettings credential verification request.
+ * @hide
+ */
+public final class VerifyCredentialResponse implements Parcelable {
+
+    public static final int RESPONSE_ERROR = -1;
+    public static final int RESPONSE_OK = 0;
+    public static final int RESPONSE_RETRY = 1;
+
+    public static final VerifyCredentialResponse OK = new VerifyCredentialResponse();
+    public static final VerifyCredentialResponse ERROR
+            = new VerifyCredentialResponse(RESPONSE_ERROR, 0, null);
+    private static final String TAG = "VerifyCredentialResponse";
+
+    private int mResponseCode;
+    private byte[] mPayload;
+    private int mTimeout;
+
+    public static final Parcelable.Creator<VerifyCredentialResponse> CREATOR
+            = new Parcelable.Creator<VerifyCredentialResponse>() {
+        @Override
+        public VerifyCredentialResponse createFromParcel(Parcel source) {
+            int responseCode = source.readInt();
+            VerifyCredentialResponse response = new VerifyCredentialResponse(responseCode, 0, null);
+            if (responseCode == RESPONSE_RETRY) {
+                response.setTimeout(source.readInt());
+            } else if (responseCode == RESPONSE_OK) {
+                int size = source.readInt();
+                if (size > 0) {
+                    byte[] payload = new byte[size];
+                    source.readByteArray(payload);
+                    response.setPayload(payload);
+                }
+            }
+            return response;
+        }
+
+        @Override
+        public VerifyCredentialResponse[] newArray(int size) {
+            return new VerifyCredentialResponse[size];
+        }
+
+    };
+
+    public VerifyCredentialResponse() {
+        mResponseCode = RESPONSE_OK;
+        mPayload = null;
+    }
+
+
+    public VerifyCredentialResponse(byte[] payload) {
+        mPayload = payload;
+        mResponseCode = RESPONSE_OK;
+    }
+
+    public VerifyCredentialResponse(int timeout) {
+        mTimeout = timeout;
+        mResponseCode = RESPONSE_RETRY;
+        mPayload = null;
+    }
+
+    private VerifyCredentialResponse(int responseCode, int timeout, byte[] payload) {
+        mResponseCode = responseCode;
+        mTimeout = timeout;
+        mPayload = payload;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mResponseCode);
+        if (mResponseCode == RESPONSE_RETRY) {
+            dest.writeInt(mTimeout);
+        } else if (mResponseCode == RESPONSE_OK) {
+            if (mPayload != null) {
+                dest.writeInt(mPayload.length);
+                dest.writeByteArray(mPayload);
+            }
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public byte[] getPayload() {
+        return mPayload;
+    }
+
+    public int getTimeout() {
+        return mTimeout;
+    }
+
+    public int getResponseCode() {
+        return mResponseCode;
+    }
+
+    private void setTimeout(int timeout) {
+        mTimeout = timeout;
+    }
+
+    private void setPayload(byte[] payload) {
+        mPayload = payload;
+    }
+
+    public VerifyCredentialResponse stripPayload() {
+        return new VerifyCredentialResponse(mResponseCode, mTimeout, new byte[0]);
+    }
+
+    public static VerifyCredentialResponse fromGateKeeperResponse(
+            GateKeeperResponse gateKeeperResponse) {
+        VerifyCredentialResponse response;
+        int responseCode = gateKeeperResponse.getResponseCode();
+        if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+            response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
+        } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+            byte[] token = gateKeeperResponse.getPayload();
+            if (token == null) {
+                // something's wrong if there's no payload with a challenge
+                Slog.e(TAG, "verifyChallenge response had no associated payload");
+                response = VerifyCredentialResponse.ERROR;
+            } else {
+                response = new VerifyCredentialResponse(token);
+            }
+        } else {
+            response = VerifyCredentialResponse.ERROR;
+        }
+        return response;
+    }
+}
diff --git a/com/android/internal/widget/ViewInfoStore.java b/com/android/internal/widget/ViewInfoStore.java
new file mode 100644
index 0000000..6784a85
--- /dev/null
+++ b/com/android/internal/widget/ViewInfoStore.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.LongSparseArray;
+import android.util.Pools;
+
+import static com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
+import static com.android.internal.widget.RecyclerView.ViewHolder;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_POST;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * This class abstracts all tracking for Views to run animations.
+ */
+class ViewInfoStore {
+
+    private static final boolean DEBUG = false;
+
+    /**
+     * View data records for pre-layout
+     */
+    @VisibleForTesting
+    final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
+
+    @VisibleForTesting
+    final LongSparseArray<ViewHolder> mOldChangedHolders = new LongSparseArray<>();
+
+    /**
+     * Clears the state and all existing tracking data
+     */
+    void clear() {
+        mLayoutHolderMap.clear();
+        mOldChangedHolders.clear();
+    }
+
+    /**
+     * Adds the item information to the prelayout tracking
+     * @param holder The ViewHolder whose information is being saved
+     * @param info The information to save
+     */
+    void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
+        InfoRecord record = mLayoutHolderMap.get(holder);
+        if (record == null) {
+            record = InfoRecord.obtain();
+            mLayoutHolderMap.put(holder, record);
+        }
+        record.preInfo = info;
+        record.flags |= FLAG_PRE;
+    }
+
+    boolean isDisappearing(ViewHolder holder) {
+        final InfoRecord record = mLayoutHolderMap.get(holder);
+        return record != null && ((record.flags & FLAG_DISAPPEARED) != 0);
+    }
+
+    /**
+     * Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it.
+     *
+     * @param vh The ViewHolder whose information is being queried
+     * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
+     */
+    @Nullable
+    ItemHolderInfo popFromPreLayout(ViewHolder vh) {
+        return popFromLayoutStep(vh, FLAG_PRE);
+    }
+
+    /**
+     * Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it.
+     *
+     * @param vh The ViewHolder whose information is being queried
+     * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
+     */
+    @Nullable
+    ItemHolderInfo popFromPostLayout(ViewHolder vh) {
+        return popFromLayoutStep(vh, FLAG_POST);
+    }
+
+    private ItemHolderInfo popFromLayoutStep(ViewHolder vh, int flag) {
+        int index = mLayoutHolderMap.indexOfKey(vh);
+        if (index < 0) {
+            return null;
+        }
+        final InfoRecord record = mLayoutHolderMap.valueAt(index);
+        if (record != null && (record.flags & flag) != 0) {
+            record.flags &= ~flag;
+            final ItemHolderInfo info;
+            if (flag == FLAG_PRE) {
+                info = record.preInfo;
+            } else if (flag == FLAG_POST) {
+                info = record.postInfo;
+            } else {
+                throw new IllegalArgumentException("Must provide flag PRE or POST");
+            }
+            // if not pre-post flag is left, clear.
+            if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) {
+                mLayoutHolderMap.removeAt(index);
+                InfoRecord.recycle(record);
+            }
+            return info;
+        }
+        return null;
+    }
+
+    /**
+     * Adds the given ViewHolder to the oldChangeHolders list
+     * @param key The key to identify the ViewHolder.
+     * @param holder The ViewHolder to store
+     */
+    void addToOldChangeHolders(long key, ViewHolder holder) {
+        mOldChangedHolders.put(key, holder);
+    }
+
+    /**
+     * Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the
+     * LayoutManager during a pre-layout pass. We distinguish them from other views that were
+     * already in the pre-layout so that ItemAnimator can choose to run a different animation for
+     * them.
+     *
+     * @param holder The ViewHolder to store
+     * @param info The information to save
+     */
+    void addToAppearedInPreLayoutHolders(ViewHolder holder, ItemHolderInfo info) {
+        InfoRecord record = mLayoutHolderMap.get(holder);
+        if (record == null) {
+            record = InfoRecord.obtain();
+            mLayoutHolderMap.put(holder, record);
+        }
+        record.flags |= FLAG_APPEAR;
+        record.preInfo = info;
+    }
+
+    /**
+     * Checks whether the given ViewHolder is in preLayout list
+     * @param viewHolder The ViewHolder to query
+     *
+     * @return True if the ViewHolder is present in preLayout, false otherwise
+     */
+    boolean isInPreLayout(ViewHolder viewHolder) {
+        final InfoRecord record = mLayoutHolderMap.get(viewHolder);
+        return record != null && (record.flags & FLAG_PRE) != 0;
+    }
+
+    /**
+     * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns
+     * null.
+     * @param key The key to be used to find the ViewHolder.
+     *
+     * @return A ViewHolder if exists or null if it does not exist.
+     */
+    ViewHolder getFromOldChangeHolders(long key) {
+        return mOldChangedHolders.get(key);
+    }
+
+    /**
+     * Adds the item information to the post layout list
+     * @param holder The ViewHolder whose information is being saved
+     * @param info The information to save
+     */
+    void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
+        InfoRecord record = mLayoutHolderMap.get(holder);
+        if (record == null) {
+            record = InfoRecord.obtain();
+            mLayoutHolderMap.put(holder, record);
+        }
+        record.postInfo = info;
+        record.flags |= FLAG_POST;
+    }
+
+    /**
+     * A ViewHolder might be added by the LayoutManager just to animate its disappearance.
+     * This list holds such items so that we can animate / recycle these ViewHolders properly.
+     *
+     * @param holder The ViewHolder which disappeared during a layout.
+     */
+    void addToDisappearedInLayout(ViewHolder holder) {
+        InfoRecord record = mLayoutHolderMap.get(holder);
+        if (record == null) {
+            record = InfoRecord.obtain();
+            mLayoutHolderMap.put(holder, record);
+        }
+        record.flags |= FLAG_DISAPPEARED;
+    }
+
+    /**
+     * Removes a ViewHolder from disappearing list.
+     * @param holder The ViewHolder to be removed from the disappearing list.
+     */
+    void removeFromDisappearedInLayout(ViewHolder holder) {
+        InfoRecord record = mLayoutHolderMap.get(holder);
+        if (record == null) {
+            return;
+        }
+        record.flags &= ~FLAG_DISAPPEARED;
+    }
+
+    void process(ProcessCallback callback) {
+        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
+            final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
+            final InfoRecord record = mLayoutHolderMap.removeAt(index);
+            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
+                // Appeared then disappeared. Not useful for animations.
+                callback.unused(viewHolder);
+            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
+                // Set as "disappeared" by the LayoutManager (addDisappearingView)
+                if (record.preInfo == null) {
+                    // similar to appear disappear but happened between different layout passes.
+                    // this can happen when the layout manager is using auto-measure
+                    callback.unused(viewHolder);
+                } else {
+                    callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
+                }
+            } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
+                // Appeared in the layout but not in the adapter (e.g. entered the viewport)
+                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
+            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
+                // Persistent in both passes. Animate persistence
+                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
+            } else if ((record.flags & FLAG_PRE) != 0) {
+                // Was in pre-layout, never been added to post layout
+                callback.processDisappeared(viewHolder, record.preInfo, null);
+            } else if ((record.flags & FLAG_POST) != 0) {
+                // Was not in pre-layout, been added to post layout
+                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
+            } else if ((record.flags & FLAG_APPEAR) != 0) {
+                // Scrap view. RecyclerView will handle removing/recycling this.
+            } else if (DEBUG) {
+                throw new IllegalStateException("record without any reasonable flag combination:/");
+            }
+            InfoRecord.recycle(record);
+        }
+    }
+
+    /**
+     * Removes the ViewHolder from all list
+     * @param holder The ViewHolder which we should stop tracking
+     */
+    void removeViewHolder(ViewHolder holder) {
+        for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) {
+            if (holder == mOldChangedHolders.valueAt(i)) {
+                mOldChangedHolders.removeAt(i);
+                break;
+            }
+        }
+        final InfoRecord info = mLayoutHolderMap.remove(holder);
+        if (info != null) {
+            InfoRecord.recycle(info);
+        }
+    }
+
+    void onDetach() {
+        InfoRecord.drainCache();
+    }
+
+    public void onViewDetached(ViewHolder viewHolder) {
+        removeFromDisappearedInLayout(viewHolder);
+    }
+
+    interface ProcessCallback {
+        void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo,
+                @Nullable ItemHolderInfo postInfo);
+        void processAppeared(ViewHolder viewHolder, @Nullable ItemHolderInfo preInfo,
+                ItemHolderInfo postInfo);
+        void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo,
+                @NonNull ItemHolderInfo postInfo);
+        void unused(ViewHolder holder);
+    }
+
+    static class InfoRecord {
+        // disappearing list
+        static final int FLAG_DISAPPEARED = 1;
+        // appear in pre layout list
+        static final int FLAG_APPEAR = 1 << 1;
+        // pre layout, this is necessary to distinguish null item info
+        static final int FLAG_PRE = 1 << 2;
+        // post layout, this is necessary to distinguish null item info
+        static final int FLAG_POST = 1 << 3;
+        static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
+        static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
+        static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
+        int flags;
+        @Nullable ItemHolderInfo preInfo;
+        @Nullable ItemHolderInfo postInfo;
+        static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
+
+        private InfoRecord() {
+        }
+
+        static InfoRecord obtain() {
+            InfoRecord record = sPool.acquire();
+            return record == null ? new InfoRecord() : record;
+        }
+
+        static void recycle(InfoRecord record) {
+            record.flags = 0;
+            record.preInfo = null;
+            record.postInfo = null;
+            sPool.release(record);
+        }
+
+        static void drainCache() {
+            //noinspection StatementWithEmptyBody
+            while (sPool.acquire() != null);
+        }
+    }
+}
diff --git a/com/android/internal/widget/ViewPager.java b/com/android/internal/widget/ViewPager.java
new file mode 100644
index 0000000..d5b6def
--- /dev/null
+++ b/com/android/internal/widget/ViewPager.java
@@ -0,0 +1,2827 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.FocusFinder;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.Interpolator;
+import android.widget.EdgeEffect;
+import android.widget.Scroller;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Framework copy of the support-v4 ViewPager class.
+ */
+public class ViewPager extends ViewGroup {
+    private static final String TAG = "ViewPager";
+    private static final boolean DEBUG = false;
+
+    private static final int MAX_SCROLL_X = 2 << 23;
+    private static final boolean USE_CACHE = false;
+
+    private static final int DEFAULT_OFFSCREEN_PAGES = 1;
+    private static final int MAX_SETTLE_DURATION = 600; // ms
+    private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
+
+    private static final int DEFAULT_GUTTER_SIZE = 16; // dips
+
+    private static final int MIN_FLING_VELOCITY = 400; // dips
+
+    private static final int[] LAYOUT_ATTRS = new int[] {
+        com.android.internal.R.attr.layout_gravity
+    };
+
+    /**
+     * Used to track what the expected number of items in the adapter should be.
+     * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
+     */
+    private int mExpectedAdapterCount;
+
+    static class ItemInfo {
+        Object object;
+        boolean scrolling;
+        float widthFactor;
+
+        /** Logical position of the item within the pager adapter. */
+        int position;
+
+        /** Offset between the starting edges of the item and its container. */
+        float offset;
+    }
+
+    private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
+        @Override
+        public int compare(ItemInfo lhs, ItemInfo rhs) {
+            return lhs.position - rhs.position;
+        }
+    };
+
+    private static final Interpolator sInterpolator = new Interpolator() {
+        public float getInterpolation(float t) {
+            t -= 1.0f;
+            return t * t * t * t * t + 1.0f;
+        }
+    };
+
+    private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
+    private final ItemInfo mTempItem = new ItemInfo();
+
+    private final Rect mTempRect = new Rect();
+
+    private PagerAdapter mAdapter;
+    private int mCurItem;   // Index of currently displayed page.
+    private int mRestoredCurItem = -1;
+    private Parcelable mRestoredAdapterState = null;
+    private ClassLoader mRestoredClassLoader = null;
+    private final Scroller mScroller;
+    private PagerObserver mObserver;
+
+    private int mPageMargin;
+    private Drawable mMarginDrawable;
+    private int mTopPageBounds;
+    private int mBottomPageBounds;
+
+    /**
+     * The increment used to move in the "left" direction. Dependent on layout
+     * direction.
+     */
+    private int mLeftIncr = -1;
+
+    // Offsets of the first and last items, if known.
+    // Set during population, used to determine if we are at the beginning
+    // or end of the pager data set during touch scrolling.
+    private float mFirstOffset = -Float.MAX_VALUE;
+    private float mLastOffset = Float.MAX_VALUE;
+
+    private int mChildWidthMeasureSpec;
+    private int mChildHeightMeasureSpec;
+    private boolean mInLayout;
+
+    private boolean mScrollingCacheEnabled;
+
+    private boolean mPopulatePending;
+    private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
+
+    private boolean mIsBeingDragged;
+    private boolean mIsUnableToDrag;
+    private final int mDefaultGutterSize;
+    private int mGutterSize;
+    private final int mTouchSlop;
+    /**
+     * Position of the last motion event.
+     */
+    private float mLastMotionX;
+    private float mLastMotionY;
+    private float mInitialMotionX;
+    private float mInitialMotionY;
+    /**
+     * ID of the active pointer. This is used to retain consistency during
+     * drags/flings if multiple pointers are used.
+     */
+    private int mActivePointerId = INVALID_POINTER;
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    /**
+     * Determines speed during touch scrolling
+     */
+    private VelocityTracker mVelocityTracker;
+    private final int mMinimumVelocity;
+    private final int mMaximumVelocity;
+    private final int mFlingDistance;
+    private final int mCloseEnough;
+
+    // If the pager is at least this close to its final position, complete the scroll
+    // on touch down and let the user interact with the content inside instead of
+    // "catching" the flinging pager.
+    private static final int CLOSE_ENOUGH = 2; // dp
+
+    private final EdgeEffect mLeftEdge;
+    private final EdgeEffect mRightEdge;
+
+    private boolean mFirstLayout = true;
+    private boolean mCalledSuper;
+    private int mDecorChildCount;
+
+    private OnPageChangeListener mOnPageChangeListener;
+    private OnPageChangeListener mInternalPageChangeListener;
+    private OnAdapterChangeListener mAdapterChangeListener;
+    private PageTransformer mPageTransformer;
+
+    private static final int DRAW_ORDER_DEFAULT = 0;
+    private static final int DRAW_ORDER_FORWARD = 1;
+    private static final int DRAW_ORDER_REVERSE = 2;
+    private int mDrawingOrder;
+    private ArrayList<View> mDrawingOrderedChildren;
+    private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
+
+    /**
+     * Indicates that the pager is in an idle, settled state. The current page
+     * is fully in view and no animation is in progress.
+     */
+    public static final int SCROLL_STATE_IDLE = 0;
+
+    /**
+     * Indicates that the pager is currently being dragged by the user.
+     */
+    public static final int SCROLL_STATE_DRAGGING = 1;
+
+    /**
+     * Indicates that the pager is in the process of settling to a final position.
+     */
+    public static final int SCROLL_STATE_SETTLING = 2;
+
+    private final Runnable mEndScrollRunnable = new Runnable() {
+        public void run() {
+            setScrollState(SCROLL_STATE_IDLE);
+            populate();
+        }
+    };
+
+    private int mScrollState = SCROLL_STATE_IDLE;
+
+    /**
+     * Callback interface for responding to changing state of the selected page.
+     */
+    public interface OnPageChangeListener {
+
+        /**
+         * This method will be invoked when the current page is scrolled, either as part
+         * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+         *
+         * @param position Position index of the first page currently being displayed.
+         *                 Page position+1 will be visible if positionOffset is nonzero.
+         * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
+         * @param positionOffsetPixels Value in pixels indicating the offset from position.
+         */
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
+
+        /**
+         * This method will be invoked when a new page becomes selected. Animation is not
+         * necessarily complete.
+         *
+         * @param position Position index of the new selected page.
+         */
+        public void onPageSelected(int position);
+
+        /**
+         * Called when the scroll state changes. Useful for discovering when the user
+         * begins dragging, when the pager is automatically settling to the current page,
+         * or when it is fully stopped/idle.
+         *
+         * @param state The new scroll state.
+         * @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE
+         * @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING
+         * @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING
+         */
+        public void onPageScrollStateChanged(int state);
+    }
+
+    /**
+     * Simple implementation of the {@link OnPageChangeListener} interface with stub
+     * implementations of each method. Extend this if you do not intend to override
+     * every method of {@link OnPageChangeListener}.
+     */
+    public static class SimpleOnPageChangeListener implements OnPageChangeListener {
+        @Override
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+            // This space for rent
+        }
+
+        @Override
+        public void onPageSelected(int position) {
+            // This space for rent
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {
+            // This space for rent
+        }
+    }
+
+    /**
+     * A PageTransformer is invoked whenever a visible/attached page is scrolled.
+     * This offers an opportunity for the application to apply a custom transformation
+     * to the page views using animation properties.
+     *
+     * <p>As property animation is only supported as of Android 3.0 and forward,
+     * setting a PageTransformer on a ViewPager on earlier platform versions will
+     * be ignored.</p>
+     */
+    public interface PageTransformer {
+        /**
+         * Apply a property transformation to the given page.
+         *
+         * @param page Apply the transformation to this page
+         * @param position Position of page relative to the current front-and-center
+         *                 position of the pager. 0 is front and center. 1 is one full
+         *                 page position to the right, and -1 is one page position to the left.
+         */
+        public void transformPage(View page, float position);
+    }
+
+    /**
+     * Used internally to monitor when adapters are switched.
+     */
+    interface OnAdapterChangeListener {
+        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
+    }
+
+    /**
+     * Used internally to tag special types of child views that should be added as
+     * pager decorations by default.
+     */
+    interface Decor {}
+
+    public ViewPager(Context context) {
+        this(context, null);
+    }
+
+    public ViewPager(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        setWillNotDraw(false);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setFocusable(true);
+
+        mScroller = new Scroller(context, sInterpolator);
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        final float density = context.getResources().getDisplayMetrics().density;
+
+        mTouchSlop = configuration.getScaledPagingTouchSlop();
+        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mLeftEdge = new EdgeEffect(context);
+        mRightEdge = new EdgeEffect(context);
+
+        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
+        mCloseEnough = (int) (CLOSE_ENOUGH * density);
+        mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
+
+        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        removeCallbacks(mEndScrollRunnable);
+        super.onDetachedFromWindow();
+    }
+
+    private void setScrollState(int newState) {
+        if (mScrollState == newState) {
+            return;
+        }
+
+        mScrollState = newState;
+        if (mPageTransformer != null) {
+            // PageTransformers can do complex things that benefit from hardware layers.
+            enableLayers(newState != SCROLL_STATE_IDLE);
+        }
+        if (mOnPageChangeListener != null) {
+            mOnPageChangeListener.onPageScrollStateChanged(newState);
+        }
+    }
+
+    /**
+     * Set a PagerAdapter that will supply views for this pager as needed.
+     *
+     * @param adapter Adapter to use
+     */
+    public void setAdapter(PagerAdapter adapter) {
+        if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(mObserver);
+            mAdapter.startUpdate(this);
+            for (int i = 0; i < mItems.size(); i++) {
+                final ItemInfo ii = mItems.get(i);
+                mAdapter.destroyItem(this, ii.position, ii.object);
+            }
+            mAdapter.finishUpdate(this);
+            mItems.clear();
+            removeNonDecorViews();
+            mCurItem = 0;
+            scrollTo(0, 0);
+        }
+
+        final PagerAdapter oldAdapter = mAdapter;
+        mAdapter = adapter;
+        mExpectedAdapterCount = 0;
+
+        if (mAdapter != null) {
+            if (mObserver == null) {
+                mObserver = new PagerObserver();
+            }
+            mAdapter.registerDataSetObserver(mObserver);
+            mPopulatePending = false;
+            final boolean wasFirstLayout = mFirstLayout;
+            mFirstLayout = true;
+            mExpectedAdapterCount = mAdapter.getCount();
+            if (mRestoredCurItem >= 0) {
+                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
+                setCurrentItemInternal(mRestoredCurItem, false, true);
+                mRestoredCurItem = -1;
+                mRestoredAdapterState = null;
+                mRestoredClassLoader = null;
+            } else if (!wasFirstLayout) {
+                populate();
+            } else {
+                requestLayout();
+            }
+        }
+
+        if (mAdapterChangeListener != null && oldAdapter != adapter) {
+            mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
+        }
+    }
+
+    private void removeNonDecorViews() {
+        for (int i = 0; i < getChildCount(); i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (!lp.isDecor) {
+                removeViewAt(i);
+                i--;
+            }
+        }
+    }
+
+    /**
+     * Retrieve the current adapter supplying pages.
+     *
+     * @return The currently registered PagerAdapter
+     */
+    public PagerAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
+        mAdapterChangeListener = listener;
+    }
+
+    private int getPaddedWidth() {
+        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+    }
+
+    /**
+     * Set the currently selected page. If the ViewPager has already been through its first
+     * layout with its current adapter there will be a smooth animated transition between
+     * the current item and the specified item.
+     *
+     * @param item Item index to select
+     */
+    public void setCurrentItem(int item) {
+        mPopulatePending = false;
+        setCurrentItemInternal(item, !mFirstLayout, false);
+    }
+
+    /**
+     * Set the currently selected page.
+     *
+     * @param item Item index to select
+     * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
+     */
+    public void setCurrentItem(int item, boolean smoothScroll) {
+        mPopulatePending = false;
+        setCurrentItemInternal(item, smoothScroll, false);
+    }
+
+    public int getCurrentItem() {
+        return mCurItem;
+    }
+
+    boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
+        return setCurrentItemInternal(item, smoothScroll, always, 0);
+    }
+
+    boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
+        if (mAdapter == null || mAdapter.getCount() <= 0) {
+            setScrollingCacheEnabled(false);
+            return false;
+        }
+
+        item = MathUtils.constrain(item, 0, mAdapter.getCount() - 1);
+        if (!always && mCurItem == item && mItems.size() != 0) {
+            setScrollingCacheEnabled(false);
+            return false;
+        }
+
+        final int pageLimit = mOffscreenPageLimit;
+        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
+            // We are doing a jump by more than one page.  To avoid
+            // glitches, we want to keep all current pages in the view
+            // until the scroll ends.
+            for (int i = 0; i < mItems.size(); i++) {
+                mItems.get(i).scrolling = true;
+            }
+        }
+
+        final boolean dispatchSelected = mCurItem != item;
+        if (mFirstLayout) {
+            // We don't have any idea how big we are yet and shouldn't have any pages either.
+            // Just set things up and let the pending layout handle things.
+            mCurItem = item;
+            if (dispatchSelected && mOnPageChangeListener != null) {
+                mOnPageChangeListener.onPageSelected(item);
+            }
+            if (dispatchSelected && mInternalPageChangeListener != null) {
+                mInternalPageChangeListener.onPageSelected(item);
+            }
+            requestLayout();
+        } else {
+            populate(item);
+            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
+        }
+
+        return true;
+    }
+
+    private void scrollToItem(int position, boolean smoothScroll, int velocity,
+            boolean dispatchSelected) {
+        final int destX = getLeftEdgeForItem(position);
+
+        if (smoothScroll) {
+            smoothScrollTo(destX, 0, velocity);
+
+            if (dispatchSelected && mOnPageChangeListener != null) {
+                mOnPageChangeListener.onPageSelected(position);
+            }
+            if (dispatchSelected && mInternalPageChangeListener != null) {
+                mInternalPageChangeListener.onPageSelected(position);
+            }
+        } else {
+            if (dispatchSelected && mOnPageChangeListener != null) {
+                mOnPageChangeListener.onPageSelected(position);
+            }
+            if (dispatchSelected && mInternalPageChangeListener != null) {
+                mInternalPageChangeListener.onPageSelected(position);
+            }
+
+            completeScroll(false);
+            scrollTo(destX, 0);
+            pageScrolled(destX);
+        }
+    }
+
+    private int getLeftEdgeForItem(int position) {
+        final ItemInfo info = infoForPosition(position);
+        if (info == null) {
+            return 0;
+        }
+
+        final int width = getPaddedWidth();
+        final int scaledOffset = (int) (width * MathUtils.constrain(
+                info.offset, mFirstOffset, mLastOffset));
+
+        if (isLayoutRtl()) {
+            final int itemWidth = (int) (width * info.widthFactor + 0.5f);
+            return MAX_SCROLL_X - itemWidth - scaledOffset;
+        } else {
+            return scaledOffset;
+        }
+    }
+
+    /**
+     * Set a listener that will be invoked whenever the page changes or is incrementally
+     * scrolled. See {@link OnPageChangeListener}.
+     *
+     * @param listener Listener to set
+     */
+    public void setOnPageChangeListener(OnPageChangeListener listener) {
+        mOnPageChangeListener = listener;
+    }
+
+    /**
+     * Set a {@link PageTransformer} that will be called for each attached page whenever
+     * the scroll position is changed. This allows the application to apply custom property
+     * transformations to each page, overriding the default sliding look and feel.
+     *
+     * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
+     * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
+     *
+     * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
+     *                            to be drawn from last to first instead of first to last.
+     * @param transformer PageTransformer that will modify each page's animation properties
+     */
+    public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
+        final boolean hasTransformer = transformer != null;
+        final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
+        mPageTransformer = transformer;
+        setChildrenDrawingOrderEnabled(hasTransformer);
+        if (hasTransformer) {
+            mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
+        } else {
+            mDrawingOrder = DRAW_ORDER_DEFAULT;
+        }
+        if (needsPopulate) populate();
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
+        final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
+        return result;
+    }
+
+    /**
+     * Set a separate OnPageChangeListener for internal use by the support library.
+     *
+     * @param listener Listener to set
+     * @return The old listener that was set, if any.
+     */
+    OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
+        OnPageChangeListener oldListener = mInternalPageChangeListener;
+        mInternalPageChangeListener = listener;
+        return oldListener;
+    }
+
+    /**
+     * Returns the number of pages that will be retained to either side of the
+     * current page in the view hierarchy in an idle state. Defaults to 1.
+     *
+     * @return How many pages will be kept offscreen on either side
+     * @see #setOffscreenPageLimit(int)
+     */
+    public int getOffscreenPageLimit() {
+        return mOffscreenPageLimit;
+    }
+
+    /**
+     * Set the number of pages that should be retained to either side of the
+     * current page in the view hierarchy in an idle state. Pages beyond this
+     * limit will be recreated from the adapter when needed.
+     *
+     * <p>This is offered as an optimization. If you know in advance the number
+     * of pages you will need to support or have lazy-loading mechanisms in place
+     * on your pages, tweaking this setting can have benefits in perceived smoothness
+     * of paging animations and interaction. If you have a small number of pages (3-4)
+     * that you can keep active all at once, less time will be spent in layout for
+     * newly created view subtrees as the user pages back and forth.</p>
+     *
+     * <p>You should keep this limit low, especially if your pages have complex layouts.
+     * This setting defaults to 1.</p>
+     *
+     * @param limit How many pages will be kept offscreen in an idle state.
+     */
+    public void setOffscreenPageLimit(int limit) {
+        if (limit < DEFAULT_OFFSCREEN_PAGES) {
+            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
+                    DEFAULT_OFFSCREEN_PAGES);
+            limit = DEFAULT_OFFSCREEN_PAGES;
+        }
+        if (limit != mOffscreenPageLimit) {
+            mOffscreenPageLimit = limit;
+            populate();
+        }
+    }
+
+    /**
+     * Set the margin between pages.
+     *
+     * @param marginPixels Distance between adjacent pages in pixels
+     * @see #getPageMargin()
+     * @see #setPageMarginDrawable(android.graphics.drawable.Drawable)
+     * @see #setPageMarginDrawable(int)
+     */
+    public void setPageMargin(int marginPixels) {
+        final int oldMargin = mPageMargin;
+        mPageMargin = marginPixels;
+
+        final int width = getWidth();
+        recomputeScrollPosition(width, width, marginPixels, oldMargin);
+
+        requestLayout();
+    }
+
+    /**
+     * Return the margin between pages.
+     *
+     * @return The size of the margin in pixels
+     */
+    public int getPageMargin() {
+        return mPageMargin;
+    }
+
+    /**
+     * Set a drawable that will be used to fill the margin between pages.
+     *
+     * @param d Drawable to display between pages
+     */
+    public void setPageMarginDrawable(Drawable d) {
+        mMarginDrawable = d;
+        if (d != null) refreshDrawableState();
+        setWillNotDraw(d == null);
+        invalidate();
+    }
+
+    /**
+     * Set a drawable that will be used to fill the margin between pages.
+     *
+     * @param resId Resource ID of a drawable to display between pages
+     */
+    public void setPageMarginDrawable(@DrawableRes int resId) {
+        setPageMarginDrawable(getContext().getDrawable(resId));
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return super.verifyDrawable(who) || who == mMarginDrawable;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        final Drawable marginDrawable = mMarginDrawable;
+        if (marginDrawable != null && marginDrawable.isStateful()
+                && marginDrawable.setState(getDrawableState())) {
+            invalidateDrawable(marginDrawable);
+        }
+    }
+
+    // We want the duration of the page snap animation to be influenced by the distance that
+    // the screen has to travel, however, we don't want this duration to be effected in a
+    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
+    // of travel has on the overall snap duration.
+    float distanceInfluenceForSnapDuration(float f) {
+        f -= 0.5f; // center the values about 0.
+        f *= 0.3f * Math.PI / 2.0f;
+        return (float) Math.sin(f);
+    }
+
+    /**
+     * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param x the number of pixels to scroll by on the X axis
+     * @param y the number of pixels to scroll by on the Y axis
+     */
+    void smoothScrollTo(int x, int y) {
+        smoothScrollTo(x, y, 0);
+    }
+
+    /**
+     * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param x the number of pixels to scroll by on the X axis
+     * @param y the number of pixels to scroll by on the Y axis
+     * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
+     */
+    void smoothScrollTo(int x, int y, int velocity) {
+        if (getChildCount() == 0) {
+            // Nothing to do.
+            setScrollingCacheEnabled(false);
+            return;
+        }
+        int sx = getScrollX();
+        int sy = getScrollY();
+        int dx = x - sx;
+        int dy = y - sy;
+        if (dx == 0 && dy == 0) {
+            completeScroll(false);
+            populate();
+            setScrollState(SCROLL_STATE_IDLE);
+            return;
+        }
+
+        setScrollingCacheEnabled(true);
+        setScrollState(SCROLL_STATE_SETTLING);
+
+        final int width = getPaddedWidth();
+        final int halfWidth = width / 2;
+        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
+        final float distance = halfWidth + halfWidth *
+                distanceInfluenceForSnapDuration(distanceRatio);
+
+        int duration = 0;
+        velocity = Math.abs(velocity);
+        if (velocity > 0) {
+            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+        } else {
+            final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
+            final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
+            duration = (int) ((pageDelta + 1) * 100);
+        }
+        duration = Math.min(duration, MAX_SETTLE_DURATION);
+
+        mScroller.startScroll(sx, sy, dx, dy, duration);
+        postInvalidateOnAnimation();
+    }
+
+    ItemInfo addNewItem(int position, int index) {
+        ItemInfo ii = new ItemInfo();
+        ii.position = position;
+        ii.object = mAdapter.instantiateItem(this, position);
+        ii.widthFactor = mAdapter.getPageWidth(position);
+        if (index < 0 || index >= mItems.size()) {
+            mItems.add(ii);
+        } else {
+            mItems.add(index, ii);
+        }
+        return ii;
+    }
+
+    void dataSetChanged() {
+        // This method only gets called if our observer is attached, so mAdapter is non-null.
+
+        final int adapterCount = mAdapter.getCount();
+        mExpectedAdapterCount = adapterCount;
+        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
+                mItems.size() < adapterCount;
+        int newCurrItem = mCurItem;
+
+        boolean isUpdating = false;
+        for (int i = 0; i < mItems.size(); i++) {
+            final ItemInfo ii = mItems.get(i);
+            final int newPos = mAdapter.getItemPosition(ii.object);
+
+            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
+                continue;
+            }
+
+            if (newPos == PagerAdapter.POSITION_NONE) {
+                mItems.remove(i);
+                i--;
+
+                if (!isUpdating) {
+                    mAdapter.startUpdate(this);
+                    isUpdating = true;
+                }
+
+                mAdapter.destroyItem(this, ii.position, ii.object);
+                needPopulate = true;
+
+                if (mCurItem == ii.position) {
+                    // Keep the current item in the valid range
+                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
+                    needPopulate = true;
+                }
+                continue;
+            }
+
+            if (ii.position != newPos) {
+                if (ii.position == mCurItem) {
+                    // Our current item changed position. Follow it.
+                    newCurrItem = newPos;
+                }
+
+                ii.position = newPos;
+                needPopulate = true;
+            }
+        }
+
+        if (isUpdating) {
+            mAdapter.finishUpdate(this);
+        }
+
+        Collections.sort(mItems, COMPARATOR);
+
+        if (needPopulate) {
+            // Reset our known page widths; populate will recompute them.
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.isDecor) {
+                    lp.widthFactor = 0.f;
+                }
+            }
+
+            setCurrentItemInternal(newCurrItem, false, true);
+            requestLayout();
+        }
+    }
+
+    public void populate() {
+        populate(mCurItem);
+    }
+
+    void populate(int newCurrentItem) {
+        ItemInfo oldCurInfo = null;
+        int focusDirection = View.FOCUS_FORWARD;
+        if (mCurItem != newCurrentItem) {
+            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+            oldCurInfo = infoForPosition(mCurItem);
+            mCurItem = newCurrentItem;
+        }
+
+        if (mAdapter == null) {
+            sortChildDrawingOrder();
+            return;
+        }
+
+        // Bail now if we are waiting to populate.  This is to hold off
+        // on creating views from the time the user releases their finger to
+        // fling to a new position until we have finished the scroll to
+        // that position, avoiding glitches from happening at that point.
+        if (mPopulatePending) {
+            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
+            sortChildDrawingOrder();
+            return;
+        }
+
+        // Also, don't populate until we are attached to a window.  This is to
+        // avoid trying to populate before we have restored our view hierarchy
+        // state and conflicting with what is restored.
+        if (getWindowToken() == null) {
+            return;
+        }
+
+        mAdapter.startUpdate(this);
+
+        final int pageLimit = mOffscreenPageLimit;
+        final int startPos = Math.max(0, mCurItem - pageLimit);
+        final int N = mAdapter.getCount();
+        final int endPos = Math.min(N-1, mCurItem + pageLimit);
+
+        if (N != mExpectedAdapterCount) {
+            String resName;
+            try {
+                resName = getResources().getResourceName(getId());
+            } catch (Resources.NotFoundException e) {
+                resName = Integer.toHexString(getId());
+            }
+            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
+                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
+                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
+                    " Pager id: " + resName +
+                    " Pager class: " + getClass() +
+                    " Problematic adapter: " + mAdapter.getClass());
+        }
+
+        // Locate the currently focused item or add it if needed.
+        int curIndex = -1;
+        ItemInfo curItem = null;
+        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
+            final ItemInfo ii = mItems.get(curIndex);
+            if (ii.position >= mCurItem) {
+                if (ii.position == mCurItem) curItem = ii;
+                break;
+            }
+        }
+
+        if (curItem == null && N > 0) {
+            curItem = addNewItem(mCurItem, curIndex);
+        }
+
+        // Fill 3x the available width or up to the number of offscreen
+        // pages requested to either side, whichever is larger.
+        // If we have no current item we have no work to do.
+        if (curItem != null) {
+            float extraWidthLeft = 0.f;
+            int itemIndex = curIndex - 1;
+            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+            final int clientWidth = getPaddedWidth();
+            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
+                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
+            for (int pos = mCurItem - 1; pos >= 0; pos--) {
+                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
+                    if (ii == null) {
+                        break;
+                    }
+                    if (pos == ii.position && !ii.scrolling) {
+                        mItems.remove(itemIndex);
+                        mAdapter.destroyItem(this, pos, ii.object);
+                        if (DEBUG) {
+                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+                                    " view: " + ii.object);
+                        }
+                        itemIndex--;
+                        curIndex--;
+                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+                    }
+                } else if (ii != null && pos == ii.position) {
+                    extraWidthLeft += ii.widthFactor;
+                    itemIndex--;
+                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+                } else {
+                    ii = addNewItem(pos, itemIndex + 1);
+                    extraWidthLeft += ii.widthFactor;
+                    curIndex++;
+                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+                }
+            }
+
+            float extraWidthRight = curItem.widthFactor;
+            itemIndex = curIndex + 1;
+            if (extraWidthRight < 2.f) {
+                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
+                        (float) getPaddingRight() / (float) clientWidth + 2.f;
+                for (int pos = mCurItem + 1; pos < N; pos++) {
+                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
+                        if (ii == null) {
+                            break;
+                        }
+                        if (pos == ii.position && !ii.scrolling) {
+                            mItems.remove(itemIndex);
+                            mAdapter.destroyItem(this, pos, ii.object);
+                            if (DEBUG) {
+                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+                                        " view: " + ii.object);
+                            }
+                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                        }
+                    } else if (ii != null && pos == ii.position) {
+                        extraWidthRight += ii.widthFactor;
+                        itemIndex++;
+                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                    } else {
+                        ii = addNewItem(pos, itemIndex);
+                        itemIndex++;
+                        extraWidthRight += ii.widthFactor;
+                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                    }
+                }
+            }
+
+            calculatePageOffsets(curItem, curIndex, oldCurInfo);
+        }
+
+        if (DEBUG) {
+            Log.i(TAG, "Current page list:");
+            for (int i=0; i<mItems.size(); i++) {
+                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
+            }
+        }
+
+        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
+
+        mAdapter.finishUpdate(this);
+
+        // Check width measurement of current pages and drawing sort order.
+        // Update LayoutParams as needed.
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            lp.childIndex = i;
+            if (!lp.isDecor && lp.widthFactor == 0.f) {
+                // 0 means requery the adapter for this, it doesn't have a valid width.
+                final ItemInfo ii = infoForChild(child);
+                if (ii != null) {
+                    lp.widthFactor = ii.widthFactor;
+                    lp.position = ii.position;
+                }
+            }
+        }
+        sortChildDrawingOrder();
+
+        if (hasFocus()) {
+            View currentFocused = findFocus();
+            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
+            if (ii == null || ii.position != mCurItem) {
+                for (int i=0; i<getChildCount(); i++) {
+                    View child = getChildAt(i);
+                    ii = infoForChild(child);
+                    if (ii != null && ii.position == mCurItem) {
+                        final Rect focusRect;
+                        if (currentFocused == null) {
+                            focusRect = null;
+                        } else {
+                            focusRect = mTempRect;
+                            currentFocused.getFocusedRect(mTempRect);
+                            offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+                            offsetRectIntoDescendantCoords(child, mTempRect);
+                        }
+                        if (child.requestFocus(focusDirection, focusRect)) {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void sortChildDrawingOrder() {
+        if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
+            if (mDrawingOrderedChildren == null) {
+                mDrawingOrderedChildren = new ArrayList<View>();
+            } else {
+                mDrawingOrderedChildren.clear();
+            }
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                mDrawingOrderedChildren.add(child);
+            }
+            Collections.sort(mDrawingOrderedChildren, sPositionComparator);
+        }
+    }
+
+    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
+        final int N = mAdapter.getCount();
+        final int width = getPaddedWidth();
+        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
+
+        // Fix up offsets for later layout.
+        if (oldCurInfo != null) {
+            final int oldCurPosition = oldCurInfo.position;
+
+            // Base offsets off of oldCurInfo.
+            if (oldCurPosition < curItem.position) {
+                int itemIndex = 0;
+                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
+                for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) {
+                    ItemInfo ii = mItems.get(itemIndex);
+                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
+                        itemIndex++;
+                        ii = mItems.get(itemIndex);
+                    }
+
+                    while (pos < ii.position) {
+                        // We don't have an item populated for this,
+                        // ask the adapter for an offset.
+                        offset += mAdapter.getPageWidth(pos) + marginOffset;
+                        pos++;
+                    }
+
+                    ii.offset = offset;
+                    offset += ii.widthFactor + marginOffset;
+                }
+            } else if (oldCurPosition > curItem.position) {
+                int itemIndex = mItems.size() - 1;
+                float offset = oldCurInfo.offset;
+                for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) {
+                    ItemInfo ii = mItems.get(itemIndex);
+                    while (pos < ii.position && itemIndex > 0) {
+                        itemIndex--;
+                        ii = mItems.get(itemIndex);
+                    }
+
+                    while (pos > ii.position) {
+                        // We don't have an item populated for this,
+                        // ask the adapter for an offset.
+                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
+                        pos--;
+                    }
+
+                    offset -= ii.widthFactor + marginOffset;
+                    ii.offset = offset;
+                }
+            }
+        }
+
+        // Base all offsets off of curItem.
+        final int itemCount = mItems.size();
+        float offset = curItem.offset;
+        int pos = curItem.position - 1;
+        mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
+        mLastOffset = curItem.position == N - 1 ?
+                curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
+
+        // Previous pages
+        for (int i = curIndex - 1; i >= 0; i--, pos--) {
+            final ItemInfo ii = mItems.get(i);
+            while (pos > ii.position) {
+                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
+            }
+            offset -= ii.widthFactor + marginOffset;
+            ii.offset = offset;
+            if (ii.position == 0) mFirstOffset = offset;
+        }
+
+        offset = curItem.offset + curItem.widthFactor + marginOffset;
+        pos = curItem.position + 1;
+
+        // Next pages
+        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
+            final ItemInfo ii = mItems.get(i);
+            while (pos < ii.position) {
+                offset += mAdapter.getPageWidth(pos++) + marginOffset;
+            }
+            if (ii.position == N - 1) {
+                mLastOffset = offset + ii.widthFactor - 1;
+            }
+            ii.offset = offset;
+            offset += ii.widthFactor + marginOffset;
+        }
+    }
+
+    /**
+     * This is the persistent state that is saved by ViewPager.  Only needed
+     * if you are creating a sublass of ViewPager that must save its own
+     * state, in which case it should implement a subclass of this which
+     * contains that state.
+     */
+    public static class SavedState extends BaseSavedState {
+        int position;
+        Parcelable adapterState;
+        ClassLoader loader;
+
+        public SavedState(Parcel source) {
+            super(source);
+        }
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(position);
+            out.writeParcelable(adapterState, flags);
+        }
+
+        @Override
+        public String toString() {
+            return "FragmentPager.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " position=" + position + "}";
+        }
+
+        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+
+        SavedState(Parcel in, ClassLoader loader) {
+            super(in);
+            if (loader == null) {
+                loader = getClass().getClassLoader();
+            }
+            position = in.readInt();
+            adapterState = in.readParcelable(loader);
+            this.loader = loader;
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.position = mCurItem;
+        if (mAdapter != null) {
+            ss.adapterState = mAdapter.saveState();
+        }
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        SavedState ss = (SavedState)state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        if (mAdapter != null) {
+            mAdapter.restoreState(ss.adapterState, ss.loader);
+            setCurrentItemInternal(ss.position, false, true);
+        } else {
+            mRestoredCurItem = ss.position;
+            mRestoredAdapterState = ss.adapterState;
+            mRestoredClassLoader = ss.loader;
+        }
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (!checkLayoutParams(params)) {
+            params = generateLayoutParams(params);
+        }
+        final LayoutParams lp = (LayoutParams) params;
+        lp.isDecor |= child instanceof Decor;
+        if (mInLayout) {
+            if (lp != null && lp.isDecor) {
+                throw new IllegalStateException("Cannot add pager decor view during layout");
+            }
+            lp.needsMeasure = true;
+            addViewInLayout(child, index, params);
+        } else {
+            super.addView(child, index, params);
+        }
+
+        if (USE_CACHE) {
+            if (child.getVisibility() != GONE) {
+                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
+            } else {
+                child.setDrawingCacheEnabled(false);
+            }
+        }
+    }
+
+    public Object getCurrent() {
+        final ItemInfo itemInfo = infoForPosition(getCurrentItem());
+        return itemInfo == null ? null : itemInfo.object;
+    }
+
+    @Override
+    public void removeView(View view) {
+        if (mInLayout) {
+            removeViewInLayout(view);
+        } else {
+            super.removeView(view);
+        }
+    }
+
+    ItemInfo infoForChild(View child) {
+        for (int i=0; i<mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            if (mAdapter.isViewFromObject(child, ii.object)) {
+                return ii;
+            }
+        }
+        return null;
+    }
+
+    ItemInfo infoForAnyChild(View child) {
+        ViewParent parent;
+        while ((parent=child.getParent()) != this) {
+            if (parent == null || !(parent instanceof View)) {
+                return null;
+            }
+            child = (View)parent;
+        }
+        return infoForChild(child);
+    }
+
+    ItemInfo infoForPosition(int position) {
+        for (int i = 0; i < mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            if (ii.position == position) {
+                return ii;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mFirstLayout = true;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // For simple implementation, our internal size is always 0.
+        // We depend on the container to specify the layout size of
+        // our view.  We can't really know what it is since we will be
+        // adding and removing different arbitrary views and do not
+        // want the layout to change as this happens.
+        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
+                getDefaultSize(0, heightMeasureSpec));
+
+        final int measuredWidth = getMeasuredWidth();
+        final int maxGutterSize = measuredWidth / 10;
+        mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
+
+        // Children are just made to fill our space.
+        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
+        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+
+        /*
+         * Make sure all children have been properly measured. Decor views first.
+         * Right now we cheat and make this less complicated by assuming decor
+         * views won't intersect. We will pin to edges based on gravity.
+         */
+        int size = getChildCount();
+        for (int i = 0; i < size; ++i) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp != null && lp.isDecor) {
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+                    int widthMode = MeasureSpec.AT_MOST;
+                    int heightMode = MeasureSpec.AT_MOST;
+                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
+                    boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
+
+                    if (consumeVertical) {
+                        widthMode = MeasureSpec.EXACTLY;
+                    } else if (consumeHorizontal) {
+                        heightMode = MeasureSpec.EXACTLY;
+                    }
+
+                    int widthSize = childWidthSize;
+                    int heightSize = childHeightSize;
+                    if (lp.width != LayoutParams.WRAP_CONTENT) {
+                        widthMode = MeasureSpec.EXACTLY;
+                        if (lp.width != LayoutParams.FILL_PARENT) {
+                            widthSize = lp.width;
+                        }
+                    }
+                    if (lp.height != LayoutParams.WRAP_CONTENT) {
+                        heightMode = MeasureSpec.EXACTLY;
+                        if (lp.height != LayoutParams.FILL_PARENT) {
+                            heightSize = lp.height;
+                        }
+                    }
+                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
+                    child.measure(widthSpec, heightSpec);
+
+                    if (consumeVertical) {
+                        childHeightSize -= child.getMeasuredHeight();
+                    } else if (consumeHorizontal) {
+                        childWidthSize -= child.getMeasuredWidth();
+                    }
+                }
+            }
+        }
+
+        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
+        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
+
+        // Make sure we have created all fragments that we need to have shown.
+        mInLayout = true;
+        populate();
+        mInLayout = false;
+
+        // Page views next.
+        size = getChildCount();
+        for (int i = 0; i < size; ++i) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
+                        + ": " + mChildWidthMeasureSpec);
+
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp == null || !lp.isDecor) {
+                    final int widthSpec = MeasureSpec.makeMeasureSpec(
+                            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
+                    child.measure(widthSpec, mChildHeightMeasureSpec);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        // Make sure scroll position is set correctly.
+        if (w != oldw) {
+            recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
+        }
+    }
+
+    private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
+        if (oldWidth > 0 && !mItems.isEmpty()) {
+            final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
+            final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight()
+                                           + oldMargin;
+            final int xpos = getScrollX();
+            final float pageOffset = (float) xpos / oldWidthWithMargin;
+            final int newOffsetPixels = (int) (pageOffset * widthWithMargin);
+
+            scrollTo(newOffsetPixels, getScrollY());
+            if (!mScroller.isFinished()) {
+                // We now return to your regularly scheduled scroll, already in progress.
+                final int newDuration = mScroller.getDuration() - mScroller.timePassed();
+                ItemInfo targetInfo = infoForPosition(mCurItem);
+                mScroller.startScroll(newOffsetPixels, 0,
+                        (int) (targetInfo.offset * width), 0, newDuration);
+            }
+        } else {
+            final ItemInfo ii = infoForPosition(mCurItem);
+            final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
+            final int scrollPos = (int) (scrollOffset *
+                                         (width - getPaddingLeft() - getPaddingRight()));
+            if (scrollPos != getScrollX()) {
+                completeScroll(false);
+                scrollTo(scrollPos, getScrollY());
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int count = getChildCount();
+        int width = r - l;
+        int height = b - t;
+        int paddingLeft = getPaddingLeft();
+        int paddingTop = getPaddingTop();
+        int paddingRight = getPaddingRight();
+        int paddingBottom = getPaddingBottom();
+        final int scrollX = getScrollX();
+
+        int decorCount = 0;
+
+        // First pass - decor views. We need to do this in two passes so that
+        // we have the proper offsets for non-decor views later.
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                int childLeft = 0;
+                int childTop = 0;
+                if (lp.isDecor) {
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+                    switch (hgrav) {
+                        default:
+                            childLeft = paddingLeft;
+                            break;
+                        case Gravity.LEFT:
+                            childLeft = paddingLeft;
+                            paddingLeft += child.getMeasuredWidth();
+                            break;
+                        case Gravity.CENTER_HORIZONTAL:
+                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+                                    paddingLeft);
+                            break;
+                        case Gravity.RIGHT:
+                            childLeft = width - paddingRight - child.getMeasuredWidth();
+                            paddingRight += child.getMeasuredWidth();
+                            break;
+                    }
+                    switch (vgrav) {
+                        default:
+                            childTop = paddingTop;
+                            break;
+                        case Gravity.TOP:
+                            childTop = paddingTop;
+                            paddingTop += child.getMeasuredHeight();
+                            break;
+                        case Gravity.CENTER_VERTICAL:
+                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,
+                                    paddingTop);
+                            break;
+                        case Gravity.BOTTOM:
+                            childTop = height - paddingBottom - child.getMeasuredHeight();
+                            paddingBottom += child.getMeasuredHeight();
+                            break;
+                    }
+                    childLeft += scrollX;
+                    child.layout(childLeft, childTop,
+                            childLeft + child.getMeasuredWidth(),
+                            childTop + child.getMeasuredHeight());
+                    decorCount++;
+                }
+            }
+        }
+
+        final int childWidth = width - paddingLeft - paddingRight;
+        // Page views. Do this once we have the right padding offsets from above.
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.isDecor) {
+                continue;
+            }
+
+            final ItemInfo ii = infoForChild(child);
+            if (ii == null) {
+                continue;
+            }
+
+            if (lp.needsMeasure) {
+                // This was added during layout and needs measurement.
+                // Do it now that we know what we're working with.
+                lp.needsMeasure = false;
+                final int widthSpec = MeasureSpec.makeMeasureSpec(
+                        (int) (childWidth * lp.widthFactor),
+                        MeasureSpec.EXACTLY);
+                final int heightSpec = MeasureSpec.makeMeasureSpec(
+                        (int) (height - paddingTop - paddingBottom),
+                        MeasureSpec.EXACTLY);
+                child.measure(widthSpec, heightSpec);
+            }
+
+            final int childMeasuredWidth = child.getMeasuredWidth();
+            final int startOffset = (int) (childWidth * ii.offset);
+            final int childLeft;
+            if (isLayoutRtl()) {
+                childLeft = MAX_SCROLL_X - paddingRight - startOffset - childMeasuredWidth;
+            } else {
+                childLeft = paddingLeft + startOffset;
+            }
+
+            final int childTop = paddingTop;
+            child.layout(childLeft, childTop, childLeft + childMeasuredWidth,
+                    childTop + child.getMeasuredHeight());
+        }
+
+        mTopPageBounds = paddingTop;
+        mBottomPageBounds = height - paddingBottom;
+        mDecorChildCount = decorCount;
+
+        if (mFirstLayout) {
+            scrollToItem(mCurItem, false, 0, false);
+        }
+        mFirstLayout = false;
+    }
+
+    @Override
+    public void computeScroll() {
+        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
+            final int oldX = getScrollX();
+            final int oldY = getScrollY();
+            final int x = mScroller.getCurrX();
+            final int y = mScroller.getCurrY();
+
+            if (oldX != x || oldY != y) {
+                scrollTo(x, y);
+
+                if (!pageScrolled(x)) {
+                    mScroller.abortAnimation();
+                    scrollTo(0, y);
+                }
+            }
+
+            // Keep on drawing until the animation has finished.
+            postInvalidateOnAnimation();
+            return;
+        }
+
+        // Done with scroll, clean up state.
+        completeScroll(true);
+    }
+
+    private boolean pageScrolled(int scrollX) {
+        if (mItems.size() == 0) {
+            mCalledSuper = false;
+            onPageScrolled(0, 0, 0);
+            if (!mCalledSuper) {
+                throw new IllegalStateException(
+                        "onPageScrolled did not call superclass implementation");
+            }
+            return false;
+        }
+
+        // Translate to scrollX to scrollStart for RTL.
+        final int scrollStart;
+        if (isLayoutRtl()) {
+            scrollStart = MAX_SCROLL_X - scrollX;
+        } else {
+            scrollStart = scrollX;
+        }
+
+        final ItemInfo ii = infoForFirstVisiblePage();
+        final int width = getPaddedWidth();
+        final int widthWithMargin = width + mPageMargin;
+        final float marginOffset = (float) mPageMargin / width;
+        final int currentPage = ii.position;
+        final float pageOffset = (((float) scrollStart / width) - ii.offset) /
+                (ii.widthFactor + marginOffset);
+        final int offsetPixels = (int) (pageOffset * widthWithMargin);
+
+        mCalledSuper = false;
+        onPageScrolled(currentPage, pageOffset, offsetPixels);
+        if (!mCalledSuper) {
+            throw new IllegalStateException(
+                    "onPageScrolled did not call superclass implementation");
+        }
+        return true;
+    }
+
+    /**
+     * This method will be invoked when the current page is scrolled, either as part
+     * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+     * If you override this method you must call through to the superclass implementation
+     * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
+     * returns.
+     *
+     * @param position Position index of the first page currently being displayed.
+     *                 Page position+1 will be visible if positionOffset is nonzero.
+     * @param offset Value from [0, 1) indicating the offset from the page at position.
+     * @param offsetPixels Value in pixels indicating the offset from position.
+     */
+    protected void onPageScrolled(int position, float offset, int offsetPixels) {
+        // Offset any decor views if needed - keep them on-screen at all times.
+        if (mDecorChildCount > 0) {
+            final int scrollX = getScrollX();
+            int paddingLeft = getPaddingLeft();
+            int paddingRight = getPaddingRight();
+            final int width = getWidth();
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.isDecor) continue;
+
+                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+                int childLeft = 0;
+                switch (hgrav) {
+                    default:
+                        childLeft = paddingLeft;
+                        break;
+                    case Gravity.LEFT:
+                        childLeft = paddingLeft;
+                        paddingLeft += child.getWidth();
+                        break;
+                    case Gravity.CENTER_HORIZONTAL:
+                        childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+                                paddingLeft);
+                        break;
+                    case Gravity.RIGHT:
+                        childLeft = width - paddingRight - child.getMeasuredWidth();
+                        paddingRight += child.getMeasuredWidth();
+                        break;
+                }
+                childLeft += scrollX;
+
+                final int childOffset = childLeft - child.getLeft();
+                if (childOffset != 0) {
+                    child.offsetLeftAndRight(childOffset);
+                }
+            }
+        }
+
+        if (mOnPageChangeListener != null) {
+            mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+        }
+        if (mInternalPageChangeListener != null) {
+            mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+        }
+
+        if (mPageTransformer != null) {
+            final int scrollX = getScrollX();
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+                if (lp.isDecor) continue;
+
+                final float transformPos = (float) (child.getLeft() - scrollX) / getPaddedWidth();
+                mPageTransformer.transformPage(child, transformPos);
+            }
+        }
+
+        mCalledSuper = true;
+    }
+
+    private void completeScroll(boolean postEvents) {
+        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
+        if (needPopulate) {
+            // Done with scroll, no longer want to cache view drawing.
+            setScrollingCacheEnabled(false);
+            mScroller.abortAnimation();
+            int oldX = getScrollX();
+            int oldY = getScrollY();
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+            if (oldX != x || oldY != y) {
+                scrollTo(x, y);
+            }
+        }
+        mPopulatePending = false;
+        for (int i=0; i<mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            if (ii.scrolling) {
+                needPopulate = true;
+                ii.scrolling = false;
+            }
+        }
+        if (needPopulate) {
+            if (postEvents) {
+                postOnAnimation(mEndScrollRunnable);
+            } else {
+                mEndScrollRunnable.run();
+            }
+        }
+    }
+
+    private boolean isGutterDrag(float x, float dx) {
+        return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
+    }
+
+    private void enableLayers(boolean enable) {
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
+            getChildAt(i).setLayerType(layerType, null);
+        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+        // Always take care of the touch gesture being complete.
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+            // Release the drag.
+            if (DEBUG) Log.v(TAG, "Intercept done!");
+            mIsBeingDragged = false;
+            mIsUnableToDrag = false;
+            mActivePointerId = INVALID_POINTER;
+            if (mVelocityTracker != null) {
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+            }
+            return false;
+        }
+
+        // Nothing more to do here if we have decided whether or not we
+        // are dragging.
+        if (action != MotionEvent.ACTION_DOWN) {
+            if (mIsBeingDragged) {
+                if (DEBUG) Log.v(TAG, "Being dragged, intercept returning true!");
+                return true;
+            }
+            if (mIsUnableToDrag) {
+                if (DEBUG) Log.v(TAG, "Unable to drag, intercept returning false!");
+                return false;
+            }
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from his original down touch.
+                 */
+
+                /*
+                * Locally do absolute value. mLastMotionY is set to the y value
+                * of the down event.
+                */
+                final int activePointerId = mActivePointerId;
+                if (activePointerId == INVALID_POINTER) {
+                    // If we don't have a valid id, the touch down wasn't on content.
+                    break;
+                }
+
+                final int pointerIndex = ev.findPointerIndex(activePointerId);
+                final float x = ev.getX(pointerIndex);
+                final float dx = x - mLastMotionX;
+                final float xDiff = Math.abs(dx);
+                final float y = ev.getY(pointerIndex);
+                final float yDiff = Math.abs(y - mInitialMotionY);
+                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+
+                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
+                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
+                    // Nested view has scrollable area under this point. Let it be handled there.
+                    mLastMotionX = x;
+                    mLastMotionY = y;
+                    mIsUnableToDrag = true;
+                    return false;
+                }
+                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
+                    if (DEBUG) Log.v(TAG, "Starting drag!");
+                    mIsBeingDragged = true;
+                    requestParentDisallowInterceptTouchEvent(true);
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
+                            mInitialMotionX - mTouchSlop;
+                    mLastMotionY = y;
+                    setScrollingCacheEnabled(true);
+                } else if (yDiff > mTouchSlop) {
+                    // The finger has moved enough in the vertical
+                    // direction to be counted as a drag...  abort
+                    // any attempt to drag horizontally, to work correctly
+                    // with children that have scrolling containers.
+                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
+                    mIsUnableToDrag = true;
+                }
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    if (performDrag(x)) {
+                        postInvalidateOnAnimation();
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                /*
+                 * Remember location of down touch.
+                 * ACTION_DOWN always refers to pointer index 0.
+                 */
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                mIsUnableToDrag = false;
+
+                mScroller.computeScrollOffset();
+                if (mScrollState == SCROLL_STATE_SETTLING &&
+                        Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
+                    // Let the user 'catch' the pager as it animates.
+                    mScroller.abortAnimation();
+                    mPopulatePending = false;
+                    populate();
+                    mIsBeingDragged = true;
+                    requestParentDisallowInterceptTouchEvent(true);
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                } else {
+                    completeScroll(false);
+                    mIsBeingDragged = false;
+                }
+
+                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+                        + " mIsBeingDragged=" + mIsBeingDragged
+                        + "mIsUnableToDrag=" + mIsUnableToDrag);
+                break;
+            }
+
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                break;
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        /*
+         * The only time we want to intercept motion events is if we are in the
+         * drag mode.
+         */
+        return mIsBeingDragged;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
+            // Don't handle edge touches immediately -- they may actually belong to one of our
+            // descendants.
+            return false;
+        }
+
+        if (mAdapter == null || mAdapter.getCount() == 0) {
+            // Nothing to present or scroll; nothing to touch.
+            return false;
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+        boolean needsInvalidate = false;
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                mScroller.abortAnimation();
+                mPopulatePending = false;
+                populate();
+
+                // Remember where the motion event started
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                if (!mIsBeingDragged) {
+                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                    final float x = ev.getX(pointerIndex);
+                    final float xDiff = Math.abs(x - mLastMotionX);
+                    final float y = ev.getY(pointerIndex);
+                    final float yDiff = Math.abs(y - mLastMotionY);
+                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+                    if (xDiff > mTouchSlop && xDiff > yDiff) {
+                        if (DEBUG) Log.v(TAG, "Starting drag!");
+                        mIsBeingDragged = true;
+                        requestParentDisallowInterceptTouchEvent(true);
+                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
+                                mInitialMotionX - mTouchSlop;
+                        mLastMotionY = y;
+                        setScrollState(SCROLL_STATE_DRAGGING);
+                        setScrollingCacheEnabled(true);
+
+                        // Disallow Parent Intercept, just in case
+                        ViewParent parent = getParent();
+                        if (parent != null) {
+                            parent.requestDisallowInterceptTouchEvent(true);
+                        }
+                    }
+                }
+                // Not else! Note that mIsBeingDragged can be set above.
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                    final float x = ev.getX(activePointerIndex);
+                    needsInvalidate |= performDrag(x);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    final int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
+
+                    mPopulatePending = true;
+
+                    final float scrollStart = getScrollStart();
+                    final float scrolledPages = scrollStart / getPaddedWidth();
+                    final ItemInfo ii = infoForFirstVisiblePage();
+                    final int currentPage = ii.position;
+                    final float nextPageOffset;
+                    if (isLayoutRtl()) {
+                        nextPageOffset = (ii.offset - scrolledPages) / ii.widthFactor;
+                    }  else {
+                        nextPageOffset = (scrolledPages - ii.offset) / ii.widthFactor;
+                    }
+
+                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                    final float x = ev.getX(activePointerIndex);
+                    final int totalDelta = (int) (x - mInitialMotionX);
+                    final int nextPage = determineTargetPage(
+                            currentPage, nextPageOffset, initialVelocity, totalDelta);
+                    setCurrentItemInternal(nextPage, true, true, initialVelocity);
+
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                    mLeftEdge.onRelease();
+                    mRightEdge.onRelease();
+                    needsInvalidate = true;
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged) {
+                    scrollToItem(mCurItem, true, 0, false);
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                    mLeftEdge.onRelease();
+                    mRightEdge.onRelease();
+                    needsInvalidate = true;
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                final float x = ev.getX(index);
+                mLastMotionX = x;
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+        if (needsInvalidate) {
+            postInvalidateOnAnimation();
+        }
+        return true;
+    }
+
+    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        final ViewParent parent = getParent();
+        if (parent != null) {
+            parent.requestDisallowInterceptTouchEvent(disallowIntercept);
+        }
+    }
+
+    private boolean performDrag(float x) {
+        boolean needsInvalidate = false;
+
+        final int width = getPaddedWidth();
+        final float deltaX = mLastMotionX - x;
+        mLastMotionX = x;
+
+        final EdgeEffect startEdge;
+        final EdgeEffect endEdge;
+        if (isLayoutRtl()) {
+            startEdge = mRightEdge;
+            endEdge = mLeftEdge;
+        } else {
+            startEdge = mLeftEdge;
+            endEdge = mRightEdge;
+        }
+
+        // Translate scroll to relative coordinates.
+        final float nextScrollX = getScrollX() + deltaX;
+        final float scrollStart;
+        if (isLayoutRtl()) {
+            scrollStart = MAX_SCROLL_X - nextScrollX;
+        } else {
+            scrollStart = nextScrollX;
+        }
+
+        final float startBound;
+        final ItemInfo startItem = mItems.get(0);
+        final boolean startAbsolute = startItem.position == 0;
+        if (startAbsolute) {
+            startBound = startItem.offset * width;
+        } else {
+            startBound = width * mFirstOffset;
+        }
+
+        final float endBound;
+        final ItemInfo endItem = mItems.get(mItems.size() - 1);
+        final boolean endAbsolute = endItem.position == mAdapter.getCount() - 1;
+        if (endAbsolute) {
+            endBound = endItem.offset * width;
+        } else {
+            endBound = width * mLastOffset;
+        }
+
+        final float clampedScrollStart;
+        if (scrollStart < startBound) {
+            if (startAbsolute) {
+                final float over = startBound - scrollStart;
+                startEdge.onPull(Math.abs(over) / width);
+                needsInvalidate = true;
+            }
+            clampedScrollStart = startBound;
+        } else if (scrollStart > endBound) {
+            if (endAbsolute) {
+                final float over = scrollStart - endBound;
+                endEdge.onPull(Math.abs(over) / width);
+                needsInvalidate = true;
+            }
+            clampedScrollStart = endBound;
+        } else {
+            clampedScrollStart = scrollStart;
+        }
+
+        // Translate back to absolute coordinates.
+        final float targetScrollX;
+        if (isLayoutRtl()) {
+            targetScrollX = MAX_SCROLL_X - clampedScrollStart;
+        } else {
+            targetScrollX = clampedScrollStart;
+        }
+
+        // Don't lose the rounded component.
+        mLastMotionX += targetScrollX - (int) targetScrollX;
+
+        scrollTo((int) targetScrollX, getScrollY());
+        pageScrolled((int) targetScrollX);
+
+        return needsInvalidate;
+    }
+
+    /**
+     * @return Info about the page at the current scroll position.
+     *         This can be synthetic for a missing middle page; the 'object' field can be null.
+     */
+    private ItemInfo infoForFirstVisiblePage() {
+        final int startOffset = getScrollStart();
+        final int width = getPaddedWidth();
+        final float scrollOffset = width > 0 ? (float) startOffset / width : 0;
+        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
+
+        int lastPos = -1;
+        float lastOffset = 0.f;
+        float lastWidth = 0.f;
+        boolean first = true;
+        ItemInfo lastItem = null;
+
+        final int N = mItems.size();
+        for (int i = 0; i < N; i++) {
+            ItemInfo ii = mItems.get(i);
+
+            // Seek to position.
+            if (!first && ii.position != lastPos + 1) {
+                // Create a synthetic item for a missing page.
+                ii = mTempItem;
+                ii.offset = lastOffset + lastWidth + marginOffset;
+                ii.position = lastPos + 1;
+                ii.widthFactor = mAdapter.getPageWidth(ii.position);
+                i--;
+            }
+
+            final float offset = ii.offset;
+            final float startBound = offset;
+            if (first || scrollOffset >= startBound) {
+                final float endBound = offset + ii.widthFactor + marginOffset;
+                if (scrollOffset < endBound || i == mItems.size() - 1) {
+                    return ii;
+                }
+            } else {
+                return lastItem;
+            }
+
+            first = false;
+            lastPos = ii.position;
+            lastOffset = offset;
+            lastWidth = ii.widthFactor;
+            lastItem = ii;
+        }
+
+        return lastItem;
+    }
+
+    private int getScrollStart() {
+        if (isLayoutRtl()) {
+            return MAX_SCROLL_X - getScrollX();
+        } else {
+            return getScrollX();
+        }
+    }
+
+    /**
+     * @param currentPage the position of the page with the first visible starting edge
+     * @param pageOffset the fraction of the right-hand page that's visible
+     * @param velocity the velocity of the touch event stream
+     * @param deltaX the distance of the touch event stream
+     * @return the position of the target page
+     */
+    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
+        int targetPage;
+        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+            targetPage = currentPage - (velocity < 0 ? mLeftIncr : 0);
+        } else {
+            final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
+            targetPage = (int) (currentPage - mLeftIncr * (pageOffset + truncator));
+        }
+
+        if (mItems.size() > 0) {
+            final ItemInfo firstItem = mItems.get(0);
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+
+            // Only let the user target pages we have items for
+            targetPage = MathUtils.constrain(targetPage, firstItem.position, lastItem.position);
+        }
+
+        return targetPage;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        boolean needsInvalidate = false;
+
+        final int overScrollMode = getOverScrollMode();
+        if (overScrollMode == View.OVER_SCROLL_ALWAYS ||
+                (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+                        mAdapter != null && mAdapter.getCount() > 1)) {
+            if (!mLeftEdge.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+                final int width = getWidth();
+
+                canvas.rotate(270);
+                canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
+                mLeftEdge.setSize(height, width);
+                needsInvalidate |= mLeftEdge.draw(canvas);
+                canvas.restoreToCount(restoreCount);
+            }
+            if (!mRightEdge.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int width = getWidth();
+                final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+                canvas.rotate(90);
+                canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
+                mRightEdge.setSize(height, width);
+                needsInvalidate |= mRightEdge.draw(canvas);
+                canvas.restoreToCount(restoreCount);
+            }
+        } else {
+            mLeftEdge.finish();
+            mRightEdge.finish();
+        }
+
+        if (needsInvalidate) {
+            // Keep animating
+            postInvalidateOnAnimation();
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Draw the margin drawable between pages if needed.
+        if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
+            final int scrollX = getScrollX();
+            final int width = getWidth();
+
+            final float marginOffset = (float) mPageMargin / width;
+            int itemIndex = 0;
+            ItemInfo ii = mItems.get(0);
+            float offset = ii.offset;
+
+            final int itemCount = mItems.size();
+            final int firstPos = ii.position;
+            final int lastPos = mItems.get(itemCount - 1).position;
+            for (int pos = firstPos; pos < lastPos; pos++) {
+                while (pos > ii.position && itemIndex < itemCount) {
+                    ii = mItems.get(++itemIndex);
+                }
+
+                final float itemOffset;
+                final float widthFactor;
+                if (pos == ii.position) {
+                    itemOffset = ii.offset;
+                    widthFactor = ii.widthFactor;
+                } else {
+                    itemOffset = offset;
+                    widthFactor = mAdapter.getPageWidth(pos);
+                }
+
+                final float left;
+                final float scaledOffset = itemOffset * width;
+                if (isLayoutRtl()) {
+                    left = MAX_SCROLL_X - scaledOffset;
+                } else {
+                    left = scaledOffset + widthFactor * width;
+                }
+
+                offset = itemOffset + widthFactor + marginOffset;
+
+                if (left + mPageMargin > scrollX) {
+                    mMarginDrawable.setBounds((int) left, mTopPageBounds,
+                            (int) (left + mPageMargin + 0.5f), mBottomPageBounds);
+                    mMarginDrawable.draw(canvas);
+                }
+
+                if (left > scrollX + width) {
+                    break; // No more visible, no sense in continuing
+                }
+            }
+        }
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = ev.getActionIndex();
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionX = ev.getX(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    private void endDrag() {
+        mIsBeingDragged = false;
+        mIsUnableToDrag = false;
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void setScrollingCacheEnabled(boolean enabled) {
+        if (mScrollingCacheEnabled != enabled) {
+            mScrollingCacheEnabled = enabled;
+            if (USE_CACHE) {
+                final int size = getChildCount();
+                for (int i = 0; i < size; ++i) {
+                    final View child = getChildAt(i);
+                    if (child.getVisibility() != GONE) {
+                        child.setDrawingCacheEnabled(enabled);
+                    }
+                }
+            }
+        }
+    }
+
+    public boolean canScrollHorizontally(int direction) {
+        if (mAdapter == null) {
+            return false;
+        }
+
+        final int width = getPaddedWidth();
+        final int scrollX = getScrollX();
+        if (direction < 0) {
+            return (scrollX > (int) (width * mFirstOffset));
+        } else if (direction > 0) {
+            return (scrollX < (int) (width * mLastOffset));
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Tests scrollability within child views of v given a delta of dx.
+     *
+     * @param v View to test for horizontal scrollability
+     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+     *               or just its children (false).
+     * @param dx Delta scrolled in pixels
+     * @param x X coordinate of the active touch point
+     * @param y Y coordinate of the active touch point
+     * @return true if child views of v can be scrolled by delta of dx.
+     */
+    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+        if (v instanceof ViewGroup) {
+            final ViewGroup group = (ViewGroup) v;
+            final int scrollX = v.getScrollX();
+            final int scrollY = v.getScrollY();
+            final int count = group.getChildCount();
+            // Count backwards - let topmost views consume scroll distance first.
+            for (int i = count - 1; i >= 0; i--) {
+                // TODO: Add support for transformed views.
+                final View child = group.getChildAt(i);
+                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
+                        && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
+                        && canScroll(child, true, dx, x + scrollX - child.getLeft(),
+                                y + scrollY - child.getTop())) {
+                    return true;
+                }
+            }
+        }
+
+        return checkV && v.canScrollHorizontally(-dx);
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Let the focused view and/or our descendants get the key first
+        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+    }
+
+    /**
+     * You can call this function yourself to have the scroll view perform
+     * scrolling from a key event, just as if the event had been dispatched to
+     * it by the view hierarchy.
+     *
+     * @param event The key event to execute.
+     * @return Return true if the event was handled, else false.
+     */
+    public boolean executeKeyEvent(KeyEvent event) {
+        boolean handled = false;
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    handled = arrowScroll(FOCUS_LEFT);
+                    break;
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    handled = arrowScroll(FOCUS_RIGHT);
+                    break;
+                case KeyEvent.KEYCODE_TAB:
+                    if (event.hasNoModifiers()) {
+                        handled = arrowScroll(FOCUS_FORWARD);
+                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                        handled = arrowScroll(FOCUS_BACKWARD);
+                    }
+                    break;
+            }
+        }
+        return handled;
+    }
+
+    public boolean arrowScroll(int direction) {
+        View currentFocused = findFocus();
+        if (currentFocused == this) {
+            currentFocused = null;
+        } else if (currentFocused != null) {
+            boolean isChild = false;
+            for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
+                    parent = parent.getParent()) {
+                if (parent == this) {
+                    isChild = true;
+                    break;
+                }
+            }
+            if (!isChild) {
+                // This would cause the focus search down below to fail in fun ways.
+                final StringBuilder sb = new StringBuilder();
+                sb.append(currentFocused.getClass().getSimpleName());
+                for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
+                        parent = parent.getParent()) {
+                    sb.append(" => ").append(parent.getClass().getSimpleName());
+                }
+                Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
+                        "current focused view " + sb.toString());
+                currentFocused = null;
+            }
+        }
+
+        boolean handled = false;
+
+        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
+                direction);
+        if (nextFocused != null && nextFocused != currentFocused) {
+            if (direction == View.FOCUS_LEFT) {
+                // If there is nothing to the left, or this is causing us to
+                // jump to the right, then what we really want to do is page left.
+                final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
+                final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
+                if (currentFocused != null && nextLeft >= currLeft) {
+                    handled = pageLeft();
+                } else {
+                    handled = nextFocused.requestFocus();
+                }
+            } else if (direction == View.FOCUS_RIGHT) {
+                // If there is nothing to the right, or this is causing us to
+                // jump to the left, then what we really want to do is page right.
+                final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
+                final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
+                if (currentFocused != null && nextLeft <= currLeft) {
+                    handled = pageRight();
+                } else {
+                    handled = nextFocused.requestFocus();
+                }
+            }
+        } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
+            // Trying to move left and nothing there; try to page.
+            handled = pageLeft();
+        } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
+            // Trying to move right and nothing there; try to page.
+            handled = pageRight();
+        }
+        if (handled) {
+            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+        }
+        return handled;
+    }
+
+    private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
+        if (outRect == null) {
+            outRect = new Rect();
+        }
+        if (child == null) {
+            outRect.set(0, 0, 0, 0);
+            return outRect;
+        }
+        outRect.left = child.getLeft();
+        outRect.right = child.getRight();
+        outRect.top = child.getTop();
+        outRect.bottom = child.getBottom();
+
+        ViewParent parent = child.getParent();
+        while (parent instanceof ViewGroup && parent != this) {
+            final ViewGroup group = (ViewGroup) parent;
+            outRect.left += group.getLeft();
+            outRect.right += group.getRight();
+            outRect.top += group.getTop();
+            outRect.bottom += group.getBottom();
+
+            parent = group.getParent();
+        }
+        return outRect;
+    }
+
+    boolean pageLeft() {
+        return setCurrentItemInternal(mCurItem + mLeftIncr, true, false);
+    }
+
+    boolean pageRight() {
+        return setCurrentItemInternal(mCurItem - mLeftIncr, true, false);
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        if (layoutDirection == LAYOUT_DIRECTION_LTR) {
+            mLeftIncr = -1;
+        } else {
+            mLeftIncr = 1;
+        }
+    }
+
+    /**
+     * We only want the current page that is being shown to be focusable.
+     */
+    @Override
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+        final int focusableCount = views.size();
+
+        final int descendantFocusability = getDescendantFocusability();
+
+        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+            for (int i = 0; i < getChildCount(); i++) {
+                final View child = getChildAt(i);
+                if (child.getVisibility() == VISIBLE) {
+                    ItemInfo ii = infoForChild(child);
+                    if (ii != null && ii.position == mCurItem) {
+                        child.addFocusables(views, direction, focusableMode);
+                    }
+                }
+            }
+        }
+
+        // we add ourselves (if focusable) in all cases except for when we are
+        // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
+        // to avoid the focus search finding layouts when a more precise search
+        // among the focusable children would be more interesting.
+        if (
+            descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+                // No focusable descendants
+                (focusableCount == views.size())) {
+            // Note that we can't call the superclass here, because it will
+            // add all views in.  So we need to do the same thing View does.
+            if (!isFocusable()) {
+                return;
+            }
+            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
+                    isInTouchMode() && !isFocusableInTouchMode()) {
+                return;
+            }
+            if (views != null) {
+                views.add(this);
+            }
+        }
+    }
+
+    /**
+     * We only want the current page that is being shown to be touchable.
+     */
+    @Override
+    public void addTouchables(ArrayList<View> views) {
+        // Note that we don't call super.addTouchables(), which means that
+        // we don't call View.addTouchables().  This is okay because a ViewPager
+        // is itself not touchable.
+        for (int i = 0; i < getChildCount(); i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == VISIBLE) {
+                ItemInfo ii = infoForChild(child);
+                if (ii != null && ii.position == mCurItem) {
+                    child.addTouchables(views);
+                }
+            }
+        }
+    }
+
+    /**
+     * We only want the current page that is being shown to be focusable.
+     */
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction,
+            Rect previouslyFocusedRect) {
+        int index;
+        int increment;
+        int end;
+        int count = getChildCount();
+        if ((direction & FOCUS_FORWARD) != 0) {
+            index = 0;
+            increment = 1;
+            end = count;
+        } else {
+            index = count - 1;
+            increment = -1;
+            end = -1;
+        }
+        for (int i = index; i != end; i += increment) {
+            View child = getChildAt(i);
+            if (child.getVisibility() == VISIBLE) {
+                ItemInfo ii = infoForChild(child);
+                if (ii != null && ii.position == mCurItem) {
+                    if (child.requestFocus(direction, previouslyFocusedRect)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams();
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return generateDefaultLayoutParams();
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LayoutParams && super.checkLayoutParams(p);
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+
+        event.setClassName(ViewPager.class.getName());
+        event.setScrollable(canScroll());
+
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) {
+            event.setItemCount(mAdapter.getCount());
+            event.setFromIndex(mCurItem);
+            event.setToIndex(mCurItem);
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+
+        info.setClassName(ViewPager.class.getName());
+        info.setScrollable(canScroll());
+
+        if (canScrollHorizontally(1)) {
+            info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+            info.addAction(AccessibilityAction.ACTION_SCROLL_RIGHT);
+        }
+
+        if (canScrollHorizontally(-1)) {
+            info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
+            info.addAction(AccessibilityAction.ACTION_SCROLL_LEFT);
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle args) {
+        if (super.performAccessibilityAction(action, args)) {
+            return true;
+        }
+
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+            case R.id.accessibilityActionScrollRight:
+                if (canScrollHorizontally(1)) {
+                    setCurrentItem(mCurItem + 1);
+                    return true;
+                }
+                return false;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+            case R.id.accessibilityActionScrollLeft:
+                if (canScrollHorizontally(-1)) {
+                    setCurrentItem(mCurItem - 1);
+                    return true;
+                }
+                return false;
+        }
+
+        return false;
+    }
+
+    private boolean canScroll() {
+        return mAdapter != null && mAdapter.getCount() > 1;
+    }
+
+    private class PagerObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            dataSetChanged();
+        }
+        @Override
+        public void onInvalidated() {
+            dataSetChanged();
+        }
+    }
+
+    /**
+     * Layout parameters that should be supplied for views added to a
+     * ViewPager.
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        /**
+         * true if this view is a decoration on the pager itself and not
+         * a view supplied by the adapter.
+         */
+        public boolean isDecor;
+
+        /**
+         * Gravity setting for use on decor views only:
+         * Where to position the view page within the overall ViewPager
+         * container; constants are defined in {@link android.view.Gravity}.
+         */
+        public int gravity;
+
+        /**
+         * Width as a 0-1 multiplier of the measured pager width
+         */
+        float widthFactor = 0.f;
+
+        /**
+         * true if this view was added during layout and needs to be measured
+         * before being positioned.
+         */
+        boolean needsMeasure;
+
+        /**
+         * Adapter position this view is for if !isDecor
+         */
+        int position;
+
+        /**
+         * Current child index within the ViewPager that this view occupies
+         */
+        int childIndex;
+
+        public LayoutParams() {
+            super(FILL_PARENT, FILL_PARENT);
+        }
+
+        public LayoutParams(Context context, AttributeSet attrs) {
+            super(context, attrs);
+
+            final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+            gravity = a.getInteger(0, Gravity.TOP);
+            a.recycle();
+        }
+    }
+
+    static class ViewPositionComparator implements Comparator<View> {
+        @Override
+        public int compare(View lhs, View rhs) {
+            final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
+            final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
+            if (llp.isDecor != rlp.isDecor) {
+                return llp.isDecor ? 1 : -1;
+            }
+            return llp.position - rlp.position;
+        }
+    }
+}
diff --git a/com/android/internal/widget/WatchHeaderListView.java b/com/android/internal/widget/WatchHeaderListView.java
new file mode 100644
index 0000000..0654454
--- /dev/null
+++ b/com/android/internal/widget/WatchHeaderListView.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.IdRes;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.HeaderViewListAdapter;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+public class WatchHeaderListView extends ListView {
+    private View mTopPanel;
+
+    public WatchHeaderListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public WatchHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public WatchHeaderListView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
+            ArrayList<ListView.FixedViewInfo> headerViewInfos,
+            ArrayList<ListView.FixedViewInfo> footerViewInfos,
+            ListAdapter adapter) {
+        return new WatchHeaderListAdapter(headerViewInfos, footerViewInfos, adapter);
+    }
+
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        if (mTopPanel == null) {
+            setTopPanel(child);
+        } else {
+            throw new IllegalStateException("WatchHeaderListView can host only one header");
+        }
+    }
+
+    public void setTopPanel(View v) {
+        mTopPanel = v;
+        wrapAdapterIfNecessary();
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        super.setAdapter(adapter);
+        wrapAdapterIfNecessary();
+    }
+
+    @Override
+    protected View findViewTraversal(@IdRes int id) {
+        View v = super.findViewTraversal(id);
+        if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
+            return mTopPanel.findViewById(id);
+        }
+        return v;
+    }
+
+    @Override
+    protected View findViewWithTagTraversal(Object tag) {
+        View v = super.findViewWithTagTraversal(tag);
+        if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
+            return mTopPanel.findViewWithTag(tag);
+        }
+        return v;
+    }
+
+    @Override
+    protected <T extends View> T findViewByPredicateTraversal(
+            Predicate<View> predicate, View childToSkip) {
+        View v = super.findViewByPredicateTraversal(predicate, childToSkip);
+        if (v == null && mTopPanel != null && mTopPanel != childToSkip
+                && !mTopPanel.isRootNamespace()) {
+            return (T) mTopPanel.findViewByPredicate(predicate);
+        }
+        return (T) v;
+    }
+
+    @Override
+    public int getHeaderViewsCount() {
+        return mTopPanel == null ? super.getHeaderViewsCount()
+                : super.getHeaderViewsCount() + (mTopPanel.getVisibility() == GONE ? 0 : 1);
+    }
+
+    private void wrapAdapterIfNecessary() {
+        ListAdapter adapter = getAdapter();
+        if (adapter != null && mTopPanel != null) {
+            if (!(adapter instanceof WatchHeaderListAdapter)) {
+                wrapHeaderListAdapterInternal();
+            }
+
+            ((WatchHeaderListAdapter) getAdapter()).setTopPanel(mTopPanel);
+            dispatchDataSetObserverOnChangedInternal();
+        }
+    }
+
+    private static class WatchHeaderListAdapter extends HeaderViewListAdapter {
+        private View mTopPanel;
+
+        public WatchHeaderListAdapter(
+                ArrayList<ListView.FixedViewInfo> headerViewInfos,
+                ArrayList<ListView.FixedViewInfo> footerViewInfos,
+                ListAdapter adapter) {
+            super(headerViewInfos, footerViewInfos, adapter);
+        }
+
+        public void setTopPanel(View v) {
+            mTopPanel = v;
+        }
+
+        private int getTopPanelCount() {
+            return (mTopPanel == null || mTopPanel.getVisibility() == GONE) ? 0 : 1;
+        }
+
+        @Override
+        public int getCount() {
+            return super.getCount() + getTopPanelCount();
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return getTopPanelCount() == 0 && super.areAllItemsEnabled();
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            int topPanelCount = getTopPanelCount();
+            return position < topPanelCount ? false : super.isEnabled(position - topPanelCount);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            int topPanelCount = getTopPanelCount();
+            return position < topPanelCount ? null : super.getItem(position - topPanelCount);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            int numHeaders = getHeadersCount() + getTopPanelCount();
+            if (getWrappedAdapter() != null && position >= numHeaders) {
+                int adjPosition = position - numHeaders;
+                int adapterCount = getWrappedAdapter().getCount();
+                if (adjPosition < adapterCount) {
+                    return getWrappedAdapter().getItemId(adjPosition);
+                }
+            }
+            return -1;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            int topPanelCount = getTopPanelCount();
+            return position < topPanelCount
+                    ? mTopPanel : super.getView(position - topPanelCount, convertView, parent);
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            int numHeaders = getHeadersCount() + getTopPanelCount();
+            if (getWrappedAdapter() != null && position >= numHeaders) {
+                int adjPosition = position - numHeaders;
+                int adapterCount = getWrappedAdapter().getCount();
+                if (adjPosition < adapterCount) {
+                    return getWrappedAdapter().getItemViewType(adjPosition);
+                }
+            }
+
+            return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        }
+    }
+}
diff --git a/com/android/internal/widget/WatchListDecorLayout.java b/com/android/internal/widget/WatchListDecorLayout.java
new file mode 100644
index 0000000..5b49611
--- /dev/null
+++ b/com/android/internal/widget/WatchListDecorLayout.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.ListView;
+import android.widget.FrameLayout;
+
+import java.util.ArrayList;
+
+
+/**
+ * Layout for the decor for ListViews on watch-type devices with small screens.
+ * <p>
+ * Supports one panel with the gravity set to top, and one panel with gravity set to bottom.
+ * <p>
+ * Use with one ListView child. The top and bottom panels will track the ListView's scrolling.
+ * If there is no ListView child, it will act like a normal FrameLayout.
+ */
+public class WatchListDecorLayout extends FrameLayout
+        implements ViewTreeObserver.OnScrollChangedListener {
+
+    private int mForegroundPaddingLeft = 0;
+    private int mForegroundPaddingTop = 0;
+    private int mForegroundPaddingRight = 0;
+    private int mForegroundPaddingBottom = 0;
+
+    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+    /** Track the amount the ListView has to scroll up to account for padding change difference. */
+    private int mPendingScroll;
+    private View mBottomPanel;
+    private View mTopPanel;
+    private ListView mListView;
+    private ViewTreeObserver mObserver;
+
+
+    public WatchListDecorLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public WatchListDecorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public WatchListDecorLayout(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mPendingScroll = 0;
+
+        for (int i = 0; i < getChildCount(); ++i) {
+            View child = getChildAt(i);
+            if (child instanceof ListView) {
+                if (mListView != null) {
+                    throw new IllegalArgumentException("only one ListView child allowed");
+                }
+                mListView = (ListView) child;
+
+                mListView.setNestedScrollingEnabled(true);
+                mObserver = mListView.getViewTreeObserver();
+                mObserver.addOnScrollChangedListener(this);
+            } else {
+                int gravity = (((LayoutParams) child.getLayoutParams()).gravity
+                        & Gravity.VERTICAL_GRAVITY_MASK);
+                if (gravity == Gravity.TOP && mTopPanel == null) {
+                    mTopPanel = child;
+                } else if (gravity == Gravity.BOTTOM && mBottomPanel == null) {
+                    mBottomPanel = child;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        mListView = null;
+        mBottomPanel = null;
+        mTopPanel = null;
+        if (mObserver != null) {
+            if (mObserver.isAlive()) {
+                mObserver.removeOnScrollChangedListener(this);
+            }
+            mObserver = null;
+        }
+    }
+
+    private void applyMeasureToChild(View child, int widthMeasureSpec, int heightMeasureSpec) {
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        final int childWidthMeasureSpec;
+        if (lp.width == LayoutParams.MATCH_PARENT) {
+            final int width = Math.max(0, getMeasuredWidth()
+                    - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
+                    - lp.leftMargin - lp.rightMargin);
+            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    width, MeasureSpec.EXACTLY);
+        } else {
+            childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                    getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
+                    lp.leftMargin + lp.rightMargin,
+                    lp.width);
+        }
+
+        final int childHeightMeasureSpec;
+        if (lp.height == LayoutParams.MATCH_PARENT) {
+            final int height = Math.max(0, getMeasuredHeight()
+                    - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
+                    - lp.topMargin - lp.bottomMargin);
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    height, MeasureSpec.EXACTLY);
+        } else {
+            childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                    getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
+                    lp.topMargin + lp.bottomMargin,
+                    lp.height);
+        }
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    private int measureAndGetHeight(View child, int widthMeasureSpec, int heightMeasureSpec) {
+        if (child != null) {
+            if (child.getVisibility() != GONE) {
+                applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec);
+                return child.getMeasuredHeight();
+            } else if (getMeasureAllChildren()) {
+                applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec);
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int count = getChildCount();
+
+        final boolean measureMatchParentChildren =
+                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+        mMatchParentChildren.clear();
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+        int childState = 0;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (getMeasureAllChildren() || child.getVisibility() != GONE) {
+                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                maxWidth = Math.max(maxWidth,
+                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+                maxHeight = Math.max(maxHeight,
+                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+                childState = combineMeasuredStates(childState, child.getMeasuredState());
+                if (measureMatchParentChildren) {
+                    if (lp.width == LayoutParams.MATCH_PARENT ||
+                            lp.height == LayoutParams.MATCH_PARENT) {
+                        mMatchParentChildren.add(child);
+                    }
+                }
+            }
+        }
+
+        // Account for padding too
+        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
+        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
+
+        // Check against our minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        // Check against our foreground's minimum height and width
+        final Drawable drawable = getForeground();
+        if (drawable != null) {
+            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+        }
+
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+        if (mListView != null) {
+            if (mPendingScroll != 0) {
+                mListView.scrollListBy(mPendingScroll);
+                mPendingScroll = 0;
+            }
+
+            int paddingTop = Math.max(mListView.getPaddingTop(),
+                    measureAndGetHeight(mTopPanel, widthMeasureSpec, heightMeasureSpec));
+            int paddingBottom = Math.max(mListView.getPaddingBottom(),
+                    measureAndGetHeight(mBottomPanel, widthMeasureSpec, heightMeasureSpec));
+
+            if (paddingTop != mListView.getPaddingTop()
+                    || paddingBottom != mListView.getPaddingBottom()) {
+                mPendingScroll += mListView.getPaddingTop() - paddingTop;
+                mListView.setPadding(
+                        mListView.getPaddingLeft(), paddingTop,
+                        mListView.getPaddingRight(), paddingBottom);
+            }
+        }
+
+        count = mMatchParentChildren.size();
+        if (count > 1) {
+            for (int i = 0; i < count; i++) {
+                final View child = mMatchParentChildren.get(i);
+                if (mListView == null || (child != mTopPanel && child != mBottomPanel)) {
+                    applyMeasureToChild(child, widthMeasureSpec, heightMeasureSpec);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setForegroundGravity(int foregroundGravity) {
+        if (getForegroundGravity() != foregroundGravity) {
+            super.setForegroundGravity(foregroundGravity);
+
+            // calling get* again here because the set above may apply default constraints
+            final Drawable foreground = getForeground();
+            if (getForegroundGravity() == Gravity.FILL && foreground != null) {
+                Rect padding = new Rect();
+                if (foreground.getPadding(padding)) {
+                    mForegroundPaddingLeft = padding.left;
+                    mForegroundPaddingTop = padding.top;
+                    mForegroundPaddingRight = padding.right;
+                    mForegroundPaddingBottom = padding.bottom;
+                }
+            } else {
+                mForegroundPaddingLeft = 0;
+                mForegroundPaddingTop = 0;
+                mForegroundPaddingRight = 0;
+                mForegroundPaddingBottom = 0;
+            }
+        }
+    }
+
+    private int getPaddingLeftWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
+            mPaddingLeft + mForegroundPaddingLeft;
+    }
+
+    private int getPaddingRightWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
+            mPaddingRight + mForegroundPaddingRight;
+    }
+
+    private int getPaddingTopWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
+            mPaddingTop + mForegroundPaddingTop;
+    }
+
+    private int getPaddingBottomWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
+            mPaddingBottom + mForegroundPaddingBottom;
+    }
+
+    @Override
+    public void onScrollChanged() {
+        if (mListView == null) {
+            return;
+        }
+
+        if (mTopPanel != null) {
+            if (mListView.getChildCount() > 0) {
+                if (mListView.getFirstVisiblePosition() == 0) {
+                    View firstChild = mListView.getChildAt(0);
+                    setScrolling(mTopPanel,
+                            firstChild.getY() - mTopPanel.getHeight() - mTopPanel.getTop());
+                } else {
+                    // shift to hide the frame, last child is not the last position
+                    setScrolling(mTopPanel, -mTopPanel.getHeight());
+                }
+            } else {
+                setScrolling(mTopPanel, 0); // no visible child, fallback to default behaviour
+            }
+        }
+
+        if (mBottomPanel != null) {
+            if (mListView.getChildCount() > 0) {
+                if (mListView.getLastVisiblePosition() >= mListView.getCount() - 1) {
+                    View lastChild = mListView.getChildAt(mListView.getChildCount() - 1);
+                    setScrolling(mBottomPanel, Math.max(
+                            0,
+                            lastChild.getY() + lastChild.getHeight() - mBottomPanel.getTop()));
+                } else {
+                    // shift to hide the frame, last child is not the last position
+                    setScrolling(mBottomPanel, mBottomPanel.getHeight());
+                }
+            } else {
+                setScrolling(mBottomPanel, 0); // no visible child, fallback to default behaviour
+            }
+        }
+    }
+
+    /** Only set scrolling for the panel if there is a change in its translationY. */
+    private void setScrolling(View panel, float translationY) {
+        if (panel.getTranslationY() != translationY) {
+            panel.setTranslationY(translationY);
+        }
+    }
+}
diff --git a/com/android/internal/widget/WeightedLinearLayout.java b/com/android/internal/widget/WeightedLinearLayout.java
new file mode 100644
index 0000000..385a7c3
--- /dev/null
+++ b/com/android/internal/widget/WeightedLinearLayout.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.widget.LinearLayout;
+
+import static android.view.View.MeasureSpec.*;
+import static com.android.internal.R.*;
+
+/**
+ * A special layout when measured in AT_MOST will take up a given percentage of
+ * the available space.
+ */
+public class WeightedLinearLayout extends LinearLayout {
+    private float mMajorWeightMin;
+    private float mMinorWeightMin;
+    private float mMajorWeightMax;
+    private float mMinorWeightMax;
+
+    public WeightedLinearLayout(Context context) {
+        super(context);
+    }
+
+    public WeightedLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        
+        TypedArray a = 
+            context.obtainStyledAttributes(attrs, styleable.WeightedLinearLayout);
+
+        mMajorWeightMin = a.getFloat(styleable.WeightedLinearLayout_majorWeightMin, 0.0f);
+        mMinorWeightMin = a.getFloat(styleable.WeightedLinearLayout_minorWeightMin, 0.0f);
+        mMajorWeightMax = a.getFloat(styleable.WeightedLinearLayout_majorWeightMax, 0.0f);
+        mMinorWeightMax = a.getFloat(styleable.WeightedLinearLayout_minorWeightMax, 0.0f);
+        
+        a.recycle();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+        final int screenWidth = metrics.widthPixels;
+        final boolean isPortrait = screenWidth < metrics.heightPixels;
+
+        final int widthMode = getMode(widthMeasureSpec);
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int width = getMeasuredWidth();
+        boolean measure = false;
+
+        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
+
+        final float widthWeightMin = isPortrait ? mMinorWeightMin : mMajorWeightMin;
+        final float widthWeightMax = isPortrait ? mMinorWeightMax : mMajorWeightMax;
+        if (widthMode == AT_MOST) {
+            final int weightedMin = (int) (screenWidth * widthWeightMin);
+            final int weightedMax = (int) (screenWidth * widthWeightMin);
+            if (widthWeightMin > 0.0f && width < weightedMin) {
+                widthMeasureSpec = MeasureSpec.makeMeasureSpec(weightedMin, EXACTLY);
+                measure = true;
+            } else if (widthWeightMax > 0.0f && width > weightedMax) {
+                widthMeasureSpec = MeasureSpec.makeMeasureSpec(weightedMax, EXACTLY);
+                measure = true;
+            }
+        }
+
+        // TODO: Support height?
+
+        if (measure) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+}
diff --git a/com/android/internal/widget/helper/ItemTouchHelper.java b/com/android/internal/widget/helper/ItemTouchHelper.java
new file mode 100644
index 0000000..9636ed8
--- /dev/null
+++ b/com/android/internal/widget/helper/ItemTouchHelper.java
@@ -0,0 +1,2391 @@
+/*
+ * Copyright (C) 2017 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.internal.widget.helper;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+import android.view.animation.Interpolator;
+
+import com.android.internal.R;
+import com.android.internal.widget.LinearLayoutManager;
+import com.android.internal.widget.RecyclerView;
+import com.android.internal.widget.RecyclerView.OnItemTouchListener;
+import com.android.internal.widget.RecyclerView.ViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
+ * <p>
+ * It works with a RecyclerView and a Callback class, which configures what type of interactions
+ * are enabled and also receives events when user performs these actions.
+ * <p>
+ * Depending on which functionality you support, you should override
+ * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or
+ * {@link Callback#onSwiped(ViewHolder, int)}.
+ * <p>
+ * This class is designed to work with any LayoutManager but for certain situations, it can be
+ * optimized for your custom LayoutManager by extending methods in the
+ * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler}
+ * interface in your LayoutManager.
+ * <p>
+ * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. On
+ * platforms older than Honeycomb, ItemTouchHelper uses canvas translations and View's visibility
+ * property to move items in response to touch events. You can customize these behaviors by
+ * overriding {@link Callback#onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)}
+ * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)}.
+ * <p/>
+ * Most of the time, you only need to override <code>onChildDraw</code> but due to limitations of
+ * platform prior to Honeycomb, you may need to implement <code>onChildDrawOver</code> as well.
+ */
+public class ItemTouchHelper extends RecyclerView.ItemDecoration
+        implements RecyclerView.OnChildAttachStateChangeListener {
+
+    /**
+     * Up direction, used for swipe & drag control.
+     */
+    public static final int UP = 1;
+
+    /**
+     * Down direction, used for swipe & drag control.
+     */
+    public static final int DOWN = 1 << 1;
+
+    /**
+     * Left direction, used for swipe & drag control.
+     */
+    public static final int LEFT = 1 << 2;
+
+    /**
+     * Right direction, used for swipe & drag control.
+     */
+    public static final int RIGHT = 1 << 3;
+
+    // If you change these relative direction values, update Callback#convertToAbsoluteDirection,
+    // Callback#convertToRelativeDirection.
+    /**
+     * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout
+     * direction. Used for swipe & drag control.
+     */
+    public static final int START = LEFT << 2;
+
+    /**
+     * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout
+     * direction. Used for swipe & drag control.
+     */
+    public static final int END = RIGHT << 2;
+
+    /**
+     * ItemTouchHelper is in idle state. At this state, either there is no related motion event by
+     * the user or latest motion events have not yet triggered a swipe or drag.
+     */
+    public static final int ACTION_STATE_IDLE = 0;
+
+    /**
+     * A View is currently being swiped.
+     */
+    public static final int ACTION_STATE_SWIPE = 1;
+
+    /**
+     * A View is currently being dragged.
+     */
+    public static final int ACTION_STATE_DRAG = 2;
+
+    /**
+     * Animation type for views which are swiped successfully.
+     */
+    public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1;
+
+    /**
+     * Animation type for views which are not completely swiped thus will animate back to their
+     * original position.
+     */
+    public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2;
+
+    /**
+     * Animation type for views that were dragged and now will animate to their final position.
+     */
+    public static final int ANIMATION_TYPE_DRAG = 1 << 3;
+
+    static final String TAG = "ItemTouchHelper";
+
+    static final boolean DEBUG = false;
+
+    static final int ACTIVE_POINTER_ID_NONE = -1;
+
+    static final int DIRECTION_FLAG_COUNT = 8;
+
+    private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1;
+
+    static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT;
+
+    static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT;
+
+    /**
+     * The unit we are using to track velocity
+     */
+    private static final int PIXELS_PER_SECOND = 1000;
+
+    /**
+     * Views, whose state should be cleared after they are detached from RecyclerView.
+     * This is necessary after swipe dismissing an item. We wait until animator finishes its job
+     * to clean these views.
+     */
+    final List<View> mPendingCleanup = new ArrayList<View>();
+
+    /**
+     * Re-use array to calculate dx dy for a ViewHolder
+     */
+    private final float[] mTmpPosition = new float[2];
+
+    /**
+     * Currently selected view holder
+     */
+    ViewHolder mSelected = null;
+
+    /**
+     * The reference coordinates for the action start. For drag & drop, this is the time long
+     * press is completed vs for swipe, this is the initial touch point.
+     */
+    float mInitialTouchX;
+
+    float mInitialTouchY;
+
+    /**
+     * Set when ItemTouchHelper is assigned to a RecyclerView.
+     */
+    float mSwipeEscapeVelocity;
+
+    /**
+     * Set when ItemTouchHelper is assigned to a RecyclerView.
+     */
+    float mMaxSwipeVelocity;
+
+    /**
+     * The diff between the last event and initial touch.
+     */
+    float mDx;
+
+    float mDy;
+
+    /**
+     * The coordinates of the selected view at the time it is selected. We record these values
+     * when action starts so that we can consistently position it even if LayoutManager moves the
+     * View.
+     */
+    float mSelectedStartX;
+
+    float mSelectedStartY;
+
+    /**
+     * The pointer we are tracking.
+     */
+    int mActivePointerId = ACTIVE_POINTER_ID_NONE;
+
+    /**
+     * Developer callback which controls the behavior of ItemTouchHelper.
+     */
+    Callback mCallback;
+
+    /**
+     * Current mode.
+     */
+    int mActionState = ACTION_STATE_IDLE;
+
+    /**
+     * The direction flags obtained from unmasking
+     * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current
+     * action state.
+     */
+    int mSelectedFlags;
+
+    /**
+     * When a View is dragged or swiped and needs to go back to where it was, we create a Recover
+     * Animation and animate it to its location using this custom Animator, instead of using
+     * framework Animators.
+     * Using framework animators has the side effect of clashing with ItemAnimator, creating
+     * jumpy UIs.
+     */
+    List<RecoverAnimation> mRecoverAnimations = new ArrayList<RecoverAnimation>();
+
+    private int mSlop;
+
+    RecyclerView mRecyclerView;
+
+    /**
+     * When user drags a view to the edge, we start scrolling the LayoutManager as long as View
+     * is partially out of bounds.
+     */
+    final Runnable mScrollRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mSelected != null && scrollIfNecessary()) {
+                if (mSelected != null) { //it might be lost during scrolling
+                    moveIfNecessary(mSelected);
+                }
+                mRecyclerView.removeCallbacks(mScrollRunnable);
+                mRecyclerView.postOnAnimation(this);
+            }
+        }
+    };
+
+    /**
+     * Used for detecting fling swipe
+     */
+    VelocityTracker mVelocityTracker;
+
+    //re-used list for selecting a swap target
+    private List<ViewHolder> mSwapTargets;
+
+    //re used for for sorting swap targets
+    private List<Integer> mDistances;
+
+    /**
+     * If drag & drop is supported, we use child drawing order to bring them to front.
+     */
+    private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null;
+
+    /**
+     * This keeps a reference to the child dragged by the user. Even after user stops dragging,
+     * until view reaches its final position (end of recover animation), we keep a reference so
+     * that it can be drawn above other children.
+     */
+    View mOverdrawChild = null;
+
+    /**
+     * We cache the position of the overdraw child to avoid recalculating it each time child
+     * position callback is called. This value is invalidated whenever a child is attached or
+     * detached.
+     */
+    int mOverdrawChildPosition = -1;
+
+    /**
+     * Used to detect long press.
+     */
+    GestureDetector mGestureDetector;
+
+    private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
+        @Override
+        public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
+            mGestureDetector.onTouchEvent(event);
+            if (DEBUG) {
+                Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
+            }
+            final int action = event.getActionMasked();
+            if (action == MotionEvent.ACTION_DOWN) {
+                mActivePointerId = event.getPointerId(0);
+                mInitialTouchX = event.getX();
+                mInitialTouchY = event.getY();
+                obtainVelocityTracker();
+                if (mSelected == null) {
+                    final RecoverAnimation animation = findAnimation(event);
+                    if (animation != null) {
+                        mInitialTouchX -= animation.mX;
+                        mInitialTouchY -= animation.mY;
+                        endRecoverAnimation(animation.mViewHolder, true);
+                        if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
+                            mCallback.clearView(mRecyclerView, animation.mViewHolder);
+                        }
+                        select(animation.mViewHolder, animation.mActionState);
+                        updateDxDy(event, mSelectedFlags, 0);
+                    }
+                }
+            } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+                mActivePointerId = ACTIVE_POINTER_ID_NONE;
+                select(null, ACTION_STATE_IDLE);
+            } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
+                // in a non scroll orientation, if distance change is above threshold, we
+                // can select the item
+                final int index = event.findPointerIndex(mActivePointerId);
+                if (DEBUG) {
+                    Log.d(TAG, "pointer index " + index);
+                }
+                if (index >= 0) {
+                    checkSelectForSwipe(action, event, index);
+                }
+            }
+            if (mVelocityTracker != null) {
+                mVelocityTracker.addMovement(event);
+            }
+            return mSelected != null;
+        }
+
+        @Override
+        public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
+            mGestureDetector.onTouchEvent(event);
+            if (DEBUG) {
+                Log.d(TAG,
+                        "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
+            }
+            if (mVelocityTracker != null) {
+                mVelocityTracker.addMovement(event);
+            }
+            if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
+                return;
+            }
+            final int action = event.getActionMasked();
+            final int activePointerIndex = event.findPointerIndex(mActivePointerId);
+            if (activePointerIndex >= 0) {
+                checkSelectForSwipe(action, event, activePointerIndex);
+            }
+            ViewHolder viewHolder = mSelected;
+            if (viewHolder == null) {
+                return;
+            }
+            switch (action) {
+                case MotionEvent.ACTION_MOVE: {
+                    // Find the index of the active pointer and fetch its position
+                    if (activePointerIndex >= 0) {
+                        updateDxDy(event, mSelectedFlags, activePointerIndex);
+                        moveIfNecessary(viewHolder);
+                        mRecyclerView.removeCallbacks(mScrollRunnable);
+                        mScrollRunnable.run();
+                        mRecyclerView.invalidate();
+                    }
+                    break;
+                }
+                case MotionEvent.ACTION_CANCEL:
+                    if (mVelocityTracker != null) {
+                        mVelocityTracker.clear();
+                    }
+                    // fall through
+                case MotionEvent.ACTION_UP:
+                    select(null, ACTION_STATE_IDLE);
+                    mActivePointerId = ACTIVE_POINTER_ID_NONE;
+                    break;
+                case MotionEvent.ACTION_POINTER_UP: {
+                    final int pointerIndex = event.getActionIndex();
+                    final int pointerId = event.getPointerId(pointerIndex);
+                    if (pointerId == mActivePointerId) {
+                        // This was our active pointer going up. Choose a new
+                        // active pointer and adjust accordingly.
+                        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+                        mActivePointerId = event.getPointerId(newPointerIndex);
+                        updateDxDy(event, mSelectedFlags, pointerIndex);
+                    }
+                    break;
+                }
+            }
+        }
+
+        @Override
+        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+            if (!disallowIntercept) {
+                return;
+            }
+            select(null, ACTION_STATE_IDLE);
+        }
+    };
+
+    /**
+     * Temporary rect instance that is used when we need to lookup Item decorations.
+     */
+    private Rect mTmpRect;
+
+    /**
+     * When user started to drag scroll. Reset when we don't scroll
+     */
+    private long mDragScrollStartTimeInMs;
+
+    /**
+     * Creates an ItemTouchHelper that will work with the given Callback.
+     * <p>
+     * You can attach ItemTouchHelper to a RecyclerView via
+     * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration,
+     * an onItemTouchListener and a Child attach / detach listener to the RecyclerView.
+     *
+     * @param callback The Callback which controls the behavior of this touch helper.
+     */
+    public ItemTouchHelper(Callback callback) {
+        mCallback = callback;
+    }
+
+    private static boolean hitTest(View child, float x, float y, float left, float top) {
+        return x >= left
+                && x <= left + child.getWidth()
+                && y >= top
+                && y <= top + child.getHeight();
+    }
+
+    /**
+     * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already
+     * attached to a RecyclerView, it will first detach from the previous one. You can call this
+     * method with {@code null} to detach it from the current RecyclerView.
+     *
+     * @param recyclerView The RecyclerView instance to which you want to add this helper or
+     *                     {@code null} if you want to remove ItemTouchHelper from the current
+     *                     RecyclerView.
+     */
+    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+        if (mRecyclerView == recyclerView) {
+            return; // nothing to do
+        }
+        if (mRecyclerView != null) {
+            destroyCallbacks();
+        }
+        mRecyclerView = recyclerView;
+        if (mRecyclerView != null) {
+            final Resources resources = recyclerView.getResources();
+            mSwipeEscapeVelocity = resources
+                    .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
+            mMaxSwipeVelocity = resources
+                    .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
+            setupCallbacks();
+        }
+    }
+
+    private void setupCallbacks() {
+        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
+        mSlop = vc.getScaledTouchSlop();
+        mRecyclerView.addItemDecoration(this);
+        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
+        mRecyclerView.addOnChildAttachStateChangeListener(this);
+        initGestureDetector();
+    }
+
+    private void destroyCallbacks() {
+        mRecyclerView.removeItemDecoration(this);
+        mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener);
+        mRecyclerView.removeOnChildAttachStateChangeListener(this);
+        // clean all attached
+        final int recoverAnimSize = mRecoverAnimations.size();
+        for (int i = recoverAnimSize - 1; i >= 0; i--) {
+            final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0);
+            mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
+        }
+        mRecoverAnimations.clear();
+        mOverdrawChild = null;
+        mOverdrawChildPosition = -1;
+        releaseVelocityTracker();
+    }
+
+    private void initGestureDetector() {
+        if (mGestureDetector != null) {
+            return;
+        }
+        mGestureDetector = new GestureDetector(mRecyclerView.getContext(),
+                new ItemTouchHelperGestureListener());
+    }
+
+    private void getSelectedDxDy(float[] outPosition) {
+        if ((mSelectedFlags & (LEFT | RIGHT)) != 0) {
+            outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft();
+        } else {
+            outPosition[0] = mSelected.itemView.getTranslationX();
+        }
+        if ((mSelectedFlags & (UP | DOWN)) != 0) {
+            outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop();
+        } else {
+            outPosition[1] = mSelected.itemView.getTranslationY();
+        }
+    }
+
+    @Override
+    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        float dx = 0, dy = 0;
+        if (mSelected != null) {
+            getSelectedDxDy(mTmpPosition);
+            dx = mTmpPosition[0];
+            dy = mTmpPosition[1];
+        }
+        mCallback.onDrawOver(c, parent, mSelected,
+                mRecoverAnimations, mActionState, dx, dy);
+    }
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        // we don't know if RV changed something so we should invalidate this index.
+        mOverdrawChildPosition = -1;
+        float dx = 0, dy = 0;
+        if (mSelected != null) {
+            getSelectedDxDy(mTmpPosition);
+            dx = mTmpPosition[0];
+            dy = mTmpPosition[1];
+        }
+        mCallback.onDraw(c, parent, mSelected,
+                mRecoverAnimations, mActionState, dx, dy);
+    }
+
+    /**
+     * Starts dragging or swiping the given View. Call with null if you want to clear it.
+     *
+     * @param selected    The ViewHolder to drag or swipe. Can be null if you want to cancel the
+     *                    current action
+     * @param actionState The type of action
+     */
+    void select(ViewHolder selected, int actionState) {
+        if (selected == mSelected && actionState == mActionState) {
+            return;
+        }
+        mDragScrollStartTimeInMs = Long.MIN_VALUE;
+        final int prevActionState = mActionState;
+        // prevent duplicate animations
+        endRecoverAnimation(selected, true);
+        mActionState = actionState;
+        if (actionState == ACTION_STATE_DRAG) {
+            // we remove after animation is complete. this means we only elevate the last drag
+            // child but that should perform good enough as it is very hard to start dragging a
+            // new child before the previous one settles.
+            mOverdrawChild = selected.itemView;
+            addChildDrawingOrderCallback();
+        }
+        int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
+                - 1;
+        boolean preventLayout = false;
+
+        if (mSelected != null) {
+            final ViewHolder prevSelected = mSelected;
+            if (prevSelected.itemView.getParent() != null) {
+                final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
+                        : swipeIfNecessary(prevSelected);
+                releaseVelocityTracker();
+                // find where we should animate to
+                final float targetTranslateX, targetTranslateY;
+                int animationType;
+                switch (swipeDir) {
+                    case LEFT:
+                    case RIGHT:
+                    case START:
+                    case END:
+                        targetTranslateY = 0;
+                        targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
+                        break;
+                    case UP:
+                    case DOWN:
+                        targetTranslateX = 0;
+                        targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
+                        break;
+                    default:
+                        targetTranslateX = 0;
+                        targetTranslateY = 0;
+                }
+                if (prevActionState == ACTION_STATE_DRAG) {
+                    animationType = ANIMATION_TYPE_DRAG;
+                } else if (swipeDir > 0) {
+                    animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
+                } else {
+                    animationType = ANIMATION_TYPE_SWIPE_CANCEL;
+                }
+                getSelectedDxDy(mTmpPosition);
+                final float currentTranslateX = mTmpPosition[0];
+                final float currentTranslateY = mTmpPosition[1];
+                final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
+                        prevActionState, currentTranslateX, currentTranslateY,
+                        targetTranslateX, targetTranslateY) {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        super.onAnimationEnd(animation);
+                        if (this.mOverridden) {
+                            return;
+                        }
+                        if (swipeDir <= 0) {
+                            // this is a drag or failed swipe. recover immediately
+                            mCallback.clearView(mRecyclerView, prevSelected);
+                            // full cleanup will happen on onDrawOver
+                        } else {
+                            // wait until remove animation is complete.
+                            mPendingCleanup.add(prevSelected.itemView);
+                            mIsPendingCleanup = true;
+                            if (swipeDir > 0) {
+                                // Animation might be ended by other animators during a layout.
+                                // We defer callback to avoid editing adapter during a layout.
+                                postDispatchSwipe(this, swipeDir);
+                            }
+                        }
+                        // removed from the list after it is drawn for the last time
+                        if (mOverdrawChild == prevSelected.itemView) {
+                            removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
+                        }
+                    }
+                };
+                final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
+                        targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
+                rv.setDuration(duration);
+                mRecoverAnimations.add(rv);
+                rv.start();
+                preventLayout = true;
+            } else {
+                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
+                mCallback.clearView(mRecyclerView, prevSelected);
+            }
+            mSelected = null;
+        }
+        if (selected != null) {
+            mSelectedFlags =
+                    (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
+                            >> (mActionState * DIRECTION_FLAG_COUNT);
+            mSelectedStartX = selected.itemView.getLeft();
+            mSelectedStartY = selected.itemView.getTop();
+            mSelected = selected;
+
+            if (actionState == ACTION_STATE_DRAG) {
+                mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            }
+        }
+        final ViewParent rvParent = mRecyclerView.getParent();
+        if (rvParent != null) {
+            rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
+        }
+        if (!preventLayout) {
+            mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
+        }
+        mCallback.onSelectedChanged(mSelected, mActionState);
+        mRecyclerView.invalidate();
+    }
+
+    void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) {
+        // wait until animations are complete.
+        mRecyclerView.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mRecyclerView != null && mRecyclerView.isAttachedToWindow()
+                        && !anim.mOverridden
+                        && anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) {
+                    final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator();
+                    // if animator is running or we have other active recover animations, we try
+                    // not to call onSwiped because DefaultItemAnimator is not good at merging
+                    // animations. Instead, we wait and batch.
+                    if ((animator == null || !animator.isRunning(null))
+                            && !hasRunningRecoverAnim()) {
+                        mCallback.onSwiped(anim.mViewHolder, swipeDir);
+                    } else {
+                        mRecyclerView.post(this);
+                    }
+                }
+            }
+        });
+    }
+
+    boolean hasRunningRecoverAnim() {
+        final int size = mRecoverAnimations.size();
+        for (int i = 0; i < size; i++) {
+            if (!mRecoverAnimations.get(i).mEnded) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * If user drags the view to the edge, trigger a scroll if necessary.
+     */
+    boolean scrollIfNecessary() {
+        if (mSelected == null) {
+            mDragScrollStartTimeInMs = Long.MIN_VALUE;
+            return false;
+        }
+        final long now = System.currentTimeMillis();
+        final long scrollDuration = mDragScrollStartTimeInMs
+                == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs;
+        RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+        if (mTmpRect == null) {
+            mTmpRect = new Rect();
+        }
+        int scrollX = 0;
+        int scrollY = 0;
+        lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect);
+        if (lm.canScrollHorizontally()) {
+            int curX = (int) (mSelectedStartX + mDx);
+            final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft();
+            if (mDx < 0 && leftDiff < 0) {
+                scrollX = leftDiff;
+            } else if (mDx > 0) {
+                final int rightDiff =
+                        curX + mSelected.itemView.getWidth() + mTmpRect.right
+                                - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight());
+                if (rightDiff > 0) {
+                    scrollX = rightDiff;
+                }
+            }
+        }
+        if (lm.canScrollVertically()) {
+            int curY = (int) (mSelectedStartY + mDy);
+            final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop();
+            if (mDy < 0 && topDiff < 0) {
+                scrollY = topDiff;
+            } else if (mDy > 0) {
+                final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom
+                        - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom());
+                if (bottomDiff > 0) {
+                    scrollY = bottomDiff;
+                }
+            }
+        }
+        if (scrollX != 0) {
+            scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
+                    mSelected.itemView.getWidth(), scrollX,
+                    mRecyclerView.getWidth(), scrollDuration);
+        }
+        if (scrollY != 0) {
+            scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
+                    mSelected.itemView.getHeight(), scrollY,
+                    mRecyclerView.getHeight(), scrollDuration);
+        }
+        if (scrollX != 0 || scrollY != 0) {
+            if (mDragScrollStartTimeInMs == Long.MIN_VALUE) {
+                mDragScrollStartTimeInMs = now;
+            }
+            mRecyclerView.scrollBy(scrollX, scrollY);
+            return true;
+        }
+        mDragScrollStartTimeInMs = Long.MIN_VALUE;
+        return false;
+    }
+
+    private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) {
+        if (mSwapTargets == null) {
+            mSwapTargets = new ArrayList<ViewHolder>();
+            mDistances = new ArrayList<Integer>();
+        } else {
+            mSwapTargets.clear();
+            mDistances.clear();
+        }
+        final int margin = mCallback.getBoundingBoxMargin();
+        final int left = Math.round(mSelectedStartX + mDx) - margin;
+        final int top = Math.round(mSelectedStartY + mDy) - margin;
+        final int right = left + viewHolder.itemView.getWidth() + 2 * margin;
+        final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin;
+        final int centerX = (left + right) / 2;
+        final int centerY = (top + bottom) / 2;
+        final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+        final int childCount = lm.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View other = lm.getChildAt(i);
+            if (other == viewHolder.itemView) {
+                continue; //myself!
+            }
+            if (other.getBottom() < top || other.getTop() > bottom
+                    || other.getRight() < left || other.getLeft() > right) {
+                continue;
+            }
+            final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other);
+            if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) {
+                // find the index to add
+                final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2);
+                final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2);
+                final int dist = dx * dx + dy * dy;
+
+                int pos = 0;
+                final int cnt = mSwapTargets.size();
+                for (int j = 0; j < cnt; j++) {
+                    if (dist > mDistances.get(j)) {
+                        pos++;
+                    } else {
+                        break;
+                    }
+                }
+                mSwapTargets.add(pos, otherVh);
+                mDistances.add(pos, dist);
+            }
+        }
+        return mSwapTargets;
+    }
+
+    /**
+     * Checks if we should swap w/ another view holder.
+     */
+    void moveIfNecessary(ViewHolder viewHolder) {
+        if (mRecyclerView.isLayoutRequested()) {
+            return;
+        }
+        if (mActionState != ACTION_STATE_DRAG) {
+            return;
+        }
+
+        final float threshold = mCallback.getMoveThreshold(viewHolder);
+        final int x = (int) (mSelectedStartX + mDx);
+        final int y = (int) (mSelectedStartY + mDy);
+        if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
+                && Math.abs(x - viewHolder.itemView.getLeft())
+                < viewHolder.itemView.getWidth() * threshold) {
+            return;
+        }
+        List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
+        if (swapTargets.size() == 0) {
+            return;
+        }
+        // may swap.
+        ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
+        if (target == null) {
+            mSwapTargets.clear();
+            mDistances.clear();
+            return;
+        }
+        final int toPosition = target.getAdapterPosition();
+        final int fromPosition = viewHolder.getAdapterPosition();
+        if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
+            // keep target visible
+            mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
+                    target, toPosition, x, y);
+        }
+    }
+
+    @Override
+    public void onChildViewAttachedToWindow(View view) {
+    }
+
+    @Override
+    public void onChildViewDetachedFromWindow(View view) {
+        removeChildDrawingOrderCallbackIfNecessary(view);
+        final ViewHolder holder = mRecyclerView.getChildViewHolder(view);
+        if (holder == null) {
+            return;
+        }
+        if (mSelected != null && holder == mSelected) {
+            select(null, ACTION_STATE_IDLE);
+        } else {
+            endRecoverAnimation(holder, false); // this may push it into pending cleanup list.
+            if (mPendingCleanup.remove(holder.itemView)) {
+                mCallback.clearView(mRecyclerView, holder);
+            }
+        }
+    }
+
+    /**
+     * Returns the animation type or 0 if cannot be found.
+     */
+    int endRecoverAnimation(ViewHolder viewHolder, boolean override) {
+        final int recoverAnimSize = mRecoverAnimations.size();
+        for (int i = recoverAnimSize - 1; i >= 0; i--) {
+            final RecoverAnimation anim = mRecoverAnimations.get(i);
+            if (anim.mViewHolder == viewHolder) {
+                anim.mOverridden |= override;
+                if (!anim.mEnded) {
+                    anim.cancel();
+                }
+                mRecoverAnimations.remove(i);
+                return anim.mAnimationType;
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+            RecyclerView.State state) {
+        outRect.setEmpty();
+    }
+
+    void obtainVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+        }
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    private void releaseVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private ViewHolder findSwipedView(MotionEvent motionEvent) {
+        final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+        if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
+            return null;
+        }
+        final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
+        final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX;
+        final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY;
+        final float absDx = Math.abs(dx);
+        final float absDy = Math.abs(dy);
+
+        if (absDx < mSlop && absDy < mSlop) {
+            return null;
+        }
+        if (absDx > absDy && lm.canScrollHorizontally()) {
+            return null;
+        } else if (absDy > absDx && lm.canScrollVertically()) {
+            return null;
+        }
+        View child = findChildView(motionEvent);
+        if (child == null) {
+            return null;
+        }
+        return mRecyclerView.getChildViewHolder(child);
+    }
+
+    /**
+     * Checks whether we should select a View for swiping.
+     */
+    boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
+        if (mSelected != null || action != MotionEvent.ACTION_MOVE
+                || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
+            return false;
+        }
+        if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
+            return false;
+        }
+        final ViewHolder vh = findSwipedView(motionEvent);
+        if (vh == null) {
+            return false;
+        }
+        final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
+
+        final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
+                >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
+
+        if (swipeFlags == 0) {
+            return false;
+        }
+
+        // mDx and mDy are only set in allowed directions. We use custom x/y here instead of
+        // updateDxDy to avoid swiping if user moves more in the other direction
+        final float x = motionEvent.getX(pointerIndex);
+        final float y = motionEvent.getY(pointerIndex);
+
+        // Calculate the distance moved
+        final float dx = x - mInitialTouchX;
+        final float dy = y - mInitialTouchY;
+        // swipe target is chose w/o applying flags so it does not really check if swiping in that
+        // direction is allowed. This why here, we use mDx mDy to check slope value again.
+        final float absDx = Math.abs(dx);
+        final float absDy = Math.abs(dy);
+
+        if (absDx < mSlop && absDy < mSlop) {
+            return false;
+        }
+        if (absDx > absDy) {
+            if (dx < 0 && (swipeFlags & LEFT) == 0) {
+                return false;
+            }
+            if (dx > 0 && (swipeFlags & RIGHT) == 0) {
+                return false;
+            }
+        } else {
+            if (dy < 0 && (swipeFlags & UP) == 0) {
+                return false;
+            }
+            if (dy > 0 && (swipeFlags & DOWN) == 0) {
+                return false;
+            }
+        }
+        mDx = mDy = 0f;
+        mActivePointerId = motionEvent.getPointerId(0);
+        select(vh, ACTION_STATE_SWIPE);
+        return true;
+    }
+
+    View findChildView(MotionEvent event) {
+        // first check elevated views, if none, then call RV
+        final float x = event.getX();
+        final float y = event.getY();
+        if (mSelected != null) {
+            final View selectedView = mSelected.itemView;
+            if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
+                return selectedView;
+            }
+        }
+        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
+            final RecoverAnimation anim = mRecoverAnimations.get(i);
+            final View view = anim.mViewHolder.itemView;
+            if (hitTest(view, x, y, anim.mX, anim.mY)) {
+                return view;
+            }
+        }
+        return mRecyclerView.findChildViewUnder(x, y);
+    }
+
+    /**
+     * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a
+     * View is long pressed. You can disable that behavior by overriding
+     * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}.
+     * <p>
+     * For this method to work:
+     * <ul>
+     * <li>The provided ViewHolder must be a child of the RecyclerView to which this
+     * ItemTouchHelper
+     * is attached.</li>
+     * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li>
+     * <li>There must be a previous touch event that was reported to the ItemTouchHelper
+     * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
+     * grabs previous events, this should work as expected.</li>
+     * </ul>
+     *
+     * For example, if you would like to let your user to be able to drag an Item by touching one
+     * of its descendants, you may implement it as follows:
+     * <pre>
+     *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
+     *         public boolean onTouch(View v, MotionEvent event) {
+     *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+     *                 mItemTouchHelper.startDrag(viewHolder);
+     *             }
+     *             return false;
+     *         }
+     *     });
+     * </pre>
+     * <p>
+     *
+     * @param viewHolder The ViewHolder to start dragging. It must be a direct child of
+     *                   RecyclerView.
+     * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled()
+     */
+    public void startDrag(ViewHolder viewHolder) {
+        if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) {
+            Log.e(TAG, "Start drag has been called but dragging is not enabled");
+            return;
+        }
+        if (viewHolder.itemView.getParent() != mRecyclerView) {
+            Log.e(TAG, "Start drag has been called with a view holder which is not a child of "
+                    + "the RecyclerView which is controlled by this ItemTouchHelper.");
+            return;
+        }
+        obtainVelocityTracker();
+        mDx = mDy = 0f;
+        select(viewHolder, ACTION_STATE_DRAG);
+    }
+
+    /**
+     * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View
+     * when user swipes their finger (or mouse pointer) over the View. You can disable this
+     * behavior
+     * by overriding {@link ItemTouchHelper.Callback}
+     * <p>
+     * For this method to work:
+     * <ul>
+     * <li>The provided ViewHolder must be a child of the RecyclerView to which this
+     * ItemTouchHelper is attached.</li>
+     * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li>
+     * <li>There must be a previous touch event that was reported to the ItemTouchHelper
+     * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
+     * grabs previous events, this should work as expected.</li>
+     * </ul>
+     *
+     * For example, if you would like to let your user to be able to swipe an Item by touching one
+     * of its descendants, you may implement it as follows:
+     * <pre>
+     *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
+     *         public boolean onTouch(View v, MotionEvent event) {
+     *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+     *                 mItemTouchHelper.startSwipe(viewHolder);
+     *             }
+     *             return false;
+     *         }
+     *     });
+     * </pre>
+     *
+     * @param viewHolder The ViewHolder to start swiping. It must be a direct child of
+     *                   RecyclerView.
+     */
+    public void startSwipe(ViewHolder viewHolder) {
+        if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) {
+            Log.e(TAG, "Start swipe has been called but swiping is not enabled");
+            return;
+        }
+        if (viewHolder.itemView.getParent() != mRecyclerView) {
+            Log.e(TAG, "Start swipe has been called with a view holder which is not a child of "
+                    + "the RecyclerView controlled by this ItemTouchHelper.");
+            return;
+        }
+        obtainVelocityTracker();
+        mDx = mDy = 0f;
+        select(viewHolder, ACTION_STATE_SWIPE);
+    }
+
+    RecoverAnimation findAnimation(MotionEvent event) {
+        if (mRecoverAnimations.isEmpty()) {
+            return null;
+        }
+        View target = findChildView(event);
+        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
+            final RecoverAnimation anim = mRecoverAnimations.get(i);
+            if (anim.mViewHolder.itemView == target) {
+                return anim;
+            }
+        }
+        return null;
+    }
+
+    void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
+        final float x = ev.getX(pointerIndex);
+        final float y = ev.getY(pointerIndex);
+
+        // Calculate the distance moved
+        mDx = x - mInitialTouchX;
+        mDy = y - mInitialTouchY;
+        if ((directionFlags & LEFT) == 0) {
+            mDx = Math.max(0, mDx);
+        }
+        if ((directionFlags & RIGHT) == 0) {
+            mDx = Math.min(0, mDx);
+        }
+        if ((directionFlags & UP) == 0) {
+            mDy = Math.max(0, mDy);
+        }
+        if ((directionFlags & DOWN) == 0) {
+            mDy = Math.min(0, mDy);
+        }
+    }
+
+    private int swipeIfNecessary(ViewHolder viewHolder) {
+        if (mActionState == ACTION_STATE_DRAG) {
+            return 0;
+        }
+        final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder);
+        final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection(
+                originalMovementFlags,
+                mRecyclerView.getLayoutDirection());
+        final int flags = (absoluteMovementFlags
+                & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
+        if (flags == 0) {
+            return 0;
+        }
+        final int originalFlags = (originalMovementFlags
+                & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
+        int swipeDir;
+        if (Math.abs(mDx) > Math.abs(mDy)) {
+            if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
+                // if swipe dir is not in original flags, it should be the relative direction
+                if ((originalFlags & swipeDir) == 0) {
+                    // convert to relative
+                    return Callback.convertToRelativeDirection(swipeDir,
+                            mRecyclerView.getLayoutDirection());
+                }
+                return swipeDir;
+            }
+            if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) {
+                return swipeDir;
+            }
+        } else {
+            if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) {
+                return swipeDir;
+            }
+            if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
+                // if swipe dir is not in original flags, it should be the relative direction
+                if ((originalFlags & swipeDir) == 0) {
+                    // convert to relative
+                    return Callback.convertToRelativeDirection(swipeDir,
+                            mRecyclerView.getLayoutDirection());
+                }
+                return swipeDir;
+            }
+        }
+        return 0;
+    }
+
+    private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
+        if ((flags & (LEFT | RIGHT)) != 0) {
+            final int dirFlag = mDx > 0 ? RIGHT : LEFT;
+            if (mVelocityTracker != null && mActivePointerId > -1) {
+                mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
+                        mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
+                final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
+                final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
+                final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT;
+                final float absXVelocity = Math.abs(xVelocity);
+                if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag
+                        && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
+                        && absXVelocity > Math.abs(yVelocity)) {
+                    return velDirFlag;
+                }
+            }
+
+            final float threshold = mRecyclerView.getWidth() * mCallback
+                    .getSwipeThreshold(viewHolder);
+
+            if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) {
+                return dirFlag;
+            }
+        }
+        return 0;
+    }
+
+    private int checkVerticalSwipe(ViewHolder viewHolder, int flags) {
+        if ((flags & (UP | DOWN)) != 0) {
+            final int dirFlag = mDy > 0 ? DOWN : UP;
+            if (mVelocityTracker != null && mActivePointerId > -1) {
+                mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
+                        mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
+                final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
+                final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
+                final int velDirFlag = yVelocity > 0f ? DOWN : UP;
+                final float absYVelocity = Math.abs(yVelocity);
+                if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag
+                        && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
+                        && absYVelocity > Math.abs(xVelocity)) {
+                    return velDirFlag;
+                }
+            }
+
+            final float threshold = mRecyclerView.getHeight() * mCallback
+                    .getSwipeThreshold(viewHolder);
+            if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) {
+                return dirFlag;
+            }
+        }
+        return 0;
+    }
+
+    private void addChildDrawingOrderCallback() {
+        if (Build.VERSION.SDK_INT >= 21) {
+            return; // we use elevation on Lollipop
+        }
+        if (mChildDrawingOrderCallback == null) {
+            mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
+                @Override
+                public int onGetChildDrawingOrder(int childCount, int i) {
+                    if (mOverdrawChild == null) {
+                        return i;
+                    }
+                    int childPosition = mOverdrawChildPosition;
+                    if (childPosition == -1) {
+                        childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
+                        mOverdrawChildPosition = childPosition;
+                    }
+                    if (i == childCount - 1) {
+                        return childPosition;
+                    }
+                    return i < childPosition ? i : i + 1;
+                }
+            };
+        }
+        mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
+    }
+
+    void removeChildDrawingOrderCallbackIfNecessary(View view) {
+        if (view == mOverdrawChild) {
+            mOverdrawChild = null;
+            // only remove if we've added
+            if (mChildDrawingOrderCallback != null) {
+                mRecyclerView.setChildDrawingOrderCallback(null);
+            }
+        }
+    }
+
+    /**
+     * An interface which can be implemented by LayoutManager for better integration with
+     * {@link ItemTouchHelper}.
+     */
+    public interface ViewDropHandler {
+
+        /**
+         * Called by the {@link ItemTouchHelper} after a View is dropped over another View.
+         * <p>
+         * A LayoutManager should implement this interface to get ready for the upcoming move
+         * operation.
+         * <p>
+         * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that
+         * the View under drag will be used as an anchor View while calculating the next layout,
+         * making layout stay consistent.
+         *
+         * @param view   The View which is being dragged. It is very likely that user is still
+         *               dragging this View so there might be other
+         *               {@link #prepareForDrop(View, View, int, int)} after this one.
+         * @param target The target view which is being dropped on.
+         * @param x      The <code>left</code> offset of the View that is being dragged. This value
+         *               includes the movement caused by the user.
+         * @param y      The <code>top</code> offset of the View that is being dragged. This value
+         *               includes the movement caused by the user.
+         */
+        void prepareForDrop(View view, View target, int x, int y);
+    }
+
+    /**
+     * This class is the contract between ItemTouchHelper and your application. It lets you control
+     * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user
+     * performs these actions.
+     * <p>
+     * To control which actions user can take on each view, you should override
+     * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set
+     * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END},
+     * {@link #UP}, {@link #DOWN}). You can use
+     * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use
+     * {@link SimpleCallback}.
+     * <p>
+     * If user drags an item, ItemTouchHelper will call
+     * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)
+     * onMove(recyclerView, dragged, target)}.
+     * Upon receiving this callback, you should move the item from the old position
+     * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()})
+     * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}.
+     * To control where a View can be dropped, you can override
+     * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a
+     * dragging View overlaps multiple other views, Callback chooses the closest View with which
+     * dragged View might have changed positions. Although this approach works for many use cases,
+     * if you have a custom LayoutManager, you can override
+     * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a
+     * custom drop target.
+     * <p>
+     * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls
+     * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your
+     * adapter (e.g. remove the item) and call related Adapter#notify event.
+     */
+    @SuppressWarnings("UnusedParameters")
+    public abstract static class Callback {
+
+        public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200;
+
+        public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250;
+
+        static final int RELATIVE_DIR_FLAGS = START | END
+                | ((START | END) << DIRECTION_FLAG_COUNT)
+                | ((START | END) << (2 * DIRECTION_FLAG_COUNT));
+
+        private static final ItemTouchUIUtil sUICallback = new ItemTouchUIUtilImpl();
+
+        private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT
+                | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT)
+                | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT));
+
+        private static final Interpolator sDragScrollInterpolator = new Interpolator() {
+            @Override
+            public float getInterpolation(float t) {
+                return t * t * t * t * t;
+            }
+        };
+
+        private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() {
+            @Override
+            public float getInterpolation(float t) {
+                t -= 1.0f;
+                return t * t * t * t * t + 1.0f;
+            }
+        };
+
+        /**
+         * Drag scroll speed keeps accelerating until this many milliseconds before being capped.
+         */
+        private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
+
+        private int mCachedMaxScrollSpeed = -1;
+
+        /**
+         * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for
+         * visual
+         * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different
+         * implementations for different platform versions.
+         * <p>
+         * By default, {@link Callback} applies these changes on
+         * {@link RecyclerView.ViewHolder#itemView}.
+         * <p>
+         * For example, if you have a use case where you only want the text to move when user
+         * swipes over the view, you can do the following:
+         * <pre>
+         *     public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
+         *         getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView);
+         *     }
+         *     public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+         *         if (viewHolder != null){
+         *             getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView);
+         *         }
+         *     }
+         *     public void onChildDraw(Canvas c, RecyclerView recyclerView,
+         *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
+         *             boolean isCurrentlyActive) {
+         *         getDefaultUIUtil().onDraw(c, recyclerView,
+         *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
+         *                 actionState, isCurrentlyActive);
+         *         return true;
+         *     }
+         *     public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
+         *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
+         *             boolean isCurrentlyActive) {
+         *         getDefaultUIUtil().onDrawOver(c, recyclerView,
+         *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
+         *                 actionState, isCurrentlyActive);
+         *         return true;
+         *     }
+         * </pre>
+         *
+         * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback}
+         */
+        public static ItemTouchUIUtil getDefaultUIUtil() {
+            return sUICallback;
+        }
+
+        /**
+         * Replaces a movement direction with its relative version by taking layout direction into
+         * account.
+         *
+         * @param flags           The flag value that include any number of movement flags.
+         * @param layoutDirection The layout direction of the View. Can be obtained from
+         *                        {@link View#getLayoutDirection()}.
+         * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead
+         * of {@link #LEFT}, {@link #RIGHT}.
+         * @see #convertToAbsoluteDirection(int, int)
+         */
+        public static int convertToRelativeDirection(int flags, int layoutDirection) {
+            int masked = flags & ABS_HORIZONTAL_DIR_FLAGS;
+            if (masked == 0) {
+                return flags; // does not have any abs flags, good.
+            }
+            flags &= ~masked; //remove left / right.
+            if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+                // no change. just OR with 2 bits shifted mask and return
+                flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT.
+                return flags;
+            } else {
+                // add RIGHT flag as START
+                flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS);
+                // first clean RIGHT bit then add LEFT flag as END
+                flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2;
+            }
+            return flags;
+        }
+
+        /**
+         * Convenience method to create movement flags.
+         * <p>
+         * For instance, if you want to let your items be drag & dropped vertically and swiped
+         * left to be dismissed, you can call this method with:
+         * <code>makeMovementFlags(UP | DOWN, LEFT);</code>
+         *
+         * @param dragFlags  The directions in which the item can be dragged.
+         * @param swipeFlags The directions in which the item can be swiped.
+         * @return Returns an integer composed of the given drag and swipe flags.
+         */
+        public static int makeMovementFlags(int dragFlags, int swipeFlags) {
+            return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
+                    | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
+                    | makeFlag(ACTION_STATE_DRAG, dragFlags);
+        }
+
+        /**
+         * Shifts the given direction flags to the offset of the given action state.
+         *
+         * @param actionState The action state you want to get flags in. Should be one of
+         *                    {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or
+         *                    {@link #ACTION_STATE_DRAG}.
+         * @param directions  The direction flags. Can be composed from {@link #UP}, {@link #DOWN},
+         *                    {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}.
+         * @return And integer that represents the given directions in the provided actionState.
+         */
+        public static int makeFlag(int actionState, int directions) {
+            return directions << (actionState * DIRECTION_FLAG_COUNT);
+        }
+
+        /**
+         * Should return a composite flag which defines the enabled move directions in each state
+         * (idle, swiping, dragging).
+         * <p>
+         * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int,
+         * int)}
+         * or {@link #makeFlag(int, int)}.
+         * <p>
+         * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next
+         * 8 bits are for SWIPE state and third 8 bits are for DRAG state.
+         * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in
+         * {@link ItemTouchHelper}.
+         * <p>
+         * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to
+         * swipe by swiping RIGHT, you can return:
+         * <pre>
+         *      makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
+         * </pre>
+         * This means, allow right movement while IDLE and allow right and left movement while
+         * swiping.
+         *
+         * @param recyclerView The RecyclerView to which ItemTouchHelper is attached.
+         * @param viewHolder   The ViewHolder for which the movement information is necessary.
+         * @return flags specifying which movements are allowed on this ViewHolder.
+         * @see #makeMovementFlags(int, int)
+         * @see #makeFlag(int, int)
+         */
+        public abstract int getMovementFlags(RecyclerView recyclerView,
+                ViewHolder viewHolder);
+
+        /**
+         * Converts a given set of flags to absolution direction which means {@link #START} and
+         * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout
+         * direction.
+         *
+         * @param flags           The flag value that include any number of movement flags.
+         * @param layoutDirection The layout direction of the RecyclerView.
+         * @return Updated flags which includes only absolute direction values.
+         */
+        public int convertToAbsoluteDirection(int flags, int layoutDirection) {
+            int masked = flags & RELATIVE_DIR_FLAGS;
+            if (masked == 0) {
+                return flags; // does not have any relative flags, good.
+            }
+            flags &= ~masked; //remove start / end
+            if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+                // no change. just OR with 2 bits shifted mask and return
+                flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT.
+                return flags;
+            } else {
+                // add START flag as RIGHT
+                flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS);
+                // first clean start bit then add END flag as LEFT
+                flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2;
+            }
+            return flags;
+        }
+
+        final int getAbsoluteMovementFlags(RecyclerView recyclerView,
+                ViewHolder viewHolder) {
+            final int flags = getMovementFlags(recyclerView, viewHolder);
+            return convertToAbsoluteDirection(flags, recyclerView.getLayoutDirection());
+        }
+
+        boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) {
+            final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+            return (flags & ACTION_MODE_DRAG_MASK) != 0;
+        }
+
+        boolean hasSwipeFlag(RecyclerView recyclerView,
+                ViewHolder viewHolder) {
+            final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+            return (flags & ACTION_MODE_SWIPE_MASK) != 0;
+        }
+
+        /**
+         * Return true if the current ViewHolder can be dropped over the the target ViewHolder.
+         * <p>
+         * This method is used when selecting drop target for the dragged View. After Views are
+         * eliminated either via bounds check or via this method, resulting set of views will be
+         * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}.
+         * <p>
+         * Default implementation returns true.
+         *
+         * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
+         * @param current      The ViewHolder that user is dragging.
+         * @param target       The ViewHolder which is below the dragged ViewHolder.
+         * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false
+         * otherwise.
+         */
+        public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
+                ViewHolder target) {
+            return true;
+        }
+
+        /**
+         * Called when ItemTouchHelper wants to move the dragged item from its old position to
+         * the new position.
+         * <p>
+         * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved
+         * to the adapter position of {@code target} ViewHolder
+         * ({@link ViewHolder#getAdapterPosition()
+         * ViewHolder#getAdapterPosition()}).
+         * <p>
+         * If you don't support drag & drop, this method will never be called.
+         *
+         * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
+         * @param viewHolder   The ViewHolder which is being dragged by the user.
+         * @param target       The ViewHolder over which the currently active item is being
+         *                     dragged.
+         * @return True if the {@code viewHolder} has been moved to the adapter position of
+         * {@code target}.
+         * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
+         */
+        public abstract boolean onMove(RecyclerView recyclerView,
+                ViewHolder viewHolder, ViewHolder target);
+
+        /**
+         * Returns whether ItemTouchHelper should start a drag and drop operation if an item is
+         * long pressed.
+         * <p>
+         * Default value returns true but you may want to disable this if you want to start
+         * dragging on a custom view touch using {@link #startDrag(ViewHolder)}.
+         *
+         * @return True if ItemTouchHelper should start dragging an item when it is long pressed,
+         * false otherwise. Default value is <code>true</code>.
+         * @see #startDrag(ViewHolder)
+         */
+        public boolean isLongPressDragEnabled() {
+            return true;
+        }
+
+        /**
+         * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped
+         * over the View.
+         * <p>
+         * Default value returns true but you may want to disable this if you want to start
+         * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}.
+         *
+         * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer
+         * over the View, false otherwise. Default value is <code>true</code>.
+         * @see #startSwipe(ViewHolder)
+         */
+        public boolean isItemViewSwipeEnabled() {
+            return true;
+        }
+
+        /**
+         * When finding views under a dragged view, by default, ItemTouchHelper searches for views
+         * that overlap with the dragged View. By overriding this method, you can extend or shrink
+         * the search box.
+         *
+         * @return The extra margin to be added to the hit box of the dragged View.
+         */
+        public int getBoundingBoxMargin() {
+            return 0;
+        }
+
+        /**
+         * Returns the fraction that the user should move the View to be considered as swiped.
+         * The fraction is calculated with respect to RecyclerView's bounds.
+         * <p>
+         * Default value is .5f, which means, to swipe a View, user must move the View at least
+         * half of RecyclerView's width or height, depending on the swipe direction.
+         *
+         * @param viewHolder The ViewHolder that is being dragged.
+         * @return A float value that denotes the fraction of the View size. Default value
+         * is .5f .
+         */
+        public float getSwipeThreshold(ViewHolder viewHolder) {
+            return .5f;
+        }
+
+        /**
+         * Returns the fraction that the user should move the View to be considered as it is
+         * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views
+         * below it for a possible drop.
+         *
+         * @param viewHolder The ViewHolder that is being dragged.
+         * @return A float value that denotes the fraction of the View size. Default value is
+         * .5f .
+         */
+        public float getMoveThreshold(ViewHolder viewHolder) {
+            return .5f;
+        }
+
+        /**
+         * Defines the minimum velocity which will be considered as a swipe action by the user.
+         * <p>
+         * You can increase this value to make it harder to swipe or decrease it to make it easier.
+         * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure
+         * current direction velocity is larger then the perpendicular one. Otherwise, user's
+         * movement is ambiguous. You can change the threshold by overriding
+         * {@link #getSwipeVelocityThreshold(float)}.
+         * <p>
+         * The velocity is calculated in pixels per second.
+         * <p>
+         * The default framework value is passed as a parameter so that you can modify it with a
+         * multiplier.
+         *
+         * @param defaultValue The default value (in pixels per second) used by the
+         *                     ItemTouchHelper.
+         * @return The minimum swipe velocity. The default implementation returns the
+         * <code>defaultValue</code> parameter.
+         * @see #getSwipeVelocityThreshold(float)
+         * @see #getSwipeThreshold(ViewHolder)
+         */
+        public float getSwipeEscapeVelocity(float defaultValue) {
+            return defaultValue;
+        }
+
+        /**
+         * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements.
+         * <p>
+         * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the
+         * perpendicular movement. If both directions reach to the max threshold, none of them will
+         * be considered as a swipe because it is usually an indication that user rather tried to
+         * scroll then swipe.
+         * <p>
+         * The velocity is calculated in pixels per second.
+         * <p>
+         * You can customize this behavior by changing this method. If you increase the value, it
+         * will be easier for the user to swipe diagonally and if you decrease the value, user will
+         * need to make a rather straight finger movement to trigger a swipe.
+         *
+         * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper.
+         * @return The velocity cap for pointer movements. The default implementation returns the
+         * <code>defaultValue</code> parameter.
+         * @see #getSwipeEscapeVelocity(float)
+         */
+        public float getSwipeVelocityThreshold(float defaultValue) {
+            return defaultValue;
+        }
+
+        /**
+         * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that
+         * are under the dragged View.
+         * <p>
+         * Default implementation filters the View with which dragged item have changed position
+         * in the drag direction. For instance, if the view is dragged UP, it compares the
+         * <code>view.getTop()</code> of the two views before and after drag started. If that value
+         * is different, the target view passes the filter.
+         * <p>
+         * Among these Views which pass the test, the one closest to the dragged view is chosen.
+         * <p>
+         * This method is called on the main thread every time user moves the View. If you want to
+         * override it, make sure it does not do any expensive operations.
+         *
+         * @param selected    The ViewHolder being dragged by the user.
+         * @param dropTargets The list of ViewHolder that are under the dragged View and
+         *                    candidate as a drop.
+         * @param curX        The updated left value of the dragged View after drag translations
+         *                    are applied. This value does not include margins added by
+         *                    {@link RecyclerView.ItemDecoration}s.
+         * @param curY        The updated top value of the dragged View after drag translations
+         *                    are applied. This value does not include margins added by
+         *                    {@link RecyclerView.ItemDecoration}s.
+         * @return A ViewHolder to whose position the dragged ViewHolder should be
+         * moved to.
+         */
+        public ViewHolder chooseDropTarget(ViewHolder selected,
+                List<ViewHolder> dropTargets, int curX, int curY) {
+            int right = curX + selected.itemView.getWidth();
+            int bottom = curY + selected.itemView.getHeight();
+            ViewHolder winner = null;
+            int winnerScore = -1;
+            final int dx = curX - selected.itemView.getLeft();
+            final int dy = curY - selected.itemView.getTop();
+            final int targetsSize = dropTargets.size();
+            for (int i = 0; i < targetsSize; i++) {
+                final ViewHolder target = dropTargets.get(i);
+                if (dx > 0) {
+                    int diff = target.itemView.getRight() - right;
+                    if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) {
+                        final int score = Math.abs(diff);
+                        if (score > winnerScore) {
+                            winnerScore = score;
+                            winner = target;
+                        }
+                    }
+                }
+                if (dx < 0) {
+                    int diff = target.itemView.getLeft() - curX;
+                    if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) {
+                        final int score = Math.abs(diff);
+                        if (score > winnerScore) {
+                            winnerScore = score;
+                            winner = target;
+                        }
+                    }
+                }
+                if (dy < 0) {
+                    int diff = target.itemView.getTop() - curY;
+                    if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) {
+                        final int score = Math.abs(diff);
+                        if (score > winnerScore) {
+                            winnerScore = score;
+                            winner = target;
+                        }
+                    }
+                }
+
+                if (dy > 0) {
+                    int diff = target.itemView.getBottom() - bottom;
+                    if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) {
+                        final int score = Math.abs(diff);
+                        if (score > winnerScore) {
+                            winnerScore = score;
+                            winner = target;
+                        }
+                    }
+                }
+            }
+            return winner;
+        }
+
+        /**
+         * Called when a ViewHolder is swiped by the user.
+         * <p>
+         * If you are returning relative directions ({@link #START} , {@link #END}) from the
+         * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method
+         * will also use relative directions. Otherwise, it will use absolute directions.
+         * <p>
+         * If you don't support swiping, this method will never be called.
+         * <p>
+         * ItemTouchHelper will keep a reference to the View until it is detached from
+         * RecyclerView.
+         * As soon as it is detached, ItemTouchHelper will call
+         * {@link #clearView(RecyclerView, ViewHolder)}.
+         *
+         * @param viewHolder The ViewHolder which has been swiped by the user.
+         * @param direction  The direction to which the ViewHolder is swiped. It is one of
+         *                   {@link #UP}, {@link #DOWN},
+         *                   {@link #LEFT} or {@link #RIGHT}. If your
+         *                   {@link #getMovementFlags(RecyclerView, ViewHolder)}
+         *                   method
+         *                   returned relative flags instead of {@link #LEFT} / {@link #RIGHT};
+         *                   `direction` will be relative as well. ({@link #START} or {@link
+         *                   #END}).
+         */
+        public abstract void onSwiped(ViewHolder viewHolder, int direction);
+
+        /**
+         * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed.
+         * <p/>
+         * If you override this method, you should call super.
+         *
+         * @param viewHolder  The new ViewHolder that is being swiped or dragged. Might be null if
+         *                    it is cleared.
+         * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE},
+         *                    {@link ItemTouchHelper#ACTION_STATE_SWIPE} or
+         *                    {@link ItemTouchHelper#ACTION_STATE_DRAG}.
+         * @see #clearView(RecyclerView, RecyclerView.ViewHolder)
+         */
+        public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
+            if (viewHolder != null) {
+                sUICallback.onSelected(viewHolder.itemView);
+            }
+        }
+
+        private int getMaxDragScroll(RecyclerView recyclerView) {
+            if (mCachedMaxScrollSpeed == -1) {
+                mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize(
+                        R.dimen.item_touch_helper_max_drag_scroll_per_frame);
+            }
+            return mCachedMaxScrollSpeed;
+        }
+
+        /**
+         * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true.
+         * <p>
+         * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it
+         * modifies the existing View. Because of this reason, it is important that the View is
+         * still part of the layout after it is moved. This may not work as intended when swapped
+         * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views
+         * which were not eligible for dropping over).
+         * <p>
+         * This method is responsible to give necessary hint to the LayoutManager so that it will
+         * keep the View in visible area. For example, for LinearLayoutManager, this is as simple
+         * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}.
+         *
+         * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's
+         * new position is likely to be out of bounds.
+         * <p>
+         * It is important to ensure the ViewHolder will stay visible as otherwise, it might be
+         * removed by the LayoutManager if the move causes the View to go out of bounds. In that
+         * case, drag will end prematurely.
+         *
+         * @param recyclerView The RecyclerView controlled by the ItemTouchHelper.
+         * @param viewHolder   The ViewHolder under user's control.
+         * @param fromPos      The previous adapter position of the dragged item (before it was
+         *                     moved).
+         * @param target       The ViewHolder on which the currently active item has been dropped.
+         * @param toPos        The new adapter position of the dragged item.
+         * @param x            The updated left value of the dragged View after drag translations
+         *                     are applied. This value does not include margins added by
+         *                     {@link RecyclerView.ItemDecoration}s.
+         * @param y            The updated top value of the dragged View after drag translations
+         *                     are applied. This value does not include margins added by
+         *                     {@link RecyclerView.ItemDecoration}s.
+         */
+        public void onMoved(final RecyclerView recyclerView,
+                final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x,
+                int y) {
+            final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+            if (layoutManager instanceof ViewDropHandler) {
+                ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView,
+                        target.itemView, x, y);
+                return;
+            }
+
+            // if layout manager cannot handle it, do some guesswork
+            if (layoutManager.canScrollHorizontally()) {
+                final int minLeft = layoutManager.getDecoratedLeft(target.itemView);
+                if (minLeft <= recyclerView.getPaddingLeft()) {
+                    recyclerView.scrollToPosition(toPos);
+                }
+                final int maxRight = layoutManager.getDecoratedRight(target.itemView);
+                if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) {
+                    recyclerView.scrollToPosition(toPos);
+                }
+            }
+
+            if (layoutManager.canScrollVertically()) {
+                final int minTop = layoutManager.getDecoratedTop(target.itemView);
+                if (minTop <= recyclerView.getPaddingTop()) {
+                    recyclerView.scrollToPosition(toPos);
+                }
+                final int maxBottom = layoutManager.getDecoratedBottom(target.itemView);
+                if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) {
+                    recyclerView.scrollToPosition(toPos);
+                }
+            }
+        }
+
+        void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
+                List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
+                int actionState, float dX, float dY) {
+            final int recoverAnimSize = recoverAnimationList.size();
+            for (int i = 0; i < recoverAnimSize; i++) {
+                final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
+                anim.update();
+                final int count = c.save();
+                onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
+                        false);
+                c.restoreToCount(count);
+            }
+            if (selected != null) {
+                final int count = c.save();
+                onChildDraw(c, parent, selected, dX, dY, actionState, true);
+                c.restoreToCount(count);
+            }
+        }
+
+        void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected,
+                List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
+                int actionState, float dX, float dY) {
+            final int recoverAnimSize = recoverAnimationList.size();
+            for (int i = 0; i < recoverAnimSize; i++) {
+                final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
+                final int count = c.save();
+                onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
+                        false);
+                c.restoreToCount(count);
+            }
+            if (selected != null) {
+                final int count = c.save();
+                onChildDrawOver(c, parent, selected, dX, dY, actionState, true);
+                c.restoreToCount(count);
+            }
+            boolean hasRunningAnimation = false;
+            for (int i = recoverAnimSize - 1; i >= 0; i--) {
+                final RecoverAnimation anim = recoverAnimationList.get(i);
+                if (anim.mEnded && !anim.mIsPendingCleanup) {
+                    recoverAnimationList.remove(i);
+                } else if (!anim.mEnded) {
+                    hasRunningAnimation = true;
+                }
+            }
+            if (hasRunningAnimation) {
+                parent.invalidate();
+            }
+        }
+
+        /**
+         * Called by the ItemTouchHelper when the user interaction with an element is over and it
+         * also completed its animation.
+         * <p>
+         * This is a good place to clear all changes on the View that was done in
+         * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)},
+         * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
+         * boolean)} or
+         * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}.
+         *
+         * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper.
+         * @param viewHolder   The View that was interacted by the user.
+         */
+        public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
+            sUICallback.clearView(viewHolder.itemView);
+        }
+
+        /**
+         * Called by ItemTouchHelper on RecyclerView's onDraw callback.
+         * <p>
+         * If you would like to customize how your View's respond to user interactions, this is
+         * a good place to override.
+         * <p>
+         * Default implementation translates the child by the given <code>dX</code>,
+         * <code>dY</code>.
+         * ItemTouchHelper also takes care of drawing the child after other children if it is being
+         * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
+         * is
+         * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
+         * and after, it changes View's elevation value to be greater than all other children.)
+         *
+         * @param c                 The canvas which RecyclerView is drawing its children
+         * @param recyclerView      The RecyclerView to which ItemTouchHelper is attached to
+         * @param viewHolder        The ViewHolder which is being interacted by the User or it was
+         *                          interacted and simply animating to its original position
+         * @param dX                The amount of horizontal displacement caused by user's action
+         * @param dY                The amount of vertical displacement caused by user's action
+         * @param actionState       The type of interaction on the View. Is either {@link
+         *                          #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
+         * @param isCurrentlyActive True if this view is currently being controlled by the user or
+         *                          false it is simply animating back to its original state.
+         * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
+         * boolean)
+         */
+        public void onChildDraw(Canvas c, RecyclerView recyclerView,
+                ViewHolder viewHolder,
+                float dX, float dY, int actionState, boolean isCurrentlyActive) {
+            sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
+                    isCurrentlyActive);
+        }
+
+        /**
+         * Called by ItemTouchHelper on RecyclerView's onDraw callback.
+         * <p>
+         * If you would like to customize how your View's respond to user interactions, this is
+         * a good place to override.
+         * <p>
+         * Default implementation translates the child by the given <code>dX</code>,
+         * <code>dY</code>.
+         * ItemTouchHelper also takes care of drawing the child after other children if it is being
+         * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
+         * is
+         * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
+         * and after, it changes View's elevation value to be greater than all other children.)
+         *
+         * @param c                 The canvas which RecyclerView is drawing its children
+         * @param recyclerView      The RecyclerView to which ItemTouchHelper is attached to
+         * @param viewHolder        The ViewHolder which is being interacted by the User or it was
+         *                          interacted and simply animating to its original position
+         * @param dX                The amount of horizontal displacement caused by user's action
+         * @param dY                The amount of vertical displacement caused by user's action
+         * @param actionState       The type of interaction on the View. Is either {@link
+         *                          #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
+         * @param isCurrentlyActive True if this view is currently being controlled by the user or
+         *                          false it is simply animating back to its original state.
+         * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
+         * boolean)
+         */
+        public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
+                ViewHolder viewHolder,
+                float dX, float dY, int actionState, boolean isCurrentlyActive) {
+            sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
+                    isCurrentlyActive);
+        }
+
+        /**
+         * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View
+         * will be animated to its final position.
+         * <p>
+         * Default implementation uses ItemAnimator's duration values. If
+         * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns
+         * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns
+         * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have
+         * any {@link RecyclerView.ItemAnimator} attached, this method returns
+         * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION}
+         * depending on the animation type.
+         *
+         * @param recyclerView  The RecyclerView to which the ItemTouchHelper is attached to.
+         * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG},
+         *                      {@link #ANIMATION_TYPE_SWIPE_CANCEL} or
+         *                      {@link #ANIMATION_TYPE_SWIPE_SUCCESS}.
+         * @param animateDx     The horizontal distance that the animation will offset
+         * @param animateDy     The vertical distance that the animation will offset
+         * @return The duration for the animation
+         */
+        public long getAnimationDuration(RecyclerView recyclerView, int animationType,
+                float animateDx, float animateDy) {
+            final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator();
+            if (itemAnimator == null) {
+                return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION
+                        : DEFAULT_SWIPE_ANIMATION_DURATION;
+            } else {
+                return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration()
+                        : itemAnimator.getRemoveDuration();
+            }
+        }
+
+        /**
+         * Called by the ItemTouchHelper when user is dragging a view out of bounds.
+         * <p>
+         * You can override this method to decide how much RecyclerView should scroll in response
+         * to this action. Default implementation calculates a value based on the amount of View
+         * out of bounds and the time it spent there. The longer user keeps the View out of bounds,
+         * the faster the list will scroll. Similarly, the larger portion of the View is out of
+         * bounds, the faster the RecyclerView will scroll.
+         *
+         * @param recyclerView        The RecyclerView instance to which ItemTouchHelper is
+         *                            attached to.
+         * @param viewSize            The total size of the View in scroll direction, excluding
+         *                            item decorations.
+         * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value
+         *                            is negative if the View is dragged towards left or top edge.
+         * @param totalSize           The total size of RecyclerView in the scroll direction.
+         * @param msSinceStartScroll  The time passed since View is kept out of bounds.
+         * @return The amount that RecyclerView should scroll. Keep in mind that this value will
+         * be passed to {@link RecyclerView#scrollBy(int, int)} method.
+         */
+        public int interpolateOutOfBoundsScroll(RecyclerView recyclerView,
+                int viewSize, int viewSizeOutOfBounds,
+                int totalSize, long msSinceStartScroll) {
+            final int maxScroll = getMaxDragScroll(recyclerView);
+            final int absOutOfBounds = Math.abs(viewSizeOutOfBounds);
+            final int direction = (int) Math.signum(viewSizeOutOfBounds);
+            // might be negative if other direction
+            float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize);
+            final int cappedScroll = (int) (direction * maxScroll
+                    * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio));
+            final float timeRatio;
+            if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) {
+                timeRatio = 1f;
+            } else {
+                timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS;
+            }
+            final int value = (int) (cappedScroll * sDragScrollInterpolator
+                    .getInterpolation(timeRatio));
+            if (value == 0) {
+                return viewSizeOutOfBounds > 0 ? 1 : -1;
+            }
+            return value;
+        }
+    }
+
+    /**
+     * A simple wrapper to the default Callback which you can construct with drag and swipe
+     * directions and this class will handle the flag callbacks. You should still override onMove
+     * or
+     * onSwiped depending on your use case.
+     *
+     * <pre>
+     * ItemTouchHelper mIth = new ItemTouchHelper(
+     *     new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
+     *         ItemTouchHelper.LEFT) {
+     *         public abstract boolean onMove(RecyclerView recyclerView,
+     *             ViewHolder viewHolder, ViewHolder target) {
+     *             final int fromPos = viewHolder.getAdapterPosition();
+     *             final int toPos = target.getAdapterPosition();
+     *             // move item in `fromPos` to `toPos` in adapter.
+     *             return true;// true if moved, false otherwise
+     *         }
+     *         public void onSwiped(ViewHolder viewHolder, int direction) {
+     *             // remove from adapter
+     *         }
+     * });
+     * </pre>
+     */
+    public abstract static class SimpleCallback extends Callback {
+
+        private int mDefaultSwipeDirs;
+
+        private int mDefaultDragDirs;
+
+        /**
+         * Creates a Callback for the given drag and swipe allowance. These values serve as
+         * defaults
+         * and if you want to customize behavior per ViewHolder, you can override
+         * {@link #getSwipeDirs(RecyclerView, ViewHolder)}
+         * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}.
+         *
+         * @param dragDirs  Binary OR of direction flags in which the Views can be dragged. Must be
+         *                  composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link
+         *                  #END},
+         *                  {@link #UP} and {@link #DOWN}.
+         * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be
+         *                  composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link
+         *                  #END},
+         *                  {@link #UP} and {@link #DOWN}.
+         */
+        public SimpleCallback(int dragDirs, int swipeDirs) {
+            mDefaultSwipeDirs = swipeDirs;
+            mDefaultDragDirs = dragDirs;
+        }
+
+        /**
+         * Updates the default swipe directions. For example, you can use this method to toggle
+         * certain directions depending on your use case.
+         *
+         * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped.
+         */
+        public void setDefaultSwipeDirs(int defaultSwipeDirs) {
+            mDefaultSwipeDirs = defaultSwipeDirs;
+        }
+
+        /**
+         * Updates the default drag directions. For example, you can use this method to toggle
+         * certain directions depending on your use case.
+         *
+         * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged.
+         */
+        public void setDefaultDragDirs(int defaultDragDirs) {
+            mDefaultDragDirs = defaultDragDirs;
+        }
+
+        /**
+         * Returns the swipe directions for the provided ViewHolder.
+         * Default implementation returns the swipe directions that was set via constructor or
+         * {@link #setDefaultSwipeDirs(int)}.
+         *
+         * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
+         * @param viewHolder   The RecyclerView for which the swipe direction is queried.
+         * @return A binary OR of direction flags.
+         */
+        public int getSwipeDirs(RecyclerView recyclerView, ViewHolder viewHolder) {
+            return mDefaultSwipeDirs;
+        }
+
+        /**
+         * Returns the drag directions for the provided ViewHolder.
+         * Default implementation returns the drag directions that was set via constructor or
+         * {@link #setDefaultDragDirs(int)}.
+         *
+         * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
+         * @param viewHolder   The RecyclerView for which the swipe direction is queried.
+         * @return A binary OR of direction flags.
+         */
+        public int getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder) {
+            return mDefaultDragDirs;
+        }
+
+        @Override
+        public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
+            return makeMovementFlags(getDragDirs(recyclerView, viewHolder),
+                    getSwipeDirs(recyclerView, viewHolder));
+        }
+    }
+
+    private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
+
+        ItemTouchHelperGestureListener() {
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            return true;
+        }
+
+        @Override
+        public void onLongPress(MotionEvent e) {
+            View child = findChildView(e);
+            if (child != null) {
+                ViewHolder vh = mRecyclerView.getChildViewHolder(child);
+                if (vh != null) {
+                    if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
+                        return;
+                    }
+                    int pointerId = e.getPointerId(0);
+                    // Long press is deferred.
+                    // Check w/ active pointer id to avoid selecting after motion
+                    // event is canceled.
+                    if (pointerId == mActivePointerId) {
+                        final int index = e.findPointerIndex(mActivePointerId);
+                        final float x = e.getX(index);
+                        final float y = e.getY(index);
+                        mInitialTouchX = x;
+                        mInitialTouchY = y;
+                        mDx = mDy = 0f;
+                        if (DEBUG) {
+                            Log.d(TAG,
+                                    "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
+                        }
+                        if (mCallback.isLongPressDragEnabled()) {
+                            select(vh, ACTION_STATE_DRAG);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private class RecoverAnimation implements Animator.AnimatorListener {
+
+        final float mStartDx;
+
+        final float mStartDy;
+
+        final float mTargetX;
+
+        final float mTargetY;
+
+        final ViewHolder mViewHolder;
+
+        final int mActionState;
+
+        private final ValueAnimator mValueAnimator;
+
+        final int mAnimationType;
+
+        public boolean mIsPendingCleanup;
+
+        float mX;
+
+        float mY;
+
+        // if user starts touching a recovering view, we put it into interaction mode again,
+        // instantly.
+        boolean mOverridden = false;
+
+        boolean mEnded = false;
+
+        private float mFraction;
+
+        RecoverAnimation(ViewHolder viewHolder, int animationType,
+                int actionState, float startDx, float startDy, float targetX, float targetY) {
+            mActionState = actionState;
+            mAnimationType = animationType;
+            mViewHolder = viewHolder;
+            mStartDx = startDx;
+            mStartDy = startDy;
+            mTargetX = targetX;
+            mTargetY = targetY;
+            mValueAnimator = ValueAnimator.ofFloat(0f, 1f);
+            mValueAnimator.addUpdateListener(
+                    new ValueAnimator.AnimatorUpdateListener() {
+                        @Override
+                        public void onAnimationUpdate(ValueAnimator animation) {
+                            setFraction(animation.getAnimatedFraction());
+                        }
+                    });
+            mValueAnimator.setTarget(viewHolder.itemView);
+            mValueAnimator.addListener(this);
+            setFraction(0f);
+        }
+
+        public void setDuration(long duration) {
+            mValueAnimator.setDuration(duration);
+        }
+
+        public void start() {
+            mViewHolder.setIsRecyclable(false);
+            mValueAnimator.start();
+        }
+
+        public void cancel() {
+            mValueAnimator.cancel();
+        }
+
+        public void setFraction(float fraction) {
+            mFraction = fraction;
+        }
+
+        /**
+         * We run updates on onDraw method but use the fraction from animator callback.
+         * This way, we can sync translate x/y values w/ the animators to avoid one-off frames.
+         */
+        public void update() {
+            if (mStartDx == mTargetX) {
+                mX = mViewHolder.itemView.getTranslationX();
+            } else {
+                mX = mStartDx + mFraction * (mTargetX - mStartDx);
+            }
+            if (mStartDy == mTargetY) {
+                mY = mViewHolder.itemView.getTranslationY();
+            } else {
+                mY = mStartDy + mFraction * (mTargetY - mStartDy);
+            }
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (!mEnded) {
+                mViewHolder.setIsRecyclable(true);
+            }
+            mEnded = true;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            setFraction(1f); //make sure we recover the view's state.
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+
+        }
+    }
+}
diff --git a/com/android/internal/widget/helper/ItemTouchUIUtil.java b/com/android/internal/widget/helper/ItemTouchUIUtil.java
new file mode 100644
index 0000000..e368a6d
--- /dev/null
+++ b/com/android/internal/widget/helper/ItemTouchUIUtil.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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.internal.widget.helper;
+
+import android.graphics.Canvas;
+import android.view.View;
+
+import com.android.internal.widget.RecyclerView;
+
+/**
+ * Utility class for {@link ItemTouchHelper} which handles item transformations for different
+ * API versions.
+ * <p/>
+ * This class has methods that map to {@link ItemTouchHelper.Callback}'s drawing methods. Default
+ * implementations in {@link ItemTouchHelper.Callback} call these methods with
+ * {@link RecyclerView.ViewHolder#itemView} and {@link ItemTouchUIUtil} makes necessary changes
+ * on the View depending on the API level. You can access the instance of {@link ItemTouchUIUtil}
+ * via {@link ItemTouchHelper.Callback#getDefaultUIUtil()} and call its methods with the children
+ * of ViewHolder that you want to apply default effects.
+ *
+ * @see ItemTouchHelper.Callback#getDefaultUIUtil()
+ */
+public interface ItemTouchUIUtil {
+
+    /**
+     * The default implementation for {@link ItemTouchHelper.Callback#onChildDraw(Canvas,
+     * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
+     */
+    void onDraw(Canvas c, RecyclerView recyclerView, View view,
+            float dX, float dY, int actionState, boolean isCurrentlyActive);
+
+    /**
+     * The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas,
+     * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
+     */
+    void onDrawOver(Canvas c, RecyclerView recyclerView, View view,
+            float dX, float dY, int actionState, boolean isCurrentlyActive);
+
+    /**
+     * The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView,
+     * RecyclerView.ViewHolder)}
+     */
+    void clearView(View view);
+
+    /**
+     * The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged(
+     * RecyclerView.ViewHolder, int)}
+     */
+    void onSelected(View view);
+}
+
diff --git a/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java b/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java
new file mode 100644
index 0000000..0de240b
--- /dev/null
+++ b/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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.internal.widget.helper;
+
+import android.graphics.Canvas;
+import android.view.View;
+
+import com.android.internal.R;
+import com.android.internal.widget.RecyclerView;
+
+/**
+ * Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
+ * public API, which is not desired in this case.
+ */
+class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
+    @Override
+    public void onDraw(Canvas c, RecyclerView recyclerView, View view,
+            float dX, float dY, int actionState, boolean isCurrentlyActive) {
+        if (isCurrentlyActive) {
+            Object originalElevation = view.getTag(
+                    R.id.item_touch_helper_previous_elevation);
+            if (originalElevation == null) {
+                originalElevation = view.getElevation();
+                float newElevation = 1f + findMaxElevation(recyclerView, view);
+                view.setElevation(newElevation);
+                view.setTag(R.id.item_touch_helper_previous_elevation,
+                        originalElevation);
+            }
+        }
+        view.setTranslationX(dX);
+        view.setTranslationY(dY);
+    }
+
+    private float findMaxElevation(RecyclerView recyclerView, View itemView) {
+        final int childCount = recyclerView.getChildCount();
+        float max = 0;
+        for (int i = 0; i < childCount; i++) {
+            final View child = recyclerView.getChildAt(i);
+            if (child == itemView) {
+                continue;
+            }
+            final float elevation = child.getElevation();
+            if (elevation > max) {
+                max = elevation;
+            }
+        }
+        return max;
+    }
+
+    @Override
+    public void clearView(View view) {
+        final Object tag = view.getTag(
+                R.id.item_touch_helper_previous_elevation);
+        if (tag != null && tag instanceof Float) {
+            view.setElevation((Float) tag);
+        }
+        view.setTag(R.id.item_touch_helper_previous_elevation, null);
+        view.setTranslationX(0f);
+        view.setTranslationY(0f);
+    }
+
+    @Override
+    public void onSelected(View view) {
+    }
+
+    @Override
+    public void onDrawOver(Canvas c, RecyclerView recyclerView,
+            View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {
+    }
+}